Blue Moon Rendering Tools 2.4

Part 8: Using BMRT as a "Ray Server" for PRMan


revised 9 December 1998


Larry I. Gritz
gritzl@acm.org



Abstract

This chapter explains how to render scenes using PRMan with ray traced shadows and reflections, using BMRT as an "oracle" to provide answers to computations that PRMan cannot solve. We describe a method of actually stitching the two renderers together using a Unix pipe, allowing each renderer to perform the tasks that it is best at.

Introduction

PhotoRealistic RenderMan has a Shading Language function called trace(), but since there is no ability in PRMan to compute global visibility, the trace() function always returns 0.0 (black). This is no way to ask for any other global visibility information in PRMan. Though PRMan often can fake reflections and shadows with texture mapping, there are limitations:

Both renderers share much of their input - by being RenderMan compliant, they both read the same geometry description (RIB) and shader source code files. (Note: The compatibility is limited to areas dictated by the RMan spec. The two renderers each have different formats for stored texture maps and compiled shaders, and support different feature subsets of the spec.) It's tempting to want to combine the effects of the two renderers, using each for those effects that it achieves well. Several strategies come to mind:

  1. Choosing one renderer or the other based on the project, sequence, or shot. Perhaps a strategy might be to use PRMan most of the time, BMRT if you need radiosity or ray tracing.
  2. Rendering different objects (or layered elements) with different renderers, then compositing them together to form final frames.
  3. Rendering different lighting layers with different renderers, then adding them together. For example, one might render base color with PRMan, but do an ``area light pass'' (or radiosity, or whatever) in BMRT.

All of these approaches have difficulties (though all have been done). Strategy #1 may force you to choose a slow renderer for everything, just because you need a little ray tracing. There may also be problems matching the exact look from shot to shot, if you are liberally switching between the two renderers. Strategies #2 and #3 have potential problems with "registration," or alignment, of the images computed by the renderers. Also, #3 can be very costly, as it involves renders with each renderer.

The attraction of using the two renderers together, exploiting the respective strengths of both programs while avoiding undue expense, is alluring. I have developed a method of literally stitching the two programs together.

Background: DSO Shadeops in PRMan

RenderMan Shading Language has always had a rich library of built-in functions (sometimes called ``shadeops''), already known to the SL compiler and implemented as part of the runtime shader interpreter in the renderer. This built-in function library included math operations (sin, sqrt, etc.), vector and matrix operations, coordinate transformations, etc. It has also been possible to write SL functions in Shading Language itself (in the case of PRMan, this ability was greatly enhanced with the new compiler included with release 3.7). However, defining functions in SL itself has several limitations.

The newest release of PRMan (3.8) allows you to write new built-in SL functions in 'C' or 'C++'. Writing new shadeops in C and linking them as DSO's has many advantages over writing functions in SL, including:

DSO shadeops also have several limitations that you should be aware of:

Further details about DSO shadeops, including exactly how to write them, are well beyond the scope of these course notes. For more information, please see the RenderMan Toolkit 3.8 User Manual.

How Much Can We Get Away With?

So PRMan 3.8 has a magic backdoor to the shading system. One thing it's good for is to make certain common operations much faster, by compiling them to machine code. But it also has the ability to allow us to write functions which would not be expressible in SL at all --- for example, file I/O, process control or system calls, constructing complex data structures, etc.

How far can we push this idea? Is there some implementation of trace() that we can write as a DSO which will work? Yes! The central idea is to render using PRMan, but implement trace as a call to BMRT. In this sense, we would be using BMRT as an oracle, or a ray server, that could answer the questions that PRMan needs help with, but let PRMan do the rest of the hard work.

BMRT (release 2.3.6 and later) has a ray server mode, triggered by the command line option -rayserver. When in this mode, instead of rendering the frame and writing an image file, BMRT reads the scene file but it just waits for ``ray queries'' to come over stdin. When such queries (specified by a ray server protocol) are received, BMRT computes the results of the query, and returns the value by sending data over stdout.

The PRMan side is a DSO which, when called, runs rendrib and opens a pipe to its process. Thereafter, calls to the new functions make ray queries over the pipe, then wait for the results.

New Functionality

This hybrid scheme effectively adds six new functions that you can call from your shaders:
color trace (point from; vector dir)
Traces a ray from position from in the direction of vector dir. The return value is the incoming light from that direction.
color visibility (point p1, p2)
Forces a visibility (shadow) check between two arbitrary points, retuning the spectral visibility between them. If there is no geometry between the two points, the return value will be (1,1,1). If fully opaque geometry is between the two points, the return value will be (0,0,0). Partially opaque occluders will result in the return of a partial transmission value.

An example use of this function would be to make an explicit shadow check in a light source shader, rather than to mark lights as casting shadows in the RIB stream (as described in the previous section on nonstandard attributes). For example:

    light
    shadowpointlight (float intensity = 1;
    	              color lightcolor = 1;
	              point from = point "shader" (0,0,0);
                      float raytraceshadow = 1;)
    {
        illuminate (from) {
            Cl = intensity * lightcolor / (L . L);
            if (raytraceshadow != 0)
                Cl *= visibility (Ps, from);
        }
    }
float rayhittest (point from; vector dir; 
                  output point Ph; output vector Nh) 
