Skip to content

Commit dd230f2

Browse files
atlv24pcwaltonalice-i-cecile
authored
Parallax-corrected cubemaps for reflection probes (adopted) (#22582)
# Objective - Adopts and closes #22288 ## Solution - change the example to make it easier to tell if the reflection is actually matching - fix the assets because the reflection doesnt actually match - throw the assets into the asset repo bevyengine/bevy_asset_files#7 ## Testing - running the example --------- Co-authored-by: Patrick Walton <[email protected]> Co-authored-by: Alice Cecile <[email protected]>
1 parent 0ddebc9 commit dd230f2

File tree

14 files changed

+448
-50
lines changed

14 files changed

+448
-50
lines changed

Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5415,3 +5415,15 @@ name = "Dynamic Mipmap Generation"
54155415
description = "Demonstrates use of the mipmap generation plugin to generate mipmaps for a texture"
54165416
category = "2D Rendering"
54175417
wasm = false
5418+
5419+
[[example]]
5420+
name = "pccm"
5421+
path = "examples/3d/pccm.rs"
5422+
doc-scrape-examples = true
5423+
required-features = ["free_camera", "https"]
5424+
5425+
[package.metadata.example.pccm]
5426+
name = "Parallax-Corrected Cubemaps"
5427+
description = "Demonstrates parallax-corrected cubemap reflections"
5428+
category = "3D Rendering"
5429+
wasm = true

crates/bevy_light/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ use bevy_camera::visibility::SetViewVisibility;
3131
mod probe;
3232
pub use probe::{
3333
AtmosphereEnvironmentMapLight, EnvironmentMapLight, GeneratedEnvironmentMapLight,
34-
IrradianceVolume, LightProbe,
34+
IrradianceVolume, LightProbe, NoParallaxCorrection,
3535
};
3636
mod volumetric;
3737
pub use volumetric::{FogVolume, VolumetricFog, VolumetricLight};

crates/bevy_light/src/probe.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,3 +290,31 @@ impl Default for IrradianceVolume {
290290
}
291291
}
292292
}
293+
294+
/// Add this component to a reflection probe to opt out of *parallax
295+
/// correction*.
296+
///
297+
/// For environment maps added directly to a camera, Bevy renders the reflected
298+
/// scene that a cubemap captures as though it were infinitely far away. This is
299+
/// acceptable if the cubemap captures very distant objects, such as distant
300+
/// mountains in outdoor scenes. It's less ideal, however, if the cubemap
301+
/// reflects near objects, such as the interior of a room. Therefore, by default
302+
/// for reflection probes Bevy uses *parallax-corrected cubemaps* (PCCM), which
303+
/// causes Bevy to treat the reflected scene as though it coincided with the
304+
/// boundaries of the light probe.
305+
///
306+
/// As an example, for indoor scenes, it's common to place reflection probes
307+
/// inside each room and to make the boundaries of the reflection probe (as
308+
/// determined by the light probe's [`bevy_transform::components::Transform`])
309+
/// coincide with the walls of the room. That way, the reflection probes will
310+
/// (1) apply to the objects inside the room and (2) take the positions of those
311+
/// objects into account in order to create a realistic reflection.
312+
///
313+
/// Place this component on an entity that has a [`LightProbe`] and
314+
/// [`EnvironmentMapLight`] component in order to opt out of parallax
315+
/// correction.
316+
///
317+
/// See the `pccm` example for an example of usage.
318+
#[derive(Clone, Copy, Default, Component, Reflect)]
319+
#[reflect(Clone, Default, Component)]
320+
pub struct NoParallaxCorrection;

