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.

<<API Function Definitions>>+=  
void pbrtWorldBegin() { VERIFY_OPTIONS("WorldBegin"); currentApiState = APIState::WorldBlock; for (int i = 0; i < MaxTransforms; ++i) curTransform[i] = Transform(); activeTransformBits = AllTransformsBits; namedCoordinateSystems["world"] = curTransform; }

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:

Material "matte" AttributeBegin Material "plastic" Translate 5 0 0 Shape "sphere" "float radius" [1] AttributeEnd Shape "sphere" "float radius" [1]

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.

The graphics state is stored in the GraphicsState structure. As was done previously with RenderOptions, we’ll be adding members to it throughout this section.

<<API Local Classes>>+= 
struct GraphicsState { <<Graphics State Methods>> 
std::shared_ptr<Material> CreateMaterial(const ParamSet &params); MediumInterface CreateMediumInterface();
<<Graphics State>> 
std::string currentInsideMedium, currentOutsideMedium; std::map<std::string, std::shared_ptr<Texture<Float>>> floatTextures; std::map<std::string, std::shared_ptr<Texture<Spectrum>>> spectrumTextures; ParamSet materialParams; std::string material = "matte"; std::map<std::string, std::shared_ptr<Material>> namedMaterials; std::string currentNamedMaterial; ParamSet areaLightParams; std::string areaLight; bool reverseOrientation = false;
};

When pbrtInit() is called, the current graphics state is initialized to hold default values.

<<API Initialization>>+= 
graphicsState = GraphicsState();

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.

<<API Function Definitions>>+=  
void pbrtAttributeBegin() { VERIFY_WORLD("AttributeBegin"); pushedGraphicsStates.push_back(graphicsState); pushedTransforms.push_back(curTransform); pushedActiveTransformBits.push_back(activeTransformBits); }

<<API Static Data>>+=  
static GraphicsState graphicsState; static std::vector<GraphicsState> pushedGraphicsStates; static std::vector<TransformSet> pushedTransforms; static std::vector<uint32_t> pushedActiveTransformBits;

pbrtAttributeEnd() also verifies that we do not have attribute stack underflow by checking to see if the stack is empty.

<<API Function Definitions>>+=  
void pbrtAttributeEnd() { VERIFY_WORLD("AttributeEnd"); if (!pushedGraphicsStates.size()) { Error("Unmatched pbrtAttributeEnd() encountered. " "Ignoring it."); return; } graphicsState = pushedGraphicsStates.back(); pushedGraphicsStates.pop_back(); curTransform = pushedTransforms.back(); pushedTransforms.pop_back(); activeTransformBits = pushedActiveTransformBits.back(); pushedActiveTransformBits.pop_back(); }

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.

<<API Function Declarations>>+=  
void pbrtTransformBegin(); void pbrtTransformEnd();

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.

