Page tree

Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

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 purpose of the abstraction is that authors of Bxdfs and Integrators do not have to worry about explicit indexing, sample counting, table lookups, etc.

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  tablessample 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.

...

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 ...

Example:  Bxdf GenerateSample() functions need 2D samples to generate sample directions.  The GenerateSample() functions have a RixRNG input parameter (called often calld "rng") for this purpose.  The RixRNG's sampleCtxArray contains numPts sample contexts, where numPts is the number of shading points to generate sample directions for.  Each sample context keeps track of the patternid and sampleid for that shading point.  In order to get a 2D sample for each shading point, a single call to DrawSamples2D is sufficient:

        RtFloat2 *xi = (RtFloat2 *) RixAlloca(sizeof(RtFloat2) * numPts);

        rng->DrawSamples2D(xi);   // draw numPts 2D samples; fill in xi arrayIntegrators 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".

RenderMan sets up the initial domains (patternids): a different one for each pixel.  Integrators can then derive new domains from those parent domains – separate domains for bxdf, light, and indirect – and then new domains again at the next bounce (derived from those at the first bounce), etc.

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

...

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.  The Scramble type is just an unsigned in, but made into a distinct type for type safety.

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 an independent sample domain where the samples in that domain are stratified with respect to each other, but not with respect to other previous or future samples in the same pixel (ie. other iterations / camera rays).  The sampleid of the new SampleCtx is set to the value of the 'newsampleid'.

...

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).)

This ensures that you will get consecutive samples from a single sample sequence, with 'newnsamples' samples for each iteration.  I.e. you will get the exact same samples values as if you had shot 'newnsamples' as many camera rays and only 1 sample at each camera hit.  In the NewDomainSplit() function, there is a line with the following: newd.sampleid *= newnsamples.  That means skipping ahead in the sequence by the splitting branching factor 'newnsamples', thereby ensuring that the combined samples are consecutive and non-overlapping.


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.

There are several different RixRNG constructors.  The simplest ones are simply passed an already allocated sample context array and its size, and assigns these to the RixRNG sampleCtxArray and numPts member variables.  The more fancy ones construct a new RixRNG based on an existing one, optionally with splitting or distribution.

Example:  Integrators call RixRNG constructors to set up sample domains for bxdfs lobe selection and sampling, light selection and sampling, stochastic transmission, and several other things.

RixRNG rng(parentRNG, samplectxarray, numpts);

The data in the sampleCtxArray (patternids and sampleids) can be filled in before or after the RixRNG constructor call.  For example by looping over sample points:

for (int pt = 0; pt < numPts; pt++) 

    int index = shadingContext->integratorCtxIndex[pt];

    samplectxarray[pt] = parentRNG→NewDomainsplit(index, scramble, splittingFactor);


Tips for using samples in practice

...

Another practical detail is that even though PMJ tables theoretically could be generated with sequences theoretically have infinitely many samples, in practice we limit each PMJ table to 4096 samples .  This kicks in when to keep memory use reasonable.  When more than 4096 samples per pixel are required.  When a sampleid used, we get sampleid values higher than 4096 is requested, we look ; we handle this by looking up from the beginning of another PMJ table (also with 4096 samples).  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.  For more than 8188 samples per pixel we move to yet another table, and so on.  In the unlikely case that more than 196608 samples per pixel are neededused, ie. we get to sampleids higher than 196608, then the we run out of tables – in this case the samples revert to unstratified, uniform (but deterministic) "random" samples generated with the HashToRandom() function described below.

...

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.  RixRNG constructor can be passed a pointer to a Generator ...

More information

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

...