Path: blob/main/crates/bevy_pbr/src/light_probe/irradiance_volume.rs
6604 views
//! Irradiance volumes, also known as voxel global illumination.1//!2//! An *irradiance volume* is a cuboid voxel region consisting of3//! regularly-spaced precomputed samples of diffuse indirect light. They're4//! ideal if you have a dynamic object such as a character that can move about5//! static non-moving geometry such as a level in a game, and you want that6//! dynamic object to be affected by the light bouncing off that static7//! geometry.8//!9//! To use irradiance volumes, you need to precompute, or *bake*, the indirect10//! light in your scene. Bevy doesn't currently come with a way to do this.11//! Fortunately, [Blender] provides a [baking tool] as part of the Eevee12//! renderer, and its irradiance volumes are compatible with those used by Bevy.13//! The [`bevy-baked-gi`] project provides a tool, `export-blender-gi`, that can14//! extract the baked irradiance volumes from the Blender `.blend` file and15//! package them up into a `.ktx2` texture for use by the engine. See the16//! documentation in the `bevy-baked-gi` project for more details on this17//! workflow.18//!19//! Like all light probes in Bevy, irradiance volumes are 1×1×1 cubes, centered20//! on the origin, that can be arbitrarily scaled, rotated, and positioned in a21//! scene with the [`bevy_transform::components::Transform`] component. The 3D22//! voxel grid will be stretched to fill the interior of the cube, with linear23//! interpolation, and the illumination from the irradiance volume will apply to24//! all fragments within that bounding region.25//!26//! Bevy's irradiance volumes are based on Valve's [*ambient cubes*] as used in27//! *Half-Life 2* ([Mitchell 2006, slide 27]). These encode a single color of28//! light from the six 3D cardinal directions and blend the sides together29//! according to the surface normal. For an explanation of why ambient cubes30//! were chosen over spherical harmonics, see [Why ambient cubes?] below.31//!32//! If you wish to use a tool other than `export-blender-gi` to produce the33//! irradiance volumes, you'll need to pack the irradiance volumes in the34//! following format. The irradiance volume of resolution *(Rx, Ry, Rz)* is35//! expected to be a 3D texture of dimensions *(Rx, 2Ry, 3Rz)*. The unnormalized36//! texture coordinate *(s, t, p)* of the voxel at coordinate *(x, y, z)* with37//! side *S* ∈ *{-X, +X, -Y, +Y, -Z, +Z}* is as follows:38//!39//! ```text40//! s = x41//!42//! t = y + ⎰ 0 if S ∈ {-X, -Y, -Z}43//! ⎱ Ry if S ∈ {+X, +Y, +Z}44//!45//! ⎧ 0 if S ∈ {-X, +X}46//! p = z + ⎨ Rz if S ∈ {-Y, +Y}47//! ⎩ 2Rz if S ∈ {-Z, +Z}48//! ```49//!50//! Visually, in a left-handed coordinate system with Y up, viewed from the51//! right, the 3D texture looks like a stacked series of voxel grids, one for52//! each cube side, in this order:53//!54//! | **+X** | **+Y** | **+Z** |55//! | ------ | ------ | ------ |56//! | **-X** | **-Y** | **-Z** |57//!58//! A terminology note: Other engines may refer to irradiance volumes as *voxel59//! global illumination*, *VXGI*, or simply as *light probes*. Sometimes *light60//! probe* refers to what Bevy calls a reflection probe. In Bevy, *light probe*61//! is a generic term that encompasses all cuboid bounding regions that capture62//! indirect illumination, whether based on voxels or not.63//!64//! Note that, if binding arrays aren't supported (e.g. on WebGPU or WebGL 2),65//! then only the closest irradiance volume to the view will be taken into66//! account during rendering. The required `wgpu` features are67//! [`bevy_render::settings::WgpuFeatures::TEXTURE_BINDING_ARRAY`] and68//! [`bevy_render::settings::WgpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING`].69//!70//! ## Why ambient cubes?71//!72//! This section describes the motivation behind the decision to use ambient73//! cubes in Bevy. It's not needed to use the feature; feel free to skip it74//! unless you're interested in its internal design.75//!76//! Bevy uses *Half-Life 2*-style ambient cubes (usually abbreviated as *HL2*)77//! as the representation of irradiance for light probes instead of the78//! more-popular spherical harmonics (*SH*). This might seem to be a surprising79//! choice, but it turns out to work well for the specific case of voxel80//! sampling on the GPU. Spherical harmonics have two problems that make them81//! less ideal for this use case:82//!83//! 1. The level 1 spherical harmonic coefficients can be negative. That84//! prevents the use of the efficient [RGB9E5 texture format], which only85//! encodes unsigned floating point numbers, and forces the use of the86//! less-efficient [RGBA16F format] if hardware interpolation is desired.87//!88//! 2. As an alternative to RGBA16F, level 1 spherical harmonics can be89//! normalized and scaled to the SH0 base color, as [Frostbite] does. This90//! allows them to be packed in standard LDR RGBA8 textures. However, this91//! prevents the use of hardware trilinear filtering, as the nonuniform scale92//! factor means that hardware interpolation no longer produces correct results.93//! The 8 texture fetches needed to interpolate between voxels can be upwards of94//! twice as slow as the hardware interpolation.95//!96//! The following chart summarizes the costs and benefits of ambient cubes,97//! level 1 spherical harmonics, and level 2 spherical harmonics:98//!99//! | Technique | HW-interpolated samples | Texel fetches | Bytes per voxel | Quality |100//! | ------------------------ | ----------------------- | ------------- | --------------- | ------- |101//! | Ambient cubes | 3 | 0 | 24 | Medium |102//! | Level 1 SH, compressed | 0 | 36 | 16 | Low |103//! | Level 1 SH, uncompressed | 4 | 0 | 24 | Low |104//! | Level 2 SH, compressed | 0 | 72 | 28 | High |105//! | Level 2 SH, uncompressed | 9 | 0 | 54 | High |106//!107//! (Note that the number of bytes per voxel can be reduced using various108//! texture compression methods, but the overall ratios remain similar.)109//!110//! From these data, we can see that ambient cubes balance fast lookups (from111//! leveraging hardware interpolation) with relatively-small storage112//! requirements and acceptable quality. Hence, they were chosen for irradiance113//! volumes in Bevy.114//!115//! [*ambient cubes*]: https://advances.realtimerendering.com/s2006/Mitchell-ShadingInValvesSourceEngine.pdf116//!117//! [spherical harmonics]: https://en.wikipedia.org/wiki/Spherical_harmonic_lighting118//!119//! [RGB9E5 texture format]: https://www.khronos.org/opengl/wiki/Small_Float_Formats#RGB9_E5120//!121//! [RGBA16F format]: https://www.khronos.org/opengl/wiki/Small_Float_Formats#Low-bitdepth_floats122//!123//! [Frostbite]: https://media.contentapi.ea.com/content/dam/eacom/frostbite/files/gdc2018-precomputedgiobalilluminationinfrostbite.pdf#page=53124//!125//! [Mitchell 2006, slide 27]: https://advances.realtimerendering.com/s2006/Mitchell-ShadingInValvesSourceEngine.pdf#page=27126//!127//! [Blender]: http://blender.org/128//!129//! [baking tool]: https://docs.blender.org/manual/en/latest/render/eevee/light_probes/volume.html130//!131//! [`bevy-baked-gi`]: https://github.com/pcwalton/bevy-baked-gi132//!133//! [Why ambient cubes?]: #why-ambient-cubes134135use bevy_image::Image;136use bevy_light::IrradianceVolume;137use bevy_render::{138render_asset::RenderAssets,139render_resource::{140binding_types, BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, TextureSampleType,141TextureView,142},143renderer::{RenderAdapter, RenderDevice},144texture::{FallbackImage, GpuImage},145};146use core::{num::NonZero, ops::Deref};147148use bevy_asset::AssetId;149150use crate::{151add_cubemap_texture_view, binding_arrays_are_usable, RenderViewLightProbes,152MAX_VIEW_LIGHT_PROBES,153};154155use super::LightProbeComponent;156157/// On WebGL and WebGPU, we must disable irradiance volumes, as otherwise we can158/// overflow the number of texture bindings when deferred rendering is in use159/// (see issue #11885).160pub(crate) const IRRADIANCE_VOLUMES_ARE_USABLE: bool = cfg!(not(target_arch = "wasm32"));161162/// All the bind group entries necessary for PBR shaders to access the163/// irradiance volumes exposed to a view.164pub(crate) enum RenderViewIrradianceVolumeBindGroupEntries<'a> {165/// The version used when binding arrays aren't available on the current platform.166Single {167/// The texture view of the closest light probe.168texture_view: &'a TextureView,169/// A sampler used to sample voxels of the irradiance volume.170sampler: &'a Sampler,171},172173/// The version used when binding arrays are available on the current174/// platform.175Multiple {176/// A texture view of the voxels of each irradiance volume, in the same177/// order that they are supplied to the view (i.e. in the same order as178/// `binding_index_to_cubemap` in [`RenderViewLightProbes`]).179///180/// This is a vector of `wgpu::TextureView`s. But we don't want to import181/// `wgpu` in this crate, so we refer to it indirectly like this.182texture_views: Vec<&'a <TextureView as Deref>::Target>,183184/// A sampler used to sample voxels of the irradiance volumes.185sampler: &'a Sampler,186},187}188189impl<'a> RenderViewIrradianceVolumeBindGroupEntries<'a> {190/// Looks up and returns the bindings for any irradiance volumes visible in191/// the view, as well as the sampler.192pub(crate) fn get(193render_view_irradiance_volumes: Option<&RenderViewLightProbes<IrradianceVolume>>,194images: &'a RenderAssets<GpuImage>,195fallback_image: &'a FallbackImage,196render_device: &RenderDevice,197render_adapter: &RenderAdapter,198) -> RenderViewIrradianceVolumeBindGroupEntries<'a> {199if binding_arrays_are_usable(render_device, render_adapter) {200RenderViewIrradianceVolumeBindGroupEntries::get_multiple(201render_view_irradiance_volumes,202images,203fallback_image,204)205} else {206RenderViewIrradianceVolumeBindGroupEntries::single(207render_view_irradiance_volumes,208images,209fallback_image,210)211}212}213214/// Looks up and returns the bindings for any irradiance volumes visible in215/// the view, as well as the sampler. This is the version used when binding216/// arrays are available on the current platform.217fn get_multiple(218render_view_irradiance_volumes: Option<&RenderViewLightProbes<IrradianceVolume>>,219images: &'a RenderAssets<GpuImage>,220fallback_image: &'a FallbackImage,221) -> RenderViewIrradianceVolumeBindGroupEntries<'a> {222let mut texture_views = vec![];223let mut sampler = None;224225if let Some(irradiance_volumes) = render_view_irradiance_volumes {226for &cubemap_id in &irradiance_volumes.binding_index_to_textures {227add_cubemap_texture_view(228&mut texture_views,229&mut sampler,230cubemap_id,231images,232fallback_image,233);234}235}236237// Pad out the bindings to the size of the binding array using fallback238// textures. This is necessary on D3D12 and Metal.239texture_views.resize(MAX_VIEW_LIGHT_PROBES, &*fallback_image.d3.texture_view);240241RenderViewIrradianceVolumeBindGroupEntries::Multiple {242texture_views,243sampler: sampler.unwrap_or(&fallback_image.d3.sampler),244}245}246247/// Looks up and returns the bindings for any irradiance volumes visible in248/// the view, as well as the sampler. This is the version used when binding249/// arrays aren't available on the current platform.250fn single(251render_view_irradiance_volumes: Option<&RenderViewLightProbes<IrradianceVolume>>,252images: &'a RenderAssets<GpuImage>,253fallback_image: &'a FallbackImage,254) -> RenderViewIrradianceVolumeBindGroupEntries<'a> {255if let Some(irradiance_volumes) = render_view_irradiance_volumes256&& let Some(irradiance_volume) = irradiance_volumes.render_light_probes.first()257&& irradiance_volume.texture_index >= 0258&& let Some(image_id) = irradiance_volumes259.binding_index_to_textures260.get(irradiance_volume.texture_index as usize)261&& let Some(image) = images.get(*image_id)262{263return RenderViewIrradianceVolumeBindGroupEntries::Single {264texture_view: &image.texture_view,265sampler: &image.sampler,266};267}268269RenderViewIrradianceVolumeBindGroupEntries::Single {270texture_view: &fallback_image.d3.texture_view,271sampler: &fallback_image.d3.sampler,272}273}274}275276/// Returns the bind group layout entries for the voxel texture and sampler277/// respectively.278pub(crate) fn get_bind_group_layout_entries(279render_device: &RenderDevice,280render_adapter: &RenderAdapter,281) -> [BindGroupLayoutEntryBuilder; 2] {282let mut texture_3d_binding =283binding_types::texture_3d(TextureSampleType::Float { filterable: true });284if binding_arrays_are_usable(render_device, render_adapter) {285texture_3d_binding =286texture_3d_binding.count(NonZero::<u32>::new(MAX_VIEW_LIGHT_PROBES as _).unwrap());287}288289[290texture_3d_binding,291binding_types::sampler(SamplerBindingType::Filtering),292]293}294295impl LightProbeComponent for IrradianceVolume {296type AssetId = AssetId<Image>;297298// Irradiance volumes can't be attached to the view, so we store nothing299// here.300type ViewLightProbeInfo = ();301302fn id(&self, image_assets: &RenderAssets<GpuImage>) -> Option<Self::AssetId> {303if image_assets.get(&self.voxels).is_none() {304None305} else {306Some(self.voxels.id())307}308}309310fn intensity(&self) -> f32 {311self.intensity312}313314fn affects_lightmapped_mesh_diffuse(&self) -> bool {315self.affects_lightmapped_meshes316}317318fn create_render_view_light_probes(319_: Option<&Self>,320_: &RenderAssets<GpuImage>,321) -> RenderViewLightProbes<Self> {322RenderViewLightProbes::new()323}324}325326327