diff --git a/Cargo.toml b/Cargo.toml index d531177230..b7c54b5a7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2450,6 +2450,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 diff --git a/assets/environment_maps/cubes_reflection_probe_specular_rgb9e5_zstd.ktx2 b/assets/environment_maps/cubes_reflection_probe_specular_rgb9e5_zstd.ktx2 new file mode 100644 index 0000000000..9c2f2a85a3 Binary files /dev/null and b/assets/environment_maps/cubes_reflection_probe_specular_rgb9e5_zstd.ktx2 differ diff --git a/assets/models/cubes/Cubes.glb b/assets/models/cubes/Cubes.glb new file mode 100644 index 0000000000..0908008267 Binary files /dev/null and b/assets/models/cubes/Cubes.glb differ diff --git a/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl b/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl index 494d86900d..2426f1dce2 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl +++ b/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl @@ -7,8 +7,8 @@ @group(0) @binding(3) var dt_lut_texture: texture_3d; @group(0) @binding(4) var dt_lut_sampler: sampler; #else - @group(0) @binding(15) var dt_lut_texture: texture_3d; - @group(0) @binding(16) var dt_lut_sampler: sampler; + @group(0) @binding(16) var dt_lut_texture: texture_3d; + @group(0) @binding(17) var dt_lut_sampler: sampler; #endif fn sample_current_lut(p: vec3) -> vec3 { diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 709d78179e..7e092e5a38 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -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 = [ diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index 9d334390e0..23eeb71e5b 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -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 diff --git a/crates/bevy_pbr/src/deferred/mod.rs b/crates/bevy_pbr/src/deferred/mod.rs index a47a9da519..cd9f45ba12 100644 --- a/crates/bevy_pbr/src/deferred/mod.rs +++ b/crates/bevy_pbr/src/deferred/mod.rs @@ -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, ( @@ -411,20 +408,20 @@ pub fn prepare_deferred_lighting_pipelines( Has, Has, ), + Has, ), With, >, - images: Res>, ) { 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; } diff --git a/crates/bevy_pbr/src/environment_map/environment_map.wgsl b/crates/bevy_pbr/src/environment_map/environment_map.wgsl deleted file mode 100644 index 0b05e352c2..0000000000 --- a/crates/bevy_pbr/src/environment_map/environment_map.wgsl +++ /dev/null @@ -1,50 +0,0 @@ -#define_import_path bevy_pbr::environment_map - -#import bevy_pbr::mesh_view_bindings as bindings; - -struct EnvironmentMapLight { - diffuse: vec3, - specular: vec3, -}; - -fn environment_map_light( - perceptual_roughness: f32, - roughness: f32, - diffuse_color: vec3, - NdotV: f32, - f_ab: vec2, - N: vec3, - R: vec3, - F0: vec3, -) -> 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; - out.specular = FssEss * radiance; - return out; -} diff --git a/crates/bevy_pbr/src/environment_map/mod.rs b/crates/bevy_pbr/src/environment_map/mod.rs deleted file mode 100644 index 823f264a17..0000000000 --- a/crates/bevy_pbr/src/environment_map/mod.rs +++ /dev/null @@ -1,91 +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 = - 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::() - .add_plugins(ExtractComponentPlugin::::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)] -pub struct EnvironmentMapLight { - pub diffuse_map: Handle, - pub specular_map: Handle, -} - -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) -> 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, - 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), - ] -} diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index ddf95a2c9d..ba29ccce3f 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -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::::default(), FogPlugin, ExtractResourcePlugin::::default(), ExtractComponentPlugin::::default(), LightmapPlugin, + LightProbePlugin, )) .configure_sets( PostUpdate, diff --git a/crates/bevy_pbr/src/light_probe/environment_map.rs b/crates/bevy_pbr/src/light_probe/environment_map.rs new file mode 100644 index 0000000000..ce5ffd8bbb --- /dev/null +++ b/crates/bevy_pbr/src/light_probe/environment_map.rs @@ -0,0 +1,405 @@ +//! 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, + }, + texture::{FallbackImage, Image}, +}; + +#[cfg(all(not(feature = "shader_format_glsl"), not(target_arch = "wasm32")))] +use bevy_utils::HashMap; +#[cfg(all(not(feature = "shader_format_glsl"), not(target_arch = "wasm32")))] +use std::num::NonZeroU32; +#[cfg(all(not(feature = "shader_format_glsl"), not(target_arch = "wasm32")))] +use std::ops::Deref; + +use crate::LightProbe; + +/// A handle to the environment map helper shader. +pub const ENVIRONMENT_MAP_SHADER_HANDLE: Handle = + Handle::weak_from_u128(154476556247605696); + +/// 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, + /// The typically-sharper, mipmapped image that represents specular radiance + /// surrounding a region. + pub specular_map: Handle, +} + +/// 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, + /// The typically-sharper, mipmapped image that represents specular radiance + /// surrounding a region. + pub(crate) specular: AssetId, +} + +/// 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. +#[cfg(all(not(feature = "shader_format_glsl"), not(target_arch = "wasm32")))] +#[derive(Component, Default)] +pub struct RenderViewEnvironmentMaps { + /// The list of environment maps presented to the shader, in order. + binding_index_to_cubemap: Vec, + /// 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, +} + +/// A component, part of the render world, that stores the ID of the environment +/// map attached to each view. +/// +/// This is a simplified version of the structure used when binding arrays are +/// not available on the current platform. +#[cfg(any(feature = "shader_format_glsl", target_arch = "wasm32"))] +#[derive(Component, Default)] +pub struct RenderViewEnvironmentMaps { + /// The environment map attached to the view, if any. + cubemap: Option, +} + +/// All the bind group entries necessary for PBR shaders to access the +/// environment maps exposed to a view. +#[cfg(all(not(feature = "shader_format_glsl"), not(target_arch = "wasm32")))] +pub(crate) struct RenderViewBindGroupEntries<'a> { + /// 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 ::Target>, + + /// As above, but for specular cubemaps. + specular_texture_views: Vec<&'a ::Target>, + + /// The sampler used to sample elements of both `diffuse_texture_views` and + /// `specular_texture_views`. + pub(crate) sampler: &'a Sampler, +} + +/// All the bind group entries necessary for PBR shaders to access the +/// environment maps exposed to a view. +/// +/// This is the version used when binding arrays are not available on the +/// current platform. +#[cfg(any(feature = "shader_format_glsl", target_arch = "wasm32"))] +pub(crate) struct RenderViewBindGroupEntries<'a> { + /// 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_view` and + /// `specular_texture_view`. + pub(crate) sampler: &'a Sampler, +} + +impl ExtractInstance for EnvironmentMapIds { + type Data = Read; + + type Filter = (); + + fn extract(item: QueryItem<'_, Self::Data>) -> Option { + Some(EnvironmentMapIds { + diffuse: item.diffuse_map.id(), + specular: item.specular_map.id(), + }) + } +} + +impl RenderViewEnvironmentMaps { + pub(crate) fn new() -> Self { + Self::default() + } +} + +#[cfg(all(not(feature = "shader_format_glsl"), not(target_arch = "wasm32")))] +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 + }) + } +} + +#[cfg(any(feature = "shader_format_glsl", target_arch = "wasm32"))] +impl RenderViewEnvironmentMaps { + /// Returns true if there is no environment map for this view or false if + /// there is such an environment map. + pub(crate) fn is_empty(&self) -> bool { + self.cubemap.is_none() + } + + /// Sets the environment map attached to this view, replacing the previous + /// one if any. + pub(crate) fn get_or_insert_cubemap(&mut self, cubemap_id: &EnvironmentMapIds) -> u32 { + self.cubemap = Some(*cubemap_id); + 0 + } +} + +/// Returns the bind group layout entries for the environment map diffuse and +/// specular binding arrays respectively, in addition to the sampler. +#[cfg(all(not(feature = "shader_format_glsl"), not(target_arch = "wasm32")))] +pub(crate) fn get_bind_group_layout_entries() -> [BindGroupLayoutEntryBuilder; 3] { + use crate::MAX_VIEW_REFLECTION_PROBES; + + [ + binding_types::texture_cube(TextureSampleType::Float { filterable: true }) + .count(NonZeroU32::new(MAX_VIEW_REFLECTION_PROBES as _).unwrap()), + binding_types::texture_cube(TextureSampleType::Float { filterable: true }) + .count(NonZeroU32::new(MAX_VIEW_REFLECTION_PROBES as _).unwrap()), + binding_types::sampler(SamplerBindingType::Filtering), + ] +} + +/// Returns the bind group layout entries for the environment map diffuse and +/// specular textures respectively, in addition to the sampler. +#[cfg(any(feature = "shader_format_glsl", target_arch = "wasm32"))] +pub(crate) fn get_bind_group_layout_entries() -> [BindGroupLayoutEntryBuilder; 3] { + [ + binding_types::texture_cube(TextureSampleType::Float { filterable: true }), + binding_types::texture_cube(TextureSampleType::Float { filterable: true }), + 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. + #[cfg(all(not(feature = "shader_format_glsl"), not(target_arch = "wasm32")))] + pub(crate) fn get( + render_view_environment_maps: Option<&RenderViewEnvironmentMaps>, + images: &'a RenderAssets, + fallback_image: &'a FallbackImage, + ) -> RenderViewBindGroupEntries<'a> { + use crate::MAX_VIEW_REFLECTION_PROBES; + + 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. + 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, + ); + + RenderViewBindGroupEntries { + diffuse_texture_views, + specular_texture_views, + sampler: sampler.unwrap_or(&fallback_image.cube.sampler), + } + } + + /// Looks up and returns the bindings for the environment map diffuse and + /// specular bindings respectively, as well as the sampler. + #[cfg(any(feature = "shader_format_glsl", target_arch = "wasm32"))] + pub(crate) fn get( + render_view_environment_maps: Option<&RenderViewEnvironmentMaps>, + images: &'a RenderAssets, + fallback_image: &'a FallbackImage, + ) -> RenderViewBindGroupEntries<'a> { + if let Some(&RenderViewEnvironmentMaps { + cubemap: Some(ref cubemap), + }) = render_view_environment_maps + { + if let (Some(diffuse_image), Some(specular_image)) = + (images.get(cubemap.diffuse), images.get(cubemap.specular)) + { + return RenderViewBindGroupEntries { + diffuse_texture_view: &diffuse_image.texture_view, + specular_texture_view: &specular_image.texture_view, + sampler: &diffuse_image.sampler, + }; + } + } + + RenderViewBindGroupEntries { + 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. +#[cfg(all(not(feature = "shader_format_glsl"), not(target_arch = "wasm32")))] +fn add_texture_view<'a>( + texture_views: &mut Vec<&'a ::Target>, + sampler: &mut Option<&'a Sampler>, + image_id: AssetId, + images: &'a RenderAssets, + 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); + } + } +} + +#[cfg(all(not(feature = "shader_format_glsl"), not(target_arch = "wasm32")))] +impl<'a> RenderViewBindGroupEntries<'a> { + /// Returns a list of texture views of each diffuse cubemap, in binding + /// order. + pub(crate) fn diffuse_texture_views(&'a self) -> &'a [&'a ::Target] { + self.diffuse_texture_views.as_slice() + } + + /// Returns a list of texture views of each specular cubemap, in binding + /// order. + pub(crate) fn specular_texture_views(&'a self) -> &'a [&'a ::Target] { + self.specular_texture_views.as_slice() + } +} + +#[cfg(any(feature = "shader_format_glsl", target_arch = "wasm32"))] +impl<'a> RenderViewBindGroupEntries<'a> { + /// Returns the texture view corresponding to the view's diffuse cubemap. + pub(crate) fn diffuse_texture_views(&self) -> &'a TextureView { + self.diffuse_texture_view + } + + /// Returns the texture view corresponding to the view's specular cubemap. + pub(crate) fn specular_texture_views(&self) -> &'a TextureView { + self.specular_texture_view + } +} diff --git a/crates/bevy_pbr/src/light_probe/environment_map.wgsl b/crates/bevy_pbr/src/light_probe/environment_map.wgsl new file mode 100644 index 0000000000..a500fe3e73 --- /dev/null +++ b/crates/bevy_pbr/src/light_probe/environment_map.wgsl @@ -0,0 +1,171 @@ +#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, + specular: vec3, +}; + +struct EnvironmentMapRadiances { + irradiance: vec3, + radiance: vec3, +} + +// 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, + R: vec3, + world_position: vec3, +) -> EnvironmentMapRadiances { + var radiances: EnvironmentMapRadiances; + + // Search for a reflection probe that contains the fragment. + // + // TODO: Interpolate between multiple reflection probes. + var cubemap_index: i32 = -1; + 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( + reflection_probe.inverse_transpose_transform[0], + reflection_probe.inverse_transpose_transform[1], + reflection_probe.inverse_transpose_transform[2], + vec4(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(world_position, 1.0)).xyz; + if (all(abs(probe_space_pos) <= vec3(0.5))) { + cubemap_index = reflection_probe.cubemap_index; + 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; + } + + // 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; +#endif // LIGHTMAP + + radiances.radiance = textureSampleLevel( + bindings::specular_environment_maps[cubemap_index], + bindings::environment_map_sampler, + vec3(R.xy, -R.z), + radiance_level).rgb; + + return radiances; +} + +#else // MULTIPLE_LIGHT_PROBES_IN_ARRAY + +fn compute_radiances( + perceptual_roughness: f32, + N: vec3, + R: vec3, + world_position: vec3, +) -> 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); + +#ifndef LIGHTMAP + radiances.irradiance = textureSampleLevel( + bindings::diffuse_environment_map, + bindings::environment_map_sampler, + vec3(N.xy, -N.z), + 0.0).rgb; +#endif // LIGHTMAP + + radiances.radiance = textureSampleLevel( + bindings::specular_environment_map, + bindings::environment_map_sampler, + vec3(R.xy, -R.z), + radiance_level).rgb; + + return radiances; +} + +#endif // MULTIPLE_LIGHT_PROBES_IN_ARRAY + +fn environment_map_light( + perceptual_roughness: f32, + roughness: f32, + diffuse_color: vec3, + NdotV: f32, + f_ab: vec2, + N: vec3, + R: vec3, + F0: vec3, + world_position: vec3, +) -> 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; +} diff --git a/crates/bevy_pbr/src/light_probe/mod.rs b/crates/bevy_pbr/src/light_probe/mod.rs new file mode 100644 index 0000000000..03ac1af5ea --- /dev/null +++ b/crates/bevy_pbr/src/light_probe/mod.rs @@ -0,0 +1,405 @@ +//! 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::{ + 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, +} + +/// 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, +} + +/// A map from each camera to the light probe uniform associated with it. +#[derive(Resource, Default, Deref, DerefMut)] +struct RenderLightProbes(EntityHashMap); + +/// A GPU buffer that stores information about all light probes. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct LightProbesBuffer(DynamicUniformBuffer); + +/// 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, +} + +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::() + .register_type::(); + } + + fn finish(&self, app: &mut App) { + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .add_plugins(ExtractInstancesPlugin::::new()) + .init_resource::() + .init_resource::() + .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. +fn gather_light_probes( + mut render_light_probes: ResMut, + image_assets: Res>, + light_probe_query: Extract>>, + view_query: Extract< + Query< + ( + Entity, + &GlobalTransform, + &Frustum, + Option<&EnvironmentMapLight>, + ), + With, + >, + >, + mut light_probes: Local>, + mut view_light_probes: Local>, + 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); + + // 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::(); + } 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, + mut light_probes_buffer: ResMut, + render_device: Res, + render_queue: Res, +) { + // 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, + } + } +} + +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, + ) -> (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, mut smallest_specular_mip_level_for_view) = (-1, 0); + if let Some(EnvironmentMapLight { + diffuse_map: diffuse_map_handle, + specular_map: specular_map_handle, + }) = 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; + } + }; + + // 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, + }; + + // Add reflection probes from the scene, if supported by the current + // platform. + uniform.maybe_gather_reflection_probes(&mut render_view_environment_maps, light_probes); + (uniform, render_view_environment_maps) + } + + /// Gathers up all reflection probes in the scene and writes them into this + /// uniform and `render_view_environment_maps`. + #[cfg(all(not(feature = "shader_format_glsl"), not(target_arch = "wasm32")))] + fn maybe_gather_reflection_probes( + &mut self, + render_view_environment_maps: &mut RenderViewEnvironmentMaps, + light_probes: &[LightProbeInfo], + ) { + 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, + }; + } + } + + /// This is the version of `maybe_gather_reflection_probes` used on + /// platforms in which binding arrays aren't available. It's simply a no-op. + #[cfg(any(feature = "shader_format_glsl", target_arch = "wasm32"))] + fn maybe_gather_reflection_probes( + &mut self, + _: &mut RenderViewEnvironmentMaps, + _: &[LightProbeInfo], + ) { + } +} + +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, + ) -> Option { + 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(), + }, + }) + } + + /// 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(), + ) + } +} diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 51bef6ed71..e8f9601cb1 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -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::{ @@ -464,14 +464,12 @@ pub fn queue_material_meshes( render_materials: Res>, mut render_mesh_instances: ResMut, render_material_instances: Res>, - images: Res>, render_lightmaps: Res, mut views: Query<( &ExtractedView, &VisibleEntities, Option<&Tonemapping>, Option<&DebandDither>, - Option<&EnvironmentMapLight>, Option<&ShadowFilteringMethod>, Has, ( @@ -487,6 +485,7 @@ pub fn queue_material_meshes( &mut RenderPhase, &mut RenderPhase, &mut RenderPhase, + Has, )>, ) where M::Data: PartialEq + Eq + Hash + Clone, @@ -496,7 +495,6 @@ pub fn queue_material_meshes( visible_entities, tonemapping, dither, - environment_map, shadow_filter_method, ssao, (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), @@ -507,6 +505,7 @@ pub fn queue_material_meshes( 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::>(); @@ -537,9 +536,7 @@ pub fn queue_material_meshes( 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; } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index f7666f1205..80c7e14113 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -196,7 +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, } // NOTE: this must be kept in sync with the same constants in pbr.frag @@ -644,18 +643,12 @@ pub(crate) fn spot_light_projection_matrix(angle: f32) -> Mat4 { pub fn prepare_lights( mut commands: Commands, mut texture_cache: ResMut, - images: Res>, render_device: Res, render_queue: Res, mut global_light_meta: ResMut, mut light_meta: ResMut, views: Query< - ( - Entity, - &ExtractedView, - &ExtractedClusterConfig, - Option<&EnvironmentMapLight>, - ), + (Entity, &ExtractedView, &ExtractedClusterConfig), With>, >, ambient_light: Res, @@ -901,7 +894,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 { @@ -968,10 +961,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), }; // TODO: this should select lights based on relevance to the view instead of the first ones that show up in a query diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index b1734b621a..23820a2761 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -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, }; @@ -866,6 +873,9 @@ impl SpecializedMeshPipeline for MeshPipeline { }, )); + #[cfg(all(not(feature = "shader_format_glsl"), not(target_arch = "wasm32")))] + shader_defs.push("MULTIPLE_LIGHT_PROBES_IN_ARRAY".into()); + let format = if key.contains(MeshPipelineKey::HDR) { ViewTarget::TEXTURE_FORMAT_HDR } else { @@ -994,6 +1004,7 @@ pub fn prepare_mesh_bind_group( let Some(model) = mesh_uniforms.binding() else { return; }; + groups.model_only = Some(layouts.model_only(&render_device, &model)); let skin = skins_uniform.buffer.buffer(); @@ -1031,6 +1042,7 @@ impl RenderCommand

