diff --git a/assets/environment_maps/cubes_reflection_probe_specular_rgb9e5_zstd.ktx2 b/assets/environment_maps/cubes_reflection_probe_specular_rgb9e5_zstd.ktx2 index 9c2f2a85a3bb8..147adba49f2db 100644 Binary files a/assets/environment_maps/cubes_reflection_probe_specular_rgb9e5_zstd.ktx2 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 index 9e1b481ebfe0e..5ab33b10af4ed 100644 Binary files a/assets/models/cubes/Cubes.glb and b/assets/models/cubes/Cubes.glb differ diff --git a/crates/bevy_core_pipeline/Cargo.toml b/crates/bevy_core_pipeline/Cargo.toml index 1b13d9b22878c..8477d32e85928 100644 --- a/crates/bevy_core_pipeline/Cargo.toml +++ b/crates/bevy_core_pipeline/Cargo.toml @@ -39,6 +39,7 @@ radsort = "0.1" nonmax = "0.5" smallvec = "1" thiserror = "1.0" +arrayvec = "0.7" [lints] workspace = true diff --git a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs index cf062d340ff79..fd8aebeb66352 100644 --- a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs +++ b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs @@ -1,17 +1,38 @@ +use std::{iter, marker::PhantomData}; + use crate::{ core_3d::graph::Core3d, tonemapping::{DebandDither, Tonemapping}, }; +use arrayvec::ArrayVec; +use bevy_app::{App, Plugin}; +use bevy_asset::Assets; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::prelude::*; +use bevy_math::{uvec4, UVec2}; use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; use bevy_render::{ - camera::{Camera, CameraMainTextureUsages, CameraRenderGraph, Exposure, Projection}, + batching::gpu_preprocessing::GpuPreprocessingSupport, + camera::{ + Camera, CameraMainTextureUsages, CameraRenderGraph, CubemapFaceProjections, Exposure, + ExtractedCamera, NormalizedRenderTarget, OmnidirectionalProjection, Projection, + RenderTarget, TemporalJitter, Viewport, + }, extract_component::ExtractComponent, - primitives::Frustum, + extract_instances::{ExtractInstance, ExtractedInstances}, + primitives::{CubemapFrusta, Frustum}, render_resource::{LoadOp, TextureUsages}, - view::{ColorGrading, VisibleEntities}, + texture::Image, + view::{ + ColorGrading, CubemapVisibleEntities, ExtractedView, GpuCulling, RenderLayers, + VisibleEntities, + }, + Extract, ExtractSchedule, RenderApp, }; use bevy_transform::prelude::{GlobalTransform, Transform}; +use bevy_utils::EntityHashMap; +use bitflags::bitflags; +use nonmax::NonMaxU32; use serde::{Deserialize, Serialize}; /// Configuration for the "main 3d render graph". @@ -174,3 +195,374 @@ impl Default for Camera3dBundle { } } } + +/// A 360° camera that renders to a cubemap image. +/// +/// These cubemap images are typically attached to an environment map light +/// on a light probe, in order to achieve real-time reflective surfaces. +/// +/// Internally, these cameras become six subcameras, one for each side of the +/// cube. Consequently, omnidirectional cameras are quite expensive by default. +/// The [`ActiveCubemapSides`] bitfield may be used to reduce this load by +/// rendering to only a subset of the cubemap faces each frame. A common +/// technique is to render to only one cubemap face per frame, cycling through +/// the faces in a round-robin fashion. +#[derive(Bundle)] +pub struct OmnidirectionalCamera3dBundle { + pub camera: Camera, + pub camera_render_graph: CameraRenderGraph, + pub projection: OmnidirectionalProjection, + pub visible_entities: CubemapVisibleEntities, + pub active_cubemap_sides: ActiveCubemapSides, + pub frustum: CubemapFrusta, + pub transform: Transform, + pub global_transform: GlobalTransform, + pub camera_3d: Camera3d, + pub tonemapping: Tonemapping, + pub deband_dither: DebandDither, + pub color_grading: ColorGrading, + pub exposure: Exposure, + pub main_texture_usages: CameraMainTextureUsages, +} + +impl Default for OmnidirectionalCamera3dBundle { + fn default() -> Self { + Self { + camera: Default::default(), + camera_render_graph: CameraRenderGraph::new(Core3d), + projection: Default::default(), + visible_entities: Default::default(), + frustum: Default::default(), + transform: Default::default(), + global_transform: Default::default(), + camera_3d: Default::default(), + tonemapping: Default::default(), + deband_dither: DebandDither::Enabled, + color_grading: Default::default(), + exposure: Default::default(), + main_texture_usages: Default::default(), + active_cubemap_sides: Default::default(), + } + } +} + +#[derive(Resource, Default, Deref, DerefMut)] +pub struct RenderOmnidirectionalCameras(EntityHashMap>); + +bitflags! { + /// Specifies which sides of an omnidirectional camera will be rendered to + /// this frame. + /// + /// Enabling a flag will cause the renderer to refresh the corresponding + /// cubemap side on this frame. + #[derive(Clone, Copy, Component)] + pub struct ActiveCubemapSides: u8 { + const X = 0x01; + const NEG_X = 0x02; + const Y = 0x04; + const NEG_Y = 0x08; + const NEG_Z = 0x10; + const Z = 0x20; + } +} + +impl Default for ActiveCubemapSides { + fn default() -> ActiveCubemapSides { + ActiveCubemapSides::all() + } +} + +/// Extracts components from main world cameras to render world cameras. +/// +/// You should generally use this plugin instead of [`ExtractComponentPlugin`] +/// for components on cameras, because in the case of omnidirectional cameras +/// each main world camera will extract to as many as six different sub-cameras, +/// one for each face, and components should be copied onto each face camera. +pub struct ExtractCameraComponentPlugin { + marker: PhantomData (C, F)>, +} + +impl Plugin for ExtractCameraComponentPlugin +where + C: ExtractComponent, + C::Out: Clone + 'static, + F: 'static, +{ + fn build(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + render_app.add_systems( + ExtractSchedule, + extract_camera_components::.after(extract_omnidirectional_cameras), + ); + } +} + +impl Default for ExtractCameraComponentPlugin { + fn default() -> Self { + Self { + marker: Default::default(), + } + } +} + +/// A plugin that extracts one or more components from a camera into the render +/// world like the [`bevy_render::extract_instances::ExtractInstancesPlugin`] +/// does. +/// +/// This plugin should be used instead of +/// [`bevy_render::extract_instances::ExtractInstancesPlugin`] for any component +/// intended to be attached to cameras, because in the case of omnidirectional +/// cameras it'll copy the component to the six individual faces. +pub struct ExtractCameraInstancesPlugin +where + EI: ExtractInstance + Clone, +{ + marker: PhantomData EI>, +} + +impl Plugin for ExtractCameraInstancesPlugin +where + EI: ExtractInstance + Clone, +{ + fn build(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app.init_resource::>(); + render_app.add_systems( + ExtractSchedule, + extract_instances_from_cameras::.after(extract_omnidirectional_cameras), + ); + } +} + +impl ExtractCameraInstancesPlugin +where + EI: ExtractInstance + Clone, +{ + /// Creates a new [`ExtractCameraInstancesPlugin`] for a single instance. + pub fn new() -> ExtractCameraInstancesPlugin { + ExtractCameraInstancesPlugin { + marker: PhantomData, + } + } +} + +impl Default for ExtractCameraInstancesPlugin +where + EI: ExtractInstance + Clone, +{ + fn default() -> Self { + Self::new() + } +} + +/// A system that performs the component extraction to the render world for a +/// component with a corresponding [`ExtractCameraComponentPlugin`]. +/// +/// This system knows about omnidirectional cameras and will copy the component +/// to the individual face cameras as appropriate. +pub fn extract_camera_components( + mut commands: Commands, + mut previous_to_spawn_len: Local, + omnidirectional_cameras: Res, + query: Extract>, +) where + C: ExtractComponent, + C::Out: Clone + 'static, +{ + let mut to_spawn = Vec::with_capacity(*previous_to_spawn_len); + + for (camera, row) in &query { + let Some(extracted_component) = C::extract_component(row) else { + continue; + }; + + // If this is an omnidirectional camera, gather up its subcameras; + // otherwise, just use the camera entity from the query. + let view_entities: ArrayVec = match omnidirectional_cameras.get(&camera) { + None => iter::once(camera).collect(), + Some(entities) => entities.clone(), + }; + + for view_entity in view_entities { + to_spawn.push((view_entity, extracted_component.clone())); + } + } + + *previous_to_spawn_len = to_spawn.len(); + commands.insert_or_spawn_batch(to_spawn); +} + +/// A system that pulls components from the main world and places them into an +/// [`ExtractedComponents`] resource in the render world, for components present +/// on cameras. +/// +/// This system is added by the [`ExtractCameraInstancesPlugin`]. It knows about +/// omnidirectional cameras and will correctly extract components to their six +/// sub-cameras as appropriate. +pub fn extract_instances_from_cameras( + mut extracted_instances: ResMut>, + omnidirectional_cameras: Res, + query: Extract>, +) where + EI: ExtractInstance + Clone, +{ + extracted_instances.clear(); + + for (camera, row) in &query { + let Some(extract_instance) = EI::extract(row) else { + continue; + }; + + let view_entities: ArrayVec = match omnidirectional_cameras.get(&camera) { + None => iter::once(camera).collect(), + Some(entities) => entities.iter().cloned().collect(), + }; + for view_entity in view_entities { + extracted_instances.insert(view_entity, extract_instance.clone()); + } + } +} + +/// A system that extracts all omnidirectional cameras to the render world. +/// +/// This system populates the [`RenderOmnidirectionalCameras`] resource with +/// newly-created entity IDs for the individual face cameras as it does so. It +/// must run before any systems that add components to the individual face +/// cameras from omnidirectional cameras. +pub fn extract_omnidirectional_cameras( + mut commands: Commands, + images: Extract>>, + query: Extract< + Query<( + Entity, + &Camera, + &Camera3d, + &Tonemapping, + &CameraRenderGraph, + &GlobalTransform, + &CubemapVisibleEntities, + &CubemapFrusta, + ( + Option<&ColorGrading>, + Option<&Exposure>, + Option<&TemporalJitter>, + Option<&RenderLayers>, + ), + &OmnidirectionalProjection, + &CameraMainTextureUsages, + &ActiveCubemapSides, + Has, + )>, + >, + gpu_preprocessing_support: Res, + mut omnidirectional_cameras: ResMut, +) { + omnidirectional_cameras.clear(); + + for ( + camera_entity, + camera, + camera_3d, + tonemapping, + camera_render_graph, + camera_transform, + visible_entities, + cubemap_frusta, + (color_grading, exposure, temporal_jitter, render_layers), + projection, + main_texture_usages, + active_cubemap_sides, + gpu_culling, + ) in query.iter() + { + if !camera.is_active { + continue; + } + + let RenderTarget::Image(ref cubemap_image_handle) = camera.target else { + continue; + }; + let Some(cubemap_image) = images.get(cubemap_image_handle) else { + continue; + }; + + let view_translation = GlobalTransform::from_translation(camera_transform.translation()); + let color_grading = color_grading.cloned().unwrap_or_default(); + let cubemap_projections = CubemapFaceProjections::new(projection.near); + + // Create the individual subcameras. We may not end up having all six of + // them if some of them are inactive. + let mut subcameras: ArrayVec = ArrayVec::new(); + for (face_index, (view_rotation, frustum)) in cubemap_projections + .rotations + .iter() + .zip(&cubemap_frusta.frusta) + .enumerate() + { + // If this side is inactive, skip it. + if !active_cubemap_sides.contains(ActiveCubemapSides::from_bits_retain(1 << face_index)) + { + continue; + } + + let mut entity_commands = commands.spawn(ExtractedView { + clip_from_view: cubemap_projections.projection, + world_from_view: view_translation * *view_rotation, + clip_from_world: None, + hdr: camera.hdr, + viewport: uvec4(0, 0, cubemap_image.width(), cubemap_image.height()), + color_grading: color_grading.clone(), + }); + + entity_commands + .insert(ExtractedCamera { + target: Some(NormalizedRenderTarget::Image(cubemap_image_handle.clone())), + viewport: Some(Viewport { + physical_position: UVec2::ZERO, + physical_size: cubemap_image.size(), + depth: match camera.viewport { + Some(ref viewport) => viewport.depth.clone(), + None => 0.0..1.0, + }, + }), + physical_viewport_size: Some(cubemap_image.size()), + physical_target_size: Some(cubemap_image.size()), + render_graph: **camera_render_graph, + order: camera.order, + output_mode: camera.output_mode, + msaa_writeback: camera.msaa_writeback, + clear_color: camera.clear_color, + sorted_camera_index_for_target: 0, + exposure: exposure.cloned().unwrap_or_default().exposure(), + render_target_layer: Some(NonMaxU32::try_from(face_index as u32).unwrap()), + hdr: camera.hdr, + }) + .insert(camera_3d.clone()) + .insert(*frustum) + .insert(*main_texture_usages) + .insert(*tonemapping) + .insert(visible_entities.get(face_index).clone()); + + if let Some(temporal_jitter) = temporal_jitter { + entity_commands.insert(temporal_jitter.clone()); + } + + if let Some(render_layers) = render_layers { + entity_commands.insert(render_layers.clone()); + } + + if gpu_culling { + gpu_preprocessing_support.maybe_add_gpu_culling(&mut entity_commands); + } + + subcameras.push(entity_commands.id()); + } + + omnidirectional_cameras.insert(camera_entity, subcameras); + } +} diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index a847607911a44..5db75ab05bccf 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -62,7 +62,7 @@ pub const DEPTH_TEXTURE_SAMPLING_SUPPORTED: bool = false; #[cfg(any(feature = "webgpu", not(target_arch = "wasm32")))] pub const DEPTH_TEXTURE_SAMPLING_SUPPORTED: bool = true; -use std::ops::Range; +use std::{iter, ops::Range}; use bevy_asset::AssetId; use bevy_color::LinearRgba; @@ -75,7 +75,6 @@ use bevy_ecs::{entity::EntityHashSet, prelude::*}; use bevy_math::FloatOrd; use bevy_render::{ camera::{Camera, ExtractedCamera}, - extract_component::ExtractComponentPlugin, mesh::Mesh, prelude::Msaa, render_graph::{EmptyNode, RenderGraphApp, ViewNodeRunner}, @@ -121,7 +120,14 @@ impl Plugin for Core3dPlugin { fn build(&self, app: &mut App) { app.register_type::() .register_type::() - .add_plugins((SkyboxPlugin, ExtractComponentPlugin::::default())) + .add_plugins(( + SkyboxPlugin, + ExtractCameraComponentPlugin::::default(), + ExtractCameraComponentPlugin::::default(), + ExtractCameraComponentPlugin::::default(), + ExtractCameraComponentPlugin::::default(), + ExtractCameraComponentPlugin::::default(), + )) .add_systems(PostUpdate, check_msaa); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { @@ -136,8 +142,13 @@ impl Plugin for Core3dPlugin { .init_resource::>() .init_resource::>() .init_resource::>() - .add_systems(ExtractSchedule, extract_core_3d_camera_phases) - .add_systems(ExtractSchedule, extract_camera_prepass_phase) + .init_resource::() + .add_systems(ExtractSchedule, extract_omnidirectional_cameras) + .add_systems( + ExtractSchedule, + (extract_core_3d_camera_phases, extract_camera_prepass_phase) + .after(extract_omnidirectional_cameras), + ) .add_systems( Render, ( @@ -492,6 +503,7 @@ impl CachedRenderPipelinePhaseItem for Transparent3d { } } +#[allow(clippy::too_many_arguments)] pub fn extract_core_3d_camera_phases( mut commands: Commands, mut opaque_3d_phases: ResMut>, @@ -499,15 +511,24 @@ pub fn extract_core_3d_camera_phases( mut transmissive_3d_phases: ResMut>, mut transparent_3d_phases: ResMut>, cameras_3d: Extract>>, + omnidirectional_cameras: Res, mut live_entities: Local, ) { live_entities.clear(); - for (entity, camera) in &cameras_3d { - if !camera.is_active { - continue; - } - + for entity in cameras_3d + .iter() + .filter_map( + |(entity, camera)| { + if camera.is_active { + Some(entity) + } else { + None + } + }, + ) + .chain(omnidirectional_cameras.values().flatten().cloned()) + { commands.get_or_spawn(entity); opaque_3d_phases.insert_or_clear(entity); @@ -525,8 +546,8 @@ pub fn extract_core_3d_camera_phases( } // Extract the render phases for the prepass +#[allow(clippy::too_many_arguments)] pub fn extract_camera_prepass_phase( - mut commands: Commands, mut opaque_3d_prepass_phases: ResMut>, mut alpha_mask_3d_prepass_phases: ResMut>, mut opaque_3d_deferred_phases: ResMut>, @@ -545,47 +566,48 @@ pub fn extract_camera_prepass_phase( >, >, mut live_entities: Local, + omnidirectional_cameras: Res, ) { live_entities.clear(); - for (entity, camera, depth_prepass, normal_prepass, motion_vector_prepass, deferred_prepass) in - cameras_3d.iter() + for ( + camera_entity, + camera, + depth_prepass, + normal_prepass, + motion_vector_prepass, + deferred_prepass, + ) in cameras_3d.iter() { if !camera.is_active { continue; } - if depth_prepass || normal_prepass || motion_vector_prepass { - opaque_3d_prepass_phases.insert_or_clear(entity); - alpha_mask_3d_prepass_phases.insert_or_clear(entity); - } else { - opaque_3d_prepass_phases.remove(&entity); - alpha_mask_3d_prepass_phases.remove(&entity); - } - - if deferred_prepass { - opaque_3d_deferred_phases.insert_or_clear(entity); - alpha_mask_3d_deferred_phases.insert_or_clear(entity); - } else { - opaque_3d_deferred_phases.remove(&entity); - alpha_mask_3d_deferred_phases.remove(&entity); - } + // Attach the prepass phases to each of the omnidirectional subcameras + // if applicable. + let view_entities = match omnidirectional_cameras.get(&camera_entity) { + Some(view_entities) => view_entities.clone(), + None => iter::once(camera_entity).collect(), + }; - live_entities.insert(entity); + for entity in view_entities { + if depth_prepass || normal_prepass || motion_vector_prepass { + opaque_3d_prepass_phases.insert_or_clear(entity); + alpha_mask_3d_prepass_phases.insert_or_clear(entity); + } else { + opaque_3d_prepass_phases.remove(&entity); + alpha_mask_3d_prepass_phases.remove(&entity); + } - let mut entity = commands.get_or_spawn(entity); + if deferred_prepass { + opaque_3d_deferred_phases.insert_or_clear(entity); + alpha_mask_3d_deferred_phases.insert_or_clear(entity); + } else { + opaque_3d_deferred_phases.remove(&entity); + alpha_mask_3d_deferred_phases.remove(&entity); + } - if depth_prepass { - entity.insert(DepthPrepass); - } - if normal_prepass { - entity.insert(NormalPrepass); - } - if motion_vector_prepass { - entity.insert(MotionVectorPrepass); - } - if deferred_prepass { - entity.insert(DeferredPrepass); + live_entities.insert(entity); } } diff --git a/crates/bevy_core_pipeline/src/prepass/mod.rs b/crates/bevy_core_pipeline/src/prepass/mod.rs index 861bba12b6dc8..005471da65946 100644 --- a/crates/bevy_core_pipeline/src/prepass/mod.rs +++ b/crates/bevy_core_pipeline/src/prepass/mod.rs @@ -34,6 +34,7 @@ use bevy_ecs::prelude::*; use bevy_math::Mat4; use bevy_reflect::Reflect; use bevy_render::{ + extract_component::ExtractComponent, mesh::Mesh, render_phase::{ BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItem, @@ -52,21 +53,21 @@ pub const NORMAL_PREPASS_FORMAT: TextureFormat = TextureFormat::Rgb10a2Unorm; pub const MOTION_VECTOR_PREPASS_FORMAT: TextureFormat = TextureFormat::Rg16Float; /// If added to a [`crate::prelude::Camera3d`] then depth values will be copied to a separate texture available to the main pass. -#[derive(Component, Default, Reflect, Clone)] +#[derive(Component, Default, Reflect, Clone, ExtractComponent)] pub struct DepthPrepass; /// If added to a [`crate::prelude::Camera3d`] then vertex world normals will be copied to a separate texture available to the main pass. /// Normals will have normal map textures already applied. -#[derive(Component, Default, Reflect, Clone)] +#[derive(Component, Default, Reflect, Clone, ExtractComponent)] pub struct NormalPrepass; /// If added to a [`crate::prelude::Camera3d`] then screen space motion vectors will be copied to a separate texture available to the main pass. -#[derive(Component, Default, Reflect, Clone)] +#[derive(Component, Default, Reflect, Clone, ExtractComponent)] pub struct MotionVectorPrepass; /// If added to a [`crate::prelude::Camera3d`] then deferred materials will be rendered to the deferred gbuffer texture and will be available to subsequent passes. /// Note the default deferred lighting plugin also requires `DepthPrepass` to work correctly. -#[derive(Component, Default, Reflect)] +#[derive(Component, Default, Reflect, Clone, ExtractComponent)] pub struct DeferredPrepass; #[derive(Component, ShaderType, Clone)] diff --git a/crates/bevy_core_pipeline/src/skybox/mod.rs b/crates/bevy_core_pipeline/src/skybox/mod.rs index 4ff5eccc51d65..1c6160d9a5450 100644 --- a/crates/bevy_core_pipeline/src/skybox/mod.rs +++ b/crates/bevy_core_pipeline/src/skybox/mod.rs @@ -9,8 +9,7 @@ use bevy_ecs::{ use bevy_render::{ camera::Exposure, extract_component::{ - ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin, - UniformComponentPlugin, + ComponentUniforms, DynamicUniformIndex, ExtractComponent, UniformComponentPlugin, }, render_asset::RenderAssets, render_resource::{ @@ -24,7 +23,7 @@ use bevy_render::{ }; use prepass::{SkyboxPrepassPipeline, SKYBOX_PREPASS_SHADER_HANDLE}; -use crate::core_3d::CORE_3D_DEPTH_FORMAT; +use crate::core_3d::{ExtractCameraComponentPlugin, CORE_3D_DEPTH_FORMAT}; const SKYBOX_SHADER_HANDLE: Handle = Handle::weak_from_u128(55594763423201); @@ -43,7 +42,7 @@ impl Plugin for SkyboxPlugin { ); app.add_plugins(( - ExtractComponentPlugin::::default(), + ExtractCameraComponentPlugin::::default(), UniformComponentPlugin::::default(), )); diff --git a/crates/bevy_pbr/src/bundle.rs b/crates/bevy_pbr/src/bundle.rs index 3a6dec734fb72..36ff3adfa3da6 100644 --- a/crates/bevy_pbr/src/bundle.rs +++ b/crates/bevy_pbr/src/bundle.rs @@ -6,6 +6,7 @@ use bevy_asset::Handle; use bevy_ecs::entity::EntityHashMap; use bevy_ecs::{bundle::Bundle, component::Component, reflect::ReflectComponent}; use bevy_reflect::Reflect; +use bevy_render::view::CubemapVisibleEntities; use bevy_render::{ mesh::Mesh, primitives::{CascadesFrusta, CubemapFrusta, Frustum}, @@ -45,31 +46,6 @@ impl Default for MaterialMeshBundle { } } -#[derive(Component, Clone, Debug, Default, Reflect)] -#[reflect(Component)] -pub struct CubemapVisibleEntities { - #[reflect(ignore)] - data: [VisibleEntities; 6], -} - -impl CubemapVisibleEntities { - pub fn get(&self, i: usize) -> &VisibleEntities { - &self.data[i] - } - - pub fn get_mut(&mut self, i: usize) -> &mut VisibleEntities { - &mut self.data[i] - } - - pub fn iter(&self) -> impl DoubleEndedIterator { - self.data.iter() - } - - pub fn iter_mut(&mut self) -> impl DoubleEndedIterator { - self.data.iter_mut() - } -} - #[derive(Component, Clone, Debug, Default, Reflect)] #[reflect(Component)] pub struct CascadesVisibleEntities { diff --git a/crates/bevy_pbr/src/cluster/mod.rs b/crates/bevy_pbr/src/cluster/mod.rs index a0d628474db8e..6016314a9c146 100644 --- a/crates/bevy_pbr/src/cluster/mod.rs +++ b/crates/bevy_pbr/src/cluster/mod.rs @@ -5,21 +5,21 @@ use std::num::NonZeroU64; use bevy_ecs::{ component::Component, entity::{Entity, EntityHashMap}, - query::Without, + query::{QueryItem, Without}, reflect::ReflectComponent, - system::{Commands, Query, Res, Resource}, + system::{lifetimeless::Read, Commands, Query, Res, Resource}, world::{FromWorld, World}, }; use bevy_math::{AspectRatio, UVec2, UVec3, UVec4, Vec3Swizzles as _, Vec4}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ camera::Camera, + extract_component::ExtractComponent, render_resource::{ BindingResource, BufferBindingType, ShaderSize as _, ShaderType, StorageBuffer, UniformBuffer, }, renderer::{RenderDevice, RenderQueue}, - Extract, }; use bevy_utils::{hashbrown::HashSet, tracing::warn}; @@ -169,7 +169,7 @@ pub struct GpuClusterableObjectsStorage { data: Vec, } -#[derive(Component)] +#[derive(Clone, Copy, Component)] pub struct ExtractedClusterConfig { /// Special near value for cluster calculations pub(crate) near: f32, @@ -178,12 +178,13 @@ pub struct ExtractedClusterConfig { pub(crate) dimensions: UVec3, } +#[derive(Clone, Copy)] enum ExtractedClusterableObjectElement { ClusterHeader(u32, u32), ClusterableObjectEntity(Entity), } -#[derive(Component)] +#[derive(Component, Clone)] pub struct ExtractedClusterableObjects { data: Vec, } @@ -508,14 +509,16 @@ pub(crate) fn clusterable_object_order( .then_with(|| entity_1.cmp(entity_2)) // stable } -/// Extracts clusters from the main world from the render world. -pub fn extract_clusters( - mut commands: Commands, - views: Extract>, -) { - for (entity, clusters, camera) in &views { +impl ExtractComponent for Clusters { + type QueryData = (Read, Read); + + type QueryFilter = (); + + type Out = (ExtractedClusterableObjects, ExtractedClusterConfig); + + fn extract_component((clusters, camera): QueryItem<'_, Self::QueryData>) -> Option { if !camera.is_active { - continue; + return None; } let num_entities: usize = clusters @@ -536,14 +539,14 @@ pub fn extract_clusters( } } - commands.get_or_spawn(entity).insert(( + Some(( ExtractedClusterableObjects { data }, ExtractedClusterConfig { near: clusters.near, far: clusters.far, dimensions: clusters.dimensions, }, - )); + )) } } diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index a6c94badf477d..6bf2866065a5c 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -100,7 +100,10 @@ pub mod graph { use crate::{deferred::DeferredPbrLightingPlugin, graph::NodePbr}; use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, AssetApp, Assets, Handle}; -use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d}; +use bevy_core_pipeline::core_3d::{ + graph::{Core3d, Node3d}, + ExtractCameraComponentPlugin, +}; use bevy_ecs::prelude::*; use bevy_render::{ alpha::AlphaMode, @@ -288,7 +291,6 @@ impl Plugin for PbrPlugin { .register_type::() .register_type::() .register_type::() - .register_type::() .register_type::() .register_type::() .register_type::() @@ -328,6 +330,7 @@ impl Plugin for PbrPlugin { VolumetricFogPlugin, ScreenSpaceReflectionsPlugin, )) + .add_plugins(ExtractCameraComponentPlugin::::default()) .configure_sets( PostUpdate, ( @@ -401,7 +404,7 @@ impl Plugin for PbrPlugin { // Extract the required data from the main world render_app - .add_systems(ExtractSchedule, (extract_clusters, extract_lights)) + .add_systems(ExtractSchedule, extract_lights) .add_systems( Render, ( diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index 4273de71d5cfe..77598acba9ce7 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -3,17 +3,17 @@ use bevy_ecs::prelude::*; use bevy_math::{Mat4, Vec3A, Vec4}; use bevy_reflect::prelude::*; use bevy_render::{ - camera::{Camera, CameraProjection}, + camera::{Camera, CameraProjection, CubemapFaceProjections}, extract_component::ExtractComponent, extract_resource::ExtractResource, mesh::Mesh, primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, Sphere}, view::{ - InheritedVisibility, RenderLayers, ViewVisibility, VisibilityRange, VisibleEntities, - VisibleEntityRanges, WithMesh, + visibility, CubemapVisibleEntities, InheritedVisibility, RenderLayers, ViewVisibility, + VisibilityRange, VisibleEntities, VisibleEntityRanges, WithMesh, }, }; -use bevy_transform::components::{GlobalTransform, Transform}; +use bevy_transform::components::GlobalTransform; use crate::*; @@ -567,12 +567,7 @@ pub fn update_point_light_frusta( Or<(Changed, Changed)>, >, ) { - let clip_from_view = - Mat4::perspective_infinite_reverse_rh(std::f32::consts::FRAC_PI_2, 1.0, POINT_LIGHT_NEAR_Z); - let view_rotations = CUBE_MAP_FACES - .iter() - .map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up)) - .collect::>(); + let cubemap_face_projections = CubemapFaceProjections::new(POINT_LIGHT_NEAR_Z); for (entity, transform, point_light, mut cubemap_frusta) in &mut views { // The frusta are used for culling meshes to the light for shadow mapping @@ -584,23 +579,12 @@ pub fn update_point_light_frusta( continue; } - // ignore scale because we don't want to effectively scale light radius and range - // by applying those as a view transform to shadow map rendering of objects - // and ignore rotation because we want the shadow map projections to align with the axes - let view_translation = Transform::from_translation(transform.translation()); - let view_backward = transform.back(); - - for (view_rotation, frustum) in view_rotations.iter().zip(cubemap_frusta.iter_mut()) { - let world_from_view = view_translation * *view_rotation; - let clip_from_world = clip_from_view * world_from_view.compute_matrix().inverse(); - - *frustum = Frustum::from_clip_from_world_custom_far( - &clip_from_world, - &transform.translation(), - &view_backward, - point_light.range, - ); - } + visibility::update_cubemap_frusta( + transform, + &mut cubemap_frusta, + &cubemap_face_projections, + point_light.range, + ); } } @@ -682,22 +666,6 @@ pub fn check_light_mesh_visibility( >, visible_entity_ranges: Option>, ) { - fn shrink_entities(visible_entities: &mut VisibleEntities) { - // Check that visible entities capacity() is no more than two times greater than len() - let capacity = visible_entities.entities.capacity(); - let reserved = capacity - .checked_div(visible_entities.entities.len()) - .map_or(0, |reserve| { - if reserve > 2 { - capacity / (reserve / 2) - } else { - capacity - } - }); - - visible_entities.entities.shrink_to(reserved); - } - let visible_entity_ranges = visible_entity_ranges.as_deref(); // Directional lights @@ -798,7 +766,9 @@ pub fn check_light_mesh_visibility( } for (_, cascade_view_entities) in &mut visible_entities.entities { - cascade_view_entities.iter_mut().for_each(shrink_entities); + cascade_view_entities + .iter_mut() + .for_each(|entities| entities.shrink()); } } @@ -813,77 +783,22 @@ pub fn check_light_mesh_visibility( maybe_view_mask, )) = point_lights.get_mut(light_entity) { - for visible_entities in cubemap_visible_entities.iter_mut() { - visible_entities.entities.clear(); - } + cubemap_visible_entities.clear(); // NOTE: If shadow mapping is disabled for the light then it must have no visible entities if !point_light.shadows_enabled { continue; } - let view_mask = maybe_view_mask.unwrap_or_default(); - let light_sphere = Sphere { - center: Vec3A::from(transform.translation()), - radius: point_light.range, - }; - - for ( - entity, - inherited_visibility, - mut view_visibility, - maybe_entity_mask, - maybe_aabb, - maybe_transform, - has_visibility_range, - ) in &mut visible_entity_query - { - if !inherited_visibility.get() { - continue; - } - - let entity_mask = maybe_entity_mask.unwrap_or_default(); - if !view_mask.intersects(entity_mask) { - continue; - } - - // Check visibility ranges. - if has_visibility_range - && visible_entity_ranges.is_some_and(|visible_entity_ranges| { - !visible_entity_ranges.entity_is_in_range_of_any_view(entity) - }) - { - continue; - } - - // If we have an aabb and transform, do frustum culling - if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) { - let model_to_world = transform.affine(); - // Do a cheap sphere vs obb test to prune out most meshes outside the sphere of the light - if !light_sphere.intersects_obb(aabb, &model_to_world) { - continue; - } - - for (frustum, visible_entities) in cubemap_frusta - .iter() - .zip(cubemap_visible_entities.iter_mut()) - { - if frustum.intersects_obb(aabb, &model_to_world, true, true) { - view_visibility.set(); - visible_entities.push::(entity); - } - } - } else { - view_visibility.set(); - for visible_entities in cubemap_visible_entities.iter_mut() { - visible_entities.push::(entity); - } - } - } - - for visible_entities in cubemap_visible_entities.iter_mut() { - shrink_entities(visible_entities); - } + visibility::check_cubemap_mesh_visibility( + &mut visible_entity_query, + &mut cubemap_visible_entities, + cubemap_frusta, + transform, + maybe_view_mask.unwrap_or_default(), + point_light.range, + visible_entity_ranges, + ); } // Spot lights @@ -949,7 +864,7 @@ pub fn check_light_mesh_visibility( } } - shrink_entities(&mut visible_entities); + visible_entities.shrink(); } } } diff --git a/crates/bevy_pbr/src/light_probe/mod.rs b/crates/bevy_pbr/src/light_probe/mod.rs index 97607d34a74ab..52a64501d4f3e 100644 --- a/crates/bevy_pbr/src/light_probe/mod.rs +++ b/crates/bevy_pbr/src/light_probe/mod.rs @@ -2,12 +2,15 @@ use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, AssetId, Handle}; -use bevy_core_pipeline::core_3d::Camera3d; +use bevy_core_pipeline::core_3d::{ + extract_omnidirectional_cameras, Camera3d, ExtractCameraInstancesPlugin, + RenderOmnidirectionalCameras, +}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ component::Component, entity::Entity, - query::With, + query::{AnyOf, With}, reflect::ReflectComponent, schedule::IntoSystemConfigs, system::{Commands, Local, Query, Res, ResMut, Resource}, @@ -15,14 +18,13 @@ use bevy_ecs::{ use bevy_math::{Affine3A, FloatOrd, Mat4, Vec3A, Vec4}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ - extract_instances::ExtractInstancesPlugin, - primitives::{Aabb, Frustum}, + primitives::{Aabb, CubemapFrusta, Frustum}, render_asset::RenderAssets, render_resource::{DynamicUniformBuffer, Sampler, Shader, ShaderType, TextureView}, renderer::{RenderDevice, RenderQueue}, settings::WgpuFeatures, texture::{FallbackImage, GpuImage, Image}, - view::ExtractedView, + view::{ExtractedView, RenderLayers}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_transform::prelude::GlobalTransform; @@ -196,6 +198,8 @@ where // of assets (e.g. a reflection probe references two cubemap assets while an // irradiance volume references a single 3D texture asset), this is generic. asset_id: C::AssetId, + + maybe_render_layers: Option, } /// A component, part of the render world, that stores the mapping from asset ID @@ -328,10 +332,16 @@ impl Plugin for LightProbePlugin { }; render_app - .add_plugins(ExtractInstancesPlugin::::new()) + .add_plugins(ExtractCameraInstancesPlugin::::new()) .init_resource::() - .add_systems(ExtractSchedule, gather_light_probes::) - .add_systems(ExtractSchedule, gather_light_probes::) + .add_systems( + ExtractSchedule, + gather_light_probes::.after(extract_omnidirectional_cameras), + ) + .add_systems( + ExtractSchedule, + gather_light_probes::.after(extract_omnidirectional_cameras), + ) .add_systems( Render, upload_light_probes.in_set(RenderSet::PrepareResources), @@ -343,8 +353,22 @@ impl Plugin for LightProbePlugin { /// to views, performing frustum culling and distance sorting in the process. fn gather_light_probes( image_assets: Res>, - light_probe_query: Extract>>, - view_query: Extract), With>>, + omnidirectional_cameras: Res, + light_probe_query: Extract< + Query<(&GlobalTransform, &C, Option<&RenderLayers>), With>, + >, + view_query: Extract< + Query< + ( + Entity, + &GlobalTransform, + AnyOf<(&Frustum, &CubemapFrusta)>, + Option<&RenderLayers>, + Option<&C>, + ), + With, + >, + >, mut reflection_probes: Local>>, mut view_reflection_probes: Local>>, mut commands: Commands, @@ -360,38 +384,114 @@ fn gather_light_probes( ); // Build up the light probes uniform and the key table. - for (view_entity, view_transform, view_frustum, view_component) in view_query.iter() { - // Cull light probes outside the view frustum. - view_reflection_probes.clear(); - view_reflection_probes.extend( - reflection_probes - .iter() - .filter(|light_probe_info| light_probe_info.frustum_cull(view_frustum)) - .cloned(), - ); + for ( + view_entity, + view_transform, + (frustum, cubemap_frusta), + maybe_render_layers, + view_component, + ) in view_query.iter() + { + if let Some(frustum) = frustum { + gather_light_probes_for_camera( + view_entity, + view_transform, + frustum, + maybe_render_layers, + view_component, + &mut commands, + &mut reflection_probes, + &mut *view_reflection_probes, + &image_assets, + ); + } + + // Create the light probes for each face sub-camera of an + // omnidirectional camera, if necessary. + + let (Some(cubemap_frusta), Some(omnidirectional_camera_entities)) = + (cubemap_frusta, omnidirectional_cameras.get(&view_entity)) + else { + continue; + }; + + for (view_entity, frustum) in omnidirectional_camera_entities + .iter() + .zip(cubemap_frusta.iter()) + { + gather_light_probes_for_camera( + *view_entity, + view_transform, + frustum, + maybe_render_layers, + view_component, + &mut commands, + &mut reflection_probes, + &mut *view_reflection_probes, + &image_assets, + ); + } + } +} + +/// Extracts light probes from the main world for a single camera. +/// +/// [`gather_light_probes`] calls this both for standard 3D cameras and for the +/// individual sub-cameras of each omnidirectional 3D camera. +#[allow(clippy::too_many_arguments)] +fn gather_light_probes_for_camera( + view_entity: Entity, + view_transform: &GlobalTransform, + view_frustum: &Frustum, + maybe_view_render_layers: Option<&RenderLayers>, + view_component: Option<&C>, + commands: &mut Commands, + reflection_probes: &mut [LightProbeInfo], + view_reflection_probes: &mut Vec>, + image_assets: &RenderAssets, +) where + C: LightProbeComponent, +{ + // Cull light probes outside the view frustum. + view_reflection_probes.clear(); + view_reflection_probes.extend(reflection_probes.iter().filter_map(|light_probe_info| { + if !light_probe_info.frustum_cull(view_frustum) { + return None; + } - // Sort by distance to camera. - view_reflection_probes.sort_by_cached_key(|light_probe_info| { - light_probe_info.camera_distance_sort_key(view_transform) - }); - - // Create the light probes list. - let mut render_view_light_probes = - C::create_render_view_light_probes(view_component, &image_assets); - - // Gather up the light probes in the list. - render_view_light_probes.maybe_gather_light_probes(&view_reflection_probes); - - // Record the per-view light probes. - if render_view_light_probes.is_empty() { - commands - .get_or_spawn(view_entity) - .remove::>(); - } else { - commands - .get_or_spawn(view_entity) - .insert(render_view_light_probes); + if let (Some(view_render_layers), Some(light_probe_render_layers)) = ( + maybe_view_render_layers, + light_probe_info.maybe_render_layers.as_ref(), + ) { + if !view_render_layers.intersects(light_probe_render_layers) { + return None; + } } + + Some((*light_probe_info).clone()) + })); + + // Sort by distance to camera. + view_reflection_probes.sort_by_cached_key(|light_probe_info| { + light_probe_info.camera_distance_sort_key(view_transform) + }); + + // Create the light probes list. + let mut render_view_light_probes = + C::create_render_view_light_probes(view_component, image_assets); + + // Gather up the light probes in the list. + render_view_light_probes.maybe_gather_light_probes(view_reflection_probes); + + // Record the per-view light probes. + if render_view_light_probes.is_empty() { + commands + .get_or_spawn(view_entity) + .remove::>(); + } else { + commands + .get_or_spawn(view_entity) + .insert(render_view_light_probes); } } @@ -504,7 +604,11 @@ where /// [`LightProbeInfo`]. This is done for every light probe in the scene /// every frame. fn new( - (light_probe_transform, environment_map): (&GlobalTransform, &C), + (light_probe_transform, environment_map, maybe_render_layers): ( + &GlobalTransform, + &C, + Option<&RenderLayers>, + ), image_assets: &RenderAssets, ) -> Option> { environment_map.id(image_assets).map(|id| LightProbeInfo { @@ -512,6 +616,7 @@ where light_from_world: light_probe_transform.compute_matrix().inverse(), asset_id: id, intensity: environment_map.intensity(), + maybe_render_layers: maybe_render_layers.cloned(), }) } @@ -624,6 +729,7 @@ where world_from_light: self.world_from_light, intensity: self.intensity, asset_id: self.asset_id.clone(), + maybe_render_layers: self.maybe_render_layers.clone(), } } } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 0f17c2cabc8f9..0b89e2717a8c9 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -5,7 +5,9 @@ use bevy_ecs::entity::EntityHashSet; use bevy_ecs::prelude::*; use bevy_ecs::{entity::EntityHashMap, system::lifetimeless::Read}; use bevy_math::{Mat4, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; +use bevy_render::camera::CubemapFaceProjections; use bevy_render::mesh::Mesh; +use bevy_render::view::CubemapVisibleEntities; use bevy_render::{ diagnostic::RecordDiagnostics, mesh::GpuMesh, @@ -19,7 +21,7 @@ use bevy_render::{ view::{ExtractedView, RenderLayers, ViewVisibility, VisibleEntities, WithMesh}, Extract, }; -use bevy_transform::{components::GlobalTransform, prelude::Transform}; +use bevy_transform::components::GlobalTransform; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; use bevy_utils::tracing::{error, warn}; @@ -359,51 +361,6 @@ pub fn extract_lights( pub(crate) const POINT_LIGHT_NEAR_Z: f32 = 0.1f32; -pub(crate) struct CubeMapFace { - pub(crate) target: Vec3, - pub(crate) up: Vec3, -} - -// Cubemap faces are [+X, -X, +Y, -Y, +Z, -Z], per https://www.w3.org/TR/webgpu/#texture-view-creation -// Note: Cubemap coordinates are left-handed y-up, unlike the rest of Bevy. -// See https://registry.khronos.org/vulkan/specs/1.2/html/chap16.html#_cube_map_face_selection -// -// For each cubemap face, we take care to specify the appropriate target/up axis such that the rendered -// texture using Bevy's right-handed y-up coordinate space matches the expected cubemap face in -// left-handed y-up cubemap coordinates. -pub(crate) const CUBE_MAP_FACES: [CubeMapFace; 6] = [ - // +X - CubeMapFace { - target: Vec3::X, - up: Vec3::Y, - }, - // -X - CubeMapFace { - target: Vec3::NEG_X, - up: Vec3::Y, - }, - // +Y - CubeMapFace { - target: Vec3::Y, - up: Vec3::Z, - }, - // -Y - CubeMapFace { - target: Vec3::NEG_Y, - up: Vec3::NEG_Z, - }, - // +Z (with left-handed conventions, pointing forwards) - CubeMapFace { - target: Vec3::NEG_Z, - up: Vec3::Y, - }, - // -Z (with left-handed conventions, pointing backwards) - CubeMapFace { - target: Vec3::Z, - up: Vec3::Y, - }, -]; - fn face_index_to_name(face_index: usize) -> &'static str { match face_index { 0 => "+x", @@ -547,12 +504,10 @@ pub fn prepare_lights( }; // Pre-calculate for PointLights - let cube_face_projection = - Mat4::perspective_infinite_reverse_rh(std::f32::consts::FRAC_PI_2, 1.0, POINT_LIGHT_NEAR_Z); - let cube_face_rotations = CUBE_MAP_FACES - .iter() - .map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up)) - .collect::>(); + let CubemapFaceProjections { + projection: ref cube_face_projection, + rotations: ref cube_face_rotations, + } = CubemapFaceProjections::new(POINT_LIGHT_NEAR_Z); global_light_meta.entity_to_index.clear(); @@ -914,7 +869,7 @@ pub fn prepare_lights( ), world_from_view: view_translation * *view_rotation, clip_from_world: None, - clip_from_view: cube_face_projection, + clip_from_view: *cube_face_projection, hdr: false, color_grading: Default::default(), }, diff --git a/crates/bevy_render/src/batching/gpu_preprocessing.rs b/crates/bevy_render/src/batching/gpu_preprocessing.rs index 62cb9d8c89eb4..5584fabd37203 100644 --- a/crates/bevy_render/src/batching/gpu_preprocessing.rs +++ b/crates/bevy_render/src/batching/gpu_preprocessing.rs @@ -6,11 +6,11 @@ use bevy_ecs::{ entity::Entity, query::{Has, With}, schedule::IntoSystemConfigs as _, - system::{Query, Res, ResMut, Resource, StaticSystemParam}, + system::{EntityCommands, Query, Res, ResMut, Resource, StaticSystemParam}, world::{FromWorld, World}, }; use bevy_encase_derive::ShaderType; -use bevy_utils::EntityHashMap; +use bevy_utils::{warn_once, EntityHashMap}; use bytemuck::{Pod, Zeroable}; use nonmax::NonMaxU32; use smallvec::smallvec; @@ -71,6 +71,18 @@ pub enum GpuPreprocessingSupport { Culling, } +impl GpuPreprocessingSupport { + /// If GPU culling is requested, adds the [`GpuCulling`] component to the + /// given entity. + pub fn maybe_add_gpu_culling(&self, entity_commands: &mut EntityCommands) { + if *self == GpuPreprocessingSupport::Culling { + entity_commands.insert(GpuCulling); + } else { + warn_once!("GPU culling isn't supported on this platform; ignoring `GpuCulling`."); + } + } +} + /// The GPU buffers holding the data needed to render batches. /// /// For example, in the 3D PBR pipeline this holds `MeshUniform`s, which are the diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 97b94e2762180..84fa30778b777 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -28,14 +28,18 @@ use bevy_math::{vec2, Dir3, Mat4, Ray3d, Rect, URect, UVec2, UVec4, Vec2, Vec3}; use bevy_reflect::prelude::*; use bevy_render_macros::ExtractComponent; use bevy_transform::components::GlobalTransform; -use bevy_utils::{tracing::warn, warn_once}; +use bevy_utils::tracing::warn; use bevy_utils::{HashMap, HashSet}; use bevy_window::{ NormalizedWindowRef, PrimaryWindow, Window, WindowCreated, WindowRef, WindowResized, WindowScaleFactorChanged, }; +use nonmax::NonMaxU32; use std::ops::Range; -use wgpu::{BlendState, TextureFormat, TextureUsages}; +use wgpu::{ + BlendState, TextureAspect, TextureFormat, TextureUsages, TextureViewDescriptor, + TextureViewDimension, +}; use super::{ClearColorConfig, Projection}; @@ -596,17 +600,34 @@ impl NormalizedRenderTarget { windows: &'a ExtractedWindows, images: &'a RenderAssets, manual_texture_views: &'a ManualTextureViews, - ) -> Option<&'a TextureView> { + render_target_layer: Option, + ) -> Option { match self { NormalizedRenderTarget::Window(window_ref) => windows .get(&window_ref.entity()) - .and_then(|window| window.swap_chain_texture_view.as_ref()), + .and_then(|window| window.swap_chain_texture_view.clone()), NormalizedRenderTarget::Image(image_handle) => { - images.get(image_handle).map(|image| &image.texture_view) - } - NormalizedRenderTarget::TextureView(id) => { - manual_texture_views.get(id).map(|tex| &tex.texture_view) + images + .get(image_handle) + .map(|image| match render_target_layer { + None => image.texture_view.clone(), + Some(base_array_layer) => { + image.texture.create_view(&TextureViewDescriptor { + label: None, + format: Some(image.texture_format), + dimension: Some(TextureViewDimension::D2), + aspect: TextureAspect::All, + base_mip_level: 0, + mip_level_count: Some(1), + base_array_layer: base_array_layer.into(), + array_layer_count: Some(1), + }) + } + }) } + NormalizedRenderTarget::TextureView(id) => manual_texture_views + .get(id) + .map(|tex| tex.texture_view.clone()), } } @@ -823,6 +844,7 @@ pub struct ExtractedCamera { pub clear_color: ClearColorConfig, pub sorted_camera_index_for_target: usize, pub exposure: f32, + pub render_target_layer: Option, pub hdr: bool, } @@ -903,6 +925,7 @@ pub fn extract_cameras( exposure: exposure .map(|e| e.exposure()) .unwrap_or_else(|| Exposure::default().exposure()), + render_target_layer: None, hdr: camera.hdr, }, ExtractedView { @@ -935,13 +958,7 @@ pub fn extract_cameras( } if gpu_culling { - if *gpu_preprocessing_support == GpuPreprocessingSupport::Culling { - commands.insert(GpuCulling); - } else { - warn_once!( - "GPU culling isn't supported on this platform; ignoring `GpuCulling`." - ); - } + gpu_preprocessing_support.maybe_add_gpu_culling(&mut commands); } } } @@ -956,6 +973,7 @@ pub struct SortedCamera { pub order: isize, pub target: Option, pub hdr: bool, + pub render_target_layer: Option, } pub fn sort_cameras( @@ -969,6 +987,7 @@ pub fn sort_cameras( order: camera.order, target: camera.target.clone(), hdr: camera.hdr, + render_target_layer: camera.render_target_layer, }); } // sort by order and ensure within an order, RenderTargets of the same type are packed together @@ -982,7 +1001,11 @@ pub fn sort_cameras( let mut ambiguities = HashSet::new(); let mut target_counts = HashMap::new(); for sorted_camera in &mut sorted_cameras.0 { - let new_order_target = (sorted_camera.order, sorted_camera.target.clone()); + let new_order_target = ( + sorted_camera.order, + sorted_camera.target.clone(), + sorted_camera.render_target_layer, + ); if let Some(previous_order_target) = previous_order_target { if previous_order_target == new_order_target { ambiguities.insert(new_order_target.clone()); diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_render/src/camera/projection.rs index 0a6c3ca00afab..797abdc6b224d 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_render/src/camera/projection.rs @@ -1,3 +1,5 @@ +use std::array; +use std::f32::consts::FRAC_PI_2; use std::marker::PhantomData; use std::ops::{Div, DivAssign, Mul, MulAssign}; @@ -5,11 +7,11 @@ use crate::primitives::Frustum; use crate::view::VisibilitySystems; use bevy_app::{App, Plugin, PostStartup, PostUpdate}; use bevy_ecs::prelude::*; -use bevy_math::{AspectRatio, Mat4, Rect, Vec2, Vec3A}; +use bevy_math::{AspectRatio, Mat4, Rect, Vec2, Vec3, Vec3A}; use bevy_reflect::{ std_traits::ReflectDefault, GetTypeRegistration, Reflect, ReflectDeserialize, ReflectSerialize, }; -use bevy_transform::components::GlobalTransform; +use bevy_transform::components::{GlobalTransform, Transform}; use bevy_transform::TransformSystem; use serde::{Deserialize, Serialize}; @@ -64,6 +66,43 @@ impl Default for CameraPr #[derive(SystemSet, Clone, Eq, PartialEq, Hash, Debug)] pub struct CameraUpdateSystem; +/// The target vector and the X vector for each cubemap face, respectively. +/// +/// Cubemap faces are [+X, -X, +Y, -Y, +Z, -Z], per +/// Note: Cubemap +/// coordinates are left-handed y-up, unlike the rest of Bevy. See +/// +/// +/// For each cubemap face, we take care to specify the appropriate target/up +/// axis such that the rendered texture using Bevy's right-handed y-up +/// coordinate space matches the expected cubemap face in left-handed y-up +/// cubemap coordinates. +pub static CUBEMAP_FACES: [(Vec3, Vec3); 6] = [ + (Vec3::X, Vec3::Y), // +X + (Vec3::NEG_X, Vec3::Y), // -X + (Vec3::Y, Vec3::Z), // +Y + (Vec3::NEG_Y, Vec3::NEG_Z), // -Y + (Vec3::NEG_Z, Vec3::Y), // +Z (left-handed, pointing forward) + (Vec3::Z, Vec3::Y), // -Z (left-handed, pointing backward) +]; + +pub struct CubemapFaceProjections { + pub rotations: [Transform; 6], + pub projection: Mat4, +} + +impl CubemapFaceProjections { + pub fn new(near_z: f32) -> Self { + CubemapFaceProjections { + rotations: array::from_fn(|i| { + let (target, up) = CUBEMAP_FACES[i]; + Transform::IDENTITY.looking_at(target, up) + }), + projection: Mat4::perspective_infinite_reverse_rh(FRAC_PI_2, 1.0, near_z), + } + } +} + /// Trait to control the projection matrix of a camera. /// /// Components implementing this trait are automatically polled for changes, and used @@ -227,6 +266,34 @@ impl Default for PerspectiveProjection { } } +/// A set of 3D perspective projections that render to all six faces of a cube. +#[derive(Component, Reflect)] +#[reflect(Component)] +pub struct OmnidirectionalProjection { + /// The distance from the camera in world units of the viewing frustum's near plane. + /// + /// Objects closer to the camera than this value will not be visible. + /// + /// Defaults to a value of `0.1`. + pub near: f32, + + /// The distance from the camera in world units of the viewing frustum's far plane. + /// + /// Objects farther from the camera than this value will not be visible. + /// + /// Defaults to a value of `1000.0`. + pub far: f32, +} + +impl Default for OmnidirectionalProjection { + fn default() -> Self { + OmnidirectionalProjection { + near: 0.1, + far: 1000.0, + } + } +} + /// Scaling mode for [`OrthographicProjection`]. /// /// # Examples diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 17f626d410c5e..a3c632a40aa20 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -2,13 +2,14 @@ pub mod visibility; pub mod window; use bevy_asset::{load_internal_asset, Handle}; +use nonmax::NonMaxU32; pub use visibility::*; pub use window::*; use crate::{ camera::{ CameraMainTextureUsages, ClearColor, ClearColorConfig, Exposure, ExtractedCamera, - ManualTextureViews, MipBias, TemporalJitter, + ManualTextureViews, MipBias, NormalizedRenderTarget, TemporalJitter, }, extract_resource::{ExtractResource, ExtractResourcePlugin}, prelude::Shader, @@ -106,6 +107,7 @@ impl Plugin for ViewPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() .register_type::() .init_resource::() // NOTE: windows.is_changed() handles cases where a window was resized @@ -792,6 +794,27 @@ struct MainTargetTextures { main_texture: Arc, } +/// A helper structure used by [`prepare_view_targets`]. +/// +/// Each unique view target output texture key gets one output texture. +#[derive(Clone, PartialEq, Eq, Hash)] +struct ViewTargetOutputTextureKey { + target: NormalizedRenderTarget, + render_target_layer: Option, +} + +/// A helper structure used by [`prepare_view_targets`]. +/// +/// Each unique view target output texture key gets one set of main textures. +/// *Main textures* are used as intermediate targets for rendering, before +/// postprocessing and tonemapping resolves to the final output. +#[derive(Clone, PartialEq, Eq, Hash)] +struct ViewTargetTextureKey { + target: Option, + render_target_layer: Option, + hdr: bool, +} + #[allow(clippy::too_many_arguments)] pub fn prepare_view_targets( mut commands: Commands, @@ -817,14 +840,25 @@ pub fn prepare_view_targets( continue; }; - let Some(out_texture) = output_textures.entry(target.clone()).or_insert_with(|| { - target - .get_texture_view(&windows, &images, &manual_texture_views) - .zip(target.get_texture_format(&windows, &images, &manual_texture_views)) - .map(|(view, format)| { - OutputColorAttachment::new(view.clone(), format.add_srgb_suffix()) - }) - }) else { + let Some(out_texture) = output_textures + .entry(ViewTargetOutputTextureKey { + target: target.clone(), + render_target_layer: camera.render_target_layer, + }) + .or_insert_with(|| { + target + .get_texture_view( + &windows, + &images, + &manual_texture_views, + camera.render_target_layer, + ) + .zip(target.get_texture_format(&windows, &images, &manual_texture_views)) + .map(|(view, format)| { + OutputColorAttachment::new(view.clone(), format.add_srgb_suffix()) + }) + }) + else { continue; }; @@ -847,7 +881,11 @@ pub fn prepare_view_targets( }; let (a, b, sampled, main_texture) = textures - .entry((camera.target.clone(), view.hdr)) + .entry(ViewTargetTextureKey { + target: camera.target.clone(), + render_target_layer: camera.render_target_layer, + hdr: view.hdr, + }) .or_insert_with(|| { let descriptor = TextureDescriptor { label: None, diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index 893f3abc7764f..faf8f8b564659 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -3,6 +3,7 @@ mod render_layers; use std::any::TypeId; +use bevy_math::Vec3A; pub use range::*; pub use render_layers::*; @@ -12,13 +13,16 @@ use bevy_derive::Deref; use bevy_ecs::{prelude::*, query::QueryFilter}; use bevy_hierarchy::{Children, Parent}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_transform::{components::GlobalTransform, TransformSystem}; +use bevy_transform::{ + components::{GlobalTransform, Transform}, + TransformSystem, +}; use bevy_utils::{Parallel, TypeIdMap}; use crate::{ - camera::{Camera, CameraProjection}, + camera::{Camera, CameraProjection, CubemapFaceProjections, OmnidirectionalProjection}, mesh::Mesh, - primitives::{Aabb, Frustum, Sphere}, + primitives::{Aabb, CubemapFrusta, Frustum, Sphere}, }; use super::NoCpuCulling; @@ -228,6 +232,64 @@ impl VisibleEntities { { self.get_mut::().push(entity); } + + pub fn shrink(&mut self) { + // Check that visible entities capacity() is no more than two times greater than len() + let capacity = self.entities.capacity(); + let reserved = capacity + .checked_div(self.entities.len()) + .map_or(0, |reserve| { + if reserve > 2 { + capacity / (reserve / 2) + } else { + capacity + } + }); + + self.entities.shrink_to(reserved); + } +} + +/// A component that stores which entities are visible from each face of a +/// cubemap. +#[derive(Component, Clone, Debug, Default, Reflect)] +#[reflect(Component)] +pub struct CubemapVisibleEntities { + #[reflect(ignore)] + data: [VisibleEntities; 6], +} + +impl CubemapVisibleEntities { + /// Returns a reference to the list of visible entities from one face of the + /// cubemap. + pub fn get(&self, i: usize) -> &VisibleEntities { + &self.data[i] + } + + /// Returns a mutable reference to the list of visible entities from one + /// face of the cubemap. + pub fn get_mut(&mut self, i: usize) -> &mut VisibleEntities { + &mut self.data[i] + } + + /// Iterates over the visible entities of each cubemap face in turn. + pub fn iter(&self) -> impl DoubleEndedIterator { + self.data.iter() + } + + /// Iterates over the visible entities of each cubemap face in turn, + /// mutably. + pub fn iter_mut(&mut self) -> impl DoubleEndedIterator { + self.data.iter_mut() + } + + /// Clears out all entities from all faces of the cubemap in preparation for + /// a new frame. + pub fn clear(&mut self) { + for visible_entities in &mut self.data { + visible_entities.entities.clear(); + } + } } /// A convenient alias for `With>`, for use with @@ -267,6 +329,8 @@ impl Plugin for VisibilityPlugin { calculate_bounds.in_set(CalculateBounds), (visibility_propagate_system, reset_view_visibility).in_set(VisibilityPropagate), check_visibility::.in_set(CheckVisibility), + update_omnidirectional_camera_frusta.in_set(UpdateFrusta), + check_omnidirectional_camera_visibility.in_set(CheckVisibility), ), ); } @@ -494,6 +558,190 @@ pub fn check_visibility( } } +/// A system that checks visibility for every face of an omnidirectional camera. +/// +/// This has to be done separately from the normal [`check_visibility`] because +/// omnidirectional cameras have six individual sub-cameras, one for each face, +/// and each face needs to know the entities visible from that face. +pub fn check_omnidirectional_camera_visibility( + mut visible_entity_query: Query<( + Entity, + &InheritedVisibility, + &mut ViewVisibility, + Option<&RenderLayers>, + Option<&Aabb>, + Option<&GlobalTransform>, + Has, + )>, + mut omnidirectional_camera_query: Query<( + &mut CubemapVisibleEntities, + &CubemapFrusta, + &GlobalTransform, + &OmnidirectionalProjection, + Option<&RenderLayers>, + )>, + visible_entity_ranges: Option>, +) { + for ( + mut cubemap_visible_entities, + cubemap_frusta, + cubemap_transform, + cubemap_projection, + maybe_render_layers, + ) in omnidirectional_camera_query.iter_mut() + { + cubemap_visible_entities.clear(); + + check_cubemap_mesh_visibility( + &mut visible_entity_query, + &mut cubemap_visible_entities, + cubemap_frusta, + cubemap_transform, + maybe_render_layers.unwrap_or_default(), + cubemap_projection.far, + visible_entity_ranges.as_deref(), + ); + } +} + +/// A helper function that determines which entities are visible from one face +/// of a cubemap. +/// +/// This is used by [`check_omnidirectional_camera_visibility`] as well as the +/// point light shadow map logic. +pub fn check_cubemap_mesh_visibility( + visible_entity_query: &mut Query< + ( + Entity, + &InheritedVisibility, + &mut ViewVisibility, + Option<&RenderLayers>, + Option<&Aabb>, + Option<&GlobalTransform>, + Has, + ), + QF, + >, + cubemap_visible_entities: &mut CubemapVisibleEntities, + cubemap_frusta: &CubemapFrusta, + transform: &GlobalTransform, + view_mask: &RenderLayers, + range: f32, + visible_entity_ranges: Option<&VisibleEntityRanges>, +) where + QF: QueryFilter, +{ + let light_sphere = Sphere { + center: Vec3A::from(transform.translation()), + radius: range, + }; + + for ( + entity, + inherited_visibility, + mut view_visibility, + maybe_entity_mask, + maybe_aabb, + maybe_transform, + has_visibility_range, + ) in visible_entity_query + { + if !inherited_visibility.get() { + continue; + } + + let entity_mask = maybe_entity_mask.unwrap_or_default(); + if !view_mask.intersects(entity_mask) { + continue; + } + + // Check visibility ranges. + if has_visibility_range + && visible_entity_ranges.is_some_and(|visible_entity_ranges| { + !visible_entity_ranges.entity_is_in_range_of_any_view(entity) + }) + { + continue; + } + + // If we have an aabb and transform, do frustum culling + if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) { + let model_to_world = transform.affine(); + // Do a cheap sphere vs obb test to prune out most meshes outside the sphere of the light + if !light_sphere.intersects_obb(aabb, &model_to_world) { + continue; + } + + for (frustum, visible_entities) in cubemap_frusta + .iter() + .zip(cubemap_visible_entities.iter_mut()) + { + if frustum.intersects_obb(aabb, &model_to_world, true, true) { + view_visibility.set(); + visible_entities.push::(entity); + } + } + } else { + view_visibility.set(); + for visible_entities in cubemap_visible_entities.iter_mut() { + visible_entities.push::(entity); + } + } + } + + for visible_entities in cubemap_visible_entities.iter_mut() { + visible_entities.shrink(); + } +} + +pub fn update_omnidirectional_camera_frusta( + mut omnidirectional_camera_query: Query<( + &mut CubemapFrusta, + &GlobalTransform, + &OmnidirectionalProjection, + )>, +) { + for (mut cubemap_frusta, transform, projection) in omnidirectional_camera_query.iter_mut() { + let face_projections = CubemapFaceProjections::new(projection.near); + update_cubemap_frusta( + transform, + &mut cubemap_frusta, + &face_projections, + projection.far, + ); + } +} + +pub fn update_cubemap_frusta( + transform: &GlobalTransform, + cubemap_frusta: &mut CubemapFrusta, + cubemap_face_projections: &CubemapFaceProjections, + range: f32, +) { + let CubemapFaceProjections { + rotations: ref view_rotations, + projection: ref clip_from_view, + } = *cubemap_face_projections; + + // ignore scale because we don't want to effectively scale light radius and range + // by applying those as a view transform to shadow map rendering of objects + // and ignore rotation because we want the shadow map projections to align with the axes + let view_translation = Transform::from_translation(transform.translation()); + let view_backward = transform.back(); + + for (view_rotation, frustum) in view_rotations.iter().zip(cubemap_frusta.iter_mut()) { + let world_from_view = view_translation * *view_rotation; + let clip_from_world = *clip_from_view * world_from_view.compute_matrix().inverse(); + + *frustum = Frustum::from_clip_from_world_custom_far( + &clip_from_world, + &transform.translation(), + &view_backward, + range, + ); + } +} + #[cfg(test)] mod test { use bevy_app::prelude::*; diff --git a/examples/3d/reflection_probes.rs b/examples/3d/reflection_probes.rs index 3184982cb486f..72a9f4782ef77 100644 --- a/examples/3d/reflection_probes.rs +++ b/examples/3d/reflection_probes.rs @@ -1,35 +1,49 @@ //! 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. +//! Use the radio buttons to switch between no reflections, environment map +//! reflections (i.e. the skybox only, not the cubes), and static and dynamic +//! full reflections. Static reflections are "baked" ahead of time and +//! consequently rotating the cubes won't change the reflections. Dynamic +//! reflections are updated every frame and so rotating the cube will update +//! them. //! //! Reflection probes don't work on WebGL 2 or WebGPU. -use bevy::core_pipeline::Skybox; +use bevy::core_pipeline::core_3d::OmnidirectionalCamera3dBundle; +use bevy::core_pipeline::tonemapping::Tonemapping; use bevy::prelude::*; +use bevy::render::camera::RenderTarget; +use bevy::render::render_resource::{ + Extent3d, TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, + TextureViewDescriptor, TextureViewDimension, +}; +use bevy::render::view::RenderLayers; +use bevy::{core_pipeline::Skybox, ecs::system::EntityCommands}; use std::{ f32::consts::PI, fmt::{Display, Formatter, Result as FmtResult}, + marker::PhantomData, }; -static STOP_ROTATION_HELP_TEXT: &str = "Press Enter to stop rotation"; -static START_ROTATION_HELP_TEXT: &str = "Press Enter to start rotation"; +static FONT_PATH: &str = "fonts/FiraMono-Medium.ttf"; -static REFLECTION_MODE_HELP_TEXT: &str = "Press Space to switch reflection mode"; +const SKY_INTENSITY: f32 = 5000.0; +const ENVIRONMENT_MAP_INTENSITY: f32 = 1000.0; // The mode the application is in. -#[derive(Resource)] +#[derive(Resource, Default)] 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, + camera_rotating: CameraRotationMode, + // Whether the user has requested the cubes to rotate. + cubes_rotating: CubeRotationMode, } // Which environment maps the user has requested to display. -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Default, PartialEq)] enum ReflectionMode { // No environment maps are shown. None = 0, @@ -37,9 +51,57 @@ enum ReflectionMode { EnvironmentMap = 1, // Both a world environment map and a reflection probe are present. The // reflection probe is shown in the sphere. - ReflectionProbe = 2, + #[default] + StaticReflectionProbe = 2, + // Both a world environment map and a dynamic reflection probe, updated + // every frame, are present. The reflection probe is shown in the sphere. + DynamicReflectionProbe = 3, } +// Whether the user has requested the scene to rotate. +#[derive(Clone, Copy, Default, PartialEq)] +enum CameraRotationMode { + #[default] + Rotating, + Stationary, +} + +// Whether the user has requested the cubes to rotate. +#[derive(Clone, Copy, Default, PartialEq)] +enum CubeRotationMode { + #[default] + Stationary, + Rotating, +} + +// A marker component that we place on all radio `Button`s. +#[derive(Component, Deref, DerefMut)] +struct RadioButton(T); + +// A marker component that we place on all `Text` children of the radio buttons. +#[derive(Component, Deref, DerefMut)] +struct RadioButtonText(T); + +// An event that's sent whenever one of the radio buttons changes state. +#[derive(Event)] +struct RadioButtonChangeEvent(PhantomData); + +// A marker component for the main viewing camera that renders to the window. +#[derive(Component)] +struct MainCamera; + +// A marker component for the reflection camera that generates the reflection in +// the sphere. +#[derive(Component)] +struct ReflectionCamera; + +// Stores the original transform for each cube. +// +// We do this so that the cubes will snap back to their original positions when +// rotation is disabled. +#[derive(Component, Deref, DerefMut)] +struct OriginalTransform(Transform); + // The various reflection maps. #[derive(Resource)] struct Cubemaps { @@ -51,8 +113,15 @@ struct Cubemaps { // 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 static specular cubemap that reflects both the world and the cubes. + // + // This is baked ahead of time and consequently won't changes as the cubes + // rotate. + static_specular_reflection_probe: Handle, + + // The dynamic specular cubemap that reflects the world and the cubes, + // updated in real time. + dynamic_specular_reflection_probe: Handle, // The skybox cubemap image. This is almost the same as // `specular_environment_map`. @@ -62,20 +131,49 @@ struct Cubemaps { fn main() { // Create the app. App::new() - .add_plugins(DefaultPlugins) + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "Bevy Reflection Probes Example".into(), + ..default() + }), + ..default() + })) .init_resource::() .init_resource::() + .add_event::>() + .add_event::>() + .add_event::>() .add_systems(Startup, setup) - .add_systems(PreUpdate, add_environment_map_to_camera) + .add_systems( + PreUpdate, + ( + add_environment_map_to_camera, + save_original_cubemap_transforms, + ), + ) .add_systems(Update, change_reflection_type) .add_systems(Update, toggle_rotation) .add_systems( Update, - rotate_camera + (rotate_camera, rotate_cubes) .after(toggle_rotation) .after(change_reflection_type), ) - .add_systems(Update, update_text.after(rotate_camera)) + .add_systems( + Update, + handle_ui_interactions + .after(rotate_camera) + .after(rotate_cubes), + ) + .add_systems( + Update, + ( + update_reflection_mode_radio_buttons, + update_camera_rotation_mode_radio_buttons, + update_cube_rotation_mode_radio_buttons, + ) + .after(handle_ui_interactions), + ) .run(); } @@ -85,14 +183,16 @@ fn setup( mut meshes: ResMut>, mut materials: ResMut>, asset_server: Res, - app_status: Res, cubemaps: Res, ) { + let font = asset_server.load(FONT_PATH); + spawn_scene(&mut commands, &asset_server); - spawn_camera(&mut commands); + spawn_main_camera(&mut commands); spawn_sphere(&mut commands, &mut meshes, &mut materials); - spawn_reflection_probe(&mut commands, &cubemaps); - spawn_text(&mut commands, &app_status); + spawn_reflection_probes(&mut commands, &cubemaps, ReflectionMode::default()); + spawn_reflection_camera(&mut commands, &cubemaps); + spawn_buttons(&mut commands, &font); } // Spawns the cubes, light, and camera. @@ -104,15 +204,17 @@ fn spawn_scene(commands: &mut Commands, asset_server: &AssetServer) { } // Spawns the camera. -fn spawn_camera(commands: &mut Commands) { - commands.spawn(Camera3dBundle { - camera: Camera { - hdr: true, +fn spawn_main_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() - }, - transform: Transform::from_xyz(-6.483, 0.325, 4.381).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); + }) + .insert(MainCamera); } // Creates the sphere mesh and spawns it. @@ -138,38 +240,97 @@ fn spawn_sphere( }); } -// 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: 5000.0, - }, - }); +// Spawns the reflection probes. +fn spawn_reflection_probes(commands: &mut Commands, cubemaps: &Cubemaps, mode: ReflectionMode) { + if mode == ReflectionMode::None { + return; + } + + // Spawn the static light probe. + // + // This is on render layer 1 so that it can be hidden when the static + // reflection mode isn't in use. + 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: match mode { + ReflectionMode::DynamicReflectionProbe => { + cubemaps.dynamic_specular_reflection_probe.clone() + } + ReflectionMode::StaticReflectionProbe => { + cubemaps.static_specular_reflection_probe.clone() + } + ReflectionMode::EnvironmentMap | ReflectionMode::None => { + cubemaps.specular_environment_map.clone() + } + }, + intensity: match mode { + ReflectionMode::DynamicReflectionProbe + | ReflectionMode::StaticReflectionProbe => ENVIRONMENT_MAP_INTENSITY, + ReflectionMode::EnvironmentMap | ReflectionMode::None => SKY_INTENSITY, + }, + }, + }) + .insert(RenderLayers::layer(1)); + + if mode != ReflectionMode::DynamicReflectionProbe { + return; + } + + // Spawn the dynamic light probe, which provides a reflection that's updated + // every frame. + // + // This is on render layer 2 so that it won't be applied to the rendering of + // the reflection itself, which would be circular. + 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.dynamic_specular_reflection_probe.clone(), + intensity: ENVIRONMENT_MAP_INTENSITY, + }, + }) + .insert(RenderLayers::layer(2)); } -// Spawns the help text. -fn spawn_text(commands: &mut Commands, app_status: &AppStatus) { - // Create the text. - commands.spawn( - TextBundle { - text: app_status.create_text(), +// Spawns the omnidirectional camera that provides the dynamic reflection probe. +fn spawn_reflection_camera(commands: &mut Commands, cubemaps: &Cubemaps) { + commands + .spawn(OmnidirectionalCamera3dBundle { + camera: Camera { + target: RenderTarget::Image(cubemaps.dynamic_specular_reflection_probe.clone()), + order: -1, + hdr: true, + is_active: false, + ..default() + }, + tonemapping: Tonemapping::None, ..default() - } - .with_style(Style { - position_type: PositionType::Absolute, - bottom: Val::Px(12.0), - left: Val::Px(12.0), - ..default() - }), - ); + }) + .insert(ReflectionCamera) + .insert(Skybox { + image: cubemaps.skybox.clone(), + brightness: SKY_INTENSITY, + }) + .insert(EnvironmentMapLight { + diffuse_map: cubemaps.diffuse.clone(), + specular_map: cubemaps.static_specular_reflection_probe.clone(), + intensity: SKY_INTENSITY, + }) + .insert(RenderLayers::from_layers(&[0, 1])); } // Adds a world environment map to the camera. This separate system is needed because the camera is @@ -177,73 +338,100 @@ fn spawn_text(commands: &mut Commands, app_status: &AppStatus) { // the environment map after the fact. fn add_environment_map_to_camera( mut commands: Commands, - query: Query>, + main_camera_query: Query, With)>, cubemaps: Res, ) { - for camera_entity in query.iter() { + for camera_entity in main_camera_query.iter() { commands .entity(camera_entity) .insert(create_camera_environment_map_light(&cubemaps)) .insert(Skybox { image: cubemaps.skybox.clone(), - brightness: 5000.0, + brightness: SKY_INTENSITY, }); } } +// Stores the original transform on the cubes so we can restore it later. +fn save_original_cubemap_transforms( + mut commands: Commands, + mut cubes: Query< + (Entity, &Transform), + (With>, With, Without), + >, +) { + for (cube, cube_transform) in cubes.iter_mut() { + commands + .entity(cube) + .insert(OriginalTransform(*cube_transform)); + } +} + // 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, + main_camera_query: Query, With)>, + mut reflection_camera_query: Query<&mut Camera, (With, With)>, + mut reflection_probe_query: Query>, + app_status: ResMut, cubemaps: Res, + mut reflection_mode_change_events: EventReader>, ) { - // Only do anything if space was pressed. - if !keyboard.just_pressed(KeyCode::Space) { + if reflection_mode_change_events.read().count() == 0 { 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), - } + for camera_entity in main_camera_query.iter() { + // Add or remove the reflection probes. + for reflection_probe in reflection_probe_query.iter_mut() { + commands.entity(reflection_probe).despawn(); + } + spawn_reflection_probes(&mut commands, &cubemaps, app_status.reflection_mode); - // Add or remove the environment map from the camera. - for camera in camera_query.iter() { + // Add or remove the environment map from the camera. match app_status.reflection_mode { ReflectionMode::None => { - commands.entity(camera).remove::(); + commands + .entity(camera_entity) + .remove::(); } - ReflectionMode::EnvironmentMap | ReflectionMode::ReflectionProbe => { + ReflectionMode::EnvironmentMap + | ReflectionMode::StaticReflectionProbe + | ReflectionMode::DynamicReflectionProbe => { commands - .entity(camera) + .entity(camera_entity) .insert(create_camera_environment_map_light(&cubemaps)); } } + + // Set the render layers for the camera. + match app_status.reflection_mode { + ReflectionMode::DynamicReflectionProbe => { + commands + .entity(camera_entity) + .insert(RenderLayers::from_layers(&[0, 2])); + } + ReflectionMode::None + | ReflectionMode::EnvironmentMap + | ReflectionMode::StaticReflectionProbe => { + commands.entity(camera_entity).remove::(); + } + } + } + + // Enable or disable the reflection camera. + for mut camera in reflection_camera_query.iter_mut() { + camera.is_active = app_status.reflection_mode == ReflectionMode::DynamicReflectionProbe; } } // 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) { - for mut text in text_query.iter_mut() { - *text = app_status.create_text(); + app_status.camera_rotating = match app_status.camera_rotating { + CameraRotationMode::Rotating => CameraRotationMode::Stationary, + CameraRotationMode::Stationary => CameraRotationMode::Rotating, + } } } @@ -254,7 +442,8 @@ impl TryFrom for ReflectionMode { match value { 0 => Ok(ReflectionMode::None), 1 => Ok(ReflectionMode::EnvironmentMap), - 2 => Ok(ReflectionMode::ReflectionProbe), + 2 => Ok(ReflectionMode::StaticReflectionProbe), + 3 => Ok(ReflectionMode::DynamicReflectionProbe), _ => Err(()), } } @@ -265,53 +454,34 @@ impl Display for ReflectionMode { let text = match *self { ReflectionMode::None => "No reflections", ReflectionMode::EnvironmentMap => "Environment map", - ReflectionMode::ReflectionProbe => "Reflection probe", + ReflectionMode::StaticReflectionProbe => "Static reflection probe", + ReflectionMode::DynamicReflectionProbe => "Dynamic 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) -> 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::default(), - ) - } -} - // 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: 5000.0, + intensity: SKY_INTENSITY, } } -// Rotates the camera a bit every frame. +// Rotates the camera a bit every frame, if enabled. fn rotate_camera( time: Res