2.10 Interactions

The last abstraction in this chapter, SurfaceInteraction, represents local information at a point on a 2D surface. For example, the ray–shape intersection routines in Chapter 3 return information about the local differential geometry at intersection points in a SurfaceInteraction. Later, the texturing code in Chapter 10 computes material properties given a point on a surface represented by a SurfaceInteraction. The closely related MediumInteraction class is used to represent points where light scatters in participating media like smoke or clouds; it will be defined in Section 11.3 after additional preliminaries have been introduced. The implementations of these classes are in the files core/interaction.h and core/interaction.cpp.

Both SurfaceInteraction and MediumInteraction inherit from a generic Interaction class, which provides some common member variables and methods. Some parts of the system (notably the light source implementations) operate with respect to Interactions, as the differences between surface and medium interactions don’t matter to them.

<<Interaction Declarations>>= 
struct Interaction { <<Interaction Public Methods>> 
Interaction() : time(0) { } Interaction(const Point3f &p, const Normal3f &n, const Vector3f &pError, const Vector3f &wo, Float time, const MediumInterface &mediumInterface) : p(p), time(time), pError(pError), wo(wo), n(n), mediumInterface(mediumInterface) { } bool IsSurfaceInteraction() const { return n != Normal3f(); } Ray SpawnRay(const Vector3f &d) const { Point3f o = OffsetRayOrigin(p, pError, n, d); return Ray(o, d, Infinity, time, GetMedium(d)); } Ray SpawnRayTo(const Point3f &p2) const { Point3f origin = OffsetRayOrigin(p, pError, n, p2 - p); Vector3f d = p2 - origin; return Ray(origin, d, 1 - ShadowEpsilon, time, GetMedium(d)); } Ray SpawnRayTo(const Interaction &it) const { Point3f origin = OffsetRayOrigin(p, pError, n, it.p - p); Point3f target = OffsetRayOrigin(it.p, it.pError, it.n, origin - it.p); Vector3f d = target - origin; return Ray(origin, d, 1-ShadowEpsilon, time, GetMedium(d)); } Interaction(const Point3f &p, const Vector3f &wo, Float time, const MediumInterface &mediumInterface) : p(p), time(time), wo(wo), mediumInterface(mediumInterface) { } Interaction(const Point3f &p, Float time, const MediumInterface &mediumInterface) : p(p), time(time), mediumInterface(mediumInterface) { } bool IsMediumInteraction() const { return !IsSurfaceInteraction(); } const Medium *GetMedium(const Vector3f &w) const { return Dot(w, n) > 0 ? mediumInterface.outside : mediumInterface.inside; } const Medium *GetMedium() const { Assert(mediumInterface.inside == mediumInterface.outside); return mediumInterface.inside; }
<<Interaction Public Data>> 
Point3f p; Float time; Vector3f pError; Vector3f wo; Normal3f n; MediumInterface mediumInterface;
};

A number of Interaction constructors are available; depending on what sort of interaction is being constructed and what sort of information about it is relevant, corresponding sets of parameters are accepted. This one is the most general of them.

<<Interaction Public Methods>>= 
Interaction(const Point3f &p, const Normal3f &n, const Vector3f &pError, const Vector3f &wo, Float time, const MediumInterface &mediumInterface) : p(p), time(time), pError(pError), wo(wo), n(n), mediumInterface(mediumInterface) { }

All interactions must have a point normal p Subscript and time associated with them.

<<Interaction Public Data>>= 
Point3f p; Float time;

For interactions where the point normal p Subscript was computed by ray intersection, some floating-point error is generally present in the p value. pError gives a conservative bound on this error; it’s left-parenthesis 0 comma 0 comma 0 right-parenthesis for points in participating media. See Section 3.9 for more on pbrt’s approach to managing floating-point error and in particular Section 3.9.4 for how this bound is computed for various shapes.

<<Interaction Public Data>>+=  
Vector3f pError;

