Particle Event Examples

The following custom particle event examples are provided:

• JScript Example: Setting up a Particle Simulation through Scripting—demonstrates how to set up a scene with a scripted particle event, a goal (including how to access the goal attributes from the particle event), a force and an obstacle, and then shows how to navigate through the particle cloud.

• VBScript Example: Particle Attractor—a particle event which demonstrates how to influence the position, velocity and color of a particle by the proximity to the attractor object.

| In addition, there is a example of a flocking effect implemented using a custom operator instead of a particle event, which you may find useful for comparison, included with the XSI SDK installation. It is implemented as a compiled operator using the C++ API. Check the following location:

       %XSI_ROOT%/XSISDK/examples/operators/ParticleOpExample

JScript Example: Setting up a Particle Simulation through Scripting

There are lots of things going on here because the example attempts to touch on as many aspects of working with a particle system as possible. It is chunked into several pieces so as to make it more manageable:

Creating a Particle Type with an Event

Start by creating a new ParticleType object. The second parameter of the command is an output parameter, which you can access through an output argument ISIVTCollection.

       NewScene( null, false );
       var rtn = CreateParticleType( siBlobType );
       var ptype = rtn(0);

| The new PType sits in the Scene container (ParTypes), unattached to any cloud in the scene until it is explicitly added to an emitter property (EmissionProp).

Now you can use the new ParticleType object as the base for your new Particle Event in the same way (returned ISIVTCollection) except that you get an XSICollection of one event, so you have to get the first member of the XSICollection too which is a Property (PEvent) object

       var rtn = AddParticleEvent( ptype );
       var pevcoll = rtn(0);
       var pevent = pevcoll(0);

       // Now you can enumerate the ParameterCollection on the PEvent Property
       var param = new Enumerator( pevent.Parameters );
       for ( ; !param.atEnd(); param.moveNext() ) {
           logmessage( param.item().Name + " = " + param.item().Value );
       }
           // The following information is logged:
           //INFO : Name = PEvent
           //INFO : Mute = false
           //INFO : Trigger = 0
           //INFO : Value = 100
           //INFO : Var = 0
           //INFO : Distrib. = 0
           //INFO : Seed = 0
           //INFO : Action = 1
           //INFO : File =
           //INFO : Proc. =
           //INFO : Script =
           //INFO : Use Script File = false
           //INFO : ScriptLanguage = 0
           //INFO : ScriptContext = 0
           //INFO : Emission = undefined
           //INFO : Obstacle = undefined

       // Alternatively, you could identify each parameter by its
       // Parameter.ScriptName from the ParameterCollection:
       var eparams = pevent.Parameters;
       eparams( "EventTrigger" ).Value = 3; // Change the trigger
       eparams( "TriggerValue" ).Value = 30; // to every 30 frames

To set up a scripted event, you need to change the Action to 6 (Script) and because you're using JScript, you also need to change the ScriptLanguage parameter to 1:

       eparams( "EventAction" ).Value = 6;
       eparams( "ScriptLanguage" ).Value = 1;

6. Then you have to decide whether to use an embedded script or a script defined in a file on disk. To do so, you have to set one of the following sequences of parameters:

FOR EMBEDDED SCRIPT: | FOR SCRIPT FILE:
ScriptContext | UseScriptFile
Script | ScriptFile
| ScriptProc

Adding the Implementation of the Embedded Scripted Particle Event

This first example sets up an embedded script with a Per Particle context and uses a function defined in this file for convenience:

       eparams( "ScriptContext" ).Value = 1;
       eparams( "Script" ).Value = "PEventHandlerEmbedded();"
           + PEventHandlerEmbedded.toString();

           // When you run this script with example #1 enabled,
           // information like the following is logged (only
           // much, much more of it):
           //INFO : Particle99's ID = 99
           //INFO :  index = 101
           //INFO :  inParticleCloud = cloud.cloud
           //INFO :  inTriggerParticleIndices = 0,1,2,3,...46,47,48,49
           //INFO :  inSimFrame = 31
           //INFO :  inParticleCollection.Count = 102

