PRMan for Python

PRMan for Python

A Bridge from PRMan to Python (and Back)

April, 2009

Introduction

  • Note

    This document assumes familiarity with Python. You can learn more about Python on the Web, at www.python.org.

RenderMan Pro Server 14 introduces prman_for_python, a Python plugin module that allows you to build Pixar's RenderMan into your Python applications. With the plugin module prman_for_python.so comes a simple Python wrapper module, prman.py. Together they make accessing the power of prman as easy as py.

prman_for_python can be used to write RIB files and to render pixels directly from Python. You can even express RenderMan procedural primitive plugins (procprims) and Ri filters (rifs) directly in Python. Here's a simple example that can produce either a RIB file or final pixels:

% setenv PYTHONPATH $RMANTREE/bin   # this is where prman.py lives
% python
import prman
ri = prman.Ri() # create an instance of the RenderMan interface
rendertarget = "helloworld.rib"
ri.Begin(rendertarget)  # set rendertarget to ri.RENDER to render pixels
ri.Display("helloworld.exr", "openexr", "rgba")
ri.Format(512,512,1)
ri.Projection(ri.PERSPECTIVE, {ri.FOV: 45}) # standard Ri tokens are available
ri.Translate(0,0,10)
ri.WorldBegin()
ri.ジオメトリ("teapot")
ri.WorldEnd()
ri.End()

There is a great deal of similarity between a Python script and a RIB file. As we'll see, Python files can express anything that RIB files can, but because Python is a full-blown scripting language, it can express much more. Consider:

for i in range(0,20):
    ri.Translate(0,0,i)
    ri.Sphere(.5, -.5, .5, 360)

Python's support for loops, data structures and logic far exceed the power of RIB, and this can make source files much more editable and expressive. For example, we can change this script to produce 30 spheres with a one-character change. Sadly, this expressiveness comes at a cost in performance. The time and memory required to process a large scene description can be significantly more with Python than RIB. The good news is that you can get the combined benefits of both. We recommend that you carefully consider the tradeoffs before converting your pipeline entirely over to Python.

Here's a simple take on a hybrid approach that relies on ReadArchive to intermix RIB and Python (delayed archives work, too):

ri.Begin()
for char in ["plant1", "plant2", "bug1", "bug2", "set1", "set2"]:
    setupState(ri, char)  # a custom python procedure
    ri.ReadArchive(char + ".rib")
ri.End()

Clearly, any Python bridge to RenderMan opens numerous doors to exciting pipeline capabilities. Many RenderMan users are already well aware of this and have mature Python-based pipelines. There is significant latitude for expressing the RenderMan Interface within Python. Our binding strives for simplicity and relies on the combination of positional parameters, unstructured Python sequences, and Python dictionaries. To express a sphere with primitive variables, for example:

