4.1 Primitive Interface and Geometric Primitives
The abstract Primitive base class is the bridge between the geometry processing and shading subsystems of pbrt.
There are a number of geometric routines in the Primitive interface, all of which are similar to a corresponding Shape method. The first, Primitive::WorldBound(), returns a box that encloses the primitive’s geometry in world space. There are many uses for such a bound; one of the most important is to place the Primitive in the acceleration data structures.
The next two methods provide ray intersection tests. One difference between the two base classes is that Shape::Intersect() returns the parametric distance along the ray to the intersection in a Float * output variable, while Primitive::Intersect() is responsible for updating Ray::tMax with this value if an intersection is found.
Upon finding an intersection, the Primitive’s Intersect() method is also responsible for initializing additional SurfaceInteraction member variables, including a pointer to the Primitive that the ray hit.
Primitive objects have a few methods related to non-geometric properties as well. The first, Primitive::GetAreaLight(), returns a pointer to the AreaLight that describes the primitive’s emission distribution, if the primitive is itself a light source. If the primitive is not emissive, this method should return nullptr.
GetMaterial() returns a pointer to the material instance assigned to the primitive. If nullptr is returned, ray intersections with the primitive should be ignored; the primitive only serves to delineate a volume of space for participating media. This method is also used to check if two rays have intersected the same object by comparing their Material pointers.
The third material-related method, ComputeScatteringFunctions(), initializes representations of the light-scattering properties of the material at the intersection point on the surface. The BSDF object (introduced in Section 9.1) describes local light-scattering properties at the intersection point. If applicable, this method also initializes a BSSRDF, which describes subsurface scattering inside the primitive—light that enters the surface at points far from where it exits. While subsurface light transport has little effect on the appearance of objects like metal, cloth, or plastic, it is the dominant light-scattering mechanism for biological materials like skin, thick liquids like milk, etc. The BSSRDF is supported by an extension of the path tracing algorithm discussed in Section 15.
In addition to a MemoryArena to allocate memory for the BSDF and/or BSSRDF, this method takes a TransportMode enumerant that indicates whether the ray path that found this intersection point started from the camera or from a light source; as will be discussed further in Section 16.1, this detail has important implications for how some parts of material models are evaluated. The allowMultipleLobes parameter controls a detail of how some types of BRDFs are represented; it is discussed further in Section 9.2. Section 9.1.1 discusses the use of the MemoryArena for BSDF memory allocation in more detail.
4.1.1 Geometric Primitives
The GeometricPrimitive class represents a single shape (e.g., a sphere) in the scene. One GeometricPrimitive is allocated for each shape in the scene description provided by the user. This class is implemented in the files core/primitive.h and core/primitive.cpp.
Each GeometricPrimitive holds a reference to a Shape and its Material. In addition, because primitives in pbrt may be area light sources, it stores a pointer to an AreaLight object that describes its emission characteristics (this pointer is set to nullptr if the primitive does not emit light). Finally, the MediumInterface attribute encodes information about the participating media on the inside and outside of the primitive.
The GeometricPrimitive constructor just initializes these variables from the parameters passed to it. It’s straightforward, so we don’t include it here.
Most of the methods of the Primitive interface related to geometric processing are simply forwarded to the corresponding Shape method. For example, GeometricPrimitive::Intersect() calls the Shape::Intersect() method of its enclosed Shape to do the actual intersection test and initialize a SurfaceInteraction to describe the intersection, if any. It also uses the returned parametric hit distance to update the Ray::tMax member. The advantage of storing the distance to the closest hit in Ray::tMax is that this makes it easy to avoid performing intersection tests with any primitives that lie farther along the ray than any already-found intersections.
We won’t include the implementations of the GeometricPrimitive’s WorldBound() or IntersectP() methods here; they just forward these requests on to the Shape in a similar manner. Similarly, GetAreaLight() just returns the GeometricPrimitive::areaLight member.
Finally, the ComputeScatteringFunctions() method just forwards the request on to the Material.
4.1.2 TransformedPrimitive: Object Instancing and Animated Primitives
TransformedPrimitive holds a single Primitive and also includes an AnimatedTransform that is injected in between the underlying primitive and its representation in the scene. This extra transformation enables two useful features: object instancing and primitives with animated transformations.
Object instancing is a classic technique in rendering that reuses transformed copies of a single collection of geometry at multiple positions in a scene. For example, in a model of a concert hall with thousands of identical seats, the scene description can be compressed substantially if all of the seats refer to a shared geometric representation of a single seat. The ecosystem scene in Figure 4.1 has 23,241 individual plants of various types, although only 31 unique plant models. Because each plant model is instanced multiple times with a different transformation for each instance, the complete scene has a total of 3.1 billion triangles, although only 24 million triangles are stored in memory, thanks to primitive reuse through object instancing. pbrt uses just over 7 GB of memory when rendering this scene with object instancing (1.7 GB for BVHs, 2.3 GB for triangle meshes, and 3 GB for texture maps), but would need upward of 516 GB to render it without instancing.
Animated transformations enable rigid-body animation of primitives in the scene via the AnimatedTransform class. See Figure 2.15 for an image that exhibits motion blur due to animated transformations.
Recall that the Shapes of Chapter 3 themselves had object-to-world transformations applied to them to place them in the scene. If a shape is held by a TransformedPrimitive, then the shape’s notion of world space isn’t the actual scene world space—only after the TransformedPrimitive’s transformation is also applied is the shape actually in world space. For the applications here, it makes sense for the shape to not be at all aware of the additional transformations being applied. For animated shapes, it’s simpler to isolate all of the handling of animated transformations to a single class here, rather than require all Shapes to support AnimatedTransforms. Similarly, for instanced primitives, letting Shapes know all of the instance transforms is of limited utility: we wouldn’t want the TriangleMesh to make a copy of its vertex positions for each instance transformation and transform them all the way to world space, since this would negate the memory savings of object instancing.
The TransformedPrimitive constructor takes a reference to the Primitive that represents the model, and the transformation that places it in the scene. If the geometry is described by multiple Primitives, the calling code is responsible for placing them in an Aggregate implementation so that only a single Primitive needs to be stored here. For the code that creates aggregates as needed, see the pbrtObjectInstance() function in Section A.3.6 of Appendix B for the case of primitive instances, and see the pbrtShape() function in Section A.3.5 for animated shapes.
The key task of the TransformedPrimitive is to bridge the Primitive interface that it implements and the Primitive that it holds a pointer to, accounting for the effects of the additional transformation that it holds. The TransformedPrimitive’s PrimitiveToWorld transformation defines the transformation from the coordinate system of this particular instance of the geometry to world space. If the primitive member has its own transformation, that should be interpreted as the transformation from object space to the TransformedPrimitive’s coordinate system. The complete transformation to world space requires both of these transformations together.
As such, the TransformedPrimitive::Intersect() method transforms the given ray to the primitive’s coordinate system and passes the transformed ray to its Intersect() routine. If a hit is found, the tMax value from the transformed ray needs to be copied into the ray r originally passed to the Intersect() routine.
To transform the ray, we need to interpolate the transformation based on the ray’s time. Although we want to transform the ray r from world space to primitive space, here we actually interpolate PrimitiveToWorld and then invert the resulting Transform to get the transformation. This surprising approach is necessary because of how the polar decomposition-based transformation interpolation algorithm in Section 2.9.3 works: interpolating PrimitiveToWorld to some time and inverting it doesn’t necessarily give the same result as interpolating its inverse, the animated world to primitive transformation, directly. Because Primitive::WorldBound() uses PrimitiveToWorld to compute the primitive’s bounding box, we must also interpolate PrimitiveToWorld here for consistency.
Finally, the SurfaceInteraction at the intersection point needs to be transformed to world space; the primitive’s intersection member will already have transformed the SurfaceInteraction to its notion of world space, so here we only need to apply the effect of the additional transformation held here.
The TransformedPrimitive GetAreaLight(), GetMaterial(), and ComputeScattering Functions() methods should never be called. The corresponding methods of the primitive that the ray actually hit should always be called instead. Therefore, any attempt to call the TransformedPrimitive implementations of these methods (not shown here) results in a run-time error.