Here is the helper function: it is just a convenience to allow you to easily specify a string for the Particle Event handler:

       function PEventHandlerEmbedded()
       {
           // You have access to the Particle Cloud through the
           // inParticleCloud (ParticleCloudPrimitive object)
           // in this context:
           LogMessage( inParticle.Name + "'s ID = " + inParticle.ID );

           // This context also gives you the particle's ID within
           // the cloud (the same as what you just logged through
           // the OM property above):
           LogMessage( "\tindex = " + inParticleCnt );

           // All contexts support these hooks:
           LogMessage( "\tinParticleCloud = " + inParticleCloud );
           LogMessage( "\tinTriggerParticleIndices = "
              + inTriggerParticleIndices.toString() );
           LogMessage( "\tinSimFrame = " + inSimFrame );
           LogMessage( "\tinParticleCollection.Count = "
              + inParticleCollection.Count );
       }

Adding the Implementation of the File-Based Scripted Particle Event

This second example sets up a file on disk to link to, saved as pevent.js in the <user>/Data/Scripts folder and then uses the PEventHandlerEmbedded() function as the handler:

       var fileName = InstallationPath( siUserPath ) + "\\pevent.js";
       SaveScriptFileToUserDir( fileName )

       eparams( "UseScriptFile" ).Value = true;
       eparams( "ScriptFile" ).Value = fileName;
       eparams( "ScriptProc" ).Value = "PEventHandlerLinked";

           // When you run this script with example #2 enabled,
           // information like the following is logged:
           //INFO :  inParticleCloud = cloud.cloud
           //INFO :  inTriggerParticleIndices = [object Object]
           //INFO :  inSimFrame = 31
           //INFO :  inParticleCloud = cloud.cloud
           //INFO :  inTriggerParticleIndices = [object Object]
           //INFO :  inSimFrame = 61
           //INFO :  inParticleCloud = cloud.cloud
           //INFO :  inTriggerParticleIndices = [object Object]
           //INFO :  inSimFrame = 91

Here are more convenience functions: PEventHandlerLinked() contains the implementation of the file-based scripted event and SaveScriptFileToUserDir() simply offloads the work of creating the file on disk so it doesn’t have to appear in the main function:

       function PEventHandlerLinked(
                     inParticleCloud,
                     inTriggerParticleIndices,
                     inSimFrame
                  )
       {
           // You have access to no contexts from script files but
           // the following hooks are still available:
           LogMessage( "\tinParticleCloud = " + inParticleCloud );
           LogMessage( "\tinTriggerParticleIndices = "
              + inTriggerParticleIndices.toString() );
           LogMessage( "\tinSimFrame = " + inSimFrame );

           // In addition, since there's a goal in this scene we can
           // access its information through the ParticleAttributeCollection
           ReadGoalInfo( inParticleCloud, inTriggerParticleIndices, inSimFrame );
       }

       function SaveScriptFileToUserDir( infile )
       {
           var fso = XSIFactory.CreateActiveXObject( "Scripting.FileSystemObject" );
           var ts = fso.CreateTextFile( infile );
           ts.Write( PEventHandlerLinked.toString() + "\n"
                  + ReadGoalInfo.toString() );

       }

