FreezePFlow - Snapshot As Mesh

This MacroScript lets you create a Mesh Snapshot of a Particle Flow particle system.
Currently, neither Mesher Compound Object nor Snapshot Utility can create an Editable Mesh copy of a Particle Flow system.

Natural Language
Complete Script

macroScript FreezePFlow
category:"Particle Flow"
tooltip:"Snapshot PFlow Particles as Mesh"
(

  fn filter_one_pflow = (selection.count == 1 and classof selection[1] == PF_Source)
  on isEnabled return filter_one_pflow()
  on isVisible return filter_one_pflow()

  on Execute do
  (
    pick_pflow = selection[1]
    holder_mesh = Editable_Mesh()
    holder_mesh.name = uniquename (pick_pflow.name+"_Freeze")

    with undo off
    (
      i = 0
      finish = false
      while not finish do
      (
        i += 1
        pick_pflow.particleIndex = i
        current_mesh = pick_pflow.particleShape
        if current_mesh == undefined then
          finish = true
        else
        (
          new_mesh = Editable_Mesh()
          new_mesh.mesh = current_mesh
          new_mesh.transform = pick_pflow.particleTM
          attach holder_mesh new_mesh
        )
      )
    )

    update holder_mesh
    pushPrompt (i as string +" Particle(s) Frozen!")
  )
)

Step-By-Step Comments

macroScript FreezePFlow
category:"Particle Flow"
tooltip:"Snapshot PFlow Particles as Mesh"
(

The script starts with the keyword macroScript which defines a script that can be used as an Action Item on a Toolbar, Menu, QuadMenu or as a Shortcut. 
It is followed by the name of the macroScript, in our case FreezePFlow.
A macroScript requires the name of the category it will appear in. 
The optional Tooltip is the text displayed in the Customize... dialog and when the mouse rolls over the button on a Toolbar.

fn filter_one_pflow = (selection.count == 1 and classof selection[1] == PF_Source)

This line defines a local function which will be accessible only inside this macroScript. 
It compares the number of selected objects with 1 and  the class of the first selected object with the PF_Source class which is the class of the Particle Flow Source objects. Because of the logical AND, if any of the two comparisons returns false, the result of the function will be false. If both are true, the result of the function will be true. This function will be used to define the enabled and visible state of the script's ActionItem.

on isEnabled return filter_one_pflow()

The isEnabled handler is called every time a Toolbar button has to be updated by the 3ds max UI. 
When it returns true, the button can be pressed by the user. 
When it returns false, the button will be grayed out .and cannot be pressed. 
We will call the function defined above to decide when to enable the button. 
At the end, the button will only be accessible if a single object is selected in the scene and this object is a Particle Flow Source.

on isVisible return filter_one_pflow()

The isVisible handler is called every time a QuadMenu Item has to be updated by the 3ds max UI. 
When it returns true, the item appears on the QuadMenu. 
When it returns false, the item does not appear at all. 
We will call the same function defined above to decide when to make the item visible. 
At the end, the item will only appear on the QuadMenu if a single object is selected in the scene and this object is a Particle Flow Source.

on Execute do
(

The Execute handler is called whenever the user presses the button, selects the Menu Item or presses the keyboard shortcut the macroScript is assigned to. It contains  the body of the script.

pick_pflow = selection[1]

The pick_pflow is a local variable for the macroScript. (Variables are implicitly local in MAXScript). 
We will store the selected Particle Flow Source object in this variable for easier access in the rest of the script.

holder_mesh = Editable_Mesh()

We will create a local variable called holder_mesh and assign it a new empty Editable Mesh object as value. 
This Editable Mesh has no vertices or faces yet and will be used as the holder for the complete snapshot of the particle system.

holder_mesh.name = uniquename (pick_pflow.name+"_Freeze")

The name of the snapshot mesh will be based on the original name of the Particle Flow source plus the suffix "_Freeze" and a unique number added automatically by MAXScript (for the cases where multiple snapshots of the same particle system have been created).

with undo off
(

This disables the automatic undo record creation of 3ds max to speed things up.

i = 0

The i variable will be used as an index to the current particle. 
We initialize it to 0 and will increment it for each particle we process.

finish = false

The finish variable will be used as a flag to tell the script whether there are more particles to process. 
It is initialized to false (not finished yet) and will be set to true when the last particle has been reached.

while not finish do
(

The while loop will repeat the following code inside the brackets for each particle. 
It will continue until the finish variable becomes true. 
While expects a true value to keep going. 
Since finish is set to false in the beginning, we use a logical NOT to turn the false to true to keep the loop running.

i += 1

Now we increase the particle index by 1.

pick_pflow.particleIndex = i

Using the particle index variable stored in i, we set the current particle in the Particle Flow Source to that index. 
Particle Flow lets you perform operations on particles by first setting the current particle index. 
Any following particle property get/set operations will be performed with the particle whose index was set by .particleIndex
Note that .particleIndex is actually a property of the ParticleObjectExt Interface exposed by the Particle Flow Source Class, but we don't need to explicitly mention the Interface. The above could be written as
pick_pflow.ParticleObjectExt.particleIndex = i, but it would just make the script too crowded.

current_mesh = pick_pflow.particleShape

Using another property of the ParticleObjectExt interface, .particleShape, we get the shape of the current (i-th) particle. 
The .particleShape property returns a TriMesh value which is the internal representation of a mesh in 3ds max
A TriMesh value contains all vertex, face etc. arrays defining a mesh, but the TriMesh value is not an object and cannot be seen in the viewport unless assigned to a holder object like an Editable_Mesh object or a Particle Flow Particle. It is great for copying geometry around without polluting the scene with temporary objects.

if current_mesh == undefined then

After getting the particle shape, we have to check we actually got a valid value. A particle that is not active in the scene will have no shape assigned. When we encounter such a particle, we will set the finish variable to true to break the loop and finish the script execution (see next line).

finish = true

As described above, if the particle has no valid mesh assigned, we set the finish variable to true to stop the script.

else
(

The following block in the brackets will be executed if the current_mesh variable actually received a valid TriMesh value.

new_mesh = Editable_Mesh()

We will need a temporary Editable Mesh object in the scene to prepare the current particle's mesh. At first, it will have no vertices or faces and will be located at the origin of the world with zero position and rotation values and default 100% scaling.

new_mesh.mesh = current_mesh

Now we assign the TriMesh value of the current particle to the mesh property of the Editable Mesh. This effectively copies the complete mesh definition of the current particle to the temporary Editable Mes.

new_mesh.transform = pick_pflow.particleTM

Next we get the transformation matrix of the current particle which defines the position, rotation and scale of the particle, and copy the result to the transformation property of the temporary Editable Mesh containing the particle mesh value. In result, the Editable Mesh is moved, rotated and scaled exactly as the current particle.

attach holder_mesh new_mesh

Finally, we attach the temporary mesh object to the first Editable Mesh we created. This adds the current particle's mesh to the snapshot object.

)

This is the end of the else block.

)

This is the end of the while loop.

)

This is the end of the undo disabling context.

update holder_mesh

After the script has copied all particles to the snapshot object, we update all its cashes to ensure correct display in the scene.

pushPrompt (i as string +" Particle(s) Frozen!")

And at the end, we push a new message to the 3ds max Prompt line. The text will contain the final value of the particle index variable to tell the user how many particles have been processed.

)

This is the end of the execute handler.

)

And this is the end of the macroScript.

 

 

Copyright 2003 by Borislav 'Bobo' Petrov.