radius,zmin,zmax,angle = (1,-1,1,360)
primvars = {"uniform color Cs": [1,0,0],
            "varying float myvar: [.1, .2, .3, .4]}
ri.Sphere(radius, zmin, zmax, angle, primvars)

If our choices for binding don't match the bindings in place at your studio you can still refer to the prman_for_python feature-set to assess how to integrate some of the new capabilities exposed in the revised PRMan SDK. prman_for_python is actually only a thin layer atop RenderMan Pro Server's essential libprman.so. You can think of the bridge as a useful example application of the PRMan SDK as well as a useful stand-alone tool.

The prman Layer

prman.py represents the highest level layer of the bridge to prman. prman.py is a Python module that exports a combination of procedures, constants, and class definitions. You must first import the module to gain access to prman. Python offers a number of methods to ensure a module import succeeds. Assuming you can successfully import prman, you can cause your Python application to mimic the behavior of the classic prman executable. Here's how:

import prman
prman.Init(["-progress"])  # a list of string arguments, same as prman executable
ri = prman.Ri()
ri.Begin(ri.RENDER)
for r in ribfilelist:
    ri.ReadArchive(r)
ri.End()

You can also cause your app to behave more like classic catrib:

import prman
ri = prman.Ri()
ri.Option("rib", {"string asciistyle": "indented"})
ri.Begin("-")       # Special filename for stdout
for r in ribfilelist:
    prman.ParseFile(r)
ri.End()

Here are the exports of the prman layer:

prman.Init(arglist)         # optional initialization
prman.Cleanup()             # optional cleanup
prman.RifInit(riflist)      # reset the current Rif state
prman.ParseFile(filename, layer=FirstLayer) # parse a RIB file in a Rif layer
prman.Ri()                  # class definition, create an Ri instance
prman.Rif()                 # class definition, create a Rif instance
prman.ThisLayer             # (plus NextLayer, FirstLayer, used by prman.ParseFile)
prman.Version               # (plus a few other useful constants)
prman.RicFlush(marker, synchronous, flushmode) # Synchronize with a detached prman
prman.RicGetProgress()      # returns percentage of a render completed
prman.RicProcessCallbacks() # delivers queued error messages and pixels for display

The Ri Layer

The Ri layer is at the heart of prman and embodies the RenderMan Interface. All standard Ri procedure calls are provided. Rather than require complex custom data structure support, we've opted for the simple, generic approach, wherein standard Python entities are preferred. To represent an array of three rgb colors, we require/support any of these representations:

[1, 0, 0, 0, 1, 0,  0, 0, 1]    # list
(1, 0, 0, 0, 1, 0,  0, 0, 1)    # tuple
array.array('f',  [1, 0, 0, 0, 1, 0,  0, 0, 1] )  # array

RenderMan's variable-length parameter list is represented in prman_for_python as a standard Python dictionary whose keys are the parameter declaration and whose values are scalars or sequences whose length is governed by the declaration and standard binding semantics (see the Primitive Variable Application Note for more details). Here's a typical ri.Option call:

ri.Option("user", {
    "string pass_id": "perspShape_Final",
    "string pass_phase": "/Job/Frames/Images",
    "string pass_class": "Final",
    "string pass_flavor": "",
    "string pass_crew": "",
    "string pass_camera_name": "perspShape",
    "string pass_camera_flavor": "",
    "int pass_features_trace": [0]
    })

The Ri layer also provides standard variables and tokens like these:

Ri.RGBA             # (plus all standard Ri tokens)]
Ri.BEZIERSTEP           # (and all basis steps)
Ri.BezierBasis          # (and all standard bases)
Ri.BoxFilter            # (all standard filters)
Ri.ErrorPrintOnce       # (and all standard error handlers)
Ri.ProcDelayedReadArchive   # (all standard procprims)

Unlike the C binding, prman_for_python is able to infer the size of varying length arrays directly, and does not require passing in the size as a separate parameter. For example, the Python bindings for the Polygon creation routines do not require passing in the numbers of vertices, loops, or polygons. Similarly, the binding for subdivision meshes does not require specifying the number of faces or tags:

ri.Polygon({ri.P:pointData, ri.CS:colorData})
ri.GeneralPolygon (nvertices, {ri.P:genPolyPointData})
ri.PointsPolygons(pointsPolyNvertices, pointsPolyVertices,
                  {ri.P:pointsPolyPointData})
ri.PointsGeneralPolygons(nloops, pointsPolyNvertices, pointsPolyVertices,
                         {ri.P:pointsPolyPointData})
ri.SubdivisionMesh("loop", [3, 3, 3, 3], verts, [ri.CREASE],
                   [2, 1], [3, 0], [20], {ri.P:pointData})

Finally, perhaps the most novel feature of prman_for_python is its support for RenderMan plugins expressed in Python. With respect to the Ri layer, this amounts to support for Python-based procedural primitives. Consider this Python snippet:

def MengerSubdivide(data, detail):
    ri,x,y,z,size,depth = data
    ... more code here...
    ri.Sphere(.25, -.25, .25, 360)

ri.Procedural((ri, 0, 0, 0, 1, 4), [0, 1, 0, 1, 0, 1], MengerSubdivide, None)

The Procedural request registers a Python callback routine that can be used to generate additional geometry on demand. If the renderer requires the service it will pass its first argument (here, a simple tuple) to the data parameter of the MengerSubdivide procedure. Python's rich data-structure and serialization capabilities offer numerous options. Since RIB doesn't support arbitrary subdivide callbacks, this capability is only supported when rendering directly (via ri.Begin(ri.RENDER)).

The Rif Layer

Ri Filter plugins can also be expressed with Python. This makes it trivial to manipulate the Ri stream at the mouth of the renderer or simply to filter RIB files. To define a new Rif, simply subclass the provided Rif base class as follows:

import prman
class myRif(prman.Rif):  # myRif is a subclass of prman.Rif
    def __init__(self, ri):
        prman.Rif.__init__(self, ri)  # make sure superclass is properly
                                      # initialized

    def シェーディングRate(self, rate):  # provide a シェーディングRate filter
        out = rate * 3
        # print "changing シェーディングRate from : %f to %f" % (rate, out)
        self.m_ri.シェーディングRate(out)

Now, create Rif instances and register them with the prman layer:

ri = prman.Ri()
rif1 = myRif(ri)
prman.RifInit([rif1])  # you can build a list of rif instances, comprising a Rif Chain

Now the Rif is considered active. You can filter an arbitrary stream of Ri calls now. The mouth-of-the-renderer option is usually the best:

ri.Begin(ri.RENDER)
for r in ribfilelist:
    ri.ReadArchive(r)
ri.End()

But you can also run your python rifs on a RIB file. Here's a rif to convert subdivs to polygons.

#!/usr/bin/env python
#
# An example prman_for_python rif that converts all
# subdivs in a rib file into points-general-polys.
#
# usage: subdiv2poly input.rib output.rib
#

import prman
import sys

class myRif(prman.Rif):
   def __init__(self, ri):
       prman.Rif.__init__(self, ri)
       self.m_nsubdivs = 0

   def SubdivisionMesh(self, mask, nverts, verts, tags, nargs,
                       intargs, floatargs, plist):
       # Convert to general polygons since faces may be concave.
       # We currently do nothing special to look for holes.
       # nloops is an array of 1 whose length is the same of nverts
       nloops = [1 for i in range(len(nverts))]
       self.m_ri.PointsGeneralPolygons(nloops, nverts, verts, plist)
       self.m_nsubdivs = self.m_nsubdivs + 1

if len(sys.argv) == 3:
   infile = sys.argv[1]
   outfile = sys.argv[2]
   prman.Init(["-catrib", outfile, "-progress"])
   ri = prman.Ri()
   rif1 = myRif(ri)
   prman.RifInit([rif1])
   ri.Begin(ri.RENDER)
   prman.ParseFile(infile)
   ri.End()
   print("Converted %d subdivs to polys from %s into %s" %
           (rif1.m_nsubdivs, infile, outfile))
else:
   print("usage: %s infile.rib outfile.rib" % sys.argv[0])

Finally, to disable Rif, simply reinitialize:

prman.RifInit([])

The Ric Layer

As noted above, the Ric API is provided by the prman layer. For more information on Ric please refer to Ri Control.

Caveats, Limitations, Future Directions

We currently require Python 2.5. As Python's plugin mechanisms are platform-specific and subject to change, it is possible that prman_for_python won't be compatible with your site's standard install. We anticipate extending the Python binding to expose all sensible public interfaces of libprman but have not done so (yet) for the Point Cloud and Deep テクスチャ APIs. Currently there is a limit to a single Ri instance, but you can nest ri.Begin()/End() blocks to, for example, write out RIB files while rendering.