Extra Helper Function to Work with Goal Data

       function ReadGoalInfo( inParticleCloud, inTriggerParticleIndices, inSimFrame )
       {
           logmessage( "CURRENT FRAME: " + inSimFrame );
           for ( var i=0; i<inParticleCloud.Particles.Count; i++ ) {
              var inParticle = inParticleCloud.Particles(i);
              LogMessage( "Looking at particle ID# " + inParticle.ID );

              var a = new Enumerator( inParticle.Attributes );
              for ( ; !a.atEnd(); a.moveNext()  ) {
                  // Grab the current ParticleAttribute for convenience
                  var gattr = a.item();

                  // This switch block traps any complex data structures like
                  // vectors by looking at the ParticleAttribute name before
                  // displaying any values and storing any values in an
                  // appropriate structure.
                  switch (gattr.AttributeType) {
                     case 0 :
                         // The UVWI pattern is stored in a vector4. Once you
                         // create a Quat to hold the stored value, you can just
                         // access each member by index (to keep things simple)
                         var q = XSIMath.CreateQuaternion();
                         q = gattr.Value;

                         // Getting the Values by index instead of name
                         LogMessage( "\t" + gattr.Name + " = "
                             + q(0) + ", " + q(1) + ", " + q(2) + ", " + q(3)
                             + " (UVWI)" );

                         // Getting the Values by name would look like this:
                         //LogMessage( "\t" + gattr.Name + " = "
                         //  + q.W + ", " + q.X + ", " + q.Y + ", " + q.Z
                         //  + " (WXYZ)" );

                         // You always have to break out of a switch statement to
                         // avoid running every scenario
                         break;
                     case 1 :
                         // The Offset is stored in a vector3. Like the UVWI, you
                         // just create an empty v3 object and then you can access
                         // it either by index or name (which works in this case
                         // because the member names X,Y,Z correspond to the
                         // position of the offset
                         var v3 = XSIMath.CreateVector3();
                         v3 = gattr.Value;

                         // Getting the Values by name instead of index
                         LogMessage( "\t" + gattr.Name + " = "
                             + v3.X + ", " + v3.Y + ", " + v3.Z + " (XYZ)" );

                         // Getting the Values by index would look like this:
                         //LogMessage( "\t" + gattr.Name + " = "
                         //  + v3(0) + ", " + v3(1) + ", " + v3(2) + " (XYZ)" );
                         break;
                     default :
                         // NOTE: Relying on 'default' is a little dangerous because
                         // if something wierd happened and the AttributeType was
                         // returning a negative number and the Value was undefined
                         // or null the script would break. To compensate, test
                         // the Value before using it
                         if (gattr.Value != null && typeof(gattr.Value) != "undefined")
                         {
                             LogMessage( "\t" + gattr.Name + " = " + gattr.Value );
                         }
                         break;
                  }
              }
           }
       }

Setting up the Particle Cloud with Force, Goal and Obstacle

You can add a new particle cloud to the Scene_Root and then associate the PType to it. You could do this with the object model (X3DObject.AddParticleCloud), but using the CreateParticleCloud command creates the emitter, particles and particle simulation operator automatically (typically, you would use the object model if you wanted very low-level control over the particles).

The CreateParticleCloud command returns an XSICollection, so you have to get its 1st member:

       var pcloud = CreateParticleCloud( null, "Disc" )(0);

       // Now we can set up a fan...
       var f = CreateForce( "Fan", pcloud );
       f.posx = -4; f.posy = -1; f.rotz = 37;
       AddParticleForce( pcloud, f );

       // ... and a goal...
       var g = ActiveSceneRoot.AddGeometry( "Grid", "MeshSurface" );
       g.posy = 15; g.ulength = 3; g.vlength = 3; g.subdivu = 4; g.subdivv = 4;
       AddParticleGoal( pcloud, g );

       // ... and an obstacle
       var o = ActiveSceneRoot.AddGeometry( "Torus", "MeshSurface" );
       o.radius = 1; sectionradius = 0.25; o.posy = 5;
       AddParticleObstacle( pcloud, o );

As soon as you add an obstacle, it adds a new (or replaces any existing) PEvent on the cloud's PType with a Bounce event. You can then change the Bounce event action to a Bounce & Emit event action where the new PType emitted will be the custom PType with the scripted event created above.

Tweaking the Results (Navigating through the Cloud)

To change the current Bounce event, first you need to get the PType currently associated with the cloud, use it to get the PEvent associated with the PType and then access its EventAction parameter.

