/*==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 "plParticleSystem.h"
#include "plParticleEmitter.h"
#include "plParticleGenerator.h"
#include "plParticleEffect.h"
#include "plParticle.h"
#include "plParticleSDLMod.h"
#include "plgDispatch.h"
#include "hsResMgr.h"
#include "../pnSceneObject/plSceneObject.h"
#include "../pnSceneObject/plDrawInterface.h"
#include "../pnSceneObject/plCoordinateInterface.h"
#include "../pnMessage/plTimeMsg.h"
#include "../plMessage/plRenderMsg.h"
#include "../plMessage/plAgeLoadedMsg.h"
#include "../plMessage/plParticleUpdateMsg.h"
#include "../plInterp/plController.h"
#include "../plSurface/hsGMaterial.h"
#include "plPipeline.h"
#include "hsTimer.h"
#include "plProfile.h"
#include "plTweak.h"
#include "../plDrawable/plParticleFiller.h"
plProfile_CreateCounter("Num Particles", "Particles", NumParticles);
const hsScalar plParticleSystem::GRAVITY_ACCEL_FEET_PER_SEC2 = 32.0f;
plParticleSystem::plParticleSystem() : fParticleSDLMod(nil), fAttachedToAvatar(false)
{
}
plParticleSystem::~plParticleSystem()
{
int i;
for (i = 0; i < fNumValidEmitters; i++)
{
delete fEmitters[i];
}
delete [] fEmitters;
delete fAmbientCtl;
delete fDiffuseCtl;
delete fOpacityCtl;
delete fWidthCtl;
delete fHeightCtl;
}
void plParticleSystem::Init(UInt32 xTiles, UInt32 yTiles, UInt32 maxTotalParticles, UInt32 maxEmitters,
plController *ambientCtl, plController *diffuseCtl, plController *opacityCtl,
plController *widthCtl, plController *heightCtl)
{
fTarget = nil;
fLastTime = 0;
fCurrTime = 0;
SetGravity(1.0f);
fDrag = 0;
fWindMult = 1.f;
fNumValidEmitters = 0;
fNextEmitterToGo = 0;
fMiscFlags = 0;
fForces.Reset();
fEffects.Reset();
fConstraints.Reset();
fXTiles = xTiles;
fYTiles = yTiles;
fMaxTotalParticles = fMaxTotalParticlesLeft = maxTotalParticles;
fMaxEmitters = maxEmitters;
fEmitters = TRACKED_NEW plParticleEmitter *[fMaxEmitters];
int i;
for (i = 0; i < maxEmitters; i++)
fEmitters[i] = nil;
fAmbientCtl = ambientCtl;
fDiffuseCtl = diffuseCtl;
fOpacityCtl = opacityCtl;
fWidthCtl = widthCtl;
fHeightCtl = heightCtl;
}
void plParticleSystem::IAddEffect(plParticleEffect *effect, UInt32 type)
{
switch(type)
{
case kEffectForce:
fForces.Append(effect);
break;
case kEffectMisc:
default:
fEffects.Append(effect);
break;
case kEffectConstraint:
fConstraints.Append(effect);
break;
}
}
plParticleEmitter* plParticleSystem::GetAvailEmitter()
{
if( !fNumValidEmitters ) // got to start with at least one.
return nil;
hsScalar minTTL = 1.e33;
int iMinTTL = -1;
int i;
for( i = 0; i < fNumValidEmitters; i++ )
{
if( fEmitters[i]->GetTimeToLive() < minTTL )
{
minTTL = fEmitters[i]->GetTimeToLive();
iMinTTL = i;
}
}
if( minTTL > 0 )
{
if( fNumValidEmitters < fMaxEmitters )
{
minTTL = 0;
iMinTTL = fNumValidEmitters++;
fEmitters[iMinTTL] = TRACKED_NEW plParticleEmitter();
fEmitters[iMinTTL]->Clone(fEmitters[0], iMinTTL);
fMaxTotalParticlesLeft -= fEmitters[iMinTTL]->fMaxParticles;
hsAssert(fMaxTotalParticlesLeft >= 0, "Should have planned better");
// Don't really use this. fEmitters[i]->GetSpanIndex() always == i.
fNextEmitterToGo = (fNextEmitterToGo + 1) % fMaxEmitters;
}
}
return fEmitters[iMinTTL];
}
UInt32 plParticleSystem::AddEmitter(UInt32 maxParticles, plParticleGenerator *gen, UInt32 emitterFlags)
{
if (fMaxEmitters == 0) // silly rabbit, Trix are for kids!
return 0;
UInt32 currEmitter;
if (fNumValidEmitters == fMaxEmitters) // No more free spots, snag the next in line.
{
int i;
for (i = 0; i < fMaxEmitters; i++)
{
if (fEmitters[i]->GetSpanIndex() == fNextEmitterToGo)
break;
}
currEmitter = i;
fMaxTotalParticlesLeft += fEmitters[currEmitter]->fMaxParticles;
hsAssert(fMaxTotalParticlesLeft <= fMaxTotalParticles, "Particle system somehow has more particles than it started with.");
delete fEmitters[currEmitter];
}
else
{
currEmitter = fNumValidEmitters;
fNumValidEmitters++;
}
if (maxParticles > fMaxTotalParticlesLeft)
maxParticles = fMaxTotalParticlesLeft;
if (maxParticles < 0)
maxParticles = 0;
fEmitters[currEmitter] = TRACKED_NEW plParticleEmitter();
fEmitters[currEmitter]->Init(this, maxParticles, fNextEmitterToGo, emitterFlags, gen);
fMaxTotalParticlesLeft -= fEmitters[currEmitter]->fMaxParticles;
fNextEmitterToGo = (fNextEmitterToGo + 1) % fMaxEmitters;
return maxParticles;
}
void plParticleSystem::AddParticle(hsPoint3 &pos, hsVector3 &velocity, UInt32 tileIndex,
hsScalar hSize, hsScalar vSize, hsScalar scale, hsScalar invMass, hsScalar life,
hsPoint3 &orientation, UInt32 miscFlags, hsScalar radsPerSec)
{
hsAssert(fNumValidEmitters > 0, "Trying to explicitly add particles to a system with no valid emitters.");
if (fNumValidEmitters == 0)
return;
fEmitters[0]->AddParticle(pos, velocity, tileIndex, hSize, vSize, scale, invMass, life, orientation, miscFlags, radsPerSec);
}
void plParticleSystem::GenerateParticles(UInt32 num, hsScalar dt /* = 0.f */)
{
if (num <= 0)
return;
if (fNumValidEmitters > 0 && fEmitters[0]->fGenerator)
fEmitters[0]->fGenerator->AddAutoParticles(fEmitters[0], dt, num);
GetTarget(0)->DirtySynchState(kSDLParticleSystem, 0);
}
void plParticleSystem::WipeExistingParticles()
{
int i;
for (i = 0; i < fNumValidEmitters; i++)
fEmitters[i]->WipeExistingParticles();
}
void plParticleSystem::KillParticles(hsScalar num, hsScalar timeToDie, UInt8 flags)
{
if (fEmitters[0])
fEmitters[0]->KillParticles(num, timeToDie, flags);
GetTarget(0)->DirtySynchState(kSDLParticleSystem, 0);
}
void plParticleSystem::TranslateAllParticles(hsPoint3 &amount)
{
int i;
for (i = 0; i < fNumValidEmitters; i++)
fEmitters[i]->TranslateAllParticles(amount);
}
void plParticleSystem::DisableGenerators()
{
int i;
for (i = 0; i < fNumValidEmitters; i++)
fEmitters[i]->UpdateGenerator(plParticleUpdateMsg::kParamEnabled, 0.f);
}
UInt16 plParticleSystem::StealParticlesFrom(plParticleSystem *victim, UInt16 num)
{
if (fNumValidEmitters <= 0)
return 0; // you just lose
if (victim)
{
UInt16 numStolen = fEmitters[0]->StealParticlesFrom(victim->fNumValidEmitters > 0 ? victim->fEmitters[0] : nil, num);
GetTarget(0)->DirtySynchState(kSDLParticleSystem, 0);
victim->GetTarget(0)->DirtySynchState(kSDLParticleSystem, 0);
return numStolen;
}
return 0;
}
plParticleGenerator *plParticleSystem::GetExportedGenerator() const
{
return (fNumValidEmitters > 0 ? fEmitters[0]->fGenerator : nil);
}
plParticleEffect *plParticleSystem::GetEffect(UInt16 type) const
{
int i;
for (i = 0; i < fForces.GetCount(); i++)
if (fForces[i]->ClassIndex() == type)
return fForces[i];
for (i = 0; i < fEffects.GetCount(); i++)
if (fEffects[i]->ClassIndex() == type)
return fEffects[i];
for (i = 0; i < fConstraints.GetCount(); i++)
if (fConstraints[i]->ClassIndex() == type)
return fConstraints[i];
return nil;
}
UInt32 plParticleSystem::GetNumValidParticles(hsBool immortalOnly /* = false */) const
{
UInt32 count = 0;
int i, j;
for (i = 0; i < fNumValidEmitters; i++)
{
if (immortalOnly)
{
for (j = 0; j < fEmitters[i]->fNumValidParticles; j++)
{
if (fEmitters[i]->fParticleExts[j].fMiscFlags & plParticleExt::kImmortal)
count++;
}
}
else
count += fEmitters[i]->fNumValidParticles;
}
return count;
}
const hsMatrix44 &plParticleSystem::GetLocalToWorld() const
{
return fTarget->GetCoordinateInterface()->GetLocalToWorld();
}
hsBool plParticleSystem::IEval(double secs, hsScalar del, UInt32 dirty)
{
return false;
}
hsBool plParticleSystem::IShouldUpdate(plPipeline* pipe) const
{
if (fMiscFlags & kParticleSystemAlwaysUpdate)
return true;
if (IGetTargetDrawInterface(0) && IGetTargetDrawInterface(0)->GetProperty(plDrawInterface::kDisable))
return false;
// First, what are the cumulative bounds for this system.
hsBounds3Ext wBnd;
wBnd.MakeEmpty();
int i;
for( i = 0; i < fNumValidEmitters; i++ )
{
if( fEmitters[i]->GetBoundingBox().GetType() == kBoundsNormal )
wBnd.Union(&fEmitters[i]->GetBoundingBox());
}
// Always update if we are currently empty
if( wBnd.GetType() == kBoundsEmpty )
{
return true;
}
// Now, are we visible?
hsBool isVisible = pipe->TestVisibleWorld(wBnd);
hsScalar delta = fLastTime > 0 ? hsScalar(fCurrTime - fLastTime) : hsTimer::GetDelSysSeconds();
if( isVisible )
{
// If we know how fast the fastest particle is moving, then we can
// decide if the system is too far away to need to update every single frame.
// In fact, based on the speed of the fastest particle, we can determine an
// exact update rate for a given error (say 3 pixels?).
// But we don't currently know the speed of the fastest particle, so
// until we do, I'm commenting this out. Most of the speed gain from
// culling particle updates was from not updating systems that aren't visible
// anyway.
#if 0 // ALWAYS_IF_VISIBLE
// We're in view, but how close are we to the camera? Look at closest point.
hsPoint2 depth;
wBnd.TestPlane(pipe->GetViewDirWorld(), depth);
hsScalar eyeDist = pipe->GetViewDirWorld().InnerProduct(pipe->GetViewPositionWorld());
hsScalar dist = depth.fX - eyeDist;
static hsScalar kUpdateCutoffDist = 100.f;
if( dist > kUpdateCutoffDist )
{
static hsScalar kDistantUpdateSecs = 0.1f;
return delta >= kDistantUpdateSecs;
}
#endif // ALWAYS_IF_VISIBLE
return true;
}
static hsScalar kOffscreenUpdateSecs = 1.f;
return delta >= kOffscreenUpdateSecs;
}
plDrawInterface* plParticleSystem::ICheckDrawInterface()
{
plDrawInterface* di = IGetTargetDrawInterface(0);
if( !di )
return nil;
if( di->GetDrawableMeshIndex(0) == UInt32(-1) )
{
di->SetUpForParticleSystem( fMaxEmitters + 1, fMaxTotalParticles, fTexture, fPermaLights );
hsAssert(di->GetDrawableMeshIndex( 0 ) != (UInt32)-1, "SetUpForParticleSystem should never fail"); // still invalid, didn't fix it.
}
return di;
}
void plParticleSystem::IHandleRenderMsg(plPipeline* pipe)
{
fCurrTime = hsTimer::GetSysSeconds();
hsScalar delta = hsScalar(fCurrTime - fLastTime);
if (delta == 0)
return;
plConst(hsScalar) kMaxDelta(0.3f);
if( delta > kMaxDelta )
delta = kMaxDelta;
plDrawInterface* di = ICheckDrawInterface();
if( !di )
return;
hsBool disabled = di->GetProperty(plDrawInterface::kDisable);
if (!IShouldUpdate(pipe))
{
if (disabled)
di->ResetParticleSystem(); // Need to call this, otherwise particles get drawn, even though the DI is disabled.
// (Yes, it's lame.)
// Otherwise, we leave the DI alone, and the particles draw in the same place they were last frame.
return;
}
fContext.fPipeline = pipe;
fContext.fSystem = this;
fContext.fSecs = fCurrTime;
fContext.fDelSecs = delta;
di->ResetParticleSystem();
if (fPreSim > 0)
IPreSim();
int i;
for (i = 0; i < fNumValidEmitters; i++)
{
fEmitters[i]->IUpdate(delta);
plProfile_IncCount(NumParticles, fEmitters[i]->fNumValidParticles);
if (!disabled)
{
if( fEmitters[ i ]->GetParticleCount() > 0 )
di->AssignEmitterToParticleSystem( fEmitters[ i ] ); // Go make those polys!
}
}
// plParticleFiller::FillParticlePolys(pipe, di);
fLastTime = fCurrTime;
}
#include "plProfile.h"
plProfile_CreateTimer("ParticleSys", "RenderSetup", ParticleSys);
hsBool plParticleSystem::MsgReceive(plMessage* msg)
{
plGenRefMsg* refMsg;
plParticleUpdateMsg *partMsg;
plParticleKillMsg *killMsg;
plSceneObject *scene;
plParticleEffect *effect;
hsGMaterial *mat;
plRenderMsg *rend;
plAgeLoadedMsg* ageLoaded;
if (rend = plRenderMsg::ConvertNoRef(msg))
{
plProfile_BeginLap(ParticleSys, this->GetKey()->GetUoid().GetObjectName());
IHandleRenderMsg(rend->Pipeline());
plProfile_EndLap(ParticleSys, this->GetKey()->GetUoid().GetObjectName());
return true;
}
else if (refMsg = plGenRefMsg::ConvertNoRef(msg))
{
if (scene = plSceneObject::ConvertNoRef(refMsg->GetRef()))
{
if (refMsg->GetContext() & (plRefMsg::kOnCreate|plRefMsg::kOnRequest|plRefMsg::kOnReplace))
AddTarget(scene);
else
RemoveTarget(scene);
return true;
}
if (mat = hsGMaterial::ConvertNoRef(refMsg->GetRef()))
{
if (refMsg->GetContext() & (plRefMsg::kOnCreate|plRefMsg::kOnRequest|plRefMsg::kOnReplace))
fTexture = mat;
else
fTexture = nil;
return true;
}
if (effect = plParticleEffect::ConvertNoRef(refMsg->GetRef()))
{
if (refMsg->GetContext() & (plRefMsg::kOnCreate|plRefMsg::kOnRequest|plRefMsg::kOnReplace))
IAddEffect(effect, refMsg->fType);
//else
// IRemoveEffect(effect, refMsg->fType);
return true;
}
}
else if (partMsg = plParticleUpdateMsg::ConvertNoRef(msg))
{
UpdateGenerator(partMsg->GetParamID(), partMsg->GetParamValue());
return true;
}
else if (killMsg = plParticleKillMsg::ConvertNoRef(msg))
{
KillParticles(killMsg->fNumToKill, killMsg->fTimeLeft, killMsg->fFlags);
return true;
}
else if( (ageLoaded = plAgeLoadedMsg::ConvertNoRef(msg)) && ageLoaded->fLoaded )
{
ICheckDrawInterface();
return true;
}
return plModifier::MsgReceive(msg);
}
void plParticleSystem::UpdateGenerator(UInt32 paramID, hsScalar value)
{
int i;
for (i = 0; i < fNumValidEmitters; i++)
fEmitters[i]->UpdateGenerator(paramID, value);
}
void plParticleSystem::AddTarget(plSceneObject *so)
{
if (fTarget != nil)
RemoveTarget(fTarget);
fTarget = so;
plgDispatch::Dispatch()->RegisterForExactType(plTimeMsg::Index(), GetKey());
plgDispatch::Dispatch()->RegisterForExactType(plRenderMsg::Index(), GetKey());
plgDispatch::Dispatch()->RegisterForExactType(plAgeLoadedMsg::Index(), GetKey());
delete fParticleSDLMod;
fParticleSDLMod = TRACKED_NEW plParticleSDLMod;
fParticleSDLMod->SetAttachedToAvatar(fAttachedToAvatar);
so->AddModifier(fParticleSDLMod);
}
void plParticleSystem::RemoveTarget(plSceneObject *so)
{
if (so == fTarget && so != nil)
{
if (fParticleSDLMod)
{
so->RemoveModifier(fParticleSDLMod);
delete fParticleSDLMod;
fParticleSDLMod = nil;
}
fTarget = nil;
}
}
// This can be done much faster, but it's only done on load, and very very clean as is. Saving optimization for
// when we observe that it's too slow.
void plParticleSystem::IPreSim()
{
const double PRESIM_UPDATE_TICK = 0.1;
int i;
for (i = 0; i < fNumValidEmitters; i++)
{
double secs = fPreSim;
while (secs > 0)
{
fEmitters[i]->IUpdateParticles((hsScalar)PRESIM_UPDATE_TICK);
secs -= PRESIM_UPDATE_TICK;
}
}
fPreSim = 0;
}
void plParticleSystem::IReadEffectsArray(hsTArray &effects, UInt32 type, hsStream *s, hsResMgr *mgr)
{
plGenRefMsg *msg;
effects.Reset();
UInt32 count = s->ReadSwap32();
int i;
for (i = 0; i < count; i++)
{
msg = TRACKED_NEW plGenRefMsg(GetKey(), plRefMsg::kOnCreate, 0, (Int8)type);
mgr->ReadKeyNotifyMe(s, msg, plRefFlags::kActiveRef);
}
}
void plParticleSystem::Read(hsStream *s, hsResMgr *mgr)
{
plModifier::Read(s, mgr);
plGenRefMsg* msg;
msg = TRACKED_NEW plGenRefMsg(GetKey(), plRefMsg::kOnCreate, 0, 0); // Material
mgr->ReadKeyNotifyMe(s, msg, plRefFlags::kActiveRef);
fAmbientCtl = plController::ConvertNoRef(mgr->ReadCreatable(s));
fDiffuseCtl = plController::ConvertNoRef(mgr->ReadCreatable(s));
fOpacityCtl = plController::ConvertNoRef(mgr->ReadCreatable(s));
fWidthCtl = plController::ConvertNoRef(mgr->ReadCreatable(s));
fHeightCtl = plController::ConvertNoRef(mgr->ReadCreatable(s));
UInt32 xTiles = s->ReadSwap32();
UInt32 yTiles = s->ReadSwap32();
UInt32 maxTotal = s->ReadSwap32();
UInt32 maxEmitters = s->ReadSwap32();
Init(xTiles, yTiles, maxTotal, maxEmitters, fAmbientCtl, fDiffuseCtl, fOpacityCtl, fWidthCtl, fHeightCtl);
fPreSim = s->ReadSwapScalar();
fAccel.Read(s);
fDrag = s->ReadSwapScalar();
fWindMult = s->ReadSwapScalar();
fNumValidEmitters = s->ReadSwap32();
int i;
for (i = 0; i < fNumValidEmitters; i++)
{
fEmitters[i] = plParticleEmitter::ConvertNoRef(mgr->ReadCreatable(s));
fEmitters[i]->ISetSystem(this);
}
IReadEffectsArray(fForces, kEffectForce, s, mgr);
IReadEffectsArray(fEffects, kEffectMisc, s, mgr);
IReadEffectsArray(fConstraints, kEffectConstraint, s, mgr);
int count = s->ReadSwap32();
fPermaLights.SetCount(count);
for( i = 0; i < count; i++ )
{
fPermaLights[i] = mgr->ReadKey(s);
}
}
void plParticleSystem::Write(hsStream *s, hsResMgr *mgr)
{
plModifier::Write(s, mgr);
int i;
mgr->WriteKey(s, fTexture);
// Arguments to the Init() function
mgr->WriteCreatable(s, fAmbientCtl);
mgr->WriteCreatable(s, fDiffuseCtl);
mgr->WriteCreatable(s, fOpacityCtl);
mgr->WriteCreatable(s, fWidthCtl);
mgr->WriteCreatable(s, fHeightCtl);
s->WriteSwap32(fXTiles);
s->WriteSwap32(fYTiles);
s->WriteSwap32(fMaxTotalParticles);
s->WriteSwap32(fMaxEmitters);
s->WriteSwapScalar(fPreSim);
fAccel.Write(s);
s->WriteSwapScalar(fDrag);
s->WriteSwapScalar(fWindMult);
s->WriteSwap32(fNumValidEmitters);
for (i = 0; i < fNumValidEmitters; i++)
{
mgr->WriteCreatable(s, fEmitters[i]);
}
int count;
count = fForces.GetCount();
s->WriteSwap32(count);
for (i = 0; i < count; i++)
mgr->WriteKey(s, fForces.Get(i));
count = fEffects.GetCount();
s->WriteSwap32(count);
for (i = 0; i < count; i++)
mgr->WriteKey(s, fEffects.Get(i));
count = fConstraints.GetCount();
s->WriteSwap32(count);
for (i = 0; i < count; i++)
mgr->WriteKey(s, fConstraints.Get(i));
count = fPermaLights.GetCount();
s->WriteSwap32(count);
for( i = 0; i < count; i++ )
mgr->WriteKey(s, fPermaLights[i]);
}
void plParticleSystem::SetAttachedToAvatar(bool attached)
{
fAttachedToAvatar = attached;
if (fParticleSDLMod)
fParticleSDLMod->SetAttachedToAvatar(attached);
}
void plParticleSystem::AddLight(plKey liKey)
{
fPermaLights.Append(liKey);
}