|
OpenQMC API
|
Higher level sampler API and sampler types. More...
Classes | |
| class | oqmc::SamplerInterface< Impl > |
| Public sampler API. More... | |
Typedefs | |
| using | oqmc::LatticeSampler = SamplerInterface< LatticeImpl > |
| Rank one lattice sampler. | |
| using | oqmc::LatticeBnSampler = SamplerInterface< LatticeBnImpl > |
| Blue noise variant of lattice sampler. | |
| using | oqmc::PmjSampler = SamplerInterface< PmjImpl > |
| Low discrepancy pmj sampler. | |
| using | oqmc::PmjBnSampler = SamplerInterface< PmjBnImpl > |
| Blue noise variant of pmj sampler. | |
| using | oqmc::SobolSampler = SamplerInterface< SobolImpl > |
| Owen scrambled sobol sampler. | |
| using | oqmc::SobolBnSampler = SamplerInterface< SobolBnImpl > |
| Blue noise variant of sobol sampler. | |
This module outlines the higher level sampler API, as well as each availble sampler type. Sampler type implementations are non-public, and all functionality is accessible via the SamplerInterface. There are two variants for each sampler, a base variant and a blue noise variant. Sampler types and a corresponding header file are listed under Typedef Documentation.
Blue noise sampler variants
Blue noise variants offer spatial blue noise dithering between pixels, with progressive pixel sampling. This is done using an offline optimisation process that is based on the work by Belcour and Heitz in 'Lessons Learned and Improvements when Building Screen-Space Samplers with Blue-Noise Error Distribution'.
Each variant achieves a blue noise distribution using two pixel tables, one holds keys to seed the sequence, and the other index ranks. These tables are then randomised by toroidally shifting the table lookups for each domain using random offsets. Correlation between the offsets and the pixels allows for a single pair of tables to provide keys and ranks for all domains.
Although the spatial blue noise does not reduce the error for an individual pixel, it does give a better perceptual result due to less low frequency structure in the error between pixels. Also, if an image is either spatially filtered (as with denoising), the resulting error can be lower when using a blue noise variant.
Blue noise variants are recommended, as the additional performance cost will likely be a favourable tradeoff for the quality gains at low sample counts. However, access to the data tables can impact performance depending on the architecture (e.g. GPU), so it is worth benchmarking if this is a concern.
Passing and packing samplers
Sampler objects can be efficiently passed by value into functions, as well as packed and queued for deferred evaluation. Achieving this means that the memory footprint of a sampler must be very small, and the type trivially copyable.
Sampler types are either 8 or 16 bytes in size depending on the type. The small memory footprint is possible due to the state size of PCG-RXS-M-RX-32 from the PCG family of PRNGs as described by O'Neill in 'PCG: A Family of Simple Fast Space-Efficient Statistically Good Algorithms for Random Number Generation'.
When deriving domains the sampler will use an LCG state transition, and only perform a permutation prior to drawing samples analogous to PCG. This provides high quality bits when drawing samples, but keeps the cost low when deriving domains, which might not be used.
| class oqmc::SamplerInterface |
This is a sampler interface that defines a generic API for all sampler types. The interface is composed of an internal implementation, meaning only this public API is exposed to calling code.
Different samplers defined using the interface should be interchangeable allowing for new implementations to be tested and compared without changing the calling code. The interface is static, so all functions should be inlined to allow for zero cost abstraction. This also means that enabling compile time optimisations might provide a noticable improvement.
Samplers can only be constructed, their state cannot change. In most cases the object variable can be marked constant. New samplers are created from a parent sampler using the newDomain member functions. Sample values are retrieved using the draw member functions. Calls to newDomain functions should be cheap in comparison to calls to draw functions.
| Impl | Internal sampler implementation type as described above. |
Public Member Functions | |
| SamplerInterface ()=default | |
| Construct an invalid sampler object. | |
| SamplerInterface (int x, int y, int frame, int index, const void *cache) | |
| Parametrised pixel constructor. | |
| SamplerInterface | newDomain (int key) const |
| Derive a sampler object as a new domain. | |
| SamplerInterface | newDomainSplit (int key, int size, int index) const |
| Derive a split sampler object with a local and a global distribution. | |
| SamplerInterface | newDomainDistrib (int key, int index) const |
| Derive a split sampler object with a local distribution. | |
| SamplerInterface | newDomainChain (int key, int index) const |
| Derive a split sampler object with a global distribution. | |
| template<int Size> | |
| void | drawSample (std::uint32_t sample[Size]) const |
| Draw integer sample values from domain. | |
| template<int Size> | |
| void | drawSample (std::uint32_t range, std::uint32_t sample[Size]) const |
| Draw ranged integer sample values from domain. | |
| template<int Size> | |
| void | drawSample (float sample[Size]) const |
| Draw floating point sample values from domain. | |
| template<int Size> | |
| void | drawRnd (std::uint32_t rnd[Size]) const |
| Draw integer pseudo random values from domain. | |
| template<int Size> | |
| void | drawRnd (std::uint32_t range, std::uint32_t rnd[Size]) const |
| Draw ranged integer pseudo random values from domain. | |
| template<int Size> | |
| void | drawRnd (float rnd[Size]) const |
| Draw floating point pseudo random values from domain. | |
Static Public Member Functions | |
| static void | initialiseCache (void *cache) |
| Initialise the cache allocation. | |
Static Public Attributes | |
| static constexpr std::size_t | cacheSize = Impl::cacheSize |
| Required allocation size of the cache. | |
|
default |
Create a placeholder object to allocate containers, etc. The resulting object is invalid, and you should initialise it by replacing the object with another from a parametrised constructor.
| oqmc::SamplerInterface< Impl >::SamplerInterface | ( | int | x, |
| int | y, | ||
| int | frame, | ||
| int | index, | ||
| const void * | cache | ||
| ) |
Create an object based on the pixel, frame and sample indices. This also requires a pre-allocated and initialised cache. Once constructed the object is valid and ready for use.
For each pixel this constructor is expected to be called multiple times, once for each index. Pixels might have a varying number of indicies when adaptive sampling.
| [in] | x | Pixel coordinate on the x axis. |
| [in] | y | Pixel coordinate on the y axis. |
| [in] | frame | Time index value. |
| [in] | index | Sample index. Must be positive. |
| [in] | cache | Allocated and initialised cache. |
|
static |
Prior to construction of a sampler object, a cache needs to be allocated and initialised for any given sampler type. This function will initialise that allocation. Once the cache is initialised it may be used to construct a sampler object, or copied to a new address.
Care must be taken to make sure the memory address is accessible at the point of construction. On the CPU this is trivial. But when constructing a sampler object on the GPU, the caller is expected to either use unified memory for the allocation, or manually copy the memory from the host to the device after the cache has been initialised.
A single cache (for each sampler type) is expected to be constructed only once for the duration of a calling process. This single cache can be used to construct many sampler objects.
| [in,out] | cache | Memory address of the cache allocation. |
cacheSize variable above. | SamplerInterface< Impl > oqmc::SamplerInterface< Impl >::newDomain | ( | int | key | ) | const |
The function derives a mutated copy of the current sampler object. This new object is called a domain. Each domain produces an independent 4 dimensional pattern. Calling the draw* member functions below on the new child domain will produce a different value to that of the current parent domain.
N child domains can be derived from a single parent domain with the use of the key argument. Keys must have at least one bit difference, but can be a simple incrementing sequence. A single child domain can itself derive N child domains. This process results in a domain tree.
The calling code can use up to 4 dimensions from each domain (these are typically of the highest quality), joining them together to form an N dimensional pattern. This technique is called padding.
| [in] | key | Index key of next domain. |