for SetMeshViewBindGroup Read, Read, Read, + Read, Read, ); type ItemData = (); @@ -1038,7 +1050,7 @@ impl RenderCommand

for SetMeshViewBindGroup #[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, >, @@ -1049,7 +1061,12 @@ impl RenderCommand

for SetMeshViewBindGroup 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 diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.rs b/crates/bevy_pbr/src/render/mesh_view_bindings.rs index eb020cf4e5..8b77e81eb3 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.rs @@ -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)] @@ -234,9 +235,11 @@ fn layout_entries( (9, uniform_buffer::(false)), // Fog (10, uniform_buffer::(true)), + // Light probes + (11, uniform_buffer::(true)), // Screen space ambient occlusion texture ( - 11, + 12, texture_2d(TextureSampleType::Float { filterable: false }), ), ), @@ -245,16 +248,16 @@ fn layout_entries( // EnvironmentMapLight let environment_map_entries = environment_map::get_bind_group_layout_entries(); 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 +267,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 +278,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() @@ -331,18 +334,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>, FallbackImageMsaa, - Res, + Res, Res, ), msaa: Res, globals_buffer: Res, tonemapping_luts: Res, + light_probes_buffer: Res, ) { if let ( Some(view_binding), @@ -350,12 +354,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 +370,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 +399,23 @@ 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, + ); entries = entries.extend_with_indices(( - (12, env_map_bindings.0), - (13, env_map_bindings.1), - (14, env_map_bindings.2), + (13, bind_group_entries.diffuse_texture_views()), + (14, bind_group_entries.specular_texture_views()), + (15, bind_group_entries.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 +425,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 +441,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), diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl index 6f4293d6d6..37d54bf60d 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl @@ -33,44 +33,50 @@ @group(0) @binding(9) var globals: Globals; @group(0) @binding(10) var fog: types::Fog; +@group(0) @binding(11) var light_probes: types::LightProbes; -@group(0) @binding(11) var screen_space_ambient_occlusion_texture: texture_2d; +@group(0) @binding(12) var screen_space_ambient_occlusion_texture: texture_2d; -@group(0) @binding(12) var environment_map_diffuse: texture_cube; -@group(0) @binding(13) var environment_map_specular: texture_cube; -@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>; +@group(0) @binding(14) var specular_environment_maps: binding_array>; +#else +@group(0) @binding(13) var diffuse_environment_map: texture_cube; +@group(0) @binding(14) var specular_environment_map: texture_cube; +#endif +@group(0) @binding(15) var environment_map_sampler: sampler; -@group(0) @binding(15) var dt_lut_texture: texture_3d; -@group(0) @binding(16) var dt_lut_sampler: sampler; +@group(0) @binding(16) var dt_lut_texture: texture_3d; +@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; +@group(0) @binding(19) var normal_prepass_texture: texture_multisampled_2d; #endif // NORMAL_PREPASS #ifdef MOTION_VECTOR_PREPASS -@group(0) @binding(19) var motion_vector_prepass_texture: texture_multisampled_2d; +@group(0) @binding(20) var motion_vector_prepass_texture: texture_multisampled_2d; #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; +@group(0) @binding(19) var normal_prepass_texture: texture_2d; #endif // NORMAL_PREPASS #ifdef MOTION_VECTOR_PREPASS -@group(0) @binding(19) var motion_vector_prepass_texture: texture_2d; +@group(0) @binding(20) var motion_vector_prepass_texture: texture_2d; #endif // MOTION_VECTOR_PREPASS #endif // MULTISAMPLED #ifdef DEFERRED_PREPASS -@group(0) @binding(20) var deferred_prepass_texture: texture_2d; +@group(0) @binding(21) var deferred_prepass_texture: texture_2d; #endif // DEFERRED_PREPASS -@group(0) @binding(21) var view_transmission_texture: texture_2d; -@group(0) @binding(22) var view_transmission_sampler: sampler; +@group(0) @binding(22) var view_transmission_texture: texture_2d; +@group(0) @binding(23) var view_transmission_sampler: sampler; diff --git a/crates/bevy_pbr/src/render/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index 3062ad671a..8d643fd719 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -109,3 +109,22 @@ struct ClusterOffsetsAndCounts { data: array, 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, + cubemap_index: i32, +}; + +struct LightProbes { + // This must match `MAX_VIEW_REFLECTION_PROBES` on the Rust side. + reflection_probes: array, + 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, +}; diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index 3f2e8c661f..e01fa9be5f 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -322,7 +322,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 * occlusion) + environment_light.specular; // we'll use the specular component of the transmitted environment @@ -348,7 +357,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(1.0), 1.0, f_ab, -in.N, T, vec3(1.0)); + let transmitted_environment_light = bevy_pbr::environment_map::environment_map_light( + perceptual_roughness, + roughness, + vec3(1.0), + 1.0, + f_ab, + -in.N, + T, + vec3(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; } diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index ba2f794504..110aae2df0 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -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"] } diff --git a/crates/bevy_render/src/render_resource/shader.rs b/crates/bevy_render/src/render_resource/shader.rs index 7b10677784..677378cc90 100644 --- a/crates/bevy_render/src/render_resource/shader.rs +++ b/crates/bevy_render/src/render_resource/shader.rs @@ -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"), } } diff --git a/examples/3d/reflection_probes.rs b/examples/3d/reflection_probes.rs new file mode 100644 index 0000000000..cea7362f2d --- /dev/null +++ b/examples/3d/reflection_probes.rs @@ -0,0 +1,345 @@ +//! 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, + + // The specular cubemap that reflects the world, but not the cubes. + specular_environment_map: Handle, + + // The specular cubemap that reflects both the world and the cubes. + specular_reflection_probe: Handle, + + // The skybox cubemap image. This is almost the same as + // `specular_environment_map`. + skybox: Handle, +} + +fn main() { + // Create the app. + App::new() + .add_plugins(DefaultPlugins) + .init_resource::() + .init_resource::() + .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>, + mut materials: ResMut>, + asset_server: Res, + app_status: Res, + cubemaps: Res, +) { + spawn_scene(&mut commands, &asset_server); + 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() + }); +} + +// Creates the sphere mesh and spawns it. +fn spawn_sphere( + commands: &mut Commands, + meshes: &mut Assets, + materials: &mut Assets, +) { + // 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(), + }, + }); +} + +// 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>, + cubemaps: Res, +) { + for camera_entity in query.iter() { + commands + .entity(camera_entity) + .insert(create_camera_environment_map_light(&cubemaps)) + .insert(Skybox(cubemaps.skybox.clone())); + } +} + +// A system that handles switching between different reflection modes. +fn change_reflection_type( + mut commands: Commands, + light_probe_query: Query>, + camera_query: Query>, + keyboard: Res>, + mut app_status: ResMut, + cubemaps: Res, +) { + // 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::(); + } + 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>, mut app_status: ResMut) { + 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, + asset_server: Res, +) { + for mut text in text_query.iter_mut() { + *text = app_status.create_text(&asset_server); + } +} + +impl TryFrom for ReflectionMode { + type Error = (); + + fn try_from(value: u32) -> Result { + 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(), + } +} + +// Rotates the camera a bit every frame. +fn rotate_camera( + mut camera_query: Query<&mut Transform, With>, + app_status: Res, +) { + 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::(); + + // 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, + } + } +} diff --git a/examples/README.md b/examples/README.md index 7a0e2fa88d..d9e4db4c7b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -133,6 +133,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