This is the documentation for Enlighten.

Solving for probe points


Overview

Beginning in Enlighten version 2.21, there are two alternative methods for solving for light probe values: the original Probe Solver, which uses the RadProbeTask structure; and the newer Entire Probe Set Solver, which uses the EntireProbeSetTask structure.

Compared to the older Probe Solver, the precomputed data required for the Entire Probe Set Solver is significantly more compressed, working out at 25-30% of the data size for a typical regular grid of probe locations. Unlike the original Probe Solver, which requires different data to be precomputed for L1 and L2 output, the Entire Probe Set Solver can use the same data to solve either L1 or L2 spherical harmonics at runtime. The execution time is also significantly shorter, taking approximately 60% of the time to solve all probes in a set. In addition to this, temporal coherence optimisations are implemented in the Entire Probe Set Solver, so that redundant repeat computations can be skipped when lighting conditions are not changing.

The trade off is that it is not possible to solve individual probes with the Entire Probe Set Solver.

Precompute data blocks

The Enlighten precompute generates data blocks for both the original Probe Solver and the newer Entire Probe Set Solver. These are both members of the RadProbeSetCore class:

//--------------------------------------------------------------------------------------------------
/// Constitutes the core precomputed data for a probe set.
//--------------------------------------------------------------------------------------------------
class RadProbeSetCore
{
public:
	/// Data describing the probe set, its size, requirements and relations to other systems
	RadProbeSetMetaData		m_MetaData;

	/// Precomputed data necessary for probe radiosity lighting
	RadDataBlock			m_ProbeSetPrecomp;

	/// Precomputed data block for the "probe set solver" which solves entire probe sets atomically,
	/// with optional temporal coherence optimisations.
	RadDataBlock			m_EntireProbeSetPrecomp;

You need to load only one of these data blocks. If you use the older Probe Solver, load only the m_ProbeSetPrecomp data block. Conversely, if you use the Entire Probe Set Solver, load only the m_EntireProbeSetPrecomp block.

Providing input lighting data to the probe solver

The mechanism for passing input to a probe task is the same as for irradiance tasks; you pass an ordered list of input lighting buffer pointers. There are helper functions to prepare this list.

When using the Entire Probe Set Solver you must use the versions which take a RadDataBlock pointer, and pass the m_EntireProbeSetPrecomp block. (The older version which takes a RadProbeSetCore pointer will return results for the older Probe Solver, or fail if that data block is not loaded.)

/// Returns the length of the input lighting buffer list expected when solving radiosity using this core data.
Geo::s32 GEO_CALL GetInputWorkspaceListLength(const Enlighten::RadDataBlock* dataBlock);

/// Returns the GUID of a specific entry in the expected input lighting buffer list.
Geo::GeoGuid GEO_CALL GetInputWorkspaceGUID(const Enlighten::RadDataBlock* dataBlock, Geo::s32 index);

/// Places the unordered list of lighting buffers into the correct order for the solver.
bool GEO_CALL PrepareInputLightingList(const Enlighten::RadDataBlock* dataBlock,
                                       const Enlighten::InputLightingBuffer** inputLightingBuffers,
                                       Geo::s32 numLightingBuffers,
                                       const Enlighten::InputLightingBuffer** listILBOut);

Example - Probe Solver

The following simplified example solves for the first index in the probe set, using the original Probe Solver. In this case, the list of input indices and the list of output pointers are very simple. In a more realistic use case, you would have to decide which indices to compute based on the world position of the dynamic object you wish to relight.

RadProbeTask probeTask;
probeTask.m_CoreProbeSet = probeSetCore;
probeTask.m_InputLighting = GEO_NEW_ARRAY(const InputLighitngBuffer*, Enlighten::GetInputWorkspaceListLength(probeSetCore));
Enlighten::PrepareInputLightingList(probeSetCore, const_cast<const InputLightingBuffer**>(inputLightingBuffers), numSystems, probeTask.m_InputLighting);

// Make the list of indices we wish to solve for.

s32 indexList[1] = { 0 };
probeTask.m_NumIndicesToSolve = 1;
probeTask.m_IndicesToSolve = indexList;
// List of input indices - only one entry long.

// Reserve space for the output.
// SolveProbeTaskL1 generates 12 floating-point coefficients per index.
float shOutput[12];
float* pShOutput = shOutput;
probeTask.m_OutputPointers = &pShOutput;
// List of output pointers - only one entry long.

// Ask Enlighten to compute the light probe data!
bool solveStatus = Enlighten::SolveProbeTaskL1(&probeTask);

Example - Entire Probe Set Solver

EntireProbeSetTask probeTask;
probeTask.m_CoreProbeSet = probeSetCore;
probeTask.m_InputLighting = GEO_NEW_ARRAY(const InputLighitngBuffer*, Enlighten::GetInputWorkspaceListLength(probeSetCore));
Enlighten::PrepareInputLightingList(&probeSetCore->m_EntireProbeSetPrecomp, const_cast<const InputLightingBuffer**>(inputLightingBuffers), numSystems, probeTask.m_InputLighting);
probeTask.m_Environment = <pointer to an input lighting buffer for the emissive environment>

probeTask.m_OutputShOrder = <Enlighten::SH_ORDER_L1 or Enlighten::SH_ORDER_L2>          // choose whether to solve either L1 or L2 SH
probeTask.m_Output = <pointer to an array of 12 floats per probe for L1 or 27 for L2>	  // supply this pointer for float output
probeTask.m_U8Output = NULL;								                            // supply this pointer instead for U8 output (only valid for L1)
probeTask.m_U8OutputScale = 1.0f;							                            // if using U8, use this to change range

// set these parameters to enable temporal coherence optimisations
probeTask.m_TemporalCoherenceThreshold = 0.01f;
probeTask.m_TemporalCoherenceBuffer = <a persistent buffer of length probeSetCore->m_MetaData.m_RequiredTemporalCoherenceBufferSize>

// this workspace is always required by SolveEntireProbeSetTask
void* workspace = <a buffer of length probeSetCore->m_MetaData.m_RequiredWorkspaceSize>

bool solveStatus = Enlighten::SolveEntireProbeSetTask(&probeTask, workspace timeUs, numSolvedProbes);

If used, the temporal coherence optimisation requires that all probe sets are kept in sync so that no changes in lighting are missed. Nevertheless, some probe sets can be updated less frequently than others by using the Freeze functions in place of a solve. FreezeEntireProbeSetTask performs the minimal housekeeping required to keep track of light changes for the temporal optimisation; no output SH coefficients are updated. The input parameters are exactly the same as for SolveEntireProbeSetTask:

bool solveStatus = Enlighten::FreezeEntireProbeSetTask(&probeTask, workspace timeUs, numSolvedProbes);

Compressed output

Although the calculation of probe volumes is all done in floating point, it is not always necessary to retain such precision in the final answer. To reduce memory requirements for the L1 solver, it is possible to pass an array of u8 to the probeTask, along with a scale value (similar to the BounceScale). The rest of the setup and function call is unaffected. See Light probe evaluation for more details about the compressed encoding.

// Reserve space for the output.
// SolveProbeTaskL1/SolveEntireProbeSetTask generates 12 u8 coefficients per index.
Geo::u8 shOutput[12];
Geo::u8* pShOutput = shOutput;
probeTask.m_U8OutputPointers = &pShOutput;
probeTask.m_U8OutputScale = 1.0f;