<<TextureParams Declarations>>= 
class TextureParams { public: <<TextureParams Public Methods>> 
TextureParams(const ParamSet &geomParams, const ParamSet &materialParams, std::map<std::string, std::shared_ptr<Texture<Float>>> &fTex, std::map<std::string, std::shared_ptr<Texture<Spectrum>>> &sTex) : floatTextures(fTex), spectrumTextures(sTex), geomParams(geomParams), materialParams(materialParams) { } std::shared_ptr<Texture<Spectrum>> GetSpectrumTexture(const std::string &name, const Spectrum &def) const; std::shared_ptr<Texture<Float>> GetFloatTexture(const std::string &name, Float def) const; std::shared_ptr<Texture<Float>> GetFloatTextureOrNull(const std::string &name) const; Float FindFloat(const std::string &n, Float d) const { return geomParams.FindOneFloat(n, materialParams.FindOneFloat(n, d)); } std::string FindString(const std::string &n, const std::string &d = "") const { return geomParams.FindOneString(n, materialParams.FindOneString(n, d)); } std::string FindFilename(const std::string &n, const std::string &d = "") const { return geomParams.FindOneFilename(n, materialParams.FindOneFilename(n, d)); } int FindInt(const std::string &n, int d) const { return geomParams.FindOneInt(n, materialParams.FindOneInt(n, d)); } bool FindBool(const std::string &n, bool d) const { return geomParams.FindOneBool(n, materialParams.FindOneBool(n, d)); } Point3f FindPoint3f(const std::string &n, const Point3f &d) const { return geomParams.FindOnePoint3f(n, materialParams.FindOnePoint3f(n, d)); } Vector3f FindVector3f(const std::string &n, const Vector3f &d) const { return geomParams.FindOneVector3f(n, materialParams.FindOneVector3f(n, d)); } Normal3f FindNormal3f(const std::string &n, const Normal3f &d) const { return geomParams.FindOneNormal3f(n, materialParams.FindOneNormal3f(n, d)); } Spectrum FindSpectrum(const std::string &n, const Spectrum &d) const { return geomParams.FindOneSpectrum(n, materialParams.FindOneSpectrum(n, d)); } void ReportUnused() const { geomParams.ReportUnused(); materialParams.ReportUnused(); } const ParamSet &GetGeomParams() const { return geomParams; } const ParamSet &GetMaterialParams() const { return materialParams; }
private: <<TextureParams Private Data>> 
std::map<std::string, std::shared_ptr<Texture<Float>>> &floatTextures; std::map<std::string, std::shared_ptr<Texture<Spectrum>>> &spectrumTextures; const ParamSet &geomParams, &materialParams;
};

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.

<<TextureParams Private Data>>= 
std::map<std::string, std::shared_ptr<Texture<Float>>> &floatTextures; std::map<std::string, std::shared_ptr<Texture<Spectrum>>> &spectrumTextures; const ParamSet &geomParams, &materialParams;

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

Material "matte" "color Kd" [ 1 0 0 ] Shape "sphere" "color Kd" [ 0 1 0 ]

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.

<<TextureParams Method Definitions>>= 
std::shared_ptr<Texture<Spectrum>> TextureParams::GetSpectrumTexture(const std::string &n, const Spectrum &def) const { std::string name = geomParams.FindTexture(n); if (name == "") name = materialParams.FindTexture(n); if (name != "") { if (spectrumTextures.find(name) != spectrumTextures.end()) return spectrumTextures[name]; else Error("Couldn't find spectrum texture named \"%s\" " "for parameter \"%s\"", name.c_str(), n.c_str()); } Spectrum val = materialParams.FindOneSpectrum(n, def); val = geomParams.FindOneSpectrum(n, val); return std::make_shared<ConstantTexture<Spectrum>>(val); }

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.

<<TextureParams Public Methods>>= 
Float FindFloat(const std::string &n, Float d) const { return geomParams.FindOneFloat(n, materialParams.FindOneFloat(n, d)); }

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.

<<API Function Definitions>>+=  
void pbrtTexture(const std::string &name, const std::string &type, const std::string &texname, const ParamSet &params) { VERIFY_WORLD("Texture"); TextureParams tp(params, params, graphicsState.floatTextures, graphicsState.spectrumTextures); if (type == "float") { <<Create Float texture and store in floatTextures>> 
if (graphicsState.floatTextures.find(name) != graphicsState.floatTextures.end()) Info("Texture \"%s\" being redefined", name.c_str()); WARN_IF_ANIMATED_TRANSFORM("Texture"); std::shared_ptr<Texture<Float>> ft = MakeFloatTexture(texname, curTransform[0], tp); if (ft) graphicsState.floatTextures[name] = ft;
} else if (type == "color" || type == "spectrum") { <<Create color texture and store in spectrumTextures>> 
if (graphicsState.spectrumTextures.find(name) != graphicsState.spectrumTextures.end()) Info("Texture \"%s\" being redefined", name.c_str()); WARN_IF_ANIMATED_TRANSFORM("Texture"); std::shared_ptr<Texture<Spectrum>> st = MakeSpectrumTexture(texname, curTransform[0], tp); if (st) graphicsState.spectrumTextures[name] = st;
} else Error("Texture type \"%s\" unknown.", type.c_str()); }

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.

