Implement minimal reflection probes (fixed macOS, iOS, and Android). (#11366)

This pull request re-submits #10057, which was backed out for breaking
macOS, iOS, and Android. I've tested this version on macOS and Android
and on the iOS simulator.

# Objective

This pull request implements *reflection probes*, which generalize
environment maps to allow for multiple environment maps in the same
scene, each of which has an axis-aligned bounding box. This is a
standard feature of physically-based renderers and was inspired by [the
corresponding feature in Blender's Eevee renderer].

## Solution

This is a minimal implementation of reflection probes that allows
artists to define cuboid bounding regions associated with environment
maps. For every view, on every frame, a system builds up a list of the
nearest 4 reflection probes that are within the view's frustum and
supplies that list to the shader. The PBR fragment shader searches
through the list, finds the first containing reflection probe, and uses
it for indirect lighting, falling back to the view's environment map if
none is found. Both forward and deferred renderers are fully supported.

A reflection probe is an entity with a pair of components, *LightProbe*
and *EnvironmentMapLight* (as well as the standard *SpatialBundle*, to
position it in the world). The *LightProbe* component (along with the
*Transform*) defines the bounding region, while the
*EnvironmentMapLight* component specifies the associated diffuse and
specular cubemaps.

A frequent question is "why two components instead of just one?" The
advantages of this setup are:

1. It's readily extensible to other types of light probes, in particular
*irradiance volumes* (also known as ambient cubes or voxel global
illumination), which use the same approach of bounding cuboids. With a
single component that applies to both reflection probes and irradiance
volumes, we can share the logic that implements falloff and blending
between multiple light probes between both of those features.

2. It reduces duplication between the existing *EnvironmentMapLight* and
these new reflection probes. Systems can treat environment maps attached
to cameras the same way they treat environment maps applied to
reflection probes if they wish.

Internally, we gather up all environment maps in the scene and place
them in a cubemap array. At present, this means that all environment
maps must have the same size, mipmap count, and texture format. A
warning is emitted if this restriction is violated. We could potentially
relax this in the future as part of the automatic mipmap generation
work, which could easily do texture format conversion as part of its
preprocessing.

An easy way to generate reflection probe cubemaps is to bake them in
Blender and use the `export-blender-gi` tool that's part of the
[`bevy-baked-gi`] project. This tool takes a `.blend` file containing
baked cubemaps as input and exports cubemap images, pre-filtered with an
embedded fork of the [glTF IBL Sampler], alongside a corresponding
`.scn.ron` file that the scene spawner can use to recreate the
reflection probes.

Note that this is intentionally a minimal implementation, to aid
reviewability. Known issues are:

* Reflection probes are basically unsupported on WebGL 2, because WebGL
2 has no cubemap arrays. (Strictly speaking, you can have precisely one
reflection probe in the scene if you have no other cubemaps anywhere,
but this isn't very useful.)

* Reflection probes have no falloff, so reflections will abruptly change
when objects move from one bounding region to another.

* As mentioned before, all cubemaps in the world of a given type
(diffuse or specular) must have the same size, format, and mipmap count.

Future work includes:

* Blending between multiple reflection probes.

* A falloff/fade-out region so that reflected objects disappear
gradually instead of vanishing all at once.

* Irradiance volumes for voxel-based global illumination. This should
reuse much of the reflection probe logic, as they're both GI techniques
based on cuboid bounding regions.

* Support for WebGL 2, by breaking batches when reflection probes are
used.

These issues notwithstanding, I think it's best to land this with
roughly the current set of functionality, because this patch is useful
as is and adding everything above would make the pull request
significantly larger and harder to review.

---

## Changelog

### Added

* A new *LightProbe* component is available that specifies a bounding
region that an *EnvironmentMapLight* applies to. The combination of a
*LightProbe* and an *EnvironmentMapLight* offers *reflection probe*
functionality similar to that available in other engines.

[the corresponding feature in Blender's Eevee renderer]:
https://docs.blender.org/manual/en/latest/render/eevee/light_probes/reflection_cubemaps.html

[`bevy-baked-gi`]: https://github.com/pcwalton/bevy-baked-gi

[glTF IBL Sampler]: https://github.com/KhronosGroup/glTF-IBL-Sampler
This commit is contained in:
Patrick Walton 2024-01-18 23:33:52 -08:00 committed by GitHub
parent f795656d65
commit 83d6600267
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1582 additions and 264 deletions

View file

@ -2508,6 +2508,17 @@ name = "fallback_image"
path = "examples/shader/fallback_image.rs"
doc-scrape-examples = true
[[example]]
name = "reflection_probes"
path = "examples/3d/reflection_probes.rs"
doc-scrape-examples = true
[package.metadata.example.reflection_probes]
name = "Reflection Probes"
description = "Demonstrates reflection probes"
category = "3D Rendering"
wasm = false
[package.metadata.example.fallback_image]
hidden = true

Binary file not shown.

View file

@ -7,8 +7,8 @@
@group(0) @binding(3) var dt_lut_texture: texture_3d<f32>;
@group(0) @binding(4) var dt_lut_sampler: sampler;
#else
@group(0) @binding(15) var dt_lut_texture: texture_3d<f32>;
@group(0) @binding(16) var dt_lut_sampler: sampler;
@group(0) @binding(16) var dt_lut_texture: texture_3d<f32>;
@group(0) @binding(17) var dt_lut_sampler: sampler;
#endif
fn sample_current_lut(p: vec3<f32>) -> vec3<f32> {

View file

@ -58,7 +58,10 @@ symphonia-vorbis = ["bevy_audio/symphonia-vorbis"]
symphonia-wav = ["bevy_audio/symphonia-wav"]
# Shader formats
shader_format_glsl = ["bevy_render/shader_format_glsl"]
shader_format_glsl = [
"bevy_render/shader_format_glsl",
"bevy_pbr?/shader_format_glsl",
]
shader_format_spirv = ["bevy_render/shader_format_spirv"]
serialize = [

View file

@ -10,6 +10,7 @@ keywords = ["bevy"]
[features]
webgl = []
shader_format_glsl = ["naga_oil/glsl"]
pbr_transmission_textures = []
[dependencies]
@ -34,8 +35,17 @@ fixedbitset = "0.4"
# direct dependency required for derive macro
bytemuck = { version = "1", features = ["derive"] }
radsort = "0.1"
naga_oil = "0.11"
smallvec = "1.6"
thread_local = "1.0"
[target.'cfg(target_arch = "wasm32")'.dependencies]
naga_oil = { version = "0.11" }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
# Omit the `glsl` feature in non-WebAssembly by default.
naga_oil = { version = "0.11", default-features = false, features = [
"test_shader",
] }
[lints]
workspace = true

View file

@ -1,4 +1,7 @@
use crate::{MeshPipeline, MeshViewBindGroup, ScreenSpaceAmbientOcclusionSettings};
use crate::{
environment_map::RenderViewEnvironmentMaps, MeshPipeline, MeshViewBindGroup,
ScreenSpaceAmbientOcclusionSettings, ViewLightProbesUniformOffset,
};
use bevy_app::prelude::*;
use bevy_asset::{load_internal_asset, Handle};
use bevy_core_pipeline::{
@ -14,25 +17,17 @@ use bevy_render::{
extract_component::{
ComponentUniforms, ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin,
},
render_asset::RenderAssets,
render_graph::{NodeRunError, RenderGraphContext, ViewNode, ViewNodeRunner},
render_resource::{
binding_types::uniform_buffer, Operations, PipelineCache, RenderPassDescriptor,
},
render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner},
render_resource::binding_types::uniform_buffer,
render_resource::*,
renderer::{RenderContext, RenderDevice},
texture::Image,
view::{ViewTarget, ViewUniformOffset},
Render, RenderSet,
};
use bevy_render::{
render_graph::RenderGraphApp, render_resource::*, texture::BevyDefault, view::ExtractedView,
RenderApp,
texture::BevyDefault,
view::{ExtractedView, ViewTarget, ViewUniformOffset},
Render, RenderApp, RenderSet,
};
use crate::{
EnvironmentMapLight, MeshPipelineKey, ShadowFilteringMethod, ViewFogUniformOffset,
ViewLightsUniformOffset,
MeshPipelineKey, ShadowFilteringMethod, ViewFogUniformOffset, ViewLightsUniformOffset,
};
pub struct DeferredPbrLightingPlugin;
@ -151,6 +146,7 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode {
&'static ViewUniformOffset,
&'static ViewLightsUniformOffset,
&'static ViewFogUniformOffset,
&'static ViewLightProbesUniformOffset,
&'static MeshViewBindGroup,
&'static ViewTarget,
&'static DeferredLightingIdDepthTexture,
@ -165,6 +161,7 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode {
view_uniform_offset,
view_lights_offset,
view_fog_offset,
view_light_probes_offset,
mesh_view_bind_group,
target,
deferred_lighting_id_depth_texture,
@ -218,6 +215,7 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode {
view_uniform_offset.offset,
view_lights_offset.offset,
view_fog_offset.offset,
**view_light_probes_offset,
],
);
render_pass.set_bind_group(1, &bind_group_1, &[]);
@ -403,7 +401,6 @@ pub fn prepare_deferred_lighting_pipelines(
&ExtractedView,
Option<&Tonemapping>,
Option<&DebandDither>,
Option<&EnvironmentMapLight>,
Option<&ShadowFilteringMethod>,
Has<ScreenSpaceAmbientOcclusionSettings>,
(
@ -411,20 +408,20 @@ pub fn prepare_deferred_lighting_pipelines(
Has<DepthPrepass>,
Has<MotionVectorPrepass>,
),
Has<RenderViewEnvironmentMaps>,
),
With<DeferredPrepass>,
>,
images: Res<RenderAssets<Image>>,
) {
for (
entity,
view,
tonemapping,
dither,
environment_map,
shadow_filter_method,
ssao,
(normal_prepass, depth_prepass, motion_vector_prepass),
has_environment_maps,
) in &views
{
let mut view_key = MeshPipelineKey::from_hdr(view.hdr);
@ -471,11 +468,10 @@ pub fn prepare_deferred_lighting_pipelines(
view_key |= MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION;
}
let environment_map_loaded = match environment_map {
Some(environment_map) => environment_map.is_loaded(&images),
None => false,
};
if environment_map_loaded {
// We don't need to check to see whether the environment map is loaded
// because [`gather_light_probes`] already checked that for us before
// adding the [`RenderViewEnvironmentMaps`] component.
if has_environment_maps {
view_key |= MeshPipelineKey::ENVIRONMENT_MAP;
}

View file

@ -1,50 +0,0 @@
#define_import_path bevy_pbr::environment_map
#import bevy_pbr::mesh_view_bindings as bindings;
struct EnvironmentMapLight {
diffuse: vec3<f32>,
specular: vec3<f32>,
};
fn environment_map_light(
perceptual_roughness: f32,
roughness: f32,
diffuse_color: vec3<f32>,
NdotV: f32,
f_ab: vec2<f32>,
N: vec3<f32>,
R: vec3<f32>,
F0: vec3<f32>,
) -> EnvironmentMapLight {
// Split-sum approximation for image based lighting: https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf
// Technically we could use textureNumLevels(environment_map_specular) - 1 here, but we use a uniform
// because textureNumLevels() does not work on WebGL2
let radiance_level = perceptual_roughness * f32(bindings::lights.environment_map_smallest_specular_mip_level);
let irradiance = textureSampleLevel(bindings::environment_map_diffuse, bindings::environment_map_sampler, vec3(N.xy, -N.z), 0.0).rgb;
let radiance = textureSampleLevel(bindings::environment_map_specular, bindings::environment_map_sampler, vec3(R.xy, -R.z), radiance_level).rgb;
// No real world material has specular values under 0.02, so we use this range as a
// "pre-baked specular occlusion" that extinguishes the fresnel term, for artistic control.
// See: https://google.github.io/filament/Filament.html#specularocclusion
let specular_occlusion = saturate(dot(F0, vec3(50.0 * 0.33)));
// Multiscattering approximation: https://www.jcgt.org/published/0008/01/03/paper.pdf
// Useful reference: https://bruop.github.io/ibl
let Fr = max(vec3(1.0 - roughness), F0) - F0;
let kS = F0 + Fr * pow(1.0 - NdotV, 5.0);
let Ess = f_ab.x + f_ab.y;
let FssEss = kS * Ess * specular_occlusion;
let Ems = 1.0 - Ess;
let Favg = F0 + (1.0 - F0) / 21.0;
let Fms = FssEss * Favg / (1.0 - Ems * Favg);
let FmsEms = Fms * Ems;
let Edss = 1.0 - (FssEss + FmsEms);
let kD = diffuse_color * Edss;
var out: EnvironmentMapLight;
out.diffuse = (FmsEms + kD) * irradiance * bindings::lights.environment_map_intensity;
out.specular = FssEss * radiance * bindings::lights.environment_map_intensity;
return out;
}

View file

@ -1,98 +0,0 @@
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, Handle};
use bevy_core_pipeline::prelude::Camera3d;
use bevy_ecs::{prelude::Component, query::With};
use bevy_reflect::Reflect;
use bevy_render::{
extract_component::{ExtractComponent, ExtractComponentPlugin},
render_asset::RenderAssets,
render_resource::{
binding_types::{sampler, texture_cube},
*,
},
texture::{FallbackImageCubemap, Image},
};
pub const ENVIRONMENT_MAP_SHADER_HANDLE: Handle<Shader> =
Handle::weak_from_u128(154476556247605696);
pub struct EnvironmentMapPlugin;
impl Plugin for EnvironmentMapPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
ENVIRONMENT_MAP_SHADER_HANDLE,
"environment_map.wgsl",
Shader::from_wgsl
);
app.register_type::<EnvironmentMapLight>()
.add_plugins(ExtractComponentPlugin::<EnvironmentMapLight>::default());
}
}
/// Environment map based ambient lighting representing light from distant scenery.
///
/// When added to a 3D camera, this component adds indirect light
/// to every point of the scene (including inside, enclosed areas) based on
/// an environment cubemap texture. This is similar to [`crate::AmbientLight`], but
/// higher quality, and is intended for outdoor scenes.
///
/// The environment map must be prefiltered into a diffuse and specular cubemap based on the
/// [split-sum approximation](https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf).
///
/// To prefilter your environment map, you can use `KhronosGroup`'s [glTF-IBL-Sampler](https://github.com/KhronosGroup/glTF-IBL-Sampler).
/// The diffuse map uses the Lambertian distribution, and the specular map uses the GGX distribution.
///
/// `KhronosGroup` also has several prefiltered environment maps that can be found [here](https://github.com/KhronosGroup/glTF-Sample-Environments).
#[derive(Component, Reflect, Clone, ExtractComponent)]
#[extract_component_filter(With<Camera3d>)]
pub struct EnvironmentMapLight {
pub diffuse_map: Handle<Image>,
pub specular_map: Handle<Image>,
/// Scale factor applied to the diffuse and specular light generated by this component.
///
/// After applying this multiplier, the resulting values should
/// be in units of [cd/m^2](https://en.wikipedia.org/wiki/Candela_per_square_metre).
///
/// See also <https://google.github.io/filament/Filament.html#lighting/imagebasedlights/iblunit>.
pub intensity: f32,
}
impl EnvironmentMapLight {
/// Whether or not all textures necessary to use the environment map
/// have been loaded by the asset server.
pub fn is_loaded(&self, images: &RenderAssets<Image>) -> bool {
images.get(&self.diffuse_map).is_some() && images.get(&self.specular_map).is_some()
}
}
pub fn get_bindings<'a>(
environment_map_light: Option<&EnvironmentMapLight>,
images: &'a RenderAssets<Image>,
fallback_image_cubemap: &'a FallbackImageCubemap,
) -> (&'a TextureView, &'a TextureView, &'a Sampler) {
let (diffuse_map, specular_map) = match (
environment_map_light.and_then(|env_map| images.get(&env_map.diffuse_map)),
environment_map_light.and_then(|env_map| images.get(&env_map.specular_map)),
) {
(Some(diffuse_map), Some(specular_map)) => {
(&diffuse_map.texture_view, &specular_map.texture_view)
}
_ => (
&fallback_image_cubemap.texture_view,
&fallback_image_cubemap.texture_view,
),
};
(diffuse_map, specular_map, &fallback_image_cubemap.sampler)
}
pub fn get_bind_group_layout_entries() -> [BindGroupLayoutEntryBuilder; 3] {
[
texture_cube(TextureSampleType::Float { filterable: true }),
texture_cube(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
]
}

View file

@ -3,10 +3,10 @@ pub mod wireframe;
mod alpha;
mod bundle;
pub mod deferred;
mod environment_map;
mod extended_material;
mod fog;
mod light;
mod light_probe;
mod lightmap;
mod material;
mod parallax;
@ -17,10 +17,10 @@ mod ssao;
pub use alpha::*;
pub use bundle::*;
pub use environment_map::EnvironmentMapLight;
pub use extended_material::*;
pub use fog::*;
pub use light::*;
pub use light_probe::*;
pub use lightmap::*;
pub use material::*;
pub use parallax::*;
@ -37,9 +37,12 @@ pub mod prelude {
DirectionalLightBundle, MaterialMeshBundle, PbrBundle, PointLightBundle,
SpotLightBundle,
},
environment_map::EnvironmentMapLight,
fog::{FogFalloff, FogSettings},
light::{AmbientLight, DirectionalLight, PointLight, SpotLight},
light_probe::{
environment_map::{EnvironmentMapLight, ReflectionProbeBundle},
LightProbe,
},
material::{Material, MaterialPlugin},
parallax::ParallaxMappingMethod,
pbr_material::StandardMaterial,
@ -71,7 +74,6 @@ use bevy_render::{
ExtractSchedule, Render, RenderApp, RenderSet,
};
use bevy_transform::TransformSystem;
use environment_map::EnvironmentMapPlugin;
use crate::deferred::DeferredPbrLightingPlugin;
@ -255,12 +257,12 @@ impl Plugin for PbrPlugin {
..Default::default()
},
ScreenSpaceAmbientOcclusionPlugin,
EnvironmentMapPlugin,
ExtractResourcePlugin::<AmbientLight>::default(),
FogPlugin,
ExtractResourcePlugin::<DefaultOpaqueRendererMethod>::default(),
ExtractComponentPlugin::<ShadowFilteringMethod>::default(),
LightmapPlugin,
LightProbePlugin,
))
.configure_sets(
PostUpdate,

View file

@ -0,0 +1,370 @@
//! Environment maps and reflection probes.
//!
//! An *environment map* consists of a pair of diffuse and specular cubemaps
//! that together reflect the static surrounding area of a region in space. When
//! available, the PBR shader uses these to apply diffuse light and calculate
//! specular reflections.
//!
//! Environment maps come in two flavors, depending on what other components the
//! entities they're attached to have:
//!
//! 1. If attached to a view, they represent the objects located a very far
//! distance from the view, in a similar manner to a skybox. Essentially, these
//! *view environment maps* represent a higher-quality replacement for
//! [`crate::AmbientLight`] for outdoor scenes. The indirect light from such
//! environment maps are added to every point of the scene, including
//! interior enclosed areas.
//!
//! 2. If attached to a [`LightProbe`], environment maps represent the immediate
//! surroundings of a specific location in the scene. These types of
//! environment maps are known as *reflection probes*.
//! [`ReflectionProbeBundle`] is available as a mechanism to conveniently add
//! these to a scene.
//!
//! Typically, environment maps are static (i.e. "baked", calculated ahead of
//! time) and so only reflect fixed static geometry. The environment maps must
//! be pre-filtered into a pair of cubemaps, one for the diffuse component and
//! one for the specular component, according to the [split-sum approximation].
//! To pre-filter your environment map, you can use the [glTF IBL Sampler] or
//! its [artist-friendly UI]. The diffuse map uses the Lambertian distribution,
//! while the specular map uses the GGX distribution.
//!
//! The Khronos Group has [several pre-filtered environment maps] available for
//! you to use.
//!
//! Currently, reflection probes (i.e. environment maps attached to light
//! probes) use binding arrays (also known as bindless textures) and
//! consequently aren't supported on WebGL2 or WebGPU. Reflection probes are
//! also unsupported if GLSL is in use, due to `naga` limitations. Environment
//! maps attached to views are, however, supported on all platforms.
//!
//! [split-sum approximation]: https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf
//!
//! [glTF IBL Sampler]: https://github.com/KhronosGroup/glTF-IBL-Sampler
//!
//! [artist-friendly UI]: https://github.com/pcwalton/gltf-ibl-sampler-egui
//!
//! [several pre-filtered environment maps]: https://github.com/KhronosGroup/glTF-Sample-Environments
use bevy_asset::{AssetId, Handle};
use bevy_ecs::{
bundle::Bundle, component::Component, query::QueryItem, system::lifetimeless::Read,
};
use bevy_reflect::Reflect;
use bevy_render::{
extract_instances::ExtractInstance,
prelude::SpatialBundle,
render_asset::RenderAssets,
render_resource::{
binding_types, BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, Shader,
TextureSampleType, TextureView,
},
renderer::RenderDevice,
settings::WgpuFeatures,
texture::{FallbackImage, Image},
};
use bevy_utils::HashMap;
use std::num::NonZeroU32;
use std::ops::Deref;
use crate::{LightProbe, MAX_VIEW_REFLECTION_PROBES};
/// A handle to the environment map helper shader.
pub const ENVIRONMENT_MAP_SHADER_HANDLE: Handle<Shader> =
Handle::weak_from_u128(154476556247605696);
/// How many texture bindings are used in the fragment shader, *not* counting
/// environment maps.
const STANDARD_MATERIAL_FRAGMENT_SHADER_MIN_TEXTURE_BINDINGS: usize = 16;
/// A pair of cubemap textures that represent the surroundings of a specific
/// area in space.
///
/// See [`crate::environment_map`] for detailed information.
#[derive(Clone, Component, Reflect)]
pub struct EnvironmentMapLight {
/// The blurry image that represents diffuse radiance surrounding a region.
pub diffuse_map: Handle<Image>,
/// The typically-sharper, mipmapped image that represents specular radiance
/// surrounding a region.
pub specular_map: Handle<Image>,
/// Scale factor applied to the diffuse and specular light generated by this component.
///
/// After applying this multiplier, the resulting values should
/// be in units of [cd/m^2](https://en.wikipedia.org/wiki/Candela_per_square_metre).
///
/// See also <https://google.github.io/filament/Filament.html#lighting/imagebasedlights/iblunit>.
pub intensity: f32,
}
/// Like [`EnvironmentMapLight`], but contains asset IDs instead of handles.
///
/// This is for use in the render app.
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct EnvironmentMapIds {
/// The blurry image that represents diffuse radiance surrounding a region.
pub(crate) diffuse: AssetId<Image>,
/// The typically-sharper, mipmapped image that represents specular radiance
/// surrounding a region.
pub(crate) specular: AssetId<Image>,
}
/// A bundle that contains everything needed to make an entity a reflection
/// probe.
///
/// A reflection probe is a type of environment map that specifies the light
/// surrounding a region in space. For more information, see
/// [`crate::environment_map`].
#[derive(Bundle)]
pub struct ReflectionProbeBundle {
/// Contains a transform that specifies the position of this reflection probe in space.
pub spatial: SpatialBundle,
/// Marks this environment map as a light probe.
pub light_probe: LightProbe,
/// The cubemaps that make up this environment map.
pub environment_map: EnvironmentMapLight,
}
/// A component, part of the render world, that stores the mapping from
/// environment map ID to texture index in the diffuse and specular binding
/// arrays.
///
/// Cubemap textures belonging to environment maps are collected into binding
/// arrays, and the index of each texture is presented to the shader for runtime
/// lookup.
///
/// This component is attached to each view in the render world, because each
/// view may have a different set of cubemaps that it considers and therefore
/// cubemap indices are per-view.
#[derive(Component, Default)]
pub struct RenderViewEnvironmentMaps {
/// The list of environment maps presented to the shader, in order.
binding_index_to_cubemap: Vec<EnvironmentMapIds>,
/// The reverse of `binding_index_to_cubemap`: a map from the environment
/// map IDs to the index in `binding_index_to_cubemap`.
cubemap_to_binding_index: HashMap<EnvironmentMapIds, u32>,
}
/// All the bind group entries necessary for PBR shaders to access the
/// environment maps exposed to a view.
pub(crate) enum RenderViewBindGroupEntries<'a> {
/// The version used when binding arrays aren't available on the current
/// platform.
Single {
/// The texture view of the view's diffuse cubemap.
diffuse_texture_view: &'a TextureView,
/// The texture view of the view's specular cubemap.
specular_texture_view: &'a TextureView,
/// The sampler used to sample elements of both `diffuse_texture_views` and
/// `specular_texture_views`.
sampler: &'a Sampler,
},
/// The version used when binding arrays aren't available on the current
/// platform.
Multiple {
/// A texture view of each diffuse cubemap, in the same order that they are
/// supplied to the view (i.e. in the same order as
/// `binding_index_to_cubemap` in [`RenderViewEnvironmentMaps`]).
///
/// This is a vector of `wgpu::TextureView`s. But we don't want to import
/// `wgpu` in this crate, so we refer to it indirectly like this.
diffuse_texture_views: Vec<&'a <TextureView as Deref>::Target>,
/// As above, but for specular cubemaps.
specular_texture_views: Vec<&'a <TextureView as Deref>::Target>,
/// The sampler used to sample elements of both `diffuse_texture_views` and
/// `specular_texture_views`.
sampler: &'a Sampler,
},
}
impl ExtractInstance for EnvironmentMapIds {
type Data = Read<EnvironmentMapLight>;
type Filter = ();
fn extract(item: QueryItem<'_, Self::Data>) -> Option<Self> {
Some(EnvironmentMapIds {
diffuse: item.diffuse_map.id(),
specular: item.specular_map.id(),
})
}
}
impl RenderViewEnvironmentMaps {
pub(crate) fn new() -> Self {
Self::default()
}
}
impl RenderViewEnvironmentMaps {
/// Whether there are no environment maps associated with the view.
pub(crate) fn is_empty(&self) -> bool {
self.binding_index_to_cubemap.is_empty()
}
/// Adds a cubemap to the list of bindings, if it wasn't there already, and
/// returns its index within that list.
pub(crate) fn get_or_insert_cubemap(&mut self, cubemap_id: &EnvironmentMapIds) -> u32 {
*self
.cubemap_to_binding_index
.entry(*cubemap_id)
.or_insert_with(|| {
let index = self.binding_index_to_cubemap.len() as u32;
self.binding_index_to_cubemap.push(*cubemap_id);
index
})
}
}
/// Returns the bind group layout entries for the environment map diffuse and
/// specular binding arrays respectively, in addition to the sampler.
pub(crate) fn get_bind_group_layout_entries(
render_device: &RenderDevice,
) -> [BindGroupLayoutEntryBuilder; 3] {
let mut texture_cube_binding =
binding_types::texture_cube(TextureSampleType::Float { filterable: true });
if binding_arrays_are_usable(render_device) {
texture_cube_binding =
texture_cube_binding.count(NonZeroU32::new(MAX_VIEW_REFLECTION_PROBES as _).unwrap());
}
[
texture_cube_binding,
texture_cube_binding,
binding_types::sampler(SamplerBindingType::Filtering),
]
}
impl<'a> RenderViewBindGroupEntries<'a> {
/// Looks up and returns the bindings for the environment map diffuse and
/// specular binding arrays respectively, as well as the sampler.
pub(crate) fn get(
render_view_environment_maps: Option<&RenderViewEnvironmentMaps>,
images: &'a RenderAssets<Image>,
fallback_image: &'a FallbackImage,
render_device: &RenderDevice,
) -> RenderViewBindGroupEntries<'a> {
if binding_arrays_are_usable(render_device) {
let mut diffuse_texture_views = vec![];
let mut specular_texture_views = vec![];
let mut sampler = None;
if let Some(environment_maps) = render_view_environment_maps {
for &cubemap_id in &environment_maps.binding_index_to_cubemap {
add_texture_view(
&mut diffuse_texture_views,
&mut sampler,
cubemap_id.diffuse,
images,
fallback_image,
);
add_texture_view(
&mut specular_texture_views,
&mut sampler,
cubemap_id.specular,
images,
fallback_image,
);
}
}
// Pad out the bindings to the size of the binding array using fallback
// textures. This is necessary on D3D12 and Metal.
diffuse_texture_views.resize(
MAX_VIEW_REFLECTION_PROBES,
&*fallback_image.cube.texture_view,
);
specular_texture_views.resize(
MAX_VIEW_REFLECTION_PROBES,
&*fallback_image.cube.texture_view,
);
return RenderViewBindGroupEntries::Multiple {
diffuse_texture_views,
specular_texture_views,
sampler: sampler.unwrap_or(&fallback_image.cube.sampler),
};
}
if let Some(environment_maps) = render_view_environment_maps {
if let Some(cubemap) = environment_maps.binding_index_to_cubemap.first() {
if let (Some(diffuse_image), Some(specular_image)) =
(images.get(cubemap.diffuse), images.get(cubemap.specular))
{
return RenderViewBindGroupEntries::Single {
diffuse_texture_view: &diffuse_image.texture_view,
specular_texture_view: &specular_image.texture_view,
sampler: &diffuse_image.sampler,
};
}
}
}
RenderViewBindGroupEntries::Single {
diffuse_texture_view: &fallback_image.cube.texture_view,
specular_texture_view: &fallback_image.cube.texture_view,
sampler: &fallback_image.cube.sampler,
}
}
}
/// Adds a diffuse or specular texture view to the `texture_views` list, and
/// populates `sampler` if this is the first such view.
fn add_texture_view<'a>(
texture_views: &mut Vec<&'a <TextureView as Deref>::Target>,
sampler: &mut Option<&'a Sampler>,
image_id: AssetId<Image>,
images: &'a RenderAssets<Image>,
fallback_image: &'a FallbackImage,
) {
match images.get(image_id) {
None => {
// Use the fallback image if the cubemap isn't loaded yet.
texture_views.push(&*fallback_image.cube.texture_view);
}
Some(image) => {
// If this is the first texture view, populate `sampler`.
if sampler.is_none() {
*sampler = Some(&image.sampler);
}
texture_views.push(&*image.texture_view);
}
}
}
/// Many things can go wrong when attempting to use texture binding arrays
/// (a.k.a. bindless textures). This function checks for these pitfalls:
///
/// 1. If GLSL support is enabled at the feature level, then in debug mode
/// `naga_oil` will attempt to compile all shader modules under GLSL to check
/// validity of names, even if GLSL isn't actually used. This will cause a crash
/// if binding arrays are enabled, because binding arrays are currently
/// unimplemented in the GLSL backend of Naga. Therefore, we disable binding
/// arrays if the `shader_format_glsl` feature is present.
///
/// 2. If there aren't enough texture bindings available to accommodate all the
/// binding arrays, the driver will panic. So we also bail out if there aren't
/// enough texture bindings available in the fragment shader.
///
/// 3. If binding arrays aren't supported on the hardware, then we obviously
/// can't use them.
///
/// If binding arrays aren't usable, we disable reflection probes, as they rely
/// on them.
pub(crate) fn binding_arrays_are_usable(render_device: &RenderDevice) -> bool {
!cfg!(feature = "shader_format_glsl")
&& render_device.limits().max_storage_textures_per_shader_stage
>= (STANDARD_MATERIAL_FRAGMENT_SHADER_MIN_TEXTURE_BINDINGS + MAX_VIEW_REFLECTION_PROBES)
as u32
&& render_device
.features()
.contains(WgpuFeatures::TEXTURE_BINDING_ARRAY)
}

View file

@ -0,0 +1,176 @@
#define_import_path bevy_pbr::environment_map
#import bevy_pbr::mesh_view_bindings as bindings
#import bevy_pbr::mesh_view_bindings::light_probes
struct EnvironmentMapLight {
diffuse: vec3<f32>,
specular: vec3<f32>,
};
struct EnvironmentMapRadiances {
irradiance: vec3<f32>,
radiance: vec3<f32>,
}
// Define two versions of this function, one for the case in which there are
// multiple light probes and one for the case in which only the view light probe
// is present.
#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY
fn compute_radiances(
perceptual_roughness: f32,
N: vec3<f32>,
R: vec3<f32>,
world_position: vec3<f32>,
) -> EnvironmentMapRadiances {
var radiances: EnvironmentMapRadiances;
// Search for a reflection probe that contains the fragment.
//
// TODO: Interpolate between multiple reflection probes.
var cubemap_index: i32 = -1;
var intensity: f32 = 1.0;
for (var reflection_probe_index: i32 = 0;
reflection_probe_index < light_probes.reflection_probe_count;
reflection_probe_index += 1) {
let reflection_probe = light_probes.reflection_probes[reflection_probe_index];
// Unpack the inverse transform.
let inverse_transpose_transform = mat4x4<f32>(
reflection_probe.inverse_transpose_transform[0],
reflection_probe.inverse_transpose_transform[1],
reflection_probe.inverse_transpose_transform[2],
vec4<f32>(0.0, 0.0, 0.0, 1.0));
let inverse_transform = transpose(inverse_transpose_transform);
// Check to see if the transformed point is inside the unit cube
// centered at the origin.
let probe_space_pos = (inverse_transform * vec4<f32>(world_position, 1.0)).xyz;
if (all(abs(probe_space_pos) <= vec3(0.5))) {
cubemap_index = reflection_probe.cubemap_index;
intensity = reflection_probe.intensity;
break;
}
}
// If we didn't find a reflection probe, use the view environment map if applicable.
if (cubemap_index < 0) {
cubemap_index = light_probes.view_cubemap_index;
intensity = light_probes.intensity_for_view;
}
// If there's no cubemap, bail out.
if (cubemap_index < 0) {
radiances.irradiance = vec3(0.0);
radiances.radiance = vec3(0.0);
return radiances;
}
// Split-sum approximation for image based lighting: https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf
let radiance_level = perceptual_roughness * f32(textureNumLevels(bindings::specular_environment_maps[cubemap_index]) - 1u);
#ifndef LIGHTMAP
radiances.irradiance = textureSampleLevel(
bindings::diffuse_environment_maps[cubemap_index],
bindings::environment_map_sampler,
vec3(N.xy, -N.z),
0.0).rgb * intensity;
#endif // LIGHTMAP
radiances.radiance = textureSampleLevel(
bindings::specular_environment_maps[cubemap_index],
bindings::environment_map_sampler,
vec3(R.xy, -R.z),
radiance_level).rgb * intensity;
return radiances;
}
#else // MULTIPLE_LIGHT_PROBES_IN_ARRAY
fn compute_radiances(
perceptual_roughness: f32,
N: vec3<f32>,
R: vec3<f32>,
world_position: vec3<f32>,
) -> EnvironmentMapRadiances {
var radiances: EnvironmentMapRadiances;
if (light_probes.view_cubemap_index < 0) {
radiances.irradiance = vec3(0.0);
radiances.radiance = vec3(0.0);
return radiances;
}
// Split-sum approximation for image based lighting: https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf
// Technically we could use textureNumLevels(specular_environment_map) - 1 here, but we use a uniform
// because textureNumLevels() does not work on WebGL2
let radiance_level = perceptual_roughness * f32(light_probes.smallest_specular_mip_level_for_view);
let intensity = light_probes.intensity_for_view;
#ifndef LIGHTMAP
radiances.irradiance = textureSampleLevel(
bindings::diffuse_environment_map,
bindings::environment_map_sampler,
vec3(N.xy, -N.z),
0.0).rgb * intensity;
#endif // LIGHTMAP
radiances.radiance = textureSampleLevel(
bindings::specular_environment_map,
bindings::environment_map_sampler,
vec3(R.xy, -R.z),
radiance_level).rgb * intensity;
return radiances;
}
#endif // MULTIPLE_LIGHT_PROBES_IN_ARRAY
fn environment_map_light(
perceptual_roughness: f32,
roughness: f32,
diffuse_color: vec3<f32>,
NdotV: f32,
f_ab: vec2<f32>,
N: vec3<f32>,
R: vec3<f32>,
F0: vec3<f32>,
world_position: vec3<f32>,
) -> EnvironmentMapLight {
let radiances = compute_radiances(perceptual_roughness, N, R, world_position);
// No real world material has specular values under 0.02, so we use this range as a
// "pre-baked specular occlusion" that extinguishes the fresnel term, for artistic control.
// See: https://google.github.io/filament/Filament.html#specularocclusion
let specular_occlusion = saturate(dot(F0, vec3(50.0 * 0.33)));
// Multiscattering approximation: https://www.jcgt.org/published/0008/01/03/paper.pdf
// Useful reference: https://bruop.github.io/ibl
let Fr = max(vec3(1.0 - roughness), F0) - F0;
let kS = F0 + Fr * pow(1.0 - NdotV, 5.0);
let Ess = f_ab.x + f_ab.y;
let FssEss = kS * Ess * specular_occlusion;
let Ems = 1.0 - Ess;
let Favg = F0 + (1.0 - F0) / 21.0;
let Fms = FssEss * Favg / (1.0 - Ems * Favg);
let FmsEms = Fms * Ems;
let Edss = 1.0 - (FssEss + FmsEms);
let kD = diffuse_color * Edss;
var out: EnvironmentMapLight;
// If there's a lightmap, ignore the diffuse component of the reflection
// probe, so we don't double-count light.
#ifdef LIGHTMAP
out.diffuse = vec3(0.0);
#else
out.diffuse = (FmsEms + kD) * radiances.irradiance;
#endif
out.specular = FssEss * radiances.radiance;
return out;
}

View file

@ -0,0 +1,438 @@
//! Light probes for baked global illumination.
use bevy_app::{App, Plugin};
use bevy_asset::load_internal_asset;
use bevy_core_pipeline::core_3d::Camera3d;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component,
entity::Entity,
query::With,
reflect::ReflectComponent,
schedule::IntoSystemConfigs,
system::{Commands, Local, Query, Res, ResMut, Resource},
};
use bevy_math::{Affine3A, Mat4, Vec3A, Vec4};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
extract_instances::ExtractInstancesPlugin,
primitives::{Aabb, Frustum},
render_asset::RenderAssets,
render_resource::{DynamicUniformBuffer, Shader, ShaderType},
renderer::{RenderDevice, RenderQueue},
texture::Image,
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
};
use bevy_transform::prelude::GlobalTransform;
use bevy_utils::{EntityHashMap, FloatOrd};
use crate::light_probe::environment_map::{
binding_arrays_are_usable, EnvironmentMapIds, EnvironmentMapLight, RenderViewEnvironmentMaps,
ENVIRONMENT_MAP_SHADER_HANDLE,
};
pub mod environment_map;
/// The maximum number of reflection probes that each view will consider.
///
/// Because the fragment shader does a linear search through the list for each
/// fragment, this number needs to be relatively small.
pub const MAX_VIEW_REFLECTION_PROBES: usize = 8;
/// Adds support for light probes: cuboid bounding regions that apply global
/// illumination to objects within them.
///
/// This also adds support for view environment maps: diffuse and specular
/// cubemaps applied to all objects that a view renders.
pub struct LightProbePlugin;
/// A marker component for a light probe, which is a cuboid region that provides
/// global illumination to all fragments inside it.
///
/// The light probe range is conceptually a unit cube (1×1×1) centered on the
/// origin. The [`bevy_transform::prelude::Transform`] applied to this entity
/// can scale, rotate, or translate that cube so that it contains all fragments
/// that should take this light probe into account.
///
/// Note that a light probe will have no effect unless the entity contains some
/// kind of illumination. At present, the only supported type of illumination is
/// the [`EnvironmentMapLight`].
#[derive(Component, Debug, Clone, Copy, Default, Reflect)]
#[reflect(Component, Default)]
pub struct LightProbe;
/// A GPU type that stores information about a reflection probe.
#[derive(Clone, Copy, ShaderType, Default)]
struct RenderReflectionProbe {
/// The transform from the world space to the model space. This is used to
/// efficiently check for bounding box intersection.
inverse_transpose_transform: [Vec4; 3],
/// The index of the environment map in the diffuse and specular cubemap
/// binding arrays.
cubemap_index: i32,
/// Scale factor applied to the diffuse and specular light generated by this
/// reflection probe.
///
/// See the comment in [`EnvironmentMapLight`] for details.
intensity: f32,
}
/// A per-view shader uniform that specifies all the light probes that the view
/// takes into account.
#[derive(ShaderType)]
pub struct LightProbesUniform {
/// The list of applicable reflection probes, sorted from nearest to the
/// camera to the farthest away from the camera.
reflection_probes: [RenderReflectionProbe; MAX_VIEW_REFLECTION_PROBES],
/// The number of reflection probes in the list.
reflection_probe_count: i32,
/// The index of the diffuse and specular environment maps associated with
/// the view itself. This is used as a fallback if no reflection probe in
/// the list contains the fragment.
view_cubemap_index: i32,
/// The smallest valid mipmap level for the specular environment cubemap
/// associated with the view.
smallest_specular_mip_level_for_view: u32,
/// The intensity of the environment cubemap associated with the view.
///
/// See the comment in [`EnvironmentMapLight`] for details.
intensity_for_view: f32,
}
/// A map from each camera to the light probe uniform associated with it.
#[derive(Resource, Default, Deref, DerefMut)]
struct RenderLightProbes(EntityHashMap<Entity, LightProbesUniform>);
/// A GPU buffer that stores information about all light probes.
#[derive(Resource, Default, Deref, DerefMut)]
pub struct LightProbesBuffer(DynamicUniformBuffer<LightProbesUniform>);
/// A component attached to each camera in the render world that stores the
/// index of the [`LightProbesUniform`] in the [`LightProbesBuffer`].
#[derive(Component, Default, Deref, DerefMut)]
pub struct ViewLightProbesUniformOffset(u32);
/// Information that [`gather_light_probes`] keeps about each light probe.
#[derive(Clone, Copy)]
#[allow(dead_code)]
struct LightProbeInfo {
// The transform from world space to light probe space.
inverse_transform: Mat4,
// The transform from light probe space to world space.
affine_transform: Affine3A,
// The diffuse and specular environment maps associated with this light
// probe.
environment_maps: EnvironmentMapIds,
// Scale factor applied to the diffuse and specular light generated by this
// reflection probe.
//
// See the comment in [`EnvironmentMapLight`] for details.
intensity: f32,
}
impl LightProbe {
/// Creates a new light probe component.
#[inline]
pub fn new() -> Self {
Self
}
}
impl Plugin for LightProbePlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
ENVIRONMENT_MAP_SHADER_HANDLE,
"environment_map.wgsl",
Shader::from_wgsl
);
app.register_type::<LightProbe>()
.register_type::<EnvironmentMapLight>();
}
fn finish(&self, app: &mut App) {
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.add_plugins(ExtractInstancesPlugin::<EnvironmentMapIds>::new())
.init_resource::<LightProbesBuffer>()
.init_resource::<RenderLightProbes>()
.add_systems(ExtractSchedule, gather_light_probes)
.add_systems(
Render,
upload_light_probes.in_set(RenderSet::PrepareResources),
);
}
}
/// Gathers up all light probes in the scene and assigns them to views,
/// performing frustum culling and distance sorting in the process.
///
/// This populates the [`RenderLightProbes`] resource.
#[allow(clippy::too_many_arguments)]
fn gather_light_probes(
mut render_light_probes: ResMut<RenderLightProbes>,
image_assets: Res<RenderAssets<Image>>,
render_device: Res<RenderDevice>,
light_probe_query: Extract<Query<(&GlobalTransform, &EnvironmentMapLight), With<LightProbe>>>,
view_query: Extract<
Query<
(
Entity,
&GlobalTransform,
&Frustum,
Option<&EnvironmentMapLight>,
),
With<Camera3d>,
>,
>,
mut light_probes: Local<Vec<LightProbeInfo>>,
mut view_light_probes: Local<Vec<LightProbeInfo>>,
mut commands: Commands,
) {
// Create [`LightProbeInfo`] for every light probe in the scene.
light_probes.clear();
light_probes.extend(
light_probe_query
.iter()
.filter_map(|query_row| LightProbeInfo::new(query_row, &image_assets)),
);
// Build up the light probes uniform and the key table.
render_light_probes.clear();
for (view_entity, view_transform, view_frustum, view_environment_maps) in view_query.iter() {
// Cull light probes outside the view frustum.
view_light_probes.clear();
view_light_probes.extend(
light_probes
.iter()
.filter(|light_probe_info| light_probe_info.frustum_cull(view_frustum))
.cloned(),
);
// Sort by distance to camera.
view_light_probes.sort_by_cached_key(|light_probe_info| {
light_probe_info.camera_distance_sort_key(view_transform)
});
// Create the light probes uniform.
let (light_probes_uniform, render_view_environment_maps) = LightProbesUniform::build(
view_environment_maps,
&view_light_probes,
&image_assets,
&render_device,
);
// Record the uniforms.
render_light_probes.insert(view_entity, light_probes_uniform);
// Record the per-view environment maps.
let mut commands = commands.get_or_spawn(view_entity);
if render_view_environment_maps.is_empty() {
commands.remove::<RenderViewEnvironmentMaps>();
} else {
commands.insert(render_view_environment_maps);
}
}
}
/// Uploads the result of [`gather_light_probes`] to the GPU.
fn upload_light_probes(
mut commands: Commands,
light_probes_uniforms: Res<RenderLightProbes>,
mut light_probes_buffer: ResMut<LightProbesBuffer>,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
) {
// Get the uniform buffer writer.
let Some(mut writer) =
light_probes_buffer.get_writer(light_probes_uniforms.len(), &render_device, &render_queue)
else {
return;
};
// Send each view's uniforms to the GPU.
for (&view_entity, light_probes_uniform) in light_probes_uniforms.iter() {
commands
.entity(view_entity)
.insert(ViewLightProbesUniformOffset(
writer.write(light_probes_uniform),
));
}
}
impl Default for LightProbesUniform {
fn default() -> Self {
Self {
reflection_probes: [RenderReflectionProbe::default(); MAX_VIEW_REFLECTION_PROBES],
reflection_probe_count: 0,
view_cubemap_index: -1,
smallest_specular_mip_level_for_view: 0,
intensity_for_view: 1.0,
}
}
}
impl LightProbesUniform {
/// Constructs a [`LightProbesUniform`] containing all the environment maps
/// that fragments rendered by a single view need to consider.
///
/// The `view_environment_maps` parameter describes the environment maps
/// attached to the view. The `light_probes` parameter is expected to be the
/// list of light probes in the scene, sorted by increasing view distance
/// from the camera.
fn build(
view_environment_maps: Option<&EnvironmentMapLight>,
light_probes: &[LightProbeInfo],
image_assets: &RenderAssets<Image>,
render_device: &RenderDevice,
) -> (LightProbesUniform, RenderViewEnvironmentMaps) {
let mut render_view_environment_maps = RenderViewEnvironmentMaps::new();
// Find the index of the cubemap associated with the view, and determine
// its smallest mip level.
let mut view_cubemap_index = -1;
let mut smallest_specular_mip_level_for_view = 0;
let mut intensity_for_view = 1.0;
if let Some(EnvironmentMapLight {
diffuse_map: diffuse_map_handle,
specular_map: specular_map_handle,
intensity,
}) = view_environment_maps
{
if let (Some(_), Some(specular_map)) = (
image_assets.get(diffuse_map_handle),
image_assets.get(specular_map_handle),
) {
view_cubemap_index =
render_view_environment_maps.get_or_insert_cubemap(&EnvironmentMapIds {
diffuse: diffuse_map_handle.id(),
specular: specular_map_handle.id(),
}) as i32;
smallest_specular_mip_level_for_view = specular_map.mip_level_count - 1;
intensity_for_view = *intensity;
}
};
// Initialize the uniform to only contain the view environment map, if
// applicable.
let mut uniform = LightProbesUniform {
reflection_probes: [RenderReflectionProbe::default(); MAX_VIEW_REFLECTION_PROBES],
reflection_probe_count: light_probes.len().min(MAX_VIEW_REFLECTION_PROBES) as i32,
view_cubemap_index,
smallest_specular_mip_level_for_view,
intensity_for_view,
};
// Add reflection probes from the scene, if supported by the current
// platform.
uniform.maybe_gather_reflection_probes(
&mut render_view_environment_maps,
light_probes,
render_device,
);
(uniform, render_view_environment_maps)
}
/// Gathers up all reflection probes in the scene and writes them into this
/// uniform and `render_view_environment_maps`.
fn maybe_gather_reflection_probes(
&mut self,
render_view_environment_maps: &mut RenderViewEnvironmentMaps,
light_probes: &[LightProbeInfo],
render_device: &RenderDevice,
) {
if !binding_arrays_are_usable(render_device) {
return;
}
for (reflection_probe, light_probe) in self
.reflection_probes
.iter_mut()
.zip(light_probes.iter().take(MAX_VIEW_REFLECTION_PROBES))
{
// Determine the index of the cubemap in the binding array.
let cubemap_index = render_view_environment_maps
.get_or_insert_cubemap(&light_probe.environment_maps)
as i32;
// Transpose the inverse transform to compress the structure on the
// GPU (from 4 `Vec4`s to 3 `Vec4`s). The shader will transpose it
// to recover the original inverse transform.
let inverse_transpose_transform = light_probe.inverse_transform.transpose();
// Write in the reflection probe data.
*reflection_probe = RenderReflectionProbe {
inverse_transpose_transform: [
inverse_transpose_transform.x_axis,
inverse_transpose_transform.y_axis,
inverse_transpose_transform.z_axis,
],
cubemap_index,
intensity: light_probe.intensity,
};
}
}
}
impl LightProbeInfo {
/// Given the set of light probe components, constructs and returns
/// [`LightProbeInfo`]. This is done for every light probe in the scene
/// every frame.
fn new(
(light_probe_transform, environment_map): (&GlobalTransform, &EnvironmentMapLight),
image_assets: &RenderAssets<Image>,
) -> Option<LightProbeInfo> {
if image_assets.get(&environment_map.diffuse_map).is_none()
|| image_assets.get(&environment_map.specular_map).is_none()
{
return None;
}
Some(LightProbeInfo {
affine_transform: light_probe_transform.affine(),
inverse_transform: light_probe_transform.compute_matrix().inverse(),
environment_maps: EnvironmentMapIds {
diffuse: environment_map.diffuse_map.id(),
specular: environment_map.specular_map.id(),
},
intensity: environment_map.intensity,
})
}
/// Returns true if this light probe is in the viewing frustum of the camera
/// or false if it isn't.
fn frustum_cull(&self, view_frustum: &Frustum) -> bool {
view_frustum.intersects_obb(
&Aabb {
center: Vec3A::default(),
half_extents: Vec3A::splat(0.5),
},
&self.affine_transform,
true,
false,
)
}
/// Returns the squared distance from this light probe to the camera,
/// suitable for distance sorting.
fn camera_distance_sort_key(&self, view_transform: &GlobalTransform) -> FloatOrd {
FloatOrd(
(self.affine_transform.translation - view_transform.translation_vec3a())
.length_squared(),
)
}
}

