6.2 Spheres

Spheres are a special case of a general type of surface called quadrics—surfaces described by quadratic polynomials in x , y , and  z . They offer a good starting point for introducing ray intersection algorithms. In conjunction with a transformation matrix, pbrt’s Sphere shape can also take the form of an ellipsoid. pbrt supports two other basic types of quadrics: cylinders and disks. Other quadrics such as the cone, hyperboloid, and paraboloid are less useful for most rendering applications, and so are not included in the system.

Many surfaces can be described in one of two main ways: in implicit form and in parametric form. An implicit function describes a 3D surface as

f left-parenthesis x comma y comma z right-parenthesis equals 0 period

The set of all points ( x , y , z ) that fulfill this condition defines the surface. For a unit sphere at the origin, the familiar implicit equation is x squared plus y squared plus z squared minus 1 equals 0 . Only the set of points one unit from the origin satisfies this constraint, giving the unit sphere’s surface.

Many surfaces can also be described parametrically using a function to map 2D points to 3D points on the surface. For example, a sphere of radius r can be described as a function of 2D spherical coordinates left-parenthesis theta comma phi right-parenthesis , where theta ranges from 0 to pi and phi ranges from 0 to 2 pi (Figure 6.3):

StartLayout 1st Row 1st Column x 2nd Column equals r sine theta cosine phi 2nd Row 1st Column y 2nd Column equals r sine theta sine phi 3rd Row 1st Column z 2nd Column equals r cosine theta period EndLayout
(6.1)

Figure 6.3: Basic Setting for the Sphere Shape. It has a radius of r and is centered at the object space origin. A partial sphere may be described by specifying a maximum phi value.

We can transform this function f left-parenthesis theta comma phi right-parenthesis into a function f left-parenthesis u comma v right-parenthesis over left-bracket 0 comma 1 right-bracket squared and generalize it slightly to allow partial spheres that only sweep out theta element-of left-bracket theta Subscript normal m normal i normal n Baseline comma theta Subscript normal m normal a normal x Baseline right-bracket and phi element-of left-bracket 0 comma phi Subscript normal m normal a normal x Baseline right-bracket with the substitution

StartLayout 1st Row 1st Column phi 2nd Column equals u phi Subscript normal m normal a normal x Baseline 2nd Row 1st Column theta 2nd Column equals theta Subscript normal m normal i normal n Baseline plus v left-parenthesis theta Subscript normal m normal a normal x Baseline minus theta Subscript normal m normal i normal n Baseline right-parenthesis period EndLayout
(6.2)

This form is particularly useful for texture mapping, where it can be directly used to map a texture defined over left-bracket 0 comma 1 right-bracket squared to the sphere. Figure 6.4 shows an image of two spheres; a grid image map has been used to show the left-parenthesis u comma v right-parenthesis parameterization.

Figure 6.4: Two Spheres. On the left is a partial sphere (with z Subscript normal m normal a normal x Baseline less-than r and phi Subscript normal m normal a normal x Baseline less-than 2 pi ) and on the right is a complete sphere. Note that the texture image used shows the left-parenthesis u comma v right-parenthesis parameterization of the shape; the singularity at one of the poles is visible in the complete sphere.

As we describe the implementation of the sphere shape, we will make use of both the implicit and parametric descriptions of the shape, depending on which is a more natural way to approach the particular problem we are facing.

The Sphere class represents a sphere that is centered at the origin. As with all the other shapes, its implementation is in the files shapes.h and shapes.cpp.

