A.1 Main Include File
The core/pbrt.h file is included by all other source files in the system. It contains all global function declarations and inline functions, a few macros and numeric constants, and other globally accessible data. All files that include pbrt.h get a number of other included header files from pbrt.h. This simplifies creation of new source files, almost all of which will want access to these extra headers. However, in the interest of compile time efficiency, we keep the number of these automatically included files to a minimum; the ones here are necessary for almost all modules.
Almost all floating-point values in pbrt are declared as Floats. (The only exception is a few cases where a 32-bit float or a 64-bit double is specifically needed (e.g., when saving binary values to files). Whether a Float is actually a float or a double is determined at compile time with the PBRT_FLOAT_AS_DOUBLE macro; this makes it possible to build versions of pbrt using either representation. 32-bit floats almost always have sufficient precision for ray tracing, but it’s helpful to be able to switch to double for numerically tricky situations as well as to verify that rounding error with floats isn’t causing errors for a given scene.
Clamp() clamps the given value val to lie between the values low and high. For convenience Clamp() allows the types of the values giving the extent to be different than the type being clamped (but its implementation requires that implicit conversion is legal to the type being clamped). By being implemented this way, the implementation allows calls like Clamp(floatValue, 0, 1) which would otherwise be disallowed by C++’s template type resolution rules.
Mod() computes the remainder of . pbrt has its own version of this (rather than using %) in order to provide the behavior that the modulus of a negative number is always positive. Starting with C++11, the behavior of % has been specified to return a negative value in this case, so that the identity (a/b)*b + a%b == a holds.
A specialization for Floats calls out to the corresponding standard library function.
A number of constants, most of them related to , are used enough that it’s worth having them easily available.
Converting between Angle Measures
Two simple functions convert from angles expressed in degrees to radians, and vice versa:
Because the math library doesn’t provide a base-2 logarithm function, we provide one here, using the identity .
It’s also useful to be able to compute an integer base-2 logarithm. Rather than computing an (expensive) floating-point logarithm and converting to an integer, it’s much more efficient to count the number of leading zeros up to the first one in the 32-bit binary representation of the value and then subtract this value from 31, which gives the index of the first bit set, which is in turn the integer base-2 logarithm. (This efficiency comes in part from the fact that most CPUs have an instruction to count these zeros.)
The code here uses the __builtin_clz() intrinsic, which is available in the g++ and clang compilers; _BitScanReverse() is used to implement similar functionality with MSVC in code that isn’t shown here.
There are clever tricks that can be used to efficiently determine if a given integer is an exact power of 2, or round an integer up to the next higher (or equal) power of 2. (It’s worthwhile to take a minute and work through for yourself how these two functions work.)
A variant of RoundUpPow2() for int64_t is also provided but isn’t included in the text here.
Some of the low-discrepancy sampling code in Chapter 7 needs to efficiently count the number of trailing zeros in the binary representation of a value; CountTrailingZeros() is a wrapper around a compiler-specific intrinsic that maps to a single instruction on most architectures.
FindInterval() is a helper function that emulates the behavior of std::upper_bound(), but uses a function object to get values at various indices instead of requiring access to an actual array. This way, it becomes possible to bisect arrays that are procedurally generated, such as those interpolated from point samples. The implementation here also adds some bounds checking for corner cases (e.g., making sure that a valid interval is selected even in the case the predicate evaluates to true or false for all entries), which would normally have to follow a call to std::upper_bound().
A.1.2 Pseudo-Random Numbers
pbrt uses an implementation of the PCG pseudo-random number generator (O’Neill 2014) to generate pseudo-random numbers. This generator is one of the best random number generators currently known. Not only does it pass a variety of rigorous statistical tests that have been the bane of earlier pseudo-random number generators, but its implementation is also extremely efficient.
We wrap its implementation in a small random number generator class, RNG. Doing so allows us to use it with slightly less verbose calls throughout the rest of the system. Random number generator implementation is an esoteric art; therefore, we will not include or discuss the implementation here but will describe the APIs provided.
The RNG class provides two constructors. The first, which takes no arguments, sets the internal state to reasonable defaults. The second takes a single argument that selects a sequence of pseudo-random values.
The PCG random number generator actually allows the user to provide two 64-bit values to configure its operation: one chooses from one of different sequences of random numbers, while the second effectively selects a starting point within such a sequence. Many pseudo-random number generators only allow this second form of configuration, which alone isn’t as good: having independent non-overlapping sequences of values rather than different starting points in a single sequence provides greater non-uniformity in the generated values.
For pbrt’s needs, selecting different sequences is sufficient, so the RNG implementation doesn’t provide a mechanism to also select the starting point within a sequence.
RNGs shouldn’t be used in pbrt without either providing an initial sequence index via the constructor or a call to the SetSequence() method; otherwise there’s risk that different parts of the system will inadvertently use correlated sequences of pseudo-random values, which in turn could cause surprising errors.
There are two variants of the UniformUInt32() method. The first returns a pseudo-random number in the range .
The second returns a value uniformly distributed in the range given a bound . The last two versions of pbrt effectively used UniformUInt32() % b for this second computation. That approach is subtly flawed—in the case that b doesn’t evenly divide , then there is slightly higher probability of choosing values in the sub-range .
The implementation here first computes the above remainder efficiently using only 32 bit arithmetic and stores it in the variable threshold. Then, if the pseudo-random value returned by UniformUInt32() is less than threshold, it is discarded and a new value is generated. The resulting distribution of values has a uniform distribution after the modulus operation, giving a uniformly distributed sample value.
UniformFloat() generates a pseudo-random floating-point number in the half-open interval .