This is the documentation for Enlighten.

Lightmap LOD

Introduction

In order to allow Enlighten to scale to large maps where the player can explore both indoors and outdoors areas, there is a mechanism for Level of Detail when solving Enlighten lightmaps. This is similar to Terrain LOD, however it is intended to be employed on systems which are not terrain. For Terrain systems it is recommended to use the Terrain LOD mechanism.

The general idea behind the Lightmap LOD mechanism is to generate Enlighten system lightmaps at different resolutions, and to give you control over which resolutions should be solved and rendered at a given time. For example, consider the hut model in the figure below. When the player (camera) is close we want to solve indirect light at high fidelity, however as the camera moves away we are less interested in the fine details.

Increasing the lightmap pixel size for lower Levels Of Detail results in fewer pixels being solved by the Enlighten runtime. Solving LOD 0 requires Enlighten to solve 4264 pixels for the whole hut, this gets reduced to 2640 for LOD 1, 1400 for LOD 2 and finally 880 for LOD 4. Reducing the number of pixels that need solving for distant objects will allow you to solve more parts of the whole map while still keeping to the same Enlighten budget. This will result in smoother and quicker convergence of indirect light especially in parts of the scene where wide vistas are being rendered.

Overview

Let us consider a simple case of one Enlighten system, that contains two (instances of) geometries. Geom 1 is a block that sits on top of a floor (Geom 2). Let us say that the user has requested three LODs to be generated for both Geom 1 and Geom 2 (details of how this is done are provided later). Both Geom 1 and Geom 2 are packed separately, each of them is packed three times, and in each packing the pixel size is increased by a factor of two. See the figure below.

Next in the OutputSystem creation step, packed geometries of the same LOD are assembled (and repacked) into the Output (Packed) System's LODs:

Note that all the LOD versions of the System share the same geometry (vertices, faces, normals, albedo UVs etc), they only differ in output lightmap pixel size and lightmap UVs (charts). Next, the clustering (leaf clusters, cluster tree hierarchy) is created for the System. This is shared between all the LOD versions of the System. The fact that the clustering is the same for all the LODs means that all the per cluster buffers used at run time (input lighting, bounce buffer, albedo buffer, etc.) stay valid when the solved LOD changes.

Cluster and duster positions are shared among all the LOD versions of the System. Duster points will have a separate UV coordinate for their position in each LOD.

The Create Light Transport stage then runs for all the LOD versions of the system. Different form factors are obtained for pixels from different LODs. The final data sets are compiled and separate RadCores are produced for all the LODs. Those RadCores can then be used at run time to solve a particular LOD version of the system. Since per-cluster buffers are shared among all LODs, there is no need to recalculate the input lighting when the LOD changes, similarly the bounce and albedo buffers used in the previous LOD can be reused. The lightmap UVs needed to access the lightmap at a given LOD can be obtained by accessing the Pack System's Packed Instance and Packed Geometry for that LOD.

High Level Build System

There is a small amount of special mark-up that needs to be added to the XML scene description in order to enable Lightmap LOD generation. This section describes the necessary steps, and the outputs of the Precompute pipeline when Lightmap LODs are enabled.

The decision of how many LODs to generate is done on a per <geom> basis. Different <geom> can have a different number of LODs. If instances of different geometries with different numbers of LODs end up in the same system, than that system will generate a number of LODs equal to the highest number requested for its geometries. For example if there is a system with two geometries, one with two LODs and the other with four LODs, than the System will have four LODs. When a non-existent LOD is requested for a geometry, it will use the lowest LOD that it has. See example below:

The number of LODs to generate is requested with the numLods XML attribute (inside .geom file):

<?xml version="1.0" encoding="utf-8"?>
<geom name="Cube_0" version="3" numLods="2" //...other geom attributes...// >
	<mesh name="Cube_0_LOD0" //...other mesh attributes...// />
</geom>

