Writing Procedural Primitives with RenderDotC

Contents:
Introduction
DelayedReadArchive
RunProgram
DynamicLoad


Introduction

Procedural primitives provide a way to create geometry at render time, when an estimate of the size of the object in pixels is known.  This is a powerful tool for controlling level of detail and minimizing storage of geometry.

The RenderMan 3.1 spec provided one function for writing procedural primtives, RiProcedural().  The user would write a subdivide() function and a free() function, and pass function pointers through the RenderMan procedural interface.  A severe limitation of this feature was that it could not be used from RIB.  Due to the popularity of RIB, procedural primitives were rarely used.

RenderDotC supports three new types of procedural primitives that can be used from RIB.  Each is described below.


DelayedReadArchive

The simplest of the new procedural primitives simply reads a RIB file:
Procedural "DelayedReadArchive" ["sphere.rib"] [-1 1 -1 1 -1 1]
Where "sphere.rib" is the name of the RIB file and [-1 1 -1 1 -1 1] is the object space bound of the geometry in the RIB file.  DelayedReadArchive is similar to ReadArchive except that the archive is loaded at render time when the bound of the object is encountered.  If the object is off screen or entirely occluded by other geometry, then the archive won't be loaded at all.  For these reasons, it's generally preferable to use DelayedReadArchive rather than ReadArchive.  Some cases where you cannot use DelayedReadArchive are within a motion block (procedural primitives may not explicitly undergo deformational motion blur) or when accurate bounds are not known.

In the procedural interface, the syntax for DelayedReadArchive is:

RtString filename = strdup("sphere.rib");
RtBound bound = {-1, 1, -1, 1, -1, 1};
RiProcedural(&filename, bound, RiDelayedReadArchive, free);

RunProgram

Another way to create procedural geometry is to write a helper program that generates RIB:
Procedural "RunProgram" ["gencurve" "50"] [-1 1 -1 1 0 0]
Where "gencurve" is the name of an executable program, "50" is an arbitrary data string meaningful to the helper program, and [-1 1 -1 1 0 0] is the object space bound of the geometry to be generated by the program.  If and when the bound is encountered at render time, RenderDotC will launch the program and send it a string on its standard input.  The format of the string is:
"403.1 50\n"
Where 403.1 is the area of the bound in pixels, 50 is the data string from the Procedural command above, and \n is the newline character.  The helper program should then emit RIB on its standard output, presumably using a RIB client library.

Here is a complete example of a helper program:

#include <stdio.h>
#include <stdlib.h>
#include "ri.h"

int main(int argc, char **argv)
{
    RtPoint *P;
    RtInt *nvertices;
    RtFloat constantwidth = 0.01;
    RtFloat detail;
    int i, j, ncurves = 50, nsegs = 2, ncvs;
    char buf[256];

    while (gets(buf) != NULL) {
        sscanf(buf, "%g %d", &detail, &ncurves);
        ncvs = 3 * nsegs + 1;
        P = (RtPoint *)malloc(ncvs * ncurves * sizeof(RtPoint));
        nvertices = (RtInt *)malloc(ncurves * sizeof(RtInt));
        RiBegin(RI_NULL);
        for (i = 0; i < ncurves; i++) {
            nvertices[i] = ncvs;
            P[i*ncvs][0] = 2 * ((RtFloat)rand() / RAND_MAX) - 1;
            P[i*ncvs][1] = 2 * ((RtFloat)rand() / RAND_MAX) - 1;
            P[i*ncvs][2] = 0.0;
            for (j = 1; j < ncvs; j++) {
                P[i*ncvs+j][0] = 2 * ((RtFloat)rand() / RAND_MAX) - 1;
                P[i*ncvs+j][1] = 2 * ((RtFloat)rand() / RAND_MAX) - 1;
                P[i*ncvs+j][2] = 0.0;
            }
        }
        RiCurves(RI_CUBIC, ncurves, nvertices, RI_NONPERIODIC,
            RI_P, P, RI_CONSTANTWIDTH, (RtPointer)&constantwidth,
            RI_NULL);
        RiArchiveRecord(RI_COMMENT, "\377");
        RiEnd();
    }
    return 0;
}