<<Create Float texture and store in floatTextures>>= 
if (graphicsState.floatTextures.find(name) != graphicsState.floatTextures.end()) Info("Texture \"%s\" being redefined", name.c_str()); WARN_IF_ANIMATED_TRANSFORM("Texture"); std::shared_ptr<Texture<Float>> ft = MakeFloatTexture(texname, curTransform[0], tp); if (ft) graphicsState.floatTextures[name] = ft;

<<Graphics State>>+=  
std::map<std::string, std::shared_ptr<Texture<Float>>> floatTextures; std::map<std::string, std::shared_ptr<Texture<Spectrum>>> spectrumTextures;

The current material is specified by a call to pbrtMaterial(). Its ParamSet is stored until a Material object needs to be created later when a shape is specified.

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

The default material is matte.

<<Graphics State>>+=  
ParamSet materialParams; std::string material = "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.

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

<<Graphics State>>+=  
std::map<std::string, std::shared_ptr<Material>> namedMaterials; std::string currentNamedMaterial;

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

<<API Function Definitions>>+=  
void pbrtLightSource(const std::string &name, const ParamSet &params) { VERIFY_WORLD("LightSource"); WARN_IF_ANIMATED_TRANSFORM("LightSource"); MediumInterface mi = graphicsState.CreateMediumInterface(); std::shared_ptr<Light> lt = MakeLight(name, params, curTransform[0], mi); if (!lt) Error("LightSource: light type \"%s\" unknown.", name.c_str()); else renderOptions->lights.push_back(lt); }

<<RenderOptions Public Data>>+=  
std::vector<std::shared_ptr<Light>> lights;

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.

<<Graphics State>>+= 
ParamSet areaLightParams; std::string areaLight;

<<API Function Definitions>>+=  
void pbrtAreaLightSource(const std::string &name, const ParamSet &params) { VERIFY_WORLD("AreaLightSource"); graphicsState.areaLight = name; graphicsState.areaLightParams = params; }

B.3.5 Shapes

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.

