diff --git a/Cargo.toml b/Cargo.toml index 7dda86c8f1418..e24058e52cae8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4960,3 +4960,14 @@ name = "Mirror" description = "Demonstrates how to create a mirror with a second camera" category = "3D Rendering" wasm = true + +[[example]] +name = "pccm" +path = "examples/3d/pccm.rs" +doc-scrape-examples = true + +[package.metadata.example.pccm] +name = "Parallax-Corrected Cubemaps" +description = "Demonstrates parallax-corrected cubemap reflections" +category = "3D Rendering" +wasm = true diff --git a/assets/environment_maps/BevyPCCMExample_diffuse.ktx2 b/assets/environment_maps/BevyPCCMExample_diffuse.ktx2 new file mode 100644 index 0000000000000..1546fce014dec Binary files /dev/null and b/assets/environment_maps/BevyPCCMExample_diffuse.ktx2 differ diff --git a/assets/environment_maps/BevyPCCMExample_specular.ktx2 b/assets/environment_maps/BevyPCCMExample_specular.ktx2 new file mode 100644 index 0000000000000..4a1632a2d9b50 Binary files /dev/null and b/assets/environment_maps/BevyPCCMExample_specular.ktx2 differ diff --git a/assets/models/PCCMExample/PCCMExample.glb b/assets/models/PCCMExample/PCCMExample.glb new file mode 100644 index 0000000000000..31a07e17ec19b Binary files /dev/null and b/assets/models/PCCMExample/PCCMExample.glb differ diff --git a/crates/bevy_light/src/lib.rs b/crates/bevy_light/src/lib.rs index 416a5565169e2..8bbf6006b3297 100644 --- a/crates/bevy_light/src/lib.rs +++ b/crates/bevy_light/src/lib.rs @@ -29,7 +29,7 @@ pub use ambient_light::{AmbientLight, GlobalAmbientLight}; mod probe; pub use probe::{ AtmosphereEnvironmentMapLight, EnvironmentMapLight, GeneratedEnvironmentMapLight, - IrradianceVolume, LightProbe, + IrradianceVolume, LightProbe, NoParallaxCorrection, }; mod volumetric; pub use volumetric::{FogVolume, VolumetricFog, VolumetricLight}; diff --git a/crates/bevy_light/src/probe.rs b/crates/bevy_light/src/probe.rs index 213a02b4a0619..ca080384ff369 100644 --- a/crates/bevy_light/src/probe.rs +++ b/crates/bevy_light/src/probe.rs @@ -219,3 +219,31 @@ impl Default for IrradianceVolume { } } } + +/// Add this component to a reflection probe to opt out of *parallax +/// correction*. +/// +/// For environment maps added directly to a camera, Bevy renders the reflected +/// scene that a cubemap captures as though it were infinitely far away. This is +/// acceptable if the cubemap captures very distant objects, such as distant +/// mountains in outdoor scenes. It's less ideal, however, if the cubemap +/// reflects near objects, such as the interior of a room. Therefore, by default +/// for reflection probes Bevy uses *parallax-corrected cubemaps* (PCCM), which +/// causes Bevy to treat the reflected scene as though it coincided with the +/// boundaries of the light probe. +/// +/// As an example, for indoor scenes, it's common to place reflection probes +/// inside each room and to make the boundaries of the reflection probe (as +/// determined by the light probe's [`bevy_transform::components::Transform`]) +/// coincide with the walls of the room. That way, the reflection probes will +/// (1) apply to the objects inside the room and (2) take the positions of those +/// objects into account in order to create a realistic reflection. +/// +/// Place this component on an entity that has a [`LightProbe`] and +/// [`EnvironmentMapLight`] component in order to opt out of parallax +/// correction. +/// +/// See the `pccm` example for an example of usage. +#[derive(Clone, Copy, Default, Component, Reflect)] +#[reflect(Clone, Default, Component)] +pub struct NoParallaxCorrection; diff --git a/crates/bevy_pbr/src/light_probe/environment_map.rs b/crates/bevy_pbr/src/light_probe/environment_map.rs index bd1ba1aeeb663..9afdde3e689aa 100644 --- a/crates/bevy_pbr/src/light_probe/environment_map.rs +++ b/crates/bevy_pbr/src/light_probe/environment_map.rs @@ -45,9 +45,12 @@ //! [several pre-filtered environment maps]: https://github.com/KhronosGroup/glTF-Sample-Environments use bevy_asset::AssetId; -use bevy_ecs::{query::QueryItem, system::lifetimeless::Read}; +use bevy_ecs::{ + query::{Has, QueryData, QueryItem}, + system::lifetimeless::Read, +}; use bevy_image::Image; -use bevy_light::EnvironmentMapLight; +use bevy_light::{EnvironmentMapLight, NoParallaxCorrection}; use bevy_render::{ extract_instances::ExtractInstance, render_asset::RenderAssets, @@ -64,7 +67,7 @@ use core::{num::NonZero, ops::Deref}; use crate::{ add_cubemap_texture_view, binding_arrays_are_usable, EnvironmentMapUniform, - MAX_VIEW_LIGHT_PROBES, + RenderLightProbeFlags, MAX_VIEW_LIGHT_PROBES, }; use super::{LightProbeComponent, RenderViewLightProbes}; @@ -242,6 +245,8 @@ impl LightProbeComponent for EnvironmentMapLight { // view. type ViewLightProbeInfo = EnvironmentMapViewLightProbeInfo; + type QueryData = Has; + fn id(&self, image_assets: &RenderAssets) -> Option { if image_assets.get(&self.diffuse_map).is_none() || image_assets.get(&self.specular_map).is_none() @@ -259,8 +264,18 @@ impl LightProbeComponent for EnvironmentMapLight { self.intensity } - fn affects_lightmapped_mesh_diffuse(&self) -> bool { - self.affects_lightmapped_mesh_diffuse + fn flags( + &self, + no_parallax_correction: ::Item<'_, '_>, + ) -> RenderLightProbeFlags { + let mut flags = RenderLightProbeFlags::empty(); + if self.affects_lightmapped_mesh_diffuse { + flags.insert(RenderLightProbeFlags::AFFECTS_LIGHTMAPPED_MESH_DIFFUSE); + } + if !no_parallax_correction { + flags.insert(RenderLightProbeFlags::ENABLE_PARALLAX_CORRECTION); + } + flags } fn create_render_view_light_probes( diff --git a/crates/bevy_pbr/src/light_probe/environment_map.wgsl b/crates/bevy_pbr/src/light_probe/environment_map.wgsl index e6709649e7997..4782d6616e58e 100644 --- a/crates/bevy_pbr/src/light_probe/environment_map.wgsl +++ b/crates/bevy_pbr/src/light_probe/environment_map.wgsl @@ -4,9 +4,15 @@ #import bevy_pbr::mesh_view_bindings as bindings #import bevy_pbr::mesh_view_bindings::light_probes #import bevy_pbr::mesh_view_bindings::environment_map_uniform +#import bevy_pbr::mesh_view_types::{ + LIGHT_PROBE_FLAG_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE, LIGHT_PROBE_FLAG_PARALLAX_CORRECT +} #import bevy_pbr::lighting::{F_Schlick_vec, LightingInput, LayerLightingInput, LAYER_BASE, LAYER_CLEARCOAT} #import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges +// The maximum representable value in a 32-bit floating point number. +const FLOAT_MAX: f32 = 3.40282347e+38; + struct EnvironmentMapLight { diffuse: vec3, specular: vec3, @@ -17,6 +23,56 @@ struct EnvironmentMapRadiances { radiance: vec3, } +// Computes the direction at which to sample the reflection probe. +fn compute_cubemap_sample_dir( + world_ray_origin: vec3, + world_ray_direction: vec3, + light_from_world: mat4x4, + parallax_correct: bool +) -> vec3 { + var sample_dir: vec3; + + // If we're supposed to parallax correct, then intersect with the light cube. + if (parallax_correct) { + // Compute the direction of the ray bouncing off the surface, in light + // probe space. + // Recall that light probe space is a 1×1×1 cube centered at the origin. + let ray_origin = (light_from_world * vec4(world_ray_origin, 1.0)).xyz; + let ray_direction = (light_from_world * vec4(world_ray_direction, 0.0)).xyz; + + // Solve for the intersection of that ray with each side of the cube. + // Since our light probe is a 1×1×1 cube centered at the origin in light + // probe space, the faces of the cube are at X = ±0.5, Y = ±0.5, and Z = + // ±0.5. + var t0 = (vec3(-0.5) - ray_origin) / ray_direction; + var t1 = (vec3(0.5) - ray_origin) / ray_direction; + + // We're shooting the rays forward, so we need to rule out negative time + // values. So, if t is negative, make it a large value so that we won't + // choose it below. + // We would use infinity here but WGSL forbids it: + // https://github.com/gfx-rs/wgpu/issues/5515 + t0 = select(vec3(FLOAT_MAX), t0, t0 >= vec3(0.0)); + t1 = select(vec3(FLOAT_MAX), t1, t1 >= vec3(0.0)); + + // Choose the minimum valid time value to find the intersection of the + // first cube face. + let t_min = min(t0, t1); + let t = min(min(t_min.x, t_min.y), t_min.z); + + // Compute the sample direction. (It doesn't have to be normalized.) + sample_dir = ray_origin + ray_direction * t; + } else { + // We treat the reflection as infinitely far away in the non-parallax + // case, so the ray origin is irrelevant. + sample_dir = (light_from_world * vec4(world_ray_direction, 0.0)).xyz; + } + + // Cubemaps are left-handed, so we negate the Z coordinate. + sample_dir.z = -sample_dir.z; + return sample_dir; +} + // 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. @@ -48,8 +104,11 @@ fn compute_radiances( if (query_result.texture_index < 0) { query_result.texture_index = light_probes.view_cubemap_index; query_result.intensity = light_probes.intensity_for_view; - query_result.affects_lightmapped_mesh_diffuse = - light_probes.view_environment_map_affects_lightmapped_mesh_diffuse != 0u; + if light_probes.view_environment_map_affects_lightmapped_mesh_diffuse != 0u { + query_result.flags = LIGHT_PROBE_FLAG_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE; + } else { + query_result.flags = 0u; + } } // If there's no cubemap, bail out. @@ -67,16 +126,19 @@ fn compute_radiances( // environment map, note that. var enable_diffuse = !found_diffuse_indirect; #ifdef LIGHTMAP - enable_diffuse = enable_diffuse && query_result.affects_lightmapped_mesh_diffuse; + enable_diffuse = enable_diffuse && + (query_result.flags & LIGHT_PROBE_FLAG_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE) != 0u; #endif // LIGHTMAP + let parallax_correct = (query_result.flags & LIGHT_PROBE_FLAG_PARALLAX_CORRECT) != 0u; + if (enable_diffuse) { - var irradiance_sample_dir = N; - // Rotating the world space ray direction by the environment light map transform matrix, it is - // equivalent to rotating the diffuse environment cubemap itself. - irradiance_sample_dir = (environment_map_uniform.transform * vec4(irradiance_sample_dir, 1.0)).xyz; - // Cube maps are left-handed so we negate the z coordinate. - irradiance_sample_dir.z = -irradiance_sample_dir.z; + let irradiance_sample_dir = compute_cubemap_sample_dir( + world_position, + N, + query_result.light_from_world, + parallax_correct + ); radiances.irradiance = textureSampleLevel( bindings::diffuse_environment_maps[query_result.texture_index], bindings::environment_map_sampler, @@ -85,11 +147,13 @@ fn compute_radiances( } var radiance_sample_dir = radiance_sample_direction(N, R, roughness); - // Rotating the world space ray direction by the environment light map transform matrix, it is - // equivalent to rotating the specular environment cubemap itself. - radiance_sample_dir = (environment_map_uniform.transform * vec4(radiance_sample_dir, 1.0)).xyz; - // Cube maps are left-handed so we negate the z coordinate. - radiance_sample_dir.z = -radiance_sample_dir.z; + radiance_sample_dir = compute_cubemap_sample_dir( + world_position, + radiance_sample_dir, + query_result.light_from_world, + parallax_correct + ); + radiances.radiance = textureSampleLevel( bindings::specular_environment_maps[query_result.texture_index], bindings::environment_map_sampler, diff --git a/crates/bevy_pbr/src/light_probe/irradiance_volume.rs b/crates/bevy_pbr/src/light_probe/irradiance_volume.rs index 417f22083ead6..a65de846f2ce0 100644 --- a/crates/bevy_pbr/src/light_probe/irradiance_volume.rs +++ b/crates/bevy_pbr/src/light_probe/irradiance_volume.rs @@ -149,8 +149,8 @@ use core::{num::NonZero, ops::Deref}; use bevy_asset::AssetId; use crate::{ - add_cubemap_texture_view, binding_arrays_are_usable, RenderViewLightProbes, - MAX_VIEW_LIGHT_PROBES, + add_cubemap_texture_view, binding_arrays_are_usable, RenderLightProbeFlags, + RenderViewLightProbes, MAX_VIEW_LIGHT_PROBES, }; use super::LightProbeComponent; @@ -300,6 +300,8 @@ impl LightProbeComponent for IrradianceVolume { // here. type ViewLightProbeInfo = (); + type QueryData = (); + fn id(&self, image_assets: &RenderAssets) -> Option { if image_assets.get(&self.voxels).is_none() { None @@ -312,8 +314,12 @@ impl LightProbeComponent for IrradianceVolume { self.intensity } - fn affects_lightmapped_mesh_diffuse(&self) -> bool { - self.affects_lightmapped_meshes + fn flags(&self, _: Self::QueryData) -> RenderLightProbeFlags { + if self.affects_lightmapped_meshes { + RenderLightProbeFlags::AFFECTS_LIGHTMAPPED_MESH_DIFFUSE + } else { + RenderLightProbeFlags::empty() + } } fn create_render_view_light_probes( diff --git a/crates/bevy_pbr/src/light_probe/irradiance_volume.wgsl b/crates/bevy_pbr/src/light_probe/irradiance_volume.wgsl index f079bd6a2e6e6..5076a1d92af07 100644 --- a/crates/bevy_pbr/src/light_probe/irradiance_volume.wgsl +++ b/crates/bevy_pbr/src/light_probe/irradiance_volume.wgsl @@ -34,7 +34,7 @@ fn irradiance_volume_light( // If we're lightmapped, and the irradiance volume contributes no diffuse // light, then bail out. #ifdef LIGHTMAP - if (!query_result.affects_lightmapped_mesh_diffuse) { + if ((query_result.flags & LIGHT_PROBE_FLAG_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE) == 0u) { return vec3(0.0f); } #endif // LIGHTMAP diff --git a/crates/bevy_pbr/src/light_probe/light_probe.wgsl b/crates/bevy_pbr/src/light_probe/light_probe.wgsl index 16a211258a379..d283987eb6a37 100644 --- a/crates/bevy_pbr/src/light_probe/light_probe.wgsl +++ b/crates/bevy_pbr/src/light_probe/light_probe.wgsl @@ -3,7 +3,10 @@ #import bevy_pbr::clustered_forward #import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges #import bevy_pbr::mesh_view_bindings::light_probes -#import bevy_pbr::mesh_view_types::LightProbe +#import bevy_pbr::mesh_view_types::{ + LightProbe, LIGHT_PROBE_FLAG_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE, + LIGHT_PROBE_FLAG_PARALLAX_CORRECT +} // The result of searching for a light probe. struct LightProbeQueryResult { @@ -16,8 +19,9 @@ struct LightProbeQueryResult { // Transform from world space to the light probe model space. In light probe // model space, the light probe is a 1×1×1 cube centered on the origin. light_from_world: mat4x4, - // Whether this light probe contributes diffuse light to lightmapped meshes. - affects_lightmapped_mesh_diffuse: bool, + // The flags that the light probe has: a combination of + // `LIGHT_PROBE_FLAG_*`. + flags: u32, }; fn transpose_affine_matrix(matrix: mat3x4) -> mat4x4 { @@ -82,8 +86,7 @@ fn query_light_probe( result.texture_index = light_probe.cubemap_index; result.intensity = light_probe.intensity; result.light_from_world = light_from_world; - result.affects_lightmapped_mesh_diffuse = - light_probe.affects_lightmapped_mesh_diffuse != 0u; + result.flags = light_probe.flags; break; } } @@ -136,8 +139,7 @@ fn query_light_probe( result.texture_index = light_probe.cubemap_index; result.intensity = light_probe.intensity; result.light_from_world = light_from_world; - result.affects_lightmapped_mesh_diffuse = - light_probe.affects_lightmapped_mesh_diffuse != 0u; + result.flags = light_probe.flags; // TODO: Workaround for ICE in DXC https://github.com/microsoft/DirectXShaderCompiler/issues/6183 // We can't use `break` here because of the ICE. diff --git a/crates/bevy_pbr/src/light_probe/mod.rs b/crates/bevy_pbr/src/light_probe/mod.rs index 1e0088a8c7c8a..846043029d3ce 100644 --- a/crates/bevy_pbr/src/light_probe/mod.rs +++ b/crates/bevy_pbr/src/light_probe/mod.rs @@ -10,7 +10,7 @@ use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ component::Component, entity::Entity, - query::With, + query::{QueryData, ReadOnlyQueryData, With}, resource::Resource, schedule::IntoScheduleConfigs, system::{Commands, Local, Query, Res, ResMut}, @@ -32,6 +32,7 @@ use bevy_render::{ }; use bevy_shader::load_shader_library; use bevy_transform::{components::Transform, prelude::GlobalTransform}; +use bitflags::bitflags; use tracing::error; use core::{hash::Hash, ops::Deref}; @@ -80,9 +81,9 @@ struct RenderLightProbe { /// See the comment in [`EnvironmentMapLight`] for details. intensity: f32, - /// Whether this light probe adds to the diffuse contribution of the - /// irradiance for meshes with lightmaps. - affects_lightmapped_mesh_diffuse: u32, + /// Various flags associated with the light probe: the bit value of + /// [`RenderLightProbeFlags`]. + flags: u32, } /// A per-view shader uniform that specifies all the light probes that the view @@ -157,9 +158,8 @@ where // See the comment in [`EnvironmentMapLight`] for details. intensity: f32, - // Whether this light probe adds to the diffuse contribution of the - // irradiance for meshes with lightmaps. - affects_lightmapped_mesh_diffuse: bool, + // Various flags associated with the light probe. + flags: RenderLightProbeFlags, // The IDs of all assets associated with this light probe. // @@ -169,6 +169,21 @@ where asset_id: C::AssetId, } +bitflags! { + /// Various flags that can be associated with light probes. + #[derive(Clone, Copy, PartialEq, Debug)] + pub struct RenderLightProbeFlags: u8 { + /// Whether this light probe adds to the diffuse contribution of the + /// irradiance for meshes with lightmaps. + const AFFECTS_LIGHTMAPPED_MESH_DIFFUSE = 1; + /// Whether this light probe has parallax correction enabled. + /// + /// See the comments in [`bevy_light::NoParallaxCorrection`] for more + /// information. + const ENABLE_PARALLAX_CORRECTION = 2; + } +} + /// A component, part of the render world, that stores the mapping from asset ID /// or IDs to the texture index in the appropriate binding arrays. /// @@ -239,6 +254,10 @@ pub trait LightProbeComponent: Send + Sync + Component + Sized { /// attached directly to views. type ViewLightProbeInfo: Send + Sync + Default; + /// Any additional query data needed to determine the + /// [`RenderLightProbeFlags`] for this light probe. + type QueryData: ReadOnlyQueryData; + /// Returns the asset ID or asset IDs of the texture or textures referenced /// by this light probe. fn id(&self, image_assets: &RenderAssets) -> Option; @@ -249,9 +268,12 @@ pub trait LightProbeComponent: Send + Sync + Component + Sized { /// sampled from the texture. fn intensity(&self) -> f32; - /// Returns true if this light probe contributes diffuse lighting to meshes - /// with lightmaps or false otherwise. - fn affects_lightmapped_mesh_diffuse(&self) -> bool; + /// Returns the appropriate value of [`RenderLightProbeFlags`] for this + /// component. + fn flags( + &self, + query_components: ::Item<'_, '_>, + ) -> RenderLightProbeFlags; /// Creates an instance of [`RenderViewLightProbes`] containing all the /// information needed to render this light probe. @@ -346,7 +368,7 @@ fn gather_environment_map_uniform( /// to views, performing frustum culling and distance sorting in the process. fn gather_light_probes( image_assets: Res>, - light_probe_query: Extract>>, + light_probe_query: Extract>>, view_query: Extract< Query<(RenderEntity, &GlobalTransform, &Frustum, Option<&C>), With>, >, @@ -540,7 +562,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, query_components): ( + &GlobalTransform, + &C, + ::Item<'_, '_>, + ), image_assets: &RenderAssets, ) -> Option> { let light_from_world_transposed = @@ -554,7 +580,7 @@ where ], asset_id: id, intensity: environment_map.intensity(), - affects_lightmapped_mesh_diffuse: environment_map.affects_lightmapped_mesh_diffuse(), + flags: environment_map.flags(query_components), }) } @@ -643,8 +669,7 @@ where light_from_world_transposed: light_probe.light_from_world, texture_index: cubemap_index as i32, intensity: light_probe.intensity, - affects_lightmapped_mesh_diffuse: light_probe.affects_lightmapped_mesh_diffuse - as u32, + flags: light_probe.flags.bits() as u32, }); } } @@ -659,7 +684,7 @@ where light_from_world: self.light_from_world, world_from_light: self.world_from_light, intensity: self.intensity, - affects_lightmapped_mesh_diffuse: self.affects_lightmapped_mesh_diffuse, + flags: self.flags, asset_id: self.asset_id.clone(), } } diff --git a/crates/bevy_pbr/src/render/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index 112556ee1eb76..f2a505af1b684 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -121,14 +121,19 @@ struct ClusterOffsetsAndCounts { }; #endif +// Whether this light probe contributes diffuse light to lightmapped meshes. +const LIGHT_PROBE_FLAG_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE: u32 = 1; +// Whether this light probe has parallax correction enabled. +const LIGHT_PROBE_FLAG_PARALLAX_CORRECT: u32 = 2; + struct LightProbe { // This is stored as the transpose in order to save space in this structure. // It'll be transposed in the `environment_map_light` function. light_from_world_transposed: mat3x4, cubemap_index: i32, intensity: f32, - // Whether this light probe contributes diffuse light to lightmapped meshes. - affects_lightmapped_mesh_diffuse: u32, + // Various flags that apply to this light probe. + flags: u32, }; struct LightProbes { diff --git a/examples/3d/pccm.rs b/examples/3d/pccm.rs new file mode 100644 index 0000000000000..3910a474a1e89 --- /dev/null +++ b/examples/3d/pccm.rs @@ -0,0 +1,230 @@ +//! Demonstrates parallax-corrected cubemap reflections. + +use bevy::{light::NoParallaxCorrection, math::ops, prelude::*, render::view::Hdr}; + +use crate::widgets::{WidgetClickEvent, WidgetClickSender}; + +#[path = "../helpers/widgets.rs"] +mod widgets; + +/// A marker component for the inner rotating reflective cube. +#[derive(Clone, Component)] +struct InnerCube; + +/// The brightness of the cubemap. +/// +/// Since the cubemap image was baked in Blender, which uses a different +/// exposure setting than that of Bevy, we need this factor in order to make the +/// exposure of the baked image match ours. +const ENVIRONMENT_MAP_INTENSITY: f32 = 2000.0; + +/// The speed at which the camera rotates in radians per second. +const CAMERA_ROTATION_SPEED: f32 = 0.25; + +/// The speed at which the rotating inner cube rotates about the X axis, in radians per second. +const INNER_CUBE_ROTATION_SPEED_X: f32 = 1.5; +/// The speed at which the rotating inner cube rotates about the Z axis, in radians per second. +const INNER_CUBE_ROTATION_SPEED_Z: f32 = 1.3; + +/// The current value of user-customizable settings for this demo. +#[derive(Resource, Default)] +struct AppStatus { + /// Whether parallax correction is enabled. + pccm_enabled: PccmEnableStatus, +} + +/// Whether parallax correction is enabled. +#[derive(Clone, Copy, PartialEq, Default)] +enum PccmEnableStatus { + /// Parallax correction is enabled. + #[default] + Enabled, + /// Parallax correction is disabled. + Disabled, +} + +/// The example entry point. +fn main() { + App::new() + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "Bevy Parallax-Corrected Cubemaps Example".into(), + ..default() + }), + ..default() + })) + .init_resource::() + .add_message::>() + .add_systems(Startup, setup) + .add_systems(FixedUpdate, rotate_inner_cube) + .add_systems(Update, widgets::handle_ui_interactions::) + .add_systems( + Update, + (handle_pccm_enable_change, update_radio_buttons) + .after(widgets::handle_ui_interactions::), + ) + .add_systems(Update, rotate_camera) + .run(); +} + +/// Creates the initial scene. +fn setup( + mut commands: Commands, + asset_server: Res, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // Spawn the glTF scene. + commands.spawn(SceneRoot( + asset_server.load("models/PCCMExample/PCCMExample.glb#Scene0"), + )); + + spawn_camera(&mut commands); + spawn_inner_cube(&mut commands, &mut meshes, &mut materials); + spawn_reflection_probe(&mut commands, &asset_server); + spawn_buttons(&mut commands); +} + +/// Spawns the rotating camera. +fn spawn_camera(commands: &mut Commands) { + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(0.0, 0.0, 3.5).looking_at(Vec3::ZERO, Dir3::Y), + Hdr, + )); +} + +/// Spawns the inner rotating reflective cube in the center of the scene. +fn spawn_inner_cube( + commands: &mut Commands, + meshes: &mut Assets, + materials: &mut Assets, +) { + let cube_mesh = meshes.add( + Cuboid::default() + .mesh() + .build() + .with_duplicated_vertices() + .with_computed_flat_normals(), + ); + let cube_material = materials.add(StandardMaterial { + base_color: Color::WHITE, + metallic: 1.0, + perceptual_roughness: 0.0, + ..default() + }); + + commands.spawn(( + Mesh3d(cube_mesh), + MeshMaterial3d(cube_material), + Transform::default(), + InnerCube, + )); +} + +/// Spawns the reflection probe (i.e. cubemap reflection) in the center of the scene. +fn spawn_reflection_probe(commands: &mut Commands, asset_server: &AssetServer) { + let diffuse_map = asset_server.load("environment_maps/BevyPCCMExample_diffuse.ktx2"); + let specular_map = asset_server.load("environment_maps/BevyPCCMExample_specular.ktx2"); + commands.spawn(( + LightProbe, + EnvironmentMapLight { + diffuse_map, + specular_map, + intensity: ENVIRONMENT_MAP_INTENSITY, + ..default() + }, + Transform::from_scale(Vec3::splat(5.0)), + )); +} + +/// Spawns the buttons at the bottom of the screen. +fn spawn_buttons(commands: &mut Commands) { + commands.spawn(( + widgets::main_ui_node(), + children![widgets::option_buttons( + "Parallax Correction", + &[ + (PccmEnableStatus::Enabled, "On"), + (PccmEnableStatus::Disabled, "Off"), + ], + )], + )); +} + +/// Rotates the inner reflective cube every frame. +fn rotate_inner_cube(mut cubes_query: Query<&mut Transform, With>, time: Res