/*==LICENSE==* CyanWorlds.com Engine - MMOG client, server and tools Copyright (C) 2011 Cyan Worlds, Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . You can contact Cyan Worlds, Inc. by email legal@cyan.com or by snail mail at: Cyan Worlds, Inc. 14617 N Newport Hwy Mead, WA 99021 *==LICENSE==*/ #include "hsTypes.h" #include "hsUtils.h" #include "hsResMgr.h" #include "pnMessage/plRefMsg.h" #include "plMessage/plParticleUpdateMsg.h" #include "plParticleGenerator.h" #include "plParticleEmitter.h" #include "plParticleSystem.h" #include "plParticle.h" #include "plParticleEffect.h" #include "hsColorRGBA.h" #include "plInterp/plController.h" #include "plSurface/hsGMaterial.h" #include "plSurface/plLayerInterface.h" #include "plProfile.h" #include "hsFastMath.h" plProfile_CreateTimer("Update", "Particles", ParticleUpdate); plProfile_CreateTimer("Generate", "Particles", ParticleGenerate); plParticleEmitter::plParticleEmitter() { fParticleCores = nil; fParticleExts = nil; fGenerator = nil; fLocalToWorld.Reset(); fTimeToLive = 0; } void plParticleEmitter::Init(plParticleSystem *system, UInt32 maxParticles, UInt32 spanIndex, UInt32 miscFlags, plParticleGenerator *gen /* = nil */) { IClear(); fSystem = system; plLayerInterface *layer = system->fTexture->GetLayer(0)->BottomOfStack(); fMiscFlags = miscFlags | kNeedsUpdate; if( fMiscFlags & kOnReserve ) fTimeToLive = -1.f; // Wait for someone to give us a spurt of life. if( layer->GetShadeFlags() & hsGMatState::kShadeEmissive ) { fMiscFlags |= kMatIsEmissive; fColor = layer->GetAmbientColor(); } else { fColor = layer->GetRuntimeColor(); } fColor.a = layer->GetOpacity(); fGenerator = gen; fMaxParticles = maxParticles; fSpanIndex = spanIndex; ISetupParticleMem(); } void plParticleEmitter::Clone(plParticleEmitter* src, UInt32 spanIndex) { Init(src->fSystem, src->fMaxParticles, spanIndex, src->fMiscFlags, src->fGenerator); fMiscFlags |= kBorrowedGenerator; fTimeToLive = -1.f; } void plParticleEmitter::OverrideLocalToWorld(const hsMatrix44& l2w) { fLocalToWorld = l2w; fMiscFlags |= kOverrideLocalToWorld; } plParticleEmitter::~plParticleEmitter() { IClear(); } void plParticleEmitter::IClear() { delete [] fParticleCores; fParticleCores = nil; delete [] fParticleExts; fParticleExts = nil; if( !(fMiscFlags & kBorrowedGenerator) ) delete fGenerator; fGenerator = nil; } void plParticleEmitter::ISetupParticleMem() { fNumValidParticles = 0; fParticleCores = TRACKED_NEW plParticleCore[fMaxParticles]; fParticleExts = TRACKED_NEW plParticleExt[fMaxParticles]; fTargetInfo.fPos = (UInt8 *)fParticleCores; fTargetInfo.fColor = (UInt8 *)fParticleCores + sizeof(hsPoint3); fTargetInfo.fPosStride = fTargetInfo.fColorStride = sizeof(plParticleCore); fTargetInfo.fVelocity = (UInt8 *)fParticleExts; fTargetInfo.fInvMass = fTargetInfo.fVelocity + sizeof(hsVector3); fTargetInfo.fAcceleration = fTargetInfo.fInvMass + sizeof(hsScalar); fTargetInfo.fMiscFlags = (UInt8 *)&(fParticleExts[0].fMiscFlags); fTargetInfo.fRadsPerSec = (UInt8 *)&(fParticleExts[0].fRadsPerSec); fTargetInfo.fVelocityStride = fTargetInfo.fInvMassStride = fTargetInfo.fAccelerationStride = fTargetInfo.fRadsPerSecStride = fTargetInfo.fMiscFlagsStride = sizeof(plParticleExt); } UInt32 plParticleEmitter::GetNumTiles() const { return fSystem->GetNumTiles(); } const hsMatrix44 &plParticleEmitter::GetLocalToWorld() const { return fMiscFlags & kOverrideLocalToWorld ? fLocalToWorld : fSystem->GetLocalToWorld(); } void plParticleEmitter::AddParticle(hsPoint3 &pos, hsVector3 &velocity, UInt32 tileIndex, hsScalar hSize, hsScalar vSize, hsScalar scale, hsScalar invMass, hsScalar life, hsPoint3 &orientation, UInt32 miscFlags, hsScalar radsPerSec) { plParticleCore *core; plParticleExt *ext; UInt32 currParticle; if (fNumValidParticles == fMaxParticles) return; // No more room... you lose! else currParticle = fNumValidParticles++; core = &fParticleCores[currParticle]; core->fPos = pos; core->fOrientation = orientation; core->fColor = CreateHexColor(fColor); core->fHSize = hSize * scale; core->fVSize = vSize * scale; // even if the kNormalUp flag isn't there, we should initialize it to something sane. //if (core->fMiscFlags & kNormalUp != 0) core->fNormal.Set(0, 0, 1); hsScalar xOff = (tileIndex % fSystem->fXTiles) / (float)fSystem->fXTiles; hsScalar yOff = (tileIndex / fSystem->fXTiles) / (float)fSystem->fYTiles; core->fUVCoords[0].fX = xOff; core->fUVCoords[0].fY = yOff + 1.0f / fSystem->fYTiles; core->fUVCoords[0].fZ = 1.0f; core->fUVCoords[1].fX = xOff + 1.0f / fSystem->fXTiles; core->fUVCoords[1].fY = yOff + 1.0f / fSystem->fYTiles; core->fUVCoords[1].fZ = 1.0f; core->fUVCoords[2].fX = xOff + 1.0f / fSystem->fXTiles; core->fUVCoords[2].fY = yOff; core->fUVCoords[2].fZ = 1.0f; core->fUVCoords[3].fX = xOff; core->fUVCoords[3].fY = yOff; core->fUVCoords[3].fZ = 1.0f; ext = &fParticleExts[currParticle]; ext->fVelocity = velocity; ext->fInvMass = invMass; ext->fLife = ext->fStartLife = life; ext->fMiscFlags = miscFlags; // Is this ever NOT zero? if (life <= 0) ext->fMiscFlags |= plParticleExt::kImmortal; ext->fRadsPerSec = radsPerSec; ext->fAcceleration.Set(0, 0, 0); ext->fScale = scale; } void plParticleEmitter::WipeExistingParticles() { fNumValidParticles = 0; // That was easy. } // This method is called from a network received message. Don't trust the args without checking. void plParticleEmitter::KillParticles(hsScalar num, hsScalar timeToDie, UInt8 flags) { if (flags & plParticleKillMsg::kParticleKillPercentage) { if (num < 0) num = 0; if (num > 1) num = 1; num *= fNumValidParticles; } else { if (num < 0) num = 0; } if (timeToDie < 0) timeToDie = 0; int i; for (i = 0; i < fNumValidParticles && num > 0; i++) { if ((flags & plParticleKillMsg::kParticleKillImmortalOnly) && !(fParticleExts[i].fMiscFlags & plParticleExt::kImmortal)) continue; fParticleExts[i].fLife = fParticleExts[i].fStartLife = timeToDie; fParticleExts[i].fMiscFlags &= ~plParticleExt::kImmortal; num--; } } void plParticleEmitter::UpdateGenerator(UInt32 paramID, hsScalar paramValue) { if (fGenerator != nil) fGenerator->UpdateParam(paramID, paramValue); } UInt16 plParticleEmitter::StealParticlesFrom(plParticleEmitter *victim, UInt16 num) { UInt16 spaceAvail = (UInt16)(fMaxParticles - fNumValidParticles); UInt16 numToCopy = (UInt16)(hsMinimum(num, (victim ? victim->fNumValidParticles : 0))); if (spaceAvail < numToCopy) numToCopy = spaceAvail; if (numToCopy > 0) { // copy them over memcpy(&(fParticleCores[fNumValidParticles]), &(victim->fParticleCores[victim->fNumValidParticles - numToCopy]), numToCopy * sizeof(plParticleCore)); memcpy(&(fParticleExts[fNumValidParticles]), &(victim->fParticleExts[victim->fNumValidParticles- numToCopy]), numToCopy * sizeof(plParticleExt)); fNumValidParticles += numToCopy; victim->fNumValidParticles -= numToCopy; } return numToCopy; } void plParticleEmitter::TranslateAllParticles(hsPoint3 &amount) { int i; for (i = 0; i < fNumValidParticles; i++) fParticleCores[i].fPos += amount; } hsBool plParticleEmitter::IUpdate(hsScalar delta) { if (fMiscFlags & kNeedsUpdate) { plProfile_BeginTiming(ParticleUpdate); IUpdateParticles(delta); IUpdateBoundsAndNormals(delta); plProfile_EndTiming(ParticleUpdate); } if (fGenerator == nil && fNumValidParticles <= 0) return false; // no generator, no particles, let the system decide if this emitter is done else return true; } void plParticleEmitter::IUpdateParticles(hsScalar delta) { int i, j; // Have to remove particles before adding new ones, or we can run out of room. for (i = 0; i < fNumValidParticles; i++) { fParticleExts[i].fLife -= delta; if (fParticleExts[i].fLife <= 0 && !(fParticleExts[i].fMiscFlags & plParticleExt::kImmortal)) { IRemoveParticle(i); i--; // so that we hit this index again on the next iteration continue; } } fTargetInfo.fFirstNewParticle = fNumValidParticles; if ((fGenerator != nil) && (fTimeToLive >= 0)) { plProfile_BeginLap(ParticleGenerate, fSystem->GetKeyName()); if (!fGenerator->AddAutoParticles(this, delta)) { delete fGenerator; fGenerator = nil; } if( (fTimeToLive > 0) && ((fTimeToLive -= delta) <= 0) ) fTimeToLive = -1.f; plProfile_EndLap(ParticleGenerate, fSystem->GetKeyName()); } fTargetInfo.fContext = fSystem->fContext; fTargetInfo.fNumValidParticles = fNumValidParticles; const hsVector3 up(0, 0, 1.0f); hsVector3 currDirection; hsPoint3 *currPos; hsVector3 *currVelocity; hsVector3 *currAccel; hsPoint3 color(fColor.r, fColor.g, fColor.b); hsScalar alpha = fColor.a; plController *colorCtl; // Allow effects a chance to cache any upfront calculations // that will apply to all particles. for (j = 0; j < fSystem->fForces.GetCount(); j++) { fSystem->fForces[j]->PrepareEffect(fTargetInfo); } for (j = 0; j < fSystem->fEffects.GetCount(); j++) { fSystem->fEffects[j]->PrepareEffect(fTargetInfo); } for (j = 0; j < fSystem->fConstraints.GetCount(); j++) { fSystem->fConstraints[j]->PrepareEffect(fTargetInfo); } for (i = 0; i < fNumValidParticles; i++) { if (!( fParticleExts[i].fMiscFlags & plParticleExt::kImmortal )) { hsScalar percent = (1.0f - fParticleExts[i].fLife / fParticleExts[i].fStartLife); colorCtl = (fMiscFlags & kMatIsEmissive ? fSystem->fAmbientCtl : fSystem->fDiffuseCtl); if (colorCtl != nil) colorCtl->Interp(colorCtl->GetLength() * percent, &color); if (fSystem->fOpacityCtl != nil) { fSystem->fOpacityCtl->Interp(fSystem->fOpacityCtl->GetLength() * percent, &alpha); alpha /= 100.0f; if (alpha < 0) alpha = 0; else if (alpha > 1.f) alpha = 1.f; } if (fSystem->fWidthCtl != nil) { fSystem->fWidthCtl->Interp(fSystem->fWidthCtl->GetLength() * percent, &fParticleCores[i].fHSize); fParticleCores[i].fHSize *= fParticleExts[i].fScale; } if (fSystem->fHeightCtl != nil) { fSystem->fHeightCtl->Interp(fSystem->fHeightCtl->GetLength() * percent, &fParticleCores[i].fVSize); fParticleCores[i].fVSize *= fParticleExts[i].fScale; } fParticleCores[i].fColor = CreateHexColor(color.fX, color.fY, color.fZ, alpha); } for (j = 0; j < fSystem->fForces.GetCount(); j++) { fSystem->fForces[j]->ApplyEffect(fTargetInfo, i); } currPos = (hsPoint3 *)(fTargetInfo.fPos + i * fTargetInfo.fPosStride); currVelocity = (hsVector3 *)(fTargetInfo.fVelocity + i * fTargetInfo.fVelocityStride); //currAccel = (hsVector3 *)(fTargetInfo.fAcceleration + i * fTargetInfo.fAccelerationStride); currAccel = &fSystem->fAccel; // Nothing accellerates on a per-particle basis (yet) *currPos += *currVelocity * delta; // This is the only orientation option (so far) that requires an update here if (fMiscFlags & (kOrientationVelocityBased | kOrientationVelocityStretch | kOrientationVelocityFlow)) fParticleCores[i].fOrientation.Set(&(*currVelocity * delta)); // mf - want the orientation to be a delposition else if( fParticleExts[i].fRadsPerSec != 0 ) { hsScalar sinX, cosX; hsFastMath::SinCos(fParticleExts[i].fLife * fParticleExts[i].fRadsPerSec * 2.f * hsScalarPI, sinX, cosX); fParticleCores[i].fOrientation.Set(sinX, -cosX, 0); } // Viscous force F(t) = -k V(t) // Integral S from t0 to t1 of F(t) is // = S(-kV(t))[t1..t0] // = -k(P(t1) - P(t0)) // = -k*(currVelocity * delta) // or // V = V + -k*(V * delta) // V *= (1 + -k * delta) // Giving the change in velocity. hsScalar drag = 1.f + fSystem->fDrag * delta; // Clamp it at 0. Drag should never cause a reversal in velocity direction. if( drag < 0.f ) drag = 0.f; *currVelocity *= drag; *currVelocity += *currAccel * delta; for (j = 0; j < fSystem->fEffects.GetCount(); j++) { fSystem->fEffects[j]->ApplyEffect(fTargetInfo, i); } // We may need to do more than one iteration through the constraints. It's a trade-off // between accurracy and speed (what's new?) but I'm going to go with just one // for now until we decide things don't "look right" for (j = 0; j < fSystem->fConstraints.GetCount(); j++) { if( fSystem->fConstraints[j]->ApplyEffect(fTargetInfo, i) ) { IRemoveParticle(i); i--; // so that we hit this index again on the next iteration break; // break will break us out of loop over constraints, // and since we're last, we move onto next particle. } } } // Notify the effects that they are done for now. for (j = 0; j < fSystem->fForces.GetCount(); j++) { fSystem->fForces[j]->EndEffect(fTargetInfo); } for (j = 0; j < fSystem->fEffects.GetCount(); j++) { fSystem->fEffects[j]->EndEffect(fTargetInfo); } for (j = 0; j < fSystem->fConstraints.GetCount(); j++) { fSystem->fConstraints[j]->EndEffect(fTargetInfo); } } plProfile_CreateTimer("Bound", "Particles", ParticleBound); plProfile_CreateTimer("Normal", "Particles", ParticleNormal); void plParticleEmitter::IUpdateBoundsAndNormals(hsScalar delta) { plProfile_BeginTiming(ParticleBound); fBoundBox.MakeEmpty(); int i; for (i = 0; i < fNumValidParticles; i++) fBoundBox.Union(&fParticleCores[i].fPos); hsPoint3 center; if (fNumValidParticles > 0) center = fBoundBox.GetCenter(); plProfile_EndTiming(ParticleBound); plProfile_BeginTiming(ParticleNormal); hsVector3 normal; if (fMiscFlags & kNormalVelUpVel) { for (i = fNumValidParticles - 1; i >=0; i--) { //currDirection.Set(&fParticleCores[i].fPos, &fParticleExts[i].fOldPos); //normal = (currDirection % up % currDirection); normal.Set(-fParticleExts[i].fVelocity.fX * fParticleExts[i].fVelocity.fZ, -fParticleExts[i].fVelocity.fY * fParticleExts[i].fVelocity.fZ, (fParticleExts[i].fVelocity.fX * fParticleExts[i].fVelocity.fX + fParticleExts[i].fVelocity.fY * fParticleExts[i].fVelocity.fY)); if (!normal.IsEmpty()) // zero length check { normal.Normalize(); fParticleCores[i].fNormal = normal; } } } else if (fMiscFlags & kNormalFromCenter) { for (i = fNumValidParticles - 1; i >=0; i--) { normal.Set(&fParticleCores[i].fPos, ¢er); if (!normal.IsEmpty()) // zero length check { normal.Normalize(); fParticleCores[i].fNormal = normal; } } } // otherwise we just keep the last normal. plProfile_EndTiming(ParticleNormal); } void plParticleEmitter::IRemoveParticle(UInt32 index) { hsAssert(index < fNumValidParticles, "Trying to remove an invalid particle"); fNumValidParticles--; if (fNumValidParticles <= 0) { fNumValidParticles = 0; return; } fParticleCores[index] = fParticleCores[fNumValidParticles]; fParticleExts[index] = fParticleExts[fNumValidParticles]; } // Reading and writing doesn't transfer individual particle info. We assume those are expendable. // Only the configuration info necessary to began spawning particles again is saved. // The particle system that owns this emitter is responsible for setting the pointer back to itself void plParticleEmitter::Read(hsStream *s, hsResMgr *mgr) { plCreatable::Read(s, mgr); fGenerator = plParticleGenerator::ConvertNoRef(mgr->ReadCreatable(s)); fSpanIndex = s->ReadSwap32(); fMaxParticles = s->ReadSwap32(); fMiscFlags = s->ReadSwap32(); fColor.Read(s); if( fMiscFlags & kOnReserve ) fTimeToLive = -1.f; // Wait for someone to give us a spurt of life. ISetupParticleMem(); } void plParticleEmitter::Write(hsStream *s, hsResMgr *mgr) { plCreatable::Write(s, mgr); mgr->WriteCreatable(s, fGenerator); s->WriteSwap32(fSpanIndex); s->WriteSwap32(fMaxParticles); s->WriteSwap32(fMiscFlags); fColor.Write(s); } UInt32 plParticleEmitter::CreateHexColor(const hsColorRGBA &color) { return CreateHexColor(color.r, color.g, color.b, color.a); } UInt32 plParticleEmitter::CreateHexColor(const hsScalar r, const hsScalar g, const hsScalar b, const hsScalar a) { UInt32 ru, gu, bu, au; au = (UInt32)(a * 255.0f); ru = (UInt32)(r * 255.0f); gu = (UInt32)(g * 255.0f); bu = (UInt32)(b * 255.0f); return ( au << 24 ) | ( ru << 16 ) | ( gu << 8 ) | ( bu ); }