Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Binary file not shown.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+23 MB +8 MB for this example is not justifiable, for reference all the other bevy assets combined are 54 MB. Can we put this on the web assets repo or use something smaller?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, my plan was to put this on bevy asset files once it’s reviewed.

Binary file not shown.
Binary file added assets/models/PCCMExample/PCCMExample.glb
Binary file not shown.
2 changes: 1 addition & 1 deletion crates/bevy_light/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
28 changes: 28 additions & 0 deletions crates/bevy_light/src/probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
25 changes: 20 additions & 5 deletions crates/bevy_pbr/src/light_probe/environment_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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};
Expand Down Expand Up @@ -242,6 +245,8 @@ impl LightProbeComponent for EnvironmentMapLight {
// view.
type ViewLightProbeInfo = EnvironmentMapViewLightProbeInfo;

type QueryData = Has<NoParallaxCorrection>;

fn id(&self, image_assets: &RenderAssets<GpuImage>) -> Option<Self::AssetId> {
if image_assets.get(&self.diffuse_map).is_none()
|| image_assets.get(&self.specular_map).is_none()
Expand All @@ -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: <Self::QueryData as QueryData>::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(
Expand Down
92 changes: 78 additions & 14 deletions crates/bevy_pbr/src/light_probe/environment_map.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -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<f32>,
specular: vec3<f32>,
Expand All @@ -17,6 +23,56 @@ struct EnvironmentMapRadiances {
radiance: vec3<f32>,
}

// Computes the direction at which to sample the reflection probe.
fn compute_cubemap_sample_dir(
world_ray_origin: vec3<f32>,
world_ray_direction: vec3<f32>,
light_from_world: mat4x4<f32>,
parallax_correct: bool
) -> vec3<f32> {
var sample_dir: vec3<f32>;

// 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.
Expand Down Expand Up @@ -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.
Expand All @@ -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,
Expand All @@ -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,
Expand Down
14 changes: 10 additions & 4 deletions crates/bevy_pbr/src/light_probe/irradiance_volume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -300,6 +300,8 @@ impl LightProbeComponent for IrradianceVolume {
// here.
type ViewLightProbeInfo = ();

type QueryData = ();

fn id(&self, image_assets: &RenderAssets<GpuImage>) -> Option<Self::AssetId> {
if image_assets.get(&self.voxels).is_none() {
None
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_pbr/src/light_probe/irradiance_volume.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 9 additions & 7 deletions crates/bevy_pbr/src/light_probe/light_probe.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<f32>,
// 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<f32>) -> mat4x4<f32> {
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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.
Expand Down
Loading