## 6.2 Spheres

Spheres are a special case of a general type of surface called quadrics—surfaces described by quadratic polynomials in , , and . 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

The set of all points (, , ) that fulfill this condition defines the surface. For a unit sphere at the origin, the familiar implicit equation is . 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 can be described as a function of 2D spherical coordinates , where ranges from to and ranges from to (Figure 6.3):

(6.1)

We can transform this function into a function over and generalize it slightly to allow partial spheres that only sweep out and with the substitution

(6.2)

This form is particularly useful for texture mapping, where it can be directly used to map a texture defined over to the sphere. Figure 6.4 shows an image of two spheres; a grid image map has been used to show the parameterization.

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>>
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));
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 {};
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 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 >>
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 >>
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 {}; }
} 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 and >>
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 and >>
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 and 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);
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 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 for cone>>
Float sinThetaMax = radius / Distance(ctx.p(), pCenter); Float sin2ThetaMax = Sqr(sinThetaMax); Float cosThetaMax = SafeSqrt(1 - sin2ThetaMax); Float oneMinusCosThetaMax = 1 - cosThetaMax;
<<Compute and 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 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 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 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 value can be set. The sphere sweeps out values from 0 to the given such that the section of the sphere with spherical values above 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 and 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 is less than . This improvement is left as an exercise. This object-space bounding box is transformed to rendering space before being returned.

<<Sphere Method Definitions>>=

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 along the ray where the intersection occurred, the object-space intersection point, and the sphere’s value there. As its name suggests, this structure will be used by the other quadrics in the same way it is here.

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));
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 {};
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 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 >>
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 >>
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 {}; }
}

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 , its implicit representation is

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

Note that all elements of this equation besides are known values. The 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 ,

where

(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));
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 {};
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 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.

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 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 approach that gives more accuracy; it is described in Section B.2.10.

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 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 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 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 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 or ranges—intersections that are in clipped areas must be ignored. The implementation starts by computing the value for the hit point. Using the parametric representation of the sphere,

so . It is necessary to remap the result of the standard library’s std::atan() function to a value between and , to match the sphere’s original definition.

<<Compute sphere hit position and >>=
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 and . One subtlety is that it is important to skip the tests if the range includes the entire sphere; the computed pHit.z value may be slightly out of the 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 intersection is not valid, the routine tries again with .

<<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 >>
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 value in rendering space. Therefore, it can be returned directly.

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 and >>
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 and >>
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 and 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);
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 and values by scaling the previously computed value for the hit to lie between 0 and 1 and by computing a value between 0 and 1 for the hit point based on the range of values for the given sphere. Then it finds the parametric partial derivatives of position and and surface normal and .

<<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 and >>
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 and >>
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 and 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 component of , , is calculated; the other components are found similarly. Using the parametric definition of the sphere, we have

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

Similarly,

and

A similar process gives . The complete result is

and the implementation follows directly.

<<Compute sphere and >>=
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 and 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 and are given by the Weingarten equations from differential geometry:

where , , and are coefficients of the first fundamental form and are given by

These are easily computed with the and values found earlier. The , , and are coefficients of the second fundamental form,

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 , , and , it is necessary to compute the second-order partial derivatives and so on.

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

The translation into code is straightforward.

<<Compute sphere and >>=
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 and 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 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 and 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, . 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.)

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 from to is revolved around the axis, the surface area of the resulting swept surface is

where denotes the derivative . Since most of our surfaces of revolution are only partially swept around the axis, we will instead use the formula

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

and its derivative is

Recall that the sphere is clipped at and . The surface area is therefore

For the full sphere , , and , so we have the standard formula , 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 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 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 parametric coordinates for the point are given by inverting Equations (6.1) and (6.2).

<<Compute 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 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 for cone>>
Float sinThetaMax = radius / Distance(ctx.p(), pCenter); Float sin2ThetaMax = Sqr(sinThetaMax); Float cosThetaMax = SafeSqrt(1 - sin2ThetaMax); Float oneMinusCosThetaMax = 1 - cosThetaMax;
<<Compute and 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 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 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 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

where 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 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 for cone>>
Float sinThetaMax = radius / Distance(ctx.p(), pCenter); Float sin2ThetaMax = Sqr(sinThetaMax); Float cosThetaMax = SafeSqrt(1 - sin2ThetaMax); Float oneMinusCosThetaMax = 1 - cosThetaMax;
<<Compute and 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 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 the sphere subtends an angle

(6.4)

where is the radius of the sphere and is its center (Figure 6.5). The sampling method here computes the cosine of the subtended angle 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 from the center vector and then uniformly sampling a rotation angle around the vector. That function is not used here, however, as we will need some of the intermediate values in the following fragments.

<<Compute quantities related to the 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 between and 1 gives the cosine of a uniformly sampled direction in the cone.

<<Compute and 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 angles, is close to one. Computing 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 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 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 between the vector from the reference point to the sampled point on the sphere and the vector from the center of the sphere to . The basic setting is shown in Figure 6.6.

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

Because is an obtuse angle, . Given two of the three angles of the triangle, it follows that

We can avoid expensive inverse trigonometric functions by taking advantage of the fact that we only need the sine and cosine of . If we take the cosine of both sides of this equation, apply the cosine angle addition formula, and then use the two relationships and , we can find

The value of follows from the identity .

<<Compute angle 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 angle and give the spherical coordinates for the sampled direction with respect to a coordinate system with 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 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 . (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 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 , 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;