Each consecutive LOD has a pixel size increased by a factor of 2 compared to the previous LOD. The numLods attribute needs to be a positive integer that is greater than or equal to 1. Setting it to 1 is equivalent to switching LOD generation off. The default value is 1.

Example and Precompute outputs

Let us consider the simple scene again. The scene consists of two instances (Instance A and Instance B) of two geometries (Geom A and Geom B respectively). Geom A is set to have 2 LODs and Geom B is set to have 4 LODs:

Geom_A.geom
<?xml version="1.0" encoding="utf-8"?>
<geom name="Geom_A" version="3" simpNumIterationsPerSimp="500" simpMaxNumSimps="250" simpUsePixelUnits="true" numLods="2">
	<mesh
		name="Geom_A"
		guid="c9408aee000000000000000000000000"
		filename="Geom_A.pim"
		direct="true" indirect="true" target="true"/>
</geom>
Geom_B.geom
<?xml version="1.0" encoding="utf-8"?>
<geom name="Geom_B" version="3" simpNumIterationsPerSimp="500" simpMaxNumSimps="250" simpUsePixelUnits="true" numLods="4">
	<mesh
		name="Geom_B"
		guid="dc670a1c000000000000000000000000"
		filename="Geom_B.pim"
		direct="true" indirect="true" target="true"/>
</geom>
World.scene
<?xml version="1.0" encoding="utf-8"?>
<scene name="World" version="1" axes="-x+y+z">
	<instance name="Instance_B" instanceGuid="00000000000000000000000000000001" systemId="System_0" systemGuid="8494c213000000000000000000000000" paramSet="High" geometry="Geom_B" type="Radiosity" position="0.000000 20.000000 50.000000" rotation="0.000000 0.000000 0.000000 1.000000"/>
	<instance name="Instance_A" instanceGuid="00000000000000000000000000000002" systemId="System_0" systemGuid="8494c213000000000000000000000000" paramSet="High" geometry="Geom_A" type="Radiosity" position="230.000000 20.000000 290.000000" rotation="0.000000 0.000000 0.000000 1.000000"/>
</scene>

When the scene with this mark-up is used as input to the High Level Build System, the following outputs will be generated in the precomp folder:

Filename

Description

Geom_A_High.ig

A serialisation of the IPrecompInputGeometry object that describes Geom_A (meshes, vertices, normals, albedo UVs,...). It also hold information of how many LODs should be generated for this geometry. See IPrecompInputGeometry API for more information.

Geom_B_High.ig

A serialisation of the IPrecompInputGeometry object that describes Geom_B (meshes, verticess, normals, albedo UVs,...). It also hold information of how many LODs should be generated for this geometry. See IPrecompInputGeometry API for more information.

System_0.is

A serialisation of the IPrecompInputSystem object that describes Instances of Geom_A and Geom_B used in System_0. See IPrecompInputSystem API for more information.

Geom_A_High.pag

A serialisation of the IPrecompPackedGeometry for all the LODs of Geom_A. See IPrecompPackedGeometry API for information on how to access LODs. Each LOD contains the geometry lightmap UV, which combined with UV Instance transforms from the IPrecompPackedSystem can be used for mapping this geometry to the lightmap texture for that LOD.

Geom_B_High.pag

A serialisation of the IPrecompPackedGeometry for all the LODs of Geom_B. See IPrecompPackedGeometry API for information on how to access LODs. Each LOD contains the geometry lightmap UV, which combined with UV Instance transforms from the IPrecompPackedSystem can be used for mapping this geometry to the lightmap texture for that LOD.

System_0.pas

A serialisation of the IPrecompPackedSystem for all the LODs. See IPrecompPackedSystem API for information on how to access LODs. Each LOD contains a set of IPrecompPackedInstance objects that can be used for obtaining the lightmap UVs for given Instance at that LOD.

System_0.sdeps

A serialisation of the system dependencies of System_0. System dependencies are calculated using only LOD 0 versions of all the systems.

System_0.prc