<<Sphere Definition>>= 
class Sphere { public: <<Sphere Public Methods>> 
static Sphere *Create(const Transform *renderFromObject, const Transform *objectFromRender, bool reverseOrientation, const ParameterDictionary &parameters, const FileLoc *loc, Allocator alloc); std::string ToString() const; Sphere(const Transform *renderFromObject, const Transform *objectFromRender, bool reverseOrientation, Float radius, Float zMin, Float zMax, Float phiMax) : renderFromObject(renderFromObject), objectFromRender(objectFromRender), reverseOrientation(reverseOrientation), transformSwapsHandedness(renderFromObject->SwapsHandedness()), radius(radius), zMin(Clamp(std::min(zMin, zMax), -radius, radius)), zMax(Clamp(std::max(zMin, zMax), -radius, radius)), thetaZMin(std::acos(Clamp(std::min(zMin, zMax) / radius, -1, 1))), thetaZMax(std::acos(Clamp(std::max(zMin, zMax) / radius, -1, 1))), phiMax(Radians(Clamp(phiMax, 0, 360))) {} PBRT_CPU_GPU Bounds3f Bounds() const; DirectionCone NormalBounds() const { return DirectionCone::EntireSphere(); } pstd::optional<ShapeIntersection> Intersect(const Ray &ray, Float tMax = Infinity) const { pstd::optional<QuadricIntersection> isect = BasicIntersect(ray, tMax); if (!isect) return {}; SurfaceInteraction intr = InteractionFromIntersection(*isect, -ray.d, ray.time); return ShapeIntersection{intr, isect->tHit}; } pstd::optional<QuadricIntersection> BasicIntersect(const Ray &r, Float tMax) const { Float phi; Point3f pHit; <<Transform Ray origin and direction to object space>>  <<Solve quadratic equation to compute sphere t0 and t1>> 
Interval t0, t1; <<Compute sphere quadratic coefficients>> 
Interval a = Sqr(di.x) + Sqr(di.y) + Sqr(di.z); Interval b = 2 * (di.x * oi.x + di.y * oi.y + di.z * oi.z); Interval c = Sqr(oi.x) + Sqr(oi.y) + Sqr(oi.z) - Sqr(Interval(radius));
<<Compute sphere quadratic discriminant discrim>> 
Vector3fi v(oi - b / (2 * a) * di); Interval length = Length(v); Interval discrim = 4 * a * (Interval(radius) + length) * (Interval(radius) - length); if (discrim.LowerBound() < 0) return {};
<<Compute quadratic t values>> 
Interval rootDiscrim = Sqrt(discrim); Interval q; if ((Float)b < 0) q = -.5f * (b - rootDiscrim); else q = -.5f * (b + rootDiscrim); t0 = q / a; t1 = c / q; <<Swap quadratic t values so that t0 is the lesser>> 
if (t0.LowerBound() > t1.LowerBound()) pstd::swap(t0, t1);
<<Check quadric shape t0 and t1 for nearest intersection>> 
if (t0.UpperBound() > tMax || t1.LowerBound() <= 0) return {}; Interval tShapeHit = t0; if (tShapeHit.LowerBound() <= 0) { tShapeHit = t1; if (tShapeHit.UpperBound() > tMax) return {}; }
<<Compute sphere hit position and phi >> 
pHit = Point3f(oi) + (Float)tShapeHit * Vector3f(di); <<Refine sphere intersection point>> 
pHit *= radius / Distance(pHit, Point3f(0, 0, 0));
if (pHit.x == 0 && pHit.y == 0) pHit.x = 1e-5f * radius; phi = std::atan2(pHit.y, pHit.x); if (phi < 0) phi += 2 * Pi;
<<Test sphere intersection against clipping parameters>> 
if ((zMin > -radius && pHit.z < zMin) || (zMax < radius && pHit.z > zMax) || phi > phiMax) { if (tShapeHit == t1) return {}; if (t1.UpperBound() > tMax) return {}; tShapeHit = t1; <<Compute sphere hit position and phi >> 
pHit = Point3f(oi) + (Float)tShapeHit * Vector3f(di); <<Refine sphere intersection point>> 
pHit *= radius / Distance(pHit, Point3f(0, 0, 0));
if (pHit.x == 0 && pHit.y == 0) pHit.x = 1e-5f * radius; phi = std::atan2(pHit.y, pHit.x); if (phi < 0) phi += 2 * Pi;
if ((zMin > -radius && pHit.z < zMin) || (zMax < radius && pHit.z > zMax) || phi > phiMax) return {}; }
<<Return QuadricIntersection for sphere intersection>> 
return QuadricIntersection{Float(tShapeHit), pHit, phi};
} bool IntersectP(const Ray &r, Float tMax = Infinity) const { return BasicIntersect(r, tMax).has_value(); } SurfaceInteraction InteractionFromIntersection( const QuadricIntersection &isect, Vector3f wo, Float time) const { Point3f pHit = isect.pObj; Float phi = isect.phi; <<Find parametric representation of sphere hit>> 
Float u = phi / phiMax; Float cosTheta = pHit.z / radius; Float theta = SafeACos(cosTheta); Float v = (theta - thetaZMin) / (thetaZMax - thetaZMin); <<Compute sphere partial-differential normal p slash partial-differential u and partial-differential normal p slash partial-differential v >> 
Float zRadius = std::sqrt(Sqr(pHit.x) + Sqr(pHit.y)); Float cosPhi = pHit.x / zRadius, sinPhi = pHit.y / zRadius; Vector3f dpdu(-phiMax * pHit.y, phiMax * pHit.x, 0); Float sinTheta = SafeSqrt(1 - Sqr(cosTheta)); Vector3f dpdv = (thetaZMax - thetaZMin) * Vector3f(pHit.z * cosPhi, pHit.z * sinPhi, -radius * sinTheta);
<<Compute sphere partial-differential bold n Subscript slash partial-differential u and partial-differential bold n Subscript slash partial-differential v >> 
Vector3f d2Pduu = -phiMax * phiMax * Vector3f(pHit.x, pHit.y, 0); Vector3f d2Pduv = (thetaZMax - thetaZMin) * pHit.z * phiMax * Vector3f(-sinPhi, cosPhi, 0.); Vector3f d2Pdvv = -Sqr(thetaZMax - thetaZMin) * Vector3f(pHit.x,pHit.y,pHit.z); <<Compute coefficients for fundamental forms>> 
Float E = Dot(dpdu, dpdu), F = Dot(dpdu, dpdv), G = Dot(dpdv, dpdv); Vector3f n = Normalize(Cross(dpdu, dpdv)); Float e = Dot(n, d2Pduu), f = Dot(n, d2Pduv), g = Dot(n, d2Pdvv);
<<Compute partial-differential bold n Subscript slash partial-differential u and partial-differential bold n Subscript slash partial-differential v from fundamental form coefficients>> 
Float EGF2 = DifferenceOfProducts(E, G, F, F); Float invEGF2 = (EGF2 == 0) ? Float(0) : 1 / EGF2; Normal3f dndu = Normal3f((f * F - e * G) * invEGF2 * dpdu + (e * F - f * E) * invEGF2 * dpdv); Normal3f dndv = Normal3f((g * F - f * G) * invEGF2 * dpdu + (f * F - g * E) * invEGF2 * dpdv);
<<Compute error bounds for sphere intersection>> 
Vector3f pError = gamma(5) * Abs((Vector3f)pHit);
<<Return SurfaceInteraction for quadric intersection>> 
bool flipNormal = reverseOrientation ^ transformSwapsHandedness; Vector3f woObject = (*objectFromRender)(wo); return (*renderFromObject)( SurfaceInteraction(Point3fi(pHit, pError), Point2f(u, v), woObject, dpdu, dpdv, dndu, dndv, time, flipNormal));
} Float Area() const { return phiMax * radius * (zMax - zMin); } PBRT_CPU_GPU pstd::optional<ShapeSample> Sample(Point2f u) const; Float PDF(const Interaction &) const { return 1 / Area(); } pstd::optional<ShapeSample> Sample(const ShapeSampleContext &ctx, Point2f u) const { <<Sample uniformly on sphere if normal p Subscript is inside it>> 
Point3f pCenter = (*renderFromObject)(Point3f(0, 0, 0)); Point3f pOrigin = ctx.OffsetRayOrigin(pCenter); if (DistanceSquared(pOrigin, pCenter) <= Sqr(radius)) { <<Sample shape by area and compute incident direction wi>> 
pstd::optional<ShapeSample> ss = Sample(u); ss->intr.time = ctx.time; Vector3f wi = ss->intr.p() - ctx.p(); if (LengthSquared(wi) == 0) return {}; wi = Normalize(wi);
<<Convert area sampling PDF in ss to solid angle measure>> 
ss->pdf /= AbsDot(ss->intr.n, -wi) / DistanceSquared(ctx.p(), ss->intr.p()); if (IsInf(ss->pdf)) return {};
return ss; }
<<Sample sphere uniformly inside subtended cone>> 
<<Compute quantities related to the theta Subscript normal m normal a normal x for cone>> 
Float sinThetaMax = radius / Distance(ctx.p(), pCenter); Float sin2ThetaMax = Sqr(sinThetaMax); Float cosThetaMax = SafeSqrt(1 - sin2ThetaMax); Float oneMinusCosThetaMax = 1 - cosThetaMax;
<<Compute theta and phi values for sample in cone>> 
Float cosTheta = (cosThetaMax - 1) * u[0] + 1; Float sin2Theta = 1 - Sqr(cosTheta); if (sin2ThetaMax < 0.00068523f /* sin^2(1.5 deg) */) { <<Compute cone sample via Taylor series expansion for small angles>> 
sin2Theta = sin2ThetaMax * u[0]; cosTheta = std::sqrt(1 - sin2Theta); oneMinusCosThetaMax = sin2ThetaMax / 2;
}
<<Compute angle alpha from center of sphere to sampled point on surface>> 
Float cosAlpha = sin2Theta / sinThetaMax + cosTheta * SafeSqrt(1 - sin2Theta / Sqr(sinThetaMax)); Float sinAlpha = SafeSqrt(1 - Sqr(cosAlpha));
<<Compute surface normal and sampled point on sphere>> 
Float phi = u[1] * 2 * Pi; Vector3f w = SphericalDirection(sinAlpha, cosAlpha, phi); Frame samplingFrame = Frame::FromZ(Normalize(pCenter - ctx.p())); Normal3f n(samplingFrame.FromLocal(-w)); Point3f p = pCenter + radius * Point3f(n.x, n.y, n.z); if (reverseOrientation) n *= -1;
<<Return ShapeSample for sampled point on sphere>> 
<<Compute pError for sampled point on sphere>> 
Vector3f pError = gamma(5) * Abs((Vector3f)p);
<<Compute left-parenthesis u comma v right-parenthesis coordinates for sampled point on sphere>> 
Point3f pObj = (*objectFromRender)(p); Float theta = SafeACos(pObj.z / radius); Float spherePhi = std::atan2(pObj.y, pObj.x); if (spherePhi < 0) spherePhi += 2 * Pi; Point2f uv(spherePhi / phiMax, (theta - thetaZMin) / (thetaZMax - thetaZMin));
return ShapeSample{Interaction(Point3fi(p, pError), n, ctx.time, uv), 1 / (2 * Pi * oneMinusCosThetaMax)};
} Float PDF(const ShapeSampleContext &ctx, Vector3f wi) const { Point3f pCenter = (*renderFromObject)(Point3f(0, 0, 0)); Point3f pOrigin = ctx.OffsetRayOrigin(pCenter); if (DistanceSquared(pOrigin, pCenter) <= Sqr(radius)) { <<Return solid angle PDF for point inside sphere>> 
<<Intersect sample ray with shape geometry>> 
Ray ray = ctx.SpawnRay(wi); pstd::optional<ShapeIntersection> isect = Intersect(ray); if (!isect) return 0;
<<Compute PDF in solid angle measure from shape intersection point>> 
Float pdf = (1 / Area()) / (AbsDot(isect->intr.n, -wi) / DistanceSquared(ctx.p(), isect->intr.p())); if (IsInf(pdf)) pdf = 0;
return pdf;
} <<Compute general solid angle sphere PDF>> 
Float sin2ThetaMax = radius * radius / DistanceSquared(ctx.p(), pCenter); Float cosThetaMax = SafeSqrt(1 - sin2ThetaMax); Float oneMinusCosThetaMax = 1 - cosThetaMax; <<Compute more accurate oneMinusCosThetaMax for small solid angle>> 
if (sin2ThetaMax < 0.00068523f /* sin^2(1.5 deg) */) oneMinusCosThetaMax = sin2ThetaMax / 2;
return 1 / (2 * Pi * oneMinusCosThetaMax);
}
private: <<Sphere Private Members>> 
Float radius; Float zMin, zMax; Float thetaZMin, thetaZMax, phiMax; const Transform *renderFromObject, *objectFromRender; bool reverseOrientation, transformSwapsHandedness;
};

As mentioned earlier, spheres in pbrt are defined in a coordinate system where the center of the sphere is at the origin. The sphere constructor is provided transformations that map between the sphere’s object space and rendering space.

Although pbrt supports animated transformation matrices, the transformations here are not AnimatedTransforms. (Such is also the case for all the shapes defined in this chapter.) Animated shape transformations are instead handled by using a TransformedPrimitive to represent the shape in the scene. Doing so allows us to centralize some of the tricky details related to animated transformations in a single place, rather than requiring all Shapes to handle this case.

The radius of the sphere can have an arbitrary positive value, and the sphere’s extent can be truncated in two different ways. First, minimum and maximum z values may be set; the parts of the sphere below and above these planes, respectively, are cut off. Second, considering the parameterization of the sphere in spherical coordinates, a maximum phi value can be set. The sphere sweeps out phi values from 0 to the given phi Subscript normal m normal a normal x such that the section of the sphere with spherical phi values above phi Subscript normal m normal a normal x is also removed.

Finally, the Sphere constructor also takes a Boolean parameter, reverseOrientation, that indicates whether their surface normal directions should be reversed from the default (which is pointing outside the sphere). This capability is useful because the orientation of the surface normal is used to determine which side of a shape is “outside.” For example, shapes that emit illumination are by default emissive only on the side the surface normal lies on. The value of this parameter is managed via the ReverseOrientation statement in pbrt scene description files.

<<Sphere Public Methods>>= 

<<Sphere Private Members>>= 
Float radius; Float zMin, zMax; Float thetaZMin, thetaZMax, phiMax; const Transform *renderFromObject, *objectFromRender; bool reverseOrientation, transformSwapsHandedness;

6.2.1 Bounding

Computing an object-space bounding box for a sphere is straightforward. The implementation here uses the values of z Subscript normal m normal i normal n and z Subscript normal m normal a normal x provided by the user to tighten up the bound when less than an entire sphere is being rendered. However, it does not do the extra work to compute a tighter bounding box when phi Subscript normal m normal a normal x is less than 3 pi slash 2 . This improvement is left as an exercise. This object-space bounding box is transformed to rendering space before being returned.

<<Sphere Method Definitions>>= 
Bounds3f Sphere::Bounds() const { return (*renderFromObject)( Bounds3f(Point3f(-radius, -radius, zMin), Point3f( radius, radius, zMax))); }

The Sphere’s NormalBounds() method does not consider any form of partial spheres but always returns the bounds for an entire sphere, which is all possible directions.

