Skip to content

Reflection probes

To improve performance radiance light in the scene can be recorded in a reflection probe. There are two kind of reflection probes.

  • World: Records the light from the background only.
  • Probe: Records light from geometry in the scene from a specific location.

flowchart LR
    RenderCubemap --> RemapCubeToOctahedralProjection --> UpdateWorldIrradianceFromOctahedralProjection
Each reflection probe is first rendered in a cubemap. See CaptureView::render_world or CaptureView::render_probes. The cubemap is remapped to octahedral projection. See ReflectionProbeModule::remap_to_octahedral_projection.

In case we are creating a reflection probe for a world the irradiance of the world should also be updated. See ReflectionProbeModule::update_world_irradiance

World probe baking

sequenceDiagram
    participant Instance as Instance
    participant View as CaptureView
    participant Module as ReflectionProbeModule
    participant Pipeline as WorldProbePipeline

    activate Instance
    Instance->>View: render_world()
    activate View
        alt world needs update
            loop for each cubemap side
                View->>Pipeline: render()
                activate Pipeline
                deactivate Pipeline
            end

            View->>Module:remap_to_octrahedral_projection()
            activate Module
            deactivate Module

            View->>Module:update_probes_texture_mipmaps()
            activate Module
            deactivate Module
        end

        alt update irradiance needed?
            View->>Module:update_world_irradiance()
            activate Module
            deactivate Module
        end

    deactivate View
    deactivate Instance

Reflection probe baking

When a change is detected for a reflection probe it is marked to be updated. Prerequisites to update the reflection probes are:

  • All material shaders should have been compiled.
  • The DeferredProbePipeline should be activated. To save overhead the DeferredProbePipeline and the (probe_prepass, probe_shading) material passes are only created and populated when there are probes that needs to be updated.

The previous state of the reflection probes will be used, until the reflection probes are updated.

sequenceDiagram
    participant Instance as Instance
    participant View as CaptureView
    participant Module as ReflectionProbeModule
    participant Pipeline as DeferredProbePipeline

    activate Instance
    Instance->>View: render_probes()
    activate View
        loop while reflection probes left to update
            View->>Module: update_info_pop
            activate Module
            deactivate Module

            loop for each cubemap side
                View->>Pipeline: render()
                activate Pipeline
                deactivate Pipeline
            end
            View->>Module:remap_to_octrahedral_projection()
            activate Module
            deactivate Module
        end

        alt new probe rendered
            View->>Module:update_probes_texture_mipmaps()
            activate Module
            deactivate Module
        end
    deactivate View
    deactivate Instance

Octahedral mapping

To reduce memory needs and improve sampling performance the cubemap is stored in octahedral mapping space. This is done in eevee_reflection_probe_remap_comp.glsl.

The regular octahedral mapping has been extended to fix leakages at the edges of the texture and to be able to be used on an atlas texture and by sampling the texture once.

We follow the regular octahedral mapping process. The input is a cubemap what can be evaluated using directions. These directions can be represented as a sphere. The next images are used as a visual guidance how the algorithm is structured.

Sphere representing directional

Project the directions of the sphere onto an octahedron.

Projection onto octahedron

Unwrap the octahedron. At this point we can find a uv coordinate based on an directional vector.

Unwrapped octahedron

To reduce sampling cost and improve the quality we add an border around the octahedral map and extend the octahedral coordinates. This also allows us to generate lower resolution mipmaps of the atlas texture using 2x2 box filtering from a higher resolution.

Extended octahedral mapping

The octahedral map has an additional of 8 pixels of border on each side to support 4 mipmap levels.

Update Irradiance Cache

Diffuse lighting in the scene requires an Irradiance Volume object. When a scene doesn't have this object setup the world light would not be visible on diffuse objects. We update the first brick of the irradiance atlas texture with the world lighting so it will still be visible. This is done inside ReflectionProbeModule::update_world_irradiance.

eevee_reflection_probe_update_irradiance_comp.glsl calculates the spherical harmonics by evaluating the octahedral mapped world probe and stores the calculated spherical harmonics homogeneously in the irradiance atlas texture.

Every probe resolution always contains a subdivision level containing a map of 64x64. This map would be selected as it is faster to extract the data from this resolution than a higher one and has enough data to capture the probe.

Probe resolution layer_subdivision layer containing 64x64
2048x2048 0 5
1024x1024 1 4
512x512 2 3
256x256 3 2
128x128 4 1
64x64 5 0

Storage

The recorded reflection probes are stored in a single texture array. Each probe can be of any resolution as long as it is a power of 2 and not larger than 2048 or smaller than 64. So valid options are (2048x2048, 1024x1024, 512x512, etc).

Each probe can be stored in their own resolution and can be set by the user.

Note

Eventually we would like to add automatic resolution selection. This could be done by the distance from the camera or view.

The probes are packed in an 2d texture array with the dimension of 2048*2048. The number of layers depends on the actual needed layers to store all the probes.

Subdivisions and areas

Probes data are stored besides the texture. The data is used to find out where the probe is stored in the texture. It is also used to find free space to store new probes.

For each probe the next data is stored:

  • layer: on which layer of the texture contains the data of the probe
  • layer_subdivision: what subdivision is used by the probe. With the subdivision the resolution of probe in the layer can be determined. 0 would use the resolution of the texture (2048x2048). 1 divides the texture in 4 even areas of (1024x1024). 2 divides the texture in 16 areas of (512x512).
  • area_index: What area of the layer contains the probe. A layer contains out of (2^layer_subdivision) * (2^layer_subdivision) areas.

This approach ensures that we can be flexible at storing probes with different resolutions on the same layer. Lets see an example how that works

Example

Lets assume we have a world probe with the resolution of 1024x1024 and 2 reflection probes with the resolution of 512x512. The resolution of the texture is 2048x2048. This could be stored in the texture as follow

Probe layer layer_subdivision area_index Area in texture coordinates
0 0 1 0 0,0,0 - 1023,1023,0
1 0 2 2 1024,0,0 - 1535,511,0
2 0 2 3 1536,0,0 - 2047,511,0

Visually:

Layer 0 viewed at subdivision level 2
+---+---+---+---+
|   |   |   |   |
|   |   |   |   |
+---+---+---+---+
|   |   |   |   |
|   |   |   |   |
+---+---+---+---+
|000|000|   |   |
|000|000|   |   |
+---+---+---+---+
|000|000|111|222|
|000|000|111|222|
+---+---+---+---+

Looking at the same data, but in subdivision level 1 would look like:

Layer 0 viewed at subdivision level 1
+-------+-------+
|       |       |
|       |       |
|       |       |
|       |       |
|       |       |
+-------+-------+
|0000000|1212121|
|0000000|2121212|
|0000000|1212121|
|0000000|2121212|
|0000000|1212121|
+-------+-------+
On subdivision level 1 there are 2 empty spaces left, the lower right part is allocated by probes with a higher subdivision. When wanting to allocate an area with subdivision 1 it would select area 0,1024,0 - 1023,2047,0.

Code-wise this is implemented by ProbeLocationFinder. ProbeLocationFinder can view a texture in a given subdivision level and mark areas that are covered by probes. When finding a free spot it returns the first empty area.

Evaluation

Evaluation is implemented in eevee_reflection_probe_eval_lib.glsl.

  1. Select the closest reflection probe. This is done by calculating the distance between P and the position of the reflection probe.
  2. Get the pixel based of the closest reflection probe for the needed vector
  3. If pixel is transparent or no reflection probe is available. * get the world background based on the same vector * mix the background with the pixel from the reflection probe.
  4. Apply GGX BRDF