A serialisation of the pre clustering (cluster mesh, leaf cluster) data. Each vertex of the cluster mesh has the lightmap UVs for all the LODs of the System.

System_0.clu

A serialisation of the cluster tree hierarchy, which does not contain any LOD data.

System_0.dust

A serialisation of the system dusters (used for input lighting and bounce resampling). Each duster point has lightmap UVs for all of the LODs. These are required for bounce resampling for all the LODs.

System_0.lt

A serialisation of the IPrecompSystemLightTransport which contains light transport data (e.g. form factors) for System_0 and all of its LODs.

System_0.ltz

A serialisation of the IPrecompSystemCompressedLightTransport which contains light transport data in a form that ready for export to RadCores.

Note that System_0.prc, System_0.clu, System_0.dust, System_0.lt and System_0.ltz are internal stages of the Precompute pipeline and do not provide any useful public interface.

Following outputs will be generated in the radiosity folder:

Filename

Description

System_0.caw

System_0 cluster albedo workspace. LOD independent.

System_0.clo

System_0 cluster output. LOD independent. For debugging/diagnosis.

System_0.iw.ref

System_0 input workspace data to be used with reference solver. LOD independent.

System_0.iw.sse

System_0 input workspace data to be used with the SSE optimised solver. LOD independent.

System_0.lto

System_0 light transport output for all the LODs. See the ILightTransportOutput API to see how to access LOD data. For debugging/diagnosis.

System_0.vis

System_0 directional visibility data. LOD independent.

System_0.mso

System_0 at LOD 0 mesh simplification output. For debugging/diagnosis. Contains packing data for meshes, charts, lightmap UVs, etc.

System_0_LOD_1.mso

System_0 at LOD 1 mesh simplification output. For debugging/diagnosis. Contains packing data for meshes, charts, lightmap UVs, etc.

System_0_LOD_2.mso

System_0 at LOD 2 mesh simplification output. For debugging/diagnosis. Contains packing data for meshes, charts, lightmap UVs, etc.

System_0_LOD_3.mso

System_0 at LOD 3 mesh simplification output. For debugging/diagnosis. Contains packing data for meshes, charts, lightmap UVs, etc.

System_0.rc.ref

System_0 at LOD 0 radCore data. Core data required by reference solver to solve irradiance.

System_0.rc_LOD_1.ref

System_0 at LOD 1 radCore data. Core data required by reference solver to solve irradiance.

System_0.rc_LOD_2.ref

System_0 at LOD 2 radCore data. Core data required by reference solver to solve irradiance.

System_0.rc_LOD_3.ref

System_0 at LOD 3 radCore data. Core data required by reference solver to solve irradiance.

System_0.rc.sse

System_0 at LOD 0 radCore data. Core data required by the SSE optimised solver to solve irradiance.

System_0.rc_LOD_1.sse

System_0 at LOD 1 radCore data. Core data required by the SSE optimised solver to solve irradiance.

System_0.rc_LOD_2.sse

System_0 at LOD 2 radCore data. Core data required by the SSE optimised solver to solve irradiance.

System_0.rc_LOD_3.sse

System_0 at LOD 3 radCore data. Core data required by the SSE optimised solver to solve irradiance.

System_0.rnt

System_0 at LOD 0 radiosity normal texture. Needed by solvers to solve directional irradiance.

System_0_LOD_1.rnt

System_0 at LOD 1 radiosity normal texture. Needed by solvers to solve directional irradiance.

System_0_LOD_2.rnt

System_0 at LOD 2 radiosity normal texture. Needed by solvers to solve directional irradiance.

System_0_LOD_3.rnt

System_0 at LOD 3 radiosity normal texture. Needed by solvers to solve directional irradiance.

Low level API access to Precompute LOD data

Requesting LOD generation

LOD generation is requested on a per geometry basis. The IPrecompInputGeometry class has a method to set the number of LODs that the geometry packing stage should generate for this geometry.

IPrecompInputGeometry interface for requesting LOD generation
/// Get the number of LODs packing should generate for this input geometry
virtual Geo::s32				GetNumLods() const = 0;

