Page tree

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 5 Next »

Contents

In order to render images with as low noise and as fast convergence as possible, it is important to use samples that do not clump and do not leave large parts of the sampling domain empty.  We call such samples well stratified.  There are several different ways of generating well-stratified samples; RenderMan uses lookups in pre-generated tables of progressive multi-jittered (PMJ) sample sequences which are suitable for progressive rendering and are stratified in 1D, 2D, and 3D.  The 1D sample values are between 0 and 1, the 2D samples are on the unit square, and the 3D samples are in the unit cube.  (Practical tips on how to map to other domains are mentioned below.)

In order to look up in one of the pre-generated sample tables one needs to specify which table to use and which sample number is requested.  RenderMan provides a convenient abstraction class and API for this, called RixRNG.  The API is defined in the include/RixRNG.h header file (and the implementation of PMJ table lookups are in the include/RixRNGProgressive.h file).

The SampleCtx struct

A sample context (SampleCtx) consists of two unsigned integers: patternid and sampleid.  Patternid is a 32-bit pattern that gets mapped to one of the built-in PMJ  tables, and sampleid determines which of the samples in the table to use.  Camera rays are initialized such that samples in a given pixel will have the same patternid; typically, the first sample in each pixel will have sampleid 0, the next sample in the pixel will have sample id 1, etc.

The RixRNG class

The "RNG" part of RixRNG stands for "Random Number Generator" even though the samples are not random all.  The RixRNG class is basically just a wrapper around an array of per-shading-point sample contexts (SampleCtxArray) and an integer (numPts) specifying how many sample contexts there are in the array.  There is typically one SampleCtx for each point in a ray shading batch.  The RixRNG wrapper class makes it convenient to generate sample points (or new sample domains) for an entire ray shading batch with just a single function call.

Generating samples

There are six different functions to generate samples: GenerateSample1D(), GenerateSample2D(), GenerateSample3D(), DrawSample1D(), DrawSample2D(), and DrawSample3D().  The Generate functions increment the sample context sampleid; Draw functions do not.

There are also six multipoint functions that generate samples for all points in a RixRNG: GenerateSamples1D(), GenerateSamples2D(), GenerateSamples3D(), DrawSamples1D(), DrawSamples2D(), and DrawSamples3D().

// Generate a sample and increment sample id

Bxdf calls ...

Integrators call ...

Generating new sample domains (new patternids)

Here "domain" actually just means a different patternid bit-pattern.  The name "domain" was chosen because typically a different patternid is used for bxdf sampling, light source sampling, etc., with the bxdf and light sources being different "sample domains".

There are three SampleCtx functions to generate a new SampleCtx based on an existing one, but with a different patternid: NewDomain(), NewDomainDistrib(), and newDomainSplit().

SampleCtx NewDomain(Scramble scramble);

The simplest function is NewDomain().  Given a (32-bit unsigned int) "scramble" bit-pattern and an existing "parent" sampleCtx, it returns a new sampleCtx with a patternid that is different from the existing one (and a sampleid that is the same as the existing one).  Pass a different scramble bit-pattern for different sample domains: bxdfs, light source sampling, etc.

SampleCtx NewDomainDistrib(Scramble scramble, unsigned newsampleid);

The NewDomainDistrib() function is similar, but should be used where the new domain's expected number of samples differs from that of the parent and repeated visits may nor may not have the same sample count or may consume differing numbers of samples: distribution sampling.  In other words, it generates a sample domain where the samples in that domain are stratified with respect to each other, but not with respect to other samples in the same pixel (ie. other iterations / camera rays).  The sampleid of the new SampleCtx is set to the value of the 'newsampleid'.

SampleCtx NewDomainSplit(Scramble scramble, unsigned newnsamples);

The NewDomainSplit() function is also similar to NewDomain(), but should be used where every visit will consume the same number of samples, and it is expected that all sibling visits will also always result in the drawing of a new domain – thus exploring the full space.   A fancy term for this is "trajectory splitting".  (If newnsamples is 1 this is the same as NewDomain(scramble).)