For interactions that lie along a ray (either from a ray–shape intersection or from a ray passing through participating media), the negative ray direction is stored in wo, which corresponds to omega Subscript normal o , the notation we use for the outgoing direction when computing lighting at points. For other types of interaction points where the notion of an outgoing direction doesn’t apply (e.g., those found by randomly sampling points on the surface of shapes), wo has the value left-parenthesis 0 comma 0 comma 0 right-parenthesis .

<<Interaction Public Data>>+=  

For interactions on surfaces, n stores the surface normal at the point.

<<Interaction Public Data>>+=  

<<Interaction Public Methods>>+=  
bool IsSurfaceInteraction() const { return n != Normal3f(); }

Interactions also need to record the scattering media at their point (if any); this is handled by an instance of the MediumInterface class, which is defined in Section 11.3.1.

<<Interaction Public Data>>+= 
MediumInterface mediumInterface;

2.10.1 Surface Interaction

The geometry of a particular point on a surface (often a position found by intersecting a ray against the surface) is represented by a SurfaceInteraction. Having this abstraction lets most of the system work with points on surfaces without needing to consider the particular type of geometric shape the points lie on; the SurfaceInteraction abstraction supplies enough information about the surface point to allow the shading and geometric operations in the rest of pbrt to be implemented generically.

<<SurfaceInteraction Declarations>>= 
class SurfaceInteraction : public Interaction { public: <<SurfaceInteraction Public Methods>> 
SurfaceInteraction() { } SurfaceInteraction(const Point3f &p, const Vector3f &pError, const Point2f &uv, const Vector3f &wo, const Vector3f &dpdu, const Vector3f &dpdv, const Normal3f &dndu, const Normal3f &dndv, Float time, const Shape *sh); void SetShadingGeometry(const Vector3f &dpdu, const Vector3f &dpdv, const Normal3f &dndu, const Normal3f &dndv, bool orientationIsAuthoritative); void ComputeScatteringFunctions(const RayDifferential &ray, MemoryArena &arena, bool allowMultipleLobes = false, TransportMode mode = TransportMode::Radiance); void ComputeDifferentials(const RayDifferential &r) const; Spectrum Le(const Vector3f &w) const;
<<SurfaceInteraction Public Data>> 
Point2f uv; Vector3f dpdu, dpdv; Normal3f dndu, dndv; const Shape *shape = nullptr; struct { Normal3f n; Vector3f dpdu, dpdv; Normal3f dndu, dndv; } shading; const Primitive *primitive = nullptr; BSDF *bsdf = nullptr; BSSRDF *bssrdf = nullptr; mutable Vector3f dpdx, dpdy; mutable Float dudx = 0, dvdx = 0, dudy = 0, dvdy = 0;
};

In addition to the point p and surface normal n from the Interaction base class, the SurfaceInteraction also stores left-parenthesis u comma v right-parenthesis coordinates from the parameterization of the surface and the parametric partial derivatives of the point partial-differential normal p slash partial-differential u and partial-differential normal p slash partial-differential v . See Figure 2.19 for a depiction of these values. It’s also useful to have a pointer to the Shape that the point lies on (the Shape class will be introduced in the next chapter) as well as the partial derivatives of the surface normal.

<<SurfaceInteraction Public Data>>= 
Point2f uv; Vector3f dpdu, dpdv; Normal3f dndu, dndv; const Shape *shape = nullptr;

Figure 2.19: The Local Differential Geometry around a Point normal p Subscript . The parametric partial derivatives of the surface, partial-differential normal p slash partial-differential u and partial-differential normal p slash partial-differential v , lie in the tangent plane but are not necessarily orthogonal. The surface normal bold n Subscript is given by the cross product of partial-differential normal p slash partial-differential u and partial-differential normal p slash partial-differential v . The vectors partial-differential bold n Subscript slash partial-differential u and partial-differential bold n Subscript slash partial-differential v (not shown here) record the differential change in surface normal as we move u and v along the surface.

This representation implicitly assumes that shapes have a parametric description—that for some range of left-parenthesis u comma v right-parenthesis values, points on the surface are given by some function f such that p equals f left-parenthesis u comma v right-parenthesis . Although this isn’t true for all shapes, all of the shapes that pbrt supports do have at least a local parametric description, so we will stick with the parametric representation since this assumption is helpful elsewhere (e.g., for antialiasing of textures in Chapter 10).

