9.9 Scattering from Hair

Human hair and animal fur can be modeled with a rough dielectric interface surrounding a pigmented translucent core. Reflectance at the interface is generally the same for all wavelengths and it is therefore wavelength-variation in the amount of light that is absorbed inside the hair that determines its color. While these scattering effects could be modeled using a combination of the DielectricBxDF and the volumetric scattering models from Chapters 11 and 14, not only would doing so be computationally expensive, requiring ray tracing within the hair geometry, but importance sampling the resulting model would be difficult. Therefore, this section introduces a specialized BSDF for hair and fur that encapsulates these lower-level scattering processes into a model that can be efficiently evaluated and sampled.

See Figure 9.39 for an example of the visual complexity of scattering in hair and a comparison to rendering using a conventional BRDF model with hair geometry.

Figure 9.39: Comparison of a BSDF that Models Hair to a Coated Diffuse BSDF. (a) Geometric model of hair rendered using a BSDF based on a diffuse base layer with a rough dielectric interface above it (Section 14.3.3). (b) Model rendered using the HairBxDF from this section. Because the HairBxDF is based on an accurate model of the hair microgeometry and also models light transmission through hair, it gives a much more realistic appearance. (Hair geometry courtesy of Cem Yuksel.)

9.9.1 Geometry

Before discussing the mechanisms of light scattering from hair, we will start by defining a few ways of measuring incident and outgoing directions at ray intersection points on hair. In doing so, we will assume that the hair BSDF is used with the Curve shape from Section 6.7, which allows us to make assumptions about the parameterization of the hair geometry. For the geometric discussion in this section, we will assume that the Curve variant corresponding to a flat ribbon that is always facing the incident ray is being used. However, in the BSDF model, we will interpret intersection points as if they were on the surface of the corresponding swept cylinder. If there is no interpenetration between hairs and if the hair’s width is not much larger than a pixel’s width, there is no harm in switching between these interpretations.

Throughout the implementation of this scattering model, we will find it useful to separately consider scattering in the longitudinal plane, effectively using a side view of the curve, and scattering in the azimuthal plane, considering the curve head-on at a particular point along it. To understand these parameterizations, first recall that Curves are parameterized such that the u direction is along the length of the curve and v spans its width. At a given u , all the possible surface normals of the curve are given by the surface normals of the circular cross-section at that point. All of these normals lie in a plane that is called the normal plane (Figure 9.40).

Figure 9.40: At any parametric point along a Curve shape, the cross-section of the curve is defined by a circle. All of the circle’s surface normals (arrows) lie in a plane (dashed lines), dubbed the “normal plane.”

Figure 9.41: (a) Given a direction omega Subscript at a point on a curve, the longitudinal angle theta is defined by the angle between omega Subscript and the normal plane at the point (thick line). The curve’s tangent vector at the point is aligned with the x axis in the BSDF coordinate system. (b) For a direction omega Subscript , the azimuthal angle phi is found by projecting the direction into the normal plane and computing its angle with the y axis, which corresponds to the curve’s partial-differential normal p slash partial-differential v in the BSDF coordinate system.

We will find it useful to represent directions at a ray–curve intersection point with respect to coordinates left-parenthesis theta comma phi right-parenthesis that are defined with respect to the normal plane at the u position where the ray intersected the curve. The angle theta is the longitudinal angle, which is the offset of the ray with respect to the normal plane (Figure 9.41(a)); theta ranges from negative pi slash 2 to pi slash 2 , where pi slash 2 corresponds to a direction aligned with partial-differential normal p slash partial-differential u and negative pi slash 2 corresponds to minus partial-differential normal p slash partial-differential u . As explained in Section 9.1.1, in pbrt’s regular BSDF coordinate system, partial-differential normal p slash partial-differential u is aligned with the plus x axis, so given a direction in the BSDF coordinate system, we have sine theta equals omega Subscript Baseline Subscript x , since the normal plane is perpendicular to partial-differential normal p slash partial-differential u .

In the BSDF coordinate system, the normal plane is spanned by the y and z coordinate axes. ( y corresponds to partial-differential normal p slash partial-differential v for curves, which is always perpendicular to the curve’s partial-differential normal p slash partial-differential u , and z is aligned with the ribbon normal.) The azimuthal angle phi is found by projecting a direction omega Subscript into the normal plane and computing its angle with the y axis. It thus ranges from 0 to 2 pi (Figure 9.41(b)).

One more measurement with respect to the curve will be useful in the following. Consider incident rays with some direction omega Subscript : at any given parametric u value, all such rays that intersect the curve can only possibly intersect one half of the circle swept along the curve (Figure 9.42). We will parameterize the circle’s diameter with the variable h , where h equals plus-or-minus 1 corresponds to the ray grazing the edge of the circle, and h equals 0 corresponds to hitting it edge-on. Because pbrt parameterizes curves with v element-of left-bracket 0 comma 1 right-bracket across the curve, we can compute h equals negative 1 plus 2 v .

Given the h for a ray intersection, we can compute the angle between the surface normal of the inferred swept cylinder (which is by definition in the normal plane) and the direction omega Subscript , which we will denote by gamma . (Note: this is unrelated to the gamma Subscript n notation used for floating-point error bounds in Section 6.8.) See Figure 9.42, which shows that sine gamma equals h .

Figure 9.42: Given an incident direction omega Subscript of a ray that intersected a Curve projected to the normal plane, we can parameterize the curve’s width with h element-of left-bracket negative 1 comma 1 right-bracket . Given the h for a ray that has intersected the curve, trigonometry shows how to compute the angle gamma between omega Subscript and the surface normal on the curve’s surface at the intersection point. The two angles gamma are equal, and because the circle’s radius is 1, sine gamma equals h .

9.9.2 Scattering from Hair

Geometric setting in hand, we will now turn to discuss the general scattering behaviors that give hair its distinctive appearance and some of the assumptions that we will make in the following.

Hair and fur have three main components:

  • Cuticle: The outer layer, which forms the boundary with air. The cuticle’s surface is a nested series of scales at a slight angle to the hair surface.
  • Cortex: The next layer inside the cuticle. The cortex generally accounts for around 90% of hair’s volume but less for fur. It is typically colored with pigments that mostly absorb light.
  • Medulla: The center core at the middle of the cortex. It is larger and more significant in thicker hair and fur. The medulla is also pigmented. Scattering from the medulla is much more significant than scattering from the medium in the cortex.

For the following scattering model, we will make a few assumptions. (Approaches for relaxing some of them are discussed in the exercises at the end of this chapter.) First, we assume that the cuticle can be modeled as a rough dielectric cylinder with scales that are all angled at the same angle alpha (effectively giving a nested series of cones; see Figure 9.43). We also treat the hair interior as a homogeneous medium that only absorbs light—scattering inside the hair is not modeled directly. (Chapters 11 and 14 have detailed discussion of light transport in participating media.)

Figure 9.43: The surface of hair is formed by scales that deviate by a small angle alpha from the ideal cylinder. ( alpha is generally around ; the angle shown here is larger for illustrative purposes.)

We will also make the assumption that scattering can be modeled accurately by a BSDF—we model light as entering and exiting the hair at the same place. (A BSSRDF could certainly be used instead; the “Further Reading” section has pointers to work that has investigated that alternative.) Note that this assumption requires that the hair’s diameter be fairly small with respect to how quickly illumination changes over the surface; this assumption is generally fine in practice at typical viewing distances.

Figure 9.44: Incident light arriving at a hair can be scattered in a variety of ways. p equals 0 corresponds to light reflected from the surface of the cuticle. Light may also be transmitted through the hair and leave the other side: p equals 1 . It may be transmitted into the hair and reflected back into it again before being transmitted back out: p equals 2 , and so forth.

Incident light arriving at a hair may be scattered one or more times before leaving the hair; Figure 9.44 shows a few of the possible cases. We will use p to denote the number of path segments it follows inside the hair before being scattered back out to air. We will sometimes refer to terms with a shorthand that describes the corresponding scattering events at the boundary: p equals 0 corresponds to upper R , for reflection, p equals 1 is upper T upper T , for two transmissions p equals 2 is upper T upper R upper T , p equals 3 is upper T upper R upper R upper T , and so forth.

