Contents
Introduction
Production renderers often output multiple display channels. For example, one output image of the renderer may contain all of the illumination that appears in the final rendered image, while separate output images may consist of just the skin subsurface scattering illumination, direct specular illumination, indirect diffuse illumination, the illumination from a particular group of lights, the illumination on a specific set of geometry, and so forth.
Light Path Expressions (LPEs) in RenderMan are used to specify what light transport paths to output to various display channel outputs. The Rix Light Path Expression (RixLPE
) interface allows a person writing a custom integrator to communicate information about light scattering events that is then used to route specific light transport paths to various output display channels, as indicated by the user-supplied Light Path Expressions.
An advantage of Light Path Expressions is that by allowing the user to write a short expression that describes the set of light transport paths that should be included in a particular display channel output, there is no need to modify the shader or BxDF source code if the routing of light transport paths to output display channels is later changed. Instead the RixLPE
system tracks the light scattering events along each light transport path internally, and is able to generate new output display channels for an arbitrary set of user-supplied Light Path Expressions.
The source code of the PxrDirectLighting
and PxrPathTracer
integrators provides an example of how to use the RixLPE
interface, and the documentation below shows the specific API calls one would make to add tracking of light scattering events to an integrator in order to support Light Path Expression display channel outputs.
Initialization
In order to use the RixLPE
interface, first obtain a RixLPE
instance:
RixLPE *rixLPE = integratorCtx->GetRixLPE();
Then allocate RixLPEState
instances (typically one per light transport path):
RixLPEState *states = rixLPE->AllocateStates(maxShadingCtxSize);
The AllocateStates()
method takes a parameter that indicates the number of RixLPEState
instances to allocate. In the example here, "maxShadingCtxSize
" is used (the maximum number of shading samples per shading context that will be passed into the integrator per batch), which means that we will be able to track scattering events separately for each shading sample in the shading context.
In general, a separate RixLPEState
should be allocated for each light transport path that will be processed per batch of shading samples in the shading context.
State Transitions
Next, we can track the light scattering events along each light transport path. At each scattering event along the light path, call MoveCamera()
and then MoveVertex()
on the RixLPEState
instance corresponding to that light path in order to track relevant scattering events. For example:
states[sCtxIndex].MoveCamera(sCtx, sCtxIndex);
states[sCtxIndex].MoveVertex(sCtx, sCtxIndex, scatterEvent1); // primary hit
states[sCtxIndex].MoveVertex(sCtx, sCtxIndex, scatterEvent2); // secondary hit
In the calls to MoveCamera()
and MoveVertex()
above, the shading context sample is identified using the shading context index variable ("sCtxIndex
"). The RixShadingContext
instance (the "sCtx
" variable in the example code above) provides the shading context to the API calls so that any needed context state is accessible. In this example, we have just one light transport path per sample in the shading context batch; a different indexing scheme into the array of RixLPEState
instances could be used if there were more than one light transport path per shading sample in the batch.
The MoveVertex()
call takes an additional RixLPEScatterEvent
parameter (the "scatterEvent1
" and "scatterEvent2
" variables above) that provides information about the type of light scattering event.
Recording Results
After the light scattering events have been recorded using MoveCamera()
and MoveVertex()
along the light transport path, use the RixLPE::SplatHelper
class to aid with writing results to the LPE AOV display channels:
RixLPE::SplatHelper aovs(...);
aovs.SplatPerLobe(lobeWeights, weightIndex, thruput, isFinite, clamp, isHoldout);
Note that SplatHelper::SplatPerLobe()
is a utility routine that performs the final direct lighting (or emissive object) light path transitions and will accumulate the per-lobe contributions into the beauty and LPE AOVs. (See the implementation of SplatPerLobe()
in RixLPEInline.h
for details.)
The parameters to SplatHelper::SplatPerLobe()
supply information about the per-lobe contributions (the "lobeWeights
" argument in the example above) and provide an index into the per-lobe contributions array ("weightIndex
"). The method also takes as input the path throughput ("thruput
"), whether the contribution is finite ("isFinite
"), a clamping factor ("clamp
"), and whether this is a holdout contribution ("isHoldout
"). If any of the geometry along the light path is a holdout piece of geometry, then the contribution for that light path is a "holdout" contribution, and "true" should be passed in for the "isHoldout
" parameter.
Alternatively, some integrators may wish to manually perform the final state transitions that reach a light or emissive object, and then make separate per-lobe invocations of the SplatHelper::SplatValue()
method to accumulate contributions for each lobe. E.g.,:
states[sCtxIndex].MoveVertex(sCtx, sCtxIndex, scatterEvent3);
states[sCtxIndex].MoveLight(sCtx, sCtxIndex, ...);
RixLPE::SplatHelper aovs(...);
aovs.SplatValue(lobe1Contribution, ...);
aovs.SplatValue(lobe2Contribution, ...);
For emissive objects, the SplatHelper::SplatEmission()
method is used to record the illumination scattering event information:
aovs.SplatEmission(emission, thruput, isFinite, clamp, isHoldout);
Similar to the SplatHelper::SplatPerLobe()
method above, the SplatHelper::SplatEmission()
method takes as input information about the contribution for this light path (the "emission
" argument), the path throughput ("thruput
"), whether the contribution is finite ("isFinite
"), a clamping factor ("clamp
"), and whether this is a holdout contribution ("isHoldout
").
Cleaning Up
When the integrator is finished with the RixLPEState
instances, then use RixLPE::FreeStates()
to free the memory of the RixLPEState
instances:
rixLPE->FreeStates(maxShadingCtxSize, states);
Checking for LPE Existence
It is possible to check for the existence of any user-supplied Light Path Expressions in the scene. If there are no Light Path Expressions in the scene, integrators can avoid performing some work as an optimization.
Use the RixLPE::AnyLPEs()
method to determine whether there are any light path expression (LPE) AOV display channels at all.