The most important thing to note about this program is that it doesn't exit until its standard input is closed.  Instead, it runs as a loop, dispatching requests from the renderer.  Each loop is terminated with RiArchiveRecord(RI_COMMENT, "\377"), indicating to the renderer that it has finished processing one request.  RenderDotC keeps helper programs open in case they are used again.  In order to avoid wasting resources by launching and relaunching programs, helper programs should be designed with a main loop as above.  In the example, RiBegin() and RiEnd() are inside the main loop.  This is particularly important if the helper program generates compressed RIB, as the renderer will be expecting a new gzip header for each iteration.

The procedural interface for RunProgram is:

RtVoid freestrings(RtString *twostrings) {
    free((void *)twostrings[0]);
    free((void *)twostrings[1]);
    free((void *)twostrings);
}
RtString *twostrings = (RtString *)malloc(2 * sizeof(RtString);
twostrings[0] = strdup("gencurve");
twostrings[1] = strdup("50");
RtBound bound = {-1, 1, -1, 1, 0, 0};
RiProcedural(twostrings, bound, RiProcRunProgram, (RtFreeFunc)freestrings);

DynamicLoad

The most sophisticated of the new procedural primitives is the shared library:
Procedural "DynamicLoad" ["sphere" "1 -1 1 0 360"] [-1 1 -1 1 -1 1]
 Where "sphere" is the name of the compiled shared library without the extension (".so" on Unix, ".dll" on Windows), "1 -1 1 0 360" is an arbitrary string understood by the shared library, and [-1 1 -1 1 -1 1] is the object space bounds.  When the bounds is encountered at render-time, RenderDotC searches for the shared library on the path set by PROCEDURALS or Option "searchpath" "procedural".  If found, the shared library is loaded and the renderer makes use of three entry points:
RtPointer ConvertParameters(RtString paramstr)
RtVoid Subdivide(RtPointer data, RtFloat detail)
RtVoid Free(RtPointer data)
ConvertParameters takes the data string from the RIB (e.g. "1 -1 1 0 360") and converts it to dynamically allocated blind data for the Subdivide() and Free() functions.  All DynamicLoad procedurals must define all three entry points with exactly the names given above.  On Windows, these functions must be explicitly exported from the DLL.  If your source code is written in C++, then the three functions must be further declared extern "C".  If the subdivide function wishes to defer to one or more simple procedural primitives, it should call RiProcedural(data, bound, Subdivide, Free) and not RiProcedural(data, bound, RiProcDynamicLoad, Free).  See the example below.

Here's a non-trivial way to render a sphere:

#include <stdlib.h>
#include <math.h>
#include <float.h>
#include "ri.h"

#if defined(_Windows) || defined(_WIN32)
#define DLLEXPORT __declspec(dllexport)
#else
#define DLLEXPORT
#endif

struct spheredata {
    float radius;
    float zmin, zmax;
    float thetamin, thetamax;
};

#define min(a, b) ((a) < (b) ? (a) : (b))
#define max(a, b) ((a) > (b) ? (a) : (b))

/*
 * Forward declarations
 */
RtPointer DLLEXPORT ConvertParameters(RtString paramstr);
RtVoid DLLEXPORT Subdivide(RtPointer data, float detail);
RtVoid DLLEXPORT Free(RtPointer data);

RtPointer DLLEXPORT ConvertParameters(RtString paramstr)
{
    struct spheredata *sd =
        (struct spheredata *)malloc(sizeof(struct spheredata));
    sscanf(paramstr, "%f %f %f %f %f",
        &sd->radius,
        &sd->zmin,
        &sd->zmax,
        &sd->thetamin,
        &sd->thetamax);
    return (RtPointer)sd;
}

static RtVoid boundRevolve(RtBound b, float rmin, float rmax,
    float thetamin, float thetamax)
{
    /*
     * Cache commonly used sines and cosines.
     */
    float ctm = cosf(thetamin);
    float stm = sinf(thetamin);
    float ctx = cosf(thetamax);
    float stx = sinf(thetamax);

    /*
     * Compute x bounds
     */
    float atm = fabsf(thetamin);
    float atx = fabsf(thetamax);
    float m = min(ctx, ctm);            /* Consider theta with minimum x */
    if (atx > M_PI/2 && atm < 3*M_PI/2) {
        b[0] = -rmax;                   /* Best when patch crosses x-axis */
        if (atx < M_PI || atm > M_PI)
            b[0] *= -m;                 /* Doesn't cross, scale by cosine */
    }
    else                                /* xmin > 0: use rmin */
        b[0] = rmin * m;                /* Scale by cosine */

    m = max(ctx, ctm);                  /* Consider theta with maximum x */
    if (atm < M_PI/2 || atx > 3*M_PI/2)     /* xmax > 0: use rmax */
        b[1] = rmax * m;                /* Can't cross x-axis, just meet it */
    else                                /* xmax < 0: use rmin */
        b[1] = rmin * m;                /* Scale by cosine */

    /*
     * Compute y bound
     */
    atm = fabsf(M_PI/2 - thetamin);
    atx = fabsf(M_PI/2 - thetamax);
    m = min(stx, stm);                  /* Consider theta with minimum y */
    if (atx > M_PI/2 && atm < 3*M_PI/2) {
        b[2] = -rmax;                   /* Best when patch crosses y-axis */
        if (atx < M_PI || atm > M_PI)
            b[2] *= -m;                 /* Doesn't cross, scale by sine */
    }
    else                                /* ymin > 0: use rmin */
        b[2] = rmin * m;                /* Scale by sine */

    atm = fabsf(M_PI/2 + thetamin);
    atx = fabsf(M_PI/2 + thetamax);
    m = max(stx, stm);                  /* Consider theta with maximum y */
    if (atx > M_PI/2 && atm < 3*M_PI/2) {
        b[3] = rmax;                    /* Best when patch crosses y-axis */
        if (atx < M_PI || atm > M_PI)
            b[3] *= m;                  /* Doesn't cross, scale by sine */
    }
    else                                /* ymax > 0: use rmin */
        b[3] = rmin * m;                /* Scale by sine */
}

static RtVoid Bound(struct spheredata *s, RtBound b)
{
    /*
     * rmax is the maximum distance from the z-axis to a point on the sphere.
     * rmin is the minimum.
     * z0 is a temporary to help compute them.
     * Don't attempt to take the sqrt of a negative number.
     */
    float rmax, rmin, z0;
    if (s->zmin < 0 && s->zmax > 0)
        rmax = s->radius;
    else {
        z0 = min(fabsf(s->zmin), fabsf(s->zmax));
        if (s->radius - z0 > FLT_EPSILON)
            rmax = sqrtf(s->radius * s->radius - z0 * z0);
        else
            rmax = 0;
    }

    z0 = max(fabsf(s->zmin), fabsf(s->zmax));
    if (s->radius - z0 > FLT_EPSILON)
        rmin = sqrtf(s->radius * s->radius - z0 * z0);
    else
        rmin = 0;

    boundRevolve(b, rmin, rmax, M_PI*s->thetamin/180, M_PI*s->thetamax/180);

    /*
     * Compute z bounds
     */
    b[4] = s->zmin;
    b[5] = s->zmax;
}

RtVoid DLLEXPORT Subdivide(RtPointer data, float detail)
{
    struct spheredata *sd = (struct spheredata *)data;

    if (detail < 100 || detail > 0.9 * RI_INFINITY) {
        RiTransformBegin();
        RiRotate(sd->thetamin, 0, 0, 1);
        RiSphere(sd->radius, sd->zmin, sd->zmax, sd->thetamax - sd->thetamin,
            RI_NULL);
        RiTransformEnd();
    }
    else {
        struct spheredata *sd0 =
            (struct spheredata *)malloc(sizeof(struct spheredata));
        struct spheredata *sd1 =
            (struct spheredata *)malloc(sizeof(struct spheredata));
        struct spheredata *sd2 =
            (struct spheredata *)malloc(sizeof(struct spheredata));
        struct spheredata *sd3 =
            (struct spheredata *)malloc(sizeof(struct spheredata));

        float zhalf = 0.5 * (sd->zmin + sd->zmax);
        float thetahalf = 0.5 * (sd->thetamin + sd->thetamax);

        RtBound b;

        sd0->radius = sd->radius;
        sd0->zmin = sd->zmin;
        sd0->zmax = zhalf;
        sd0->thetamin = sd->thetamin;
        sd0->thetamax = thetahalf;
        Bound(sd0, b);
        RiProcedural((RtPointer)sd0, b, Subdivide, Free);

        sd1->radius = sd->radius;
        sd1->zmin = zhalf;
        sd1->zmax = sd->zmax;
        sd1->thetamin = sd->thetamin;
        sd1->thetamax = thetahalf;
        Bound(sd1, b);
        RiProcedural((RtPointer)sd1, b, Subdivide, Free);

        sd2->radius = sd->radius;
        sd2->zmin = sd->zmin;
        sd2->zmax = zhalf;
        sd2->thetamin = thetahalf;
        sd2->thetamax = sd->thetamax;
        Bound(sd2, b);
        RiProcedural((RtPointer)sd2, b, Subdivide, Free);

        sd3->radius = sd->radius;
        sd3->zmin = zhalf;
        sd3->zmax = sd->zmax;
        sd3->thetamin = thetahalf;
        sd3->thetamax = sd->thetamax;
        Bound(sd3, b);
        RiProcedural((RtPointer)sd3, b, Subdivide, Free);
    }
}

RtVoid DLLEXPORT Free(RtPointer data)
{
    free(data);
}

Compilation details depend up the platform.

SGI mips3:    CC -I$RDCROOT/include -mips3 -n32 -O -elf -shared sphere.c -o sphere.so
SGI mips4:    CC -I$RDCROOT/include -mips4 -n32 -O -elf -shared sphere.c -o sphere.so
Visual C++:   cl -I%RDCROOT%/include -LD sphere.c
Borland C++:  bcc32 -I%RDCROOT%/include -tWD sphere.c
Linux:        g++ -I$RDCROOT/include -shared sphere.c -o sphere.so
BSD/OS:       g++ -I$RDCROOT/include -shared sphere.c -o sphere.so

The procedural interface for DynamicLoad is:

RtVoid freestrings(RtString *twostrings) {
    free((void *)twostrings[0]);
    free((void *)twostrings[1]);
    free((void *)twostrings);
}
RtString *twostrings = (RtString *)malloc(2 * sizeof(RtString);
twostrings[0] = strdup("sphere");
twostrings[1] = strdup("1 -1 1 0 360");
RtBound bound = {-1, 1, -1, 1, -1, 1};
RiProcedural(twostrings, bound, RiProcDynamicLoad, (RtFreeFunc)freestrings);

Copyright © 2001-2006 Dot C Software, Inc. All rights reserved.
Dot C Software, Inc., 182 Kuuhale St., Kailua, HI 96734
(808) 262-6715 (voice) (808) 261-2039 (fax)
The RenderMan® Interface Procedure and RIB Protocol are:
Copyright © 1988, 1989, Pixar. All rights reserved.
RenderMan® is a registered trademark of Pixar.