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 theDeferredProbePipeline
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.
Project the directions of the sphere onto an octahedron.
Unwrap the octahedron. At this point we can find a uv coordinate based on an directional vector.
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.
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|
+-------+-------+
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
.
- Select the closest reflection probe. This is done by calculating the distance between P and the position of the reflection probe.
- Get the pixel based of the closest reflection probe for the needed vector
- 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.
- Apply GGX BRDF