Procedural Primitives

The goal for this introductory project was to get familiarized with C++ in order to generate RenderMan procedurals. 

The final result was achieved by loading a custom C++ script with a RenderMan Procedural and subsequently using an additional custom Python script to instantaneously update the data within Maya.

Process Breakdown

We were allowed to choose to either scatter RenderMan archives in a scene or place RenderMan procedural spheres on an animated model. I chose the latter.

I decided to use a previously existent model. I took it from Maya’s Content Browser.

I picked a dinosaur from the animals folder. I imported it into the scene and scaled it down to one tenth of its original size, since I’m more comfortable working on that scale.

I’m super stubborn. Since I wanted to use this model I had to rig it and animate it myself instead of using a pre-existing rig and animation from online. 

For it I made a very minimalist rig, skinned the raptor geometry to it and created some controllers around it to be able to animate it.

After excrutiating skin weight painting and setting up rig controllers to a satisfying degree I proceded to animate a walk cycle. Here’s the result of said animation:

I don’t specialize in animation nor rigging. Evidently the rig is rudimentary and so are my animating skills. The movement might be a little rough around the edges but good enough for the purposes of this project. 

Finally, I felt I needed a bit of set dressing for the scene. It would also justify the walk cycle, so I put the chicken dinosaur on a treadmill with some motivation in front of it to make it walk and I called the scene setup done!

The Nitty-Gritty

Now with the easy part done, it’s time to talk about the technical stuff, and I’ll try to be as clear as possible because it can get a little confusing really quickly. 

Normally when making use of external scripts within Maya, it is necessary to somehow tell the program to look for those scripts. The way to achieve this is by modifying the file “maya.env”.

In it, it’s needed to add custom paths that tell Maya to look for scripts in specific folders. The following are the key ones:
				
					MAYA_USER_DIR= C:\Users\Felipe Amaya\Documents\maya

//Sets the user directory.

RFM_SITE_PATH=$MAYA_USER_DIR\rfm_scripts

//A leftover from previous RenderMan versions. Added as an extra precaution.

RMS_SCRIPT_PATHS=$MAYA_USER_DIR\rfm_scripts\image_tool

//Tells Maya where to find custom (RenderMan For Maya) scripts.

MAYA_SCRIPT_PATH=$RFM_SITE_PATH/mel:$MAYA_USER_DIR\scripts

//Tells Maya where to find custom MEL scripts.
				
			

With Maya understanding where to look for stuff, the scripts can now be implemented within the software. Note that the C++ script is not being read automatically by Maya through the use of the previously set paths. Within Maya, it’s necessary to create a RenderMan procedural node and load the script with it.

The script loaded is not the “.cpp” file but rather the compiled file with extension “.dso” or “.dll”, depending on the operating system in use.

Note:

The “.cpp” has to be compiled previously. In the case of the class, we use professor Kesson’s Cutter.

 

To be able to make use of this, it was necessary to tell the RenderMan Procedural to use the additional Python script. In the script editor, on a Pyton tab, said script has to be loaded. With two short lines you can tell Maya to import and reload it:
				
					import pp_place_spheres
reload(pp_place_spheres)
				
			

Thanks to the custom paths that were setup initially, Maya knows where to look for the script. 

Now in the RenderMan Procedural, we tell it to make use of the Python script by typing this on the “Pre Shape Python Script” of the drop down menu “Scripts” on the RenderMan Procedural:

				
					import rfm2.api.strings as apistr; import pp_place_spheres;
pp_place_spheres.setDataStr(apistr.expand_string("<shape>"))
				
			

Additionally, custom attributes can be added to the Procedural node. With this C++ script, what’s being done is placing spheres on the coordinates of each vertex of a preexisting mesh. In my case, my animated chicken dinosaur. To put it in very simple terms, the spheres are generated using RenderMan procedural geometry at render time with a C++ code. The Python script is used to feed custom interface attributes and data to the script and update it. This allows to have easier control over the generated spheres, as well as faster feedback. I made a diagram that breaks down the matter visually: 