| SamplerInterface< Impl > oqmc::SamplerInterface< Impl >::newDomainSplit | ( | int | key, |
| int | size, | ||
| int | index | ||
| ) | const |
Like newDomain, this function derives a mutated copy of the current sampler object. However, using a technique called splitting, this domain can have a higher sample rate based on a fixed multiplier.
The result from taking N indexed domains with this function will be both a locally and a gloablly well distributed sub-pattern. This sub-pattern will of the highest quality due to being globally correlated with the sub-patterns of other samples.
If a mutliplier is adaptive (non-constant or unknown) then either the newDomainDistrib or newDomainChain functions should be used instead. These methods relax the constraint for a fixed multiplier, allowing for adaptive multilpiers in excahnge for a reduction in the quality
Calling code should use a constant key for any given domain while then incrementing the index value N times to increase the sampling rate by N for that given domain. The function will be called N times, once for each unique index. N must be passed as 'size' and remain constant.
| [in] | key | Index key of next domain. |
| [in] | size | Sample index multiplier. Must greater than zero. |
| [in] | index | Sample index of next domain. Must be positive. |
| SamplerInterface< Impl > oqmc::SamplerInterface< Impl >::newDomainDistrib | ( | int | key, |
| int | index | ||
| ) | const |
Like newDomain, this function derives a mutated copy of the current sampler object. However, using a technique called splitting, this domain can have a higher sample rate based on an adaptive multiplier.
The result from taking N indexed domains with this function will be a locally well distributed sub-pattern. This sub-pattern will be of lower quality when combined with the sub-patterns of other samples. That is because the correlation between the sub-patterns globally is lost.
If a mutliplier is fixed (constant and known) then the newDomainSplit function will produce better quality sample points and should be used instead. This is because newDomainSplit will preserved correlation between sub-patterns from other samples.
Calling code should use a constant key for any given domain while then incrementing the index value N times to increase the sampling rate by N for that given domain. The function will be called N times, once for each unique index.
| [in] | key | Index key of next domain. |
| [in] | index | Sample index of next domain. Must be positive. |
| SamplerInterface< Impl > oqmc::SamplerInterface< Impl >::newDomainChain | ( | int | key, |
| int | index | ||
| ) | const |
Like newDomain, this function derives a mutated copy of the current sampler object. However, using a technique called splitting, this domain can have a higher sample rate based on an adaptive multiplier.
The result from taking N indexed domains with this function will be a globally well distributed sub-pattern. This sub-pattern will be of lower quality when looking at the local correlation between different index values. But, each index will be correlated globally.
If a mutliplier is fixed (constant and known) then the newDomainSplit function will produce better quality sample points and should be used instead. This is because newDomainSplit will preserved correlation between local points from different index values.
Calling code should use a constant key for any given domain while then incrementing the index value N times to increase the sampling rate by N for that given domain. The function will be called N times, once for each unique index.
| [in] | key | Index key of next domain. |
| [in] | index | Sample index of next domain. Must be positive. |
| void oqmc::SamplerInterface< Impl >::drawSample | ( | std::uint32_t | sample[Size] | ) | const |
This can compute sample values with up to 4 dimensions for the given domain. The operation does not change the state of the object, and for a single domain and index, the result of this function will always be the same. Output values are uniformly distributed integers within the range of [0, 2^32).
These values are of high quality and should be handled with care as to not introduce bias into an estimate. For low quality, but fast and safe random numbers, use the drawRnd member functions below.
| Size | Number of dimensions to draw. Must be within [1, 4]. |
| [out] | sample | Output array to store sample values. |
| void oqmc::SamplerInterface< Impl >::drawSample | ( | std::uint32_t | range, |
| std::uint32_t | sample[Size] | ||
| ) | const |
This function wraps the integer variant of drawSample above. But transforms the output values into uniformly distributed integers within the range of [0, range).
| Size | Number of dimensions to draw. Must be within [1, 4]. |
| [in] | range | Exclusive end of range. Greater than zero. |
| [out] | sample | Output array to store sample values. |
| void oqmc::SamplerInterface< Impl >::drawSample | ( | float | sample[Size] | ) | const |
This function wraps the integer variant of drawSample above. But transforms the output values into uniformly distributed floats within the range of [0, 1).
| Size | Number of dimensions to draw. Must be within [1, 4]. |
| [out] | sample | Output array to store sample values. |
| void oqmc::SamplerInterface< Impl >::drawRnd | ( | std::uint32_t | rnd[Size] | ) | const |
This can compute rnd values with up to 4 dimensions for the given domain. The operation does not change the state of the object, and for a single domain and index, the result of this function will always be the same. Output values are uniformly distributed integers within the range of [0, 2^32).
These values are of low quality but are fast to compute and have little risk of biasing an estimate. For higher quality samples, use the drawSample member functions above.
| Size | Number of dimensions to draw. Must be within [1, 4]. |
| [out] | rnd | Output array to store rnd values. |
| void oqmc::SamplerInterface< Impl >::drawRnd | ( | std::uint32_t | range, |
| std::uint32_t | rnd[Size] | ||
| ) | const |
This function wraps the integer variant of drawRnd above. But transforms the output values into uniformly distributed integers within the range of [0, range).
| Size | Number of dimensions to draw. Must be within [1, 4]. |
| [in] | range | Exclusive end of range. Greater than zero. |
| [out] | rnd | Output array to store rnd values. |
| void oqmc::SamplerInterface< Impl >::drawRnd | ( | float | rnd[Size] | ) | const |
This function wraps the integer variant of drawRnd above. But transforms the output values into uniformly distributed floats within the range of [0, 1).
| Size | Number of dimensions to draw. Must be within [1, 4]. |
| [out] | rnd | Output array to store rnd values. |
|
staticconstexpr |
Prior to construction of a sampler object, a cache needs to be allocated and initialised for any given sampler type. This variable is the minimum required size in bytes of that allocation. The allocation itself is performed and owned by the caller. Responsibility for the de-allocation is also that of the caller.
#include <oqmc/lattice.h>
The implementation uses the generator vector from Hickernell et al. in 'Weighted compound integration rules with higher order convergence for all N' to construct a 4D lattice. This is then made into a progressive sequence using a scalar based on a radical inversion of the sample index. Randomisation uses toroidal shifts.
This sampler has no cache initialisation cost, it generates all samples on the fly without touching memory. Runtime performance is also high with a relatively low computation cost for a single draw sample call. However the rate of integration per pixel can be lower when compared to other samplers.
#include <oqmc/latticebn.h>
Same as oqmc::LatticeSampler, with additional spatial blue noise dithering between pixels, with progressive pixel sampling support.
#include <oqmc/pmj.h>
The implementation uses the stochastic method described by Helmer et la. in 'Stochastic Generation of (t, s) Sample Sequences' to efficiently construct a progressive multi-jittered (0,2) sequence. The first pair of dimensions in a domain have the same intergration properties as the Sobol implementation. However as the sequence doesn't extend to more than two dimensions, the second pair is randomised relative to the first in a single domain.
This sampler pre-computes a base 4D pattern for all sample indices during the cache initialisation. Permuted index values are then looked up from memory at runtime, before being XOR scrambled. This amortises the cost of initialisation. The rate of integration is very high, especially for the first and second pairs of dimensions. You may however not want to use this implementation if memory space or access is a concern.
#include <oqmc/pmjbn.h>
Same as oqmc::PmjSampler, with additional spatial blue noise dithering between pixels, with progressive pixel sampling support.
#include <oqmc/sobol.h>
The implementation uses an elegant construction by Burley in 'Practical Hash-based Owen Scrambling' for an Owen scrambled Sobol sequence. This also includes performance improvements such as limiting the index to 16 bits, pre-inverting the input and output matrices, and making use of CPU vector intrinsics. You need to select an OPENQMC_ARCH_TYPE to make use of the performance from vector intrinsics for a given architecture.
This sampler has no cache initialisation cost, it generates all samples on the fly without touching memory. However the cost per draw sample call is computationally higher than other samplers. The quality of Owen scramble sequences often outweigh this cost due to their random error cancellation and incredibly high rate of integration for smooth functions.
#include <oqmc/sobolbn.h>
Same as oqmc::SobolSampler, with additional spatial blue noise dithering between pixels, with progressive pixel sampling support.