<<Sphere Public Methods>>+=  
DirectionCone NormalBounds() const { return DirectionCone::EntireSphere(); }

6.2.2 Intersection Tests

The ray intersection test is broken into two stages. First, BasicIntersect() does the basic ray–sphere intersection test and returns a small structure, QuadricIntersection, if an intersection is found. A subsequent call to the InteractionFromIntersection() method transforms the QuadricIntersection into a full-blown SurfaceInteraction, which can be returned from the Intersection() method.

There are two motivations for separating the intersection test into two stages like this. One is that doing so allows the IntersectP() method to be implemented as a trivial wrapper around BasicIntersect(). The second is that pbrt’s GPU rendering path is organized such that the closest intersection among all shapes is found before the full SurfaceInteraction is constructed; this decomposition fits that directly.

<<Sphere Public Methods>>+=  
pstd::optional<ShapeIntersection> Intersect(const Ray &ray, Float tMax = Infinity) const { pstd::optional<QuadricIntersection> isect = BasicIntersect(ray, tMax); if (!isect) return {}; SurfaceInteraction intr = InteractionFromIntersection(*isect, -ray.d, ray.time); return ShapeIntersection{intr, isect->tHit}; }

QuadricIntersection stores the parametric t along the ray where the intersection occurred, the object-space intersection point, and the sphere’s phi value there. As its name suggests, this structure will be used by the other quadrics in the same way it is here.

<<QuadricIntersection Definition>>= 
struct QuadricIntersection { Float tHit; Point3f pObj; Float phi; };

The basic intersection test transforms the provided rendering-space ray to object space and intersects it with the complete sphere. If a partial sphere has been specified, some additional tests reject intersections with portions of the sphere that have been removed.

<<Sphere Public Methods>>+=  
pstd::optional<QuadricIntersection> BasicIntersect(const Ray &r, Float tMax) const { Float phi; Point3f pHit; <<Transform Ray origin and direction to object space>>  <<Solve quadratic equation to compute sphere t0 and t1>> 
Interval t0, t1; <<Compute sphere quadratic coefficients>> 
Interval a = Sqr(di.x) + Sqr(di.y) + Sqr(di.z); Interval b = 2 * (di.x * oi.x + di.y * oi.y + di.z * oi.z); Interval c = Sqr(oi.x) + Sqr(oi.y) + Sqr(oi.z) - Sqr(Interval(radius));
<<Compute sphere quadratic discriminant discrim>> 
Vector3fi v(oi - b / (2 * a) * di); Interval length = Length(v); Interval discrim = 4 * a * (Interval(radius) + length) * (Interval(radius) - length); if (discrim.LowerBound() < 0) return {};
<<Compute quadratic t values>> 
Interval rootDiscrim = Sqrt(discrim); Interval q; if ((Float)b < 0) q = -.5f * (b - rootDiscrim); else q = -.5f * (b + rootDiscrim); t0 = q / a; t1 = c / q; <<Swap quadratic t values so that t0 is the lesser>> 
if (t0.LowerBound() > t1.LowerBound()) pstd::swap(t0, t1);
<<Check quadric shape t0 and t1 for nearest intersection>> 
if (t0.UpperBound() > tMax || t1.LowerBound() <= 0) return {}; Interval tShapeHit = t0; if (tShapeHit.LowerBound() <= 0) { tShapeHit = t1; if (tShapeHit.UpperBound() > tMax) return {}; }
<<Compute sphere hit position and phi >> 
pHit = Point3f(oi) + (Float)tShapeHit * Vector3f(di); <<Refine sphere intersection point>> 
pHit *= radius / Distance(pHit, Point3f(0, 0, 0));
if (pHit.x == 0 && pHit.y == 0) pHit.x = 1e-5f * radius; phi = std::atan2(pHit.y, pHit.x); if (phi < 0) phi += 2 * Pi;
<<Test sphere intersection against clipping parameters>> 
if ((zMin > -radius && pHit.z < zMin) || (zMax < radius && pHit.z > zMax) || phi > phiMax) { if (tShapeHit == t1) return {}; if (t1.UpperBound() > tMax) return {}; tShapeHit = t1; <<Compute sphere hit position and phi >> 
pHit = Point3f(oi) + (Float)tShapeHit * Vector3f(di); <<Refine sphere intersection point>> 
pHit *= radius / Distance(pHit, Point3f(0, 0, 0));
if (pHit.x == 0 && pHit.y == 0) pHit.x = 1e-5f * radius; phi = std::atan2(pHit.y, pHit.x); if (phi < 0) phi += 2 * Pi;
if ((zMin > -radius && pHit.z < zMin) || (zMax < radius && pHit.z > zMax) || phi > phiMax) return {}; }
<<Return QuadricIntersection for sphere intersection>> 
return QuadricIntersection{Float(tShapeHit), pHit, phi};
}

The transformed ray origin and direction are respectively stored using Point3fi and Vector3fi classes rather than the usual Point3f and Vector3f. These classes represent those quantities as small intervals in each dimension that bound the floating-point round-off error that was introduced by applying the transformation. Later, we will see that these error bounds will be useful for improving the geometric accuracy of the intersection computation. For the most part, these classes can respectively be used just like Point3f and Vector3f.

<<Transform Ray origin and direction to object space>>= 

If a sphere is centered at the origin with radius r , its implicit representation is

x squared plus y squared plus z squared minus r squared equals 0 period

By substituting the parametric representation of the ray from Equation (3.4) into the implicit sphere equation, we have

left-parenthesis normal o Subscript x Baseline plus t bold d Subscript x Baseline right-parenthesis squared plus left-parenthesis normal o Subscript y Baseline plus t bold d Subscript y Baseline right-parenthesis squared plus left-parenthesis normal o Subscript z Baseline plus t bold d Subscript z Baseline right-parenthesis squared equals r squared period

Note that all elements of this equation besides t are known values. The t values where the equation holds give the parametric positions along the ray where the implicit sphere equation is satisfied and thus the points along the ray where it intersects the sphere. We can expand this equation and gather the coefficients for a general quadratic equation in  t ,

a t squared plus b t plus c equals 0 comma

where

StartLayout 1st Row 1st Column a 2nd Column equals bold d Subscript x Superscript 2 Baseline plus bold d Subscript y Superscript 2 Baseline plus bold d Subscript z Superscript 2 Baseline 2nd Row 1st Column b 2nd Column equals 2 left-parenthesis bold d Subscript x Baseline normal o Subscript x Baseline plus bold d Subscript y Baseline normal o Subscript y Baseline plus bold d Subscript z Baseline normal o Subscript z Baseline right-parenthesis 3rd Row 1st Column c 2nd Column equals normal o Subscript x Superscript 2 Baseline plus normal o Subscript y Superscript 2 Baseline plus normal o Subscript z Superscript 2 Baseline minus r squared period EndLayout
(6.3)

The Interval class stores a small range of floating-point values to maintain bounds on floating-point rounding error. It is defined in Section B.2.15 and is analogous to Float in the way that Point3fi is to Point3f, for example.

<<Solve quadratic equation to compute sphere t0 and t1>>= 
Interval t0, t1; <<Compute sphere quadratic coefficients>> 
Interval a = Sqr(di.x) + Sqr(di.y) + Sqr(di.z); Interval b = 2 * (di.x * oi.x + di.y * oi.y + di.z * oi.z); Interval c = Sqr(oi.x) + Sqr(oi.y) + Sqr(oi.z) - Sqr(Interval(radius));
<<Compute sphere quadratic discriminant discrim>> 
Vector3fi v(oi - b / (2 * a) * di); Interval length = Length(v); Interval discrim = 4 * a * (Interval(radius) + length) * (Interval(radius) - length); if (discrim.LowerBound() < 0) return {};
<<Compute quadratic t values>> 
Interval rootDiscrim = Sqrt(discrim); Interval q; if ((Float)b < 0) q = -.5f * (b - rootDiscrim); else q = -.5f * (b + rootDiscrim); t0 = q / a; t1 = c / q; <<Swap quadratic t values so that t0 is the lesser>> 
if (t0.LowerBound() > t1.LowerBound()) pstd::swap(t0, t1);

Given Interval, Equation (6.3) directly translates to the following fragment of source code.

<<Compute sphere quadratic coefficients>>= 
Interval a = Sqr(di.x) + Sqr(di.y) + Sqr(di.z); Interval b = 2 * (di.x * oi.x + di.y * oi.y + di.z * oi.z); Interval c = Sqr(oi.x) + Sqr(oi.y) + Sqr(oi.z) - Sqr(Interval(radius));

The fragment <<Compute sphere quadratic discriminant discrim>> computes the discriminant b squared minus 4 a c in a way that maintains numerical accuracy in tricky cases. It is defined later, in Section 6.8.3, after related topics about floating-point arithmetic have been introduced. Proceeding here with its value in discrim, the quadratic equation can be applied. Here we use a variant of the traditional left-parenthesis negative b plus-or-minus StartRoot b squared minus 4 a c EndRoot right-parenthesis slash left-parenthesis 2 a right-parenthesis approach that gives more accuracy; it is described in Section B.2.10.

<<Compute quadratic t values>>= 
Interval rootDiscrim = Sqrt(discrim); Interval q; if ((Float)b < 0) q = -.5f * (b - rootDiscrim); else q = -.5f * (b + rootDiscrim); t0 = q / a; t1 = c / q; <<Swap quadratic t values so that t0 is the lesser>> 
if (t0.LowerBound() > t1.LowerBound()) pstd::swap(t0, t1);

