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.

<<Global Forward Declarations>>+= 
struct Options { int nThreads = 0; bool quickRender = false; bool quiet = false, verbose = false; std::string imageFile; };

<<API Function Definitions>>= 
void pbrtInit(const Options &opt) { PbrtOptions = opt; <<API Initialization>> 
if (currentApiState != APIState::Uninitialized) Error("pbrtInit() has already been called."); currentApiState = APIState::OptionsBlock; renderOptions.reset(new RenderOptions); graphicsState = GraphicsState();
<<General pbrt Initialization>> 
SampledSpectrum::Init(); InitProfiler();
}

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().

<<API Global Variables>>= 
Options PbrtOptions;

<<API Function Definitions>>+=  
void pbrtCleanup() { <<API Cleanup>> 
if (currentApiState == APIState::Uninitialized) Error("pbrtCleanup() called without pbrtInit()."); else if (currentApiState == APIState::WorldBlock) Error("pbrtCleanup() called while inside world block."); currentApiState = APIState::Uninitialized; renderOptions.reset(nullptr);
}

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:

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().

<<API Static Data>>= 
enum class APIState { Uninitialized, OptionsBlock, WorldBlock }; static APIState currentApiState = APIState::Uninitialized;

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.

<<API Initialization>>= 
if (currentApiState != APIState::Uninitialized) Error("pbrtInit() has already been called."); currentApiState = APIState::OptionsBlock;

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.

<<API Cleanup>>= 
if (currentApiState == APIState::Uninitialized) Error("pbrtCleanup() called without pbrtInit()."); else if (currentApiState == APIState::WorldBlock) Error("pbrtCleanup() called while inside world block."); currentApiState = APIState::Uninitialized;

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.)

<<API Macros>>= 
#define VERIFY_INITIALIZED(func) \ if (currentApiState == APIState::Uninitialized) { \ Error("pbrtInit() must be before calling \"%s()\". " \ "Ignoring.", func); \ return; \ } else /* swallow trailing semicolon */

The implementations of VERIFY_OPTIONS() and VERIFY_WORLD() are analogous.

B.2.2 Transformations

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.

<<API Static Data>>+=  
static TransformSet curTransform; static int activeTransformBits = AllTransformsBits;

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.

<<API Local Classes>>= 
constexpr int MaxTransforms = 2; constexpr int StartTransformBits = 1 << 0; constexpr int EndTransformBits = 1 << 1; constexpr int AllTransformsBits = (1 << MaxTransforms) - 1;

TransformSet is a small utility class that stores an array of transformations and provides some utility routines for managing them.

<<API Local Classes>>+=  
struct TransformSet { <<TransformSet Public Methods>> 
Transform &operator[](int i) { return t[i]; } friend TransformSet Inverse(const TransformSet &ts) { TransformSet tInv; for (int i = 0; i < MaxTransforms; ++i) tInv.t[i] = Inverse(ts.t[i]); return tInv; } bool IsAnimated() const { for (int i = 0; i < MaxTransforms - 1; ++i) if (t[i] != t[i + 1]) return true; return false; }
private: Transform t[MaxTransforms]; };

An accessor function is provided to access the individual Transforms.

<<TransformSet Public Methods>>= 
Transform &operator[](int i) { return t[i]; }

The Inverse() method returns a new TransformSet that holds the inverses of the individual Transforms.

<<TransformSet Public Methods>>+=  
friend TransformSet Inverse(const TransformSet &ts) { TransformSet tInv; for (int i = 0; i < MaxTransforms; ++i) tInv.t[i] = Inverse(ts.t[i]); return tInv; }

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.

<<API Function Definitions>>+=  
void pbrtIdentity() { VERIFY_INITIALIZED("Identity"); FOR_ACTIVE_TRANSFORMS(curTransform[i] = Transform();) }

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.

<<API Macros>>+=  
#define FOR_ACTIVE_TRANSFORMS(expr) \ for (int i = 0; i < MaxTransforms; ++i) \ if (activeTransformBits & (1 << i)) { expr }

<<API Function Definitions>>+=  
void pbrtTranslate(Float dx, Float dy, Float dz) { VERIFY_INITIALIZED("Translate"); FOR_ACTIVE_TRANSFORMS(curTransform[i] = curTransform[i] * Translate(Vector3f(dx, dy, dz));) }

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.

<<API Function Declarations>>= 
void pbrtRotate(Float angle, Float ax, Float ay, Float az); void pbrtScale(Float sx, Float sy, Float sz); void pbrtLookAt(Float ex, Float ey, Float ez, Float lx, Float ly, Float lz, Float ux, Float uy, Float uz); void pbrtConcatTransform(Float transform[16]); void pbrtTransform(Float transform[16]);

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 left-parenthesis 0 comma 0 comma 0 right-parenthesis . 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.

<<API Static Data>>+=  
static std::map<std::string, TransformSet> namedCoordinateSystems;

<<API Function Definitions>>+=  
void pbrtCoordinateSystem(const std::string &name) { VERIFY_INITIALIZED("CoordinateSystem"); namedCoordinateSystems[name] = curTransform; }