/// Set the number of LODs packing should generate for this input geometry
virtual void					SetNumLods(Geo::s32 numLods) = 0;

Note that SetNumLods and GetNumLods are applicable to Lightmap LODs only, and are not used with Terrain LOD generation (i.e. when SetIsTerrain(true) has been called). It is illegal for a geometry to be set as a terrain geometry and also have a number of lightmap LODs set to anything other than 1.

Accessing LOD data

All low level API for accessing LOD data follow the same pattern. If there is a precompute object that can have LOD representations, then two API functions on that object are provided. One returns the number of LODs that the object has, the other gives read-only access to a specified LOD representation.

LOD access for IPrecompPackedGeometry
/// Get number of LODs of this geometry.
virtual Geo::s32						GetNumLods() const = 0;

/// Get a given (lodIndex) LOD of this geometry. GetLod(0) will return the geometry itself.
virtual const IPrecompPackedGeometry*	GetLod(Geo::s32 lodIndex) const = 0;
LOD access for IPrecompPackedSystem
/// LOD access
/// For systems with no LODs generated GetNumLods will return 1 (i.e. the main system is considered to be the first LOD)
virtual Geo::s32						GetNumLods() const = 0;

///Access the IPrecompPackedSystem representing the LOD version of the system. GetLod(0) will return the pointer to the main system (i.e. this system)
virtual const IPrecompPackedSystem*		GetLod(Geo::s32 lodIndex) const = 0;
LOD access for IPrecompSystemCompressedLightTransport
/// For systems with no LODs generated GetNumLods will return 1 (i.e. the main system is considered to be the first LOD)
virtual Geo::s32						GetNumLods() const = 0;

/// Access the IPrecompSystemCompressedLightTransport representing the LOD version of the system. GetLod(0) will return the pointer to the main system (i.e. this system)
virtual const IPrecompSystemCompressedLightTransport*		GetLod(Geo::s32 lodIndex) const = 0;
LOD access for IPrecompSystemRadiosity
/// For systems with no LODs generated GetNumLods will return 1 (i.e. the main system is considered to be the first LOD)
virtual Geo::s32					GetNumLods() const = 0;

/// Access the RadSystemCore representing the LOD version of the system. GetLod(0) will return the pointer to the main system core (i.e. this system)
virtual const RadSystemCore*		GetLodRadCore(Geo::s32 lodIndex) const = 0;

Because GetNumLods() always returns a value equal to or greater than 1, and GetLod(0) returns the pointer to the object itself (which represents LOD 0), it is possible to iterate over all LODs of an object. For example to iterate over all LODs of an IPrecompPackedGeometry (pGeom) one would use something like:

Iterate all LODs of a packed geometry
for (Geo::s32 lodIndex = 0; lodIndex < pGeom->GetNumLods(); ++lodIndex)
{
    // Get the LOD version of the packed geometry
    IPrecompPackedGeometry const* pGeomLOD = pGeom->GetLod(lodIndex);
    // do something specific to this LOD
    pGeomLOD->DoSomethingForThisLOD();
}

The above code iterates over all LODs of the packed geometry, including LOD 0.

Precompute APIs

All of the Precompute APIs automatically handle input objects with Levels of Detail. If an input object to a given Precompute API has LODs then the output of that call will have LODs generated (unless the output is LOD independent). For example if IPrecompPackedGeometry objects passed to the Precompute::PackSystem API have LODs then the resulting IPrecompPackedSystem will have LODs as well.

Lightmap LODs and Terrain LODs

It is not allowed to mix Lightmap LODs and Terrain LODs within the same Enlighten System. That is, if an Enlighten system is marked as terrain system, it can only have instances of geometries that are also marked as terrain. Those geometries cannot have their number of lightmap LODs set to anything other than 1.

Using lightmap LODs in the low-level runtime API

