Game Development Community

dev|Pro Game Development Curriculum

Particle Alignment

by Adam Larson · 08/29/2008 (7:18 am) · 14 comments

Download Code File

The particle alignment resource allows the user to specify an alignment axis for particles in a particle emitter datablock. Currently the particle engine supports billboarding particles so they always face the camera, or aligning the particles so that they face along the velocity axis. With this, particles can be aligned to face along an absolute vector.

A simple usage example would be creating simple wave particles emitting from the player when running through shallow water. The particles would have an alignment axis of 0,1,0 (straight up) so they would look like they were rendering on the surface of the water.

There are two fields added to the particle emitter datablock: alignParticles - which should be set to true to enable absolute axis alignment; and alignDirection - which is the vector along which each particle should face.

The changes:

In particleEngine.h:

Line 178 (at the end of the ParticleEmitterData class definition)
// --------------------------------------------------------------------------
   // Particle Alignment Resource
   bool alignParticles;                      ///< Particles always face along a particular axis
   Point3F alignDirection;                   ///< The direction aligned particles should face
   // --------------------------------------------------------------------------
Line 284 (in the protected section of the ParticleEmitter class definition)
// --------------------------------------------------------------------------
   // Particle Alignment Resource
   /// Renders a particle which will always orient along a specified axis.
   /// @parma part Particle
   void renderAlignedParticle( const Particle &#8706; );
   // --------------------------------------------------------------------------
In particleEngine.cpp:

Line 94 (at the end of the ParticleEmitterData constructor)
// --------------------------------------------------------------------------
   // Particle Alignment Resource
   alignParticles = false;
   alignDirection = Point3F(0.0f, 1.0f, 0.0f);
   // --------------------------------------------------------------------------
Line 121 (at the end of the ParticleEmitterData::initPersistFields method)
// --------------------------------------------------------------------------
   // Particle Alignment Resource
   addField("alignParticles",       TypeBool,    Offset(alignParticles,     ParticleEmitterData));
   addField("alignDirection",       TypePoint3F, Offset(alignDirection,    ParticleEmitterData));
   // --------------------------------------------------------------------------
Line 158 (at the end of the ParticleEmitterData::packData method)
// --------------------------------------------------------------------------
   // Particle Alignment Resource
   if (stream->writeFlag(alignParticles))
   {
      stream->write(alignDirection.x);
      stream->write(alignDirection.y);
      stream->write(alignDirection.z);
   }
   // --------------------------------------------------------------------------
Line 198 (at the end of the ParticleEmitterData::unpackData method)
// --------------------------------------------------------------------------
   // Particle Alignment Resource
   alignParticles = stream->readFlag();
   if (alignParticles)
   {
      stream->read(&alignDirection.x);
      stream->read(&alignDirection.y);
      stream->read(&alignDirection.z);
   }
   // --------------------------------------------------------------------------
Line 957 (after the if( mDataBlock->orientParticles block))
// --------------------------------------------------------------------------
      // Particle Alignment Resource
      else if ( mDataBlock->alignParticles )
      {
         renderAlignedParticle( *particle );
      }
      // --------------------------------------------------------------------------
Line 1121 (after the renderBillboardParticle method)
// --------------------------------------------------------------------------
// Particle Alignment Resource
inline void ParticleEmitter::renderAlignedParticle( const Particle &#8706; )
{
   Point3F dir = mDataBlock->alignDirection;
   
   Point3F cross(0.0f, 1.0f, 0.0f);
   if (mFabs(dir.y) > 0.9f)
      cross.set(0.0f, 0.0f, 1.0f);
   
   // optimization - this is the same for every particle
   Point3F crossDir;
   mCross(cross, dir, &crossDir);
   crossDir.normalize();
   dir.normalize();
   
   lightParticle(part);
   
   glBegin(GL_QUADS);
   
   F32 width = part.size * 0.5f;
   
   dir *= width;
   crossDir *= width;
   Point3F start = part.pos - dir;
   Point3F end = part.pos + dir;
   
   glTexCoord2f(0.0f, 0.0f);
   glVertex3fv( start + crossDir );
   
   glTexCoord2f(0.0f, 1.0f);
   glVertex3fv( start - crossDir );
   
   glTexCoord2f(1.0f, 1.0f);
   glVertex3fv( end - crossDir );
   
   glTexCoord2f(1.0f, 0.0f);
   glVertex3fv( end + crossDir );

   glEnd();
}
// --------------------------------------------------------------------------

#1
08/29/2008 (8:20 pm)
Great resource for the community! Well done!
#2
08/31/2008 (1:17 pm)
Sweet! That's quite a neat idea, though I'm having trouble thinking of applications. Never mind - I'm sure there are plenty :)
#3
09/01/2008 (3:21 pm)
One quick fix, in the code posted above, you need to change to &part
#4
09/07/2008 (2:53 pm)
Can someone explain this a bit further to me, Ive already made the engine changes.

Edit: Thanks for the information, Will check it out later today.

Thanks,
Daniel
#5
09/08/2008 (1:50 pm)
@Adam: Nice addition.

@Daniel: "0.0 0.0 1.0". In script, vectors (and positions, orientations, colors... any "array" type references) are specified as strings. You knew that, right? =)
#6
09/08/2008 (2:11 pm)
Daniel,

alignDirection = "0.0 1.0 0.0";

Experiment with those values to get the desired effect, it's essentially the same as the rotation field on a Shape.
#7
09/08/2008 (2:16 pm)
Oops, Kevin beat me...

If you use the Particle Editor resource at http://www.garagegames.com/index.php?sec=mg&mod=resource&page=view&qid=15204 , you'll need to make the following changes... if you can't read a diff, it's pretty explanatory which new lines need to be added

Index: creator/editor/ParticleEditor.gui
===================================================================
--- creator/editor/ParticleEditor.gui (revision 2535)
+++ creator/editor/ParticleEditor.gui (revision 2760)
@@ -830,4 +830,54 @@
                buttonType = "ToggleButton";
                useInactiveState = "0";
+            };
+            new GuiCheckBoxCtrl(PEE_alignParticles) {
+               canSaveDynamicFields = "0";
+               Profile = "LayoCheckBoxProfile";
+               HorizSizing = "right";
+               VertSizing = "bottom";
+               Position = "7 261";
+               Extent = "87 30";
+               MinExtent = "8 2";
+               canSave = "1";
+               Visible = "1";
+               Color = "1 1 1 1";
+               Variable = "0";
+               Command = "PE_Emitter2.updateControls();";
+               renderTooltip = "1";
+               hovertime = "1000";
+               text = "alignParticles";
+               mouseOver = "0";
+               toggleLocked = "0";
+               pulseRed = "0";
+               pulseGreen = "0";
+               groupNum = "-1";
+               buttonType = "ToggleButton";
+               useInactiveState = "0";
+            };
+            new GuiTextEditCtrl(PEE_alignDirection) {
+               canSaveDynamicFields = "0";
+               Profile = "LayoTextEditProfile";
+               HorizSizing = "right";
+               VertSizing = "bottom";
+               Position = "98 266";
+               Extent = "98 20";
+               MinExtent = "8 2";
+               canSave = "1";
+               Visible = "1";
+               Color = "1 1 1 1";
+               AltCommand = "PEE_alignDirection.updateControls();";
+               renderTooltip = "1";
+               hovertime = "1000";
+               text = "1 1 1 1";
+               maxLength = "255";
+               dropShadow = "0";
+               historySize = "0";
+               password = "0";
+               tabComplete = "0";
+               wantReturn = "0";
+               sinkAllKeyEvents = "0";
+               password = "0";
+               passwordMask = "*";
+               firstRespondOnly = "0";
             };
             new GuiCheckBoxCtrl(PEE_useEmitterSizes) {
Index: creator/editor/particleEditor.cs
===================================================================
--- creator/editor/particleEditor.cs (revision 2535)
+++ creator/editor/particleEditor.cs (revision 2760)
@@ -208,4 +208,6 @@
 	PEE_orientParticles.setValue(%data.orientParticles);
 	PEE_useEmitterSizes.setValue(%data.useEmitterSizes);
+    PEE_alignParticles.setValue(%data.alignParticles);
+    PEE_alignDirection.setText(%data.alignDirection);
 
 	PE_EmitterBase.updateControls();
@@ -255,4 +257,7 @@
 	$ParticleEditor::currEmitter.phiVariance = PEE_phiVariance.getValue();
 	txtPhiVariance.text = PEE_phiVariance.getValue();
+
+	$ParticleEditor::currEmitter.alignDirection = PEE_alignDirection.getValue();
+    alignDirection.text = PEE_alignDirection.getValue();
 
 	//checkboxes have no accompanying textBoxes
@@ -262,4 +267,5 @@
 	$ParticleEditor::currEmitter.orientParticles = PEE_orientParticles.getValue();
 	$ParticleEditor::currEmitter.useEmitterSizes = PEE_useEmitterSizes.getValue();
+    $ParticleEditor::currEmitter.alignParticles = PEE_alignParticles.getValue();
 }
 