In the following, we will find it useful to consider these scattering modes separately and so will write the hair BSDF as a sum over terms p :

f Subscript Baseline left-parenthesis omega Subscript normal o Baseline comma omega Subscript normal i Baseline right-parenthesis equals sigma-summation Underscript p equals 0 Overscript normal infinity Endscripts f Subscript p Baseline left-parenthesis omega Subscript normal o Baseline comma omega Subscript normal i Baseline right-parenthesis period

To make the scattering model evaluation and sampling easier, many hair scattering models factor f Subscript into terms where one depends only on the angles theta and another on phi , the difference between phi Subscript normal o and phi Subscript normal i . This semi-separable model is given by:

f Subscript p Baseline left-parenthesis omega Subscript normal o Baseline comma omega Subscript normal i Baseline right-parenthesis equals StartFraction upper M Subscript p Baseline left-parenthesis theta Subscript normal o Baseline comma theta Subscript normal i Baseline right-parenthesis upper A Subscript p Baseline left-parenthesis omega Subscript normal o Baseline right-parenthesis upper N Subscript p Baseline left-parenthesis phi right-parenthesis Over StartAbsoluteValue cosine theta Subscript normal i Baseline EndAbsoluteValue EndFraction comma

where we have a longitudinal scattering function upper M Subscript p , an attenuation function upper A Subscript p , and an azimuthal scattering function upper N Subscript p . The division by StartAbsoluteValue cosine theta Subscript normal i Baseline EndAbsoluteValue cancels out the corresponding factor in the reflection equation.

In the following implementation, we will evaluate the first few terms of the sum in Equation (9.47) and then represent all higher-order terms with a single one. The pMax constant controls how many are evaluated before the switch-over.

<<HairBxDF Constants>>= 
static constexpr int pMax = 3;

The model implemented in the HairBxDF is parameterized by six values:

  • h: the left-bracket negative 1 comma 1 right-bracket offset along the curve width where the ray intersected the oriented ribbon.
  • eta: the index of refraction of the interior of the hair (typically, 1.55).
  • sigma_a: the absorption coefficient of the hair interior, where distance is measured with respect to the hair cylinder’s diameter. (The absorption coefficient is introduced in Section 11.1.1.)
  • beta_m: the longitudinal roughness of the hair, mapped to the range left-bracket 0 comma 1 right-bracket .
  • beta_n: the azimuthal roughness, also mapped to left-bracket 0 comma 1 right-bracket .
  • alpha: the angle at which the small scales on the surface of hair are offset from the base cylinder, expressed in degrees (typically, 2).

<<HairBxDF Definition>>= 
class HairBxDF { public: <<HairBxDF Public Methods>> 
HairBxDF() = default; PBRT_CPU_GPU HairBxDF(Float h, Float eta, const SampledSpectrum &sigma_a, Float beta_m, Float beta_n, Float alpha); PBRT_CPU_GPU SampledSpectrum f(Vector3f wo, Vector3f wi, TransportMode mode) const; PBRT_CPU_GPU pstd::optional<BSDFSample> Sample_f(Vector3f wo, Float uc, Point2f u, TransportMode mode, BxDFReflTransFlags sampleFlags) const; PBRT_CPU_GPU Float PDF(Vector3f wo, Vector3f wi, TransportMode mode, BxDFReflTransFlags sampleFlags) const; PBRT_CPU_GPU void Regularize() {} PBRT_CPU_GPU static constexpr const char *Name() { return "HairBxDF"; } std::string ToString() const; PBRT_CPU_GPU BxDFFlags Flags() const { return BxDFFlags::GlossyReflection; } PBRT_CPU_GPU static RGBUnboundedSpectrum SigmaAFromConcentration(Float ce, Float cp); PBRT_CPU_GPU static SampledSpectrum SigmaAFromReflectance(const SampledSpectrum &c, Float beta_n, const SampledWavelengths &lambda);
private: <<HairBxDF Constants>> 
static constexpr int pMax = 3;
<<HairBxDF Private Methods>> 
Float Mp(Float cosTheta_i, Float cosTheta_o, Float sinTheta_i, Float sinTheta_o, Float v) { Float a = cosTheta_i * cosTheta_o / v, b = sinTheta_i * sinTheta_o / v; Float mp = (v <= .1) ? (FastExp(LogI0(a) - b - 1 / v + 0.6931f + std::log(1 / (2 * v)))) : (FastExp(-b) * I0(a)) / (std::sinh(1 / v) * 2 * v); return mp; } pstd::array<SampledSpectrum, pMax + 1> Ap(Float cosTheta_o, Float eta, Float h, SampledSpectrum T) { pstd::array<SampledSpectrum, pMax + 1> ap; <<Compute p equals 0 attenuation at initial cylinder intersection>> 
Float cosGamma_o = SafeSqrt(1 - Sqr(h)); Float cosTheta = cosTheta_o * cosGamma_o; Float f = FrDielectric(cosTheta, eta); ap[0] = SampledSpectrum(f);
<<Compute p equals 1 attenuation term>> 
ap[1] = Sqr(1 - f) * T;
<<Compute attenuation terms up to p equals monospace p monospace upper M monospace a monospace x >> 
for (int p = 2; p < pMax; ++p) ap[p] = ap[p - 1] * T * f;
<<Compute attenuation term accounting for remaining orders of scattering>> 
if (1 - T * f) ap[pMax] = ap[pMax - 1] * f * T / (1 - T * f);
return ap; } Float Phi(int p, Float gamma_o, Float gamma_t) { return 2 * p * gamma_t - 2 * gamma_o + p * Pi; } Float Np(Float phi, int p, Float s, Float gamma_o, Float gamma_t) { Float dphi = phi - Phi(p, gamma_o, gamma_t); <<Remap dphi to left-bracket negative pi comma pi right-bracket >> 
while (dphi > Pi) dphi -= 2 * Pi; while (dphi < -Pi) dphi += 2 * Pi;
return TrimmedLogistic(dphi, s, -Pi, Pi); } pstd::array<Float, pMax + 1> ApPDF(Float cosTheta_o) const;
<<HairBxDF Private Members>> 
Float h, eta; SampledSpectrum sigma_a; Float beta_m, beta_n; Float v[pMax + 1]; Float s; Float sin2kAlpha[pMax], cos2kAlpha[pMax];
};

Beyond initializing corresponding member variables, the HairBxDF constructor performs some additional precomputation of values that will be useful for sampling and evaluation of the scattering model. The corresponding code will be added to the <<HairBxDF constructor implementation>> fragment in the following, closer to where the corresponding values are defined and used. Note that alpha is not stored in a member variable; it is used to compute a few derived quantities that will be, however.

<<HairBxDF Private Members>>= 
Float h, eta; SampledSpectrum sigma_a; Float beta_m, beta_n;

We will start with the method that evaluates the BSDF.

