Skip to content

Commit

Permalink
Implement opt-in sharp screen-space reflections for the deferred
Browse files Browse the repository at this point in the history
renderer.

This commit implements *screen-space reflections* (SSR), which
approximate real-time reflections based on raymarching through the depth
buffer and copying samples from the final rendered frame. Numerous
variations and refinements to screen-space reflections exist in the
literature. This patch foregoes all of them in favor of implementing the
bare minimum, so as to provide a flexible base on which to customize and
build in the future.

For a general basic overview of screen-space reflections, see [1]. The
raymarching shader uses the basic algorithm of tracing forward in large
steps (what I call a *major* trace), and then refining that trace in
smaller increments via binary search (what I call a *minor* trace). No
filtering, whether temporal or spatial, is performed at all; for this
reason, SSR currently only operates on very shiny surfaces. No
acceleration via the hierarchical Z-buffer is implemented (though note
that #12899 will add the infrastructure for this). Reflections are
traced at full resolution, which is often considered slow. All of these
improvements and more can be follow-ups.

SSR is built on top of the deferred renderer and is currently only
supported in that mode. Forward screen-space reflections are possible
albeit uncommon (though e.g. *Doom Eternal* uses them); however, they
require tracing from the previous frame, which would add complexity.
This patch leaves the door open to implementing SSR in the forward
rendering path but doesn't itself have such an implementation.
Screen-space reflections *are* supported in WebGL 2.

To add screen-space reflections to a camera, use the
`ScreenSpaceReflections` component. `DepthPrepass` and `DeferredPrepass`
must also be present for the reflections to show up. The
`ScreenSpaceReflections` component contains several settings that
artists can tweak, and also comes with sensible defaults.

A new example, `ssr`, has been added. It's loosely based on the
[three.js ocean sample], but all the assets are original. Note that the
three.js demo has no screen-space reflections and instead renders a
mirror world.

Additionally, this patch fixes a random bug I ran across: that the
`"TONEMAP_METHOD_ACES_FITTED"` `#define` is incorrectly supplied to the
shader as `"TONEMAP_METHOD_ACES_FITTED "` (with an extra space) in some
paths.

[1]: https://lettier.github.io/3d-game-shaders-for-beginners/screen-space-reflection.html

[three.js ocean sample]: https://threejs.org/examples/webgl_shaders_ocean.html
  • Loading branch information
pcwalton committed Apr 14, 2024
1 parent a362c27 commit f44dfb3
Show file tree
Hide file tree
Showing 20 changed files with 1,305 additions and 131 deletions.
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2871,6 +2871,16 @@ description = "Demonstrates FPS overlay"
category = "Dev tools"
wasm = true

[[example]]
name = "ssr"
path = "examples/3d/ssr.rs"

[package.metadata.example.ssr]
name = "Screen Space Reflections"
description = "Demonstrates screen space reflections"
category = "3D Rendering"
wasm = true

[profile.wasm-release]
inherits = "release"
opt-level = "z"
Expand Down
59 changes: 59 additions & 0 deletions assets/shaders/water_material.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// A shader that creates water ripples by overlaying 4 normal maps on top of one
// another.
//
// This is used in the `ssr` example. It only supports deferred rendering.

#import bevy_pbr::{
pbr_deferred_functions::deferred_output,
pbr_fragment::pbr_input_from_standard_material,
prepass_io::{VertexOutput, FragmentOutput},
}
#import bevy_render::globals::Globals

// Parameters to the water shader.
struct WaterSettings {
// How much to displace each octave each frame, in the u and v directions.
// Two octaves are packed into each `vec4`.
octave_vectors: array<vec4<f32>, 2>,
// How wide the waves are in each octave.
octave_scales: vec4<f32>,
// How high the waves are in each octave.
octave_strengths: vec4<f32>,
}

@group(0) @binding(1) var<uniform> globals: Globals;

@group(2) @binding(100) var water_normals_texture: texture_2d<f32>;
@group(2) @binding(101) var water_normals_sampler: sampler;
@group(2) @binding(102) var<uniform> water_settings: WaterSettings;

// Samples a single octave of noise and returns the resulting normal.
fn sample_noise_octave(uv: vec2<f32>, strength: f32) -> vec3<f32> {
let N = textureSample(water_normals_texture, water_normals_sampler, uv).rgb * 2.0 - 1.0;
// This isn't slerp, but it's good enough.
return normalize(mix(vec3(0.0, 1.0, 0.0), N, strength));
}