@@ -1140,4 +1146,6 @@
 	"\n    phiReferenceVel = "  @ $ParticleEditor::currEmitter.phiReferenceVel @ ";" @
 	"\n    phiVariance = " @ $ParticleEditor::currEmitter.phiVariance @ ";" @
+    "\n    alignParticles = "  @ $ParticleEditor::currEmitter.alignParticles @ ";" @
+    "\n    alignDirection = " @ $ParticleEditor::currEmitter.alignDirection @ ";" @
 	"\n" @
 	"\n    overrideAdvances = " @ $ParticleEditor::currEmitter.overrideAdvances @ ";" @
#8
09/08/2008 (4:06 pm)
Screenie for those interested, thanks again Adam:

orth.ca/ripplefinal.jpg
#9
09/14/2008 (2:21 pm)
Love the way people have been using this for ripple effects, it's perfect.

@James, yay more people are using my resource :D
#10
09/30/2008 (8:06 am)
Anyone looking for a TGEA 1.7.1 version of this fine resource can find it over here.
#11
12/19/2008 (4:04 am)
PLEASE could someone help me setting up the wake effect as pictured above??

I've been trying for weeks now, and just cant work out how its done. I've done the particle alignment changes correctly, purchased the shapes and lines pack for the wake, Ijust cant work out how to get the player wake/ripple to work.

