I was asked by Joker Martini to convert a MAXScript geometry plugin that he wrote into a C++ plugin for performance and functionality reasons. John Martini came up with the concept for the plugin and had a nearly complete working MAXScript implementation with only a few gotchas. One of those gotchas was performance for a spline with many knot points. The other gotcha was getting the generated spline to update properly when the scene node(s) it depended on were updated. A C++ based plugin solved these issues and allowed for additional functionality as well. Check out these videos to see the plugin in action:

You can purchase the plugin here if you are interested.

Development Details

My first step in prototyping this plugin was to create a skeleton geometry plugin based on SimpleSpline. This class wraps up a lot of the details of a spline plugin including dealing with render time and viewport mesh generation. Besides the usual boilerplate 3ds Max plugin code (of which there is quite a bit), you really only have to implement a single method to generate the spline geometry: BuildShape(TimeValue t, BezierShape& ashape).

Once I had the plugin drawing a test spline, it was time to see how to get a list of referenced nodes and watch for changes to them. Luckily, the built-in ParamBlock2 parameter type TYPE_NODELISTBOX will auto-manage a list of nodes and references to those nodes allowing you to receive notifications when the reference changes:

static ParamBlockDesc2 theLabyrinthObjectNodeListParmBlock
(
	labyrinth_nodelist_params,		// block id
	_T("LabyrinthNodes"),	     	// internal name string
	0,								// local name string
	&theLabyrinthObjectClassDesc,   // class description
	P_AUTO_CONSTRUCT + P_AUTO_UI,	// flags
	labyrinth_nodelist_params,	    // pblock num
	IDD_NODELIST_PANEL, IDS_NODELIST, 0, 0, NULL,

	//Parameter Specifications ----------------------
	labyrinth_nodelist_node_tab, _T("nodes"), TYPE_INODE_TAB, 0, P_VARIABLE_SIZE, IDS_NODES,
		p_ui, TYPE_NODELISTBOX, IDC_NODE_LIST, IDC_PICK_BUTTON, IDC_REPLACE_BUTTON, IDC_DELETE_BUTTON,
		p_end,
	p_end
);

Notice that there is auto-ui support for the pick button, replace button, and delete button! As long as you provide the proper dialog IDs, you get all that functionality for free!

Now that the plugin has a node list parameter (thanks to the magic of the TYPE_NODELISTBOX parameter type), it will start to receive REFMSG_CHANGE notifications in the NotifyRefChanged() method when any of the nodes in the list change. Internally, the parameter block is taking a reference to the nodes that are added to the list and the parameter block itself re-broadcasts the change message downstream to the plugin object. From the NotifyRefChanged(), if we receive REFMSG_CHANGE from the node list parameter block we can invalidate the geometry cache and force a spline re-build on demand. This keeps the generated spline always up to date as nodes and objects change.

With the node list working, it was pretty straightforward to translate the provided reference MAXScript plugin into C++. I was careful to make sure the array for the points is only reallocated when the point count changes and reused whenever possible to avoid slowdowns.

After the core plugin was implemented and working I wanted to add PFlow support. Unfortunately, it wasn’t super clear to me from the SDK docs how to identify and access a PFlow object’s particles. There are a lot of classes with ‘Particle’ in the name and ParticleObject/SimpleParticleObject was not working for PFLow objects. After a lot of digging into the header files, here is how I ended up doing it:

ObjectState os = pNode->EvalWorldState(t);
Object *pObj = os.obj;
valid = valid & os.Validity(t);
if (pObj->IsParticleSystem())
{
    IParticleObjectExt* pExt = GetParticleObjectExtInterface(pObj);
    if (pExt != 0)
    {
        pExt->UpdateParticles(pNode, t); // need to call this or we get the last frame...
        int pcount = pExt->NumParticles();
        // now process the particles in some way
        for (int i=0; i < pcount; i++)
        {
            Point3* pPoint = pExt->GetParticlePositionByIndex(i);
            // do something with the positions
            ...
        }
    }
    // else this is probably a legacy particle system...
}

The key here is getting the IParticleObjectExt interface from the object using the macro GetParticleObjectExtInterface(). This object is not really described in the docs as being related to PFlow, but it works, go figure.

Another feature I wanted to add was the ability to generate random points for the spline on the surface of a triangle mesh. I wanted to generate points evenly based on face area so I came up with this method:

  • Compute the area for every triangle and store it in a cumulative distribution array called cda (i.e. [face0’s area, (face0’s area + face1’s area), …])
    • Consider each element to actually represent an interval. The value of the array at index i represents the interval: cda[i-1] -> cda[i]
    • So if cda[0] == .1 and cda[1] = 0.3, the index 0 represents the interval (0,0.1] and index 1 represents the interval (0.1, 0.3]
  • For each point you want to generate:
    • Pick a random number r between 0 and 1
    • Generate the random ‘area’ value av to search for by multiplying r by the total face area
      • We will search for the interval index in cda that contains the value av
    • Guess a reasonable search starting index for cda by multiplying the random number r by the number of faces in the mesh
      • This is just a guess to help get close for the next step…
    • Do a binary search of cda looking for the index i where the av value we generated fits into the implied interval:
      • When cda[i-1] < av <= cda[i] we return i and break out of the binary search
    • The index i can now be used to index a triangle from the mesh which should now be randomly selected weighted by face area
    • Finally just generate a random but valid Barycentric coordinate to pick a random point on the triangle itself

