Various TSE Particle Engine Enhancements
by Max Robinson · 09/05/2006 (11:21 pm) · 14 comments
Download Code File
5/15/07 Update: This code is not up to date for TSE 1.01. I'll be downloading it and merging sometime soon -ish though. In the meantime, you will have to merge it manually. Don't overwrite!
Overview
Attached is a .rar file containing modified versions of the following files:
particleEmitter.cpp
particleEmitter.h
particle.cpp
particle.h
particleEmitterNode.cpp
These files have been updated to work with TSE4.0. At the time of this writing, there are no "omissions" due to changes done by GG on the particle engine, so you can count them as up to date.
NOTE: These changes change the emitParticles functions. Every instance in normal TSE will no longer work with these until you fix them. I would comment out any you don't plan on using soon. For those that you do need working, just read the section at the end. If you find a case I don't have an example for, feel free to comment, I'll see if I can add it in.
The changes I have made are as follows:
* particles are spawned at a transform matrix instead of a point
* initialParticles - allows you to create a particle "burst"
* 3-axis (x,y,z) ejection, offset and variance for both - lets you create square, cubic, rectangular emitters and more
* particle decay - % chance for a particle to spawn another particle at % lifetime
* multiple particles per emitter with weighting - lets you cram more particles on one emitter
* randomized per-particle spin base - fixes a visual quirk that always bothered me
Negatives of these changes:
* The particle structure now has an extra bool and F32 value.
* The datablocks are going to be a bit larger.
* There is an emission style that was used by the explosion class that is no longer available. I thought it was redundant
One issue to think about which is caused by these changes:
* You have to pass transforms to spawn particles. In some cases this is faster, for instance, when spawning particles "at" a node on a vehicle or player. In some cases, like spawning at a projectile, you will need to synthesize a matrix based on velocity and position, which is obviously going to be more work for you and the computer (though not much). A workaround would be possible, but it would require writing alternate versions of 2 large and intrinsic functions to the emitter class, which I considered a potential maintenance problem.
Overall I think these changes make it easier to create good looking particle emitters.
Here is a demo video of explosions I created using self-made textures and datablocks:
www.echelon5.net/max/talon_explosions_tse3_XVID.avi
Fixing ->emitParticles instances
For explosions, here is a version of updateEmitters which works for me. Essentially, you are passing the explosion's transform, but you flip the y & z axes because the emitters like to point "up" not "forward".
For projectiles, debris, and such, you need to create 2 matrices which can be passed on. Here is a version of Projectile::emitParticles which works for me:
For vehicle emitters, and any node-centered emitter on an object with the getVelocity() function, it is simple. You merely need to pass on the node transform. Here are some lines from a modified flyingVehicle file I use. You will need to tailor this to work with whatever you are using it on.
The particleEmitterNode is sort of like the explosion class crossed with the node example - essentially: get transform, flip axes, pass along with velocity. I've included a version of that class' .cpp file since I doubt most people bother changing it. You can go ahead and use that.
I have these files up and running on 2 branches of TSE and there aren't any obvious issues, but I havn't tested it as much with TSE3.5 and it's possible that in some modified versions of TSE there will be trouble, or that I've typed up something wrong here. And of course, comments, critiques, suggestions, and additions are very welcome.
If you are a ninja with DX9 rendering, I'd love for you to look at how I have it render the multiple particleDatas. I'm curious if there are better ways to do sorting, or if there is a way to utilize shaders and materials to broaden the particle engine capability even more.
Datablock Examples
Here is one of the fire + smoke particle emitters and particle data that is used for the missiles in the video. This uses initialParticles, multiple particleDatas on the emitter, and decay. The timings & sizes are all carefully tweaked, but I've replaced the textures with the demo folder ones, so it might look kind of crappy. If you have any nice explosion-ey fire/smoke textures, try it with those :)
And here are the datablocks which give you the mushroom cloud shaped stuff from the video. I've left out the boring extra stuff, and again, I'm pointing these to the demo folder textures, so it won't look the same.
Notice how the ExplosionFire particledata has gravityCoefficient = 0 but in one emitter I make it go up and in the other it just goes out. Emitters you'll need to include in the explosion: Example2Cloud, Example2Ring, Example2Updraft
Other usage things you will want to know:
Obviously, to put multiple particleDatas on one emitter you use particles[0], particles[1], etc.
I have it using a static array for those, so there is a limit. I have that limit set at 4. You can, of course, change it. (For reference: PEDC_MAX_PARTICLEDATAS in particleEmitter.h)
For weights, you need to put "particleWeight[idx]" where idx is the index in the datablock. The default weight is 1. To get a figure for how often it will be emitted, add up all the weights and think of it as " out of ." So if you have 3 datas with weight 1 each, its 1 in 3 chance for each. If you make one of those 2, it'll be 2 out of 4 for the 2 one, and the others will be 1 out of 4. It's pretty simple really.
Just FYI, I have it (on the client only) normalize and properly distribute them all across the range of 0-1 after loading, and it picks by generating a random 0-1 float. The results seem to match up with the datablock numbers.
5/15/07 Update: This code is not up to date for TSE 1.01. I'll be downloading it and merging sometime soon -ish though. In the meantime, you will have to merge it manually. Don't overwrite!
Overview
Attached is a .rar file containing modified versions of the following files:
particleEmitter.cpp
particleEmitter.h
particle.cpp
particle.h
particleEmitterNode.cpp
These files have been updated to work with TSE4.0. At the time of this writing, there are no "omissions" due to changes done by GG on the particle engine, so you can count them as up to date.
NOTE: These changes change the emitParticles functions. Every instance in normal TSE will no longer work with these until you fix them. I would comment out any you don't plan on using soon. For those that you do need working, just read the section at the end. If you find a case I don't have an example for, feel free to comment, I'll see if I can add it in.
The changes I have made are as follows:
* particles are spawned at a transform matrix instead of a point
* initialParticles - allows you to create a particle "burst"
* 3-axis (x,y,z) ejection, offset and variance for both - lets you create square, cubic, rectangular emitters and more
* particle decay - % chance for a particle to spawn another particle at % lifetime
* multiple particles per emitter with weighting - lets you cram more particles on one emitter
* randomized per-particle spin base - fixes a visual quirk that always bothered me
Negatives of these changes:
* The particle structure now has an extra bool and F32 value.
* The datablocks are going to be a bit larger.
* There is an emission style that was used by the explosion class that is no longer available. I thought it was redundant
One issue to think about which is caused by these changes:
* You have to pass transforms to spawn particles. In some cases this is faster, for instance, when spawning particles "at" a node on a vehicle or player. In some cases, like spawning at a projectile, you will need to synthesize a matrix based on velocity and position, which is obviously going to be more work for you and the computer (though not much). A workaround would be possible, but it would require writing alternate versions of 2 large and intrinsic functions to the emitter class, which I considered a potential maintenance problem.
Overall I think these changes make it easier to create good looking particle emitters.
Here is a demo video of explosions I created using self-made textures and datablocks:
www.echelon5.net/max/talon_explosions_tse3_XVID.avi
Fixing ->emitParticles instances
For explosions, here is a version of updateEmitters which works for me. Essentially, you are passing the explosion's transform, but you flip the y & z axes because the emitters like to point "up" not "forward".
void Explosion::updateEmitters( F32 dt )
{
Point3F pos = getPosition();
for( int i=0; i<ExplosionData::EC_NUM_EMITTERS; i++ )
{
if( mEmitterList[i] )
{
// flip these around
MatrixF mat = getTransform();
Point3F y,z;
mat.getColumn(1,&y);
mat.getColumn(2,&z);
mat.setColumn(1,z);
mat.setColumn(2,y);
// particle emits along y, not z
mEmitterList[i]->emitParticles(mat , mat, Point3F( 0.0, 0.0, 0.0 ), (U32)(dt * 1000));
}
}
}For projectiles, debris, and such, you need to create 2 matrices which can be passed on. Here is a version of Projectile::emitParticles which works for me:
void Projectile::emitParticles(const Point3F& from, const Point3F& to, const Point3F& vel, const U32 ms)
{
if( mDead )
return;
Point3F axis = vel;
axis.normalize();
Point3F axisZ(axis.x,axis.y,0.0f);
if(axisZ.isZero())
axisZ.set(1.0,0.0,0.0);
else
axisZ.normalize();
Point3F axisX;
mCross(axis,axisZ,&axisX);
mCross(axis,axisX,&axisZ);
MatrixF fromMat;
MatrixF toMat;
fromMat.setColumn(0,axisX);
fromMat.setColumn(1,axis);
fromMat.setColumn(2,axisZ);
fromMat.setColumn(3,from);
toMat = fromMat;
toMat.setColumn(3,to);
if (bool(mParticleEmitter))
mParticleEmitter->emitParticles(fromMat, toMat, vel, ms);
}For vehicle emitters, and any node-centered emitter on an object with the getVelocity() function, it is simple. You merely need to pass on the node transform. Here are some lines from a modified flyingVehicle file I use. You will need to tailor this to work with whatever you are using it on.
MatrixF mat; mat.mul(getRenderTransform(),mShapeInstance->mNodeTransforms[mDataBlock->jetNode[FlyingVehicleData::UpwardJetNode + i]]); mHoverEmitter[i]->emitParticles(mat,true,getVelocity(),(U32)(dt * 1000));
The particleEmitterNode is sort of like the explosion class crossed with the node example - essentially: get transform, flip axes, pass along with velocity. I've included a version of that class' .cpp file since I doubt most people bother changing it. You can go ahead and use that.
I have these files up and running on 2 branches of TSE and there aren't any obvious issues, but I havn't tested it as much with TSE3.5 and it's possible that in some modified versions of TSE there will be trouble, or that I've typed up something wrong here. And of course, comments, critiques, suggestions, and additions are very welcome.
If you are a ninja with DX9 rendering, I'd love for you to look at how I have it render the multiple particleDatas. I'm curious if there are better ways to do sorting, or if there is a way to utilize shaders and materials to broaden the particle engine capability even more.
Datablock Examples
Here is one of the fire + smoke particle emitters and particle data that is used for the missiles in the video. This uses initialParticles, multiple particleDatas on the emitter, and decay. The timings & sizes are all carefully tweaked, but I've replaced the textures with the demo folder ones, so it might look kind of crappy. If you have any nice explosion-ey fire/smoke textures, try it with those :)
datablock ParticleData(ExplosionSmoke)
{
dragCoefficient = 0.95;
windCoefficientMin = 0.4;
windCoefficientMax = 0.7;
gravityCoefficient = -0.28;
inheritedVelFactor = 0.65;
lifetimeMS = 2300;
lifetimeVarianceMS = 700;
useInvAlpha = true;
spinRandomMin = -100.0;
spinRandomMax = 100.0;
textureName = "demo/data/shapes/particles/smoke";
colors[0] = "1.0 1.0 0.9 0.1";
colors[1] = "0.8 0.8 0.8 0.28";
colors[2] = "0.4 0.4 0.4 0.12";
colors[3] = "0.4 0.4 0.4 0.0";
sizes[0] = 12;
sizes[1] = 16;
sizes[2] = 17;
sizes[3] = 20;
times[0] = 0.0;
times[1] = 0.14;
times[2] = 0.32;
times[3] = 1.0;
};
datablock ParticleData(ExplosionFire1)
{
dragCoefficient = 0.9;
gravityCoefficient = 0.0;
windCoefficient = 0.0;
spinRandomMin = -65.0;
spinRandomMax = 65.0;
inheritedVelFactor = 0.2;
constantAcceleration = 0.0;
lifetimeMS = 615;
lifetimeVarianceMS = 185;
textureName = "demo/data/shapes/particles/fire";
useInvAlpha = false;
colors[0] = "1.00 1.00 0.9 0.3";
colors[1] = "1.00 0.74 0.3 0.72";
colors[2] = "1.00 0.22 0.0 0.17";
colors[3] = "1.00 0.22 0.0 0.0";
sizes[0] = 4.7;
sizes[1] = 5.9;
sizes[2] = 7.35;
sizes[3] = 9.6;
times[0] = 0.0;
times[1] = 0.3;
times[2] = 0.6;
times[3] = 1.0;
useTimeDecay = true;
timeDecayParticle = ExplosionSmoke;
timeDecayTimePct = 0.68;
timeDecayProbability = 0.5;
};
datablock ParticleData(ExplosionFire2 : ExplosionFire1)
{
textureName = "demo/data/shapes/particles/spark";
};
datablock ParticleEmitterData(ExplosionFireEmitter)
{
initialParticles = 52;
ejectionPeriodMS = 0;
periodVarianceMS = 0;
ejectionOffset = 0.0;
ejectionVelocity = 20.0;
velocityVariance = 14.0;
thetaMin = 0.0;
thetaMax = 76.0;
lifetimeMS = 5000;
particles[0] = ExplosionFire1;
particles[1] = ExplosionFire2;
};And here are the datablocks which give you the mushroom cloud shaped stuff from the video. I've left out the boring extra stuff, and again, I'm pointing these to the demo folder textures, so it won't look the same.
Notice how the ExplosionFire particledata has gravityCoefficient = 0 but in one emitter I make it go up and in the other it just goes out. Emitters you'll need to include in the explosion: Example2Cloud, Example2Ring, Example2Updraft
datablock ParticleData(Example2CloudSmoke)
{
dragCoefficient = 0.02;
gravityCoefficient = -0.66;
inheritedVelFactor = 1.0;
lifetimeMS = 2000;
lifetimeVarianceMS = 400;
useInvAlpha = true;
spinRandomMin = -60.0;
spinRandomMax = 60.0;
textureName = "demo/data/shapes/particles/smoke";
colors[0] = "0.55 1.00 0.55 0.0";
colors[1] = "0.30 0.70 0.45 0.22";
colors[2] = "0.05 0.40 0.10 0.22";
colors[3] = "0.05 0.10 0.05 0.0";
sizes[0] = 11.77;
sizes[1] = 12.2;
sizes[2] = 13.4;
sizes[3] = 14.6;
times[0] = 0.0;
times[1] = 0.1;
times[2] = 0.4;
times[3] = 1.0;
};
datablock ParticleData(Example2CloudFire)
{
dragCoefficient = 0.0;
gravityCoefficient = -0.44;
inheritedVelFactor = 0.0;
lifetimeMS = 1380;
lifetimeVarianceMS = 480;
textureName = "demo/data/shapes/particles/fire";
useInvAlpha = false;
spinRandomMin = -220.0;
spinRandomMax = 220.0;
colors[0] = "1.00 1.00 1.00 0.8";
colors[1] = "0.65 1.00 0.70 0.5";
colors[2] = "0.52 1.00 0.60 0.15";
colors[3] = "0.35 1.00 0.50 0.0";
sizes[0] = 4.0;
sizes[1] = 7.5;
sizes[2] = 10.5;
sizes[3] = 13.0;
times[0] = 0.0;
times[1] = 0.333;
times[2] = 0.666;
times[3] = 1.0;
useTimeDecay = true;
timeDecayParticle = Example2CloudSmoke;
timeDecayTimePct = 0.3;
timeDecayProbability = 0.5;
};
datablock ParticleEmitterData(Example2Cloud)
{
initialParticles = 70;
ejectionPeriodMS = 0;
ejectionOffset = 2;
offsetVairance = 1.6;
ejectionVelocity = 3;
ejectionVelocity3D = "0 8 0";
thetaMin = 0.0;
thetaMax = 180.0;
lifetimeMS = 6000;
particles = Example2CloudFire;
};
datablock ParticleData(Example2ExplosionSmoke)
{
dragCoefficient = 0.02;
gravityCoefficient = 0;
inheritedVelFactor = 1.0;
lifetimeMS = 1800;
lifetimeVarianceMS = 300;
useInvAlpha = true;
spinRandomMin = -100.0;
spinRandomMax = 100.0;
textureName = "demo/data/shapes/particles/smoke";
colors[0] = "0.55 1.00 0.55 0.0";
colors[1] = "0.30 0.70 0.45 0.22";
colors[2] = "0.05 0.40 0.10 0.22";
colors[3] = "0.05 0.10 0.05 0.0";
sizes[0] = 6.8;
sizes[1] = 7.1;
sizes[2] = 7.7;
sizes[3] = 8.3;
times[0] = 0.0;
times[1] = 0.1;
times[2] = 0.4;
times[3] = 1.0;
};
datablock ParticleData(Example2ExplosionFire)
{
dragCoefficient = 0;
gravityCoefficient = 0;
inheritedVelFactor = 0;
lifetimeMS = 850;
lifetimeVarianceMS = 550;
textureName = "demo/data/shapes/particles/fire";
useInvAlpha = false;
spinRandomMin = -400.0;
spinRandomMax = 400.0;
colors[0] = "1.00 1.00 1.00 0.25";
colors[1] = "0.65 1.00 0.70 0.4";
colors[2] = "0.52 1.00 0.60 0.15";
colors[3] = "0.35 1.00 0.50 0.0";
sizes[0] = 2.4;
sizes[1] = 5.5;
sizes[2] = 7.5;
sizes[3] = 8.8;
times[0] = 0.0;
times[1] = 0.333;
times[2] = 0.666;
times[3] = 1.0;
useTimeDecay = true;
timeDecayParticle = Example2ExplosionSmoke;
timeDecayTimePct = 0.3;
timeDecayProbability = 0.25;
};
datablock ParticleEmitterData(Example2Updraft)
{
ejectionPeriodMS = 8;
ejectionVelocity3d = "0 4.5 0";
ejectionOffset = 7.2;
offsetVariance = 4.4;
ejectionVelocity = -3.4;
velocityVariance = 2.2;
thetaMin = 90.0;
thetaMax = 90.0;
lifetimeMS = 900;
particles = Example2ExplosionFire;
};
datablock ParticleEmitterData(Example2Ring)
{
initialParticles = 80;
ejectionPeriodMS = 3;
ejectionOffset = 5.2;
offsetVariance = 1.6;
ejectionVelocity = 8;
velocityVariance = 1.6;
thetaMin = 90.0;
thetaMax = 90.0;
lifetimeMS = 200;
particles = Example2ExplosionFire;
};Other usage things you will want to know:
Obviously, to put multiple particleDatas on one emitter you use particles[0], particles[1], etc.
I have it using a static array for those, so there is a limit. I have that limit set at 4. You can, of course, change it. (For reference: PEDC_MAX_PARTICLEDATAS in particleEmitter.h)
For weights, you need to put "particleWeight[idx]" where idx is the index in the datablock. The default weight is 1. To get a figure for how often it will be emitted, add up all the weights and think of it as "
Just FYI, I have it (on the client only) normalize and properly distribute them all across the range of 0-1 after loading, and it picks by generating a random 0-1 float. The results seem to match up with the datablock numbers.
About the author
#2
08/20/2006 (2:43 pm)
Ishbuu that's a great idea. I'll try and round up some good examples a little bit later.
#3
And yes, some Examples would be great!
08/20/2006 (5:19 pm)
Very nice resource. A bit sketchy on some of the other emitParticles, but since they aren't all working or re-setup, commenting out is easy enough till its all fixed one day.And yes, some Examples would be great!
#4
08/20/2006 (9:47 pm)
Jeremiah, which situations have I skipped? I probably have it somewhere and forgot about it :)
#5
Its pretty easy to make most of this right. For instance Point3F pos = getPosition(); becomes MatrixF mat = getTransform(); Then pass the matrix to the particle function. You also have to remove the axis thing, it no longer has 5 arguments since it takes more data into the first (the third argument should be removed from every instance)
[Ishbuu]
08/21/2006 (7:32 am)
Max: everything and anything else that has to do with particles. Shapeimage, player, explosion, hover vehicle, flying vehicle, wheeled vehicle, vehicle, debris. Correct me if I missed something ;)Its pretty easy to make most of this right. For instance Point3F pos = getPosition(); becomes MatrixF mat = getTransform(); Then pass the matrix to the particle function. You also have to remove the axis thing, it no longer has 5 arguments since it takes more data into the first (the third argument should be removed from every instance)
[Ishbuu]
#7
09/06/2006 (5:45 am)
Nice video
#8
09/30/2006 (2:01 pm)
Updated to TSE 4.0
#9
Could you expand a little on how you created the shockwave effect in some of the explosions.
Thanks for posting this resource!
Todd
02/01/2007 (11:08 am)
Hey Max,Could you expand a little on how you created the shockwave effect in some of the explosions.
Thanks for posting this resource!
Todd
#10
It's a project I worked on. It's basically just a new object class that renders a sphere with keyframed sizes and a shader, and it supplies the effect's lifetime as a float to the shader. The shader is pretty much just a pixel refract effect that fades out near the end of the lifetime. I was trying to do an effect like the ones you see in FEAR or other recent shooters, but I never got it quite right. In fact, the shockwave is hard to see from various angles, and thus I've never considered the object finished. It's due to texture wrapping/rendering issue (its rendering a sphere). I never fixed it, mostly because fixing it was becomming a pain :)
02/02/2007 (11:37 pm)
Are you referring to the pulse/ripple effect?It's a project I worked on. It's basically just a new object class that renders a sphere with keyframed sizes and a shader, and it supplies the effect's lifetime as a float to the shader. The shader is pretty much just a pixel refract effect that fades out near the end of the lifetime. I was trying to do an effect like the ones you see in FEAR or other recent shooters, but I never got it quite right. In fact, the shockwave is hard to see from various angles, and thus I've never considered the object finished. It's due to texture wrapping/rendering issue (its rendering a sphere). I never fixed it, mostly because fixing it was becomming a pain :)
#11
Yes, that is the effect I was asking about. Thanks for explaining what you did... I think it looks really good on some of those explosions!
Todd
02/03/2007 (8:21 am)
Hi Max,Yes, that is the effect I was asking about. Thanks for explaining what you did... I think it looks really good on some of those explosions!
Todd
#12
error C2661: 'ParticleEmitter::emitParticles' : no overloaded function takes 5 arguments
any ideas on how to fix this??
05/16/2007 (4:11 pm)
Well for some reason, when I compile I get this error:error C2661: 'ParticleEmitter::emitParticles' : no overloaded function takes 5 arguments
any ideas on how to fix this??
#13
07/03/2007 (5:27 pm)
You've got some object that wants to use one of the old methods for particle spawning. It had it's uses, but the initial particles feature I added basically replicates that use. It was only used by a rarely used feature of the explosion class, so I removed it. You'll need to use common sense to "fix" it, just use one of the other methods which still exist in the class.
#14
02/15/2008 (2:29 pm)
Any chance we could get some of this ported to TGE? I'm interested in the particle decay especially. =P 
Torque Owner Ishbuu
Any chance we could grab some datablock examples? Would be really cool :)
[Ishbuu]