As far as the low-level run-time is concerned, there is no special API required to handle LOD. The precompute output is a RadSystemCore for each LOD as identified by the filename structure documented above. The integration can load and manage these individual RadSystemCore using the same existing API functions.

What does change however is how the inputs to the solve functions and their respective memory is managed. While some of the inputs can be maintained on a per system basis, some of the inputs will need to be managed per LOD.

Input Lighting

The DirectInputLighting and IndirectInputLighting stages are unaffected by LOD. All inputs and outputs to and from these functions are the same as for non-LOD systems. This also applies to UpdateTransparencyWorkspace() and UpdateProbeBounceBuffer().

System Solving

The following solver inputs remain unchanged and should continue to be maintained per-system:

Per-system parameters
//Per system members
class RadIrradianceTask
{
public:
	//...
	const InputLightingBuffer**			m_InputLighting;
	const InputLightingBuffer*			m_Environment;
	eOutputFormat					m_OutputFormat;
	eOutputFormatByteOrder				m_OutputFormatByteOrder;
	float						m_OutputScale;
	float						m_TemporalCoherenceThreshold;
	float						m_TemporalCoherenceEpsilon;
	//...
};

The following inputs and outputs need to be maintained on a per-LOD basis:

Solvers Per LOD parameters
class RadIrradianceTask
{
public:
	//...
	const RadSystemCore*				m_CoreSystem;
	Geo::s32					m_OutputStride;
	Geo::s32					m_DirectionalOutputStride;
	void*						m_IrradianceOutput;
	void*						m_DirectionalOutput;
	void*						m_DirectionalOutputG;
	void*						m_DirectionalOutputB;
	void*						m_PersistentData;
	//...
};

Any API functions, such as CalcRequiredPersistentDataSize() which take RadSystemCore as input, should be passed the RadSystemCore object for the appropriate LOD.

Bounce resampling LOD

The bounce resampling stage should only be called once per system solve and should resample from only one LOD (ideally the highest detail LOD available.) If, for example, we were to solve LOD 2 to LOD 5, we would only want to resample the bounce from the LOD 2 solution.

The following input parameters remain the same regardless of input LOD:

Bounce resampling per-system parameters
class ResampleBounceParameters
{
	//...
	BounceBuffer*				m_BounceBuffer;
	float					m_OutputScale;
	//...
};

The following input parameters need to be set to those for the LOD from which you wish to resample the bounce:

Bounce resampling per-LOD parameters
class ResampleBounceParameters
{
	//...
	ResampleTextureParameters*		m_ResampleTextureParams;
	const Enlighten::RadSystemCore*		m_RadSystemCore;
	void*					m_PersistentData;
	//...
};

If no work was done by the solver due to temporal coherence optimisations, then there is no need to resample the bounce. You can test this by checking the value returned in the numSolvePixels parameter to the solver. If this value is 0, then you can skip the bounce resampling for this system.

Texture Albedo

The precomputed texture albedo sampling data is generated against LOD 0 RadSystemCore data. It is therefore important that the UVs and texture size used when rendering the albedo texture match LOD 0 UVs and resolution. This applies to: InitialiseAlbedoBufferFromTexture(), InitialiseEmissiveBufferFromTexture() and InitialiseTransparencyBufferFromTexture() only. The Enlighten::AlbedoBuffer, Enlighten::EmissiveBuffer and Enlighten::TransparencyBuffer are inputs to the IndirectInputLighting stage so are unaffected by LOD (ie the buffers are stored per system).

Using lightmap LODs in the high-level runtime API

In order for a System to make use of LOD, create and add an ISystemSolutionSpace for each LOD:

Example system and solution space allocation
// Allocate an enlighten system
HlrtSystem.System = UpdateManager->AllocateSystem(DynamicData.GetInputWorkspace(), DynamicData.GetDirectionalVisibilityData(), 1);
check(HlrtSystem.System);

