//-----------------------------------------------------------------------------
// Copyright (c) 2012 GarageGames, LLC
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//-----------------------------------------------------------------------------
//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~~//
// Arcane-FX for MIT Licensed Open Source version of Torque 3D from GarageGames
// Copyright (C) 2015 Faust Logic, Inc.
//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~~//
#include "platform/platform.h"
#include "T3D/fx/particleEmitter.h"
#include "scene/sceneManager.h"
#include "scene/sceneRenderState.h"
#include "console/consoleTypes.h"
#include "console/typeValidators.h"
#include "core/stream/bitStream.h"
#include "core/strings/stringUnit.h"
#include "math/mRandom.h"
#include "gfx/gfxDevice.h"
#include "gfx/primBuilder.h"
#include "gfx/gfxStringEnumTranslate.h"
#include "renderInstance/renderPassManager.h"
#include "T3D/gameBase/gameProcess.h"
#include "lighting/lightInfo.h"
#include "console/engineAPI.h"
#if defined(AFX_CAP_PARTICLE_POOLS)
#include "afx/util/afxParticlePool.h"
#endif
Point3F ParticleEmitter::mWindVelocity( 0.0, 0.0, 0.0 );
const F32 ParticleEmitter::AgedSpinToRadians = (1.0f/1000.0f) * (1.0f/360.0f) * M_PI_F * 2.0f;
IMPLEMENT_CO_DATABLOCK_V1(ParticleEmitterData);
IMPLEMENT_CONOBJECT(ParticleEmitter);
ConsoleDocClass( ParticleEmitter,
"@brief This object is responsible for spawning particles.\n\n"
"@note This class is not normally instantiated directly - to place a simple "
"particle emitting object in the scene, use a ParticleEmitterNode instead.\n\n"
"This class is the main interface for creating particles - though it is "
"usually only accessed from within another object like ParticleEmitterNode "
"or WheeledVehicle. If using this object class (via C++) directly, be aware "
"that it does not track changes in source axis or velocity over the "
"course of a single update, so emitParticles should be called at a fairly "
"fine grain. The emitter will potentially track the last particle to be "
"created into the next call to this function in order to create a uniformly "
"random time distribution of the particles.\n\n"
"If the object to which the emitter is attached is in motion, it should try "
"to ensure that for call (n+1) to this function, start is equal to the end "
"from call (n). This will ensure a uniform spatial distribution.\n\n"
"@ingroup FX\n"
"@see ParticleEmitterData\n"
"@see ParticleEmitterNode\n"
);
ConsoleDocClass( ParticleEmitterData,
"@brief Defines particle emission properties such as ejection angle, period "
"and velocity for a ParticleEmitter.\n\n"
"@tsexample\n"
"datablock ParticleEmitterData( GrenadeExpDustEmitter )\n"
"{\n"
" ejectionPeriodMS = 1;\n"
" periodVarianceMS = 0;\n"
" ejectionVelocity = 15;\n"
" velocityVariance = 0.0;\n"
" ejectionOffset = 0.0;\n"
" thetaMin = 85;\n"
" thetaMax = 85;\n"
" thetaVariance = 0;\n"
" phiReferenceVel = 0;\n"
" phiVariance = 360;\n"
" overrideAdvance = false;\n"
" lifetimeMS = 200;\n"
" particles = \"GrenadeExpDust\";\n"
"};\n"
"@endtsexample\n\n"
"@ingroup FX\n"
"@see ParticleEmitter\n"
"@see ParticleData\n"
"@see ParticleEmitterNode\n"
);
static const F32 sgDefaultEjectionOffset = 0.f;
static const F32 sgDefaultPhiReferenceVel = 0.f;
static const F32 sgDefaultPhiVariance = 360.f;
//-----------------------------------------------------------------------------
// ParticleEmitterData
//-----------------------------------------------------------------------------
ParticleEmitterData::ParticleEmitterData()
{
VECTOR_SET_ASSOCIATION(particleDataBlocks);
VECTOR_SET_ASSOCIATION(dataBlockIds);
ejectionPeriodMS = 100; // 10 Particles Per second
periodVarianceMS = 0; // exactly
ejectionVelocity = 2.0f; // From 1.0 - 3.0 meters per sec
velocityVariance = 1.0f;
ejectionOffset = sgDefaultEjectionOffset; // ejection from the emitter point
ejectionOffsetVariance = 0.0f;
thetaMin = 0.0f; // All heights
thetaMax = 90.0f;
thetaVariance = 0.0f;
phiReferenceVel = sgDefaultPhiReferenceVel; // All directions
phiVariance = sgDefaultPhiVariance;
softnessDistance = 1.0f;
ambientFactor = 0.0f;
lifetimeMS = 0;
lifetimeVarianceMS = 0;
overrideAdvance = true;
orientParticles = false;
orientOnVelocity = true;
ribbonParticles = false;
useEmitterSizes = false;
useEmitterColors = false;
particleString = NULL;
partListInitSize = 0;
// These members added for support of user defined blend factors
// and optional particle sorting.
blendStyle = ParticleRenderInst::BlendUndefined;
sortParticles = false;
renderReflection = true;
glow = false;
reverseOrder = false;
textureName = 0;
textureHandle = 0;
highResOnly = true;
alignParticles = false;
alignDirection = Point3F(0.0f, 1.0f, 0.0f);
ejectionInvert = false;
fade_color = false;
fade_alpha = false;
fade_size = false;
parts_per_eject = 1;
use_emitter_xfm = false;
#if defined(AFX_CAP_PARTICLE_POOLS)
pool_datablock = 0;
pool_index = 0;
pool_depth_fade = false;
pool_radial_fade = false;
do_pool_id_convert = false;
#endif
}
// Enum tables used for fields blendStyle, srcBlendFactor, dstBlendFactor.
// Note that the enums for srcBlendFactor and dstBlendFactor are consistent
// with the blending enums used in Torque Game Builder.
typedef ParticleRenderInst::BlendStyle ParticleBlendStyle;
DefineEnumType( ParticleBlendStyle );
ImplementEnumType( ParticleBlendStyle,
"The type of visual blending style to apply to the particles.\n"
"@ingroup FX\n\n")
{ ParticleRenderInst::BlendNormal, "NORMAL", "No blending style.\n" },
{ ParticleRenderInst::BlendAdditive, "ADDITIVE", "Adds the color of the pixel to the frame buffer with full alpha for each pixel.\n" },
{ ParticleRenderInst::BlendSubtractive, "SUBTRACTIVE", "Subtractive Blending. Reverses the color model, causing dark colors to have a stronger visual effect.\n" },
{ ParticleRenderInst::BlendPremultAlpha, "PREMULTALPHA", "Color blends with the colors of the imagemap rather than the alpha.\n" },
EndImplementEnumType;
IRangeValidator ejectPeriodIValidator(1, 2047);
IRangeValidator periodVarianceIValidator(0, 2047);
FRangeValidator ejectionFValidator(0.f, 655.35f);
FRangeValidator velVarianceFValidator(0.f, 163.83f);
FRangeValidator thetaFValidator(0.f, 180.f);
FRangeValidator phiFValidator(0.f, 360.f);
//-----------------------------------------------------------------------------
// initPersistFields
//-----------------------------------------------------------------------------
void ParticleEmitterData::initPersistFields()
{
docsURL;
addGroup( "ParticleEmitterData" );
addFieldV("ejectionPeriodMS", TYPEID< S32 >(), Offset(ejectionPeriodMS, ParticleEmitterData), &ejectPeriodIValidator,
"Time (in milliseconds) between each particle ejection." );
addFieldV("periodVarianceMS", TYPEID< S32 >(), Offset(periodVarianceMS, ParticleEmitterData), &periodVarianceIValidator,
"Variance in ejection period, from 1 - ejectionPeriodMS." );
addFieldV( "ejectionVelocity", TYPEID< F32 >(), Offset(ejectionVelocity, ParticleEmitterData), &ejectionFValidator,
"Particle ejection velocity." );
addFieldV( "velocityVariance", TYPEID< F32 >(), Offset(velocityVariance, ParticleEmitterData), &velVarianceFValidator,
"Variance for ejection velocity, from 0 - ejectionVelocity." );
addFieldV( "ejectionOffset", TYPEID< F32 >(), Offset(ejectionOffset, ParticleEmitterData), &ejectionFValidator,
"Distance along ejection Z axis from which to eject particles." );
addFieldV( "ejectionOffsetVariance", TYPEID< F32 >(), Offset(ejectionOffsetVariance, ParticleEmitterData), &ejectionFValidator,
"Distance Padding along ejection Z axis from which to eject particles." );
addFieldV( "thetaMin", TYPEID< F32 >(), Offset(thetaMin, ParticleEmitterData), &thetaFValidator,
"Minimum angle, from the horizontal plane, to eject from." );
addFieldV( "thetaMax", TYPEID< F32 >(), Offset(thetaMax, ParticleEmitterData), &thetaFValidator,
"Maximum angle, from the horizontal plane, to eject particles from." );
addFieldV( "thetaVariance", TYPEID< F32 >(), Offset(thetaVariance, ParticleEmitterData), &thetaFValidator,
"Angle variance from the previous particle, from 0 - 180." );
addFieldV( "phiReferenceVel", TYPEID< F32 >(), Offset(phiReferenceVel, ParticleEmitterData), &phiFValidator,
"Reference angle, from the vertical plane, to eject particles from." );
addFieldV( "phiVariance", TYPEID< F32 >(), Offset(phiVariance, ParticleEmitterData), &phiFValidator,
"Variance from the reference angle, from 0 - 360." );
addField( "softnessDistance", TYPEID< F32 >(), Offset(softnessDistance, ParticleEmitterData),
"For soft particles, the distance (in meters) where particles will be "
"faded based on the difference in depth between the particle and the "
"scene geometry." );
addField( "ambientFactor", TYPEID< F32 >(), Offset(ambientFactor, ParticleEmitterData),
"Used to generate the final particle color by controlling interpolation "
"between the particle color and the particle color multiplied by the "
"ambient light color." );
addField( "overrideAdvance", TYPEID< bool >(), Offset(overrideAdvance, ParticleEmitterData),
"If false, particles emitted in the same frame have their positions "
"adjusted. If true, adjustment is skipped and particles will clump "
"together." );
addField( "orientParticles", TYPEID< bool >(), Offset(orientParticles, ParticleEmitterData),
"If true, Particles will always face the camera." );
addField( "orientOnVelocity", TYPEID< bool >(), Offset(orientOnVelocity, ParticleEmitterData),
"If true, particles will be oriented to face in the direction they are moving." );
addField( "ribbonParticles", TYPEID< bool >(), Offset(ribbonParticles, ParticleEmitterData),
"If true, particles are rendered as a continous ribbon." );
addField( "particles", TYPEID< StringTableEntry >(), Offset(particleString, ParticleEmitterData),
"@brief List of space or TAB delimited ParticleData datablock names.\n\n"
"A random one of these datablocks is selected each time a particle is "
"emitted." );
addField( "lifetimeMS", TYPEID< S32 >(), Offset(lifetimeMS, ParticleEmitterData),
"Lifetime of emitted particles (in milliseconds)." );
addField("lifetimeVarianceMS", TYPEID< S32 >(), Offset(lifetimeVarianceMS, ParticleEmitterData),
"Variance in particle lifetime from 0 - lifetimeMS." );
addField( "useEmitterSizes", TYPEID< bool >(), Offset(useEmitterSizes, ParticleEmitterData),
"@brief If true, use emitter specified sizes instead of datablock sizes.\n"
"Useful for Debris particle emitters that control the particle size." );
addField( "useEmitterColors", TYPEID< bool >(), Offset(useEmitterColors, ParticleEmitterData),
"@brief If true, use emitter specified colors instead of datablock colors.\n\n"
"Useful for ShapeBase dust and WheeledVehicle wheel particle emitters that use "
"the current material to control particle color." );
/// These fields added for support of user defined blend factors and optional particle sorting.
//@{
addField( "blendStyle", TYPEID< ParticleRenderInst::BlendStyle >(), Offset(blendStyle, ParticleEmitterData),
"String value that controls how emitted particles blend with the scene." );
addField( "sortParticles", TYPEID< bool >(), Offset(sortParticles, ParticleEmitterData),
"If true, particles are sorted furthest to nearest.");
addField( "reverseOrder", TYPEID< bool >(), Offset(reverseOrder, ParticleEmitterData),
"@brief If true, reverses the normal draw order of particles.\n\n"
"Particles are normally drawn from newest to oldest, or in Z order "
"(furthest first) if sortParticles is true. Setting this field to "
"true will reverse that order: oldest first, or nearest first if "
"sortParticles is true." );
addField( "textureName", TYPEID< StringTableEntry >(), Offset(textureName, ParticleEmitterData),
"Optional texture to override ParticleData::textureName." );
addField( "alignParticles", TYPEID< bool >(), Offset(alignParticles, ParticleEmitterData),
"If true, particles always face along the axis defined by alignDirection." );
addProtectedField( "alignDirection", TYPEID< Point3F>(), Offset(alignDirection, ParticleEmitterData), &ParticleEmitterData::_setAlignDirection, &defaultProtectedGetFn,
"The direction aligned particles should face, only valid if alignParticles is true." );
addField( "highResOnly", TYPEID< bool >(), Offset(highResOnly, ParticleEmitterData),
"This particle system should not use the mixed-resolution renderer. "
"If your particle system has large amounts of overdraw, consider "
"disabling this option." );
addField( "renderReflection", TYPEID< bool >(), Offset(renderReflection, ParticleEmitterData),
"Controls whether particles are rendered onto reflective surfaces like water." );
addField("glow", TYPEID< bool >(), Offset(glow, ParticleEmitterData),
"If true, the particles are rendered to the glow buffer as well.");
//@}
endGroup( "ParticleEmitterData" );
addGroup("AFX");
addField("ejectionInvert", TypeBool, Offset(ejectionInvert, ParticleEmitterData));
addField("fadeColor", TypeBool, Offset(fade_color, ParticleEmitterData));
addField("fadeAlpha", TypeBool, Offset(fade_alpha, ParticleEmitterData));
addField("fadeSize", TypeBool, Offset(fade_size, ParticleEmitterData));
// useEmitterTransform currently does not work in TGEA or T3D
addField("useEmitterTransform", TypeBool, Offset(use_emitter_xfm, ParticleEmitterData));
endGroup("AFX");
#if defined(AFX_CAP_PARTICLE_POOLS)
addGroup("AFX Pooled Particles");
addField("poolData", TYPEID(), Offset(pool_datablock, ParticleEmitterData));
addField("poolIndex", TypeS32, Offset(pool_index, ParticleEmitterData));
addField("poolDepthFade", TypeBool, Offset(pool_depth_fade, ParticleEmitterData));
addField("poolRadialFade", TypeBool, Offset(pool_radial_fade, ParticleEmitterData));
endGroup("AFX Pooled Particles");
#endif
// disallow some field substitutions
disableFieldSubstitutions("particles");
onlyKeepClearSubstitutions("poolData"); // subs resolving to "~~", or "~0" are OK
Parent::initPersistFields();
}
bool ParticleEmitterData::_setAlignDirection( void *object, const char *index, const char *data )
{
ParticleEmitterData *p = static_cast( object );
Con::setData( TypePoint3F, &p->alignDirection, 0, 1, &data );
p->alignDirection.normalizeSafe();
// we already set the field
return false;
}
//-----------------------------------------------------------------------------
// packData
//-----------------------------------------------------------------------------
void ParticleEmitterData::packData(BitStream* stream)
{
Parent::packData(stream);
stream->writeInt(ejectionPeriodMS, 11); // must match limit on valid range in ParticleEmitterData::initPersistFields
stream->writeInt(periodVarianceMS, 11);
stream->writeInt((S32)(ejectionVelocity * 100), 16);
stream->writeInt((S32)(velocityVariance * 100), 14);
if( stream->writeFlag( ejectionOffset != sgDefaultEjectionOffset ) )
stream->writeInt((S32)(ejectionOffset * 100), 16);
if( stream->writeFlag( ejectionOffsetVariance != 0.0f ) )
stream->writeInt((S32)(ejectionOffsetVariance * 100), 16);
stream->writeRangedU32((U32)thetaMin, 0, 180);
stream->writeRangedU32((U32)thetaMax, 0, 180);
stream->writeRangedU32((U32)thetaVariance, 0, 180);
if( stream->writeFlag( phiReferenceVel != sgDefaultPhiReferenceVel ) )
stream->writeRangedU32((U32)phiReferenceVel, 0, 360);
if( stream->writeFlag( phiVariance != sgDefaultPhiVariance ) )
stream->writeRangedU32((U32)phiVariance, 0, 360);
stream->write( softnessDistance );
stream->write( ambientFactor );
stream->writeFlag(overrideAdvance);
stream->writeFlag(orientParticles);
stream->writeFlag(orientOnVelocity);
stream->writeFlag(ribbonParticles);
stream->write( lifetimeMS );
stream->write( lifetimeVarianceMS );
stream->writeFlag(useEmitterSizes);
stream->writeFlag(useEmitterColors);
stream->write(dataBlockIds.size());
for (U32 i = 0; i < dataBlockIds.size(); i++)
stream->write(dataBlockIds[i]);
stream->writeFlag(sortParticles);
stream->writeFlag(reverseOrder);
if (stream->writeFlag(textureName != 0))
stream->writeString(textureName);
if (stream->writeFlag(alignParticles))
{
stream->write(alignDirection.x);
stream->write(alignDirection.y);
stream->write(alignDirection.z);
}
stream->writeFlag(highResOnly);
stream->writeFlag(renderReflection);
stream->writeFlag(glow);
stream->writeInt( blendStyle, 4 );
stream->writeFlag(ejectionInvert);
stream->writeFlag(fade_color);
stream->writeFlag(fade_alpha);
stream->writeFlag(fade_size);
stream->writeFlag(use_emitter_xfm);
#if defined(AFX_CAP_PARTICLE_POOLS)
if (stream->writeFlag(pool_datablock))
{
stream->writeRangedU32(mPacked ? SimObjectId((uintptr_t)pool_datablock) : pool_datablock->getId(), DataBlockObjectIdFirst, DataBlockObjectIdLast);
stream->write(pool_index);
stream->writeFlag(pool_depth_fade);
stream->writeFlag(pool_radial_fade);
}
#endif
}
//-----------------------------------------------------------------------------
// unpackData
//-----------------------------------------------------------------------------
void ParticleEmitterData::unpackData(BitStream* stream)
{
Parent::unpackData(stream);
ejectionPeriodMS = stream->readInt(11);
periodVarianceMS = stream->readInt(11);
ejectionVelocity = stream->readInt(16) / 100.0f;
velocityVariance = stream->readInt(14) / 100.0f;
if( stream->readFlag() )
ejectionOffset = stream->readInt(16) / 100.0f;
else
ejectionOffset = sgDefaultEjectionOffset;
if( stream->readFlag() )
ejectionOffsetVariance = stream->readInt(16) / 100.0f;
else
ejectionOffsetVariance = 0.0f;
thetaMin = (F32)stream->readRangedU32(0, 180);
thetaMax = (F32)stream->readRangedU32(0, 180);
thetaVariance = (F32)stream->readRangedU32(0, 180);
if( stream->readFlag() )
phiReferenceVel = (F32)stream->readRangedU32(0, 360);
else
phiReferenceVel = sgDefaultPhiReferenceVel;
if( stream->readFlag() )
phiVariance = (F32)stream->readRangedU32(0, 360);
else
phiVariance = sgDefaultPhiVariance;
stream->read( &softnessDistance );
stream->read( &ambientFactor );
overrideAdvance = stream->readFlag();
orientParticles = stream->readFlag();
orientOnVelocity = stream->readFlag();
ribbonParticles = stream->readFlag();
stream->read( &lifetimeMS );
stream->read( &lifetimeVarianceMS );
useEmitterSizes = stream->readFlag();
useEmitterColors = stream->readFlag();
U32 size; stream->read(&size);
dataBlockIds.setSize(size);
for (U32 i = 0; i < dataBlockIds.size(); i++)
stream->read(&dataBlockIds[i]);
sortParticles = stream->readFlag();
reverseOrder = stream->readFlag();
textureName = (stream->readFlag()) ? stream->readSTString() : 0;
alignParticles = stream->readFlag();
if (alignParticles)
{
stream->read(&alignDirection.x);
stream->read(&alignDirection.y);
stream->read(&alignDirection.z);
}
highResOnly = stream->readFlag();
renderReflection = stream->readFlag();
glow = stream->readFlag();
blendStyle = stream->readInt( 4 );
ejectionInvert = stream->readFlag();
fade_color = stream->readFlag();
fade_alpha = stream->readFlag();
fade_size = stream->readFlag();
use_emitter_xfm = stream->readFlag();
#if defined(AFX_CAP_PARTICLE_POOLS)
if (stream->readFlag())
{
pool_datablock = (afxParticlePoolData*)(uintptr_t)stream->readRangedU32(DataBlockObjectIdFirst, DataBlockObjectIdLast);
stream->read(&pool_index);
pool_depth_fade = stream->readFlag();
pool_radial_fade = stream->readFlag();
do_pool_id_convert = true;
}
#endif
}
//-----------------------------------------------------------------------------
// onAdd
//-----------------------------------------------------------------------------
bool ParticleEmitterData::onAdd()
{
if( Parent::onAdd() == false )
return false;
// if (overrideAdvance == true) {
// Con::errorf(ConsoleLogEntry::General, "ParticleEmitterData: Not going to work. Fix it!");
// return false;
// }
// Validate the parameters...
//
if( ejectionPeriodMS < 1 )
{
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) period < 1 ms", getName());
ejectionPeriodMS = 1;
}
if( periodVarianceMS >= ejectionPeriodMS )
{
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) periodVariance >= period", getName());
periodVarianceMS = ejectionPeriodMS - 1;
}
if( ejectionVelocity < 0.0f )
{
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) ejectionVelocity < 0.0f", getName());
ejectionVelocity = 0.0f;
}
if( velocityVariance < 0.0f )
{
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) velocityVariance < 0.0f", getName());
velocityVariance = 0.0f;
}
if( velocityVariance > ejectionVelocity )
{
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) velocityVariance > ejectionVelocity", getName());
velocityVariance = ejectionVelocity;
}
if( ejectionOffset < 0.0f )
{
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) ejectionOffset < 0", getName());
ejectionOffset = 0.0f;
}
if( ejectionOffsetVariance < 0.0f )
{
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) ejectionOffset < 0", getName());
ejectionOffsetVariance = 0.0f;
}
if( thetaMin < 0.0f )
{
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) thetaMin < 0.0", getName());
thetaMin = 0.0f;
}
if( thetaMax > 180.0f )
{
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) thetaMax > 180.0", getName());
thetaMax = 180.0f;
}
if( thetaMin > thetaMax )
{
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) thetaMin > thetaMax", getName());
thetaMin = thetaMax;
}
if( thetaVariance > 180.0f )
{
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) thetaVariance > 180.0", getName());
thetaVariance = 180.0f;
}
if( thetaVariance < 0.0f )
{
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) thetaVariance < 0.0", getName());
thetaVariance = 0.0f;
}
if( phiVariance < 0.0f || phiVariance > 360.0f )
{
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) invalid phiVariance", getName());
phiVariance = phiVariance < 0.0f ? 0.0f : 360.0f;
}
if( thetaVariance < 0.0f || thetaVariance > 180.0f )
{
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) invalid thetaVariance", getName());
thetaVariance = thetaVariance < 0.0f ? 0.0f : 180.0f;
}
if ( softnessDistance < 0.0f )
{
Con::warnf( ConsoleLogEntry::General, "ParticleEmitterData(%s) invalid softnessDistance", getName() );
softnessDistance = 0.0f;
}
if (particleString == NULL && dataBlockIds.size() == 0)
{
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) no particleString, invalid datablock", getName());
return false;
}
if (particleString && particleString[0] == '\0')
{
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) no particleString, invalid datablock", getName());
return false;
}
if (particleString && dStrlen(particleString) > 255)
{
Con::errorf(ConsoleLogEntry::General, "ParticleEmitterData(%s) particle string too long [> 255 chars]", getName());
return false;
}
if( lifetimeMS < 0 )
{
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) lifetimeMS < 0.0f", getName());
lifetimeMS = 0;
}
if( lifetimeVarianceMS > lifetimeMS )
{
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) lifetimeVarianceMS >= lifetimeMS", getName());
lifetimeVarianceMS = lifetimeMS;
}
// load the particle datablocks...
//
if( particleString != NULL )
{
// particleString is once again a list of particle datablocks so it
// must be parsed to extract the particle references.
// First we parse particleString into a list of particle name tokens
Vector dataBlocks(__FILE__, __LINE__);
dsize_t tokLen = dStrlen(particleString) + 1;
char* tokCopy = new char[tokLen];
dStrcpy(tokCopy, particleString, tokLen);
char* currTok = dStrtok(tokCopy, " \t");
while (currTok != NULL)
{
dataBlocks.push_back(currTok);
currTok = dStrtok(NULL, " \t");
}
if (dataBlocks.size() == 0)
{
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) invalid particles string. No datablocks found", getName());
delete [] tokCopy;
return false;
}
// Now we convert the particle name tokens into particle datablocks and IDs
particleDataBlocks.clear();
dataBlockIds.clear();
for (U32 i = 0; i < dataBlocks.size(); i++)
{
ParticleData* pData = NULL;
if (Sim::findObject(dataBlocks[i], pData) == false)
{
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) unable to find particle datablock: %s", getName(), dataBlocks[i]);
}
else
{
particleDataBlocks.push_back(pData);
dataBlockIds.push_back(pData->getId());
}
}
// cleanup
delete [] tokCopy;
// check that we actually found some particle datablocks
if (particleDataBlocks.size() == 0)
{
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) unable to find any particle datablocks", getName());
return false;
}
}
return true;
}
//-----------------------------------------------------------------------------
// preload
//-----------------------------------------------------------------------------
bool ParticleEmitterData::preload(bool server, String &errorStr)
{
if( Parent::preload(server, errorStr) == false )
return false;
particleDataBlocks.clear();
for (U32 i = 0; i < dataBlockIds.size(); i++)
{
ParticleData* pData = NULL;
if (Sim::findObject(dataBlockIds[i], pData) == false)
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) unable to find particle datablock: %d", getName(), dataBlockIds[i]);
else
particleDataBlocks.push_back(pData);
}
if (!server)
{
#if defined(AFX_CAP_PARTICLE_POOLS)
if (do_pool_id_convert)
{
SimObjectId db_id = (SimObjectId)(uintptr_t)pool_datablock;
if (db_id != 0)
{
// try to convert id to pointer
if (!Sim::findObject(db_id, pool_datablock))
{
Con::errorf("ParticleEmitterData::reload() -- bad datablockId: 0x%x (poolData)", db_id);
}
}
do_pool_id_convert = false;
}
#endif
// load emitter texture if specified
if (textureName && textureName[0])
{
textureHandle = GFXTexHandle(textureName, &GFXStaticTextureSRGBProfile, avar("%s() - textureHandle (line %d)", __FUNCTION__, __LINE__));
if (!textureHandle)
{
errorStr = String::ToString("Missing particle emitter texture: %s", textureName);
return false;
}
}
// otherwise, check that all particles refer to the same texture
else if (particleDataBlocks.size() > 1)
{
StringTableEntry txr_name = particleDataBlocks[0]->getTexture();
for (S32 i = 1; i < particleDataBlocks.size(); i++)
{
// warn if particle textures are inconsistent
if (particleDataBlocks[i]->getTexture() != txr_name)
{
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) particles reference different textures.", getName());
break;
}
}
}
}
// if blend-style is undefined check legacy useInvAlpha settings
if (blendStyle == ParticleRenderInst::BlendUndefined && particleDataBlocks.size() > 0)
{
bool useInvAlpha = particleDataBlocks[0]->useInvAlpha;
for (S32 i = 1; i < particleDataBlocks.size(); i++)
{
// warn if blend-style legacy useInvAlpha settings are inconsistent
if (particleDataBlocks[i]->useInvAlpha != useInvAlpha)
{
Con::warnf(ConsoleLogEntry::General, "ParticleEmitterData(%s) particles have inconsistent useInvAlpha settings.", getName());
break;
}
}
blendStyle = (useInvAlpha) ? ParticleRenderInst::BlendNormal : ParticleRenderInst::BlendAdditive;
}
if( !server )
{
allocPrimBuffer();
}
return true;
}
//-----------------------------------------------------------------------------
// alloc PrimitiveBuffer
// The datablock allocates this static index buffer because it's the same
// for all of the emitters - each particle quad uses the same index ordering
//-----------------------------------------------------------------------------
void ParticleEmitterData::allocPrimBuffer( S32 overrideSize )
{
// calculate particle list size
AssertFatal(particleDataBlocks.size() > 0, "Error, no particles found." );
if (particleDataBlocks.empty()) return;
U32 maxPartLife = particleDataBlocks[0]->lifetimeMS + particleDataBlocks[0]->lifetimeVarianceMS;
for (S32 i = 1; i < particleDataBlocks.size(); i++)
{
U32 mpl = particleDataBlocks[i]->lifetimeMS + particleDataBlocks[i]->lifetimeVarianceMS;
if (mpl > maxPartLife)
maxPartLife = mpl;
}
partListInitSize = maxPartLife / (ejectionPeriodMS - periodVarianceMS);
partListInitSize += 8; // add 8 as "fudge factor" to make sure it doesn't realloc if it goes over by 1
if (parts_per_eject > 1)
partListInitSize *= parts_per_eject;
// if override size is specified, then the emitter overran its buffer and needs a larger allocation
if( overrideSize != -1 )
{
partListInitSize = overrideSize;
}
// create index buffer based on that size
U32 indexListSize = partListInitSize * 6; // 6 indices per particle
U16 *indices = new U16[ indexListSize ];
for( U32 i=0; iisTempClone())
{
delete particleDataBlocks[i];
particleDataBlocks[i] = 0;
}
}
#ifdef TRACK_PARTICLE_EMITTER_DATA_CLONES
if (emitter_data_clones > 0)
{
emitter_data_clones--;
if (emitter_data_clones == 0)
Con::errorf("ParticleEmitterData -- Clones eliminated!");
}
else
Con::errorf("ParticleEmitterData -- Too many clones deleted!");
#endif
}
ParticleEmitterData* ParticleEmitterData::cloneAndPerformSubstitutions(const SimObject* owner, S32 index)
{
if (!owner)
return this;
bool clone_parts_db = false;
// note -- this could be checked when the particle blocks are evaluated
for (S32 i = 0; i < this->particleDataBlocks.size(); i++)
{
if (this->particleDataBlocks[i] && (this->particleDataBlocks[i]->getSubstitutionCount() > 0))
{
clone_parts_db = true;
break;
}
}
ParticleEmitterData* sub_emitter_db = this;
if (this->getSubstitutionCount() > 0 || clone_parts_db)
{
sub_emitter_db = new ParticleEmitterData(*this, true);
performSubstitutions(sub_emitter_db, owner, index);
if (clone_parts_db)
{
for (S32 i = 0; i < sub_emitter_db->particleDataBlocks.size(); i++)
{
if (sub_emitter_db->particleDataBlocks[i] && (sub_emitter_db->particleDataBlocks[i]->getSubstitutionCount() > 0))
{
ParticleData* orig_db = sub_emitter_db->particleDataBlocks[i];
sub_emitter_db->particleDataBlocks[i] = new ParticleData(*orig_db, true);
orig_db->performSubstitutions(sub_emitter_db->particleDataBlocks[i], owner, index);
}
}
}
}
return sub_emitter_db;
}
//-----------------------------------------------------------------------------
// ParticleEmitter
//-----------------------------------------------------------------------------
ParticleEmitter::ParticleEmitter()
{
mDeleteWhenEmpty = false;
mDeleteOnTick = false;
mInternalClock = 0;
mNextParticleTime = 0;
mLastPosition.set(0, 0, 0);
mHasLastPosition = false;
mLifetimeMS = 0;
mElapsedTimeMS = 0;
part_store = 0;
part_freelist = NULL;
part_list_head.next = NULL;
n_part_capacity = 0;
n_parts = 0;
mThetaOld = 0;
mPhiOld = 0;
mCurBuffSize = 0;
mDead = false;
mDataBlock = NULL;
// ParticleEmitter should be allocated on the client only.
mNetFlags.set( IsGhost );
fade_amt = 1.0f;
forced_bbox = false;
db_temp_clone = false;
pos_pe.set(0,0,0);
sort_priority = 0;
mDataBlock = 0;
std::fill_n(sizes, ParticleData::PDC_NUM_KEYS, 0.0f);
#if defined(AFX_CAP_PARTICLE_POOLS)
pool = 0;
#endif
}
//-----------------------------------------------------------------------------
// destructor
//-----------------------------------------------------------------------------
ParticleEmitter::~ParticleEmitter()
{
for( S32 i = 0; i < part_store.size(); i++ )
{
delete [] part_store[i];
}
if (db_temp_clone && mDataBlock && mDataBlock->isTempClone())
{
for (S32 i = 0; i < mDataBlock->particleDataBlocks.size(); i++)
{
if (mDataBlock->particleDataBlocks[i] && mDataBlock->particleDataBlocks[i]->isTempClone())
{
delete mDataBlock->particleDataBlocks[i];
mDataBlock->particleDataBlocks[i] = 0;
}
}
delete mDataBlock;
mDataBlock = 0;
}
}
//-----------------------------------------------------------------------------
// onAdd
//-----------------------------------------------------------------------------
bool ParticleEmitter::onAdd()
{
if( !Parent::onAdd() )
return false;
// add to client side mission cleanup
SimGroup *cleanup = dynamic_cast( Sim::findObject( "ClientMissionCleanup") );
if( cleanup != NULL )
{
cleanup->addObject( this );
}
removeFromProcessList();
F32 radius = 5.0;
mObjBox.minExtents = Point3F(-radius, -radius, -radius);
mObjBox.maxExtents = Point3F(radius, radius, radius);
resetWorldBox();
#if defined(AFX_CAP_PARTICLE_POOLS)
if (pool)
pool->addParticleEmitter(this);
#endif
return true;
}
//-----------------------------------------------------------------------------
// onRemove
//-----------------------------------------------------------------------------
void ParticleEmitter::onRemove()
{
#if defined(AFX_CAP_PARTICLE_POOLS)
if (pool)
{
pool->removeParticleEmitter(this);
pool = 0;
}
#endif
removeFromScene();
Parent::onRemove();
}
//-----------------------------------------------------------------------------
// onNewDataBlock
//-----------------------------------------------------------------------------
bool ParticleEmitter::onNewDataBlock( GameBaseData *dptr, bool reload )
{
mDataBlock = dynamic_cast( dptr );
if ( !mDataBlock || !Parent::onNewDataBlock( dptr, reload ) )
return false;
mLifetimeMS = mDataBlock->lifetimeMS;
if( mDataBlock->lifetimeVarianceMS )
{
mLifetimeMS += S32( gRandGen.randI() % (2 * mDataBlock->lifetimeVarianceMS + 1)) - S32(mDataBlock->lifetimeVarianceMS );
}
// Allocate particle structures and init the freelist. Member part_store
// is a Vector so that we can allocate more particles if partListInitSize
// turns out to be too small.
//
if (mDataBlock->partListInitSize > 0)
{
for( S32 i = 0; i < part_store.size(); i++ )
{
delete [] part_store[i];
}
part_store.clear();
n_part_capacity = mDataBlock->partListInitSize;
Particle* store_block = new Particle[n_part_capacity];
part_store.push_back(store_block);
part_freelist = store_block;
Particle* last_part = part_freelist;
Particle* part = last_part+1;
for( S32 i = 1; i < n_part_capacity; i++, part++, last_part++ )
{
last_part->next = part;
}
store_block[n_part_capacity-1].next = NULL;
part_list_head.next = NULL;
n_parts = 0;
}
if (mDataBlock->isTempClone())
{
db_temp_clone = true;
return true;
}
scriptOnNewDataBlock();
return true;
}
//-----------------------------------------------------------------------------
// getCollectiveColor
//-----------------------------------------------------------------------------
LinearColorF ParticleEmitter::getCollectiveColor()
{
U32 count = 0;
LinearColorF color = LinearColorF(0.0f, 0.0f, 0.0f);
count = n_parts;
for( Particle* part = part_list_head.next; part != NULL; part = part->next )
{
color += part->color;
}
if(count > 0)
{
color /= F32(count);
}
//if(color.red == 0.0f && color.green == 0.0f && color.blue == 0.0f)
// color = color;
return color;
}
//-----------------------------------------------------------------------------
// prepRenderImage
//-----------------------------------------------------------------------------
void ParticleEmitter::prepRenderImage(SceneRenderState* state)
{
#if defined(AFX_CAP_PARTICLE_POOLS)
if (pool)
return;
#endif
if( state->isReflectPass() && !getDataBlock()->renderReflection )
return;
// Never render into shadows.
if (state->isShadowPass())
return;
PROFILE_SCOPE(ParticleEmitter_prepRenderImage);
if ( mDead ||
n_parts == 0 ||
part_list_head.next == NULL )
return;
RenderPassManager *renderManager = state->getRenderPass();
const Point3F &camPos = state->getCameraPosition();
copyToVB( camPos, state->getAmbientLightColor() );
if (!mVertBuff.isValid())
return;
ParticleRenderInst *ri = renderManager->allocInst();
ri->vertBuff = &mVertBuff;
ri->primBuff = &getDataBlock()->primBuff;
ri->translucentSort = true;
ri->type = RenderPassManager::RIT_Particle;
ri->sortDistSq = getRenderWorldBox().getSqDistanceToPoint( camPos );
ri->defaultKey = (-sort_priority*100);
// Draw the system offscreen unless the highResOnly flag is set on the datablock
ri->systemState = ( getDataBlock()->highResOnly ? PSS_AwaitingHighResDraw : PSS_AwaitingOffscreenDraw );
ri->modelViewProj = renderManager->allocUniqueXform( GFX->getProjectionMatrix() *
GFX->getViewMatrix() *
GFX->getWorldMatrix() );
// Update position on the matrix before multiplying it
mBBObjToWorld.setPosition(mLastPosition);
ri->bbModelViewProj = renderManager->allocUniqueXform( *ri->modelViewProj * mBBObjToWorld );
ri->wsPosition = getWorldTransform().getPosition();
ri->count = n_parts;
ri->blendStyle = mDataBlock->blendStyle;
ri->glow = mDataBlock->glow;
// use first particle's texture unless there is an emitter texture to override it
if (mDataBlock->textureHandle)
ri->diffuseTex = &*(mDataBlock->textureHandle);
else
ri->diffuseTex = &*(part_list_head.next->dataBlock->getTextureResource());
ri->softnessDistance = mDataBlock->softnessDistance;
// Sort by texture too.
ri->defaultKey = ri->diffuseTex ? (uintptr_t)ri->diffuseTex : (uintptr_t)ri->vertBuff;
renderManager->addInst( ri );
}
//-----------------------------------------------------------------------------
// setSizes
//-----------------------------------------------------------------------------
void ParticleEmitter::setSizes( F32 *sizeList )
{
for( S32 i=0; i 0 && mElapsedTimeMS > mLifetimeMS )
{
return;
}
pos_pe = point;
Point3F realStart;
if( useLastPosition && mHasLastPosition )
realStart = mLastPosition;
else
realStart = point;
emitParticles(realStart, point,
axis,
velocity,
numMilliseconds);
}
//-----------------------------------------------------------------------------
// emitParticles
//-----------------------------------------------------------------------------
void ParticleEmitter::emitParticles(const Point3F& start,
const Point3F& end,
const Point3F& axis,
const Point3F& velocity,
const U32 numMilliseconds)
{
if( mDead ) return;
if( mDataBlock->particleDataBlocks.empty() )
return;
// lifetime over - no more particles
if( mLifetimeMS > 0 && mElapsedTimeMS > mLifetimeMS )
{
return;
}
U32 currTime = 0;
bool particlesAdded = false;
Point3F axisx;
if( mFabs(axis.z) < 0.9f )
mCross(axis, Point3F(0, 0, 1), &axisx);
else
mCross(axis, Point3F(0, 1, 0), &axisx);
axisx.normalize();
if( mNextParticleTime != 0 )
{
// Need to handle next particle
//
if( mNextParticleTime > numMilliseconds )
{
// Defer to next update
// (Note that this introduces a potential spatial irregularity if the owning
// object is accelerating, and updating at a low frequency)
//
mNextParticleTime -= numMilliseconds;
mInternalClock += numMilliseconds;
mLastPosition = end;
mHasLastPosition = true;
return;
}
else
{
currTime += mNextParticleTime;
mInternalClock += mNextParticleTime;
// Emit particle at curr time
// Create particle at the correct position
Point3F pos;
pos.interpolate(start, end, F32(currTime) / F32(numMilliseconds));
addParticle(pos, axis, velocity, axisx, numMilliseconds-currTime);
particlesAdded = true;
mNextParticleTime = 0;
}
}
while( currTime < numMilliseconds )
{
S32 nextTime = mDataBlock->ejectionPeriodMS;
if( mDataBlock->periodVarianceMS != 0 )
{
nextTime += S32(gRandGen.randI() % (2 * mDataBlock->periodVarianceMS + 1)) -
S32(mDataBlock->periodVarianceMS);
}
AssertFatal(nextTime > 0, "Error, next particle ejection time must always be greater than 0");
if( currTime + nextTime > numMilliseconds )
{
mNextParticleTime = (currTime + nextTime) - numMilliseconds;
mInternalClock += numMilliseconds - currTime;
AssertFatal(mNextParticleTime > 0, "Error, should not have deferred this particle!");
break;
}
currTime += nextTime;
mInternalClock += nextTime;
// Create particle at the correct position
Point3F pos;
pos.interpolate(start, end, F32(currTime) / F32(numMilliseconds));
addParticle(pos, axis, velocity, axisx, numMilliseconds-currTime);
particlesAdded = true;
// This override-advance code is restored in order to correctly adjust
// animated parameters of particles allocated within the same frame
// update. Note that ordering is important and this code correctly
// adds particles in the same newest-to-oldest ordering of the link-list.
//
// NOTE: We are assuming that the just added particle is at the head of our
// list. If that changes, so must this...
U32 advanceMS = numMilliseconds - currTime;
if (mDataBlock->overrideAdvance == false && advanceMS != 0)
{
Particle* last_part = part_list_head.next;
if (advanceMS > last_part->totalLifetime)
{
part_list_head.next = last_part->next;
n_parts--;
last_part->next = part_freelist;
part_freelist = last_part;
}
else
{
if (advanceMS != 0)
{
F32 t = F32(advanceMS) / 1000.0;
Point3F a = last_part->acc;
a -= last_part->vel * last_part->dataBlock->dragCoefficient;
a += mWindVelocity * last_part->dataBlock->windCoefficient;
//a += Point3F(0.0f, 0.0f, -9.81f) * last_part->dataBlock->gravityCoefficient;
a.z += -9.81f*last_part->dataBlock->gravityCoefficient; // as long as gravity is a constant, this is faster
last_part->vel += a * t;
//last_part->pos += last_part->vel * t;
last_part->pos_local += last_part->vel * t;
// AFX -- allow subclasses to adjust the particle params here
sub_particleUpdate(last_part);
if (last_part->dataBlock->constrain_pos)
last_part->pos = last_part->pos_local + this->pos_pe;
else
last_part->pos = last_part->pos_local;
updateKeyData( last_part );
}
}
}
}
// DMMFIX: Lame and slow...
if( particlesAdded == true )
updateBBox();
if( n_parts > 0 && getSceneManager() == NULL )
{
gClientSceneGraph->addObjectToScene(this);
ClientProcessList::get()->addObject(this);
}
mLastPosition = end;
mHasLastPosition = true;
}
//-----------------------------------------------------------------------------
// emitParticles
//-----------------------------------------------------------------------------
void ParticleEmitter::emitParticles(const Point3F& rCenter,
const Point3F& rNormal,
const F32 radius,
const Point3F& velocity,
S32 count)
{
if( mDead ) return;
// lifetime over - no more particles
if( mLifetimeMS > 0 && mElapsedTimeMS > mLifetimeMS )
{
return;
}
Point3F axisx, axisy;
Point3F axisz = rNormal;
if( axisz.isZero() )
{
axisz.set( 0.0, 0.0, 1.0 );
}
if( mFabs(axisz.z) < 0.98 )
{
mCross(axisz, Point3F(0, 0, 1), &axisy);
axisy.normalize();
}
else
{
mCross(axisz, Point3F(0, 1, 0), &axisy);
axisy.normalize();
}
mCross(axisz, axisy, &axisx);
axisx.normalize();
// Should think of a better way to distribute the
// particles within the hemisphere.
for( S32 i = 0; i < count; i++ )
{
Point3F pos = axisx * (radius * (1 - (2 * gRandGen.randF())));
pos += axisy * (radius * (1 - (2 * gRandGen.randF())));
pos += axisz * (radius * gRandGen.randF());
Point3F axis = pos;
axis.normalize();
pos += rCenter;
addParticle(pos, axis, velocity, axisz, 0);
}
// Set world bounding box
mObjBox.minExtents = rCenter - Point3F(radius, radius, radius);
mObjBox.maxExtents = rCenter + Point3F(radius, radius, radius);
resetWorldBox();
// Make sure we're part of the world
if( n_parts > 0 && getSceneManager() == NULL )
{
gClientSceneGraph->addObjectToScene(this);
ClientProcessList::get()->addObject(this);
}
mHasLastPosition = false;
}
//-----------------------------------------------------------------------------
// updateBBox - SLOW, bad news
//-----------------------------------------------------------------------------
void ParticleEmitter::updateBBox()
{
if (forced_bbox)
return;
Point3F minPt(1e10, 1e10, 1e10);
Point3F maxPt(-1e10, -1e10, -1e10);
for (Particle* part = part_list_head.next; part != NULL; part = part->next)
{
Point3F particleSize(part->size * 0.5f);
F32 motion = getMax((part->vel.len() * part->totalLifetime / 1000.0f), 1.0f);
minPt.setMin(part->pos - particleSize - Point3F(motion));
maxPt.setMax(part->pos + particleSize + Point3F(motion));
}
mObjBox = Box3F(minPt, maxPt);
MatrixF temp = getTransform();
setTransform(temp);
mBBObjToWorld.identity();
Point3F boxScale = mObjBox.getExtents();
boxScale.x = getMax(boxScale.x, 1.0f);
boxScale.y = getMax(boxScale.y, 1.0f);
boxScale.z = getMax(boxScale.z, 1.0f);
mBBObjToWorld.scale(boxScale);
#if defined(AFX_CAP_PARTICLE_POOLS)
if (pool)
pool->updatePoolBBox(this);
#endif
}
//-----------------------------------------------------------------------------
// addParticle
//-----------------------------------------------------------------------------
void ParticleEmitter::addParticle(const Point3F& pos, const Point3F& axis, const Point3F& vel,
const Point3F& axisx, const U32 age_offset)
{
n_parts++;
if (n_parts > n_part_capacity || n_parts > mDataBlock->partListInitSize)
{
// In an emergency we allocate additional particles in blocks of 16.
// This should happen rarely.
Particle* store_block = new Particle[16];
part_store.push_back(store_block);
n_part_capacity += 16;
for (S32 i = 0; i < 16; i++)
{
store_block[i].next = part_freelist;
part_freelist = &store_block[i];
}
mDataBlock->allocPrimBuffer(n_part_capacity); // allocate larger primitive buffer or will crash
}
Particle* pNew = part_freelist;
part_freelist = pNew->next;
pNew->next = part_list_head.next;
part_list_head.next = pNew;
// for earlier access to constrain_pos, the ParticleData datablock is chosen here instead
// of later in the method.
U32 dBlockIndex = gRandGen.randI() % mDataBlock->particleDataBlocks.size();
ParticleData* part_db = mDataBlock->particleDataBlocks[dBlockIndex];
// set start position to world or local space
Point3F pos_start;
if (part_db->constrain_pos)
pos_start.set(0,0,0);
else
pos_start = pos;
Point3F ejectionAxis = axis;
F32 theta = 0.0f;
F32 thetaTarget = (mDataBlock->thetaMax + mDataBlock->thetaMin) / 2.0f;
if (mDataBlock->thetaVariance <= 0.0f)
theta = (mDataBlock->thetaMax - mDataBlock->thetaMin) * gRandGen.randF() + mDataBlock->thetaMin;
else
{
F32 thetaDelta = ( gRandGen.randF() - 0.5f) * mDataBlock->thetaVariance * 2.0f;
thetaDelta += ( (thetaTarget - mThetaOld) / mDataBlock->thetaMax ) * mDataBlock->thetaVariance * 0.25f;
theta = mThetaOld + thetaDelta;
}
mThetaOld = theta;
F32 ref = (F32(mInternalClock) / 1000.0) * mDataBlock->phiReferenceVel;
F32 phi = 0.0f;
if (mDataBlock->thetaVariance <= 0.0f)
{
phi = ref + gRandGen.randF() * mDataBlock->phiVariance;
}
else
{
F32 phiDelta = (gRandGen.randF() - 0.5f) * mDataBlock->thetaVariance * 2.0f;
phi = ref + mPhiOld + phiDelta;
if (phi > mDataBlock->phiVariance)
phi += fabs(phiDelta) * -2.0f;
if (phi < 0.0f)
phi += fabs(phiDelta) * 2.0f;
}
mPhiOld = phi;
// Both phi and theta are in degs. Create axis angles out of them, and create the
// appropriate rotation matrix...
AngAxisF thetaRot(axisx, theta * (M_PI / 180.0));
AngAxisF phiRot(axis, phi * (M_PI / 180.0));
MatrixF temp(true);
thetaRot.setMatrix(&temp);
temp.mulP(ejectionAxis);
phiRot.setMatrix(&temp);
temp.mulP(ejectionAxis);
F32 initialVel = mDataBlock->ejectionVelocity;
initialVel += (mDataBlock->velocityVariance * 2.0f * gRandGen.randF()) - mDataBlock->velocityVariance;
pNew->pos = pos_start + (ejectionAxis * (mDataBlock->ejectionOffset + mDataBlock->ejectionOffsetVariance* gRandGen.randF()) );
pNew->pos_local = pNew->pos;
pNew->vel = mDataBlock->ejectionInvert ? ejectionAxis * -initialVel : ejectionAxis * initialVel;
if (mDataBlock->orientParticles)
pNew->orientDir = ejectionAxis;
else
// note -- for non-oriented particles, we use orientDir.x to store the billboard start angle.
pNew->orientDir.x = mDegToRad(part_db->start_angle + part_db->angle_variance*2.0f*gRandGen.randF() - part_db->angle_variance);
pNew->acc.set(0, 0, 0);
pNew->currentAge = age_offset;
pNew->t_last = 0.0f;
// ribbon particles only use the first particle
if(mDataBlock->ribbonParticles)
{
mDataBlock->particleDataBlocks[0]->initializeParticle(pNew, vel);
}
else
{
dBlockIndex = gRandGen.randI() % mDataBlock->particleDataBlocks.size();
mDataBlock->particleDataBlocks[dBlockIndex]->initializeParticle(pNew, vel);
}
updateKeyData( pNew );
}
//-----------------------------------------------------------------------------
// processTick
//-----------------------------------------------------------------------------
void ParticleEmitter::processTick(const Move*)
{
if( mDeleteOnTick == true )
{
mDead = true;
deleteObject();
}
}
//-----------------------------------------------------------------------------
// advanceTime
//-----------------------------------------------------------------------------
void ParticleEmitter::advanceTime(F32 dt)
{
if( dt < 0.00001 ) return;
Parent::advanceTime(dt);
if( dt > 0.5 ) dt = 0.5;
if( mDead ) return;
mElapsedTimeMS += (S32)(dt * 1000.0f);
U32 numMSToUpdate = (U32)(dt * 1000.0f);
if( numMSToUpdate == 0 ) return;
// TODO: Prefetch
// remove dead particles
Particle* last_part = &part_list_head;
for (Particle* part = part_list_head.next; part != NULL; part = part->next)
{
part->currentAge += numMSToUpdate;
if (part->currentAge > part->totalLifetime)
{
n_parts--;
last_part->next = part->next;
part->next = part_freelist;
part_freelist = part;
part = last_part;
}
else
{
last_part = part;
}
}
AssertFatal( n_parts >= 0, "ParticleEmitter: negative part count!" );
if (n_parts < 1 && mDeleteWhenEmpty)
{
mDeleteOnTick = true;
return;
}
if( numMSToUpdate != 0 && n_parts > 0 )
{
update( numMSToUpdate );
}
}
//-----------------------------------------------------------------------------
// Update key related particle data
//-----------------------------------------------------------------------------
void ParticleEmitter::updateKeyData( Particle *part )
{
//Ensure that our lifetime is never below 0
if( part->totalLifetime < 1 )
part->totalLifetime = 1;
if (part->currentAge > part->totalLifetime)
part->currentAge = part->totalLifetime;
F32 t = (F32)part->currentAge / (F32)part->totalLifetime;
for( U32 i = 1; i < ParticleData::PDC_NUM_KEYS; i++ )
{
if( part->dataBlock->times[i] >= t )
{
F32 firstPart = t - part->dataBlock->times[i-1];
F32 total = part->dataBlock->times[i] -
part->dataBlock->times[i-1];
firstPart /= total;
if( mDataBlock->useEmitterColors )
{
part->color.interpolate(colors[i-1], colors[i], firstPart);
}
else
{
part->color.interpolate(part->dataBlock->colors[i-1],
part->dataBlock->colors[i],
firstPart);
}
if( mDataBlock->useEmitterSizes )
{
part->size = (sizes[i-1] * (1.0 - firstPart)) +
(sizes[i] * firstPart);
}
else
{
part->size = (part->dataBlock->sizes[i-1] * (1.0 - firstPart)) +
(part->dataBlock->sizes[i] * firstPart);
part->size *= part->dataBlock->sizeBias;
}
if (mDataBlock->fade_color)
{
if (mDataBlock->fade_alpha)
part->color *= fade_amt;
else
{
part->color.red *= fade_amt;
part->color.green *= fade_amt;
part->color.blue *= fade_amt;
}
}
else if (mDataBlock->fade_alpha)
part->color.alpha *= fade_amt;
if (mDataBlock->fade_size)
part->size *= fade_amt;
break;
}
}
}
//-----------------------------------------------------------------------------
// Update particles
//-----------------------------------------------------------------------------
// AFX CODE BLOCK (enhanced-emitter) <<
void ParticleEmitter::update( U32 ms )
{
F32 t = F32(ms)/1000.0f; // AFX -- moved outside loop, no need to recalculate this for every particle
for (Particle* part = part_list_head.next; part != NULL; part = part->next)
{
Point3F a = part->acc;
a -= part->vel * part->dataBlock->dragCoefficient;
a += mWindVelocity * part->dataBlock->windCoefficient;
a.z += -9.81f*part->dataBlock->gravityCoefficient; // AFX -- as long as gravity is a constant, this is faster
part->vel += a * t;
part->pos_local += part->vel * t;
// AFX -- allow subclasses to adjust the particle params here
sub_particleUpdate(part);
if (part->dataBlock->constrain_pos)
part->pos = part->pos_local + this->pos_pe;
else
part->pos = part->pos_local;
updateKeyData( part );
}
}
//-----------------------------------------------------------------------------
// Copy particles to vertex buffer
//-----------------------------------------------------------------------------
// structure used for particle sorting.
struct SortParticle
{
Particle* p;
F32 k;
};
// qsort callback function for particle sorting
S32 QSORT_CALLBACK cmpSortParticles(const void* p1, const void* p2)
{
const SortParticle* sp1 = (const SortParticle*)p1;
const SortParticle* sp2 = (const SortParticle*)p2;
if (sp2->k > sp1->k)
return 1;
else if (sp2->k == sp1->k)
return 0;
else
return -1;
}
void ParticleEmitter::copyToVB( const Point3F &camPos, const LinearColorF &ambientColor )
{
static Vector orderedVector(__FILE__, __LINE__);
PROFILE_START(ParticleEmitter_copyToVB);
PROFILE_START(ParticleEmitter_copyToVB_Sort);
// build sorted list of particles (far to near)
if (mDataBlock->sortParticles)
{
orderedVector.clear();
MatrixF modelview = GFX->getWorldMatrix();
Point3F viewvec; modelview.getRow(1, &viewvec);
// add each particle and a distance based sort key to orderedVector
for (Particle* pp = part_list_head.next; pp != NULL; pp = pp->next)
{
orderedVector.increment();
orderedVector.last().p = pp;
orderedVector.last().k = mDot(pp->pos, viewvec);
}
// qsort the list into far to near ordering
dQsort(orderedVector.address(), orderedVector.size(), sizeof(SortParticle), cmpSortParticles);
}
PROFILE_END();
static Vector tempBuff(2048);
tempBuff.reserve( n_parts*4 + 64); // make sure tempBuff is big enough
ParticleVertexType *buffPtr = tempBuff.address(); // use direct pointer (faster)
if (mDataBlock->ribbonParticles)
{
PROFILE_START(ParticleEmitter_copyToVB_Ribbon);
if (mDataBlock->reverseOrder)
{
Particle* oldPtr = NULL;
for (Particle* partPtr = part_list_head.next; partPtr != NULL; partPtr = partPtr->next, buffPtr -= 4)
{
setupRibbon(partPtr, partPtr->next, oldPtr, camPos, ambientColor, buffPtr);
oldPtr = partPtr;
}
}
else
{
Particle* oldPtr = NULL;
for (Particle* partPtr = part_list_head.next; partPtr != NULL; partPtr = partPtr->next, buffPtr += 4)
{
setupRibbon(partPtr, partPtr->next, oldPtr, camPos, ambientColor, buffPtr);
oldPtr = partPtr;
}
}
PROFILE_END();
}
else if (mDataBlock->orientParticles)
{
PROFILE_START(ParticleEmitter_copyToVB_Orient);
if (mDataBlock->reverseOrder)
{
buffPtr += 4*(n_parts-1);
// do sorted-oriented particles
if (mDataBlock->sortParticles)
{
SortParticle* partPtr = orderedVector.address();
for (U32 i = 0; i < n_parts; i++, partPtr++, buffPtr-=4 )
setupOriented(partPtr->p, camPos, ambientColor, buffPtr);
}
// do unsorted-oriented particles
else
{
for (Particle* partPtr = part_list_head.next; partPtr != NULL; partPtr = partPtr->next, buffPtr-=4)
setupOriented(partPtr, camPos, ambientColor, buffPtr);
}
}
else
{
// do sorted-oriented particles
if (mDataBlock->sortParticles)
{
SortParticle* partPtr = orderedVector.address();
for (U32 i = 0; i < n_parts; i++, partPtr++, buffPtr+=4 )
setupOriented(partPtr->p, camPos, ambientColor, buffPtr);
}
// do unsorted-oriented particles
else
{
for (Particle* partPtr = part_list_head.next; partPtr != NULL; partPtr = partPtr->next, buffPtr+=4)
setupOriented(partPtr, camPos, ambientColor, buffPtr);
}
}
PROFILE_END();
}
else if (mDataBlock->alignParticles)
{
PROFILE_START(ParticleEmitter_copyToVB_Aligned);
if (mDataBlock->reverseOrder)
{
buffPtr += 4*(n_parts-1);
// do sorted-oriented particles
if (mDataBlock->sortParticles)
{
SortParticle* partPtr = orderedVector.address();
for (U32 i = 0; i < n_parts; i++, partPtr++, buffPtr-=4 )
setupAligned(partPtr->p, ambientColor, buffPtr);
}
// do unsorted-oriented particles
else
{
Particle *partPtr = part_list_head.next;
for (; partPtr != NULL; partPtr = partPtr->next, buffPtr-=4)
setupAligned(partPtr, ambientColor, buffPtr);
}
}
else
{
// do sorted-oriented particles
if (mDataBlock->sortParticles)
{
SortParticle* partPtr = orderedVector.address();
for (U32 i = 0; i < n_parts; i++, partPtr++, buffPtr+=4 )
setupAligned(partPtr->p, ambientColor, buffPtr);
}
// do unsorted-oriented particles
else
{
Particle *partPtr = part_list_head.next;
for (; partPtr != NULL; partPtr = partPtr->next, buffPtr+=4)
setupAligned(partPtr, ambientColor, buffPtr);
}
}
PROFILE_END();
}
else
{
PROFILE_START(ParticleEmitter_copyToVB_NonOriented);
// somewhat odd ordering so that texture coordinates match the oriented
// particles
Point3F basePoints[4];
basePoints[0] = Point3F(-1.0, 0.0, 1.0);
basePoints[1] = Point3F(-1.0, 0.0, -1.0);
basePoints[2] = Point3F( 1.0, 0.0, -1.0);
basePoints[3] = Point3F( 1.0, 0.0, 1.0);
MatrixF camView = GFX->getWorldMatrix();
camView.transpose(); // inverse - this gets the particles facing camera
if (mDataBlock->reverseOrder)
{
buffPtr += 4*(n_parts-1);
// do sorted-billboard particles
if (mDataBlock->sortParticles)
{
SortParticle *partPtr = orderedVector.address();
for( U32 i=0; ip, basePoints, camView, ambientColor, buffPtr );
}
// do unsorted-billboard particles
else
{
for (Particle* partPtr = part_list_head.next; partPtr != NULL; partPtr = partPtr->next, buffPtr-=4)
setupBillboard( partPtr, basePoints, camView, ambientColor, buffPtr );
}
}
else
{
// do sorted-billboard particles
if (mDataBlock->sortParticles)
{
SortParticle *partPtr = orderedVector.address();
for( U32 i=0; ip, basePoints, camView, ambientColor, buffPtr );
}
// do unsorted-billboard particles
else
{
for (Particle* partPtr = part_list_head.next; partPtr != NULL; partPtr = partPtr->next, buffPtr+=4)
setupBillboard( partPtr, basePoints, camView, ambientColor, buffPtr );
}
}
PROFILE_END();
}
PROFILE_START(ParticleEmitter_copyToVB_LockCopy);
// create new VB if emitter size grows
if( !mVertBuff || n_parts > mCurBuffSize )
{
mCurBuffSize = n_parts;
mVertBuff.set( GFX, n_parts * 4, GFXBufferTypeDynamic );
}
// lock and copy tempBuff to video RAM
ParticleVertexType *verts = mVertBuff.lock();
dMemcpy( verts, tempBuff.address(), n_parts * 4 * sizeof(ParticleVertexType) );
mVertBuff.unlock();
PROFILE_END();
PROFILE_END();
}
//-----------------------------------------------------------------------------
// Set up particle for billboard style render
//-----------------------------------------------------------------------------
void ParticleEmitter::setupBillboard( Particle *part,
Point3F *basePts,
const MatrixF &camView,
const LinearColorF &ambientColor,
ParticleVertexType *lVerts )
{
F32 width = part->size * 0.5f;
F32 spinAngle = part->spinSpeed * part->currentAge * AgedSpinToRadians;
F32 sy, cy;
mSinCos(spinAngle, sy, cy);
const F32 ambientLerp = mClampF( mDataBlock->ambientFactor, 0.0f, 1.0f );
LinearColorF partCol = mLerp( part->color, ( part->color * ambientColor ), ambientLerp );
// fill four verts, use macro and unroll loop
#define fillVert(){ \
lVerts->point.x = cy * basePts->x - sy * basePts->z; \
lVerts->point.y = 0.0f; \
lVerts->point.z = sy * basePts->x + cy * basePts->z; \
camView.mulV( lVerts->point ); \
lVerts->point *= width; \
lVerts->point += part->pos; \
lVerts->color = partCol.toColorI(); } \
// Here we deal with UVs for animated particle (billboard)
if (part->dataBlock->animateTexture && !part->dataBlock->animTexFrames.empty())
{
S32 fm = (S32)(part->currentAge*(1.0/1000.0)*part->dataBlock->framesPerSec);
U8 fm_tile = part->dataBlock->animTexFrames[fm % part->dataBlock->numFrames];
S32 uv[4];
uv[0] = fm_tile + fm_tile/part->dataBlock->animTexTiling.x;
uv[1] = uv[0] + (part->dataBlock->animTexTiling.x + 1);
uv[2] = uv[1] + 1;
uv[3] = uv[0] + 1;
fillVert();
// Here and below, we copy UVs from particle datablock's current frame's UVs (billboard)
lVerts->texCoord = part->dataBlock->animTexUVs[uv[0]];
++lVerts;
++basePts;
fillVert();
lVerts->texCoord = part->dataBlock->animTexUVs[uv[1]];
++lVerts;
++basePts;
fillVert();
lVerts->texCoord = part->dataBlock->animTexUVs[uv[2]];
++lVerts;
++basePts;
fillVert();
lVerts->texCoord = part->dataBlock->animTexUVs[uv[3]];
++lVerts;
++basePts;
return;
}
fillVert();
// Here and below, we copy UVs from particle datablock's texCoords (billboard)
lVerts->texCoord = part->dataBlock->texCoords[0];
++lVerts;
++basePts;
fillVert();
lVerts->texCoord = part->dataBlock->texCoords[1];
++lVerts;
++basePts;
fillVert();
lVerts->texCoord = part->dataBlock->texCoords[2];
++lVerts;
++basePts;
fillVert();
lVerts->texCoord = part->dataBlock->texCoords[3];
++lVerts;
++basePts;
}
//-----------------------------------------------------------------------------
// Set up oriented particle
//-----------------------------------------------------------------------------
void ParticleEmitter::setupOriented( Particle *part,
const Point3F &camPos,
const LinearColorF &ambientColor,
ParticleVertexType *lVerts )
{
Point3F dir;
if( mDataBlock->orientOnVelocity )
{
// don't render oriented particle if it has no velocity
if( part->vel.magnitudeSafe() == 0.0 ) return;
dir = part->vel;
}
else
{
dir = part->orientDir;
}
Point3F dirFromCam = part->pos - camPos;
Point3F crossDir;
mCross( dirFromCam, dir, &crossDir );
crossDir.normalize();
dir.normalize();
F32 width = part->size * 0.5f;
dir *= width;
crossDir *= width;
Point3F start = part->pos - dir;
Point3F end = part->pos + dir;
const F32 ambientLerp = mClampF( mDataBlock->ambientFactor, 0.0f, 1.0f );
LinearColorF partCol = mLerp( part->color, ( part->color * ambientColor ), ambientLerp );
const ColorI color = partCol.toColorI();
// Here we deal with UVs for animated particle (oriented)
if (part->dataBlock->animateTexture && !part->dataBlock->animTexFrames.empty())
{
// Let particle compute the UV indices for current frame
S32 fm = (S32)(part->currentAge*(1.0f/1000.0f)*part->dataBlock->framesPerSec);
U8 fm_tile = part->dataBlock->animTexFrames[fm % part->dataBlock->numFrames];
S32 uv[4];
uv[0] = fm_tile + fm_tile/part->dataBlock->animTexTiling.x;
uv[1] = uv[0] + (part->dataBlock->animTexTiling.x + 1);
uv[2] = uv[1] + 1;
uv[3] = uv[0] + 1;
lVerts->point = start + crossDir;
lVerts->color = color;
// Here and below, we copy UVs from particle datablock's current frame's UVs (oriented)
lVerts->texCoord = part->dataBlock->animTexUVs[uv[0]];
++lVerts;
lVerts->point = start - crossDir;
lVerts->color = color;
lVerts->texCoord = part->dataBlock->animTexUVs[uv[1]];
++lVerts;
lVerts->point = end - crossDir;
lVerts->color = color;
lVerts->texCoord = part->dataBlock->animTexUVs[uv[2]];
++lVerts;
lVerts->point = end + crossDir;
lVerts->color = color;
lVerts->texCoord = part->dataBlock->animTexUVs[uv[3]];
++lVerts;
return;
}
lVerts->point = start + crossDir;
lVerts->color = color;
// Here and below, we copy UVs from particle datablock's texCoords (oriented)
lVerts->texCoord = part->dataBlock->texCoords[1];
++lVerts;
lVerts->point = start - crossDir;
lVerts->color = color;
lVerts->texCoord = part->dataBlock->texCoords[2];
++lVerts;
lVerts->point = end - crossDir;
lVerts->color = color;
lVerts->texCoord = part->dataBlock->texCoords[3];
++lVerts;
lVerts->point = end + crossDir;
lVerts->color = color;
lVerts->texCoord = part->dataBlock->texCoords[0];
++lVerts;
}
void ParticleEmitter::setupAligned( const Particle *part,
const LinearColorF &ambientColor,
ParticleVertexType *lVerts )
{
// The aligned direction will always be normalized.
Point3F dir = mDataBlock->alignDirection;
// Find a right vector for this particle.
Point3F right;
if (mFabs(dir.y) > mFabs(dir.z))
mCross(Point3F::UnitZ, dir, &right);
else
mCross(Point3F::UnitY, dir, &right);
right.normalize();
// If we have a spin velocity.
if ( !mIsZero( part->spinSpeed ) )
{
F32 spinAngle = part->spinSpeed * part->currentAge * AgedSpinToRadians;
// This is an inline quaternion vector rotation which
// is faster that QuatF.mulP(), but generates different
// results and hence cannot replace it right now.
F32 sin, qw;
mSinCos( spinAngle * 0.5f, sin, qw );
F32 qx = dir.x * sin;
F32 qy = dir.y * sin;
F32 qz = dir.z * sin;
F32 vx = ( right.x * qw ) + ( right.z * qy ) - ( right.y * qz );
F32 vy = ( right.y * qw ) + ( right.x * qz ) - ( right.z * qx );
F32 vz = ( right.z * qw ) + ( right.y * qx ) - ( right.x * qy );
F32 vw = ( right.x * qx ) + ( right.y * qy ) + ( right.z * qz );
right.x = ( qw * vx ) + ( qx * vw ) + ( qy * vz ) - ( qz * vy );
right.y = ( qw * vy ) + ( qy * vw ) + ( qz * vx ) - ( qx * vz );
right.z = ( qw * vz ) + ( qz * vw ) + ( qx * vy ) - ( qy * vx );
}
// Get the cross vector.
Point3F cross;
mCross(right, dir, &cross);
F32 width = part->size * 0.5f;
right *= width;
cross *= width;
Point3F start = part->pos - right;
Point3F end = part->pos + right;
const F32 ambientLerp = mClampF( mDataBlock->ambientFactor, 0.0f, 1.0f );
LinearColorF partCol = mLerp( part->color, ( part->color * ambientColor ), ambientLerp );
const ColorI color = partCol.toColorI();
// Here we deal with UVs for animated particle
if (part->dataBlock->animateTexture && !part->dataBlock->animTexFrames.empty())
{
// Let particle compute the UV indices for current frame
S32 fm = (S32)(part->currentAge*(1.0f/1000.0f)*part->dataBlock->framesPerSec);
U8 fm_tile = part->dataBlock->animTexFrames[fm % part->dataBlock->numFrames];
S32 uv[4];
uv[0] = fm_tile + fm_tile/part->dataBlock->animTexTiling.x;
uv[1] = uv[0] + (part->dataBlock->animTexTiling.x + 1);
uv[2] = uv[1] + 1;
uv[3] = uv[0] + 1;
lVerts->point = start + cross;
lVerts->color = color;
lVerts->texCoord = part->dataBlock->animTexUVs[uv[0]];
++lVerts;
lVerts->point = start - cross;
lVerts->color = color;
lVerts->texCoord = part->dataBlock->animTexUVs[uv[1]];
++lVerts;
lVerts->point = end - cross;
lVerts->color = color;
lVerts->texCoord = part->dataBlock->animTexUVs[uv[2]];
++lVerts;
lVerts->point = end + cross;
lVerts->color = color;
lVerts->texCoord = part->dataBlock->animTexUVs[uv[3]];
++lVerts;
}
else
{
// Here and below, we copy UVs from particle datablock's texCoords
lVerts->point = start + cross;
lVerts->color = color;
lVerts->texCoord = part->dataBlock->texCoords[0];
++lVerts;
lVerts->point = start - cross;
lVerts->color = color;
lVerts->texCoord = part->dataBlock->texCoords[1];
++lVerts;
lVerts->point = end - cross;
lVerts->color = color;
lVerts->texCoord = part->dataBlock->texCoords[2];
++lVerts;
lVerts->point = end + cross;
lVerts->color = color;
lVerts->texCoord = part->dataBlock->texCoords[3];
++lVerts;
}
}
void ParticleEmitter::setupRibbon(Particle *part,
Particle *next,
Particle *prev,
const Point3F &camPos,
const LinearColorF &ambientColor,
ParticleVertexType *lVerts)
{
Point3F dir, dirFromCam;
Point3F crossDir, crossDirNext;
Point3F start, end;
LinearColorF prevCol;
static Point3F crossDirPrev;
static int position;
static F32 alphaMod, alphaModEnd;
const F32 ambientLerp = mClampF(mDataBlock->ambientFactor, 0.0f, 1.0f);
LinearColorF partCol = mLerp(part->color, (part->color * ambientColor), ambientLerp);
if (part->currentAge > part->totalLifetime)
{
F32 alphaDeath = (part->currentAge - part->totalLifetime) / 200.0f;
if (alphaDeath > 1.0f)
alphaDeath = 1.0f;
alphaDeath = 1.0f - alphaDeath;
partCol.alpha *= alphaDeath;
}
start = part->pos;
position++;
if (next == NULL && prev == NULL) {
// a ribbon of just one particle
position = 0;
if (part->vel.magnitudeSafe() == 0.0)
dir = part->orientDir;
else
dir = part->vel;
dir.normalize();
dirFromCam = part->pos - camPos;
mCross(dirFromCam, dir, &crossDir);
crossDir.normalize();
crossDir = crossDir * part->size * 0.5;
crossDirPrev = crossDir;
partCol.alpha = 0.0f;
prevCol = partCol;
end = part->pos;
}
else if (next == NULL && prev != NULL)
{
// last link in the chain, also the oldest
dir = part->pos - prev->pos;
dir.normalize();
dirFromCam = part->pos - camPos;
mCross(dirFromCam, dir, &crossDir);
crossDir.normalize();
crossDir = crossDir * part->size * 0.5;
end = prev->pos;
partCol.alpha = 0.0f;
prevCol = mLerp(prev->color, (prev->color * ambientColor), ambientLerp);
prevCol.alpha *= alphaModEnd;
}
else if (next != NULL && prev == NULL)
{
// first link in chain, newest particle
// since we draw from current to previous, this one isn't drawn
position = 0;
dir = next->pos - part->pos;
dir.normalize();
dirFromCam = part->pos - camPos;
mCross(dirFromCam, dir, &crossDir);
crossDir.normalize();
crossDir = crossDir * part->size * 0.5f;
crossDirPrev = crossDir;
partCol.alpha = 0.0f;
prevCol = partCol;
alphaModEnd = 0.0f;
end = part->pos;
}
else
{
// middle of chain
dir = next->pos - prev->pos;
dir.normalize();
dirFromCam = part->pos - camPos;
mCross(dirFromCam, dir, &crossDir);
crossDir.normalize();
crossDir = crossDir * part->size * 0.5;
prevCol = mLerp(prev->color, (prev->color * ambientColor), ambientLerp);
if (position == 1)
{
// the second particle has a few tweaks for alpha, to smoothly match the first particle
// we only want to do this once when the particle first fades in, and avoid a strobing effect
alphaMod = (float(part->currentAge) / float(part->currentAge - prev->currentAge)) - 1.0f;
if (alphaMod > 1.0f)
alphaMod = 1.0f;
partCol.alpha *= alphaMod;
prevCol.alpha = 0.0f;
if (next->next == NULL)
alphaModEnd = alphaMod;
//Con::printf("alphaMod: %f", alphaMod );
}
else if (position == 2)
{
prevCol.alpha *= alphaMod;
alphaMod = 0.0f;
}
if (next->next == NULL && position > 1)
{
// next to last particle, start the fade out
alphaModEnd = (float(next->totalLifetime - next->currentAge)) / (float(part->totalLifetime - part->currentAge));
alphaModEnd *= 2.0f;
if (alphaModEnd > 1.0f)
alphaModEnd = 1.0f;
partCol.alpha *= alphaModEnd;
//Con::printf("alphaMod: %f Lifetime: %d Age: %d", alphaMod, part->totalLifetime, part->currentAge );
}
end = prev->pos;
}
ColorI pCol = partCol.toColorI();
// Here we deal with UVs for animated particle (oriented)
if (part->dataBlock->animateTexture && !part->dataBlock->animTexFrames.empty())
{
// Let particle compute the UV indices for current frame
S32 fm = (S32)(part->currentAge*(1.0f / 1000.0f)*part->dataBlock->framesPerSec);
U8 fm_tile = part->dataBlock->animTexFrames[fm % part->dataBlock->numFrames];
S32 uv[4];
uv[0] = fm_tile + fm_tile / part->dataBlock->animTexTiling.x;
uv[1] = uv[0] + (part->dataBlock->animTexTiling.x + 1);
uv[2] = uv[1] + 1;
uv[3] = uv[0] + 1;
lVerts->point = start + crossDir;
lVerts->color = pCol;
// Here and below, we copy UVs from particle datablock's current frame's UVs (oriented)
lVerts->texCoord = part->dataBlock->animTexUVs[uv[0]];
++lVerts;
lVerts->point = start - crossDir;
lVerts->color = pCol;
lVerts->texCoord = part->dataBlock->animTexUVs[uv[1]];
++lVerts;
lVerts->point = end - crossDirPrev;
lVerts->color = pCol;
lVerts->texCoord = part->dataBlock->animTexUVs[uv[2]];
++lVerts;
lVerts->point = end + crossDirPrev;
lVerts->color = pCol;
lVerts->texCoord = part->dataBlock->animTexUVs[uv[3]];
++lVerts;
crossDirPrev = crossDir;
return;
}
lVerts->point = start + crossDir;
lVerts->color = pCol;
// Here and below, we copy UVs from particle datablock's texCoords (oriented)
lVerts->texCoord = part->dataBlock->texCoords[0];
++lVerts;
lVerts->point = start - crossDir;
lVerts->color = pCol;
lVerts->texCoord = part->dataBlock->texCoords[1];
++lVerts;
lVerts->point = end - crossDirPrev;
lVerts->color = pCol;
lVerts->texCoord = part->dataBlock->texCoords[2];
++lVerts;
lVerts->point = end + crossDirPrev;
lVerts->color = pCol;
lVerts->texCoord = part->dataBlock->texCoords[3];
++lVerts;
crossDirPrev = crossDir;
}
bool ParticleEmitterData::reload()
{
// Clear out current particle data.
dataBlockIds.clear();
particleDataBlocks.clear();
// Parse out particle string.
U32 numUnits = 0;
if( particleString )
numUnits = StringUnit::getUnitCount( particleString, " \t" );
if( !particleString || !particleString[ 0 ] || !numUnits )
{
Con::errorf( "ParticleEmitterData(%s) has an empty particles string.", getName() );
mReloadSignal.trigger();
return false;
}
for( U32 i = 0; i < numUnits; ++ i )
{
const char* dbName = StringUnit::getUnit( particleString, i, " \t" );
ParticleData* data = NULL;
if( !Sim::findObject( dbName, data ) )
{
Con::errorf( ConsoleLogEntry::General, "ParticleEmitterData(%s) unable to find particle datablock: %s", getName(), dbName );
continue;
}
particleDataBlocks.push_back( data );
dataBlockIds.push_back( data->getId() );
}
// Check that we actually found some particle datablocks.
if( particleDataBlocks.empty() )
{
Con::errorf( ConsoleLogEntry::General, "ParticleEmitterData(%s) unable to find any particle datablocks", getName() );
mReloadSignal.trigger();
return false;
}
// Trigger reload.
mReloadSignal.trigger();
return true;
}
DefineEngineMethod(ParticleEmitterData, reload, void,(),,
"Reloads the ParticleData datablocks and other fields used by this emitter.\n"
"@tsexample\n"
"// Get the editor's current particle emitter\n"
"%emitter = PE_EmitterEditor.currEmitter\n\n"
"// Change a field value\n"
"%emitter.setFieldValue( %propertyField, %value );\n\n"
"// Reload this emitter\n"
"%emitter.reload();\n"
"@endtsexample\n")
{
object->reload();
}
void ParticleEmitter::emitParticlesExt(const MatrixF& xfm, const Point3F& point,
const Point3F& velocity, const U32 numMilliseconds)
{
if (mDataBlock->use_emitter_xfm)
{
Point3F zero_point(0.0f, 0.0f, 0.0f);
this->pos_pe = zero_point;
this->setTransform(xfm);
Point3F axis(0.0,0.0,1.0);
xfm.mulV(axis);
emitParticles(zero_point, true, axis, velocity, numMilliseconds);
}
else
{
this->pos_pe = point;
Point3F axis(0.0,0.0,1.0);
xfm.mulV(axis);
emitParticles(point, true, axis, velocity, numMilliseconds);
}
}
void ParticleEmitter::setForcedObjBox(Box3F& box)
{
mObjBox = box;
forced_bbox = true;
#if defined(AFX_CAP_PARTICLE_POOLS)
if (pool)
pool->updatePoolBBox(this);
#endif
}
void ParticleEmitter::setSortPriority(S8 priority)
{
sort_priority = (priority == 0) ? 1 : priority;
#if defined(AFX_CAP_PARTICLE_POOLS)
if (pool)
pool->setSortPriority(sort_priority);
#endif
}