crates/bevy_pbr/src/light_probe/environment_map.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,12 @@
4545
//! [several pre-filtered environment maps]: https://github.com/KhronosGroup/glTF-Sample-Environments
4646
4747
use bevy_asset::AssetId;
48-
use bevy_ecs::{query::QueryItem, system::lifetimeless::Read};
48+
use bevy_ecs::{
49+
query::{Has, QueryData, QueryItem},
50+
system::lifetimeless::Read,
51+
};
4952
use bevy_image::Image;
50-
use bevy_light::EnvironmentMapLight;
53+
use bevy_light::{EnvironmentMapLight, NoParallaxCorrection};
5154
use bevy_render::{
5255
extract_instances::ExtractInstance,
5356
render_asset::RenderAssets,
@@ -64,7 +67,7 @@ use core::{num::NonZero, ops::Deref};
6467

6568
use crate::{
6669
add_cubemap_texture_view, binding_arrays_are_usable, EnvironmentMapUniform,
67-
MAX_VIEW_LIGHT_PROBES,
70+
RenderLightProbeFlags, MAX_VIEW_LIGHT_PROBES,
6871
};
6972

7073
use super::{LightProbeComponent, RenderViewLightProbes};
@@ -242,6 +245,8 @@ impl LightProbeComponent for EnvironmentMapLight {
242245
// view.
243246
type ViewLightProbeInfo = EnvironmentMapViewLightProbeInfo;
244247

248+
type QueryData = Has<NoParallaxCorrection>;
249+
245250
fn id(&self, image_assets: &RenderAssets<GpuImage>) -> Option<Self::AssetId> {
246251
if image_assets.get(&self.diffuse_map).is_none()
247252
|| image_assets.get(&self.specular_map).is_none()
@@ -259,8 +264,18 @@ impl LightProbeComponent for EnvironmentMapLight {
259264
self.intensity
260265
}
261266

262-
fn affects_lightmapped_mesh_diffuse(&self) -> bool {
263-
self.affects_lightmapped_mesh_diffuse
267+
fn flags(
268+
&self,
269+
no_parallax_correction: <Self::QueryData as QueryData>::Item<'_, '_>,
270+
) -> RenderLightProbeFlags {
271+
let mut flags = RenderLightProbeFlags::empty();
272+
if self.affects_lightmapped_mesh_diffuse {
273+
flags.insert(RenderLightProbeFlags::AFFECTS_LIGHTMAPPED_MESH_DIFFUSE);
274+
}
275+
if !no_parallax_correction {
276+
flags.insert(RenderLightProbeFlags::ENABLE_PARALLAX_CORRECTION);
277+
}
278+
flags
264279
}
265280

266281
fn create_render_view_light_probes(

crates/bevy_pbr/src/light_probe/environment_map.wgsl

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@
44
#import bevy_pbr::mesh_view_bindings as bindings
55
#import bevy_pbr::mesh_view_bindings::light_probes
66
#import bevy_pbr::mesh_view_bindings::environment_map_uniform
7+
#import bevy_pbr::mesh_view_types::{
8+
LIGHT_PROBE_FLAG_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE, LIGHT_PROBE_FLAG_PARALLAX_CORRECT
9+
}
710
#import bevy_pbr::lighting::{F_Schlick_vec, LightingInput, LayerLightingInput, LAYER_BASE, LAYER_CLEARCOAT}
811
#import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges
912

13+
// The maximum representable value in a 32-bit floating point number.
14+
const FLOAT_MAX: f32 = 3.40282347e+38;
15+
1016
struct EnvironmentMapLight {
1117
diffuse: vec3<f32>,
1218
specular: vec3<f32>,
@@ -17,6 +23,56 @@ struct EnvironmentMapRadiances {
1723
radiance: vec3<f32>,
1824
}
1925

26+
// Computes the direction at which to sample the reflection probe.
27+
fn compute_cubemap_sample_dir(
28+
world_ray_origin: vec3<f32>,
29+
world_ray_direction: vec3<f32>,
30+
light_from_world: mat4x4<f32>,
31+
parallax_correct: bool
32+
) -> vec3<f32> {
33+
var sample_dir: vec3<f32>;
34+
35+
// If we're supposed to parallax correct, then intersect with the light cube.
36+
if (parallax_correct) {
37+
// Compute the direction of the ray bouncing off the surface, in light
38+
// probe space.
39+
// Recall that light probe space is a 1×1×1 cube centered at the origin.
40+
let ray_origin = (light_from_world * vec4(world_ray_origin, 1.0)).xyz;
41+
let ray_direction = (light_from_world * vec4(world_ray_direction, 0.0)).xyz;
42+
43+
// Solve for the intersection of that ray with each side of the cube.
44+
// Since our light probe is a 1×1×1 cube centered at the origin in light
45+
// probe space, the faces of the cube are at X = ±0.5, Y = ±0.5, and Z =
46+
// ±0.5.
47+
var t0 = (vec3(-0.5) - ray_origin) / ray_direction;
48+
var t1 = (vec3(0.5) - ray_origin) / ray_direction;
49+
50+
// We're shooting the rays forward, so we need to rule out negative time
51+
// values. So, if t is negative, make it a large value so that we won't
52+
// choose it below.
53+
// We would use infinity here but WGSL forbids it:
54+
// https://github.com/gfx-rs/wgpu/issues/5515
55+
t0 = select(vec3(FLOAT_MAX), t0, t0 >= vec3(0.0));
56+
t1 = select(vec3(FLOAT_MAX), t1, t1 >= vec3(0.0));
57+
58+
// Choose the minimum valid time value to find the intersection of the
59+
// first cube face.
60+
let t_min = min(t0, t1);
61+
let t = min(min(t_min.x, t_min.y), t_min.z);
62+
63+
// Compute the sample direction. (It doesn't have to be normalized.)
64+
sample_dir = ray_origin + ray_direction * t;
65+
} else {
66+
// We treat the reflection as infinitely far away in the non-parallax
67+
// case, so the ray origin is irrelevant.
68+
sample_dir = (light_from_world * vec4(world_ray_direction, 0.0)).xyz;
69+
}
70+
71+
// Cubemaps are left-handed, so we negate the Z coordinate.
72+
sample_dir.z = -sample_dir.z;
73+
return sample_dir;
74+
}
75+
2076
// Define two versions of this function, one for the case in which there are
2177
// multiple light probes and one for the case in which only the view light probe
2278
// is present.
@@ -48,8 +104,11 @@ fn compute_radiances(
48104
if (query_result.texture_index < 0) {
49105
query_result.texture_index = light_probes.view_cubemap_index;
50106
query_result.intensity = light_probes.intensity_for_view;
51-
query_result.affects_lightmapped_mesh_diffuse =
52-
light_probes.view_environment_map_affects_lightmapped_mesh_diffuse != 0u;
107+
if light_probes.view_environment_map_affects_lightmapped_mesh_diffuse != 0u {
108+
query_result.flags = LIGHT_PROBE_FLAG_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE;
109+
} else {
110+
query_result.flags = 0u;
111+
}
53112
}
54113

55114
// If there's no cubemap, bail out.
@@ -67,16 +126,19 @@ fn compute_radiances(
67126
// environment map, note that.
68127
var enable_diffuse = !found_diffuse_indirect;
69128
#ifdef LIGHTMAP
70-
enable_diffuse = enable_diffuse && query_result.affects_lightmapped_mesh_diffuse;
129+
enable_diffuse = enable_diffuse &&
130+
(query_result.flags & LIGHT_PROBE_FLAG_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE) != 0u;
71131
#endif // LIGHTMAP
72132

133+
let parallax_correct = (query_result.flags & LIGHT_PROBE_FLAG_PARALLAX_CORRECT) != 0u;
134+
73135
if (enable_diffuse) {
74-
var irradiance_sample_dir = N;
75-
// Rotating the world space ray direction by the environment light map transform matrix, it is
76-
// equivalent to rotating the diffuse environment cubemap itself.
77-
irradiance_sample_dir = (environment_map_uniform.transform * vec4(irradiance_sample_dir, 1.0)).xyz;
78-
// Cube maps are left-handed so we negate the z coordinate.
79-
irradiance_sample_dir.z = -irradiance_sample_dir.z;
136+
let irradiance_sample_dir = compute_cubemap_sample_dir(
137+
world_position,
138+
N,
139+
query_result.light_from_world,
140+
parallax_correct
141+
);
80142
radiances.irradiance = textureSampleLevel(
81143
bindings::diffuse_environment_maps[query_result.texture_index],
82144
bindings::environment_map_sampler,
@@ -85,11 +147,13 @@ fn compute_radiances(
85147
}
86148

87149
var radiance_sample_dir = radiance_sample_direction(N, R, roughness);
88-
// Rotating the world space ray direction by the environment light map transform matrix, it is
89-
// equivalent to rotating the specular environment cubemap itself.
90-
radiance_sample_dir = (environment_map_uniform.transform * vec4(radiance_sample_dir, 1.0)).xyz;
91-
// Cube maps are left-handed so we negate the z coordinate.
92-
radiance_sample_dir.z = -radiance_sample_dir.z;
150+
radiance_sample_dir = compute_cubemap_sample_dir(
151+
world_position,
152+
radiance_sample_dir,
153+
query_result.light_from_world,
154+
parallax_correct
155+
);
156+
93157
radiances.radiance = textureSampleLevel(
94158
bindings::specular_environment_maps[query_result.texture_index],
95159
bindings::environment_map_sampler,

crates/bevy_pbr/src/light_probe/irradiance_volume.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,8 @@ use core::{num::NonZero, ops::Deref};
149149
use bevy_asset::AssetId;
150150

151151
use crate::{
152-
add_cubemap_texture_view, binding_arrays_are_usable, RenderViewLightProbes,
153-
MAX_VIEW_LIGHT_PROBES,
152+
add_cubemap_texture_view, binding_arrays_are_usable, RenderLightProbeFlags,
153+
RenderViewLightProbes, MAX_VIEW_LIGHT_PROBES,
154154
};
155155

156156
use super::LightProbeComponent;
@@ -300,6 +300,8 @@ impl LightProbeComponent for IrradianceVolume {
300300
// here.
301301
type ViewLightProbeInfo = ();
302302

303+
type QueryData = ();
304+
303305
fn id(&self, image_assets: &RenderAssets<GpuImage>) -> Option<Self::AssetId> {
304306
if image_assets.get(&self.voxels).is_none() {
305307
None
@@ -312,8 +314,12 @@ impl LightProbeComponent for IrradianceVolume {
312314
self.intensity
313315
}
314316

315-
fn affects_lightmapped_mesh_diffuse(&self) -> bool {
316-
self.affects_lightmapped_meshes
317+
fn flags(&self, _: Self::QueryData) -> RenderLightProbeFlags {
318+
if self.affects_lightmapped_meshes {
319+
RenderLightProbeFlags::AFFECTS_LIGHTMAPPED_MESH_DIFFUSE
320+
} else {
321+
RenderLightProbeFlags::empty()
322+
}
317323
}
318324

319325
fn create_render_view_light_probes(

crates/bevy_pbr/src/light_probe/irradiance_volume.wgsl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ fn irradiance_volume_light(
3434
// If we're lightmapped, and the irradiance volume contributes no diffuse
3535
// light, then bail out.
3636
#ifdef LIGHTMAP
37-
if (!query_result.affects_lightmapped_mesh_diffuse) {
37+
if ((query_result.flags & LIGHT_PROBE_FLAG_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE) == 0u) {
3838
return vec3(0.0f);
3939
}
4040
#endif // LIGHTMAP

crates/bevy_pbr/src/light_probe/light_probe.wgsl

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
#import bevy_pbr::clustered_forward
44
#import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges
55
#import bevy_pbr::mesh_view_bindings::light_probes
6-
#import bevy_pbr::mesh_view_types::LightProbe
6+
#import bevy_pbr::mesh_view_types::{
7+
LightProbe, LIGHT_PROBE_FLAG_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE,
8+
LIGHT_PROBE_FLAG_PARALLAX_CORRECT
9+
}
710

811
// The result of searching for a light probe.
912
struct LightProbeQueryResult {
@@ -16,8 +19,9 @@ struct LightProbeQueryResult {
1619
// Transform from world space to the light probe model space. In light probe
1720
// model space, the light probe is a 1×1×1 cube centered on the origin.
1821
light_from_world: mat4x4<f32>,
19-
// Whether this light probe contributes diffuse light to lightmapped meshes.
20-
affects_lightmapped_mesh_diffuse: bool,
22+
// The flags that the light probe has: a combination of
23+
// `LIGHT_PROBE_FLAG_*`.
24+
flags: u32,
2125
};
2226

2327
fn transpose_affine_matrix(matrix: mat3x4<f32>) -> mat4x4<f32> {
@@ -82,8 +86,7 @@ fn query_light_probe(
8286
result.texture_index = light_probe.cubemap_index;
8387
result.intensity = light_probe.intensity;
8488
result.light_from_world = light_from_world;
85-
result.affects_lightmapped_mesh_diffuse =
86-
light_probe.affects_lightmapped_mesh_diffuse != 0u;
89+
result.flags = light_probe.flags;
8790
break;
8891
}
8992
}
@@ -136,8 +139,7 @@ fn query_light_probe(
136139
result.texture_index = light_probe.cubemap_index;
137140
result.intensity = light_probe.intensity;
138141
result.light_from_world = light_from_world;
139-
result.affects_lightmapped_mesh_diffuse =
140-
light_probe.affects_lightmapped_mesh_diffuse != 0u;
142+
result.flags = light_probe.flags;
141143

142144
// TODO: Workaround for ICE in DXC https://github.com/microsoft/DirectXShaderCompiler/issues/6183
143145
// We can't use `break` here because of the ICE.

0 commit comments

Comments
 (0)