View file

@ -1,4 +1,4 @@
use crate::*;
use crate::{environment_map::RenderViewEnvironmentMaps, *};
use bevy_app::{App, Plugin};
use bevy_asset::{Asset, AssetApp, AssetEvent, AssetId, AssetServer, Assets, Handle};
use bevy_core_pipeline::{
@ -466,14 +466,12 @@ pub fn queue_material_meshes<M: Material>(
render_materials: Res<RenderMaterials<M>>,
mut render_mesh_instances: ResMut<RenderMeshInstances>,
render_material_instances: Res<RenderMaterialInstances<M>>,
images: Res<RenderAssets<Image>>,
render_lightmaps: Res<RenderLightmaps>,
mut views: Query<(
&ExtractedView,
&VisibleEntities,
Option<&Tonemapping>,
Option<&DebandDither>,
Option<&EnvironmentMapLight>,
Option<&ShadowFilteringMethod>,
Has<ScreenSpaceAmbientOcclusionSettings>,
(
@ -489,6 +487,7 @@ pub fn queue_material_meshes<M: Material>(
&mut RenderPhase<AlphaMask3d>,
&mut RenderPhase<Transmissive3d>,
&mut RenderPhase<Transparent3d>,
Has<RenderViewEnvironmentMaps>,
)>,
) where
M::Data: PartialEq + Eq + Hash + Clone,
@ -498,7 +497,6 @@ pub fn queue_material_meshes<M: Material>(
visible_entities,
tonemapping,
dither,
environment_map,
shadow_filter_method,
ssao,
(normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass),
@ -509,6 +507,7 @@ pub fn queue_material_meshes<M: Material>(
mut alpha_mask_phase,
mut transmissive_phase,
mut transparent_phase,
has_environment_maps,
) in &mut views
{
let draw_opaque_pbr = opaque_draw_functions.read().id::<DrawMaterial<M>>();
@ -539,9 +538,7 @@ pub fn queue_material_meshes<M: Material>(
view_key |= MeshPipelineKey::TEMPORAL_JITTER;
}
let environment_map_loaded = environment_map.is_some_and(|map| map.is_loaded(&images));
if environment_map_loaded {
if has_environment_maps {
view_key |= MeshPipelineKey::ENVIRONMENT_MAP;
}

View file

@ -196,8 +196,6 @@ pub struct GpuLights {
n_directional_lights: u32,
// offset from spot light's light index to spot light's shadow map index
spot_light_shadowmap_offset: i32,
environment_map_smallest_specular_mip_level: u32,
environment_map_intensity: f32,
}
// NOTE: this must be kept in sync with the same constants in pbr.frag
@ -645,18 +643,12 @@ pub(crate) fn spot_light_projection_matrix(angle: f32) -> Mat4 {
pub fn prepare_lights(
mut commands: Commands,
mut texture_cache: ResMut<TextureCache>,
images: Res<RenderAssets<Image>>,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut global_light_meta: ResMut<GlobalLightMeta>,
mut light_meta: ResMut<LightMeta>,
views: Query<
(
Entity,
&ExtractedView,
&ExtractedClusterConfig,
Option<&EnvironmentMapLight>,
),
(Entity, &ExtractedView, &ExtractedClusterConfig),
With<RenderPhase<Transparent3d>>,
>,
ambient_light: Res<AmbientLight>,
@ -890,7 +882,7 @@ pub fn prepare_lights(
.write_buffer(&render_device, &render_queue);
// set up light data for each view
for (entity, extracted_view, clusters, environment_map) in &views {
for (entity, extracted_view, clusters) in &views {
let point_light_depth_texture = texture_cache.get(
&render_device,
TextureDescriptor {
@ -957,13 +949,6 @@ pub fn prepare_lights(
// index to shadow map index, we need to subtract point light count and add directional shadowmap count.
spot_light_shadowmap_offset: num_directional_cascades_enabled as i32
- point_light_count as i32,
environment_map_smallest_specular_mip_level: environment_map
.and_then(|env_map| images.get(&env_map.specular_map))
.map(|specular_map| specular_map.mip_level_count - 1)
.unwrap_or(0),
environment_map_intensity: environment_map
.map(|env_map| env_map.intensity)
.unwrap_or(1.0),
};
// TODO: this should select lights based on relevance to the view instead of the first ones that show up in a query

View file

@ -1,3 +1,8 @@
use crate::{
MaterialBindGroupId, NotShadowCaster, NotShadowReceiver, PreviousGlobalTransform, Shadow,
ViewFogUniformOffset, ViewLightProbesUniformOffset, ViewLightsUniformOffset,
CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, MAX_CASCADES_PER_LIGHT, MAX_DIRECTIONAL_LIGHTS,
};
use bevy_app::{Plugin, PostUpdate};
use bevy_asset::{load_internal_asset, AssetId, Handle};
use bevy_core_pipeline::{
@ -21,7 +26,9 @@ use bevy_render::{
render_phase::{PhaseItem, RenderCommand, RenderCommandResult, TrackedRenderPass},
render_resource::*,
renderer::{RenderDevice, RenderQueue},
texture::*,
texture::{
BevyDefault, DefaultImageSampler, GpuImage, Image, ImageSampler, TextureFormatPixelInfo,
},
view::{ViewTarget, ViewUniformOffset, ViewVisibility},
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
};
@ -48,6 +55,8 @@ use crate::render::{
};
use crate::*;
use self::environment_map::binding_arrays_are_usable;
use super::skin::SkinIndices;
#[derive(Default)]
@ -358,6 +367,12 @@ pub struct MeshPipeline {
/// ```
pub per_object_buffer_batch_size: Option<u32>,
/// Whether binding arrays (a.k.a. bindless textures) are usable on the
/// current render device.
///
/// This affects whether reflection probes can be used.
pub binding_arrays_are_usable: bool,
#[cfg(debug_assertions)]
pub did_warn_about_too_many_textures: Arc<AtomicBool>,
}
@ -416,6 +431,7 @@ impl FromWorld for MeshPipeline {
dummy_white_gpu_image,
mesh_layouts: MeshLayouts::new(&render_device),
per_object_buffer_batch_size: GpuArrayBuffer::<MeshUniform>::batch_size(&render_device),
binding_arrays_are_usable: binding_arrays_are_usable(&render_device),
#[cfg(debug_assertions)]
did_warn_about_too_many_textures: Arc::new(AtomicBool::new(false)),
}
@ -867,6 +883,10 @@ impl SpecializedMeshPipeline for MeshPipeline {
},
));
if self.binding_arrays_are_usable {
shader_defs.push("MULTIPLE_LIGHT_PROBES_IN_ARRAY".into());
}
let format = if key.contains(MeshPipelineKey::HDR) {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
@ -1032,6 +1052,7 @@ impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMeshViewBindGroup<I>
Read<ViewUniformOffset>,
Read<ViewLightsUniformOffset>,
Read<ViewFogUniformOffset>,
Read<ViewLightProbesUniformOffset>,
Read<MeshViewBindGroup>,
);
type ItemData = ();
@ -1039,7 +1060,7 @@ impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMeshViewBindGroup<I>
#[inline]
fn render<'w>(
_item: &P,
(view_uniform, view_lights, view_fog, mesh_view_bind_group): ROQueryItem<
(view_uniform, view_lights, view_fog, view_light_probes, mesh_view_bind_group): ROQueryItem<
'w,
Self::ViewData,
>,
@ -1050,7 +1071,12 @@ impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMeshViewBindGroup<I>
pass.set_bind_group(
I,
&mesh_view_bind_group.value,
&[view_uniform.offset, view_lights.offset, view_fog.offset],
&[
view_uniform.offset,
view_lights.offset,
view_fog.offset,
**view_light_probes,
],
);
RenderCommandResult::Success

View file

@ -17,7 +17,7 @@ use bevy_render::{
render_asset::RenderAssets,
render_resource::{binding_types::*, *},
renderer::RenderDevice,
texture::{BevyDefault, FallbackImageCubemap, FallbackImageMsaa, FallbackImageZero, Image},
texture::{BevyDefault, FallbackImage, FallbackImageMsaa, FallbackImageZero, Image},
view::{Msaa, ViewUniform, ViewUniforms},
};
@ -27,9 +27,10 @@ use bevy_render::render_resource::binding_types::texture_cube;
use bevy_render::render_resource::binding_types::{texture_2d_array, texture_cube_array};
use crate::{
environment_map, prepass, EnvironmentMapLight, FogMeta, GlobalLightMeta, GpuFog, GpuLights,
GpuPointLights, LightMeta, MeshPipeline, MeshPipelineKey, ScreenSpaceAmbientOcclusionTextures,
ShadowSamplers, ViewClusterBindings, ViewShadowBindings,
environment_map::{self, RenderViewBindGroupEntries, RenderViewEnvironmentMaps},
prepass, FogMeta, GlobalLightMeta, GpuFog, GpuLights, GpuPointLights, LightMeta,
LightProbesBuffer, LightProbesUniform, MeshPipeline, MeshPipelineKey,
ScreenSpaceAmbientOcclusionTextures, ShadowSamplers, ViewClusterBindings, ViewShadowBindings,
};
#[derive(Clone)]
@ -166,6 +167,7 @@ fn buffer_layout(
fn layout_entries(
clustered_forward_buffer_binding_type: BufferBindingType,
layout_key: MeshPipelineViewLayoutKey,
render_device: &RenderDevice,
) -> Vec<BindGroupLayoutEntry> {
let mut entries = DynamicBindGroupLayoutEntries::new_with_indices(
ShaderStages::FRAGMENT,
@ -234,27 +236,29 @@ fn layout_entries(
(9, uniform_buffer::<GlobalsUniform>(false)),
// Fog
(10, uniform_buffer::<GpuFog>(true)),
// Light probes
(11, uniform_buffer::<LightProbesUniform>(true)),
// Screen space ambient occlusion texture
(
11,
12,
texture_2d(TextureSampleType::Float { filterable: false }),
),
),
);
// EnvironmentMapLight
let environment_map_entries = environment_map::get_bind_group_layout_entries();
let environment_map_entries = environment_map::get_bind_group_layout_entries(render_device);
entries = entries.extend_with_indices((
(12, environment_map_entries[0]),
(13, environment_map_entries[1]),
(14, environment_map_entries[2]),
(13, environment_map_entries[0]),
(14, environment_map_entries[1]),
(15, environment_map_entries[2]),
));
// Tonemapping
let tonemapping_lut_entries = get_lut_bind_group_layout_entries();
entries = entries.extend_with_indices((
(15, tonemapping_lut_entries[0]),
(16, tonemapping_lut_entries[1]),
(16, tonemapping_lut_entries[0]),
(17, tonemapping_lut_entries[1]),
));
// Prepass
@ -264,7 +268,7 @@ fn layout_entries(
{
for (entry, binding) in prepass::get_bind_group_layout_entries(layout_key)
.iter()
.zip([17, 18, 19, 20])
.zip([18, 19, 20, 21])
{
if let Some(entry) = entry {
entries = entries.extend_with_indices(((binding as u32, *entry),));
@ -275,10 +279,10 @@ fn layout_entries(
// View Transmission Texture
entries = entries.extend_with_indices((
(
21,
22,
texture_2d(TextureSampleType::Float { filterable: true }),
),
(22, sampler(SamplerBindingType::Filtering)),
(23, sampler(SamplerBindingType::Filtering)),
));
entries.to_vec()
@ -292,7 +296,7 @@ pub fn generate_view_layouts(
) -> [MeshPipelineViewLayout; MeshPipelineViewLayoutKey::COUNT] {
array::from_fn(|i| {
let key = MeshPipelineViewLayoutKey::from_bits_truncate(i as u32);
let entries = layout_entries(clustered_forward_buffer_binding_type, key);
let entries = layout_entries(clustered_forward_buffer_binding_type, key, render_device);
#[cfg(debug_assertions)]
let texture_count: usize = entries
@ -331,18 +335,19 @@ pub fn prepare_mesh_view_bind_groups(
Option<&ScreenSpaceAmbientOcclusionTextures>,
Option<&ViewPrepassTextures>,
Option<&ViewTransmissionTexture>,
Option<&EnvironmentMapLight>,
&Tonemapping,
Option<&RenderViewEnvironmentMaps>,
)>,
(images, mut fallback_images, fallback_cubemap, fallback_image_zero): (
(images, mut fallback_images, fallback_image, fallback_image_zero): (
Res<RenderAssets<Image>>,
FallbackImageMsaa,
Res<FallbackImageCubemap>,
Res<FallbackImage>,
Res<FallbackImageZero>,
),
msaa: Res<Msaa>,
globals_buffer: Res<GlobalsBuffer>,
tonemapping_luts: Res<TonemappingLuts>,
light_probes_buffer: Res<LightProbesBuffer>,
) {
if let (
Some(view_binding),
@ -350,12 +355,14 @@ pub fn prepare_mesh_view_bind_groups(
Some(point_light_binding),
Some(globals),
Some(fog_binding),
Some(light_probes_binding),
) = (
view_uniforms.uniforms.binding(),
light_meta.view_gpu_lights.binding(),
global_light_meta.gpu_point_lights.binding(),
globals_buffer.buffer.binding(),
fog_meta.gpu_fogs.binding(),
light_probes_buffer.binding(),
) {
for (
entity,
@ -364,8 +371,8 @@ pub fn prepare_mesh_view_bind_groups(
ssao_textures,
prepass_textures,
transmission_texture,
environment_map,
tonemapping,
render_view_environment_maps,
) in &views
{
let fallback_ssao = fallback_images
@ -393,19 +400,44 @@ pub fn prepare_mesh_view_bind_groups(
(8, cluster_bindings.offsets_and_counts_binding().unwrap()),
(9, globals.clone()),
(10, fog_binding.clone()),
(11, ssao_view),
(11, light_probes_binding.clone()),
(12, ssao_view),
));
let env_map_bindings =
environment_map::get_bindings(environment_map, &images, &fallback_cubemap);
let bind_group_entries = RenderViewBindGroupEntries::get(
render_view_environment_maps,
&images,
&fallback_image,
&render_device,
);
match bind_group_entries {
RenderViewBindGroupEntries::Single {
diffuse_texture_view,
specular_texture_view,
sampler,
} => {
entries = entries.extend_with_indices((
(12, env_map_bindings.0),
(13, env_map_bindings.1),
(14, env_map_bindings.2),
(13, diffuse_texture_view),
(14, specular_texture_view),
(15, sampler),
));
}
RenderViewBindGroupEntries::Multiple {
ref diffuse_texture_views,
ref specular_texture_views,
sampler,
} => {
entries = entries.extend_with_indices((
(13, diffuse_texture_views.as_slice()),
(14, specular_texture_views.as_slice()),
(15, sampler),
));
}
}
let lut_bindings = get_lut_bindings(&images, &tonemapping_luts, tonemapping);
entries = entries.extend_with_indices(((15, lut_bindings.0), (16, lut_bindings.1)));
entries = entries.extend_with_indices(((16, lut_bindings.0), (17, lut_bindings.1)));
// When using WebGL, we can't have a depth texture with multisampling
let prepass_bindings;
@ -415,7 +447,7 @@ pub fn prepare_mesh_view_bind_groups(
for (binding, index) in prepass_bindings
.iter()
.map(Option::as_ref)
.zip([17, 18, 19, 20])
.zip([18, 19, 20, 21])
.flat_map(|(b, i)| b.map(|b| (b, i)))
{
entries = entries.extend_with_indices(((index, binding),));
@ -431,7 +463,7 @@ pub fn prepare_mesh_view_bind_groups(
.unwrap_or(&fallback_image_zero.sampler);
entries =
entries.extend_with_indices(((21, transmission_view), (22, transmission_sampler)));
entries.extend_with_indices(((22, transmission_view), (23, transmission_sampler)));
commands.entity(entity).insert(MeshViewBindGroup {
value: render_device.create_bind_group("mesh_view_bind_group", layout, &entries),

View file

@ -33,44 +33,50 @@
@group(0) @binding(9) var<uniform> globals: Globals;
@group(0) @binding(10) var<uniform> fog: types::Fog;
@group(0) @binding(11) var<uniform> light_probes: types::LightProbes;
@group(0) @binding(11) var screen_space_ambient_occlusion_texture: texture_2d<f32>;
@group(0) @binding(12) var screen_space_ambient_occlusion_texture: texture_2d<f32>;
@group(0) @binding(12) var environment_map_diffuse: texture_cube<f32>;
@group(0) @binding(13) var environment_map_specular: texture_cube<f32>;
@group(0) @binding(14) var environment_map_sampler: sampler;
#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY
@group(0) @binding(13) var diffuse_environment_maps: binding_array<texture_cube<f32>, 8u>;
@group(0) @binding(14) var specular_environment_maps: binding_array<texture_cube<f32>, 8u>;
#else
@group(0) @binding(13) var diffuse_environment_map: texture_cube<f32>;
@group(0) @binding(14) var specular_environment_map: texture_cube<f32>;
#endif
@group(0) @binding(15) var environment_map_sampler: sampler;
@group(0) @binding(15) var dt_lut_texture: texture_3d<f32>;
@group(0) @binding(16) var dt_lut_sampler: sampler;
@group(0) @binding(16) var dt_lut_texture: texture_3d<f32>;
@group(0) @binding(17) var dt_lut_sampler: sampler;
#ifdef MULTISAMPLED
#ifdef DEPTH_PREPASS
@group(0) @binding(17) var depth_prepass_texture: texture_depth_multisampled_2d;
@group(0) @binding(18) var depth_prepass_texture: texture_depth_multisampled_2d;
#endif // DEPTH_PREPASS
#ifdef NORMAL_PREPASS
@group(0) @binding(18) var normal_prepass_texture: texture_multisampled_2d<f32>;
@group(0) @binding(19) var normal_prepass_texture: texture_multisampled_2d<f32>;
#endif // NORMAL_PREPASS
#ifdef MOTION_VECTOR_PREPASS
@group(0) @binding(19) var motion_vector_prepass_texture: texture_multisampled_2d<f32>;
@group(0) @binding(20) var motion_vector_prepass_texture: texture_multisampled_2d<f32>;
#endif // MOTION_VECTOR_PREPASS
#else // MULTISAMPLED
#ifdef DEPTH_PREPASS
@group(0) @binding(17) var depth_prepass_texture: texture_depth_2d;
@group(0) @binding(18) var depth_prepass_texture: texture_depth_2d;
#endif // DEPTH_PREPASS
#ifdef NORMAL_PREPASS
@group(0) @binding(18) var normal_prepass_texture: texture_2d<f32>;
@group(0) @binding(19) var normal_prepass_texture: texture_2d<f32>;
#endif // NORMAL_PREPASS
#ifdef MOTION_VECTOR_PREPASS
@group(0) @binding(19) var motion_vector_prepass_texture: texture_2d<f32>;
@group(0) @binding(20) var motion_vector_prepass_texture: texture_2d<f32>;
#endif // MOTION_VECTOR_PREPASS
#endif // MULTISAMPLED
#ifdef DEFERRED_PREPASS
@group(0) @binding(20) var deferred_prepass_texture: texture_2d<u32>;
@group(0) @binding(21) var deferred_prepass_texture: texture_2d<u32>;
#endif // DEFERRED_PREPASS
@group(0) @binding(21) var view_transmission_texture: texture_2d<f32>;
@group(0) @binding(22) var view_transmission_sampler: sampler;
@group(0) @binding(22) var view_transmission_texture: texture_2d<f32>;
@group(0) @binding(23) var view_transmission_sampler: sampler;

View file

@ -110,3 +110,25 @@ struct ClusterOffsetsAndCounts {
data: array<vec4<u32>, 1024u>,
};
#endif
struct ReflectionProbe {
// This is stored as the transpose in order to save space in this structure.
// It'll be transposed in the `environment_map_light` function.
inverse_transpose_transform: mat3x4<f32>,
cubemap_index: i32,
intensity: f32,
};
struct LightProbes {
// This must match `MAX_VIEW_REFLECTION_PROBES` on the Rust side.
reflection_probes: array<ReflectionProbe, 8u>,
reflection_probe_count: i32,
// The index of the view environment map cubemap binding, or -1 if there's
// no such cubemap.
view_cubemap_index: i32,
// The smallest valid mipmap level for the specular environment cubemap
// associated with the view.
smallest_specular_mip_level_for_view: u32,
// The intensity of the environment map associated with the view.
intensity_for_view: f32,
};

View file

@ -323,7 +323,16 @@ fn apply_pbr_lighting(
// Environment map light (indirect)
#ifdef ENVIRONMENT_MAP
let environment_light = environment_map::environment_map_light(perceptual_roughness, roughness, diffuse_color, NdotV, f_ab, in.N, R, F0);
let environment_light = environment_map::environment_map_light(
perceptual_roughness,
roughness,
diffuse_color,
NdotV,
f_ab,
in.N,
R,
F0,
in.world_position.xyz);
indirect_light += (environment_light.diffuse * diffuse_occlusion) + (environment_light.specular * specular_occlusion);
// we'll use the specular component of the transmitted environment
@ -349,7 +358,16 @@ fn apply_pbr_lighting(
refract(in.V, -in.N, 1.0 / ior) * thickness // add refracted vector scaled by thickness, towards exit point
); // normalize to find exit point view vector
let transmitted_environment_light = bevy_pbr::environment_map::environment_map_light(perceptual_roughness, roughness, vec3<f32>(1.0), 1.0, f_ab, -in.N, T, vec3<f32>(1.0));
let transmitted_environment_light = bevy_pbr::environment_map::environment_map_light(
perceptual_roughness,
roughness,
vec3<f32>(1.0),
1.0,
f_ab,
-in.N,
T,
vec3<f32>(1.0),
in.world_position.xyz);
transmitted_light += transmitted_environment_light.diffuse * diffuse_transmissive_color;
specular_transmitted_environment_light = transmitted_environment_light.specular * specular_transmissive_color;
}

View file

@ -20,7 +20,7 @@ dds = ["ddsfile"]
pnm = ["image/pnm"]
bevy_ci_testing = ["bevy_app/bevy_ci_testing"]
shader_format_glsl = ["naga/glsl-in", "naga/wgsl-out"]
shader_format_glsl = ["naga/glsl-in", "naga/wgsl-out", "naga_oil/glsl"]
shader_format_spirv = ["wgpu/spirv", "naga/spv-in", "naga/spv-out"]
# For ktx2 supercompression
@ -67,7 +67,9 @@ wgpu = { version = "0.18", features = [
"fragile-send-sync-non-atomic-wasm",
] }
naga = { version = "0.14.2", features = ["wgsl-in"] }
naga_oil = "0.11"
naga_oil = { version = "0.11", default-features = false, features = [
"test_shader",
] }
serde = { version = "1", features = ["derive"] }
bitflags = "2.3"
bytemuck = { version = "1.5", features = ["derive"] }

View file

@ -213,7 +213,12 @@ impl From<&Source> for naga_oil::compose::ShaderLanguage {
fn from(value: &Source) -> Self {
match value {
Source::Wgsl(_) => naga_oil::compose::ShaderLanguage::Wgsl,
#[cfg(any(feature = "shader_format_glsl", target_arch = "wasm32"))]
Source::Glsl(_, _) => naga_oil::compose::ShaderLanguage::Glsl,
#[cfg(all(not(feature = "shader_format_glsl"), not(target_arch = "wasm32")))]
Source::Glsl(_, _) => panic!(
"GLSL is not supported in this configuration; use the feature `shader_format_glsl`"
),
Source::SpirV(_) => panic!("spirv not yet implemented"),
}
}
@ -223,13 +228,16 @@ impl From<&Source> for naga_oil::compose::ShaderType {
fn from(value: &Source) -> Self {
match value {
Source::Wgsl(_) => naga_oil::compose::ShaderType::Wgsl,
Source::Glsl(_, naga::ShaderStage::Vertex) => naga_oil::compose::ShaderType::GlslVertex,
Source::Glsl(_, naga::ShaderStage::Fragment) => {
naga_oil::compose::ShaderType::GlslFragment
}
Source::Glsl(_, naga::ShaderStage::Compute) => {
panic!("glsl compute not yet implemented")
}
#[cfg(any(feature = "shader_format_glsl", target_arch = "wasm32"))]
Source::Glsl(_, shader_stage) => match shader_stage {
naga::ShaderStage::Vertex => naga_oil::compose::ShaderType::GlslVertex,
naga::ShaderStage::Fragment => naga_oil::compose::ShaderType::GlslFragment,
naga::ShaderStage::Compute => panic!("glsl compute not yet implemented"),
},
#[cfg(all(not(feature = "shader_format_glsl"), not(target_arch = "wasm32")))]
Source::Glsl(_, _) => panic!(
"GLSL is not supported in this configuration; use the feature `shader_format_glsl`"
),
Source::SpirV(_) => panic!("spirv not yet implemented"),
}
}

View file

@ -0,0 +1,363 @@
//! This example shows how to place reflection probes in the scene.
//!
//! Press Space to switch between no reflections, environment map reflections
//! (i.e. the skybox only, not the cubes), and a full reflection probe that
//! reflects the skybox and the cubes. Press Enter to pause rotation.
//!
//! Reflection probes don't work on WebGL 2 or WebGPU.
use bevy::core_pipeline::Skybox;
use bevy::prelude::*;
use std::fmt::{Display, Formatter, Result as FmtResult};
// Rotation speed in radians per frame.
const ROTATION_SPEED: f32 = 0.005;
static STOP_ROTATION_HELP_TEXT: &str = "Press Enter to stop rotation";
static START_ROTATION_HELP_TEXT: &str = "Press Enter to start rotation";
static REFLECTION_MODE_HELP_TEXT: &str = "Press Space to switch reflection mode";
// The mode the application is in.
#[derive(Resource)]
struct AppStatus {
// Which environment maps the user has requested to display.
reflection_mode: ReflectionMode,
// Whether the user has requested the scene to rotate.
rotating: bool,
}
// Which environment maps the user has requested to display.
#[derive(Clone, Copy)]
enum ReflectionMode {
// No environment maps are shown.
None = 0,
// Only a world environment map is shown.
EnvironmentMap = 1,
// Both a world environment map and a reflection probe are present. The
// reflection probe is shown in the sphere.
ReflectionProbe = 2,
}
// The various reflection maps.
#[derive(Resource)]
struct Cubemaps {
// The blurry diffuse cubemap. This is used for both the world environment
// map and the reflection probe. (In reality you wouldn't do this, but this
// reduces complexity of this example a bit.)
diffuse: Handle<Image>,
// The specular cubemap that reflects the world, but not the cubes.
specular_environment_map: Handle<Image>,
// The specular cubemap that reflects both the world and the cubes.
specular_reflection_probe: Handle<Image>,
// The skybox cubemap image. This is almost the same as
// `specular_environment_map`.
skybox: Handle<Image>,
}
fn main() {
// Create the app.
App::new()
.add_plugins(DefaultPlugins)
.init_resource::<AppStatus>()
.init_resource::<Cubemaps>()
.add_systems(Startup, setup)
.add_systems(PreUpdate, add_environment_map_to_camera)
.add_systems(Update, change_reflection_type)
.add_systems(Update, toggle_rotation)
.add_systems(
Update,
rotate_camera
.after(toggle_rotation)
.after(change_reflection_type),
)
.add_systems(Update, update_text.after(rotate_camera))
.run();
}
// Spawns all the scene objects.
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
asset_server: Res<AssetServer>,
app_status: Res<AppStatus>,
cubemaps: Res<Cubemaps>,
) {
spawn_scene(&mut commands, &asset_server);
spawn_camera(&mut commands);
spawn_sphere(&mut commands, &mut meshes, &mut materials);
spawn_reflection_probe(&mut commands, &cubemaps);
spawn_text(&mut commands, &asset_server, &app_status);
}
// Spawns the cubes, light, and camera.
fn spawn_scene(commands: &mut Commands, asset_server: &AssetServer) {
commands.spawn(SceneBundle {
scene: asset_server.load("models/cubes/Cubes.glb#Scene0"),
..SceneBundle::default()
});
}
// Spawns the camera.
fn spawn_camera(commands: &mut Commands) {
commands.spawn(Camera3dBundle {
camera: Camera {
hdr: true,
..default()
},
transform: Transform::from_xyz(-6.483, 0.325, 4.381).looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
}
// Creates the sphere mesh and spawns it.
fn spawn_sphere(
commands: &mut Commands,
meshes: &mut Assets<Mesh>,
materials: &mut Assets<StandardMaterial>,
) {
// Create a sphere mesh.
let sphere_mesh = meshes.add(
Mesh::try_from(shape::Icosphere {
radius: 1.0,
subdivisions: 7,
})
.unwrap(),
);
// Create a sphere.
commands.spawn(PbrBundle {
mesh: sphere_mesh.clone(),
material: materials.add(StandardMaterial {
base_color: Color::hex("#ffd891").unwrap(),
metallic: 1.0,
perceptual_roughness: 0.0,
..StandardMaterial::default()
}),
transform: Transform::default(),
..PbrBundle::default()
});
}
// Spawns the reflection probe.
fn spawn_reflection_probe(commands: &mut Commands, cubemaps: &Cubemaps) {
commands.spawn(ReflectionProbeBundle {
spatial: SpatialBundle {
// 2.0 because the sphere's radius is 1.0 and we want to fully enclose it.
transform: Transform::from_scale(Vec3::splat(2.0)),
..SpatialBundle::default()
},
light_probe: LightProbe,
environment_map: EnvironmentMapLight {
diffuse_map: cubemaps.diffuse.clone(),
specular_map: cubemaps.specular_reflection_probe.clone(),
intensity: 150.0,
},
});
}
// Spawns the help text.
fn spawn_text(commands: &mut Commands, asset_server: &AssetServer, app_status: &AppStatus) {
// Create the text.
commands.spawn(
TextBundle {
text: app_status.create_text(asset_server),
..TextBundle::default()
}
.with_style(Style {
position_type: PositionType::Absolute,
bottom: Val::Px(10.0),
left: Val::Px(10.0),
..default()
}),
);
}
// Adds a world environment map to the camera. This separate system is needed because the camera is
// managed by the scene spawner, as it's part of the glTF file with the cubes, so we have to add
// the environment map after the fact.
fn add_environment_map_to_camera(
mut commands: Commands,
query: Query<Entity, Added<Camera3d>>,
cubemaps: Res<Cubemaps>,
) {
for camera_entity in query.iter() {
commands
.entity(camera_entity)
.insert(create_camera_environment_map_light(&cubemaps))
.insert(Skybox {
image: cubemaps.skybox.clone(),
brightness: 150.0,
});
}
}
// A system that handles switching between different reflection modes.
fn change_reflection_type(
mut commands: Commands,
light_probe_query: Query<Entity, With<LightProbe>>,
camera_query: Query<Entity, With<Camera3d>>,
keyboard: Res<ButtonInput<KeyCode>>,
mut app_status: ResMut<AppStatus>,
cubemaps: Res<Cubemaps>,
) {
// Only do anything if space was pressed.
if !keyboard.just_pressed(KeyCode::Space) {
return;
}
// Switch reflection mode.
app_status.reflection_mode =
ReflectionMode::try_from((app_status.reflection_mode as u32 + 1) % 3).unwrap();
// Add or remove the light probe.
for light_probe in light_probe_query.iter() {
commands.entity(light_probe).despawn();
}
match app_status.reflection_mode {
ReflectionMode::None | ReflectionMode::EnvironmentMap => {}
ReflectionMode::ReflectionProbe => spawn_reflection_probe(&mut commands, &cubemaps),
}
// Add or remove the environment map from the camera.
for camera in camera_query.iter() {
match app_status.reflection_mode {
ReflectionMode::None => {
commands.entity(camera).remove::<EnvironmentMapLight>();
}
ReflectionMode::EnvironmentMap | ReflectionMode::ReflectionProbe => {
commands
.entity(camera)
.insert(create_camera_environment_map_light(&cubemaps));
}
}
}
}
// A system that handles enabling and disabling rotation.
fn toggle_rotation(keyboard: Res<ButtonInput<KeyCode>>, mut app_status: ResMut<AppStatus>) {
if keyboard.just_pressed(KeyCode::Enter) {
app_status.rotating = !app_status.rotating;
}
}
// A system that updates the help text.
fn update_text(
mut text_query: Query<&mut Text>,
app_status: Res<AppStatus>,
asset_server: Res<AssetServer>,
) {
for mut text in text_query.iter_mut() {
*text = app_status.create_text(&asset_server);
}
}
impl TryFrom<u32> for ReflectionMode {
type Error = ();
fn try_from(value: u32) -> Result<Self, Self::Error> {
match value {
0 => Ok(ReflectionMode::None),
1 => Ok(ReflectionMode::EnvironmentMap),
2 => Ok(ReflectionMode::ReflectionProbe),
_ => Err(()),
}
}
}
impl Display for ReflectionMode {
fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
let text = match *self {
ReflectionMode::None => "No reflections",
ReflectionMode::EnvironmentMap => "Environment map",
ReflectionMode::ReflectionProbe => "Reflection probe",
};
formatter.write_str(text)
}
}
impl AppStatus {
// Constructs the help text at the bottom of the screen based on the
// application status.
fn create_text(&self, asset_server: &AssetServer) -> Text {
let rotation_help_text = if self.rotating {
STOP_ROTATION_HELP_TEXT
} else {
START_ROTATION_HELP_TEXT
};
Text::from_section(
format!(
"{}\n{}\n{}",
self.reflection_mode, rotation_help_text, REFLECTION_MODE_HELP_TEXT
),
TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 24.0,
color: Color::ANTIQUE_WHITE,
},
)
}
}
// Creates the world environment map light, used as a fallback if no reflection
// probe is applicable to a mesh.
fn create_camera_environment_map_light(cubemaps: &Cubemaps) -> EnvironmentMapLight {
EnvironmentMapLight {
diffuse_map: cubemaps.diffuse.clone(),
specular_map: cubemaps.specular_environment_map.clone(),
intensity: 150.0,
}
}
// Rotates the camera a bit every frame.
fn rotate_camera(
mut camera_query: Query<&mut Transform, With<Camera3d>>,
app_status: Res<AppStatus>,
) {
if !app_status.rotating {
return;
}
for mut transform in camera_query.iter_mut() {
transform.translation = Vec2::from_angle(ROTATION_SPEED)
.rotate(transform.translation.xz())
.extend(transform.translation.y)
.xzy();
transform.look_at(Vec3::ZERO, Vec3::Y);
}
}
// Loads the cubemaps from the assets directory.
impl FromWorld for Cubemaps {
fn from_world(world: &mut World) -> Self {
let asset_server = world.resource::<AssetServer>();
// Just use the specular map for the skybox since it's not too blurry.
// In reality you wouldn't do this--you'd use a real skybox texture--but
// reusing the textures like this saves space in the Bevy repository.
let specular_map = asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2");
Cubemaps {
diffuse: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
specular_reflection_probe: asset_server
.load("environment_maps/cubes_reflection_probe_specular_rgb9e5_zstd.ktx2"),
specular_environment_map: specular_map.clone(),
skybox: specular_map,
}
}
}
impl Default for AppStatus {
fn default() -> Self {
Self {
reflection_mode: ReflectionMode::ReflectionProbe,
rotating: true,
}
}
}

View file

@ -136,6 +136,7 @@ Example | Description
[Parallax Mapping](../examples/3d/parallax_mapping.rs) | Demonstrates use of a normal map and depth map for parallax mapping
[Parenting](../examples/3d/parenting.rs) | Demonstrates parent->child relationships and relative transformations
[Physically Based Rendering](../examples/3d/pbr.rs) | Demonstrates use of Physically Based Rendering (PBR) properties
[Reflection Probes](../examples/3d/reflection_probes.rs) | Demonstrates reflection probes
[Render to Texture](../examples/3d/render_to_texture.rs) | Shows how to render to a texture, useful for mirrors, UI, or exporting images
[Screen Space Ambient Occlusion](../examples/3d/ssao.rs) | A scene showcasing screen space ambient occlusion
[Shadow Biases](../examples/3d/shadow_biases.rs) | Demonstrates how shadow biases affect shadows in a 3d scene