// Allocate system solution spaces (RadCore 0..N refer to LODs)
for (int32 LodIndex = 0; LodIndex < DynamicData.GetRadCoreCount(); ++LodIndex)
{
	// Get the output textures to use by solution space
	static_assert(Enlighten::ENLIGHTEN_NUM_OUTPUT_TEXTURE_TYPES == 4, "ENLIGHTEN_NUM_OUTPUT_TEXTURE_TYPES is different than 4.");
	Enlighten::IGpuTexture* OutputTextures[Enlighten::ENLIGHTEN_NUM_OUTPUT_TEXTURE_TYPES] = { NULL, NULL, NULL, NULL };
	OutputTextures[Enlighten::ENLIGHTEN_OUTPUT_IRRADIANCE] = System->CreateUpdater(EEnlightenTextureType::Irradiance, LodIndex);
	OutputTextures[Enlighten::ENLIGHTEN_OUTPUT_DIRECTIONAL] = System->CreateUpdater(EEnlightenTextureType::Direction, LodIndex);

	// Allocate solution space
	Enlighten::ISystemSolutionSpace* NewSolutionSpace = UpdateManager->AllocateSystemSolutionSpace(DynamicData.GetRadCore(LodIndex), OutputTextures, Enlighten::OUTPUT_FORMAT_LRB);
	check(NewSolutionSpace);

	// Track solution space
	HlrtSystem.SolutionSpaces.Add(NewSolutionSpace);
}

By default all of the solution spaces assigned to a system get solved, so it is up to the engine integration to set only those solution spaces required for rendering and to indicate which index in the solution spaces array should be used for resampling the bounce. This is typically the highest quality LOD. When the camera moves and a different set of LODs are required, the engine must call SetSystemSolutionSpaces() with the different set of LODs to solve.

Example setting a LOD range to solve
// Find the system solution spaces for the selected LOD range
NumLodsToSolve = FMath::Abs(Range.FarLod - Range.NearLod) + 1;
int32 MinLod = FMath::Min(Range.FarLod, Range.NearLod);
check(MinLod >= 0);

// Enqueue command to set new range of lods to be solved
Enlighten::EnqueueWorkerFunctorCommand(UpdateManager, [=](Enlighten::IUpdateManagerWorker* worker)
{
	UMSystem->SetSystemSolutionSpaces(System.SolutionSpaces.GetData() + MinLod, NumLodsToSolve, 0);
});

Rendering

Due to the time lag from when a solution space gets set on an Enlighten System to the point at which the texture has been updated on the GPU, it is important not to begin using the lightmap until the ISystemSolutionSpace::IsReadyForRendering() method returns true. This indicates that the system has been solved and the GPU texture has been updated. (i.e. BaseWorker::UpdateGpuTextures() has been called). Failure to account for this lag will lead to popping artifacts as unsolved (and out-of-date) lightmaps are used for rendering.

In order to avoid blocking the rendering, the renderer must allow some flexibility so that it can continue to render with the old Enlighten LOD set until IsReadyForRendering() individually returns true for each of the new set of LODs.

Releasing resources

At times during game play it might be desirable to reclaim some memory taken by unused LODs. For example, you may not want to have the high detail solution spaces in memory for systems that are far off in the distance and only rendering from low detail lightmaps. This can be done after the solution spaces have been unset on the system. You can then call the IUpdateManager::EnqueueReleaseSystemSolutionSpaces() method which will release the resources on the render thread (once all references to the solution space have been detached).

Releasing solution space memory
// Enqueue command to release the solution spaces.
		UpdateManager->EnqueueReleaseSystemSolutionSpaces(SolutionSpaces.GetData(), SolutionSpaces.Num());

It is also safe to release all of the solution spaces after the system which uses the solution spaces has been removed from the High Level Runtime or one can explicitly remove all system solution spaces from the system by calling BaseSystem::RemoveSystemSolutionSpaces() prior to releasing them.

Remember: Removing - or even releasing - a system does not release the memory used by the ISystemSolutionSpace objects. These need to be explicitly released by your engine integration by calling IUpdateManager::EnqueueReleaseSystemSolutionSpaces().