The SurfaceInteraction constructor takes parameters that set all of these values. It computes the normal as the cross product of the partial derivatives.

<<SurfaceInteraction Method Definitions>>= 
SurfaceInteraction::SurfaceInteraction(const Point3f &p, const Vector3f &pError, const Point2f &uv, const Vector3f &wo, const Vector3f &dpdu, const Vector3f &dpdv, const Normal3f &dndu, const Normal3f &dndv, Float time, const Shape *shape) : Interaction(p, Normal3f(Normalize(Cross(dpdu, dpdv))), pError, wo, time, nullptr), uv(uv), dpdu(dpdu), dpdv(dpdv), dndu(dndu), dndv(dndv), shape(shape) { <<Initialize shading geometry from true geometry>> 
shading.n = n; shading.dpdu = dpdu; shading.dpdv = dpdv; shading.dndu = dndu; shading.dndv = dndv;
<<Adjust normal based on orientation and handedness>>  }

SurfaceInteraction stores a second instance of a surface normal and the various partial derivatives to represent possibly perturbed values of these quantities as can be generated by bump mapping or interpolated per-vertex normals with triangles. Some parts of the system use this shading geometry, while others need to work with the original quantities.

<<SurfaceInteraction Public Data>>+=  
struct { Normal3f n; Vector3f dpdu, dpdv; Normal3f dndu, dndv; } shading;

The shading geometry values are initialized in the constructor to match the original surface geometry. If shading geometry is present, it generally isn’t computed until some time after the SurfaceInteraction constructor runs. The SetShadingGeometry() method, to be defined shortly, updates the shading geometry.

<<Initialize shading geometry from true geometry>>= 
shading.n = n; shading.dpdu = dpdu; shading.dpdv = dpdv; shading.dndu = dndu; shading.dndv = dndv;

The surface normal has special meaning to pbrt, which assumes that, for closed shapes, the normal is oriented such that it points to the outside of the shape. For geometry used as an area light source, light is emitted from only the side of the surface that the normal points toward; the other side is black. Because normals have this special meaning, pbrt provides a mechanism for the user to reverse the orientation of the normal, flipping it to point in the opposite direction. The ReverseOrientation directive in pbrt’s input file flips the normal to point in the opposite, non-default direction. Therefore, it is necessary to check if the given Shape has the corresponding flag set and, if so, switch the normal’s direction here.

However, one other factor plays into the orientation of the normal and must be accounted for here as well. If the Shape’s transformation matrix has switched the handedness of the object coordinate system from pbrt’s default left-handed coordinate system to a right-handed one, we need to switch the orientation of the normal as well. To see why this is so, consider a scale matrix bold upper S left-parenthesis 1 comma 1 comma negative 1 right-parenthesis . We would naturally expect this scale to switch the direction of the normal, although because we have computed the normal by bold n Subscript Baseline equals partial-differential normal p slash partial-differential u times partial-differential normal p slash partial-differential v ,

StartLayout 1st Row 1st Column bold upper S left-parenthesis 1 comma 1 comma negative 1 right-parenthesis StartFraction partial-differential normal p Over partial-differential u EndFraction times bold upper S left-parenthesis 1 comma 1 comma negative 1 right-parenthesis StartFraction partial-differential normal p Over partial-differential v EndFraction 2nd Column equals bold upper S left-parenthesis negative 1 comma negative 1 comma 1 right-parenthesis StartFraction partial-differential normal p Over partial-differential u EndFraction times StartFraction partial-differential normal p Over partial-differential v EndFraction 2nd Row 1st Column Blank 2nd Column equals bold upper S left-parenthesis negative 1 comma negative 1 comma 1 right-parenthesis bold n Subscript Baseline 3rd Row 1st Column Blank 2nd Column not-equals bold upper S left-parenthesis 1 comma 1 comma negative 1 right-parenthesis bold n Subscript Baseline period EndLayout

Therefore, it is also necessary to flip the normal’s direction if the transformation switches the handedness of the coordinate system, since the flip won’t be accounted for by the computation of the normal’s direction using the cross product.