<<HairBxDF Method Definitions>>= 
SampledSpectrum HairBxDF::f(Vector3f wo, Vector3f wi, TransportMode mode) const { <<Compute hair coordinate system terms related to wo>> 
Float sinTheta_o = wo.x; Float cosTheta_o = SafeSqrt(1 - Sqr(sinTheta_o)); Float phi_o = std::atan2(wo.z, wo.y); Float gamma_o = SafeASin(h);
<<Compute hair coordinate system terms related to wi>> 
Float sinTheta_i = wi.x; Float cosTheta_i = SafeSqrt(1 - Sqr(sinTheta_i)); Float phi_i = std::atan2(wi.z, wi.y);
<<Compute cosine theta Subscript normal t for refracted ray>> 
Float sinTheta_t = sinTheta_o / eta; Float cosTheta_t = SafeSqrt(1 - Sqr(sinTheta_t));
<<Compute gamma Subscript normal t for refracted ray>> 
Float etap = SafeSqrt(Sqr(eta) - Sqr(sinTheta_o)) / cosTheta_o; Float sinGamma_t = h / etap; Float cosGamma_t = SafeSqrt(1 - Sqr(sinGamma_t)); Float gamma_t = SafeASin(sinGamma_t);
<<Compute the transmittance T of a single path through the cylinder>> 
SampledSpectrum T = Exp(-sigma_a * (2 * cosGamma_t / cosTheta_t));
<<Evaluate hair BSDF>> 
Float phi = phi_i - phi_o; pstd::array<SampledSpectrum, pMax + 1> ap = Ap(cosTheta_o, eta, h, T); SampledSpectrum fsum(0.); for (int p = 0; p < pMax; ++p) { <<Compute sine theta Subscript normal o and cosine theta Subscript normal o terms accounting for scales>> 
Float sinThetap_o, cosThetap_o; if (p == 0) { sinThetap_o = sinTheta_o * cos2kAlpha[1] - cosTheta_o * sin2kAlpha[1]; cosThetap_o = cosTheta_o * cos2kAlpha[1] + sinTheta_o * sin2kAlpha[1]; } <<Handle remainder of p values for hair scale tilt>> 
else if (p == 1) { sinThetap_o = sinTheta_o * cos2kAlpha[0] + cosTheta_o * sin2kAlpha[0]; cosThetap_o = cosTheta_o * cos2kAlpha[0] - sinTheta_o * sin2kAlpha[0]; } else if (p == 2) { sinThetap_o = sinTheta_o * cos2kAlpha[2] + cosTheta_o * sin2kAlpha[2]; cosThetap_o = cosTheta_o * cos2kAlpha[2] - sinTheta_o * sin2kAlpha[2]; } else { sinThetap_o = sinTheta_o; cosThetap_o = cosTheta_o; }
<<Handle out-of-range cosine theta Subscript normal o from scale adjustment>> 
cosThetap_o = std::abs(cosThetap_o);
fsum += Mp(cosTheta_i, cosThetap_o, sinTheta_i, sinThetap_o, v[p]) * ap[p] * Np(phi, p, s, gamma_o, gamma_t); } <<Compute contribution of remaining terms after pMax>> 
fsum += Mp(cosTheta_i, cosTheta_o, sinTheta_i, sinTheta_o, v[pMax]) * ap[pMax] / (2 * Pi);
if (AbsCosTheta(wi) > 0) fsum /= AbsCosTheta(wi); return fsum;
}

There are a few quantities related to the directions omega Subscript normal o and omega Subscript normal i that are needed for evaluating the hair scattering model—specifically, the sine and cosine of the angle theta that each direction makes with the plane perpendicular to the curve, and the angle phi in the azimuthal coordinate system.

As explained in Section 9.9.1, sine theta Subscript normal o is given by the x component of omega Subscript normal o in the BSDF coordinate system. Given sine theta Subscript normal o , because theta Subscript normal o Baseline element-of left-bracket negative pi slash 2 comma pi slash 2 right-bracket , we know that cosine theta Subscript normal o must be positive, and so we can compute cosine theta Subscript normal o using the identity sine squared theta plus cosine squared theta equals 1 . The angle phi Subscript normal o in the perpendicular plane can be computed with std::atan.

<<Compute hair coordinate system terms related to wo>>= 
Float sinTheta_o = wo.x; Float cosTheta_o = SafeSqrt(1 - Sqr(sinTheta_o)); Float phi_o = std::atan2(wo.z, wo.y); Float gamma_o = SafeASin(h);

Equivalent code computes these values for wi.

<<Compute hair coordinate system terms related to wi>>= 
Float sinTheta_i = wi.x; Float cosTheta_i = SafeSqrt(1 - Sqr(sinTheta_i)); Float phi_i = std::atan2(wi.z, wi.y);

With these values available, we will consider in turn the factors of the BSDF model— upper M Subscript p , upper A Subscript p , and upper N Subscript p —before returning to the completion of the f() method.

9.9.3 Longitudinal Scattering

upper M Subscript p defines the component of scattering related to the angles theta —longitudinal scattering. Longitudinal scattering is responsible for the specular lobe along the length of hair and the longitudinal roughness beta Subscript m controls the size of this highlight. Figure 9.45 shows hair geometry rendered with three different longitudinal scattering roughnesses.

Figure 9.45: The Effect of Varying the Longitudinal Roughness beta Subscript m . Hair model illuminated by a skylight environment map rendered with varying longitudinal roughness. (a) With a very low roughness, beta Subscript m Baseline equals 0.1 , the hair appears too shiny—almost metallic. (b) With beta Subscript m Baseline equals 0.25 , the highlight is similar to typical human hair. (c) At high roughness, beta Subscript m Baseline equals 0.7 , the hair is unrealistically flat and diffuse. (Hair geometry courtesy of Cem Yuksel.)

The mathematical details of the derivation of the scattering model are complex, so we will not include them here; as always, the “Further Reading” section has references to the details. The design goals of the model implemented here were that it be normalized (ensuring both energy conservation and no energy loss) and that it could be sampled directly. Although this model is not directly based on a physical model of how hair scatters light, it matches measured data well and has parametric control of roughness v .

The model is:

upper M Subscript p Baseline left-parenthesis theta Subscript normal o Baseline comma theta Subscript normal i Baseline right-parenthesis equals StartFraction 1 Over 2 v normal s normal i normal n normal h left-parenthesis 1 slash v right-parenthesis EndFraction normal e Superscript minus StartFraction sine theta Super Subscript normal i Superscript sine theta Super Subscript normal o Superscript Over v EndFraction Baseline upper I 0 left-parenthesis StartFraction cosine theta Subscript normal o Baseline cosine theta Subscript normal i Baseline Over v EndFraction right-parenthesis comma

where upper I 0 is the modified Bessel function of the first kind and v is the roughness variance. Figure 9.46 shows plots of upper M Subscript p .

Figure 9.46: Plots of the Longitudinal Scattering Function. The shape of upper M Subscript p as a function of theta Subscript normal i for three values of theta Subscript normal o . In all cases a roughness variance of v equals 0.02 was used. The peaks are slightly shifted from the perfect specular reflection directions (at theta Subscript normal i Baseline equals 1 , 1.3 , and 1.4 , respectively). (After d’Eon et al. (2011), Figure 4.)

This model is not numerically stable for low roughness variance values, so our implementation uses a different approach for that case that operates on the logarithm of upper I 0 before taking an exponent at the end. The v <= .1 test in the implementation below selects between the two formulations.

<<HairBxDF Private Methods>>= 
Float Mp(Float cosTheta_i, Float cosTheta_o, Float sinTheta_i, Float sinTheta_o, Float v) { Float a = cosTheta_i * cosTheta_o / v, b = sinTheta_i * sinTheta_o / v; Float mp = (v <= .1) ? (FastExp(LogI0(a) - b - 1 / v + 0.6931f + std::log(1 / (2 * v)))) : (FastExp(-b) * I0(a)) / (std::sinh(1 / v) * 2 * v); return mp; }

One challenge with this model is choosing a roughness v to achieve a desired look. Here we have implemented a perceptually uniform mapping from roughness beta Subscript m Baseline element-of left-bracket 0 comma 1 right-bracket to v where a roughness of 0 is nearly perfectly smooth and 1 is extremely rough. Different roughness values are used for different values of p . For p equals 1 , roughness is reduced by an empirical factor that models the focusing of light due to refraction through the circular boundary of the hair. It is then increased for p equals 2 and subsequent terms, which models the effect of light spreading out after multiple reflections at the rough cylinder boundary in the interior of the hair.

<<HairBxDF constructor implementation>>= 
v[0] = Sqr(0.726f * beta_m + 0.812f * Sqr(beta_m) + 3.7f * Pow<20>(beta_m)); v[1] = .25 * v[0]; v[2] = 4 * v[0]; for (int p = 3; p <= pMax; ++p) v[p] = v[2];

<<HairBxDF Private Members>>+=  
Float v[pMax + 1];

9.9.4 Absorption in Fibers

The upper A Subscript p factor describes how the incident light is affected by each of the scattering modes p . It incorporates two effects: Fresnel reflection and transmission at the hair–air boundary and absorption of light that passes through the hair (for p greater-than 0 ). Figure 9.47 has rendered images of hair with varying absorption coefficients, showing the effect of absorption. The upper A Subscript p function that we have implemented models all reflection and transmission at the hair boundary as perfect specular—a very different assumption than upper M Subscript p and upper N Subscript p (to come), which model glossy reflection and transmission. This assumption simplifies the implementation and gives reasonable results in practice (presumably in that the specular paths are, in a sense, averages over all the possible glossy paths).