TO DO: FIGURE OF TRAJECTORY SPLITTING 


There are also similar functions in the RixRNG class that can fill in new domains (patternids) for an entire array of sample contexts (RixRNG sampleCtxArray), and similar functions with an extra index parameter that are useful for filling in sample context arrays for shading groups with a different indexing order than the "parent" sample context array.


RixRNG constructors ...

The scramble patterns can be any 32-bit pattern, but it is important that the bit pattern to generate different new domains are different.  For example, if an Integrator is generating new domains for bxdf and light source sampling, those should use different scramble bit patterns.  Otherwise there will be visible correlation between bxdf and light source sampling, leading to visible artifacts and poor convergence – or even no convergence at all!  Examples of bit patterns used in the PxrPathTracer Integrator are 0x2d96c92b, 0x3917fe2e, and 0xdeb189cf; there isn't anything particular about these bit patterns, the main point is that they are "random" and different.

Tips for using samples in practice

Mapping from unit square to other domains ...  Example: uniform sampling a disk.  

RtPoint2 sample = DrawSample2D(...);  float radius = sqrt(sample.x);  float phi = 2 * M_PI * sample.y; point.x = radius * cos(phi); point.y = radius * sin(sample.x);

When to use splitting and when not.

Shirley remap.  Example: Select a light source and then a position on that light source.  Figure of samples before and after remapping?

Splitting:  intuition: 1 cam ray x 4 diff rays should give same diffuse directions as 4 cam rays x 1 diff ray.  Figure?

Non-stratified samples similar to e.g. drand48(): HashToRandom() computes a repeatable random float between 0 and 1 given two unsigned inputs.  Repeatable, and no multi-threaded contention (drand48() has notoriously bad locking, hampering multi-treaded performance).

Advanced topic: Details of PMJ table lookup implementation

In the introduction above we wrote that patternid is used to choose the PMJ table, and sampleid is used to choose the sample in that table.  If we had 2^32 pre-generated PMJ tables it really would be as simple as that.  However, in practice we have only 384 different PMJ tables, so we have to be a bit inventive to make it look like we have many, many different tables even though we don't.  The trick – implemented in include/RixRNGProgressive.h – is to further "randomize" the samples in the tables.  This randomization is done with scrambling of the sample values and shuffling of the sample order.  First the patternid is mapped to one of the 384 PMJ tables with a repeatable hash function.  Then the sampleid is shuffled a bit by carefully swapping neighbor samples and groups of four samples.  (Such local shuffling does not affect the convergence properties of each sample sequence, but decorrelates sample sequences such that they can be safely combined with each other.)  Finally the mantissa bits of the floating-point sample values are scrambled using the patternid – this scrambling changes the sample values while preserving their stratification.

Another practical detail is that even though PMJ tables theoretically could be generated with infinitely many samples, in practice we limit each table to 4096 samples.  This kicks in when more than 4096 samples per pixel are required.  When a sampleid higher than 4096 is requested, we look up from the beginning of another table.  So the samples beyond 4096 are still stratified, just not quite as well stratified with respect to the first 4096 samples as if we had had larger tables.  In the unlikely case that more than 196608 samples per pixel are needed, ie. we get to sampleids higher than 196608, then the samples revert to unstratified, uniform random samples generated with the HashToRandom() function described below.

Advanced topic: Overriding the RixRNG implementation

It is possible to override the default PMJ samples if other sample sequences are desired.  This is scary stuff, but can be useful for experimenting with e.g. primary-space Metropolis rendering algorithms.  The Generator class ... When a custom Generator has been specified, it automatically gets called when e.g. a Bxdf of Integrator calls one of the GenerateSample or DrawSample functions.

More information

For more (very nerdy!) details about the properties of PMJ samples please refer to the following paper:

  "Progressive multi-jittered sample sequences",

  Per Christensen, Andrew Kensler, and Charlie Kilpatrick,

  Computer Graphics Forum (Proceedings of the Eurographics Symposium on Rendering), 37(4):21-33, 2018.

Paper and presentation slides are available here:  graphics.pixar.com/library/ProgressiveMultijitteredSampling