// Samples all four octaves of noise and returns the resulting normal.
fn sample_noise(uv: vec2<f32>, time: f32) -> vec3<f32> {
let uv0 = uv * water_settings.octave_scales[0] + water_settings.octave_vectors[0].xy * time;
let uv1 = uv * water_settings.octave_scales[1] + water_settings.octave_vectors[0].zw * time;
let uv2 = uv * water_settings.octave_scales[2] + water_settings.octave_vectors[1].xy * time;
let uv3 = uv * water_settings.octave_scales[3] + water_settings.octave_vectors[1].zw * time;
return normalize(
sample_noise_octave(uv0, water_settings.octave_strengths[0]) +
sample_noise_octave(uv1, water_settings.octave_strengths[1]) +
sample_noise_octave(uv2, water_settings.octave_strengths[2]) +
sample_noise_octave(uv3, water_settings.octave_strengths[3])
);
}

@fragment
fn fragment(in: VertexOutput, @builtin(front_facing) is_front: bool) -> FragmentOutput {
// Create the PBR input.
var pbr_input = pbr_input_from_standard_material(in, is_front);
// Bump the normal.
pbr_input.N = sample_noise(in.uv, globals.time);
// Send the rest to the deferred shader.
return deferred_output(in, pbr_input);
}
Binary file added assets/textures/water_normals.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
@group(0) @binding(3) var dt_lut_texture: texture_3d<f32>;
@group(0) @binding(4) var dt_lut_sampler: sampler;
#else
@group(0) @binding(18) var dt_lut_texture: texture_3d<f32>;
@group(0) @binding(19) var dt_lut_sampler: sampler;
@group(0) @binding(19) var dt_lut_texture: texture_3d<f32>;
@group(0) @binding(20) var dt_lut_sampler: sampler;
#endif

