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:
<?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>
<?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>
<?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 |
Geom_B_High.ig | A serialisation of the |
System_0.is | A serialisation of the |
Geom_A_High.pag | A serialisation of the |
Geom_B_High.pag | A serialisation of the |
System_0.pas | A serialisation of the |
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 |
System_0.ltz | A serialisation of the |
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 |
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.
/// 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.
/// 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 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;
/// 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;
/// 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:
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 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:
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:
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:
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:
// 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.
// 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).
// 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().