<<API Function Definitions>>+=  
void pbrtShape(const std::string &name, const ParamSet &params) { VERIFY_WORLD("Shape"); std::vector<std::shared_ptr<Primitive>> prims; std::vector<std::shared_ptr<AreaLight>> areaLights; if (!curTransform.IsAnimated()) { <<Initialize prims and areaLights for static shape>> 
<<Create shapes for shape name>> 
Transform *ObjToWorld, *WorldToObj; transformCache.Lookup(curTransform[0], &ObjToWorld, &WorldToObj); std::vector<std::shared_ptr<Shape>> shapes = MakeShapes(name, ObjToWorld, WorldToObj, graphicsState.reverseOrientation, params); if (shapes.size() == 0) return;
std::shared_ptr<Material> mtl = graphicsState.CreateMaterial(params); params.ReportUnused(); MediumInterface mi = graphicsState.CreateMediumInterface(); for (auto s : shapes) { <<Possibly create area light for shape>> 
std::shared_ptr<AreaLight> area; if (graphicsState.areaLight != "") { area = MakeAreaLight(graphicsState.areaLight, curTransform[0], mi, graphicsState.areaLightParams, s); areaLights.push_back(area); }
prims.push_back( std::make_shared<GeometricPrimitive>(s, mtl, area, mi)); }
} else { <<Initialize prims and areaLights for animated shape>> 
<<Create initial shape or shapes for animated shape>> 
if (graphicsState.areaLight != "") Warning("Ignoring currently set area light when creating " "animated shape"); Transform *identity; transformCache.Lookup(Transform(), &identity, nullptr); std::vector<std::shared_ptr<Shape>> shapes = MakeShapes(name, identity, identity, graphicsState.reverseOrientation, params); if (shapes.size() == 0) return;
<<Create GeometricPrimitive(s) for animated shape>> 
std::shared_ptr<Material> mtl = graphicsState.CreateMaterial(params); params.ReportUnused(); MediumInterface mi = graphicsState.CreateMediumInterface(); for (auto s : shapes) prims.push_back( std::make_shared<GeometricPrimitive>(s, mtl, nullptr, mi));
<<Create single TransformedPrimitive for prims>> 
<<Get animatedObjectToWorld transform for shape>> 
Transform *ObjToWorld[2]; transformCache.Lookup(curTransform[0], &ObjToWorld[0], nullptr); transformCache.Lookup(curTransform[1], &ObjToWorld[1], nullptr); AnimatedTransform animatedObjectToWorld( ObjToWorld[0], renderOptions->transformStartTime, ObjToWorld[1], renderOptions->transformEndTime);
if (prims.size() > 1) { std::shared_ptr<Primitive> bvh = std::make_shared<BVHAccel>(prims); prims.clear(); prims.push_back(bvh); } prims[0] = std::make_shared<TransformedPrimitive>(prims[0], animatedObjectToWorld);
} <<Add prims and areaLights to scene or current instance>> 
if (renderOptions->currentInstance) { if (areaLights.size()) Warning("Area lights not supported with object instancing"); renderOptions->currentInstance->insert( renderOptions->currentInstance->end(), prims.begin(), prims.end()); } else { renderOptions->primitives.insert(renderOptions->primitives.end(), prims.begin(), prims.end()); if (areaLights.size()) renderOptions->lights.insert(renderOptions->lights.end(), areaLights.begin(), areaLights.end()); }
}

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 static shape case is mostly a matter of creating the appropriate Shapes, Material, and MediumInterface to make corresponding GeometricPrimitives.

<<Initialize prims and areaLights for static shape>>= 
<<Create shapes for shape name>> 
Transform *ObjToWorld, *WorldToObj; transformCache.Lookup(curTransform[0], &ObjToWorld, &WorldToObj); std::vector<std::shared_ptr<Shape>> shapes = MakeShapes(name, ObjToWorld, WorldToObj, graphicsState.reverseOrientation, params); if (shapes.size() == 0) return;
std::shared_ptr<Material> mtl = graphicsState.CreateMaterial(params); params.ReportUnused(); MediumInterface mi = graphicsState.CreateMediumInterface(); for (auto s : shapes) { <<Possibly create area light for shape>> 
std::shared_ptr<AreaLight> area; if (graphicsState.areaLight != "") { area = MakeAreaLight(graphicsState.areaLight, curTransform[0], mi, graphicsState.areaLightParams, s); areaLights.push_back(area); }
prims.push_back( std::make_shared<GeometricPrimitive>(s, mtl, area, mi)); }

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.

<<Create shapes for shape name>>= 
Transform *ObjToWorld, *WorldToObj; transformCache.Lookup(curTransform[0], &ObjToWorld, &WorldToObj); std::vector<std::shared_ptr<Shape>> shapes = MakeShapes(name, ObjToWorld, WorldToObj, graphicsState.reverseOrientation, params); if (shapes.size() == 0) return;

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.

<<TransformCache Private Data>>= 
std::map<Transform, std::pair<Transform *, Transform *>> cache; MemoryArena arena;

<<API Static Data>>+= 
static TransformCache transformCache;

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.

<<API Forward Declarations>>= 
std::vector<std::shared_ptr<Shape>> MakeShapes(const std::string &name, const Transform *ObjectToWorld, const Transform *WorldToObject, bool reverseOrientation, const ParamSet &paramSet);

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.

<<Graphics State Methods>>= 
std::shared_ptr<Material> CreateMaterial(const ParamSet &params);

