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

...

By convention, Bxdfs call the DrawSamples?D() functions that .  These functions do not increment the sampleids.  This means that an Integrator calling a Bxdf has to do this incrementing after the Bxdf call – otherwise multiple bxdf samples will be the same sample value, ie. the same direction.  Similar for light sampling and indirect illumination sampling: the Integrator has to increment the sampleids after the samples have been used.  This is easily done by looping over all the sample contexts in a RixRNG like this:

...

First it should be mentioned that there is a special C++ type for scramble patterns.  The scramble patterns are (as mentioned above) just 32-bit patterns, but these are easy to mistake for other integers.  To enable the compiler to catch incorrect parameter order in various function calls, the scramble pattern has its own type – an enum class called 'Scramble' that is just an alias for unsigned int.  Create a Scramble bit-pattern like this example:

...

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  As mentioned above, the Scramble type is just an unsigned inint, but made into a distinct type for type safety.

...

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  This function generates an independent sample domain where the samples in that domain are stratified with respect to each other, but not with respect to previous or future samples in the same pixel.  The sampleid of the new SampleCtx is set to the value of the 'newsampleid' parameter.

SampleCtx NewDomainSplit(Scramble scramble, unsigned newnsamplesnewnumsamples);

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 newnumsamples is 1 this is the same as NewDomain(scramble).)

This ensures that you will get consecutive samples from a single sample sequence, with 'newnsamplesnewnumsamples' samples for each iteration.  I.e. you will get the exact same samples values as if you had shot 'newnsamplesnewnumsamples' 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 *= newnsamplesnewnumsamples.  That means skipping ahead in the sequence by the splitting branching factor 'newnsamplesnewnumsamples', thereby ensuring that the combined samples are consecutive and non-overlapping.

...

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): NewDomains(), NewDomainsDistrib(), and NewDomainsSplit().

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.

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

...

Code Block
  SampleCtx* samplectxarray = new SampleCtx[numPts];
  RixRNG rng(parentRNG, samplectxarray, numPts);
  for (int pt = 0RixRNG::Scramble scramble = static_cast<RixRNG::Scramble>(0x8732f9a1)
  for (int pt = 0; pt < numPts; pt++)
  {
    int index = shadingContext->integratorCtxIndex[pt];
    samplectxarray[pt] = parentRNG→NewDomainsplit(index, 0x8732f9a1scramble, 4);
  }

Here the scramble bit-pattern is 0x8732f9a1 and the splitting factor is 4.

...

The samples returned by the DrawSample2D() and GenerateSamples2D() functions are in the unit square.  It is often necessary to map from the unit square to other domains.  Example: uniform sampling a disk with a given radius can be done by picking a radius proportional to the square root of a sample between 0 and 1, and an angle between 0 and 2pi, and then computing the xy position corresponding to that radius and angle:

...

A common – equally good – alternative is called "Shirley remapping".  It does not rely on 3D stratification.  Instead, get , but only on 2D stratification.  Get a 2D sample and first use one of the coordinates of the sample to select the sub-domain, and then map the sample within that selected domain (based on the selection probability) back to the unit square domain.  A figure provides a more intuitive explanation – in this example we choose between two light area sources, with 80% chance of selecting the first light and 20% chance of selecting the second light: 

Note how the middle image are To the left are the original samples on the unit square.  The middle square contains the samples from the left 80% of the left image, only stretched a bit horizontally.  Likewise, the right image is square contains the samples from the right 20% of the left image, but stretched 5x horizontally.  The stretching preserves the stratification properties of the original sample points.

This method of using 2D samples to both choose between sample sub-domains and at the same time stretching the samples from the chosen sub-domain to provide stratified 2D sample points is implemented in the RixChooseAndRemap() function in the RixShadingUtils.h include file.

...

Path splitting vs distribution ray tracing

Path splitting and distribution ray tracing were mentioned above (in the description of the various NewDomain*() functions).  But when is it appropriate to use which?  A couple examples should provide some guidance:

...

Resort to distribution ray tracing if there is no fixed branching factor.  For example, if some specular ray hit points spawn 16 new sample directions bu but other specular ray hit points (perhaps with a lower throughput) only spawn 4 or even 1 new sample directions.  In this case there is no simple way to compute the offset into a combined sample sequence.  Instead the 16 (or 4) sample directions are stratified with respect to each other, but not with respect to any other samples for that same pixel.  (If only 1 new sample direction is spawned then there is no stratification at all.)

...

Non-stratified samples similar to e.g. drand48() can be obtained by calling the HashToRandom() function.  It computes a repeatable random float between 0 and 1 given two unsigned int inputs, for example patternid and sampleid.  Using the same patternid and continuously incrementing sampleid gives a sequence of samples with good statistical variation – similar to drand48().  The HashToRandom() function is repeatable (iei.e. the same two inputs always give the same output), and has no multi-threaded contention (whereas drand48() has notoriously bad lockingcache line contention, hampering multi-threaded performance).  HashToRandom() is located in the RixRNGInline.h include file.

...

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 or adaptive progressive photon mapping.  The Generator class provides a way to intercept sample generation: when a custom Generator has been specified, it automatically gets called when e.g. a Bxdf or light sampling calls one of the GenerateSample() or DrawSample() functions.  Some of the RixRNG constructors can be passed a an explicit pointer to a Generator.

For example, for debugging purposes it can be useful to have all samples generated by drand48().  (Note that drand48() is very bad for regular use: convergence is slow since its values are not stratified, and it has a lock on cache line contention for access to its internal state, which slows down multi-threaded executions.)  Here is a snippet of code from a Generator that calls drand48():

Code Block
class RandomSampler : public RixRNG::Generator
{
    ...


    // Generate a uniformly distributed pseudorandom sample in [0,1)
    virtual float Sample1D(const RixRNG::SampleCtx &sc, unsigned i) const   // both params are ignored
    {
        float x = drand48();
        if (x >= 1.0f) x -= 1.0f;   // this can happen due to rounding double to float
        return x;
    }


    ... similar Sample2D() and Sample3D() functions ...
    
    // Fill in float array 'xis' with pseudorandom samples in [0,1)
    virtual void MultiSample1D(unsigned n, const RixRNG::SampleCtx &sc, float *xis) const
    {
        for (int i = 0; i < n; i++)
        {
            float x = drand48();
            if (x >= 1.0f) x -= 1.0f;   // this can happen due to rounding double to float
            xis[i] = x;
        }
    }


    ... similar MultiSample2D() and MultiSample3D() functions ...
    
}

...