With this method you can generate any number of random points on a mesh with the only additional overhead of 1 float per triangle/face and no sorting or data copying required. This is probably not the absolute fastest way to generate random surface points, but it is fast enough, works well, and is easy to implement.

Here is a video demonstrating a Hierarchical Render Pass Manager for 3ds Max that I created:

I created Hierarchical Pass Manager for 3ds Max to ensure that anyone in the pipeline could pick up a scene file and be able to submit a correct render to the farm without having to know the details of how the scene needed to be setup to generate correct passes for compositing. Our previous batch system helped move us toward this goal, but relied on Scene States which are both buggy and hard to manage/update as the scene evolves. I was also not happy with any other existing pass manager solutions so I wrote my own. It was created in C# and WPF using the .NET API for 3ds Max (2012+) with some MAXScript shimming to get it integrated deeply.

Key Features:

  • Hierarchical pass system where nested passes inherit customizations from their parent passes.
  • Plugin based customization system where small customization can be combined to define a pass setup and new customizations can be created quickly and easily.
  • Object Sets system to define a higher-level abstraction for grouping geometry to apply customizations to (like object properties or V-Ray properties).
  • Auto generated and auto versioned viewport previews and render outputs, no need to ever enter a render output path.
  • V-Ray support with customizations for V-Ray frame buffer outputs, render elements, render settings, V-Ray properties, etc.
  • Custom viewport preview integration with overlays for frame, camera, pass, scene file
  • Pre-submit checks with both C# and MAXScript support to ensure bad renders don’t get submitted.
  • Batch farm submission with a single shared scene copy for multiple passes
  • PDPlayer integration for viewing the last render or viewport preview
  • Standalone editing and submission of passes without needing to launch 3ds Max at all!

This is quick video showing a WPF IronPython console I wrote for 3ds Max a while back:

The purpose of this project was to be able to be interactively probe 3ds Max’s .NET API as well as other .NET based tools inside 3ds Max. Technically you can do most of that directly from MaxScript, but I prefer IronPython since you get to use the Python language and access the CLR types in a more straight forward manner.

This was also just a good excuse to play with the IronPython C# embedding API as well mess with WPF some more. With 3ds Max 2014 now having direct support for CPython, this is a little less useful, but I thought I would still share it anyways.

I have open sourced my Partio Exporter for Softimage. The full source code is now available at github.

Here are the details from the readme:

This is a particle export plugin using the Partio library for Autodesk Softimage

To Build:

Use cmake to generate a VS solution file and build.

To Use:

  • Load the plugin from the plugin manager (“File” -> “Plugin Manager”).
  • Open the cache manager (“View” -> “Animation” -> “Cache Manager”).
  • Select the “Write” tab.
  • Set the format to “CUSTOM” and put in one of the supported file extensions in.
  • Make sure to modify the file name template to something like “[object][version][frame #4]” to get the padding on frame numbers.
  • Add your point cloud object to the objects list.
  • Set your scene range and channels to export in the “Additional Options” tab.
  • Hit the “Write Cache” button.

IMPORTANT note about missing channels:

If your channels are not showing up in your particle files you need to be aware of how ICE optimizes data. Even if you write to an attribute explicitly with Set Data, if it is not used, it will be optimized away automatically by ICE and will not be exported. See here for details and workarounds.

Supported Formats:

Only exporting is supported and not all Partio formats supported. The following formats are supported:

  • BGEO
  • BIN1
  • GEO
  • PDA
  • PDB
  • PDC
  • PRT

I tried to do some default channel name mapping for known channel types so that data works in the resulting format as expected. See the code for mapping details. The specifics might need adjusting depending on your format choice and end software choice.


  1. if you plan on using the .BIN format, please see my pull request for the partio library which fixes the .BIN exporter. The current version in partio/master does not work properly and will crash most programs that try to read .bin files written from partio. My pull request has been ignored so you will have to merge and re-build partio manually or just clone my fork of partio directly.

I have open sourced my Krakatoa for Softimage Plugin. The full source code is now available at github.

Here are the details from the readme:

This a simple plugin for Softimage that exposes the Krakatoa SR particle renderer from Thinkbox Software as a renderer in Softimage. It is a very basic implementation at this point. This plugin has NOT been tested in production is just intended as a test/example/starting point.

You will need a valid Krakatoa “Render” license from Thinkbox for this plugin to work.

To build you will also need the Krakatoa SR C++ SDK which can be downloaded from the Thinkbox website

Pull requests welcome.

Features:

  • Renders ICE point clouds with Krakatoa from within Softimage as a native C++ renderer plug-in
  • Custom ICE channels mapped to Krakatoa channels (see below)
  • Region Render tool support
  • Light Groups can be used to control with lights are used by Krakatoa
  • Occlusion mesh support
  • Multi-channel EXR output support

The Following ICE Channels are mapped for Krakatoa if they exist and are not being optimized away by ICE:

  • PointPosition
  • Color
  • Density
  • Lighting
  • MBlurTime
  • Absorption
  • Emission
  • PointNormal
  • Tangent
  • PointVelocity
  • Eccentricity
  • PhaseEccentricity
  • SpecularPower
  • SpecularLevel
  • DiffuseLevel
  • GlintGlossiness
  • GlintLevel
  • GlintSize
  • Specular2Glossiness
  • Specular2Level
  • Specular2Shift
  • SpecularGlossiness
  • SpecularShift