Specifically for this particular sequence, the results were achieved by keyframing the following custom attributes in the RenderMan Procedural to make the chicken dino puff up with spheres and then make them scatter and disappear.

Now, the final step to make sure this works is to build the proper parenting dependency between the procedural node and the geometry. The actual model/geometry has to be a child of the RenderManProcedural Transform. 

Now, if I explained everything correctly and you followed my steps to the letter, you should be able to reproduce this result in your own scene!

Here are the scripts responsible for generating and controlling the results of the RenderMan procedural that creates the spheres.

PlaceSpheresProc.cpp
				
					#include <ri.h>
#include <RixInterfaces.h>
#include <RiTypesHelper.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
  
// A RiProcedural must implement these functions. This is a fixed requirement.
extern "C"
{
    PRMANEXPORT RtPointer ConvertParameters ( RtString paramStr              );
    PRMANEXPORT RtVoid    Subdivide         ( RtPointer data, RtFloat detail );
    PRMANEXPORT RtVoid    Free              ( RtPointer data                 );
}
  
// A custom data structure for defining an arbitrary number of spheres
// positioned at locations defined by the "coords" array.
typedef struct {
    RtFloat radius;
    RtFloat *coords;        // an array of x,y,z coordinates
    RtInt    num_coords;
    } SpheresData;
    
RtFloat randBetween(RtFloat min, RtFloat max);
// ----------------------------------------------------
// A RiProcedural required function
// ----------------------------------------------------
//
RtPointer ConvertParameters(RtString paramStr) {
    // The strtok() function cannot be used on the paramStr directly because
    // it modifies the string. 
    long len = strlen(paramStr);
    
    // We could directly create a copy of the input paramStr as an array and
    // use the strcpy(), string copy, function.
    //char copyStr[len];
    //strcpy(copyStr, paramStr);
    
    // However, because the paramStr can be very large we allocate memory
    // from the main memory pool (the "heap") and then perform a block 
    // copy of the contents of paramStr.
    char *copyStr = (char*)calloc(len + 1, sizeof(char));
    memcpy(copyStr, paramStr, len + 1);
    
    // Allocate a block of memory to store one instance of SpheresData.
    SpheresData *dataPtr = (SpheresData*)calloc(1, sizeof(SpheresData));
    
    // Irrespective of how many values are specified by the paramStr we
    // know the first two values will specify the radius of the spheres
    // and the number of coordinates that define their 3D locations.
    sscanf(copyStr, "%f %d", &dataPtr->radius, &dataPtr->num_coords);
    
    // Allocate memory to store an array of coordinates
    RtInt num_floats = dataPtr->num_coords;
    dataPtr->coords = (RtFloat*)calloc(num_floats, sizeof(RtFloat));
    
    char *strPtr = strtok(copyStr, " ");
    strPtr = strtok(NULL, " "); // eat the radius value
    strPtr = strtok(NULL, " "); // eat the num coordinates value
    long count = 0;
    while(strPtr) {
        // Convert each string to a double precision floating point number
        dataPtr->coords[count] = strtod(strPtr, NULL);
        count++;
        strPtr = strtok(NULL, " "); // grab the next part of copyStr.
        }
    // Don't forget to free the memory that was allocated for the copied text.
    free(copyStr);
    return (RtPointer)dataPtr;
}
  
// ----------------------------------------------------
// A RiProcedural required function
// ----------------------------------------------------
RtVoid Subdivide(RtPointer data, RtFloat detail) {
    RtFloat radius = ((SpheresData*)data)->radius;
    RtInt     num_coords = ((SpheresData*)data)->num_coords;
    RtFloat *coords =  ((SpheresData*)data)->coords;
  
    for(int n = 0; n < num_coords; n = n + 3) {
        RtFloat x = coords[n];
        RtFloat y = coords[n + 1];
        RtFloat z = coords[n + 2];
        RiTransformBegin();
            RiTranslate(x,y,z);
            // To assign a color to each sphere un-comment the next two lines
            // and comment line 92
            //RtColor cs[1] = { {randBetween(0,1),randBetween(0,1),randBetween(0,1) } } ;
            //RiSphere(radius, -radius, radius, 360, "constant color Cs", (RtPointer)cs, RI_NULL);
            RiSphere(radius, -radius, radius, 360, RI_NULL);
        RiTransformEnd();
        }
    }
