March, 1997 (Revised May, 2013)
- Using Existing Procedural Primitives
- Authoring Custom Procedural Primitives
- Details and Notes
RenderMan procedural primitives (procprims, for short) are user-provided subroutines that can be called upon to generate geometry (or issue other Ri requests) during the process of rendering. The advantage of procprims over concrete (RIB-based) primitives is that they can represent complex geometry very efficiently. Procprims can produce incredible geometric complexity from a small number of inputs, and we can defer this data amplification until the renderer is prepared to handle it. This may make it possible to render much more complex scenes than would be possible with a non-procedural representation. Procprims can also be thought of as units of memory management. The renderer can elect to unload all concrete geometry associated with a procprim knowing that the geometry can be regenerated should there be further need for it.
To describe procedural primitives to RenderMan, use either RiProcedural or RiProcedural2. Both entrypoints have the same underlying rendering characteristics, so developers should choose the interface that best suits their needs. They both accept callback parameters that are invoked at a later point in the rendering process. The primary distinction between the calls relates to the means by which opaque, plugin-specific data is represented. The original call characterizes custom data with an opaque pointer (a string for RIB) and as a consequence must request cleanup services from the plugin. The newer call uses standard RI parameterlists to convey the custom data and thereby needs no additional cleanup support from plugin.
In a direct-linked setting it's possible to provide the required callbacks as arguments to the RiProcedural/RiProcedural2 calls and you're done. In a RIB-based pipeline this can't work since callbacks are characterized by a function pointer, which has no meaning outside of the running process. The requirement is to encapsulate a reference to your procedural primitive and its parameters for deferred execution. RenderMan supports several built-in procprims to facilitate this very requirement. These are intended to work well in both deferred and immediate modes of operation.
A large part of the value of procedural primitives derives from the deferred and reloadable nature of the data amplification. These procprims simply defer the loading of RIB files and require no additional custom plugins. As such Delayed Archives are probably the most common form of procprim.Procedural primitive subroutines are implemented in a plugin (.dll, .so) that will be loaded by the renderer when it reads the RIB file. The RIB file must include the name of the plugin as well as its parameters. Once the plugin is loaded the routines are bound to internal primitives and these will be invoked later when the plugin's services are required.External programs can be called upon to generate RIB following the procedural geometry pattern. The RunProgram interface models the invocation of the standard subroutines as simple message over a basic interprocess communication (IPC) channel. Custom code executes in response to an IPC message and RIB is produced for consumption by the renderer, again via an IPC channel.
Using Existing Procedural Primitives
Delayed RIB File Reading
The simplest of the procprims is Delayed Read Archive. The existing interface for "including" one RIB file into another is RiReadArchive. Delayed Read Archive operates exactly like RiReadArchive, except that the reading is delayed until the procedural primitive bounding box is reached, unlike RiReadArchive which reads RIB files immediately during parsing. The advantage of the new interface is that since the reading is delayed, memory for the read primitives is not used until the bounding box is actually reached. In addition, if the bounding box proves to be off-screen, the parsing time of the entire RIB file is saved. The disadvantage is that an accurate bounding box for the contents of the RIB file is required, which was never needed before.
In RIB, the syntax for the Delayed Read Archive primitive is:
Procedural "DelayedReadArchive" [ "yourfilename" ] [ bound ]
Procedural2 "DelayedReadArchive" "SimpleBound" "string filename" "yourfilename" "float __bound" [xmin xmax ymin ymax zmin zmax]
In place of "yourfilename" you must provide a pathname for the desired RIB file. If your RIB archive is referred to with a relative path we search the paths given by the archivepath setting. As with all RIB parameters that are bounding boxes, the bound is an array of six floating point numbers measured in the current object space.
In C, the syntax for calling the Delayed Read Archive primitive is:
RtString *data; RtBound bound; RiProcedural(data, bound, RiProcDelayedReadArchive, freedata);
data is a pointer to a single RtString which contains the filename (ie. data is a char **). bound is a pointer to six floats which contain the bounding box. freedata is the user free method which frees data. data could be a global variable, but almost certainly cannot be an automatic (local) variable, as it must persist until the renderer calls the free routine. In practice, data would probably be malloced, and freedata would merely free() data.
And here's the Procedural2 variant:
RtBound bbox; RtString filename; filename = "yourfilename.rib"; RiProcedural2(RiProc2DelayedReadArchive, RiSimpleBound, "string filename", filename, "float __bound", bbox, RI_NULL);
RiSimpleBound is the name of a built-in bound subroutine that searches the parameterlist for conventionally named parameters. This paves the way for more procedural bounding callbacks, but for most purposes RiSimpleBound should suffice.
Procedural Primitive DSOs
A more efficient method for accessing subdivision routines is to write them as dynamic shared objects (DSOs), and dynamically load them into the renderer executable at runtime. In this case, developers write a subset of the subdivision, bound and free routines exactly as required in the direct-linked setting. They are compiled with special compiler options to make them runtime loadable, and you specify the name of the shared .so, .dll file in the RIB file. The renderer loads the DSO the first time it is needed to subdivide a primitive, and from then on it is called as if (and executes as fast as if) it were statically linked. When referring to a DSO keep the following in mind:
- Relative references are resolved via the procedural searchpath setting.
- File extensions are optional and, when omitted, may result in more portable Ri stream.
In RIB, the syntax for specifying a dynamically loadable procedural primitive is:
Procedural "DynamicLoad" [ "dsoname" "initial" ] [ bound ]
dsoname is the name of the .so file that contains the required entry-points, and has been compiled and prelinked as described below. The current implementation intentionally subverts the LD_LIBRARY_PATH mechanism for searching for DSOs to load, so full pathnames for the DSO archive should be used.
initial is the ASCII printable string that represents the initial data to be sent to the ConvertParameters routine. The bound is an array of six floating point numbers which is xmin, xmax, ymin, ymax, zmin, zmax in the current object space.
And here's the Procedural2 variant:
Procedural2 "DynamicLoad" "SimpleBound" "string __dsoname" "yourdsoname" "float __bound" [xmin xmax ymin ymax zmin zmax] // additional meta-arguments to renderer ... // arguments to plugin ...
Here, we employ the RiSimpleBound (and the associated bound parameter) to deliver our bounding box to the renderer. An alternate built-in bounding procedure, RiDSOBound, is provided to vector the bound request to a subroutine exported from the given DSO. This eliminates the requirement that the bound be emitted into the RIB file. RiDSOBound will only work if target DSO has implemented and exported the Bound subroutine.
Please note that the Procedural2 implementation of DynamicLoad supports meta-parameters to control when and in which context the Subdivide2 function will execute. These meta-parameters may be interspersed with the arguments intended for the plugin. Since all meta-parameters are prefixed with '__', plugins can easily discriminate meta parameters from standard plugin parameters. For backwards compatibility, the meta-parameters "dsoname" and "bound" are supported but deprecated in favor of "__dsoname" and "__bound".
Here are two variations of the RIB represention for a trivial DynamicLoad procedural primitive:
Procedural "DynamicLoad" ["a_sphere" "3.0" ] [-3 3 -3 3 -3 3] Procedural2 "DynamicLoad" "SimpleBound" "string __dsoname" "a_sphere" "float __bound" [-3 3 -3 3 -3 3] "float radius" 
More complex initialization requirements require either more structure in the initial string data for Procedural or more parameters for Procedural2. As the initial data requirements grow, the benefits of Procedural2 become more apparent.
In C, the syntax for calling the dynamic load primitive is:
RtString *data; RtBound bound; RiProcedural(data, bound, RiProcDynamicLoad, freedata) RtInt nargs; RtToken toks[...]; RtPointer vals[...]; toks = (RtToken) "float __bound"; // RI_BOUND vals = (RtPointer) bound; nargs = 1; // .. plugin data here .. RiProcedural2(RiProc2DynamicLoad, RiSimpleBound, nargs, toks, vals);
data is a pointer to an array of two RtStrings (i.e. data is a char **) that contains the DSO archive name in data, and the initial primitive data set in ASCII in data (this will be sent to the ConvertParameters routine). bound is a pointer to six floats that contain the bounding box. freedata is the user free method that frees data. data could be a global variable, but almost certainly cannot be an automatic (local) variable, as it must persist until the renderer calls the free routine. In practice, data and its strings would be allocated with malloc and freedata would call free on data, data, and data.
Note that when the Subdivide routine wants to create a child procedural primitive of the same type as itself, it should call
RiProcedural(data, bound, Subdivide, Free)
passing its private notions of data and free, and not
whose primary role is to simply load the DSO and to be representable in a RIB file. In the case of RiProcedural2 it may be advantageous for recursive procedural primitives to revert to the use of RiProcedural since it supports an unmarshalled representation of the plugin's state. In other words, the value of Procedural2 interface is primarily in the automatic serialization and deserialization to and from RIB. Once a plugin has been loaded and initialized, it's more convenient to convey data to sub-procedurals directly in the most natural (and private) struct or object representation.
RIB Generating Program
Another tool in our procprim toolkit is the RIB Generating Program. Here, the idea is that a separate program is harnessed to generate RIB at the behest of the renderer. As will be seen below, each generated procedural primitive is described by a request to the helper program, in the form of an ASCII datablock that describes the primitive to be generated. This datablock can be anything that is meaningful and adequate to the helper program, such as a sequence of a few floating point numbers, a filename, or a snippet of code in an interpreted modeling language. In addition, as above, the renderer supplies the detail of the primitive's bounding box, so that the generating program can make decisions on what to generate based on how large the object will appear on-screen.
In RIB, the syntax for specifying a RIB-generating program procedural primitive is:
Procedural "RunProgram" [ "program" "datablock" ] [ bound ]
program is the name of the helper program to execute, and may include command line options. datablock is the generation request datablock. It is an ASCII string which is meaningful to program, and adequately describes the children that are to be generated. Notice that program is a quoted string in the RIB file, so if it contains quote marks or other special characters these must be escaped in the standard way. The bound is an array of six floating point numbers - xmin, xmax, ymin, ymax, zmin, zmax - in the current object space.
In C, the syntax for calling the RIB-generating program procedural primitive is:
RtString *data; RtBound bound; RiProcedural(data, bound, RiProcRunProgram, freedata)
data is a pointer to an array of two RtStrings (i.e. data is a char **) that contain the program name in data, and the generation request data block in ASCII in data. (The program name may include command line options.) bound is a pointer to six floats which contain the bounding box. freedata is the user free method that frees data. data could be a global variable, but almost certainly cannot be an automatic (local) variable, as it must persist until the renderer calls the free routine. In practice, data and its strings would allocated with malloc, and freedata would call free on data, data, and data.
Procedural Primitive Pathnames
PRMan provides a mechanism for selecting among several compiled versions of a procedural program or plug-in, depending on which platform the RIB file is being rendered on. The renderer will expand the special strings $ARCH or %ARCH in pathnames for procedural primitive programs and plug-ins, substituting a string that identifies the basic platform for which the running prman was built. For example:
Procedural "DynamicLoad" ["/net/prmanDSOs/$ARCH/a_sphere" ...]
The default architecture names are:
|Default $ARCH name||PRMan build|
|linux-x86||Linux, 32-bit, for x86 systems|
|linux-x86-64||Linux, 64-bit, for x86-64 systems|
|windows-x86||Windows XP/Vista, 32-bit, for x86 systems|
|windows-x86-64||Windows Vista-64, 64-bit, for x86-64 systems|
|osx-x86||Mac OS X, 32-bit, for x86 systems|
|osx-x86-64||Mac OS X, 64-bit, for x86-64 systems|
These names can be remapped into site-specific abstract names by placing a line in the rendermn.ini file, for example:
/architecture/default_name new_name /architecture/linux-x86 gcc41glibc24
The ARCH value can also be set directly, using the shell environment variable RMAN_ARCHITECTURE, which overrides both the default and rendermn.ini values.
Details and Notes
Prior to subdividing a procedural primitive, the renderer restores the graphics state to the state that existed when the procedural primitive was itself created. The subdivision routine can call any RI routine that is legal within the world scope, so it may modify any attribute in the graphics state before generating new primitives (procedural or otherwise). In other words, procedural primitives are free to manipulate the color, shaders, or any other attribute of their children.
Motion blur of procedural primitives is tricky. Procedural primitives, per se, cannot exist within motion blocks, because this would force the renderer to do some sort of correspondence-matching between the children of the time 0.0 procedural primitive and the children of the time 1.0 procedural primitive. However, once the procedural primitive has decided to subdivide itself into standard RenderMan primitives, it is completely legal for those primitives to be emitted with full motion blocks. Thus, it is the responsibility of the procedural primitive methods themselves to understand "vertex motion" and generate the appropriate moving primitives at the leaf level. Note, however, that normal transformation matrix motion blur is perfectly legal on procedural primitives.
In situations where the renderer is unable to calculate the correct detail to pass into the subdivide method (for example, if the procedural primitive crosses the camera plane), the subdivision detail will be set to some extremely large number (quite possibly infinity). Subdivision methods must be able handle this case without generating infinite amounts of leaf data.
The RenderMan Interface has a command for modifying the detail that is sent to a procedural primitive. RiRelativeDetail is an attribute that provides a scaling factor for the details sent to the subdivision routine. This is a separate control from the RiShadingRate, which will determine the shading rate of the final "standard" RenderMan primitives that are generated. RiProcedurals can employ RxAttribute to obtain access to the current shading rate.
If the bounding box of a child of a procedural primitive extends outside the bounding box of the parent, the resulting image will very likely have dropouts. However, the renderer does not generate any warning messages in this case. Be sure that your bounding boxes do in fact contain all the geometry that will be generated at all lower levels. If primitives with multi-segment motion blur (more than two motion samples in their motion blocks) are generated by a procedural plugin that implements a Bound routine, the path between the shutter-open and shutter-close bounds must contain all primitive motion samples.