Part 8: Using BMRT as a "Ray Server" for PRMan
revised 9 December 1998
Larry I. Gritz
gritzl@acm.org
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.
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:
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.
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.
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.
color trace (point from; vector dir)
from
in the direction of
vector dir
. The return value is the incoming light
from that direction.
color visibility (point p1, p2)
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)
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)
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 ()
float raylevel ()
Using PRMan as a ray tracer is straightforward:
#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().
rayserver.so
(in the BMRT lib directory) is in your
include path (-I).
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!
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.
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.