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.
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 point 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.
The code fragment below 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.
missdepth = fulltrace (P, R, C, d, Ph, Nh, Pm, Rm); if (missdepth > 0) C += environment ("foo.env", Rm) / missdepth;
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.
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().
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 common.rib -bmrt bmrt.rib -prman prman.rib
That's it!
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.
Here are several tips to help you speed up the ray server.
Attribute "render" "patch_multiplier" [n]}
The -rayserver mode automatically sets n to 0.5, indicating that patches should be diced only half as finely when serving rays as when rendering whole frames. Try reducing n to 0.25 or even lower, to increase speed and decrease memory use. Make n as low as you can get it without seeing visible artifacts.