Navigating through the components of the particle cloud can be tricky: to get the emitter property (EmissionProp) you need to go through the input ports of the ParticlesOp. To drill down through the hierarchy to the PEvent node you need to use the EnumElements command:

       var op;
       var opstack = new Enumerator( pcloud.ActivePrimitive.ConstructionHistory );
       for ( ; !opstack.atEnd(); opstack.moveNext() ) {
              if ( opstack.item().Name == "ParticlesOp" ) {
              op = opstack.item();
              break;
           }
       }

       var eprop;
       if ( op != null ) {
           for ( var i=0; i<op.InputPorts.Count; i++ ) {
              if ( op.InputPorts(i).Target2 != null
                     && op.InputPorts(i).Target2.Type == "EmissionProp" ) {
                  // Get the EmissionProp nested under the particle operator
                  // and exit the loop
                  eprop = op.InputPorts(i).Target2;
              }
           }
       }

       if ( eprop != null ) {
           // Get the PType nested under the EmissionProp using the
           // EnumElements command, which basically crawls down the
           // Explorer tree and returns a collection of the immediate
           // children as an XSICollection:
           var obs_ptype, obs_pevent;
           rtn = EnumElements( eprop );
           var e = new Enumerator( rtn );
           for ( ; !e.atEnd(); e.moveNext() ) {
              // As soon as the ptype is found, grab it and quit the loop
              if ( e.item().Type == "ParType" ) {
                  obs_ptype = e.item();
                  break;
              }
           }
       }

Now that you have the ParType property, you can use a similar strategy to get the PEvent for this PType, with the one difference being that the Events container nested under the PType is not directly accessible through the PType tree, so we have to use the Dictionary.GetObject method to access it before using EnumElements on its children (which are all the PEvents nested under the PType):

       var obs_pevent_folder = Dictionary.GetObject(obs_ptype.FullName + ".Events");
       rtn = EnumElements( obs_pevent_folder );
       e = null; e = new Enumerator( rtn );

Check each child's parameters to find the one that's got a trigger of Collision (11) and an action of Bounce (3)

       for ( ; !e.atEnd(); e.moveNext() ) {
           // As soon as the right PEvent is found, grab it and quit the loop
           if ( e.item().Parameters( "EventTrigger" ).Value == 11
                  && e.item().Parameters( "EventAction" ).Value == 3 ) {
              obs_pevent = e.item();
              break;
           }
       }

Now you can change the values on the obstacle event to a Bounce & Emit action (4), emitting the custom PType with the scripted PEvent attached:

       var obs_emitter;
       if ( obs_pevent != null ) {
           // Change the action to Bounce & Emit
           obs_pevent.Parameters( "EventAction" ).Value = 4;

           // Then create and attach a new "EmissionProp"
           obs_emitter = CreateEventSource( obs_pevent )(0);
       }

To make sure the right PType is attached to the new emitter, we can navigate down the new emitter property's tree to get its PType property and set it to the custom PType

       if ( obs_emitter != null ) {
           rtn = EnumElements( obs_emitter );
           var e = new Enumerator( rtn );
           for ( ; !e.atEnd(); e.moveNext() ) {
              // As soon as the ptype is found, use it to specify the
              // custom ptype and then quit the loop
              if ( e.item().Type == "ParType" ) {
                  SetParticleType( e.item().FullName, ptype );
                  break;
              }
           }
       }

Finally...watch the show!

       // Watch the simulation
       SetDisplayMode( "Camera", "textured" );
       FirstFrame();
       PlayForwardsFromStart();

VBScript Example: Particle Attractor

This example demonstrates how to write a script that makes a particle jump toward a selected object (the attractor) and change color as it gets nearer the attractor.

It uses the scale value of the attractor object to control a few parameters. The image below gives you an idea of the results when applying the script to a particle stream emitted from a disc.

To Open the Scene for this Example

1. Open the following scene:

       <factory>\Data\XSI_SAMPLES\Scenes\ParticleEventScript_VelocityAttractor.scn

2. Play the animation in loop mode.

3. Select the null (the attractor) and translate it around to see the particles try to track it.

4. Modify the X and Y scaling values of the null to see different results.

