Skip to content

Commit

Permalink
Add support for environment map transformation (bevyengine#14290)
Browse files Browse the repository at this point in the history
# Objective

- Fixes: bevyengine#14036

## Solution

- Add a world space transformation for the environment sample direction.

## Testing

- I have tested the newly added `transform` field using the newly added
`rotate_environment_map` example.


https://github.com/user-attachments/assets/2de77c65-14bc-48ee-b76a-fb4e9782dbdb


## Migration Guide

- Since we have added a new filed to the `EnvironmentMapLight` struct,
users will need to include `..default()` or some rotation value in their
initialization code.
  • Loading branch information
Soulghost authored Jul 19, 2024
1 parent d8d49fd commit 9da18cc
Show file tree
Hide file tree
Showing 35 changed files with 373 additions and 63 deletions.
12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3315,6 +3315,18 @@ description = "Demonstrates the built-in postprocessing features"
category = "3D Rendering"
wasm = true

[[example]]
name = "rotate_environment_map"
path = "examples/3d/rotate_environment_map.rs"
doc-scrape-examples = true
required-features = ["pbr_multi_layer_material_textures"]

[package.metadata.example.rotate_environment_map]
name = "Rotate Environment Map"
description = "Demonstrates how to rotate the skybox and the environment map simultaneously"
category = "3D Rendering"
wasm = false

[profile.wasm-release]
inherits = "release"
opt-level = "z"
Expand Down
9 changes: 6 additions & 3 deletions crates/bevy_pbr/src/deferred/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{
graph::NodePbr, irradiance_volume::IrradianceVolume, prelude::EnvironmentMapLight,
MeshPipeline, MeshViewBindGroup, RenderViewLightProbes, ScreenSpaceAmbientOcclusionSettings,
ScreenSpaceReflectionsUniform, ViewLightProbesUniformOffset,
ScreenSpaceReflectionsUniform, ViewEnvironmentMapUniformOffset, ViewLightProbesUniformOffset,
ViewScreenSpaceReflectionsUniformOffset,
};
use bevy_app::prelude::*;
Expand Down Expand Up @@ -149,6 +149,7 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode {
&'static ViewFogUniformOffset,
&'static ViewLightProbesUniformOffset,
&'static ViewScreenSpaceReflectionsUniformOffset,
&'static ViewEnvironmentMapUniformOffset,
&'static MeshViewBindGroup,
&'static ViewTarget,
&'static DeferredLightingIdDepthTexture,
Expand All @@ -165,6 +166,7 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode {
view_fog_offset,
view_light_probes_offset,
view_ssr_offset,
view_environment_map_offset,
mesh_view_bind_group,
target,
deferred_lighting_id_depth_texture,
Expand Down Expand Up @@ -220,6 +222,7 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode {
view_fog_offset.offset,
**view_light_probes_offset,
**view_ssr_offset,
**view_environment_map_offset,
],
);
render_pass.set_bind_group(1, &bind_group_1, &[]);
Expand Down Expand Up @@ -256,11 +259,11 @@ impl SpecializedRenderPipeline for DeferredLightingLayout {
shader_defs.push("TONEMAP_IN_SHADER".into());
shader_defs.push(ShaderDefVal::UInt(
"TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(),
20,
21,
));
shader_defs.push(ShaderDefVal::UInt(
"TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(),
21,
22,
));

let method = key.intersection(MeshPipelineKey::TONEMAP_METHOD_RESERVED_BITS);
Expand Down
27 changes: 24 additions & 3 deletions crates/bevy_pbr/src/light_probe/environment_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,15 @@ use bevy_asset::{AssetId, Handle};
use bevy_ecs::{
bundle::Bundle, component::Component, query::QueryItem, system::lifetimeless::Read,
};
use bevy_math::Quat;
use bevy_reflect::Reflect;
use bevy_render::{
extract_instances::ExtractInstance,
prelude::SpatialBundle,
render_asset::RenderAssets,
render_resource::{
binding_types, BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, Shader,
binding_types::{self, uniform_buffer},
BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, Shader, ShaderStages,
TextureSampleType, TextureView,
},
renderer::RenderDevice,
Expand All @@ -67,7 +69,8 @@ use std::num::NonZeroU32;
use std::ops::Deref;

use crate::{
add_cubemap_texture_view, binding_arrays_are_usable, LightProbe, MAX_VIEW_LIGHT_PROBES,
add_cubemap_texture_view, binding_arrays_are_usable, EnvironmentMapUniform, LightProbe,
MAX_VIEW_LIGHT_PROBES,
};

use super::{LightProbeComponent, RenderViewLightProbes};
Expand Down Expand Up @@ -96,6 +99,22 @@ pub struct EnvironmentMapLight {
///
/// See also <https://google.github.io/filament/Filament.html#lighting/imagebasedlights/iblunit>.
pub intensity: f32,

/// World space rotation applied to the environment light cubemaps.
/// This is useful for users who require a different axis, such as the Z-axis, to serve
/// as the vertical axis.
pub rotation: Quat,
}

impl Default for EnvironmentMapLight {
fn default() -> Self {
EnvironmentMapLight {
diffuse_map: Handle::default(),
specular_map: Handle::default(),
intensity: 0.0,
rotation: Quat::IDENTITY,
}
}
}

/// Like [`EnvironmentMapLight`], but contains asset IDs instead of handles.
Expand Down Expand Up @@ -193,7 +212,7 @@ impl ExtractInstance for EnvironmentMapIds {
/// specular binding arrays respectively, in addition to the sampler.
pub(crate) fn get_bind_group_layout_entries(
render_device: &RenderDevice,
) -> [BindGroupLayoutEntryBuilder; 3] {
) -> [BindGroupLayoutEntryBuilder; 4] {
let mut texture_cube_binding =
binding_types::texture_cube(TextureSampleType::Float { filterable: true });
if binding_arrays_are_usable(render_device) {
Expand All @@ -205,6 +224,7 @@ pub(crate) fn get_bind_group_layout_entries(
texture_cube_binding,
texture_cube_binding,
binding_types::sampler(SamplerBindingType::Filtering),
uniform_buffer::<EnvironmentMapUniform>(true).visibility(ShaderStages::FRAGMENT),
]
}

Expand Down Expand Up @@ -312,6 +332,7 @@ impl LightProbeComponent for EnvironmentMapLight {
diffuse_map: diffuse_map_handle,
specular_map: specular_map_handle,
intensity,
..
}) = view_component
{
if let (Some(_), Some(specular_map)) = (
Expand Down
33 changes: 29 additions & 4 deletions crates/bevy_pbr/src/light_probe/environment_map.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#import bevy_pbr::light_probe::query_light_probe
#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::lighting::{
F_Schlick_vec, LayerLightingInput, LightingInput, LAYER_BASE, LAYER_CLEARCOAT
}
Expand Down Expand Up @@ -57,17 +58,29 @@ fn compute_radiances(
bindings::specular_environment_maps[query_result.texture_index]) - 1u);

if (!found_diffuse_indirect) {
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;
radiances.irradiance = textureSampleLevel(
bindings::diffuse_environment_maps[query_result.texture_index],
bindings::environment_map_sampler,
vec3(N.xy, -N.z),
irradiance_sample_dir,
0.0).rgb * query_result.intensity;
}

var radiance_sample_dir = R;
// 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;
radiances.radiance = textureSampleLevel(
bindings::specular_environment_maps[query_result.texture_index],
bindings::environment_map_sampler,
vec3(R.xy, -R.z),
radiance_sample_dir,
radiance_level).rgb * query_result.intensity;

return radiances;
Expand Down Expand Up @@ -102,17 +115,29 @@ fn compute_radiances(
let intensity = light_probes.intensity_for_view;

if (!found_diffuse_indirect) {
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;
radiances.irradiance = textureSampleLevel(
bindings::diffuse_environment_map,
bindings::environment_map_sampler,
vec3(N.xy, -N.z),
irradiance_sample_dir,
0.0).rgb * intensity;
}

var radiance_sample_dir = R;
// 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;
radiances.radiance = textureSampleLevel(
bindings::specular_environment_map,
bindings::environment_map_sampler,
vec3(R.xy, -R.z),
radiance_sample_dir,
radiance_level).rgb * intensity;

return radiances;
Expand Down
81 changes: 79 additions & 2 deletions crates/bevy_pbr/src/light_probe/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use bevy_render::{
view::ExtractedView,
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
};
use bevy_transform::prelude::GlobalTransform;
use bevy_transform::{components::Transform, prelude::GlobalTransform};
use bevy_utils::{tracing::error, HashMap};

use std::hash::Hash;
Expand Down Expand Up @@ -296,6 +296,31 @@ impl LightProbe {
}
}

/// The uniform struct extracted from [`EnvironmentMapLight`].
/// Will be available for use in the Environment Map shader.
#[derive(Component, ShaderType, Clone)]
pub struct EnvironmentMapUniform {
/// The world space transformation matrix of the sample ray for environment cubemaps.
transform: Mat4,
}

impl Default for EnvironmentMapUniform {
fn default() -> Self {
EnvironmentMapUniform {
transform: Mat4::IDENTITY,
}
}
}

/// A GPU buffer that stores the environment map settings for each view.
#[derive(Resource, Default, Deref, DerefMut)]
pub struct EnvironmentMapUniformBuffer(pub DynamicUniformBuffer<EnvironmentMapUniform>);

/// A component that stores the offset within the
/// [`EnvironmentMapUniformBuffer`] for each view.
#[derive(Component, Default, Deref, DerefMut)]
pub struct ViewEnvironmentMapUniformOffset(u32);

impl Plugin for LightProbePlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
Expand Down Expand Up @@ -330,15 +355,41 @@ impl Plugin for LightProbePlugin {
render_app
.add_plugins(ExtractInstancesPlugin::<EnvironmentMapIds>::new())
.init_resource::<LightProbesBuffer>()
.init_resource::<EnvironmentMapUniformBuffer>()
.add_systems(ExtractSchedule, gather_environment_map_uniform)
.add_systems(ExtractSchedule, gather_light_probes::<EnvironmentMapLight>)
.add_systems(ExtractSchedule, gather_light_probes::<IrradianceVolume>)
.add_systems(
Render,
upload_light_probes.in_set(RenderSet::PrepareResources),
(upload_light_probes, prepare_environment_uniform_buffer)
.in_set(RenderSet::PrepareResources),
);
}
}

/// Extracts [`EnvironmentMapLight`] from views and creates [`EnvironmentMapUniform`] for them.
/// Compared to the `ExtractComponentPlugin`, this implementation will create a default instance
/// if one does not already exist.
fn gather_environment_map_uniform(
view_query: Extract<Query<(Entity, Option<&EnvironmentMapLight>), With<Camera3d>>>,
mut commands: Commands,
) {
for (view_entity, environment_map_light) in view_query.iter() {
let environment_map_uniform = if let Some(environment_map_light) = environment_map_light {
EnvironmentMapUniform {
transform: Transform::from_rotation(environment_map_light.rotation)
.compute_matrix()
.inverse(),
}
} else {
EnvironmentMapUniform::default()
};
commands
.get_or_spawn(view_entity)
.insert(environment_map_uniform);
}
}

/// Gathers up all light probes of a single type in the scene and assigns them
/// to views, performing frustum culling and distance sorting in the process.
fn gather_light_probes<C>(
Expand Down Expand Up @@ -395,6 +446,32 @@ fn gather_light_probes<C>(
}
}

/// Gathers up environment map settings for each applicable view and
/// writes them into a GPU buffer.
pub fn prepare_environment_uniform_buffer(
mut commands: Commands,
views: Query<(Entity, Option<&EnvironmentMapUniform>), With<ExtractedView>>,
mut environment_uniform_buffer: ResMut<EnvironmentMapUniformBuffer>,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
) {
let Some(mut writer) =
environment_uniform_buffer.get_writer(views.iter().len(), &render_device, &render_queue)
else {
return;
};

for (view, environment_uniform) in views.iter() {
let uniform_offset = match environment_uniform {
None => 0,
Some(environment_uniform) => writer.write(environment_uniform),
};
commands
.entity(view)
.insert(ViewEnvironmentMapUniformOffset(uniform_offset));
}
}

// A system that runs after [`gather_light_probes`] and populates the GPU
// uniforms with the results.
//
Expand Down
7 changes: 5 additions & 2 deletions crates/bevy_pbr/src/meshlet/material_draw_nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use super::{
MeshletGpuScene,
};
use crate::{
MeshViewBindGroup, PrepassViewBindGroup, ViewFogUniformOffset, ViewLightProbesUniformOffset,
ViewLightsUniformOffset, ViewScreenSpaceReflectionsUniformOffset,
MeshViewBindGroup, PrepassViewBindGroup, ViewEnvironmentMapUniformOffset, ViewFogUniformOffset,
ViewLightProbesUniformOffset, ViewLightsUniformOffset, ViewScreenSpaceReflectionsUniformOffset,
};
use bevy_core_pipeline::prepass::{
MotionVectorPrepass, PreviousViewUniformOffset, ViewPrepassTextures,
Expand Down Expand Up @@ -41,6 +41,7 @@ impl ViewNode for MeshletMainOpaquePass3dNode {
&'static ViewFogUniformOffset,
&'static ViewLightProbesUniformOffset,
&'static ViewScreenSpaceReflectionsUniformOffset,
&'static ViewEnvironmentMapUniformOffset,
&'static MeshletViewMaterialsMainOpaquePass,
&'static MeshletViewBindGroups,
&'static MeshletViewResources,
Expand All @@ -59,6 +60,7 @@ impl ViewNode for MeshletMainOpaquePass3dNode {
view_fog_offset,
view_light_probes_offset,
view_ssr_offset,
view_environment_map_offset,
meshlet_view_materials,
meshlet_view_bind_groups,
meshlet_view_resources,
Expand Down Expand Up @@ -111,6 +113,7 @@ impl ViewNode for MeshletMainOpaquePass3dNode {
view_fog_offset.offset,
**view_light_probes_offset,
**view_ssr_offset,
**view_environment_map_offset,
],
);
render_pass.set_bind_group(1, meshlet_material_draw_bind_group, &[]);
Expand Down
Loading

0 comments on commit 9da18cc

Please sign in to comment.