Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_pbr/src/light_probe/irradiance_volume.rs
6604 views
1
//! Irradiance volumes, also known as voxel global illumination.
2
//!
3
//! An *irradiance volume* is a cuboid voxel region consisting of
4
//! regularly-spaced precomputed samples of diffuse indirect light. They're
5
//! ideal if you have a dynamic object such as a character that can move about
6
//! static non-moving geometry such as a level in a game, and you want that
7
//! dynamic object to be affected by the light bouncing off that static
8
//! geometry.
9
//!
10
//! To use irradiance volumes, you need to precompute, or *bake*, the indirect
11
//! light in your scene. Bevy doesn't currently come with a way to do this.
12
//! Fortunately, [Blender] provides a [baking tool] as part of the Eevee
13
//! renderer, and its irradiance volumes are compatible with those used by Bevy.
14
//! The [`bevy-baked-gi`] project provides a tool, `export-blender-gi`, that can
15
//! extract the baked irradiance volumes from the Blender `.blend` file and
16
//! package them up into a `.ktx2` texture for use by the engine. See the
17
//! documentation in the `bevy-baked-gi` project for more details on this
18
//! workflow.
19
//!
20
//! Like all light probes in Bevy, irradiance volumes are 1×1×1 cubes, centered
21
//! on the origin, that can be arbitrarily scaled, rotated, and positioned in a
22
//! scene with the [`bevy_transform::components::Transform`] component. The 3D
23
//! voxel grid will be stretched to fill the interior of the cube, with linear
24
//! interpolation, and the illumination from the irradiance volume will apply to
25
//! all fragments within that bounding region.
26
//!
27
//! Bevy's irradiance volumes are based on Valve's [*ambient cubes*] as used in
28
//! *Half-Life 2* ([Mitchell 2006, slide 27]). These encode a single color of
29
//! light from the six 3D cardinal directions and blend the sides together
30
//! according to the surface normal. For an explanation of why ambient cubes
31
//! were chosen over spherical harmonics, see [Why ambient cubes?] below.
32
//!
33
//! If you wish to use a tool other than `export-blender-gi` to produce the
34
//! irradiance volumes, you'll need to pack the irradiance volumes in the
35
//! following format. The irradiance volume of resolution *(Rx, Ry, Rz)* is
36
//! expected to be a 3D texture of dimensions *(Rx, 2Ry, 3Rz)*. The unnormalized
37
//! texture coordinate *(s, t, p)* of the voxel at coordinate *(x, y, z)* with
38
//! side *S* ∈ *{-X, +X, -Y, +Y, -Z, +Z}* is as follows:
39
//!
40
//! ```text
41
//! s = x
42
//!
43
//! t = y + ⎰ 0 if S ∈ {-X, -Y, -Z}
44
//! ⎱ Ry if S ∈ {+X, +Y, +Z}
45
//!
46
//! ⎧ 0 if S ∈ {-X, +X}
47
//! p = z + ⎨ Rz if S ∈ {-Y, +Y}
48
//! ⎩ 2Rz if S ∈ {-Z, +Z}
49
//! ```
50
//!
51
//! Visually, in a left-handed coordinate system with Y up, viewed from the
52
//! right, the 3D texture looks like a stacked series of voxel grids, one for
53
//! each cube side, in this order:
54
//!
55
//! | **+X** | **+Y** | **+Z** |
56
//! | ------ | ------ | ------ |
57
//! | **-X** | **-Y** | **-Z** |
58
//!
59
//! A terminology note: Other engines may refer to irradiance volumes as *voxel
60
//! global illumination*, *VXGI*, or simply as *light probes*. Sometimes *light
61
//! probe* refers to what Bevy calls a reflection probe. In Bevy, *light probe*
62
//! is a generic term that encompasses all cuboid bounding regions that capture
63
//! indirect illumination, whether based on voxels or not.
64
//!
65
//! Note that, if binding arrays aren't supported (e.g. on WebGPU or WebGL 2),
66
//! then only the closest irradiance volume to the view will be taken into
67
//! account during rendering. The required `wgpu` features are
68
//! [`bevy_render::settings::WgpuFeatures::TEXTURE_BINDING_ARRAY`] and
69
//! [`bevy_render::settings::WgpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING`].
70
//!
71
//! ## Why ambient cubes?
72
//!
73
//! This section describes the motivation behind the decision to use ambient
74
//! cubes in Bevy. It's not needed to use the feature; feel free to skip it
75
//! unless you're interested in its internal design.
76
//!
77
//! Bevy uses *Half-Life 2*-style ambient cubes (usually abbreviated as *HL2*)
78
//! as the representation of irradiance for light probes instead of the
79
//! more-popular spherical harmonics (*SH*). This might seem to be a surprising
80
//! choice, but it turns out to work well for the specific case of voxel
81
//! sampling on the GPU. Spherical harmonics have two problems that make them
82
//! less ideal for this use case:
83
//!
84
//! 1. The level 1 spherical harmonic coefficients can be negative. That
85
//! prevents the use of the efficient [RGB9E5 texture format], which only
86
//! encodes unsigned floating point numbers, and forces the use of the
87
//! less-efficient [RGBA16F format] if hardware interpolation is desired.
88
//!
89
//! 2. As an alternative to RGBA16F, level 1 spherical harmonics can be
90
//! normalized and scaled to the SH0 base color, as [Frostbite] does. This
91
//! allows them to be packed in standard LDR RGBA8 textures. However, this
92
//! prevents the use of hardware trilinear filtering, as the nonuniform scale
93
//! factor means that hardware interpolation no longer produces correct results.
94
//! The 8 texture fetches needed to interpolate between voxels can be upwards of
95
//! twice as slow as the hardware interpolation.
96
//!
97
//! The following chart summarizes the costs and benefits of ambient cubes,
98
//! level 1 spherical harmonics, and level 2 spherical harmonics:
99
//!
100
//! | Technique | HW-interpolated samples | Texel fetches | Bytes per voxel | Quality |
101
//! | ------------------------ | ----------------------- | ------------- | --------------- | ------- |
102
//! | Ambient cubes | 3 | 0 | 24 | Medium |
103
//! | Level 1 SH, compressed | 0 | 36 | 16 | Low |
104
//! | Level 1 SH, uncompressed | 4 | 0 | 24 | Low |
105
//! | Level 2 SH, compressed | 0 | 72 | 28 | High |
106
//! | Level 2 SH, uncompressed | 9 | 0 | 54 | High |
107
//!
108
//! (Note that the number of bytes per voxel can be reduced using various
109
//! texture compression methods, but the overall ratios remain similar.)
110
//!
111
//! From these data, we can see that ambient cubes balance fast lookups (from
112
//! leveraging hardware interpolation) with relatively-small storage
113
//! requirements and acceptable quality. Hence, they were chosen for irradiance
114
//! volumes in Bevy.
115
//!
116
//! [*ambient cubes*]: https://advances.realtimerendering.com/s2006/Mitchell-ShadingInValvesSourceEngine.pdf
117
//!
118
//! [spherical harmonics]: https://en.wikipedia.org/wiki/Spherical_harmonic_lighting
119
//!
120
//! [RGB9E5 texture format]: https://www.khronos.org/opengl/wiki/Small_Float_Formats#RGB9_E5
121
//!
122
//! [RGBA16F format]: https://www.khronos.org/opengl/wiki/Small_Float_Formats#Low-bitdepth_floats
123
//!
124
//! [Frostbite]: https://media.contentapi.ea.com/content/dam/eacom/frostbite/files/gdc2018-precomputedgiobalilluminationinfrostbite.pdf#page=53
125
//!
126
//! [Mitchell 2006, slide 27]: https://advances.realtimerendering.com/s2006/Mitchell-ShadingInValvesSourceEngine.pdf#page=27
127
//!
128
//! [Blender]: http://blender.org/
129
//!
130
//! [baking tool]: https://docs.blender.org/manual/en/latest/render/eevee/light_probes/volume.html
131
//!
132
//! [`bevy-baked-gi`]: https://github.com/pcwalton/bevy-baked-gi
133
//!
134
//! [Why ambient cubes?]: #why-ambient-cubes
135
136
use bevy_image::Image;
137
use bevy_light::IrradianceVolume;
138
use bevy_render::{
139
render_asset::RenderAssets,
140
render_resource::{
141
binding_types, BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, TextureSampleType,
142
TextureView,
143
},
144
renderer::{RenderAdapter, RenderDevice},
145
texture::{FallbackImage, GpuImage},
146
};
147
use core::{num::NonZero, ops::Deref};
148
149
use bevy_asset::AssetId;
150
151
use crate::{
152
add_cubemap_texture_view, binding_arrays_are_usable, RenderViewLightProbes,
153
MAX_VIEW_LIGHT_PROBES,
154
};
155
156
use super::LightProbeComponent;
157
158
/// On WebGL and WebGPU, we must disable irradiance volumes, as otherwise we can
159
/// overflow the number of texture bindings when deferred rendering is in use
160
/// (see issue #11885).
161
pub(crate) const IRRADIANCE_VOLUMES_ARE_USABLE: bool = cfg!(not(target_arch = "wasm32"));
162
163
/// All the bind group entries necessary for PBR shaders to access the
164
/// irradiance volumes exposed to a view.
165
pub(crate) enum RenderViewIrradianceVolumeBindGroupEntries<'a> {
166
/// The version used when binding arrays aren't available on the current platform.
167
Single {
168
/// The texture view of the closest light probe.
169
texture_view: &'a TextureView,
170
/// A sampler used to sample voxels of the irradiance volume.
171
sampler: &'a Sampler,
172
},
173
174
/// The version used when binding arrays are available on the current
175
/// platform.
176
Multiple {
177
/// A texture view of the voxels of each irradiance volume, in the same
178
/// order that they are supplied to the view (i.e. in the same order as
179
/// `binding_index_to_cubemap` in [`RenderViewLightProbes`]).
180
///
181
/// This is a vector of `wgpu::TextureView`s. But we don't want to import
182
/// `wgpu` in this crate, so we refer to it indirectly like this.
183
texture_views: Vec<&'a <TextureView as Deref>::Target>,
184
185
/// A sampler used to sample voxels of the irradiance volumes.
186
sampler: &'a Sampler,
187
},
188
}
189
190
impl<'a> RenderViewIrradianceVolumeBindGroupEntries<'a> {
191
/// Looks up and returns the bindings for any irradiance volumes visible in
192
/// the view, as well as the sampler.
193
pub(crate) fn get(
194
render_view_irradiance_volumes: Option<&RenderViewLightProbes<IrradianceVolume>>,
195
images: &'a RenderAssets<GpuImage>,
196
fallback_image: &'a FallbackImage,
197
render_device: &RenderDevice,
198
render_adapter: &RenderAdapter,
199
) -> RenderViewIrradianceVolumeBindGroupEntries<'a> {
200
if binding_arrays_are_usable(render_device, render_adapter) {
201
RenderViewIrradianceVolumeBindGroupEntries::get_multiple(
202
render_view_irradiance_volumes,
203
images,
204
fallback_image,
205
)
206
} else {
207
RenderViewIrradianceVolumeBindGroupEntries::single(
208
render_view_irradiance_volumes,
209
images,
210
fallback_image,
211
)
212
}
213
}
214
215
/// Looks up and returns the bindings for any irradiance volumes visible in
216
/// the view, as well as the sampler. This is the version used when binding
217
/// arrays are available on the current platform.
218
fn get_multiple(
219
render_view_irradiance_volumes: Option<&RenderViewLightProbes<IrradianceVolume>>,
220
images: &'a RenderAssets<GpuImage>,
221
fallback_image: &'a FallbackImage,
222
) -> RenderViewIrradianceVolumeBindGroupEntries<'a> {
223
let mut texture_views = vec![];
224
let mut sampler = None;
225
226
if let Some(irradiance_volumes) = render_view_irradiance_volumes {
227
for &cubemap_id in &irradiance_volumes.binding_index_to_textures {
228
add_cubemap_texture_view(
229
&mut texture_views,
230
&mut sampler,
231
cubemap_id,
232
images,
233
fallback_image,
234
);
235
}
236
}
237
238
// Pad out the bindings to the size of the binding array using fallback
239
// textures. This is necessary on D3D12 and Metal.
240
texture_views.resize(MAX_VIEW_LIGHT_PROBES, &*fallback_image.d3.texture_view);
241
242
RenderViewIrradianceVolumeBindGroupEntries::Multiple {
243
texture_views,
244
sampler: sampler.unwrap_or(&fallback_image.d3.sampler),
245
}
246
}
247
248
/// Looks up and returns the bindings for any irradiance volumes visible in
249
/// the view, as well as the sampler. This is the version used when binding
250
/// arrays aren't available on the current platform.
251
fn single(
252
render_view_irradiance_volumes: Option<&RenderViewLightProbes<IrradianceVolume>>,
253
images: &'a RenderAssets<GpuImage>,
254
fallback_image: &'a FallbackImage,
255
) -> RenderViewIrradianceVolumeBindGroupEntries<'a> {
256
if let Some(irradiance_volumes) = render_view_irradiance_volumes
257
&& let Some(irradiance_volume) = irradiance_volumes.render_light_probes.first()
258
&& irradiance_volume.texture_index >= 0
259
&& let Some(image_id) = irradiance_volumes
260
.binding_index_to_textures
261
.get(irradiance_volume.texture_index as usize)
262
&& let Some(image) = images.get(*image_id)
263
{
264
return RenderViewIrradianceVolumeBindGroupEntries::Single {
265
texture_view: &image.texture_view,
266
sampler: &image.sampler,
267
};
268
}
269
270
RenderViewIrradianceVolumeBindGroupEntries::Single {
271
texture_view: &fallback_image.d3.texture_view,
272
sampler: &fallback_image.d3.sampler,
273
}
274
}
275
}
276
277
/// Returns the bind group layout entries for the voxel texture and sampler
278
/// respectively.
279
pub(crate) fn get_bind_group_layout_entries(
280
render_device: &RenderDevice,
281
render_adapter: &RenderAdapter,
282
) -> [BindGroupLayoutEntryBuilder; 2] {
283
let mut texture_3d_binding =
284
binding_types::texture_3d(TextureSampleType::Float { filterable: true });
285
if binding_arrays_are_usable(render_device, render_adapter) {
286
texture_3d_binding =
287
texture_3d_binding.count(NonZero::<u32>::new(MAX_VIEW_LIGHT_PROBES as _).unwrap());
288
}
289
290
[
291
texture_3d_binding,
292
binding_types::sampler(SamplerBindingType::Filtering),
293
]
294
}
295
296
impl LightProbeComponent for IrradianceVolume {
297
type AssetId = AssetId<Image>;
298
299
// Irradiance volumes can't be attached to the view, so we store nothing
300
// here.
301
type ViewLightProbeInfo = ();
302
303
fn id(&self, image_assets: &RenderAssets<GpuImage>) -> Option<Self::AssetId> {
304
if image_assets.get(&self.voxels).is_none() {
305
None
306
} else {
307
Some(self.voxels.id())
308
}
309
}
310
311
fn intensity(&self) -> f32 {
312
self.intensity
313
}
314
315
fn affects_lightmapped_mesh_diffuse(&self) -> bool {
316
self.affects_lightmapped_meshes
317
}
318
319
fn create_render_view_light_probes(
320
_: Option<&Self>,
321
_: &RenderAssets<GpuImage>,
322
) -> RenderViewLightProbes<Self> {
323
RenderViewLightProbes::new()
324
}
325
}
326
327