Figure 9.47: Hair Rendered with Various Absorption Coefficients. In all cases, beta Subscript m Baseline equals 0.25 and beta Subscript n Baseline equals 0.3 . (a) sigma Subscript normal a Baseline equals left-parenthesis 3.35 comma 5.58 comma 10.96 right-parenthesis (RGB coefficients): in black hair, almost all transmitted light is absorbed. The white specular highlight from the p equals 0 term is the main visual feature. (b) sigma Subscript normal a Baseline equals left-parenthesis 0.84 comma 1.39 comma 2.74 right-parenthesis , giving brown hair, where the p greater-than 1 terms all introduce color to the hair. (c) With a very low absorption coefficient of sigma Subscript normal a Baseline equals left-parenthesis 0.06 comma 0.10 comma 0.20 right-parenthesis , we have blonde hair. (Hair geometry courtesy of Cem Yuksel.)

We will start by finding the fraction of incident light that remains after a path of a single transmitted segment through the hair. To do so, we need to find the distance the ray travels until it exits the cylinder; the easiest way to do this is to compute the distances in the longitudinal and azimuthal projections separately.

To compute these distances, we need the transmitted angles of the ray omega Subscript normal o , in the longitudinal and azimuthal planes, which we will denote by theta Subscript normal t and gamma Subscript normal t , respectively. Application of Snell’s law using the hair’s index of refraction eta allows us to compute sine theta Subscript normal t and cosine theta Subscript normal t .

<<Compute cosine theta Subscript normal t for refracted ray>>= 
Float sinTheta_t = sinTheta_o / eta; Float cosTheta_t = SafeSqrt(1 - Sqr(sinTheta_t));

For gamma Subscript normal t , although we could compute the transmitted direction omega Subscript normal t from omega Subscript normal o and then project omega Subscript normal t into the normal plane, it is possible to compute gamma Subscript normal t directly using a modified index of refraction that accounts for the effect of the longitudinal angle on the refracted direction in the normal plane. The modified index of refraction is given by

eta prime equals StartFraction StartRoot eta squared minus sine squared theta Subscript normal o Baseline EndRoot Over cosine theta Subscript normal o Baseline EndFraction period

Given eta prime , we can compute the refracted direction gamma Subscript normal t directly in the normal plane. Since h equals sine gamma Subscript normal o , we can apply Snell’s law to compute gamma Subscript normal t .

<<Compute gamma Subscript normal t for refracted ray>>= 
Float etap = SafeSqrt(Sqr(eta) - Sqr(sinTheta_o)) / cosTheta_o; Float sinGamma_t = h / etap; Float cosGamma_t = SafeSqrt(1 - Sqr(sinGamma_t)); Float gamma_t = SafeASin(sinGamma_t);

Figure 9.48: Computing the Transmitted Segment’s Distance. For a transmitted ray with angle gamma Subscript normal t with respect to the circle’s surface normal, half of the total distance l Subscript a is given by cosine gamma , assuming a unit radius. Because gamma Subscript normal t is the same at both halves of the segment, l Subscript a Baseline equals 2 cosine gamma Subscript normal t .

If we consider the azimuthal projection of the transmitted ray in the normal plane, we can see that the segment makes the same angle gamma Subscript normal t with the circle normal at both of its endpoints (Figure 9.48). If we denote the total length of the segment by l Subscript a , then basic trigonometry tells us that l Subscript a Baseline slash 2 equals cosine gamma Subscript normal t , assuming a unit radius circle.

Figure 9.49: The Effect of theta Subscript normal t on the Transmitted Segment’s Length. The length of the transmitted segment through the cylinder is increased by a factor of 1 slash cosine theta Subscript normal t versus a direct vertical path.

Now considering the longitudinal projection, we can see that the distance that a transmitted ray travels before exiting is scaled by a factor of 1 slash cosine theta Subscript normal t as it passes through the cylinder (Figure 9.49). Putting these together, the total segment length in terms of the hair diameter is

l equals StartFraction 2 cosine gamma Subscript normal t Baseline Over cosine theta Subscript normal t Baseline EndFraction period

Given the segment length and the medium’s absorption coefficient, the fraction of light transmitted can be computed using Beer’s law, which is introduced in Section 11.2. Because the HairBxDF defined sigma Subscript normal a to be measured with respect to the hair diameter (so that adjusting the hair geometry’s width does not completely change its color), we do not consider the hair cylinder diameter when we apply Beer’s law, and the fraction of light remaining at the end of the segment is given by

upper T Subscript normal r Baseline equals normal e Superscript minus sigma Super Subscript normal a Superscript l Baseline period

<<Compute the transmittance T of a single path through the cylinder>>= 
SampledSpectrum T = Exp(-sigma_a * (2 * cosGamma_t / cosTheta_t));

Given a single segment’s transmittance, we can now describe the function that evaluates the full upper A Subscript p function. Ap() returns an array with the values of upper A Subscript p up to p Subscript normal m normal a normal x and a final value that is the sum of attenuations for all the higher-order scattering terms.

<<HairBxDF Private Methods>>+=  
pstd::array<SampledSpectrum, pMax + 1> Ap(Float cosTheta_o, Float eta, Float h, SampledSpectrum T) { pstd::array<SampledSpectrum, pMax + 1> ap; <<Compute p equals 0 attenuation at initial cylinder intersection>> 
Float cosGamma_o = SafeSqrt(1 - Sqr(h)); Float cosTheta = cosTheta_o * cosGamma_o; Float f = FrDielectric(cosTheta, eta); ap[0] = SampledSpectrum(f);
<<Compute p equals 1 attenuation term>> 
ap[1] = Sqr(1 - f) * T;
<<Compute attenuation terms up to p equals monospace p monospace upper M monospace a monospace x >> 
for (int p = 2; p < pMax; ++p) ap[p] = ap[p - 1] * T * f;
<<Compute attenuation term accounting for remaining orders of scattering>> 
if (1 - T * f) ap[pMax] = ap[pMax - 1] * f * T / (1 - T * f);
return ap; }

For the upper A 0 term, corresponding to light that reflects at the cuticle, the Fresnel reflectance at the air–hair boundary gives the fraction of light that is reflected. We can find the cosine of the angle between the surface normal and the direction vector with angles theta Subscript normal o and gamma Subscript normal o in the hair coordinate system by cosine theta Subscript normal o cosine gamma Subscript normal o .

<<Compute p equals 0 attenuation at initial cylinder intersection>>= 
Float cosGamma_o = SafeSqrt(1 - Sqr(h)); Float cosTheta = cosTheta_o * cosGamma_o; Float f = FrDielectric(cosTheta, eta); ap[0] = SampledSpectrum(f);

For the upper T upper T term, p equals 1 , we have two 1 minus f factors, accounting for transmission into and out of the cuticle boundary, and a single upper T factor for one transmission path through the hair.

<<Compute p equals 1 attenuation term>>= 
ap[1] = Sqr(1 - f) * T;

Figure 9.50: When a transmitted ray undergoes specular reflection at the interior of the hair cylinder, it makes the same angle gamma Subscript normal t with the circle’s surface normal as the original transmitted ray did. From this, it follows that the lengths of all ray segments for a path inside the cylinder must be equal.

The p equals 2 term has one more reflection event, reflecting light back into the hair, and then a second transmission term. Since we assume perfect specular reflection at the cuticle boundary, both segments inside the hair make the same angle gamma Subscript normal t with the circle’s normal (Figure 9.50). From this, we can see that both segments must have the same length (and so forth for subsequent segments). In general, for p greater-than 0 ,

upper A Subscript p Baseline equals upper A Subscript p minus 1 Baseline upper T f equals left-parenthesis 1 minus f right-parenthesis squared upper T Superscript p Baseline f Superscript p minus 1 Baseline period

<<Compute attenuation terms up to p equals monospace p monospace upper M monospace a monospace x >>= 
for (int p = 2; p < pMax; ++p) ap[p] = ap[p - 1] * T * f;

After pMax, a final term accounts for all further orders of scattering. The sum of the infinite series of remaining terms can fortunately be found in closed form, since both upper T less-than 1 and f less-than 1 :

