Revealing the Nature of Opacity in RenderMan
Revealing the Nature of Opacity in RenderMan
This documentation is applicable to Reyes. See Visibility, Presence, and Opacity for RIS.
The concept of opacity has always been a very subtle one in the context of RenderMan shaders. Traditionally, opacity has been used to represent a material property (thin-translucence) as well as to convey the presence or absence of the material. The former is correctly represented as a color filter while the latter is more correctly represented as a percentage (or alpha) value. Presence is really a binary quantity but we represent it as a percentage to band-limit the signal and thereby prevent aliasing. But the designers of RSL chose to conflate these two quantities into the single shading property of opacity, represented by Oi. The reason is that, for rendering purposes, multiplying these values together gives us a useful result. That is, the renderer can do the right thing with the conflated result.
This is a central observation of the seminal Porter-Duff compositing paper, which may have influenced the original design of RSL. In the original REYES architecture, the visible points (Oi,Ci,Z) are composited to produce the final pixel sample color and so it is correct to characterize Oi (the conflation of presence and opacity) as a compositing property. To clarify the independent nature of these properties, RSL programmers can separate them in their programs and then combine them into Oi at the moment the state is communicated to the renderer. But how should we think of Oi outside of the context and language of compositing?
RSL and Oi
In a physical setting, light that crosses a material boundary is bent according to the change in the refractive indices of adjacent materials. In the context of computer graphics, light-paths that are bent by surface interactions are fundamentally more expensive to compute than non-bending light paths. Since the amount a light path is bent is affected by the thickness of a material we can disregard the bends for thin materials and achieve believable results. In the limit, this thin-translucence reduces to compositing; this is the mental model of opacity held by most RSL programmers. As either the thickness or relative index of refraction increases, the approximation fails and more-expensive solutions, often involving ray tracing (for refractions) or photon scattering (for caustics), must be employed.
In the context of volume rendering, Beer's law states that optical density is an exponential function of the product of distance and material density. The renderer must account for the thickness of the microvoxel in its optical density calculations. To ensure that shaders characterize volumes in a resolution-independent fashion, we interpret Oi as the material density and not the optical density. Now Oi can meaningfully take on values greater than 1, and the optical density is obtained through Beer's law. This contrasts with the thin-surface case where the attenuation distance approaches 0 and no exponentiation is required. In that case Oi is truly the optical density and thus must lie in the zero-to-one range. From an RSL programmer's point of view this duality of Oi is either obscure or viewed as convenient or natural.
Tying these threads together we can think of Oi as a simple and efficient formulation for light attenuation through both thin and thick materials when computing straight-line light paths. When set to values other than 1 (for surfaces), the renderer can inexpensively provide answers to the question of what is behind the shading element and produce the composed result. In the context of REYES we refer to this as visible-point compositing and in the context of ray tracing we utilize Oi for both continuation and transmission rays.
When more dramatic refraction effects are required we must disable the thin-surface approximation by setting the value of Oi to (1,1,1). This is initially counter-intuitive, since we'd never describe a glass object as opaque. Now, to capture refraction, shaders must explicitly integrate the refractive ray-tree via recursive ray tracing through the use of either gather() or indirectspecular(). If we set Oi to a transparent value we can see the unusual and incorrect combination of effects.
Oi Down the Shading Pipeline
Armed with this deeper understanding of the use and meaning of Oi within RenderMan we can now explore a set of optimizations to reduce the rendering costs associated with recursive ray tracing. We'll focus this discussion on glass since it's a common material that is relatively expensive to compute. Many of the ideas presented in this discussion generalize to other materials.
As motivated above, refractive glass is opaque to straight-line ray paths. An unfortunate consequence of this is that a glass model casts black/opaque shadows.
Transmission rays are used to produce ray-traced shadows cast by direct light sources and, since these are straight-line path queries, the correct glass shadowing can't be delivered. To achieve physically-correct light attenuation through glass is generally cost-prohibitive. Photons scattered from the light sources through the glass model don't follow straight light paths and are bent by the same laws of refraction as our camera rays. When we render areas that are in the shadow of the glass model we would need to trace refraction rays through the glass model and hope that they hit a light. The probability that we would choose directions from, say, the ground, that would find their way through refractions to a light source, are quite small, and that means we would have to send an immense number of rays to produce a correct shadow of reasonable quality. Currently, the most viable solution for accurately computing these effects, called caustics, is to use photon maps.
In order to improve the black shadow situation, we can either employ photon mapping or explore other approximations. One approximation available to us is to treat our glass as thin-translucent for the purposes of shadows, but opaque for camera or specular rays. Modern RSL decomposes the shading pipeline into stages and this allows us to express different values for Oi at each stage. If you are unfamiliar with the shading pipeline, please consult the Shading Pipeline section of the documentation.
When a scene is rendered, an object may be probed by rays of different types. Associated with each ray type are rules governing the subset of the general shading pipeline that we must execute. Understanding these rules enables you to produce different Oi values in different conditions.
- When a transmission ray encounters an object, the shading pipeline is only run through the opacity() method. Setting Oi in our glass shader to a transparent value will produce transparent shadowing effects.
- When a diffuse ray encounters an object, we run the shading pipeline through the diffuselighting() method. The value of Oi set in the opacity() method will be inherited in the diffuselighting() method and, unless we emit an alternate value for Oi, diffuse rays will be subject to automatic continuation. This gives us a more intuitive (though still physically incorrect) result.
- When a camera ray or a reflected specular ray encounters an object, the shading pipeline is run in its entirety. Our glass surface requires us to produce an opaque value for Oi under these conditions; we can do this by setting Oi to 1 in our specularlighting() and optional lighting methods.
Further Optimizations: The Opacity Cache, Radiosity Cache, and __computesOpacity
In most scenes, the majority of objects are opaque. Significant speedups can be achieved by bypassing shader execution on transmission rays that hit opaque objects. In cases where opacity is computed, we can cache the opacity calculations as long as they are view-independent. Only where opacity is view-dependent must we pay the full cost of the opacity() method on each transmission ray.
In general, it's not possible to perform compile-time analysis of a shader to ascertain its opacity requirements. This is perhaps obvious when you consider the expressive potential of the shading pipeline. It is common to build general shading solutions that reduce to simple cases for a set of input shader parameters, RIB attributes, or inter-shader messages.
To help in this regard, RenderMan supports a special shader parameter, __computesOpacity, that can be used to signal your intentions regarding Oi to the renderer. A 0 value indicates that the object is opaque and transmission rays can bypass shader execution entirely.
In the case of our opaque/black-shadow glass, a value of 0 would be sufficient and we wouldn't even need an opacity method. In the case of our thin-translucent shadow we should set __computesOpacity to a non-zero value. 1 indicates that Oi must be computed and can be cached. 2 indicates that Oi must be computed but cannot be cached. In the last case, we can simulate Fresnel effects by increasing opacity with (1-N.I).
When computing indirect diffuse illumination, the radiosity cache delivers substantial performance benefits. The role of the radiosity cache is to reduce the number of shader executions on the end of diffuse ray probes and, just like the opacity cache, requires that radiosity be a view-independent quantity. To determine whether diffuse rays require automatic continuation we require a value for Oi and enforce the view-independence of the Oi values by setting the I vector to -N. Since there are no guarantees regarding variety or ordering of ray types impinging upon a surface it is possible a diffuse ray will encounter a pre-populated opacity cache. For this reason, it's important to ensure that the diffuselighting() method shares the opinion about Oi with the opacity() method. Generally it's recommended that you not write to Oi in diffuselighting(). In addition, ray-type dependencies in the opacity() method may be a sign of trouble. In the case where your transmission rays are marked as uncacheable, we still end up caching Oi for the continuation behavior on diffuse rays. It would be possible to support more complexity in the form of additional cache-lines or independent execution of opacity() and diffuselighting(), but to date we have not found sufficient motivation to support these more obscure cases.
Finally we return to the interpretation of __computesOpacity in the context of camera and specular rays. Generally, all presence and thin-translucency cases require a non-zero value for __computesOpacity, so values of 1 or 2 serve their purpose well. But when, as in our glass example, special opacity behavior is required, the values for __computesOpacity should differ according to ray type. Specifically, we would like the renderer to know that the shader is opaque to camera and specular rays since that knowledge can be helpful in reducing camera-ray shading behind glass-like objects. There may be a temptation to provide a more general characterization or static opacity behavior. For example, we could have a __computesOpacity value for each ray type, but we would need three states for each of the four ray types (transmission, diffuse, specular, and camera) producing twelve different possibilities. Since many of these states are nonsensical, and because the trend toward more physical transport suggests the days for thin-translucency tricks are numbered, we have elected to support only these three useful states for __computesOpacity:
0: never computes opacity, always opaque in all conditions
1: always computes view-independent opacity
2: always computes view-dependent opacity
Two additional values offer potential benefits and are reserved for potential future implementation:
3: always computes view-independent opacity, but opaque to specular and camera
4: always computes view-dependent opacity, but opaque to specular and camera
Note that it is possible to emulate modes 3 and 4 with a combination of RSL and __computesOpacity values 1 or 2. This is accomplished by resetting the non-opaque Oi value established in the opacity method to opaque (1,1,1) at the end of both lighting and specularlighting methods.
- Do include the __computesOpacity parameter in all cases.
- Don't write to __computesOpacity in your shader.
- Do implement an opacity() method if you compute opacity.
- Don't use view-dependent opacity unless you really need it.
- Don't set Oi in diffuselighting().
- Ray-type tests in the opacity() should only be made in non-cacheable __computesOpacity modes (2 or 4).