Following the same basic approach, CreateMediumInterface() creates a MediumInterface based on the current named “inside” and “outside” media established with pbrtMediumInterface().

<<Graphics State Methods>>+= 
MediumInterface CreateMediumInterface();

If an area light has been set in the current graphics state by pbrtAreaLightSource(), the new shape is an emitter and an AreaLight needs to be made for it.

<<Possibly create area light for shape>>= 
std::shared_ptr<AreaLight> area; if (graphicsState.areaLight != "") { area = MakeAreaLight(graphicsState.areaLight, curTransform[0], mi, graphicsState.areaLightParams, s); areaLights.push_back(area); }

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.

<<Initialize prims and areaLights for animated shape>>= 
<<Create initial shape or shapes for animated shape>> 
if (graphicsState.areaLight != "") Warning("Ignoring currently set area light when creating " "animated shape"); Transform *identity; transformCache.Lookup(Transform(), &identity, nullptr); std::vector<std::shared_ptr<Shape>> shapes = MakeShapes(name, identity, identity, graphicsState.reverseOrientation, params); if (shapes.size() == 0) return;
<<Create GeometricPrimitive(s) for animated shape>> 
std::shared_ptr<Material> mtl = graphicsState.CreateMaterial(params); params.ReportUnused(); MediumInterface mi = graphicsState.CreateMediumInterface(); for (auto s : shapes) prims.push_back( std::make_shared<GeometricPrimitive>(s, mtl, nullptr, mi));
<<Create single TransformedPrimitive for prims>> 
<<Get animatedObjectToWorld transform for shape>> 
Transform *ObjToWorld[2]; transformCache.Lookup(curTransform[0], &ObjToWorld[0], nullptr); transformCache.Lookup(curTransform[1], &ObjToWorld[1], nullptr); AnimatedTransform animatedObjectToWorld( ObjToWorld[0], renderOptions->transformStartTime, ObjToWorld[1], renderOptions->transformEndTime);
if (prims.size() > 1) { std::shared_ptr<Primitive> bvh = std::make_shared<BVHAccel>(prims); prims.clear(); prims.push_back(bvh); } prims[0] = std::make_shared<TransformedPrimitive>(prims[0], animatedObjectToWorld);

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.

<<Create initial shape or shapes for animated shape>>= 
if (graphicsState.areaLight != "") Warning("Ignoring currently set area light when creating " "animated shape"); Transform *identity; transformCache.Lookup(Transform(), &identity, nullptr); std::vector<std::shared_ptr<Shape>> shapes = MakeShapes(name, identity, identity, graphicsState.reverseOrientation, params); if (shapes.size() == 0) return;

Given the initial set of shapes, it’s straightforward to create a GeometricPrimitive for each of them.

<<Create GeometricPrimitive(s) for animated shape>>= 
std::shared_ptr<Material> mtl = graphicsState.CreateMaterial(params); params.ReportUnused(); MediumInterface mi = graphicsState.CreateMediumInterface(); for (auto s : shapes) prims.push_back( std::make_shared<GeometricPrimitive>(s, mtl, nullptr, mi));

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.

<<Create single TransformedPrimitive for prims>>= 
<<Get animatedObjectToWorld transform for shape>> 
Transform *ObjToWorld[2]; transformCache.Lookup(curTransform[0], &ObjToWorld[0], nullptr); transformCache.Lookup(curTransform[1], &ObjToWorld[1], nullptr); AnimatedTransform animatedObjectToWorld( ObjToWorld[0], renderOptions->transformStartTime, ObjToWorld[1], renderOptions->transformEndTime);
if (prims.size() > 1) { std::shared_ptr<Primitive> bvh = std::make_shared<BVHAccel>(prims); prims.clear(); prims.push_back(bvh); } prims[0] = std::make_shared<TransformedPrimitive>(prims[0], animatedObjectToWorld);

The TransformCache is used again, here to get transformations for the start and end time, which are then passed to the AnimatedTransform constructor.

