B.3 Scene Definition
After the user has set up the overall rendering options, the pbrtWorldBegin() call marks the start of the description of the shapes, materials, and lights in the scene. It sets the current rendering state to APIState::WorldBlock, resets the CTMs to identity matrices, and enables all of the CTMs.
B.3.1 Hierarchical Graphics State
As the scene’s lights, geometry, and participating media are specified, a variety of attributes can be set as well. In addition to the CTMs, these include information about textures and the current material. When a geometric primitive or light source is then added to the scene, the current attributes are used when creating the corresponding object. These data are all known as the graphics state.
It is useful for a rendering API to provide some functionality for managing the graphics state. pbrt has API calls that allow the current graphics state to be managed with an attribute stack; the user can push the current set of attributes, make changes to their values, and then later pop back to the previously pushed attribute values. For example, a scene description file might contain the following:
The first sphere is affected by the translation and is bound to the plastic material, while the second sphere is matte and isn’t translated. Changes to attributes made inside a pbrtAttributeBegin()/pbrtAttributeEnd() block are forgotten at the end of the block. Being able to save and restore attributes in this manner is a classic idiom for scene description in computer graphics.
When pbrtInit() is called, the current graphics state is initialized to hold default values.
A vector of GraphicsStates is used as a stack to perform hierarchical state management. When pbrtAttributeBegin() is called, the current GraphicsState is copied and pushed onto this stack. pbrtAttributeEnd() then simply pops the state from this stack.
pbrtAttributeEnd() also verifies that we do not have attribute stack underflow by checking to see if the stack is empty.
The API also provides pbrtTransformBegin() and pbrtTransformEnd() calls. These functions are similar to pbrtAttributeBegin() and pbrtAttributeEnd(), except that they only push and pop the CTMs. We frequently want to apply a transformation to a texture, but since the list of named textures is stored in the graphics state, we cannot use pbrtAttributeBegin() to save the transformation matrix. Since the implementations of pbrtTransformBegin() and pbrtTransformEnd() are very similar to pbrtAttributeBegin() and pbrtAttributeEnd(), respectively, they are not shown here.
B.3.2 Texture and Material Parameters
Recall that all of the materials in pbrt use textures to describe all of their parameters. For example, the diffuse color of the matte material class is always obtained from a texture, even if the material is intended to have a constant reflectivity (in which case a ConstantTexture is used).
Before a material can be created, it is necessary to create these textures to pass to the material creation procedures. Textures can be either explicitly created and later referred to by name or implicitly created on the fly to represent a constant parameter. These two methods of texture creation are hidden by the TextureParams class.
The TextureParams class stores references to associative arrays of previously defined named Float and Spectrum textures, as well as two references to ParamSets that will be searched for named textures. Its constructor, which won’t be included here, just initializes these references from parameters passed to it.
Here we will show the code for finding a texture of Spectrum type; the code for finding a Float texture is analogous. The TextureParams::GetSpectrumTexture() method takes a parameter name (e.g., “Kd”), as well as a default Spectrum value. If no texture has been explicitly specified for the parameter, a constant texture will be created that returns the default spectrum value.
Finding the texture is performed in several stages; the order of these stages is significant. First, the parameter list from the Shape for which a Material is being created is searched for a named reference to an explicitly defined texture. If no such texture is found, then the material parameters are searched. Finally, if no explicit texture has been found, the two parameter lists are searched in turn for supplied constant values. If no such constants are found, the default is used.
The order of these steps is crucial, because pbrt allows a shape to override individual elements of the material that is bound to it. For example, the user should be able to create a scene description that contains the lines
These two commands create a green matte sphere: because the shape’s parameter list is searched first, the Kd parameter from the Shape will be used when the MatteMaterial constructor is called.
Because an instance of the TextureParams class is passed to material creation routines that might need to access non-texture parameter values, we also provide ways to access the other parameter list types. These methods return parameters from the geometric parameter list, if found. Otherwise, the material parameter list is searched, and finally the default value is returned.
The TextureParams::FindFloat() method is shown here. The other access methods are similar and omitted.
B.3.3 Surface and Material Description
The pbrtTexture() method creates a named texture that can be referred to later. In addition to the texture name, its type is specified. pbrt supports only “float” and “spectrum” as texture types. The supplied parameter list is used to create a TextureParams object, which will be passed to the desired texture’s creation routine.
Creating the texture is simple. This function first checks to see if a texture of the same name and type already exists and issues a warning if so. Then, the MakeFloatTexture() routine calls the creation function for the appropriate Texture implementation, and the returned texture class is added to the GraphicsState::floatTextures associative array. The code for creating a spectrum texture is similar and not shown.
The default material is matte.
pbrt also supports the notion of creating a Material with a given set of parameters and then associating an arbitrary name with the combination of material and parameter settings. pbrtMakeNamedMaterial() creates such an association, and pbrtNamedMaterial() sets the current material and material parameters based on a previously defined named material.
B.3.4 Light Sources
pbrt’s API provides two ways for the user to specify light sources for the scene. The first, pbrtLightSource(), defines a light source that doesn’t have geometry associated with it (e.g., a point light or a directional light).
The second API call to describe light sources, pbrtAreaLightSource(), specifies an area light source. All shape specifications that appear between an area light source call up to the end of the current attribute block are treated as emissive. Thus, when an area light is specified via pbrtAreaLightSource(), it can’t be created immediately since the shapes to follow are needed to define the light source’s geometry. Therefore, this function just saves the name of the area light source type and the parameters given to it.
The pbrtShape() function creates one or more new Shape objects and adds them to the scene. This function is relatively complicated, in that it has to create area light sources if an area light is being defined, it has to handle animated and static shapes differently, and it also has to deal with creating object instances when needed.
Shapes that are animated are represented with TransformedPrimitives, which include extra functionality to use AnimatedTransforms, while shapes that aren’t animated use GeometricPrimitives. Therefore, there are two code paths here for those two cases.
The code below uses a TransformCache (defined shortly), which allocates and stores a single Transform pointer for each unique transformation that is passed to its Lookup() method. In this way, if many shapes in the scene have the same transformation matrix, a single Transform pointer can be shared among all of them. MakeShapes() then handles the details of creating the shape or shapes corresponding to the given shape name, passing the ParamSet along to the shape’s creation routine.
TransformCache is a small wrapper around an associative array from transformations to pairs of Transform pointers; the first pointer is equal to the transform, and the second is its inverse. The Lookup() method just looks for the given transformation in the cache, allocates space for it and stores it and its inverse if not found, and returns the appropriate pointers.
The MakeShapes() function takes the name of the shape to be created, the CTMs, and the ParamSet for the new shape. It calls an appropriate shape creation function based on the shape name provided (e.g., for “sphere,” it calls CreateSphereShape(), which is defined in Section A.4). The shape creation routines may return multiple shapes; for triangle meshes, for example, the creation routine returns a vector of Triangles. The implementation of this function is straightforward, so we won’t include it here.
The Material for the shape is created by the MakeMaterial() call; its implementation is analogous to that of MakeShapes(). If the specified material cannot be found (usually due to a typo in the material name), a matte material is created and a warning is issued.
If the transformation matrices are animated, the task is a little more complicated. After Shape and GeometricPrimitive creation, a TransformedPrimitive is created to hold the shape or shapes that were created.
Because the Shape class doesn’t handle animated transformations, the initial shape or shapes for animated primitives are created with identity transformations. All of the details related to the shape’s transformation will be managed with the TransformedPrimitive that ends up holding the shape. Animated transformations for light sources aren’t currently supported in pbrt; thus, if an animated transform has been specified with an area light source, a warning is issued here.
Given the initial set of shapes, it’s straightforward to create a GeometricPrimitive for each of them.
If there are multiple GeometricPrimitives, then it’s worth collecting them in an aggregate and storing that in a TransformedPrimitive, rather than creating multiple TransformedPrimitives. This way, the transformation only needs to be interpolated once and the ray is only transformed once, rather than redundantly doing both of these for each primitive that the ray intersects the bounds of. Intersection efficiency also benefits; see the discussion in Exercise B.5.
If the user is in the middle of defining an object instance, pbrtObjectBegin() (defined in the following section) will have set the currentInstance member of renderOptions to point to a vector that is collecting the shapes that define the instance. In that case, the new shape or shapes are added to that array. Otherwise, the RenderOptions::primitives array is used—this array will eventually be passed to the Scene constructor. If it is also an area light, the corresponding areaLights are also added to the RenderOptions::lights array, just as pbrtLightSource() does.
B.3.6 Object Instancing
All shapes that are specified between a pbrtObjectBegin() and pbrtObjectEnd() pair are used to create a named object instance (see the discussion of object instancing and the TransformedPrimitive class in Section 4.1.2). pbrtObjectBegin() sets RenderOptions::currentInstance so that subsequent pbrtShape() calls can add the shape to this instance’s vector of primitive references. This function also pushes the graphics state, so that any changes made to the CTMs or other state while defining the instance don’t last beyond the instance definition.
When an instance is used in the scene, the instance’s vector of Primitives needs to be found in the RenderOptions::instances map, a TransformedPrimitive created, and the instance added to the scene. Note that the TransformedPrimitive constructor takes the current transformation matrix from the time when pbrtObjectInstance() is called. The instance’s complete world transformation is the composition of the CTM when it is instantiated with the CTM when it was originally created.
pbrtObjectInstance() first does some error checking to make sure that the instance is not being used inside the definition of another instance and also that the named instance has been defined. The error checking is simple and not shown here.
If there is more than one primitive in an instance, then an aggregate needs to be built for it. This must be done here rather than in the TransformedPrimitive constructor so that the resulting aggregate will be reused if this instance is used multiple times in the scene.
B.3.7 World End and Rendering
When pbrtWorldEnd() is called, the scene has been fully specified and rendering can begin. This routine makes sure that there aren’t excess graphics state structures pushed on the state stack (issuing a warning if so), creates the Scene and Integrator objects, and then calls the Integrator::Render() method.
If there are graphics states and/or transformations remaining on the respective stacks, a warning is issued for each one:
Creating the Scene object is mostly a matter of creating the Aggregate for all of the primitives and calling the Scene constructor. The MakeAccelerator() function isn’t included here; it’s similar in structure to MakeShapes() as far as using the string passed to it to determine which accelerator construction function to call.
After the scene has been created, RenderOptions clears the vectors of primitives and lights. This ensures that if a subsequent scene is defined then the scene description from this frame isn’t inadvertently included.
Integrator creation in the MakeIntegrator() method is again similar to how shapes and other named objects are created; the string name is used to dispatch to an object-specific creation function.
Once rendering is complete, the API transitions back to the “options block” rendering state, prints out any statistics gathered during rendering, and clears the CTMs and named coordinate systems so that the next frame, if any, starts with a clean slate.