fn sample_current_lut(p: vec3<f32>) -> vec3<f32> {
Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_gizmos/src/pipeline_3d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ impl SpecializedRenderPipeline for LineGizmoPipeline {

let view_layout = self
.mesh_pipeline
.view_layouts
.get_view_layout(key.view_key.into())
.clone();

Expand Down Expand Up @@ -206,6 +207,7 @@ impl SpecializedRenderPipeline for LineJointGizmoPipeline {

let view_layout = self
.mesh_pipeline
.view_layouts
.get_view_layout(key.view_key.into())
.clone();

Expand Down
26 changes: 21 additions & 5 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,
ViewLightProbesUniformOffset,
ScreenSpaceReflections, ViewLightProbesUniformOffset, ViewScreenSpaceReflectionsUniformOffset,
};
use bevy_app::prelude::*;
use bevy_asset::{load_internal_asset, Handle};
Expand Down Expand Up @@ -147,6 +147,7 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode {
&'static ViewLightsUniformOffset,
&'static ViewFogUniformOffset,
&'static ViewLightProbesUniformOffset,
&'static ViewScreenSpaceReflectionsUniformOffset,
&'static MeshViewBindGroup,
&'static ViewTarget,
&'static DeferredLightingIdDepthTexture,
Expand All @@ -162,6 +163,7 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode {
view_lights_offset,
view_fog_offset,
view_light_probes_offset,
view_ssr_offset,
mesh_view_bind_group,
target,
deferred_lighting_id_depth_texture,
Expand Down Expand Up @@ -216,6 +218,7 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode {
view_lights_offset.offset,
view_fog_offset.offset,
**view_light_probes_offset,
**view_ssr_offset,
],
);
render_pass.set_bind_group(1, &bind_group_1, &[]);
Expand Down Expand Up @@ -260,7 +263,7 @@ impl SpecializedRenderPipeline for DeferredLightingLayout {
} else if method == MeshPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE {
shader_defs.push("TONEMAP_METHOD_REINHARD_LUMINANCE".into());
} else if method == MeshPipelineKey::TONEMAP_METHOD_ACES_FITTED {
shader_defs.push("TONEMAP_METHOD_ACES_FITTED ".into());
shader_defs.push("TONEMAP_METHOD_ACES_FITTED".into());
} else if method == MeshPipelineKey::TONEMAP_METHOD_AGX {
shader_defs.push("TONEMAP_METHOD_AGX".into());
} else if method == MeshPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM {
Expand Down Expand Up @@ -301,6 +304,10 @@ impl SpecializedRenderPipeline for DeferredLightingLayout {
shader_defs.push("MOTION_VECTOR_PREPASS".into());
}

if key.contains(MeshPipelineKey::SSR) {
shader_defs.push("SSR".into());
}

// Always true, since we're in the deferred lighting pipeline
shader_defs.push("DEFERRED_PREPASS".into());

Expand All @@ -320,7 +327,10 @@ impl SpecializedRenderPipeline for DeferredLightingLayout {
RenderPipelineDescriptor {
label: Some("deferred_lighting_pipeline".into()),
layout: vec![
self.mesh_pipeline.get_view_layout(key.into()).clone(),
self.mesh_pipeline
.view_layouts
.get_view_layout(key.into())
.clone(),
self.bind_group_layout_1.clone(),
],
vertex: VertexState {
Expand Down Expand Up @@ -406,7 +416,10 @@ pub fn prepare_deferred_lighting_pipelines(
Option<&Tonemapping>,
Option<&DebandDither>,
Option<&ShadowFilteringMethod>,
Has<ScreenSpaceAmbientOcclusionSettings>,
(
Has<ScreenSpaceAmbientOcclusionSettings>,
Has<ScreenSpaceReflections>,
),
(
Has<NormalPrepass>,
Has<DepthPrepass>,
Expand All @@ -424,7 +437,7 @@ pub fn prepare_deferred_lighting_pipelines(
tonemapping,
dither,
shadow_filter_method,
ssao,
(ssao, ssr),
(normal_prepass, depth_prepass, motion_vector_prepass),
has_environment_maps,
has_irradiance_volumes,
Expand Down Expand Up @@ -473,6 +486,9 @@ pub fn prepare_deferred_lighting_pipelines(
if ssao {
view_key |= MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION;
}
if ssr {
view_key |= MeshPipelineKey::SSR;
}

// We don't need to check to see whether the environment map is loaded
// because [`gather_light_probes`] already checked that for us before
Expand Down
5 changes: 5 additions & 0 deletions crates/bevy_pbr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ mod pbr_material;
mod prepass;
mod render;
mod ssao;
mod ssr;

use bevy_color::{Color, LinearRgba};
pub use bundle::*;
Expand All @@ -48,6 +49,7 @@ pub use pbr_material::*;
pub use prepass::*;
pub use render::*;
pub use ssao::*;
pub use ssr::*;

pub mod prelude {
#[doc(hidden)]
Expand Down Expand Up @@ -81,6 +83,8 @@ pub mod graph {
DeferredLightingPass,
/// Label for the compute shader instance data building pass.
GpuPreprocess,
/// Label for the screen space reflections pass.
ScreenSpaceReflections,
}
}

Expand Down Expand Up @@ -305,6 +309,7 @@ impl Plugin for PbrPlugin {
GpuMeshPreprocessPlugin {
use_gpu_instance_buffer_builder: self.use_gpu_instance_buffer_builder,
},
ScreenSpaceReflectionsPlugin,
))
.configure_sets(
PostUpdate,
Expand Down
5 changes: 4 additions & 1 deletion crates/bevy_pbr/src/meshlet/material_draw_nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use super::{
};
use crate::{
MeshViewBindGroup, PrepassViewBindGroup, PreviousViewUniformOffset, ViewFogUniformOffset,
ViewLightProbesUniformOffset, ViewLightsUniformOffset,
ViewLightProbesUniformOffset, ViewLightsUniformOffset, ViewScreenSpaceReflectionsUniformOffset,
};
use bevy_core_pipeline::prepass::ViewPrepassTextures;
use bevy_ecs::{query::QueryItem, world::World};
Expand All @@ -35,6 +35,7 @@ impl ViewNode for MeshletMainOpaquePass3dNode {
&'static ViewLightsUniformOffset,
&'static ViewFogUniformOffset,
&'static ViewLightProbesUniformOffset,
&'static ViewScreenSpaceReflectionsUniformOffset,
&'static MeshletViewMaterialsMainOpaquePass,
&'static MeshletViewBindGroups,
&'static MeshletViewResources,
Expand All @@ -52,6 +53,7 @@ impl ViewNode for MeshletMainOpaquePass3dNode {
view_lights_offset,
view_fog_offset,
view_light_probes_offset,
view_ssr_offset,
meshlet_view_materials,
meshlet_view_bind_groups,
meshlet_view_resources,
Expand Down Expand Up @@ -103,6 +105,7 @@ impl ViewNode for MeshletMainOpaquePass3dNode {
view_lights_offset.offset,
view_fog_offset.offset,
**view_light_probes_offset,
**view_ssr_offset,
],
);
render_pass.set_bind_group(1, meshlet_material_draw_bind_group, &[]);
Expand Down
5 changes: 4 additions & 1 deletion crates/bevy_pbr/src/meshlet/material_draw_prepare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,10 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass<M: Material>(
let pipeline_descriptor = RenderPipelineDescriptor {
label: material_pipeline_descriptor.label,
layout: vec![
mesh_pipeline.get_view_layout(view_key.into()).clone(),
mesh_pipeline
.view_layouts
.get_view_layout(view_key.into())
.clone(),
gpu_scene.material_draw_bind_group_layout(),
material_pipeline.material_layout.clone(),
],
Expand Down
Loading

0 comments on commit f44dfb3

Please sign in to comment.