Because t0 and t1 represent intervals of floating-point values, it may be ambiguous which of them is less than the other. We use their lower bound for this test, so that in ambiguous cases, at least we do not risk returning a hit that is potentially farther away than an actual closer hit.

<<Swap quadratic t values so that t0 is the lesser>>= 
if (t0.LowerBound() > t1.LowerBound()) pstd::swap(t0, t1);

A similar ambiguity must be accounted for when testing the t values against the acceptable range. In ambiguous cases, we err on the side of returning no intersection rather than an invalid one. The closest valid t is then stored in tShapeHit.

<<Check quadric shape t0 and t1 for nearest intersection>>= 
if (t0.UpperBound() > tMax || t1.LowerBound() <= 0) return {}; Interval tShapeHit = t0; if (tShapeHit.LowerBound() <= 0) { tShapeHit = t1; if (tShapeHit.UpperBound() > tMax) return {}; }

Given the parametric distance along the ray to the intersection with a full sphere, the intersection point pHit can be computed as that offset along the ray. In its initializer, all the respective interval types are cast back to their non-interval equivalents, which gives their midpoint. (The remainder of the intersection test no longer needs the information provided by the intervals.) Due to floating-point precision limitations, this computed intersection point pHit may lie a bit to one side of the actual sphere surface; the <<Refine sphere intersection point>> fragment, which is defined in Section 6.8.5, improves the accuracy of this value.

It is next necessary to handle partial spheres with clipped z or phi ranges—intersections that are in clipped areas must be ignored. The implementation starts by computing the phi value for the hit point. Using the parametric representation of the sphere,

StartFraction y Over x EndFraction equals StartFraction r sine theta sine phi Over r sine theta cosine phi EndFraction equals tangent phi comma

so phi equals arc tangent y slash x . It is necessary to remap the result of the standard library’s std::atan() function to a value between 0 and 2 pi , to match the sphere’s original definition.

<<Compute sphere hit position and phi >>= 
pHit = Point3f(oi) + (Float)tShapeHit * Vector3f(di); <<Refine sphere intersection point>> 
pHit *= radius / Distance(pHit, Point3f(0, 0, 0));
if (pHit.x == 0 && pHit.y == 0) pHit.x = 1e-5f * radius; phi = std::atan2(pHit.y, pHit.x); if (phi < 0) phi += 2 * Pi;

The hit point can now be tested against the specified minima and maxima for z and phi . One subtlety is that it is important to skip the z tests if the z range includes the entire sphere; the computed pHit.z value may be slightly out of the z range due to floating-point round-off, so we should only perform this test when the user expects the sphere to be partially incomplete. If the t 0 intersection is not valid, the routine tries again with t 1 .

<<Test sphere intersection against clipping parameters>>= 
if ((zMin > -radius && pHit.z < zMin) || (zMax < radius && pHit.z > zMax) || phi > phiMax) { if (tShapeHit == t1) return {}; if (t1.UpperBound() > tMax) return {}; tShapeHit = t1; <<Compute sphere hit position and phi >> 
pHit = Point3f(oi) + (Float)tShapeHit * Vector3f(di); <<Refine sphere intersection point>> 
pHit *= radius / Distance(pHit, Point3f(0, 0, 0));
if (pHit.x == 0 && pHit.y == 0) pHit.x = 1e-5f * radius; phi = std::atan2(pHit.y, pHit.x); if (phi < 0) phi += 2 * Pi;
if ((zMin > -radius && pHit.z < zMin) || (zMax < radius && pHit.z > zMax) || phi > phiMax) return {}; }

At this point in the routine, it is certain that the ray hits the sphere. A QuadricIntersection is returned that encapsulates sufficient information about it to determine the rest of the geometric information at the intersection point. Recall from Section 6.1.4 that even though tShapeHit was computed in object space, it is also the correct t value in rendering space. Therefore, it can be returned directly.

<<Return QuadricIntersection for sphere intersection>>= 
return QuadricIntersection{Float(tShapeHit), pHit, phi};

With BasicIntersect() implemented, Sphere::IntersectP() is easily taken care of.

<<Sphere Public Methods>>+=  
bool IntersectP(const Ray &r, Float tMax = Infinity) const { return BasicIntersect(r, tMax).has_value(); }

A QuadricIntersection can be upgraded to a SurfaceInteraction with a call to InteractionFromIntersection().

<<Sphere Public Methods>>+=  
SurfaceInteraction InteractionFromIntersection( const QuadricIntersection &isect, Vector3f wo, Float time) const { Point3f pHit = isect.pObj; Float phi = isect.phi; <<Find parametric representation of sphere hit>> 
Float u = phi / phiMax; Float cosTheta = pHit.z / radius; Float theta = SafeACos(cosTheta); Float v = (theta - thetaZMin) / (thetaZMax - thetaZMin); <<Compute sphere partial-differential normal p slash partial-differential u and partial-differential normal p slash partial-differential v >> 
Float zRadius = std::sqrt(Sqr(pHit.x) + Sqr(pHit.y)); Float cosPhi = pHit.x / zRadius, sinPhi = pHit.y / zRadius; Vector3f dpdu(-phiMax * pHit.y, phiMax * pHit.x, 0); Float sinTheta = SafeSqrt(1 - Sqr(cosTheta)); Vector3f dpdv = (thetaZMax - thetaZMin) * Vector3f(pHit.z * cosPhi, pHit.z * sinPhi, -radius * sinTheta);
<<Compute sphere partial-differential bold n Subscript slash partial-differential u and partial-differential bold n Subscript slash partial-differential v >> 
Vector3f d2Pduu = -phiMax * phiMax * Vector3f(pHit.x, pHit.y, 0); Vector3f d2Pduv = (thetaZMax - thetaZMin) * pHit.z * phiMax * Vector3f(-sinPhi, cosPhi, 0.); Vector3f d2Pdvv = -Sqr(thetaZMax - thetaZMin) * Vector3f(pHit.x,pHit.y,pHit.z); <<Compute coefficients for fundamental forms>> 
Float E = Dot(dpdu, dpdu), F = Dot(dpdu, dpdv), G = Dot(dpdv, dpdv); Vector3f n = Normalize(Cross(dpdu, dpdv)); Float e = Dot(n, d2Pduu), f = Dot(n, d2Pduv), g = Dot(n, d2Pdvv);
<<Compute partial-differential bold n Subscript slash partial-differential u and partial-differential bold n Subscript slash partial-differential v from fundamental form coefficients>> 
Float EGF2 = DifferenceOfProducts(E, G, F, F); Float invEGF2 = (EGF2 == 0) ? Float(0) : 1 / EGF2; Normal3f dndu = Normal3f((f * F - e * G) * invEGF2 * dpdu + (e * F - f * E) * invEGF2 * dpdv); Normal3f dndv = Normal3f((g * F - f * G) * invEGF2 * dpdu + (f * F - g * E) * invEGF2 * dpdv);
<<Compute error bounds for sphere intersection>> 
Vector3f pError = gamma(5) * Abs((Vector3f)pHit);
<<Return SurfaceInteraction for quadric intersection>> 
bool flipNormal = reverseOrientation ^ transformSwapsHandedness; Vector3f woObject = (*objectFromRender)(wo); return (*renderFromObject)( SurfaceInteraction(Point3fi(pHit, pError), Point2f(u, v), woObject, dpdu, dpdv, dndu, dndv, time, flipNormal));
}

The method first computes u and v values by scaling the previously computed phi value for the hit to lie between 0 and 1 and by computing a theta value between 0 and 1 for the hit point based on the range of theta values for the given sphere. Then it finds the parametric partial derivatives of position partial-differential normal p slash partial-differential u and partial-differential normal p slash partial-differential v and surface normal partial-differential bold n Subscript slash partial-differential u and partial-differential bold n Subscript slash partial-differential v .

<<Find parametric representation of sphere hit>>= 
Float u = phi / phiMax; Float cosTheta = pHit.z / radius; Float theta = SafeACos(cosTheta); Float v = (theta - thetaZMin) / (thetaZMax - thetaZMin); <<Compute sphere partial-differential normal p slash partial-differential u and partial-differential normal p slash partial-differential v >> 
Float zRadius = std::sqrt(Sqr(pHit.x) + Sqr(pHit.y)); Float cosPhi = pHit.x / zRadius, sinPhi = pHit.y / zRadius; Vector3f dpdu(-phiMax * pHit.y, phiMax * pHit.x, 0); Float sinTheta = SafeSqrt(1 - Sqr(cosTheta)); Vector3f dpdv = (thetaZMax - thetaZMin) * Vector3f(pHit.z * cosPhi, pHit.z * sinPhi, -radius * sinTheta);
<<Compute sphere partial-differential bold n Subscript slash partial-differential u and partial-differential bold n Subscript slash partial-differential v >> 
Vector3f d2Pduu = -phiMax * phiMax * Vector3f(pHit.x, pHit.y, 0); Vector3f d2Pduv = (thetaZMax - thetaZMin) * pHit.z * phiMax * Vector3f(-sinPhi, cosPhi, 0.); Vector3f d2Pdvv = -Sqr(thetaZMax - thetaZMin) * Vector3f(pHit.x,pHit.y,pHit.z); <<Compute coefficients for fundamental forms>> 
Float E = Dot(dpdu, dpdu), F = Dot(dpdu, dpdv), G = Dot(dpdv, dpdv); Vector3f n = Normalize(Cross(dpdu, dpdv)); Float e = Dot(n, d2Pduu), f = Dot(n, d2Pduv), g = Dot(n, d2Pdvv);
<<Compute partial-differential bold n Subscript slash partial-differential u and partial-differential bold n Subscript slash partial-differential v from fundamental form coefficients>> 
Float EGF2 = DifferenceOfProducts(E, G, F, F); Float invEGF2 = (EGF2 == 0) ? Float(0) : 1 / EGF2; Normal3f dndu = Normal3f((f * F - e * G) * invEGF2 * dpdu + (e * F - f * E) * invEGF2 * dpdv); Normal3f dndv = Normal3f((g * F - f * G) * invEGF2 * dpdu + (f * F - g * E) * invEGF2 * dpdv);