sigma-summation Underscript p equals p Subscript normal m normal a normal x Baseline Overscript normal infinity Endscripts left-parenthesis 1 minus f right-parenthesis squared upper T Superscript p Baseline f Superscript p minus 1 Baseline equals StartFraction left-parenthesis 1 minus f right-parenthesis squared upper T Superscript p Super Subscript normal m normal a normal x Superscript Baseline f Superscript p Super Subscript normal m normal a normal x Superscript minus 1 Baseline Over 1 minus upper T f EndFraction period

<<Compute attenuation term accounting for remaining orders of scattering>>= 
if (1 - T * f) ap[pMax] = ap[pMax - 1] * f * T / (1 - T * f);

9.9.5 Azimuthal Scattering

Figure 9.51: For specular reflection, with p equals 0 , the incident and reflected directions make the same angle gamma Subscript normal o with the surface normal. The net change in angle is thus minus 2 gamma Subscript normal o . For p equals 1 , the ray is deflected from gamma Subscript normal o to gamma Subscript normal t when it enters the cylinder and then correspondingly on the way out. We can also see that when the ray is transmitted again out of the circle, it makes an angle gamma Subscript normal o with the surface normal there. Adding up the angles, the net deflection is 2 gamma Subscript normal t minus 2 gamma Subscript normal o plus pi .

Finally, we will model the component of scattering dependent on the angle phi . We will do this work entirely in the normal plane. The azimuthal scattering model is based on first computing a new azimuthal direction assuming perfect specular reflection and transmission and then defining a distribution of directions around this central direction, where increasing roughness gives a wider distribution. Therefore, we will first consider how an incident ray is deflected by specular reflection and transmission in the normal plane; Figure 9.51 illustrates the cases for the first two values of p .

Following the reasoning from Figure 9.51, we can derive the function normal upper Phi , which gives the net change in azimuthal direction:

normal upper Phi left-parenthesis p comma h right-parenthesis equals 2 p gamma Subscript normal t Baseline minus 2 gamma Subscript normal o Baseline plus p pi period

(Recall that gamma Subscript normal o and gamma Subscript normal t are derived from h .) Figure 9.52 shows a plot of this function for p equals 1 .

Figure 9.52: Plot of normal upper Phi left-parenthesis p comma h right-parenthesis for p equals 1 . As h varies from negative 1 to 1 , we can see that the range of orientations phi for the specularly transmitted ray varies rapidly. By examining the range of phi values, we can see that the possible transmitted directions cover roughly 2 slash 3 of all possible directions on the circle.

<<HairBxDF Private Methods>>+=  
Float Phi(int p, Float gamma_o, Float gamma_t) { return 2 * p * gamma_t - 2 * gamma_o + p * Pi; }

Now that we know how to compute new angles in the normal plane after specular transmission and reflection, we need a way to represent surface roughness, so that a range of directions centered around the specular direction can contribute to scattering. The logistic distribution provides a good option: it is a generally useful one for rendering, since it has a similar shape to the Gaussian, while also being normalized and integrable in closed form (unlike the Gaussian); see Section B.2.5 for more information.

Figure 9.53: Plots of the Trimmed Logistic Function over plus-or-minus pi . The curve for s equals 0.5 (blue line) is broad and flat, while at s equals 0.1 (red line), the curve is peaked. Because the function is normalized, the peak at 0 generally does not have the value 1, unlike the Gaussian.

In the following, we will find it useful to define a normalized logistic function over a range left-bracket a comma b right-bracket ; we will call this the trimmed logistic, l Subscript normal t .

l Subscript normal t Baseline left-parenthesis x comma s comma left-bracket a comma b right-bracket right-parenthesis equals StartFraction l left-parenthesis x comma s right-parenthesis Over integral Subscript a Superscript b Baseline l left-parenthesis x prime comma s right-parenthesis normal d x Superscript prime Baseline EndFraction period

Figure 9.53 shows plots of the trimmed logistic distribution for a few values of s .

Now we have the pieces to be able to implement the azimuthal scattering distribution. The Np() function computes the upper N Subscript p term, finding the angular difference between phi and normal upper Phi left-parenthesis p comma h right-parenthesis and evaluating the azimuthal distribution with that angle.

<<HairBxDF Private Methods>>+= 
Float Np(Float phi, int p, Float s, Float gamma_o, Float gamma_t) { Float dphi = phi - Phi(p, gamma_o, gamma_t); <<Remap dphi to left-bracket negative pi comma pi right-bracket >> 
while (dphi > Pi) dphi -= 2 * Pi; while (dphi < -Pi) dphi += 2 * Pi;
return TrimmedLogistic(dphi, s, -Pi, Pi); }

The difference between phi and normal upper Phi left-parenthesis p comma h right-parenthesis may be outside the range we have defined the logistic over, left-bracket negative pi comma pi right-bracket , so we rotate around the circle as needed to get the value to the right range. Because dphi never gets too far out of range for the small p used here, we use the simple approach of adding or subtracting 2 pi as needed.

<<Remap dphi to left-bracket negative pi comma pi right-bracket >>= 
while (dphi > Pi) dphi -= 2 * Pi; while (dphi < -Pi) dphi += 2 * Pi;

As with the longitudinal roughness, it is helpful to have a roughly perceptually linear mapping from azimuthal roughness beta Subscript n Baseline element-of left-bracket 0 comma 1 right-bracket to the logistic scale factor s .

<<HairBxDF constructor implementation>>+=  
static const Float SqrtPiOver8 = 0.626657069f; s = SqrtPiOver8 * (0.265f * beta_n + 1.194f * Sqr(beta_n) + 5.372f * Pow<22>(beta_n));

<<HairBxDF Private Members>>+=  
Float s;

Figure 9.54: Polar plots of upper N Subscript p for p equals 1 , theta Subscript normal o aligned with the negative x axis, and with a low roughness, beta Subscript n Baseline equals 0.1 , for (blue)  h equals negative 0.5 and (red)  h equals 0.3 . We can see that upper N Subscript p varies rapidly over the width of the hair.

Figure 9.54 shows polar plots of azimuthal scattering for the upper T upper T term, p equals 1 , with a fairly low roughness. The scattering distributions for the two different points on the curve’s width are quite different. Because we expect the hair width to be roughly pixel-sized, many rays per pixel are needed to resolve this variation well.

9.9.6 Scattering Model Evaluation

We now have almost all the pieces we need to be able to evaluate the model. The last detail is to account for the effect of scales on the hair surface (recall Figure 9.43). Suitable adjustments to theta Subscript normal o work well to model this characteristic of hair.

For the upper R term, adding the angle 2 alpha to theta Subscript normal o can model the effect of evaluating the hair scattering model with respect to the surface normal of a scale. We can then go ahead and evaluate upper M 0 with this modification to theta Subscript normal o . For upper T upper T , we have to account for two transmission events through scales. Rotating by alpha in the opposite direction approximately compensates. (Because the refraction angle is nonlinear with respect to changes in normal orientation, there is some error in this approximation, though the error is low for the typical case of small values of  alpha .) upper T upper R upper T has a reflection term inside the hair; a rotation by minus 4 alpha compensates for the overall effect.

The effects of these shifts are that the primary reflection lobe upper R is offset to be above the perfect specular direction and the secondary upper T upper R upper T lobe is shifted below it. Together, these lead to two distinct specular highlights of different colors, since upper R is not affected by the hair’s color, while upper T upper R upper T picks up the hair color due to absorption. This effect can be seen in human hair and is evident in the images in Figure 9.45, for example.

Because we only need the sine and cosine of the angle theta Subscript normal i to evaluate upper M Subscript p , we can use the trigonometric identities

StartLayout 1st Row 1st Column sine left-parenthesis theta Subscript normal o Baseline plus-or-minus alpha right-parenthesis 2nd Column equals sine theta Subscript normal o Baseline cosine alpha plus-or-minus cosine theta Subscript normal o Baseline sine alpha 2nd Row 1st Column cosine left-parenthesis theta Subscript normal o Baseline plus-or-minus alpha right-parenthesis 2nd Column equals cosine theta Subscript normal o Baseline cosine alpha minus-or-plus sine theta Subscript normal o Baseline sine alpha EndLayout