The normal’s direction is swapped if one but not both of these two conditions is met; if both were met, their effect would cancel out. The exclusive-OR operation tests this condition.

<<Adjust normal based on orientation and handedness>>= 

When a shading coordinate frame is computed, the SurfaceInteraction is updated via its SetShadingGeometry() method.

<<SurfaceInteraction Method Definitions>>+=  
void SurfaceInteraction::SetShadingGeometry(const Vector3f &dpdus, const Vector3f &dpdvs, const Normal3f &dndus, const Normal3f &dndvs, bool orientationIsAuthoritative) { <<Compute shading.n for SurfaceInteraction>> 
shading.n = Normalize((Normal3f)Cross(dpdus, dpdvs)); if (shape && (shape->reverseOrientation ^ shape->transformSwapsHandedness)) shading.n = -shading.n; if (orientationIsAuthoritative) n = Faceforward(n, shading.n); else shading.n = Faceforward(shading.n, n);
<<Initialize shading partial derivative values>> 
shading.dpdu = dpdus; shading.dpdv = dpdvs; shading.dndu = dndus; shading.dndv = dndvs;
}

After performing the same cross product (and possibly flipping the orientation of the normal) as before to compute an initial shading normal, the implementation then flips either the shading normal or the true geometric normal if needed so that the two normals lie in the same hemisphere. Since the shading normal generally represents a relatively small perturbation of the geometric normal, the two of them should always be in the same hemisphere. Depending on the context, either the geometric normal or the shading normal may more authoritatively point toward the correct “outside” of the surface, so the caller passes a Boolean value that determines which should be flipped if needed.

<<Compute shading.n for SurfaceInteraction>>= 
shading.n = Normalize((Normal3f)Cross(dpdus, dpdvs)); if (shape && (shape->reverseOrientation ^ shape->transformSwapsHandedness)) shading.n = -shading.n; if (orientationIsAuthoritative) n = Faceforward(n, shading.n); else shading.n = Faceforward(shading.n, n);

<<Initialize shading partial derivative values>>= 
shading.dpdu = dpdus; shading.dpdv = dpdvs; shading.dndu = dndus; shading.dndv = dndvs;

We’ll add a method to Transform to transform SurfaceInteractions. Most members are either transformed directly or copied, as appropriate, but given the approach that pbrt uses for bounding floating-point error in computed intersection points, transforming the p and pError member variables requires special care. The fragment that handles this, <<Transform p and pError in SurfaceInteraction>> is defined in Section 3.9, when floating-point rounding error is discussed.

<<Transform Method Definitions>>+=  
SurfaceInteraction Transform::operator()(const SurfaceInteraction &si) const { SurfaceInteraction ret; <<Transform p and pError in SurfaceInteraction>> 
ret.p = (*this)(si.p, si.pError, &ret.pError);
<<Transform remaining members of SurfaceInteraction>> 
const Transform &t = *this; ret.n = Normalize(t(si.n)); ret.wo = t(si.wo); ret.time = si.time; ret.mediumInterface = si.mediumInterface; ret.uv = si.uv; ret.shape = si.shape; ret.dpdu = t(si.dpdu); ret.dpdv = t(si.dpdv); ret.dndu = t(si.dndu); ret.dndv = t(si.dndv); ret.shading.n = Normalize(t(si.shading.n)); ret.shading.dpdu = t(si.shading.dpdu); ret.shading.dpdv = t(si.shading.dpdv); ret.shading.dndu = t(si.shading.dndu); ret.shading.dndv = t(si.shading.dndv); ret.dudx = si.dudx; ret.dvdx = si.dvdx; ret.dudy = si.dudy; ret.dvdy = si.dvdy; ret.dpdx = t(si.dpdx); ret.dpdy = t(si.dpdy); ret.bsdf = si.bsdf; ret.bssrdf = si.bssrdf; ret.primitive = si.primitive; // ret.n = Faceforward(ret.n, ret.shading.n); ret.shading.n = Faceforward(ret.shading.n, ret.n);
return ret; }