Computing the partial derivatives of a point on the sphere is a short exercise in algebra. Here we will show how the x component of partial-differential normal p slash partial-differential u , partial-differential normal p Subscript x slash partial-differential u , is calculated; the other components are found similarly. Using the parametric definition of the sphere, we have

StartLayout 1st Row 1st Column x 2nd Column equals r sine theta cosine phi 2nd Row 1st Column StartFraction partial-differential normal p Subscript Baseline Subscript x Baseline Over partial-differential u EndFraction 2nd Column equals StartFraction partial-differential Over partial-differential u EndFraction left-parenthesis r sine theta cosine phi right-parenthesis 3rd Row 1st Column Blank 2nd Column equals r sine theta StartFraction partial-differential Over partial-differential u EndFraction left-parenthesis cosine phi right-parenthesis 4th Row 1st Column Blank 2nd Column equals r sine theta left-parenthesis minus phi Subscript normal m normal a normal x Baseline sine phi right-parenthesis period EndLayout

Using a substitution based on the parametric definition of the sphere’s y coordinate, this simplifies to

StartFraction partial-differential normal p Subscript Baseline Subscript x Baseline Over partial-differential u EndFraction equals minus phi Subscript normal m normal a normal x Baseline y period

Similarly,

StartFraction partial-differential normal p Subscript Baseline Subscript y Baseline Over partial-differential u EndFraction equals phi Subscript normal m normal a normal x Baseline x comma

and

StartFraction partial-differential normal p Subscript Baseline Subscript z Baseline Over partial-differential u EndFraction equals 0 period

A similar process gives partial-differential normal p slash partial-differential v . The complete result is

StartLayout 1st Row 1st Column StartFraction partial-differential normal p Over partial-differential u EndFraction 2nd Column equals left-parenthesis minus phi Subscript normal m normal a normal x Baseline y comma phi Subscript normal m normal a normal x Baseline x comma 0 right-parenthesis 2nd Row 1st Column StartFraction partial-differential normal p Over partial-differential v EndFraction 2nd Column equals left-parenthesis theta Subscript normal m normal a normal x Baseline minus theta Subscript normal m normal i normal n Baseline right-parenthesis left-parenthesis z cosine phi comma z sine phi comma minus r sine theta right-parenthesis comma EndLayout

and the implementation follows directly.

<<Compute sphere partial-differential normal p slash partial-differential u and partial-differential normal p slash partial-differential v >>= 
Float zRadius = std::sqrt(Sqr(pHit.x) + Sqr(pHit.y)); Float cosPhi = pHit.x / zRadius, sinPhi = pHit.y / zRadius; Vector3f dpdu(-phiMax * pHit.y, phiMax * pHit.x, 0); Float sinTheta = SafeSqrt(1 - Sqr(cosTheta)); Vector3f dpdv = (thetaZMax - thetaZMin) * Vector3f(pHit.z * cosPhi, pHit.z * sinPhi, -radius * sinTheta);

It is also useful to determine how the normal changes as we move along the surface in the u and v directions. (For example, the antialiasing techniques in Chapter 10 use this information to antialias textures on objects that are seen reflected in curved surfaces.) The differential changes in normal partial-differential bold n Subscript slash partial-differential u and partial-differential bold n Subscript slash partial-differential v are given by the Weingarten equations from differential geometry:

StartLayout 1st Row 1st Column StartFraction partial-differential bold n Over partial-differential u EndFraction 2nd Column equals StartFraction f upper F minus e upper G Over upper E upper G minus upper F squared EndFraction StartFraction partial-differential normal p Over partial-differential u EndFraction plus StartFraction e upper F minus f upper E Over upper E upper G minus upper F squared EndFraction StartFraction partial-differential normal p Over partial-differential v EndFraction 2nd Row 1st Column StartFraction partial-differential bold n Over partial-differential v EndFraction 2nd Column equals StartFraction g upper F minus f upper G Over upper E upper G minus upper F squared EndFraction StartFraction partial-differential normal p Over partial-differential u EndFraction plus StartFraction f upper F minus g upper E Over upper E upper G minus upper F squared EndFraction StartFraction partial-differential normal p Over partial-differential v EndFraction comma EndLayout

where upper E , upper F , and upper G are coefficients of the first fundamental form and are given by

StartLayout 1st Row 1st Column upper E 2nd Column equals StartAbsoluteValue StartFraction partial-differential normal p Over partial-differential u EndFraction EndAbsoluteValue squared 2nd Row 1st Column upper F 2nd Column equals left-parenthesis StartFraction partial-differential normal p Over partial-differential u EndFraction dot StartFraction partial-differential normal p Over partial-differential v EndFraction right-parenthesis 3rd Row 1st Column upper G 2nd Column equals StartAbsoluteValue StartFraction partial-differential normal p Over partial-differential v EndFraction EndAbsoluteValue squared period EndLayout

These are easily computed with the partial-differential normal p slash partial-differential u and partial-differential normal p slash partial-differential v values found earlier. The e , f , and g are coefficients of the second fundamental form,

StartLayout 1st Row 1st Column e 2nd Column equals left-parenthesis bold n Subscript Baseline dot StartFraction partial-differential squared normal p Over partial-differential u squared EndFraction right-parenthesis 2nd Row 1st Column f 2nd Column equals left-parenthesis bold n Subscript Baseline dot StartFraction partial-differential squared normal p Over partial-differential u partial-differential v EndFraction right-parenthesis 3rd Row 1st Column g 2nd Column equals left-parenthesis bold n Subscript Baseline dot StartFraction partial-differential squared normal p Over partial-differential v squared EndFraction right-parenthesis period EndLayout

The two fundamental forms capture elementary metric properties of a surface, including notions of distance, angle, and curvature; see a differential geometry textbook such as Gray (1993) for details. To find e , f , and g , it is necessary to compute the second-order partial derivatives partial-differential squared normal p slash partial-differential u squared and so on.

For spheres, a little more algebra gives the second derivatives:

StartLayout 1st Row 1st Column StartFraction partial-differential squared normal p Over partial-differential u squared EndFraction 2nd Column equals minus phi Subscript normal m normal a normal x Superscript 2 Baseline left-parenthesis x comma y comma 0 right-parenthesis 2nd Row 1st Column StartFraction partial-differential squared normal p Over partial-differential u partial-differential v EndFraction 2nd Column equals left-parenthesis theta Subscript normal m normal a normal x Baseline minus theta Subscript normal m normal i normal n Baseline right-parenthesis z phi Subscript normal m normal a normal x Baseline left-parenthesis minus sine phi comma cosine phi comma 0 right-parenthesis 3rd Row 1st Column StartFraction partial-differential squared normal p Over partial-differential v squared EndFraction 2nd Column equals minus left-parenthesis theta Subscript normal m normal a normal x Baseline minus theta Subscript normal m normal i normal n Baseline right-parenthesis squared left-parenthesis x comma y comma z right-parenthesis period EndLayout

The translation into code is straightforward.

<<Compute sphere partial-differential bold n Subscript slash partial-differential u and partial-differential bold n Subscript slash partial-differential v >>= 
Vector3f d2Pduu = -phiMax * phiMax * Vector3f(pHit.x, pHit.y, 0); Vector3f d2Pduv = (thetaZMax - thetaZMin) * pHit.z * phiMax * Vector3f(-sinPhi, cosPhi, 0.); Vector3f d2Pdvv = -Sqr(thetaZMax - thetaZMin) * Vector3f(pHit.x,pHit.y,pHit.z); <<Compute coefficients for fundamental forms>> 
Float E = Dot(dpdu, dpdu), F = Dot(dpdu, dpdv), G = Dot(dpdv, dpdv); Vector3f n = Normalize(Cross(dpdu, dpdv)); Float e = Dot(n, d2Pduu), f = Dot(n, d2Pduv), g = Dot(n, d2Pdvv);
<<Compute partial-differential bold n Subscript slash partial-differential u and partial-differential bold n Subscript slash partial-differential v from fundamental form coefficients>> 
Float EGF2 = DifferenceOfProducts(E, G, F, F); Float invEGF2 = (EGF2 == 0) ? Float(0) : 1 / EGF2; Normal3f dndu = Normal3f((f * F - e * G) * invEGF2 * dpdu + (e * F - f * E) * invEGF2 * dpdv); Normal3f dndv = Normal3f((g * F - f * G) * invEGF2 * dpdu + (f * F - g * E) * invEGF2 * dpdv);

Given all the partial derivatives, it is also easy to compute the coefficients of the fundamental forms.

<<Compute coefficients for fundamental forms>>= 
Float E = Dot(dpdu, dpdu), F = Dot(dpdu, dpdv), G = Dot(dpdv, dpdv); Vector3f n = Normalize(Cross(dpdu, dpdv)); Float e = Dot(n, d2Pduu), f = Dot(n, d2Pduv), g = Dot(n, d2Pdvv);

