B.2 Initialization and Rendering Options
We now have the machinery to describe the routines that make up the rendering API. Before any other API functions can be called, the rendering system must be initialized by a call to pbrtInit(). Similarly, when rendering is done, pbrtCleanup() should be called; this handles final cleanup of the system. The definitions of these two functions will be filled in at a number of points throughout the rest of this appendix.
A few system-wide options are passed to pbrtInit() using the Options structure here.
The options are stored in a global variable for easy access by other parts of the system. This variable is only used in a read-only fashion by the system after initialization in pbrtInit().
After the system has been initialized, a subset of the API routines is available. Legal calls at this point are those that set general rendering options like the camera and sampler properties, the type of film to be used, and so on, but the user is not yet allowed to start to describe the lights, shapes, and materials in the scene.
After the overall rendering options have been set, the pbrtWorldBegin() function locks them in; it is no longer legal to call the routines that set them. At this point, the user can begin to describe the geometric primitives and lights that are in the scene. This separation of global versus scene-specific information can help simplify the implementation of the renderer. For example, consider a spline patch shape that tessellates itself into triangles. This shape might compute the required size of its generated triangles based on the area of the screen that it covers. If the camera’s position and image resolution are guaranteed not to change after the shape is created, then the shape can potentially do the tessellation work immediately at creation time.
Once the scene has been fully specified, the pbrtWorldEnd() routine is called. At this point, the renderer knows that the scene description is complete and that rendering can begin. The image will be rendered and written to a file before pbrtWorldEnd() returns. The user may then specify new options for another frame of an animation, and then another pbrtWorldBegin()/pbrtWorldEnd() block to describe the geometry for the next frame, repeating as many times as desired. The remainder of this section will discuss the routines related to setting rendering options. Section A.3 describes the routines for specifying the scene inside the world block.
B.2.1 State Tracking
There are three distinct states that the renderer’s API can be in:
- Uninitialized: Before pbrtInit() has been called or after pbrtCleanup() has been called, no other API calls are legal.
- Options block: Outside a pbrtWorldBegin() and pbrtWorldEnd() pair, scene-wide global options may be set.
- World block: Inside a pbrtWorldBegin() and pbrtWorldEnd() pair, the scene may be described.
The module static variable currentApiState starts out with the value APIState::Uninitialized, indicating that the API system hasn’t yet been initialized. Its value is updated appropriately by pbrtInit(), pbrtWorldBegin(), and pbrtCleanup().
Now we can start to define the implementation of pbrtInit(). pbrtInit() first makes sure that it hasn’t already been called and then sets the currentApiState variable to OptionsBlock to indicate that the scene-wide options can be specified.
Similarly, pbrtCleanup() makes sure that pbrtInit() has been called and that we’re not in the middle of a pbrtWorldBegin()/pbrtWorldEnd() block before resetting the state to the uninitialized state.
All API functions that are only valid in particular states invoke a state verification macro like VERIFY_INITIALIZED(), to make sure that currentApiState holds an appropriate value. If the states don’t match, an error message is printed and the function immediately returns. (Note that this check must be implemented as a macro rather than a separate function so that its return statement causes the calling function itself to return.)
The implementations of VERIFY_OPTIONS() and VERIFY_WORLD() are analogous.
As the scene is being described, pbrt maintains current transformation matrices (CTMs), one for each of a number of points in time. If the transformations are different, then they describe an animated transformation. (Recall, for example, that the AnimatedTransform class defined in Section 2.9.3 stores two transformation matrices for two given times.) A number of API calls are available to modify the CTMs; when objects like shapes, cameras, and lights are created, the CTMs are passed to their constructor to define the transformation from their local coordinate system to world space.
The code below stores two CTMs in the module-local curTransform variable. They are represented by the TransformSet class, to be defined shortly, which stores a fixed number of transformations. The activeTransformBits variable is a bit-vector indicating which of the CTMs are active; the active transforms are updated when the transformation-related API calls are made, while the others are unchanged. This mechanism allows the user to modify some of the CTMs selectively in order to define animated transformations.
The implementation here just stores two transformation matrices, one that defines the CTM for the starting time (provided via the pbrtTransformTimes() call, defined in a few pages), and the other for the ending time.
TransformSet is a small utility class that stores an array of transformations and provides some utility routines for managing them.
An accessor function is provided to access the individual Transforms.
The Inverse() method returns a new TransformSet that holds the inverses of the individual Transforms.
The actual transformation functions are straightforward. Because the CTM is used both for the rendering options and the scene description phases, these routines only need to verify that pbrtInit() has been called.
The FOR_ACTIVE_TRANSFORMS() macro encapsulates the logic for determining which of the CTMs is active and applying the given operation to those that are. The given statement is executed only for the active transforms.
Most of the rest of the functions are similarly defined, so we will not show their definitions here. pbrt also provides pbrtConcatTransform() and pbrtTransform() functions to allow the user to specify an arbitrary matrix to postmultiply or replace the active CTM(s), respectively.
It can be useful to make a named copy of the CTM so that it can be referred to later. For example, to place a light at the camera’s position, it is useful to first apply the transformation into the camera coordinate system, since then the light can just be placed at the origin . This way, if the camera position is changed and the scene is rerendered, the light will move with it. The pbrtCoordinateSystem() function copies the current TransformSet into the namedCoordinateSystems associative array, and pbrtCoordSysTransform() loads a named set of CTMs.
Not all of the types in pbrt that take transformations support animated transformations. (Textures are one example (Section A.3.2); it’s not worth the additional code complexity to support them, especially since the utility an animated texture transform brings isn’t obvious.) For such cases, WARN_IF_ANIMATED_TRANSFORM() macro warns if the CTMs are different, indicating that an animated transformation has been specified.
All of the rendering options that are set before the pbrtWorldBegin() call are stored in a RenderOptions structure. This structure contains public data members that are set by API calls and methods that help create objects used by the rest of pbrt for rendering.
A single static instance of a RenderOptions structure is available to the rest of the API functions:
When pbrtInit() is called, it allocates a RenderOptions structure that initially holds default values for all of its options:
The renderOptions variable is freed by pbrtCleanup():
A few calls are available to set which of the CTMs should be active.
The two times at which the two CTMs are defined can be provided by a calling the function pbrtTransformTimes(). By default, the start time is 0 and the end time is 1.
The API functions for setting the rest of the rendering options are mostly similar in both their interface and their implementation. For example, pbrtPixelFilter() specifies the kind of Filter to be used for filtering image samples. It takes two parameters: a string giving the name of the filter to use and a ParamSet giving the parameters to the filter.
Note that an instance of the Filter class isn’t created immediately upon a call to pbrtPixelFilter(); instead, that function just stores the name of the filter and its parameters in renderOptions. There are two reasons for this approach: first, there may be a subsequent call to pbrtPixelFilter() before the start of the world block, specifying a different filter; the (small) cost of creating the first Filter would be wasted in this case.
Second, and more importantly, there are various object creation ordering dependencies imposed by the parameters taken by various constructors. For example, the Film constructor expects a pointer to the Filter being used, and the Camera constructor expects a pointer to the Film. Thus, the camera can’t be created before the film, and the film can’t be created before the filter. We don’t want to require the user to specify the scene options in an order dictated by these internal details, so instead always just store object names and parameter sets until the end of the options block. (The Filter here could actually be created immediately, since it doesn’t depend on other objects, but we follow the same approach to it for consistency.)
The default filter is set to the box filter. If no specific filter is specified in the scene description file, then because the default ParamSet has no parameter values, the filter will be created based on its default parameter settings.
Most of the rest of the rendering-option-setting API calls are similar; they simply store their arguments in renderOptions. Therefore, we will only include the declarations of these functions here. The options controlled by each function should be apparent from its name; more information about the legal parameters to each of these routines can be found in the documentation of pbrt’s input file format.
pbrtCamera() is slightly different from the other options, since the camera-to-world transformation needs to be recorded. The CTM is used by pbrtCamera() to initialize this value, and the camera coordinate system transformation is also stored for possible future use by pbrtCoordSysTransform().
The default camera uses a perspective projection.
B.2.4 Media Description
Definitions of participating media in the scene are specified by pbrtMakeNamedMedium(). This function allows the user to associate a specific type of participation media (from Section 11.3) with an arbitrary name. For example,
creates a HomogeneousMedium instance with the name highAlbedo.
The corresponding Medium instances are stored in an associative array for access later.
Once named media have been created, pbrtMediumInterface() allows specifying the current “inside” and “outside” media. For shapes, these specify the media inside and outside the shape’s surface, where the side of the shape where the surface normal is oriented outward is “outside.” For the camera and for light sources that don’t have geometry associated with them, the “inside” medium is ignored and “outside” gives the medium containing the object. The current medium is stored in the GraphicsState class, which will be introduced shortly.
Like the transformation-related API functions, both of these functions can be called from both the options and the world blocks; the former so that media can be specified for the camera and the latter so that media can be specified for the lights and shapes in the scene.