<<API Function Definitions>>+=  
void pbrtCoordSysTransform(const std::string &name) { VERIFY_INITIALIZED("CoordSysTransform"); if (namedCoordinateSystems.find(name) != namedCoordinateSystems.end()) curTransform = namedCoordinateSystems[name]; else Warning("Couldn't find named coordinate system \"%s\"", name.c_str()); }

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.

<<API Macros>>+= 
#define WARN_IF_ANIMATED_TRANSFORM(func) \ do { if (curTransform.IsAnimated()) \ Warning("Animated transformations set; ignoring for \"%s\" " \ "and using the start transform only", func); \ } while (false) /* swallow trailing semicolon */

<<TransformSet Public Methods>>+= 
bool IsAnimated() const { for (int i = 0; i < MaxTransforms - 1; ++i) if (t[i] != t[i + 1]) return true; return false; }

B.2.3 Options

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.

<<API Local Classes>>+=  
struct RenderOptions { <<RenderOptions Public Methods>> 
Integrator *MakeIntegrator() const; Scene *MakeScene(); Camera *MakeCamera() const;
<<RenderOptions Public Data>> 
Float transformStartTime = 0, transformEndTime = 1; std::string FilterName = "box"; ParamSet FilterParams; std::string FilmName = "image"; ParamSet FilmParams; std::string SamplerName = "halton"; ParamSet SamplerParams; std::string AcceleratorName = "bvh"; ParamSet AcceleratorParams; std::string IntegratorName = "path"; ParamSet IntegratorParams; std::string CameraName = "perspective"; ParamSet CameraParams; TransformSet CameraToWorld; std::map<std::string, std::shared_ptr<Medium>> namedMedia; std::vector<std::shared_ptr<Light>> lights; std::vector<std::shared_ptr<Primitive>> primitives; std::map<std::string, std::vector<std::shared_ptr<Primitive>>> instances; std::vector<std::shared_ptr<Primitive>> *currentInstance = nullptr;
};

A single static instance of a RenderOptions structure is available to the rest of the API functions:

<<API Static Data>>+=  
static std::unique_ptr<RenderOptions> renderOptions;

When pbrtInit() is called, it allocates a RenderOptions structure that initially holds default values for all of its options:

<<API Initialization>>+=  
renderOptions.reset(new RenderOptions);

The renderOptions variable is freed by pbrtCleanup():

<<API Cleanup>>+= 
renderOptions.reset(nullptr);

A few calls are available to set which of the CTMs should be active.

<<API Function Definitions>>+=  
void pbrtActiveTransformAll() { activeTransformBits = AllTransformsBits; } void pbrtActiveTransformEndTime() { activeTransformBits = EndTransformBits; } void pbrtActiveTransformStartTime() { activeTransformBits = StartTransformBits; }

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.

<<API Function Definitions>>+=  
void pbrtTransformTimes(Float start, Float end) { VERIFY_OPTIONS("TransformTimes"); renderOptions->transformStartTime = start; renderOptions->transformEndTime = end; }

<<RenderOptions Public Data>>= 
Float transformStartTime = 0, transformEndTime = 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.)

<<API Function Definitions>>+=  
void pbrtPixelFilter(const std::string &name, const ParamSet &params) { VERIFY_OPTIONS("PixelFilter"); renderOptions->FilterName = name; renderOptions->FilterParams = params; }

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.

<<RenderOptions Public Data>>+=  
std::string FilterName = "box"; ParamSet FilterParams;

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.

<<API Function Declarations>>+=  
void pbrtFilm(const std::string &type, const ParamSet &params); void pbrtSampler(const std::string &name, const ParamSet &params); void pbrtAccelerator(const std::string &name, const ParamSet &params); void pbrtIntegrator(const std::string &name, const ParamSet &params);

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().

<<API Function Definitions>>+=  
void pbrtCamera(const std::string &name, const ParamSet &params) { VERIFY_OPTIONS("Camera"); renderOptions->CameraName = name; renderOptions->CameraParams = params; renderOptions->CameraToWorld = Inverse(curTransform); namedCoordinateSystems["camera"] = renderOptions->CameraToWorld; }

The default camera uses a perspective projection.

<<RenderOptions Public Data>>+=  
std::string CameraName = "perspective"; ParamSet CameraParams; TransformSet CameraToWorld;

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,

MakeNamedMedium "highAlbedo" "string type" "homogeneous" "color sigma_s" [5.0 5.0 5.0] "color sigma_a" [0.1 0.1 0.1]

creates a HomogeneousMedium instance with the name highAlbedo.

<<API Function Declarations>>+=  
void pbrtMakeNamedMedium(const std::string &name, const ParamSet &params);

The corresponding Medium instances are stored in an associative array for access later.

<<RenderOptions Public Data>>+=  
std::map<std::string, std::shared_ptr<Medium>> namedMedia;

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.

<<API Function Definitions>>+=  
void pbrtMediumInterface(const std::string &insideName, const std::string &outsideName) { VERIFY_INITIALIZED("MediumInterface"); graphicsState.currentInsideMedium = insideName; graphicsState.currentOutsideMedium = outsideName; }

<<Graphics State>>= 
std::string currentInsideMedium, currentOutsideMedium;