to efficiently compute the rotated angles without needing to evaluate any additional trigonometric functions. The HairBxDF constructor therefore precomputes sine left-parenthesis 2 Superscript k Baseline alpha right-parenthesis and cosine left-parenthesis 2 Superscript k Baseline alpha right-parenthesis for k equals 0 comma 1 comma 2 . These values can be computed particularly efficiently using trigonometric double angle identities: cosine 2 theta equals cosine squared theta minus sine squared theta and sine 2 theta equals 2 cosine theta sine theta .

<<HairBxDF constructor implementation>>+= 
sin2kAlpha[0] = std::sin(Radians(alpha)); cos2kAlpha[0] = SafeSqrt(1 - Sqr(sin2kAlpha[0])); for (int i = 1; i < pMax; ++i) { sin2kAlpha[i] = 2 * cos2kAlpha[i - 1] * sin2kAlpha[i - 1]; cos2kAlpha[i] = Sqr(cos2kAlpha[i - 1]) - Sqr(sin2kAlpha[i - 1]); }

<<HairBxDF Private Members>>+= 
Float sin2kAlpha[pMax], cos2kAlpha[pMax];

Evaluating the model is now mostly just a matter of calling functions that have already been defined and summing the individual terms f Subscript p .

<<Evaluate hair BSDF>>= 
Float phi = phi_i - phi_o; pstd::array<SampledSpectrum, pMax + 1> ap = Ap(cosTheta_o, eta, h, T); SampledSpectrum fsum(0.); for (int p = 0; p < pMax; ++p) { <<Compute sine theta Subscript normal o and cosine theta Subscript normal o terms accounting for scales>> 
Float sinThetap_o, cosThetap_o; if (p == 0) { sinThetap_o = sinTheta_o * cos2kAlpha[1] - cosTheta_o * sin2kAlpha[1]; cosThetap_o = cosTheta_o * cos2kAlpha[1] + sinTheta_o * sin2kAlpha[1]; } <<Handle remainder of p values for hair scale tilt>> 
else if (p == 1) { sinThetap_o = sinTheta_o * cos2kAlpha[0] + cosTheta_o * sin2kAlpha[0]; cosThetap_o = cosTheta_o * cos2kAlpha[0] - sinTheta_o * sin2kAlpha[0]; } else if (p == 2) { sinThetap_o = sinTheta_o * cos2kAlpha[2] + cosTheta_o * sin2kAlpha[2]; cosThetap_o = cosTheta_o * cos2kAlpha[2] - sinTheta_o * sin2kAlpha[2]; } else { sinThetap_o = sinTheta_o; cosThetap_o = cosTheta_o; }
<<Handle out-of-range cosine theta Subscript normal o from scale adjustment>> 
cosThetap_o = std::abs(cosThetap_o);
fsum += Mp(cosTheta_i, cosThetap_o, sinTheta_i, sinThetap_o, v[p]) * ap[p] * Np(phi, p, s, gamma_o, gamma_t); } <<Compute contribution of remaining terms after pMax>> 
fsum += Mp(cosTheta_i, cosTheta_o, sinTheta_i, sinTheta_o, v[pMax]) * ap[pMax] / (2 * Pi);
if (AbsCosTheta(wi) > 0) fsum /= AbsCosTheta(wi); return fsum;

The rotations that account for the effect of scales are implemented using the trigonometric identities listed above. Here is the code for the p equals 0 case, where theta Subscript normal o is rotated by 2 alpha . The remaining cases follow the same structure. (The rotation is by negative alpha for p equals 1 and by minus 4 alpha for p equals 2 .)

<<Compute sine theta Subscript normal o and cosine theta Subscript normal o terms accounting for scales>>= 
Float sinThetap_o, cosThetap_o; if (p == 0) { sinThetap_o = sinTheta_o * cos2kAlpha[1] - cosTheta_o * sin2kAlpha[1]; cosThetap_o = cosTheta_o * cos2kAlpha[1] + sinTheta_o * sin2kAlpha[1]; } <<Handle remainder of p values for hair scale tilt>> 
else if (p == 1) { sinThetap_o = sinTheta_o * cos2kAlpha[0] + cosTheta_o * sin2kAlpha[0]; cosThetap_o = cosTheta_o * cos2kAlpha[0] - sinTheta_o * sin2kAlpha[0]; } else if (p == 2) { sinThetap_o = sinTheta_o * cos2kAlpha[2] + cosTheta_o * sin2kAlpha[2]; cosThetap_o = cosTheta_o * cos2kAlpha[2] - sinTheta_o * sin2kAlpha[2]; } else { sinThetap_o = sinTheta_o; cosThetap_o = cosTheta_o; }
<<Handle out-of-range cosine theta Subscript normal o from scale adjustment>> 
cosThetap_o = std::abs(cosThetap_o);

When omega Subscript normal i is nearly parallel with the hair, the scale adjustment may give a slightly negative value for cosine theta Subscript normal i —effectively, in this case, it represents a theta Subscript normal i that is slightly greater than pi slash 2 , the maximum expected value of theta in the hair coordinate system. This angle is equivalent to pi minus theta Subscript normal i , and cosine left-parenthesis pi minus theta Subscript normal i Baseline right-parenthesis equals StartAbsoluteValue cosine theta Subscript normal i Baseline EndAbsoluteValue , so we can easily handle that here.

<<Handle out-of-range cosine theta Subscript normal o from scale adjustment>>= 
cosThetap_o = std::abs(cosThetap_o);

A final term accounts for all higher-order scattering inside the hair. We just use a uniform distribution upper N left-parenthesis phi right-parenthesis equals 1 slash left-parenthesis 2 pi right-parenthesis for the azimuthal distribution; this is a reasonable choice, as the direction offsets from normal upper Phi left-parenthesis p comma h right-parenthesis for p greater-than-or-equal-to p Subscript normal m normal a normal x generally have wide variation and the final upper A Subscript p term generally represents less than 15% of the overall scattering, so little error is introduced in the final result.

<<Compute contribution of remaining terms after pMax>>= 
fsum += Mp(cosTheta_i, cosTheta_o, sinTheta_i, sinTheta_o, v[pMax]) * ap[pMax] / (2 * Pi);

A Note on Reciprocity

Although we included reciprocity in the properties of physically valid BRDFs in Section 4.3.1, the model we have implemented in this section is, unfortunately, not reciprocal. An immediate issue is that the rotation for hair scales is applied only to theta Subscript normal i . However, there are more problems: first, all terms p greater-than 0 that involve transmission are not reciprocal since the transmission terms use values based on omega Subscript normal t , which itself only depends on omega Subscript normal o . Thus, if omega Subscript normal o and omega Subscript normal i are interchanged, a completely different omega Subscript normal t is computed, which in turn leads to different cosine theta Subscript normal t and gamma Subscript normal t values, which in turn give different values from the upper A Subscript p and upper N Subscript p functions. In practice, however, we have not observed artifacts in images from these shortcomings.

9.9.7 Sampling

Being able to generate sampled directions and compute the PDF for sampling a given direction according to a distribution that is similar to the overall BSDF is critical for efficient rendering, especially at low roughnesses, where the hair BSDF varies rapidly as a function of direction. In the approach implemented here, samples are generated with a two-step process: first we choose a p term to sample according to a probability based on each term’s upper A Subscript p function value, which gives its contribution to the overall scattering function. Then, we find a direction by sampling the corresponding upper M Subscript p and upper N Subscript p terms. Fortunately, both the upper M Subscript p and upper N Subscript p terms of the hair BSDF can be sampled perfectly, leaving us with a sampling scheme that exactly matches the PDF of the full BSDF.

We will first define the ApPDF() method, which returns a discrete PDF with probabilities for sampling each term upper A Subscript p according to its contribution relative to all the upper A Subscript p terms, given  theta Subscript normal o .