We now have all the values necessary to apply the Weingarten equations. For this computation, we have found it worthwhile to use DifferenceOfProducts() to compute upper E upper G minus upper F squared for the greater numerical accuracy it provides than the direct expression of that computation. Note also that we must be careful to avoid dividing by 0 if that expression is zero-valued so that dndu and dndv do not take on not-a-number values in that case.

<<Compute partial-differential bold n Subscript slash partial-differential u and partial-differential bold n Subscript slash partial-differential v from fundamental form coefficients>>= 
Float EGF2 = DifferenceOfProducts(E, G, F, F); Float invEGF2 = (EGF2 == 0) ? Float(0) : 1 / EGF2; Normal3f dndu = Normal3f((f * F - e * G) * invEGF2 * dpdu + (e * F - f * E) * invEGF2 * dpdv); Normal3f dndv = Normal3f((g * F - f * G) * invEGF2 * dpdu + (f * F - g * E) * invEGF2 * dpdv);

Having computed the surface parameterization and all the relevant partial derivatives, a SurfaceInteraction structure that contains all the necessary geometric information for this intersection can be returned. There are three things to note in the parameter values passed to the SurfaceInteraction constructor.

  1. The intersection point is provided as a Point3i that takes the pHit point computed earlier and an error bound pError that is initialized in the fragment <<Compute error bounds for sphere intersection>>, which is defined later, in Section 6.8.5.
  2. The SurfaceInteraction is initialized with object-space geometric quantities (pHit, dpdu, etc.) and is then transformed to rendering space when it is returned. However, one of the parameters is the outgoing direction, omega Subscript normal o . This is passed in to InteractionFromIntersection(), but must be transformed to object space before being passed to the constructor so that the returned Interaction::wo value is in rendering space again.
  3. The flipNormal parameter indicates whether the surface normal should be flipped after it is initially computed with the cross product of dpdu and dpdv. This should be done either if the ReverseOrientation directive has been enabled or if the object-to-rendering-space transform swaps coordinate system handedness (but not if both of these are the case). (The need for the latter condition was discussed in Section 3.11.1.)

<<Return SurfaceInteraction for quadric intersection>>= 
bool flipNormal = reverseOrientation ^ transformSwapsHandedness; Vector3f woObject = (*objectFromRender)(wo); return (*renderFromObject)( SurfaceInteraction(Point3fi(pHit, pError), Point2f(u, v), woObject, dpdu, dpdv, dndu, dndv, time, flipNormal));

6.2.3 Surface Area

To compute the surface area of quadrics, we use a standard formula from integral calculus. If a curve y equals f left-parenthesis x right-parenthesis from x equals a to x equals b is revolved around the x axis, the surface area of the resulting swept surface is

2 pi integral Subscript a Superscript b Baseline f left-parenthesis x right-parenthesis StartRoot 1 plus left-parenthesis f prime left-parenthesis x right-parenthesis right-parenthesis squared EndRoot normal d x comma

where f prime left-parenthesis x right-parenthesis denotes the derivative normal d f slash normal d x . Since most of our surfaces of revolution are only partially swept around the axis, we will instead use the formula

phi Subscript normal m normal a normal x Baseline integral Subscript a Superscript b Baseline f left-parenthesis x right-parenthesis StartRoot 1 plus left-parenthesis f prime left-parenthesis x right-parenthesis right-parenthesis squared EndRoot normal d x period

The sphere is a surface of revolution of a circular arc. The function that defines the profile curve along the z axis of the sphere is

f left-parenthesis z right-parenthesis equals StartRoot r squared minus z squared EndRoot comma

and its derivative is

f prime left-parenthesis z right-parenthesis equals minus StartFraction z Over StartRoot r squared minus z squared EndRoot EndFraction period

Recall that the sphere is clipped at z Subscript normal m normal i normal n and z Subscript normal m normal a normal x . The surface area is therefore

StartLayout 1st Row 1st Column upper A 2nd Column equals phi Subscript normal m normal a normal x Baseline integral Subscript z Subscript normal m normal i normal n Baseline Superscript z Subscript normal m normal a normal x Baseline Baseline StartRoot r squared minus z squared EndRoot StartRoot 1 plus StartFraction z squared Over r squared minus z squared EndFraction EndRoot normal d z 2nd Row 1st Column Blank 2nd Column equals phi Subscript normal m normal a normal x Baseline integral Subscript z Subscript normal m normal i normal n Baseline Superscript z Subscript normal m normal a normal x Baseline Baseline StartRoot r squared minus z squared plus z squared EndRoot normal d z 3rd Row 1st Column Blank 2nd Column equals phi Subscript normal m normal a normal x Baseline integral Subscript z Subscript normal m normal i normal n Baseline Superscript z Subscript normal m normal a normal x Baseline Baseline r normal d z 4th Row 1st Column Blank 2nd Column equals phi Subscript normal m normal a normal x Baseline r left-parenthesis z Subscript normal m normal a normal x Baseline minus z Subscript normal m normal i normal n Baseline right-parenthesis period EndLayout

For the full sphere phi Subscript normal m normal a normal x Baseline equals 2 pi , z Subscript normal m normal i normal n Baseline equals negative r , and z Subscript normal m normal a normal x Baseline equals r , so we have the standard formula upper A equals 4 pi r squared , confirming that the formula makes sense.

<<Sphere Public Methods>>+=  
Float Area() const { return phiMax * radius * (zMax - zMin); }

6.2.4 Sampling

Uniformly sampling a point on the sphere’s area is easy: Sphere::Sample() generates a point on the unit sphere using SampleUniformSphere() and then scales the point by the sphere’s radius. A bound on the numeric error in this value is found in a fragment that will be defined later.

<<Sphere Method Definitions>>+= 
pstd::optional<ShapeSample> Sphere::Sample(Point2f u) const { Point3f pObj = Point3f(0, 0, 0) + radius * SampleUniformSphere(u); <<Reproject pObj to sphere surface and compute pObjError>> 
pObj *= radius / Distance(pObj, Point3f(0, 0, 0)); Vector3f pObjError = gamma(5) * Abs((Vector3f)pObj);
<<Compute surface normal for sphere sample and return ShapeSample>> 
Normal3f nObj(pObj.x, pObj.y, pObj.z); Normal3f n = Normalize((*renderFromObject)(nObj)); if (reverseOrientation) n *= -1; <<Compute left-parenthesis u comma v right-parenthesis coordinates for sphere sample>> 
Float theta = SafeACos(pObj.z / radius); Float phi = std::atan2(pObj.y, pObj.x); if (phi < 0) phi += 2 * Pi; Point2f uv(phi / phiMax, (theta - thetaZMin) / (thetaZMax - thetaZMin));
Point3fi pi = (*renderFromObject)(Point3fi(pObj, pObjError)); return ShapeSample{Interaction(pi, n, uv), 1 / Area()};
}

Because the object-space sphere is at the origin, the object-space surface normal is easily found by converting the object-space point to a normal vector and then normalizing it. A Point3fi for the sample point can be initialized from pObj and its error bounds. The final sample is returned in rendering space with a PDF equal to one over the surface area, since this Sample() method samples uniformly by surface area.

<<Compute surface normal for sphere sample and return ShapeSample>>= 
Normal3f nObj(pObj.x, pObj.y, pObj.z); Normal3f n = Normalize((*renderFromObject)(nObj)); if (reverseOrientation) n *= -1; <<Compute left-parenthesis u comma v right-parenthesis coordinates for sphere sample>> 
Float theta = SafeACos(pObj.z / radius); Float phi = std::atan2(pObj.y, pObj.x); if (phi < 0) phi += 2 * Pi; Point2f uv(phi / phiMax, (theta - thetaZMin) / (thetaZMax - thetaZMin));
Point3fi pi = (*renderFromObject)(Point3fi(pObj, pObjError)); return ShapeSample{Interaction(pi, n, uv), 1 / Area()};

The left-parenthesis u comma v right-parenthesis parametric coordinates for the point are given by inverting Equations (6.1) and (6.2).

<<Compute left-parenthesis u comma v right-parenthesis coordinates for sphere sample>>= 
Float theta = SafeACos(pObj.z / radius); Float phi = std::atan2(pObj.y, pObj.x); if (phi < 0) phi += 2 * Pi; Point2f uv(phi / phiMax, (theta - thetaZMin) / (thetaZMax - thetaZMin));

The associated PDF() method returns the same PDF.

<<Sphere Public Methods>>+=  
Float PDF(const Interaction &) const { return 1 / Area(); }

For the sphere sampling method that is given a point being illuminated, we can do much better than sampling over the sphere’s entire area. While uniform sampling over its surface would be perfectly correct, a better approach is not to sample points on the sphere that are definitely not visible (such as those on the back side of the sphere as seen from the point). The sampling routine here instead uniformly samples directions over the solid angle subtended by the sphere from the reference point and then computes the point on the sphere corresponding to the sampled direction.