Probes geometry from point from looking in direction dir. If no geometry is hit by the ray probe, the return value will be very large (1e38). If geometry is encountered, the position and normal of the geometry hit will be stored in Ph and Nh, respectively, and the return value will be the distance to the geometry.
float fulltrace (point pos; vector dir;
                 output color hitcolor; output float hitdist;
                 output point Phit; output vector Nhit;
                 output point Pmiss; output vector Rmiss)
Traces a ray from pos in the direction dir.

If any object is hit by the ray, then hitdist will be set to the distance of the nearest object hit by the ray, Phit and Nhit will be set to the position and surface normal of that nearest object at the intersection point, and hitcolor will be set to the light color arriving from the ray (just like the return value of trace).

If no object is hit by the ray, then hitdist will be set to 1.0e30, hitcolor will bet set to (0,0,0).

In either case, in the course of tracing, if any ray (including subsequent rays traced through glass, for example) ever misses all objects entirely, then Pmiss and Rmiss will be set to the position and direction of the deepest ray that failed to hit any objects, and the return value of this function will be the depth of the ray which missed. If no ray misses (i.e. some ray eventually hits a nonreflective, nonrefractive object), then the return value of this function will be zero. An example use of this functionality would be to combine ray tracing of near objects with an environment map of far objects:

        missdepth = fulltrace (P, R, C, d, Phit, Nhit, Pmiss, Rmiss);
        if (missdepth > 0)
            C += environment ("foo.env", Rmiss) / missdepth;

The code fragment above traces a ray (for example, through glass). If the ray emerging from the far side of the glass misses all objects, it adds in a contribution from an environment map, scaled such that the more layers of glass it went through, the dimmer it will be.

float isshadowray ()
Returns 1 if this shader is being executed in order to evaluate the transparency of a surface for the purpose of a shadow ray. If the shader is instead being evaluated for visible appearance, this function will return 0. This function can be used to alter the behavior of a shader so that it does one thing in the case of visibility rays, something else in the case of shadow rays.
float raylevel ()
Returns the level of the ray which caused this shader to be executed. A return value of 0 indicates that this shader is being executed on a camera (eye) ray, 1 that it is the result of a single reflection or refraction, etc. This allows one to customize the behavior of a shader based on how "deep" in the reflection/refraction "tree.

How to use it

Using PRMan as a ray tracer is straightforward:

  1. Use these functions in your shaders. In any shader that uses the functions, you should:
         #include "rayserver.h"
    

    If you inspect rayserver.h (in the examples directory), you'll see that most the functions described above are really macros. When compiling with BMRT's compiler, the functions are unchanged (all three are actually implemented in BMRT). But when compiling with PRMan's compiler, the macros transform their arguments to world space and call a function called rayserver().

  2. Compile the shaders with both BMRT and PRMan's shader compilers. When compiling for PRMan, make sure that the DSO rayserver.so (in the BMRT lib directory) is in your include path (-I).

  3. Render the file using the frankenrender script that comes with BMRT. This is a Perl script that sets up the environment that controls the ray server, and passes the correct arguments to both PRMan and BMRT. Just look at the script for more details on how it works and what arguments are valid.

    If you are rendering the same geometry with both renderers, just use frankenrender in the same way as you would use prman or rendrib:

        frankenrender teapots.rib
    

    If you want to give separate RIB files to each renderer, use the -prman and -bmrt flags:

        frankenrender commonfile.rib -bmrt bmrtfile.rib -prman prmanfile.rib
    

That's it!

Results

Here is an example image rendered with PRMan, unassisted:

The teapot on the right makes calls to trace() for reflections, but of course this does nothing in PRMan.

Below is the same RIB file, with the same shaders, taking advantage of the ray server described above. This picture was rendered by PRMan (mostly), without using shadow or environment maps.

Pros and Cons

The big advantage here is that you can render most of your scene with PRMan, using BMRT for tracing individual rays on selected objects or calculating shadows for selected lights. This is much faster than rendering in BMRT, particularly if you only tell the ray tracer about a subset of the scene that you want in the shadows or reflections. The following effects are utterly trivial to produce with this scheme:

The big disadvantage is that it requires two renderers to both have the scene loaded at the same time. This can be alleviated somewhat by reducing the scene that the ray tracer sees, or by telling the ray tracer to use a significantly reduced tessellation rate, etc. But still, it's a significant memory hit compared to running PRMan alone.

All of the usual considerations about compatibility between the two renderers apply. Be particularly aware of new PRMan primitives and SL features not currently supported by BMRT, texture file format differences, results of noise() functions, etc.

All of the usual considerations about compatibility between the two renderers apply. Be particularly aware of new PRMan primitives and SL features not currently supported by BMRT, texture file format differences, results of noise() functions, etc.

Note that by default, all rays will be traced from the positions at the shutter open time.  Thus, reflections and shadows will not be blurred.  Indeed, they will strobe in exactly the same way (and for largely the same reasons) as ordinary shadows do with PRMan.  If you are ray tracing multipe reflection rays (or multiple shadow rays) per sample, you could try jittering the rays in time.  This can be accomplished simply by setting the environment variable RAYSERVER_JITTER_TIMES to 1.  Beware, though -- this doesn't necessarily make the scene look better, and in some circumstances could make additional artifacts.  Try both ways and decide which is best.  To turn it off, either don't set that environment variable at all, or set it to 0.


This document last updated 9 Dec by gritzl@acm.org

All BMRT components are Copyright 1990-1998 by Larry I. Gritz. All rights reserved.

The RenderMan Interface Procedures and RIB Protocol are: Copyright 1988, 1989, Pixar. All rights reserved. RenderMan is a registered trademark of Pixar.