please, someone.
#12
12/19/2008 (8:14 pm)
@deepscratch

You'll need your own 'wake' art asset as its not mine to share, but you said you had that already?

First add the data and emitter

datablock ParticleData( PlayerWakeParticle )
{
   textureName          = "~/data/shapes/particles/wake";
   dragCoefficient     = "0.0"; // Drag of a particle, must be non-negative//
   gravityCoefficient   = "0.0"; // rises slowly
   inheritedVelFactor   = "0.00"; // Multiple applied to the velocity //
   lifetimeMS           = "1200"; // life-time of each emitted particle in milsec //
   lifetimeVarianceMS   = "200"; // Must not be greater than lifetimeMS //
   windCoefficient = "0.0";
   useInvAlpha = "1"; // False = Additive I think //
   spinRandomMin = "30.0"; // (left) Particle's Min spin speed -10,000 to 10,000 //
   spinRandomMax = "30.0"; // (right) Particle's Max spin speed -10,000 to 10,000 //

   colors[0]     = "1 1 1 0.1"; //Color used for the Particle each value//
   colors[1]     = "1 1 1 0.7"; //Color used for the Particle each value//
   colors[2]     = "1 1 1 0.3"; //Color used for the Particle each value//
   colors[3]     = "0.5 0.5 0.5 0"; //Color used for the Particle each value//

   sizes[0]      = "0.1"; // Scale for each Particle at each time value//
   sizes[1]      = "0.5"; // Scale for each Particle at each time value//
   sizes[2]      = "1"; // Scale for each Particle at each time value//
   sizes[3]      = "1.6"; // Scale for each Particle at each time value//

   times[0]      = "0.0"; // Must be given in ascending order //
   times[1]      = "0.25"; // Must be given in ascending order //
   times[2]      = "0.5"; // Must be given in ascending order //
   times[3]      = "1.0";
};

datablock ParticleEmitterData( PlayerWakeEmitter )
{
   ejectionPeriodMS = "50"; // The frequency with which to emit particles //
   periodVarianceMS = "10";  // Must be less than ejectionPeriodMS //

   ejectionVelocity = "0"; // Initial velocity a particle is emitted 0-655 //
   velocityVariance = "0"; //  Must not be neg. or > ejectionVelocity 0-163//

   ejectionOffset   = "0"; // distance of offset from emitter 0-655 //

   thetaMin         = "89"; // Ejection angle relative to the x-axis 0-180//
   thetaMax         = "90"; // Ejection angle relative to the x-axis 0-180//

   phiReferenceVel  = "0"; // Ejection angle about the z-axis over time 0-360 //
   phiVariance      = "1"; // Ejection angle about the z-axis over time 0-360 //

   alignParticles = "1"; //Utilizes Adam Larson's Particle orientation resource //
   alignDirection = "0 1 0"; //Utilizes Adam Larson's Particle orientation resource //


   particles = "PlayerWakeParticle";
};

Then where you create your PlayerData datablock, you'd place the following line, or change the one that exists.

splashEmitter[0] = PlayerWakeEmitter;

Hope that helps.
#14
03/22/2009 (11:45 am)
Looks awesome

thanks