//----------------------------------------------------------------------------- // 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 }