8.4 Independent Sampler

The IndependentSampler is perhaps the simplest possible (correct) implementation of the Sampler interface. It returns independent uniform sample values for each sample request without making any further effort to ensure the quality of the distribution of samples. The IndependentSampler should never be used for rendering if image quality is a concern, but it is useful for setting a baseline to compare against better samplers.

<<IndependentSampler Definition>>= 
class IndependentSampler { public: <<IndependentSampler Public Methods>> 
IndependentSampler(int samplesPerPixel, int seed = 0) : samplesPerPixel(samplesPerPixel), seed(seed) {} static IndependentSampler *Create(const ParameterDictionary &parameters, const FileLoc *loc, Allocator alloc); PBRT_CPU_GPU static constexpr const char *Name() { return "IndependentSampler"; } int SamplesPerPixel() const { return samplesPerPixel; } void StartPixelSample(Point2i p, int sampleIndex, int dimension) { rng.SetSequence(Hash(p, seed)); rng.Advance(sampleIndex * 65536ull + dimension); } Float Get1D() { return rng.Uniform<Float>(); } Point2f Get2D() { return {rng.Uniform<Float>(), rng.Uniform<Float>()}; } Point2f GetPixel2D() { return Get2D(); } Sampler Clone(Allocator alloc); std::string ToString() const;
private: <<IndependentSampler Private Members>> 
int samplesPerPixel, seed; RNG rng;
};

Like many of the following samplers, IndependentSampler takes a seed to use when initializing the pseudo-random number generator with which it produces sample values. Setting different seeds makes it possible to generate independent sets of samples across multiple runs of the renderer, which can be useful when measuring the convergence of various sampling algorithms.

<<IndependentSampler Public Methods>>= 
IndependentSampler(int samplesPerPixel, int seed = 0) : samplesPerPixel(samplesPerPixel), seed(seed) {}

An instance of the RNG class is used to generate sample coordinate values.

<<IndependentSampler Private Members>>= 
int samplesPerPixel, seed; RNG rng;

So that the IndependentSampler always gives the same sample value for a given pixel sample, it is important to reset the RNG to a deterministic state rather than, for example, leaving it in whatever state it was at the end of the last pixel sample it was used for. To do so, we take advantage of the fact that the RNG in pbrt allows not only for specifying one of 2 Superscript 64 sequences of pseudo-random values but also for specifying an offset in that sequence. The implementation below chooses a sequence deterministically, based on the pixel coordinates and seed value. Then, an initial offset into the sequence is found based on the index of the sample, so that different samples in a pixel will start far apart in the sequence. If a nonzero starting dimension is specified, it gives an additional offset into the sequence that skips over earlier dimensions.

<<IndependentSampler Public Methods>>+=  
void StartPixelSample(Point2i p, int sampleIndex, int dimension) { rng.SetSequence(Hash(p, seed)); rng.Advance(sampleIndex * 65536ull + dimension); }

Given a seeded RNG, the implementations of the methods that return 1D and 2D samples are trivial. Note that Get2D() uses C++’s uniform initialization syntax, which ensures that the two calls to Uniform() happen in a well-defined order, which in turn gives consistent results across different compilers.

<<IndependentSampler Public Methods>>+= 
Float Get1D() { return rng.Uniform<Float>(); } Point2f Get2D() { return {rng.Uniform<Float>(), rng.Uniform<Float>()}; } Point2f GetPixel2D() { return Get2D(); }

All the methods for analyzing sampling patterns from Section 8.2 are in agreement about the IndependentSampler: it is a terrible sampler. Independent uniform samples contain all frequencies equally (they are the definition of white noise), so they do not push aliasing out to higher frequencies. Further, the discrepancy of uniform random samples is 1—the worst possible. (To see why, consider the case of all sample dimensions either having the value 0 or 1.) This sampler’s only saving grace comes in the case of integrating a function with a significant amount of energy in its high frequencies (with respect to the sampling rate). In that case, it does about as well as any of the more sophisticated samplers.