To Inspect the Script

1. Select the cloud, then choose Simulate > Inspect > Events > PEvent.

2. Click on the Script tab.

| You can also check out this other example in the same folder:
       ParticleEventScript_SpriteIndex.scn

To Set the Scene

1. Open a New Scene and choose Simulate > Create > Particles > From Disc. It creates a new particle cloud with a disc for an emitter.

2. From the new particle cloud’s property page, click the Events tab in the PType section and click New Event. The new event appears in a grid in the same tab.

3. To inspect the event, right-click on the event row, and choose Inspect Item. The Event property page appears.

Writing the ParticleAttractor Event Script

1. Click the Script tab and set the following:

- For the Trigger value, select Every N Frame.

- For the Value value, enter 1 (== every frame)

- For the Action value, select Script.

- For the ScriptContext value, select Per Cloud.

2. To change all of the particles in the cloud, you can loop through the ParticleCollection:

           'Loop for each particle
       for i = 0 to inParticleCollection.Count - 1
           ' code will appear here
       next

3. To read the particle position and change its velocity and color, either of these approaches can be adopted:

- Update one particle at a time—for each particle, get its position and change its velocity and color using the Particle object.

- Update the whole particle collection in one shot—get the position of all particles at once and set their velocity and acceleration at once through the ParticleCollection object.

| Depending on the situation, one or the other method can be more efficient. The Particle object is useful for tracking a specific particle using its ID. This example uses the ParticleCollection instead.

4. Get the information from the ParticleCollection for the color, position and velocity for each particle in the collection:

       colorList = inParticleCollection.ColorArray
       posList = inParticleCollection.PositionArray
       veloList = inParticleCollection.velocityArray

       ' Visit each particle
       for i = 0 to inParticleCollection.count - 1
           ' implementation here
       next

       ' Save the changes back to the ParticleCollection
       inParticleCollection.colorArray = colorList
       inParticleCollection.velocityArray = veloList

5. Read the position of the selected object. For a little extra stability, set some default values so that if nothing’s selected the script won’t crash.

    ' Default values, in case no object is selected
    posx = 0.0
    posy = 5.0
    posz = 0.0
    ampl = 5.0
    avar = 1.0

    ' Gets the position of the selected object (the attractor). The avar parameter
    ' is linked to the Y scaling of the attractor object and will be used to control
    ' how random the particles's velocity appears to be
    if Application.Selection.Count > 0 then
         set l_obj = Application.Selection(0)
         set l_params = l_obj.kinematics.global.parameters
         posx = l_params("posx").value
         posy = l_params("posy").value
         posz = l_params("posz").value
         ampl = l_params("sclx").value
         avar  = l_params("scly").value
    end if

| This example uses the scale values of the selected object as controls to modify the behavior of the script, but you could also create a custom property page complete with slider controls.

6. Change the velocity of each particle so it goes in the direction of the selected object and add some random value so it looks a bit less uniform.

    'Loop for each particle
    for i = 0 to inParticleCollection.count - 1

       ' Vector between the particle and the attractor
       dirx = posx - posList( 0, i )
       diry = posy - posList( 1, i )
       dirz = posz - posList( 2, i )

       ' Square of distance
       dist = dirx * dirx + diry * diry + dirz * dirz

       ' Set the velocity to be in the direction of the
       ' attractor along with some randomness
       veloList(0,i) = dirx * ampl + ( 10.0 * avar * (rnd()-0.5))
       veloList(1,i) = diry * ampl + ( 10.0 * avar * (rnd()-0.5))
       veloList(2,i) = dirz * ampl + ( 10.0 * avar * (rnd()-0.5))

7. Set the color of the particle to be blue when it’s far from the attractor and yellow when it’s close.

       ' Set the color shift (blue to yellow)
       colorList(0,i) = (10 / dist)
       colorList(1,i) = (10 / dist)
       colorList(2,i) = dist * 0.01
    next

8. To finish up, make sure that all variables are declared and add some comments to the script.