<<Get animatedObjectToWorld transform for shape>>= 
Transform *ObjToWorld[2]; transformCache.Lookup(curTransform[0], &ObjToWorld[0], nullptr); transformCache.Lookup(curTransform[1], &ObjToWorld[1], nullptr); AnimatedTransform animatedObjectToWorld( ObjToWorld[0], renderOptions->transformStartTime, ObjToWorld[1], renderOptions->transformEndTime);

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.

<<Add prims and areaLights to scene or current instance>>= 
if (renderOptions->currentInstance) { if (areaLights.size()) Warning("Area lights not supported with object instancing"); renderOptions->currentInstance->insert( renderOptions->currentInstance->end(), prims.begin(), prims.end()); } else { renderOptions->primitives.insert(renderOptions->primitives.end(), prims.begin(), prims.end()); if (areaLights.size()) renderOptions->lights.insert(renderOptions->lights.end(), areaLights.begin(), areaLights.end()); }

<<RenderOptions Public Data>>+=  
std::vector<std::shared_ptr<Primitive>> primitives;

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.

<<API Function Definitions>>+=  
void pbrtObjectBegin(const std::string &name) { pbrtAttributeBegin(); if (renderOptions->currentInstance) Error("ObjectBegin called inside of instance definition"); renderOptions->instances[name] = std::vector<std::shared_ptr<Primitive>>(); renderOptions->currentInstance = &renderOptions->instances[name]; }

<<RenderOptions Public Data>>+= 
std::map<std::string, std::vector<std::shared_ptr<Primitive>>> instances; std::vector<std::shared_ptr<Primitive>> *currentInstance = nullptr;

<<API Function Definitions>>+=  
void pbrtObjectEnd() { VERIFY_WORLD("ObjectEnd"); if (!renderOptions->currentInstance) Error("ObjectEnd called outside of instance definition"); renderOptions->currentInstance = nullptr; pbrtAttributeEnd(); }

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.

<<API Function Definitions>>+=  
void pbrtObjectInstance(const std::string &name) { VERIFY_WORLD("ObjectInstance"); <<Perform object instance error checking>> 
if (renderOptions->currentInstance) { Error("ObjectInstance can't be called inside instance definition"); return; } if (renderOptions->instances.find(name) == renderOptions->instances.end()) { Error("Unable to find instance named \"%s\"", name.c_str()); return; }
std::vector<std::shared_ptr<Primitive>> &in = renderOptions->instances[name]; if (in.size() == 0) return; if (in.size() > 1) { <<Create aggregate for instance Primitives>> 
std::shared_ptr<Primitive> accel( MakeAccelerator(renderOptions->AcceleratorName, in, renderOptions->AcceleratorParams)); if (!accel) accel = std::make_shared<BVHAccel>(in); in.erase(in.begin(), in.end()); in.push_back(accel);
} <<Create animatedInstanceToWorld transform for instance>> 
Transform *InstanceToWorld[2]; transformCache.Lookup(curTransform[0], &InstanceToWorld[0], nullptr); transformCache.Lookup(curTransform[1], &InstanceToWorld[1], nullptr); AnimatedTransform animatedInstanceToWorld(InstanceToWorld[0], renderOptions->transformStartTime, InstanceToWorld[1], renderOptions->transformEndTime);
std::shared_ptr<Primitive> prim( std::make_shared<TransformedPrimitive>(in[0], animatedInstanceToWorld)); renderOptions->primitives.push_back(prim); }

<<Create animatedInstanceToWorld transform for instance>>= 
Transform *InstanceToWorld[2]; transformCache.Lookup(curTransform[0], &InstanceToWorld[0], nullptr); transformCache.Lookup(curTransform[1], &InstanceToWorld[1], nullptr); AnimatedTransform animatedInstanceToWorld(InstanceToWorld[0], renderOptions->transformStartTime, InstanceToWorld[1], renderOptions->transformEndTime);

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.