// ----------------------------------------------------
// A RiProcedural required function
// ----------------------------------------------------
RtVoid Free(RtPointer data) {
    free(((SpheresData*)data)->coords);
    free(data);
    }
    
// ----------------------------------------------------
// Our utility functions begin here 
// ----------------------------------------------------
RtFloat randBetween(RtFloat min, RtFloat max) {
    return ((RtFloat)rand()/RAND_MAX) * (max - min) + min;
    }
				
			
pp_place_spheres.py
				
					import maya.cmds as cmds
from random import uniform, seed
  
# In the "RenderManProceduralShape->Scripts->Pre Shape Python Script" copy the following text:
  
# import rfm2.api.strings as apistr;import pp_place_spheres;pp_place_spheres.setDataStr(apistr.expand_string("<shape>"))
def setDataStr(shapeName):
    tnode = cmds.getAttr(shapeName + '.target')
    # If the name of the transform node of the target shape has
    # not been specified we assume the RenderManProgram has been
    # parented to the polymesh that will "receive" the spheres.
    if len(tnode) == 0:
        tnode = cmds.listRelatives(shapeName, parent=True)[0]
        tnode = cmds.listRelatives(tnode, parent=True)[0]
    
    jitter = cmds.getAttr(shapeName + '.jitter')
    spread = cmds.getAttr(shapeName + '.particle_spread')    
    rad = cmds.getAttr(shapeName + '.radius')
    prob = cmds.getAttr(shapeName + '.prob')
    num_particles = cmds.getAttr(shapeName + '.num_particles')
    
    use_local_space = cmds.getAttr(shapeName + '.use_local_space')
    
    coords = get_coordinates(tnode, use_local_space)
    
    jittered_coords = []
    
    seed(1)
    
    for n in range(0, len(coords), 3):
        if uniform(0,1) > prob:
            continue
        for i in range(0,num_particles):
            original_x = coords[n]  + uniform(-jitter, jitter)
            original_y = coords[n+1]  + uniform(-jitter, jitter)
            original_z = coords[n+2]  + uniform(-jitter, jitter)
            x = original_x + uniform(-spread, spread)
            y = original_y + uniform(-spread, spread)
            z = original_z + uniform(-spread, spread)
            jittered_coords.extend([x,y,z])
                        
    rounded = []
    # Reduce precision to 3 decimal places - less text to pass.
    for coord in jittered_coords:
        rounded.append(round(coord,3))
    coords_str = " ".join(map(str, rounded))
    
    text = str(rad) + ' ' + str(len(jittered_coords)) + ' ' + coords_str
    cmds.setAttr(shapeName + '.data', text, type='string')
    
# Rreturns a flat list of coords ie. [x0,y0,z0,x1,y1,z1....]
def get_coordinates(tnode, useLocalSpace=True):
    verts = []
    shape = cmds.listRelatives(tnode, shapes=True)[0]
    num_verts = cmds.polyEvaluate(tnode, vertex=True)
    for n in range(num_verts):
        vert_str = shape + '.vtx[%d]' % n;
        vert_pos = cmds.pointPosition(vert_str, local=useLocalSpace) 
        verts.extend(vert_pos)    
    return verts

				
			

Final Thoughts

I find this level customization fascinating and impressive. I can see how it all can be astonishingly useful to facilitate solutions to complicated rendering problem. Additionally, being able to script in multiple languages is a great advantage. It grants an incredible amount of control over software and even hardware to the ones who know how to wrangle code. Unfortunately, I myself am not there yet.

Leave a Reply

Your email address will not be published. Required fields are marked *