<<HairBxDF Method Definitions>>+=  
pstd::array<Float, HairBxDF::pMax + 1> HairBxDF::ApPDF(Float cosTheta_o) const { <<Initialize array of upper A Subscript p values for cosTheta_o>> 
Float sinTheta_o = SafeSqrt(1 - Sqr(cosTheta_o)); <<Compute cosine theta Subscript normal t for refracted ray>> 
Float sinTheta_t = sinTheta_o / eta; Float cosTheta_t = SafeSqrt(1 - Sqr(sinTheta_t));
<<Compute gamma Subscript normal t for refracted ray>> 
Float etap = SafeSqrt(Sqr(eta) - Sqr(sinTheta_o)) / cosTheta_o; Float sinGamma_t = h / etap; Float cosGamma_t = SafeSqrt(1 - Sqr(sinGamma_t)); Float gamma_t = SafeASin(sinGamma_t);
<<Compute the transmittance T of a single path through the cylinder>> 
SampledSpectrum T = Exp(-sigma_a * (2 * cosGamma_t / cosTheta_t));
pstd::array<SampledSpectrum, pMax + 1> ap = Ap(cosTheta_o, eta, h, T);
<<Compute upper A Subscript p PDF from individual upper A Subscript p terms>> 
pstd::array<Float, pMax + 1> apPDF; Float sumY = 0; for (const SampledSpectrum &as : ap) sumY += as.Average(); for (int i = 0; i <= pMax; ++i) apPDF[i] = ap[i].Average() / sumY;
return apPDF; }

The method starts by computing the values of upper A Subscript p for cosTheta_o. We are able to reuse some previously defined fragments to make this task easier.

<<Initialize array of upper A Subscript p values for cosTheta_o>>= 
Float sinTheta_o = SafeSqrt(1 - Sqr(cosTheta_o)); <<Compute cosine theta Subscript normal t for refracted ray>> 
Float sinTheta_t = sinTheta_o / eta; Float cosTheta_t = SafeSqrt(1 - Sqr(sinTheta_t));
<<Compute gamma Subscript normal t for refracted ray>> 
Float etap = SafeSqrt(Sqr(eta) - Sqr(sinTheta_o)) / cosTheta_o; Float sinGamma_t = h / etap; Float cosGamma_t = SafeSqrt(1 - Sqr(sinGamma_t)); Float gamma_t = SafeASin(sinGamma_t);
<<Compute the transmittance T of a single path through the cylinder>> 
SampledSpectrum T = Exp(-sigma_a * (2 * cosGamma_t / cosTheta_t));
pstd::array<SampledSpectrum, pMax + 1> ap = Ap(cosTheta_o, eta, h, T);

Next, the spectral upper A Subscript p values are converted to scalars using their luminance and these values are normalized to make a proper PDF.

<<Compute upper A Subscript p PDF from individual upper A Subscript p terms>>= 
pstd::array<Float, pMax + 1> apPDF; Float sumY = 0; for (const SampledSpectrum &as : ap) sumY += as.Average(); for (int i = 0; i <= pMax; ++i) apPDF[i] = ap[i].Average() / sumY;

With these preliminaries out of the way, we can now implement the Sample_f() method.

<<HairBxDF Method Definitions>>+=  
pstd::optional<BSDFSample> HairBxDF::Sample_f(Vector3f wo, Float uc, Point2f u, TransportMode mode, BxDFReflTransFlags sampleFlags) const { <<Compute hair coordinate system terms related to wo>> 
Float sinTheta_o = wo.x; Float cosTheta_o = SafeSqrt(1 - Sqr(sinTheta_o)); Float phi_o = std::atan2(wo.z, wo.y); Float gamma_o = SafeASin(h);
<<Determine which term p to sample for hair scattering>> 
pstd::array<Float, pMax + 1> apPDF = ApPDF(cosTheta_o); int p = SampleDiscrete(apPDF, uc, nullptr, &uc);
<<Compute sine theta Subscript normal o and cosine theta Subscript normal o terms accounting for scales>> 
Float sinThetap_o, cosThetap_o; if (p == 0) { sinThetap_o = sinTheta_o * cos2kAlpha[1] - cosTheta_o * sin2kAlpha[1]; cosThetap_o = cosTheta_o * cos2kAlpha[1] + sinTheta_o * sin2kAlpha[1]; } <<Handle remainder of p values for hair scale tilt>> 
else if (p == 1) { sinThetap_o = sinTheta_o * cos2kAlpha[0] + cosTheta_o * sin2kAlpha[0]; cosThetap_o = cosTheta_o * cos2kAlpha[0] - sinTheta_o * sin2kAlpha[0]; } else if (p == 2) { sinThetap_o = sinTheta_o * cos2kAlpha[2] + cosTheta_o * sin2kAlpha[2]; cosThetap_o = cosTheta_o * cos2kAlpha[2] - sinTheta_o * sin2kAlpha[2]; } else { sinThetap_o = sinTheta_o; cosThetap_o = cosTheta_o; }
<<Handle out-of-range cosine theta Subscript normal o from scale adjustment>> 
cosThetap_o = std::abs(cosThetap_o);
<<Sample upper M Subscript p to compute theta Subscript normal i >> 
Float cosTheta = 1 + v[p] * std::log(std::max<Float>(u[0], 1e-5) + (1 - u[0]) * FastExp(-2 / v[p])); Float sinTheta = SafeSqrt(1 - Sqr(cosTheta)); Float cosPhi = std::cos(2 * Pi * u[1]); Float sinTheta_i = -cosTheta * sinThetap_o + sinTheta * cosPhi * cosThetap_o; Float cosTheta_i = SafeSqrt(1 - Sqr(sinTheta_i));
<<Sample upper N Subscript p to compute normal upper Delta phi >> 
<<Compute gamma Subscript normal t for refracted ray>> 
Float etap = SafeSqrt(Sqr(eta) - Sqr(sinTheta_o)) / cosTheta_o; Float sinGamma_t = h / etap; Float cosGamma_t = SafeSqrt(1 - Sqr(sinGamma_t)); Float gamma_t = SafeASin(sinGamma_t);
Float dphi; if (p < pMax) dphi = Phi(p, gamma_o, gamma_t) + SampleTrimmedLogistic(uc, s, -Pi, Pi); else dphi = 2 * Pi * uc;
<<Compute wi from sampled hair scattering angles>> 
Float phi_i = phi_o + dphi; Vector3f wi(sinTheta_i, cosTheta_i * std::cos(phi_i), cosTheta_i * std::sin(phi_i));
<<Compute PDF for sampled hair scattering direction wi>> 
Float pdf = 0; for (int p = 0; p < pMax; ++p) { <<Compute sine theta Subscript normal o and cosine theta Subscript normal o terms accounting for scales>> 
Float sinThetap_o, cosThetap_o; if (p == 0) { sinThetap_o = sinTheta_o * cos2kAlpha[1] - cosTheta_o * sin2kAlpha[1]; cosThetap_o = cosTheta_o * cos2kAlpha[1] + sinTheta_o * sin2kAlpha[1]; } <<Handle remainder of p values for hair scale tilt>> 
else if (p == 1) { sinThetap_o = sinTheta_o * cos2kAlpha[0] + cosTheta_o * sin2kAlpha[0]; cosThetap_o = cosTheta_o * cos2kAlpha[0] - sinTheta_o * sin2kAlpha[0]; } else if (p == 2) { sinThetap_o = sinTheta_o * cos2kAlpha[2] + cosTheta_o * sin2kAlpha[2]; cosThetap_o = cosTheta_o * cos2kAlpha[2] - sinTheta_o * sin2kAlpha[2]; } else { sinThetap_o = sinTheta_o; cosThetap_o = cosTheta_o; }
<<Handle out-of-range cosine theta Subscript normal o from scale adjustment>> 
cosThetap_o = std::abs(cosThetap_o);
<<Handle out-of-range cosine theta Subscript normal o from scale adjustment>> 
cosThetap_o = std::abs(cosThetap_o);
pdf += Mp(cosTheta_i, cosThetap_o, sinTheta_i, sinThetap_o, v[p]) * apPDF[p] * Np(dphi, p, s, gamma_o, gamma_t); } pdf += Mp(cosTheta_i, cosTheta_o, sinTheta_i, sinTheta_o, v[pMax]) * apPDF[pMax] * (1 / (2 * Pi));
return BSDFSample(f(wo, wi, mode), wi, pdf, Flags()); }

Given the PDF over upper A Subscript p terms, a call to SampleDiscrete() takes care of choosing one. Because we only need to generate one sample from the PDF’s distribution, the work to compute an explicit CDF array (for example, by using PiecewiseConstant1D) is not worthwhile. Note that we take advantage of SampleDiscrete()’s optional capability of returning a fresh uniform random sample, overwriting the value in uc. This sample value will be used shortly for sampling upper N Subscript p .