<<Create aggregate for instance Primitives>>= 
std::shared_ptr<Primitive> accel( MakeAccelerator(renderOptions->AcceleratorName, in, renderOptions->AcceleratorParams)); if (!accel) accel = std::make_shared<BVHAccel>(in); in.erase(in.begin(), in.end()); in.push_back(accel);

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.

<<API Function Definitions>>+=  
void pbrtWorldEnd() { VERIFY_WORLD("WorldEnd"); <<Ensure there are no pushed graphics states>> 
while (pushedGraphicsStates.size()) { Warning("Missing end to pbrtAttributeBegin()"); pushedGraphicsStates.pop_back(); pushedTransforms.pop_back(); } while (pushedTransforms.size()) { Warning("Missing end to pbrtTransformBegin()"); pushedTransforms.pop_back(); }
<<Create scene and render>> 
std::unique_ptr<Integrator> integrator(renderOptions->MakeIntegrator()); std::unique_ptr<Scene> scene(renderOptions->MakeScene()); if (scene && integrator) integrator->Render(*scene); TerminateWorkerThreads();
<<Clean up after rendering>> 
graphicsState = GraphicsState(); transformCache.Clear(); currentApiState = APIState::OptionsBlock; ReportThreadStats(); if (PbrtOptions.quiet == false) { PrintStats(stdout); ReportProfilerResults(stdout); } for (int i = 0; i < MaxTransforms; ++i) curTransform[i] = Transform(); activeTransformBits = AllTransformsBits; namedCoordinateSystems.erase(namedCoordinateSystems.begin(), namedCoordinateSystems.end()); ImageTexture<Float, Float>::ClearCache(); ImageTexture<RGBSpectrum, Spectrum>::ClearCache();
}

If there are graphics states and/or transformations remaining on the respective stacks, a warning is issued for each one:

<<Ensure there are no pushed graphics states>>= 
while (pushedGraphicsStates.size()) { Warning("Missing end to pbrtAttributeBegin()"); pushedGraphicsStates.pop_back(); pushedTransforms.pop_back(); } while (pushedTransforms.size()) { Warning("Missing end to pbrtTransformBegin()"); pushedTransforms.pop_back(); }

Now the RenderOptions::MakeIntegrator() and RenderOptions::MakeScene() methods can create the corresponding objects based on the settings provided by the user.

<<Create scene and render>>= 
std::unique_ptr<Integrator> integrator(renderOptions->MakeIntegrator()); std::unique_ptr<Scene> scene(renderOptions->MakeScene()); if (scene && integrator) integrator->Render(*scene); TerminateWorkerThreads();

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.

<<API Function Definitions>>+= 
Scene *RenderOptions::MakeScene() { std::shared_ptr<Primitive> accelerator = MakeAccelerator(AcceleratorName, primitives, AcceleratorParams); if (!accelerator) accelerator = std::make_shared<BVHAccel>(primitives); Scene *scene = new Scene(accelerator, lights); <<Erase primitives and lights from RenderOptions>> 
primitives.erase(primitives.begin(), primitives.end()); lights.erase(lights.begin(), lights.end());
return scene; }

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.

<<Erase primitives and lights from RenderOptions>>= 
primitives.erase(primitives.begin(), primitives.end()); lights.erase(lights.begin(), lights.end());

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.

<<RenderOptions Public Methods>>= 
Integrator *MakeIntegrator() const;

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.

<<Clean up after rendering>>= 
currentApiState = APIState::OptionsBlock; ReportThreadStats(); if (PbrtOptions.quiet == false) { PrintStats(stdout); ReportProfilerResults(stdout); } for (int i = 0; i < MaxTransforms; ++i) curTransform[i] = Transform(); activeTransformBits = AllTransformsBits; namedCoordinateSystems.erase(namedCoordinateSystems.begin(), namedCoordinateSystems.end());