<<Sphere Public Methods>>+=  
pstd::optional<ShapeSample> Sample(const ShapeSampleContext &ctx, Point2f u) const { <<Sample uniformly on sphere if normal p Subscript is inside it>> 
Point3f pCenter = (*renderFromObject)(Point3f(0, 0, 0)); Point3f pOrigin = ctx.OffsetRayOrigin(pCenter); if (DistanceSquared(pOrigin, pCenter) <= Sqr(radius)) { <<Sample shape by area and compute incident direction wi>> 
pstd::optional<ShapeSample> ss = Sample(u); ss->intr.time = ctx.time; Vector3f wi = ss->intr.p() - ctx.p(); if (LengthSquared(wi) == 0) return {}; wi = Normalize(wi);
<<Convert area sampling PDF in ss to solid angle measure>> 
ss->pdf /= AbsDot(ss->intr.n, -wi) / DistanceSquared(ctx.p(), ss->intr.p()); if (IsInf(ss->pdf)) return {};
return ss; }
<<Sample sphere uniformly inside subtended cone>> 
<<Compute quantities related to the theta Subscript normal m normal a normal x for cone>> 
Float sinThetaMax = radius / Distance(ctx.p(), pCenter); Float sin2ThetaMax = Sqr(sinThetaMax); Float cosThetaMax = SafeSqrt(1 - sin2ThetaMax); Float oneMinusCosThetaMax = 1 - cosThetaMax;
<<Compute theta and phi values for sample in cone>> 
Float cosTheta = (cosThetaMax - 1) * u[0] + 1; Float sin2Theta = 1 - Sqr(cosTheta); if (sin2ThetaMax < 0.00068523f /* sin^2(1.5 deg) */) { <<Compute cone sample via Taylor series expansion for small angles>> 
sin2Theta = sin2ThetaMax * u[0]; cosTheta = std::sqrt(1 - sin2Theta); oneMinusCosThetaMax = sin2ThetaMax / 2;
}
<<Compute angle alpha from center of sphere to sampled point on surface>> 
Float cosAlpha = sin2Theta / sinThetaMax + cosTheta * SafeSqrt(1 - sin2Theta / Sqr(sinThetaMax)); Float sinAlpha = SafeSqrt(1 - Sqr(cosAlpha));
<<Compute surface normal and sampled point on sphere>> 
Float phi = u[1] * 2 * Pi; Vector3f w = SphericalDirection(sinAlpha, cosAlpha, phi); Frame samplingFrame = Frame::FromZ(Normalize(pCenter - ctx.p())); Normal3f n(samplingFrame.FromLocal(-w)); Point3f p = pCenter + radius * Point3f(n.x, n.y, n.z); if (reverseOrientation) n *= -1;
<<Return ShapeSample for sampled point on sphere>> 
<<Compute pError for sampled point on sphere>> 
Vector3f pError = gamma(5) * Abs((Vector3f)p);
<<Compute left-parenthesis u comma v right-parenthesis coordinates for sampled point on sphere>> 
Point3f pObj = (*objectFromRender)(p); Float theta = SafeACos(pObj.z / radius); Float spherePhi = std::atan2(pObj.y, pObj.x); if (spherePhi < 0) spherePhi += 2 * Pi; Point2f uv(spherePhi / phiMax, (theta - thetaZMin) / (thetaZMax - thetaZMin));
return ShapeSample{Interaction(Point3fi(p, pError), n, ctx.time, uv), 1 / (2 * Pi * oneMinusCosThetaMax)};
}

For points that lie inside the sphere, the entire sphere should be sampled, since the whole sphere is visible from inside it. Note that the reference point used in this determination, pOrigin, is computed using the OffsetRayOrigin() function. Doing so ensures that if the reference point came from a ray intersecting the sphere, the point tested does not lie on the wrong side of the sphere due to rounding error.

<<Sample uniformly on sphere if normal p Subscript is inside it>>= 
Point3f pCenter = (*renderFromObject)(Point3f(0, 0, 0)); Point3f pOrigin = ctx.OffsetRayOrigin(pCenter); if (DistanceSquared(pOrigin, pCenter) <= Sqr(radius)) { <<Sample shape by area and compute incident direction wi>> 
pstd::optional<ShapeSample> ss = Sample(u); ss->intr.time = ctx.time; Vector3f wi = ss->intr.p() - ctx.p(); if (LengthSquared(wi) == 0) return {}; wi = Normalize(wi);
<<Convert area sampling PDF in ss to solid angle measure>> 
ss->pdf /= AbsDot(ss->intr.n, -wi) / DistanceSquared(ctx.p(), ss->intr.p()); if (IsInf(ss->pdf)) return {};
return ss; }

A call to the first Sample() method gives an initial ShapeSample for a point on the sphere. The direction vector from the reference point to the sampled point wi is computed and then normalized, so long as it is non-degenerate.

<<Sample shape by area and compute incident direction wi>>= 
pstd::optional<ShapeSample> ss = Sample(u); ss->intr.time = ctx.time; Vector3f wi = ss->intr.p() - ctx.p(); if (LengthSquared(wi) == 0) return {}; wi = Normalize(wi);

To compute the value of the PDF, the method converts the value of the PDF with respect to surface area from the call to Sample() to a PDF with respect to solid angle from the reference point. Doing so requires division by the factor

StartFraction normal d omega Subscript normal i Baseline Over normal d upper A EndFraction equals StartFraction cosine theta Subscript normal o Baseline Over r squared EndFraction comma

where theta Subscript normal o is the angle between the direction of the ray from the point on the light to the reference point and the light’s surface normal, and r squared is the distance between the point on the light and the point being shaded (recall the discussion about transforming between area and directional integration domains in Section 4.2.3).

In the rare case that the surface normal and wi are perpendicular, this results in an infinite value, in which case no valid sample is returned.

<<Convert area sampling PDF in ss to solid angle measure>>= 
ss->pdf /= AbsDot(ss->intr.n, -wi) / DistanceSquared(ctx.p(), ss->intr.p()); if (IsInf(ss->pdf)) return {};

For the more common case of a point outside the sphere, sampling within the cone proceeds.

<<Sample sphere uniformly inside subtended cone>>= 
<<Compute quantities related to the theta Subscript normal m normal a normal x for cone>> 
Float sinThetaMax = radius / Distance(ctx.p(), pCenter); Float sin2ThetaMax = Sqr(sinThetaMax); Float cosThetaMax = SafeSqrt(1 - sin2ThetaMax); Float oneMinusCosThetaMax = 1 - cosThetaMax;
<<Compute theta and phi values for sample in cone>> 
Float cosTheta = (cosThetaMax - 1) * u[0] + 1; Float sin2Theta = 1 - Sqr(cosTheta); if (sin2ThetaMax < 0.00068523f /* sin^2(1.5 deg) */) { <<Compute cone sample via Taylor series expansion for small angles>> 
sin2Theta = sin2ThetaMax * u[0]; cosTheta = std::sqrt(1 - sin2Theta); oneMinusCosThetaMax = sin2ThetaMax / 2;
}
<<Compute angle alpha from center of sphere to sampled point on surface>> 
Float cosAlpha = sin2Theta / sinThetaMax + cosTheta * SafeSqrt(1 - sin2Theta / Sqr(sinThetaMax)); Float sinAlpha = SafeSqrt(1 - Sqr(cosAlpha));
<<Compute surface normal and sampled point on sphere>> 
Float phi = u[1] * 2 * Pi; Vector3f w = SphericalDirection(sinAlpha, cosAlpha, phi); Frame samplingFrame = Frame::FromZ(Normalize(pCenter - ctx.p())); Normal3f n(samplingFrame.FromLocal(-w)); Point3f p = pCenter + radius * Point3f(n.x, n.y, n.z); if (reverseOrientation) n *= -1;

If the reference point is outside the sphere, then as seen from the reference point normal p Subscript the sphere subtends an angle

theta Subscript normal m normal a normal x Baseline equals arc sine left-parenthesis StartFraction r Over StartAbsoluteValue normal p Subscript Baseline minus normal p Subscript normal c Baseline EndAbsoluteValue EndFraction right-parenthesis equals arc cosine StartRoot 1 minus left-parenthesis StartFraction r Over StartAbsoluteValue normal p Subscript Baseline minus normal p Subscript normal c Baseline EndAbsoluteValue EndFraction right-parenthesis squared EndRoot comma
(6.4)

where r is the radius of the sphere and normal p Subscript normal c is its center (Figure 6.5). The sampling method here computes the cosine of the subtended angle theta Subscript normal m normal a normal x using Equation (6.4) and then uniformly samples directions inside this cone of directions using an approach that is derived for the SampleUniformCone() function in Section A.5.4, sampling an offset theta from the center vector omega Subscript normal c and then uniformly sampling a rotation angle phi around the vector. That function is not used here, however, as we will need some of the intermediate values in the following fragments.

Figure 6.5: To sample points on a spherical light source, we can uniformly sample within the cone of directions around a central vector omega Subscript normal c with an angular spread of up to theta Subscript normal m normal a normal x . Trigonometry can be used to derive the value of sine theta Subscript normal m normal a normal x , r slash StartAbsoluteValue normal p Subscript normal c Baseline minus normal p Subscript Baseline EndAbsoluteValue .

<<Compute quantities related to the theta Subscript normal m normal a normal x for cone>>= 
Float sinThetaMax = radius / Distance(ctx.p(), pCenter); Float sin2ThetaMax = Sqr(sinThetaMax); Float cosThetaMax = SafeSqrt(1 - sin2ThetaMax); Float oneMinusCosThetaMax = 1 - cosThetaMax;

As shown in Section A.5.4, uniform sampling of cosine theta between cosine theta Subscript normal m normal a normal x and 1 gives the cosine of a uniformly sampled direction in the cone.

<<Compute theta and phi values for sample in cone>>= 
Float cosTheta = (cosThetaMax - 1) * u[0] + 1; Float sin2Theta = 1 - Sqr(cosTheta); if (sin2ThetaMax < 0.00068523f /* sin^2(1.5 deg) */) { <<Compute cone sample via Taylor series expansion for small angles>> 
sin2Theta = sin2ThetaMax * u[0]; cosTheta = std::sqrt(1 - sin2Theta); oneMinusCosThetaMax = sin2ThetaMax / 2;
}

