Most Krakatoa MX users familiar with the Magma channel editing system use the mouse and the 3ds Max Command Panel features to set up their modifier stack. However, there is a tiny fraction of the users doing TD or TA work that might want to set up Magma and other modifiers automatically as part of pipeline scripts, or custom tools. In fact, some Thinkbox Software products like Stoke include options to add Krakatoa Magma modifiers with a click of a button, for example to delete particles by Age...
While all of the Magma user interface is implemented in MAXScript and the relevant functionality could be found in the freely editable MS files shipping with Krakatoa MX, the process of adding a fully functional Magma node flow with input controls exposed to the Modify Panel is not exactly obvious.
Prompted by a customer support request, here is a look at how a Magma modifier can be added, set up and exposed to the UI using MAXScript.
Setting Up The Base Scene
Let's assume that for our testing, we will be adding the Magma modifier to a PRT Volume made from a Teapot primitive. To set up a scene procedurally, all we need to do is create a Teapot primitive, then create a PRT Volume object with the Teapot as the source mesh.
theTeapot = Teapot() thePRTV = PRT_Volume()
- Executing the first line will create the Teapot at the world origin.
- Executing the second line will create the PRT Volume at the world origin.
At this point we can call
to see a list of all properties exposed by the PRT Volume to MAXScript:
.TargetNode : node .VoxelLength : float .UseViewportVoxelLength : boolean .ViewportVoxelLength : float .InWorldSpace : boolean .UseDensityCompensation : boolean .DisableVertexVelocity : boolean .SubdivideEnabled : boolean .SubdivideCount : integer .JitterSamples : boolean .JitterWellDistributed : boolean .MultiplePerRegion : boolean .MultiplePerRegionCount : integer .RandomSeed : integer .RandomCount : integer .ViewportEnabled : boolean .ViewportDisableSubdivision : boolean .ViewportUsePercentage : boolean .ViewportPercentage : float .ViewportUseLimit : boolean .ViewportLimit : float .ViewportIgnoreMaterial : boolean .IconSize : float .UseShell : boolean .ShellStart : float .ShellThickness : float false
Here we see that the property we need to set is .TargetNode. We can assign the Teapot to it to convert its volume to a particle cloud:
thePRTV.TargetNode = theTeapot
Looking in the viewport, we can now see the PRT Volume's particles! We can hide the Teapot so it is not in the way:
We can also uncheck the option to use a separate Viewport Voxel Length which defaults to 3.0, as opposed to the 1.0 for rendering:
thePRTV.UseViewportVoxelLength = false
At this point, the PRT Volume closely resembles the shape of the teapot, and should have about 48866 particles.
Adding A Magma Modifier To The Stack
The first step is rather trivial - there are two functions in MAXScript for adding a modifier to an object, and both work with Magma modifiers.
The addModifier Function
The addModifier() function has been part of MAXScript since the very beginning, but its functionality changed slightly in the early days of the software. It is very useful because it does not depend on the current state of the 3ds Max UI. It requires two arguments - the object to add the modifier to, and the modifier's instance to add. It also offers a third, optional keyword argument before: which controls where on the stack the modifier is inserted.
First, let's create an instance of the Magma Modifier:
theMod = MagmaModifier()
Now we can add it to the PRT Volume using the addModifier function:
addModifier theTeapot theMod
This will add the modifier to the top of the stack, even if the PRT Volume is not the currently selected object.
The modPanel.AddModToSelection Function, Just For Clarity
The main purpose of this function is to add a modifier to the currently active Modify Panel at the current stack level, while also preserving the sub-object selection. Since the Magma modifier does not require a sub-object selection, and the addModifier() function already offers control over the level on the stack to insert at, we don't really need to pay attention to this option.
Loading A Flow From A Preset
One way to set up the nodes of a newly added Magma Modifier would be to load a preset from disk. This would make the whole process very easy, but on the other hand if your custom scripted tool is packaged in just a single MAXScript file, it would mean that script would depend on another file that needs to be distributed. So if you feel comfortable with packaging multiple files and dealing with the question "where is my preset file located so I can load it?", then here are the steps:
Creating The Preset
First, let's create the Magma flow we want to apply to our PRT Volume via MAXScript.
Let's say that we will be performing a simple Push Along Normal modulated by a Noise.
If you have executed all the code so far in an empty scene, you should have the PRT Volume with the Magma Modifier on its stack - simply open the Magma Editor and set up the flow:
- Press SHIFT+CTRL+P to create a Position Output node.
- Press SHIFT+P to create and connect a Position InputChannel node.
- Press the + key on the Numpad to insert an Add operator into the flow.
- Press SHIFT+N to connect a Normal InputChannel to the second socket of the Add.
- Press the * key on the Numpad to insert a Multiply operator into the flow behind the Normal InputChannel node.
- Press CTRL+1 to connect a Float InputValue with a value of 1.0 to the second socket of the Multiply operator.
- Press the * key on the Numpad again to insert another Multiply between the InputValue and the other Multiply operator.
- Press CTRL+R to Auto-Reorder the flow.
- Drag a wire from the second socket of the new Multiply operator and release over an empty area, then select Function, then Noise to connect a Noise operator.
- Drag a wire from the Position InputChannel node and connect to the first socket (Value) of the new Noise operator.
- Select the new wire and press the / key on the Numpad to insert a Divide operator.
- Press CTRL+1 to connect another Float InputValue with value of 1.0 to the second socket of the Divide operator.
- Rename the new InputValue node to "Noise Scale".
- Check the Expose checkbox of the InputValue.
- Select the first InputValue node, rename to "Push Amount" and check the Expose checkbox.
- Press SHIFT+CTRL+R to bake the auto-reordered positions of all nodes and make them explicit.
- Change the Push Amount to 10.0 and the Noise Scale to 12.0 and note the results in the viewport.
Now we can save this as a Preset.
- Click the File menu of the Magma Editor.
- Select "Save Flow As MAXScript..."
- In the file dialog, type in the name "PushWithNoise"
- Click the Save button of the file dialog.
Loading The Preset Using MAXScript
Now that we have our Preset flow, let's see how we can recreate the whole scene including the objects and the modifier using MAXScript:
(--start a new local scope resetMaxFile #noprompt --this will reset the scene without a prompt theTeapot = Teapot() --create a default teapot thePRTV = PRT_Volume() --create a PRT Volume thePRTV.TargetNode = theTeapot --connect the two hide theTeapot --hide the teapot thePRTV.UseViewportVoxelLength = false --switch viewport to render spacing theMod = MagmaModifier() --create a Magma modifier instance addModifier thePRTV theMod --add the Magma modifier to the PRT Volume magmaNode = theMod.magmaHolder --this is the MagmaHolder object that contains the nodes magmaNode.autoUpdate = true --this will enable the Auto-Update option --We need to get the default location of Krakatoa MX Magma flows, and get the Preset we just saved: theFileToLoad = (dotnetclass "System.Environment").GetFolderPath \
(dotnetclass "System.Environment+SpecialFolder").LocalApplicationData + \
"\\Thinkbox\\Krakatoa\\MagmaFlows\\PushWithNoise.MagmaScript" --Then all we need to do is call the following function --that loads the given file into the given Magma flow: MagmaFlowEditor_Functions.loadPreset theFileToLoad magmaNode )--end of local scope
Packaging The Preset With The Script Source
In the previous section, we assumed that the Preset file was stored in the default folder where Magma saves its Presets. However, if the Preset will be part of some scripted package that will be distributed to other users, we cannot make such an assumption, unless we either instruct the user to copy the Preset file to a specific location, or write an installer that makes sure the Preset is placed at a specific location.
To make things a lot more self-contained, we can assume that the Preset file will always be located where the master script is called from. So if you instruct the user to copy the files to any folder, and then to run the main MS script, we can modify our code to resolve the path of the Preset based on the location of the main script loading it!
All we need to do is change the line defining the theFileToLoad variable:
theFileToLoad = getFileNamePath (getSourceFileName()) + "\\PushWithNoise.MagmaScript"
By doing this, we ask where the main script is being run from, get the path of that filename, and look for the MagmaScript in the same folder!
Note that if the main script defines a MacroScript to perform the scene and Magma creation, this will result in a copy of the MacroScript body stored in a completely different folder, and would not work with this approach since the Preset would not be found...
Embedding A Magma Flow Into a Custom Scripted Tool
As mentioned already, the above approach keeps the Magma flow to be loaded external to the scripted tool. It must be located in the MagmaScripts folder of Krakatoa, or at a specific path the script knows about, or the path could be acquired based on where the script itself is being run from...
However, a more advanced scripter might want to hard-code the content of the flow right there in the source code. This would avoid problems if the external preset file is deleted by accident, or if it cannot be located due to a user mistake, or the MacroScript definition problem mentioned earlier.
Pasting The Flow Definition Into The Script
We can benefit from the fact that Copy & Paste in Magma actually creates a valid MAXScript representation of the Magma flow itself. So all we need to do is select and Copy the nodes in the Magma Editor, and then paste them into our custom script. Alternatively, we could use the content of the Preset file created in the previous section...
- Open the Magma Editor with the flow created in the previous example
- Select all nodes by pressing CTRL+A
- Open the custom MAXScript from the previous example, and replace the last two lines of the script with the content of the Windows Clipboard by pressing CTRL+V
- Remove the line with the opening bracket (--MAGMAFLOW2--
- Remove the closing bracket at the end of the pasted text.
If you would execute the script now, the scene would be recreated and the Magma flow would be rebuilt from the code pasted in our script.
However, there are a few small issues:
- All nodes in the pasted flow are selected, because they were selected when copying to the Windows Clipboard.
- The exposed InputValue controls will not appear in the Modify panel.
So we need to do some more work here.
Removing The Node Selection
To make the nodes unselected, we can simply delete the two lines of every node definition which look something like this:
magmaNode.DeclareExtensionProperty node0 "Selected" magmaNode.SetNodeProperty node0 "Selected" true
We can do this for every node.
An alternative solution would be to do Search And Replace for `"Selected" true` and turn it into `"Selected" false`. However, this will result in a longer script as the two lines per node would stay...
Evaluating our script will result in a flow with unselected nodes.
Updating The Exposed Controls
The Preset loading function we used in the previous example handled the updating of any exposed controls. That function is actually defined in the script file Krakatoa_MagmaFlowManager.ms, so it is rather easy to take a look at it and see how it does it...
The loadPreset function definition looks like this:
fn loadPreset theFileToLoad theMagmaNode = ( global MagmaNode = theMagmaNode theMagmaNode.Loading = true fileIn theFileToLoad theMagmaNode.Loading = false MagmaFlowEditor_Rollout = MagmaFlowEditor_Functions.OpenMagmaFlowEditor magmaNode offscreen:true MagmaFlowEditor_Rollout.exposeControlsToModifier() destroyDialog MagmaFlowEditor_Rollout ),
As you can see, in addition to performing a fileIn() on the preset file, it has three lines dedicated to updating the exposed controls.
- The first line actually opens the Magma Editor off screen, and stores the resulting rollout value in a variable.
- The second line calls the function exposeControlsToModifier() in that rollout.
- The last line simply destroys the dialog made from that rollout.
We need to perform these steps in our code.
Simply copy the three lines from the function definition to the end of our script, just before the closing bracket.
The Resulting Script
Here is what the script looks like after all modifications:
( resetMaxFile #noprompt theTeapot = Teapot() thePRTV = PRT_Volume() show thePRTV thePRTV.TargetNode = theTeapot hide theTeapot thePRTV.UseViewportVoxelLength = false theMod = MagmaModifier() addModifier thePRTV theMod magmaNode = theMod.magmaHolder magmaNode.autoUpdate = true global MagmaFlowEditor_EditBLOPHistory = #() node0 = magmaNode.createNode "Output" magmaNode.setNumNodeInputs node0 1 magmaNode.setNumNodeOutputs node0 0 magmaNode.setNodeProperty node0 "channelName" "Position" magmaNode.setNodeProperty node0 "channelType" "float32" magmaNode.DeclareExtensionProperty node0 "Position" magmaNode.SetNodeProperty node0 "Position" [1022,0] -------------------------------------------- node1 = magmaNode.createNode "InputChannel" magmaNode.setNumNodeInputs node1 0 magmaNode.setNumNodeOutputs node1 1 magmaNode.setNodeProperty node1 "channelName" "Position" magmaNode.setNodeProperty node1 "channelType" "" magmaNode.DeclareExtensionProperty node1 "Position" magmaNode.SetNodeProperty node1 "Position" [182,200] -------------------------------------------- node2 = magmaNode.createNode "Add" magmaNode.setNumNodeInputs node2 2 magmaNode.setNumNodeOutputs node2 1 magmaNode.DeclareExtensionProperty node2 "Position" magmaNode.SetNodeProperty node2 "Position" [882,0] magmaNode.setNodeInputDefaultValue node2 1 0.0 magmaNode.setNodeInputDefaultValue node2 2 0.0 -------------------------------------------- node3 = magmaNode.createNode "InputChannel" magmaNode.setNumNodeInputs node3 0 magmaNode.setNumNodeOutputs node3 1 magmaNode.setNodeProperty node3 "channelName" "Normal" magmaNode.setNodeProperty node3 "channelType" "" magmaNode.DeclareExtensionProperty node3 "Position" magmaNode.SetNodeProperty node3 "Position" [602,45] -------------------------------------------- node4 = magmaNode.createNode "Multiply" magmaNode.setNumNodeInputs node4 2 magmaNode.setNumNodeOutputs node4 1 magmaNode.DeclareExtensionProperty node4 "Position" magmaNode.SetNodeProperty node4 "Position" [742,30] magmaNode.setNodeInputDefaultValue node4 1 1.0 magmaNode.setNodeInputDefaultValue node4 2 1.0 -------------------------------------------- node5 = magmaNode.createNode "InputValue" magmaNode.setNumNodeInputs node5 0 magmaNode.setNumNodeOutputs node5 1 magmaNode.setNodeProperty node5 "forceInteger" false ctrl=bezier_float(); ctrl.value = 10.0 magmaNode.setNodeProperty node5 "controller" ctrl magmaNode.DeclareExtensionProperty node5 "Exposed" magmaNode.SetNodeProperty node5 "Exposed" true magmaNode.DeclareExtensionProperty node5 "Name" magmaNode.SetNodeProperty node5 "Name" "Push Amount" magmaNode.DeclareExtensionProperty node5 "Position" magmaNode.SetNodeProperty node5 "Position" [462,115] -------------------------------------------- node6 = magmaNode.createNode "Multiply" magmaNode.setNumNodeInputs node6 2 magmaNode.setNumNodeOutputs node6 1 magmaNode.DeclareExtensionProperty node6 "Position" magmaNode.SetNodeProperty node6 "Position" [602,100] magmaNode.setNodeInputDefaultValue node6 1 1.0 magmaNode.setNodeInputDefaultValue node6 2 1.0 -------------------------------------------- node7 = magmaNode.createNode "Noise" magmaNode.setNumNodeInputs node7 2 magmaNode.setNumNodeOutputs node7 1 magmaNode.setNodeProperty node7 "numOctaves" 4 magmaNode.setNodeProperty node7 "lacunarity" 0.5 magmaNode.setNodeProperty node7 "normalize" true magmaNode.DeclareExtensionProperty node7 "Position" magmaNode.SetNodeProperty node7 "Position" [462,170] magmaNode.setNodeInputDefaultValue node7 2 0.0 -------------------------------------------- node8 = magmaNode.createNode "Divide" magmaNode.setNumNodeInputs node8 2 magmaNode.setNumNodeOutputs node8 1 magmaNode.DeclareExtensionProperty node8 "Position" magmaNode.SetNodeProperty node8 "Position" [322,185] magmaNode.setNodeInputDefaultValue node8 1 1.0 magmaNode.setNodeInputDefaultValue node8 2 1.0 -------------------------------------------- node9 = magmaNode.createNode "InputValue" magmaNode.setNumNodeInputs node9 0 magmaNode.setNumNodeOutputs node9 1 magmaNode.setNodeProperty node9 "forceInteger" false ctrl=bezier_float(); ctrl.value = 12.0 magmaNode.setNodeProperty node9 "controller" ctrl magmaNode.DeclareExtensionProperty node9 "Exposed" magmaNode.SetNodeProperty node9 "Exposed" true magmaNode.DeclareExtensionProperty node9 "Name" magmaNode.SetNodeProperty node9 "Name" "Noise Scale" magmaNode.DeclareExtensionProperty node9 "Position" magmaNode.SetNodeProperty node9 "Position" [182,255] -------------------------------------------- try(magmaNode.setNodeInput node0 1 node2 1)catch() try(magmaNode.setNodeInput node2 1 node1 1)catch() try(magmaNode.setNodeInput node2 2 node4 1)catch() try(magmaNode.setNodeInput node4 1 node3 1)catch() try(magmaNode.setNodeInput node4 2 node6 1)catch() try(magmaNode.setNodeInput node6 1 node5 1)catch() try(magmaNode.setNodeInput node6 2 node7 1)catch() try(magmaNode.setNodeInput node7 1 node8 1)catch() magmaNode.setNodeInput node7 2 -1 1 try(magmaNode.setNodeInput node8 1 node1 1)catch() try(magmaNode.setNodeInput node8 2 node9 1)catch() -------------------------------------------- MagmaFlowEditor_Rollout = MagmaFlowEditor_Functions.OpenMagmaFlowEditor magmaNode offscreen:true MagmaFlowEditor_Rollout.exposeControlsToModifier() destroyDialog MagmaFlowEditor_Rollout )
Evaluating this script should recreate the scene and set up the Magma Modifier exactly as we want it.