<<Determine which term p to sample for hair scattering>>= 
pstd::array<Float, pMax + 1> apPDF = ApPDF(cosTheta_o); int p = SampleDiscrete(apPDF, uc, nullptr, &uc);

We can now sample the corresponding upper M Subscript p term given theta Subscript normal o to find theta Subscript normal i . The derivation of this sampling method is fairly involved, so we will include neither the derivation nor the implementation here. This fragment, <<Sample upper M Subscript p to compute theta Subscript normal i >>, consumes both of the sample values u[0] and u[1] and initializes variables sinTheta_i and cosTheta_i according to the sampled direction.

<<Sample upper M Subscript p to compute theta Subscript normal i >>= 
Float cosTheta = 1 + v[p] * std::log(std::max<Float>(u[0], 1e-5) + (1 - u[0]) * FastExp(-2 / v[p])); Float sinTheta = SafeSqrt(1 - Sqr(cosTheta)); Float cosPhi = std::cos(2 * Pi * u[1]); Float sinTheta_i = -cosTheta * sinThetap_o + sinTheta * cosPhi * cosThetap_o; Float cosTheta_i = SafeSqrt(1 - Sqr(sinTheta_i));

Next we will sample the azimuthal distribution upper N Subscript p . For terms up to p Subscript normal m normal a normal x , we take a sample from the logistic distribution centered around the exit direction given by normal upper Phi left-parenthesis p comma h right-parenthesis . For the last term, we sample from a uniform distribution.

<<Sample upper N Subscript p to compute normal upper Delta phi >>= 
<<Compute gamma Subscript normal t for refracted ray>> 
Float etap = SafeSqrt(Sqr(eta) - Sqr(sinTheta_o)) / cosTheta_o; Float sinGamma_t = h / etap; Float cosGamma_t = SafeSqrt(1 - Sqr(sinGamma_t)); Float gamma_t = SafeASin(sinGamma_t);
Float dphi; if (p < pMax) dphi = Phi(p, gamma_o, gamma_t) + SampleTrimmedLogistic(uc, s, -Pi, Pi); else dphi = 2 * Pi * uc;

Given theta Subscript normal i and phi Subscript normal i , we can compute the sampled direction wi. The math is similar to that used in the SphericalDirection() function, but with two important differences. First, because here theta is measured with respect to the plane perpendicular to the cylinder rather than the cylinder axis, we need to compute cosine left-parenthesis pi slash 2 minus theta right-parenthesis equals sine theta for the coordinate with respect to the cylinder axis instead of cosine theta . Second, because the hair shading coordinate system’s left-parenthesis theta comma phi right-parenthesis coordinates are oriented with respect to the plus x axis, the order of dimensions passed to the Vector3f constructor is adjusted correspondingly, since the direction returned from Sample_f() should be in the BSDF coordinate system.

<<Compute wi from sampled hair scattering angles>>= 
Float phi_i = phi_o + dphi; Vector3f wi(sinTheta_i, cosTheta_i * std::cos(phi_i), cosTheta_i * std::sin(phi_i));

Because we could sample directly from the upper M Subscript p and upper N Subscript p distributions, the overall PDF is

sigma-summation Underscript p equals 0 Overscript p Subscript normal m normal a normal x Baseline Endscripts upper M Subscript p Baseline left-parenthesis theta Subscript normal o Baseline comma theta Subscript normal i Baseline right-parenthesis ModifyingAbove upper A With tilde Subscript p Baseline left-parenthesis omega Subscript normal o Baseline right-parenthesis upper N Subscript p Baseline left-parenthesis phi right-parenthesis comma

where upper A overTilde Subscript p are the normalized PDF terms. Note that theta Subscript normal o must be shifted to account for hair scales when evaluating the PDF; this is done in the same way (and with the same code fragment) as with BSDF evaluation.

<<Compute PDF for sampled hair scattering direction wi>>= 
Float pdf = 0; for (int p = 0; p < pMax; ++p) { <<Compute sine theta Subscript normal o and cosine theta Subscript normal o terms accounting for scales>> 
Float sinThetap_o, cosThetap_o; if (p == 0) { sinThetap_o = sinTheta_o * cos2kAlpha[1] - cosTheta_o * sin2kAlpha[1]; cosThetap_o = cosTheta_o * cos2kAlpha[1] + sinTheta_o * sin2kAlpha[1]; } <<Handle remainder of p values for hair scale tilt>> 
else if (p == 1) { sinThetap_o = sinTheta_o * cos2kAlpha[0] + cosTheta_o * sin2kAlpha[0]; cosThetap_o = cosTheta_o * cos2kAlpha[0] - sinTheta_o * sin2kAlpha[0]; } else if (p == 2) { sinThetap_o = sinTheta_o * cos2kAlpha[2] + cosTheta_o * sin2kAlpha[2]; cosThetap_o = cosTheta_o * cos2kAlpha[2] - sinTheta_o * sin2kAlpha[2]; } else { sinThetap_o = sinTheta_o; cosThetap_o = cosTheta_o; }
<<Handle out-of-range cosine theta Subscript normal o from scale adjustment>> 
cosThetap_o = std::abs(cosThetap_o);
<<Handle out-of-range cosine theta Subscript normal o from scale adjustment>> 
cosThetap_o = std::abs(cosThetap_o);
pdf += Mp(cosTheta_i, cosThetap_o, sinTheta_i, sinThetap_o, v[p]) * apPDF[p] * Np(dphi, p, s, gamma_o, gamma_t); } pdf += Mp(cosTheta_i, cosTheta_o, sinTheta_i, sinTheta_o, v[pMax]) * apPDF[pMax] * (1 / (2 * Pi));

The HairBxDF::PDF() method performs the same computation and therefore the implementation is not included here.

9.9.8 Hair Absorption Coefficients

The color of hair is determined by how pigments in the cortex absorb light, which in turn is described by the normalized absorption coefficient where distance is measured in terms of the hair diameter. If a specific hair color is desired, there is a non-obvious relationship between the normalized absorption coefficient and the color of hair in a rendered image. Not only does changing the spectral values of the absorption coefficient have an unpredictable connection to the appearance of a single hair, but multiple scattering between collections of many hairs has a significant effect on each one’s apparent color. Therefore, pbrt provides implementations of two more intuitive ways to specify hair color.

The first is based on the fact that the color of human hair is determined by the concentration of two pigments. The concentration of eumelanin is the primary factor that causes the difference between black, brown, and blonde hair. (Black hair has the most eumelanin and blonde hair has the least. White hair has none.) The second pigment, pheomelanin, causes hair to be orange or red. The HairBxDF class provides a convenience method that computes an absorption coefficient using the product of user-supplied pigment concentrations and absorption coefficients of the pigments.

<<HairBxDF Method Definitions>>+= 
RGBUnboundedSpectrum HairBxDF::SigmaAFromConcentration(Float ce, Float cp) { RGB eumelaninSigma_a(0.419f, 0.697f, 1.37f); RGB pheomelaninSigma_a(0.187f, 0.4f, 1.05f); RGB sigma_a = ce * eumelaninSigma_a + cp * pheomelaninSigma_a; return RGBUnboundedSpectrum(*RGBColorSpace::sRGB, sigma_a); }

Figure 9.55: The Importance of Multiple Scattering in Blonde Hair. (a) Blonde hair rendered with up to three bounces of light inside the hair. (b) Rendered with up to fifty bounces of light. In light-colored hair, light that has been scattered many times makes an important contribution to its visual appearance. Accurately rendering very blonde or white hair is consequently more computationally intensive than rendering dark hair. (Hair geometry courtesy of Cem Yuksel.)

Eumelanin concentrations of roughly 8, 1.3, and 0.3 give reasonable representations of black, brown, and blonde hair, respectively. Accurately rendering light-colored hair requires simulating many interreflections of light, however; see Figure 9.55.

It is also sometimes useful to specify the desired hair color directly; the SigmaAFromReflectance() method, not included here, is based on a precomputed fit of absorption coefficients to scattered hair color.