For very small theta Subscript normal m normal a normal x angles, cosine squared theta Subscript normal m normal a normal x is close to one. Computing sine squared theta by subtracting this value from 1 gives a value close to 0, but with very little accuracy, since there is much less floating-point precision close to 1 than there is by 0. Therefore, in this case, we use single-term Taylor expansions near 0 to compute sine squared theta and related terms, which gives much better accuracy.

<<Compute cone sample via Taylor series expansion for small angles>>= 
sin2Theta = sin2ThetaMax * u[0]; cosTheta = std::sqrt(1 - sin2Theta); oneMinusCosThetaMax = sin2ThetaMax / 2;

Given a sample angle left-parenthesis theta comma phi right-parenthesis with respect to the sampling coordinate system computed earlier, we can directly compute the corresponding point on the sphere. The first step is to find the angle gamma between the vector from the reference point normal p Subscript normal r to the sampled point on the sphere normal p Subscript normal s and the vector from the center of the sphere normal p Subscript normal c to normal p Subscript normal s . The basic setting is shown in Figure 6.6.

Figure 6.6: Geometric Setting for Computing the Sampled Point on the Sphere Corresponding to a Sampled Angle theta . Consider the triangle shown here. The lengths of two sides are known: one is the radius of the sphere r and the other is d Subscript normal c , the distance from the reference point to the center of the sphere. We also know one angle, theta . Given these, we first solve for the angle gamma before finding cosine alpha .

We denote the distance from the reference point to the center of the sphere by d Subscript normal c . Applying the law of sines, we can find that

sine gamma equals StartFraction d Subscript normal c Baseline Over r EndFraction sine theta period

Because gamma is an obtuse angle, gamma equals pi minus arc sine left-parenthesis d Subscript normal c Baseline slash r sine theta right-parenthesis . Given two of the three angles of the triangle, it follows that

alpha equals pi minus gamma minus theta equals arc sine left-parenthesis StartFraction d Subscript normal c Baseline Over r EndFraction sine theta right-parenthesis minus theta period

We can avoid expensive inverse trigonometric functions by taking advantage of the fact that we only need the sine and cosine of alpha . If we take the cosine of both sides of this equation, apply the cosine angle addition formula, and then use the two relationships sine theta Subscript normal m normal a normal x Baseline equals r slash d Subscript normal c and cosine left-parenthesis arc sine x right-parenthesis equals StartRoot 1 minus x squared EndRoot , we can find

cosine alpha equals StartFraction sine squared theta Over sine theta Subscript normal m normal a normal x Baseline EndFraction plus cosine theta StartRoot 1 minus StartFraction sine squared theta Over sine squared theta Subscript normal m normal a normal x Baseline EndFraction EndRoot period

The value of sine alpha follows from the identity sine alpha equals StartRoot 1 minus cosine squared alpha EndRoot .

<<Compute angle alpha from center of sphere to sampled point on surface>>= 
Float cosAlpha = sin2Theta / sinThetaMax + cosTheta * SafeSqrt(1 - sin2Theta / Sqr(sinThetaMax)); Float sinAlpha = SafeSqrt(1 - Sqr(cosAlpha));

The alpha angle and phi give the spherical coordinates for the sampled direction with respect to a coordinate system with z axis centered around the vector from the reference point to the sphere center. We can use an instance of the Frame class to transform the direction from that coordinate system to rendering space. The surface normal on the sphere can then be computed as the negation of that vector and the point on the sphere can be found by scaling by the radius and translating by the sphere’s center point.

<<Compute surface normal and sampled point on sphere>>= 
Float phi = u[1] * 2 * Pi; Vector3f w = SphericalDirection(sinAlpha, cosAlpha, phi); Frame samplingFrame = Frame::FromZ(Normalize(pCenter - ctx.p())); Normal3f n(samplingFrame.FromLocal(-w)); Point3f p = pCenter + radius * Point3f(n.x, n.y, n.z); if (reverseOrientation) n *= -1;

The <<Compute left-parenthesis u comma v right-parenthesis coordinates for sampled point on sphere>> fragment applies the same mapping using the object space sampled point as is done in the Intersect() method, and so it is elided. The PDF for uniform sampling in a cone is 1 slash left-parenthesis 2 pi left-parenthesis 1 minus cosine theta Subscript normal m normal a normal x Baseline right-parenthesis right-parenthesis . (A derivation is in Section A.5.4.)

<<Return ShapeSample for sampled point on sphere>>= 
<<Compute pError for sampled point on sphere>> 
Vector3f pError = gamma(5) * Abs((Vector3f)p);
<<Compute left-parenthesis u comma v right-parenthesis coordinates for sampled point on sphere>> 
Point3f pObj = (*objectFromRender)(p); Float theta = SafeACos(pObj.z / radius); Float spherePhi = std::atan2(pObj.y, pObj.x); if (spherePhi < 0) spherePhi += 2 * Pi; Point2f uv(spherePhi / phiMax, (theta - thetaZMin) / (thetaZMax - thetaZMin));
return ShapeSample{Interaction(Point3fi(p, pError), n, ctx.time, uv), 1 / (2 * Pi * oneMinusCosThetaMax)};

The method that computes the PDF for sampling a direction toward a sphere from a reference point also differs depending on which of the two sampling strategies would be used for the point.

<<Sphere Public Methods>>+= 
Float PDF(const ShapeSampleContext &ctx, Vector3f wi) const { Point3f pCenter = (*renderFromObject)(Point3f(0, 0, 0)); Point3f pOrigin = ctx.OffsetRayOrigin(pCenter); if (DistanceSquared(pOrigin, pCenter) <= Sqr(radius)) { <<Return solid angle PDF for point inside sphere>> 
<<Intersect sample ray with shape geometry>> 
Ray ray = ctx.SpawnRay(wi); pstd::optional<ShapeIntersection> isect = Intersect(ray); if (!isect) return 0;
<<Compute PDF in solid angle measure from shape intersection point>> 
Float pdf = (1 / Area()) / (AbsDot(isect->intr.n, -wi) / DistanceSquared(ctx.p(), isect->intr.p())); if (IsInf(pdf)) pdf = 0;
return pdf;
} <<Compute general solid angle sphere PDF>> 
Float sin2ThetaMax = radius * radius / DistanceSquared(ctx.p(), pCenter); Float cosThetaMax = SafeSqrt(1 - sin2ThetaMax); Float oneMinusCosThetaMax = 1 - cosThetaMax; <<Compute more accurate oneMinusCosThetaMax for small solid angle>> 
if (sin2ThetaMax < 0.00068523f /* sin^2(1.5 deg) */) oneMinusCosThetaMax = sin2ThetaMax / 2;
return 1 / (2 * Pi * oneMinusCosThetaMax);
}

If the reference point is inside the sphere, a uniform area sampling strategy would have been used.

<<Return solid angle PDF for point inside sphere>>= 
<<Intersect sample ray with shape geometry>> 
Ray ray = ctx.SpawnRay(wi); pstd::optional<ShapeIntersection> isect = Intersect(ray); if (!isect) return 0;
<<Compute PDF in solid angle measure from shape intersection point>> 
Float pdf = (1 / Area()) / (AbsDot(isect->intr.n, -wi) / DistanceSquared(ctx.p(), isect->intr.p())); if (IsInf(pdf)) pdf = 0;
return pdf;

First, the corresponding point on the sphere is found by intersecting a ray leaving the reference point in direction wi with the sphere. Note that this is a fairly efficient computation since it is only intersecting the ray with a single sphere and not the entire scene.

<<Intersect sample ray with shape geometry>>= 
Ray ray = ctx.SpawnRay(wi); pstd::optional<ShapeIntersection> isect = Intersect(ray); if (!isect) return 0;

In turn, the uniform area density of one over the surface area is converted to a solid angle density following the same approach as was used in the previous Sample() method.

<<Compute PDF in solid angle measure from shape intersection point>>= 
Float pdf = (1 / Area()) / (AbsDot(isect->intr.n, -wi) / DistanceSquared(ctx.p(), isect->intr.p())); if (IsInf(pdf)) pdf = 0;

The value of the PDF is easily computed using the same trigonometric identities as were used in the sampling routine.

<<Compute general solid angle sphere PDF>>= 
Float sin2ThetaMax = radius * radius / DistanceSquared(ctx.p(), pCenter); Float cosThetaMax = SafeSqrt(1 - sin2ThetaMax); Float oneMinusCosThetaMax = 1 - cosThetaMax; <<Compute more accurate oneMinusCosThetaMax for small solid angle>> 
if (sin2ThetaMax < 0.00068523f /* sin^2(1.5 deg) */) oneMinusCosThetaMax = sin2ThetaMax / 2;
return 1 / (2 * Pi * oneMinusCosThetaMax);

Here it is also worth considering numerical accuracy when the sphere subtends a small solid angle from the reference point. In that case, cosThetaMax will be close to 1 and the value of oneMinusCosThetaMax will be relatively inaccurate; we then switch to the one-term Taylor approximation of 1 minus cosine theta almost-equals 1 slash 2 sine squared theta , which is more accurate near zero.

<<Compute more accurate oneMinusCosThetaMax for small solid angle>>= 
if (sin2ThetaMax < 0.00068523f /* sin^2(1.5 deg) */) oneMinusCosThetaMax = sin2ThetaMax / 2;