diff --git a/Cargo.toml b/Cargo.toml index dd904e1217590..162d89414a4be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4662,6 +4662,7 @@ name = "ssr" path = "examples/3d/ssr.rs" # Causes an ICE on docs.rs doc-scrape-examples = false +required-features = ["bluenoise_texture"] [package.metadata.example.ssr] name = "Screen Space Reflections" diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.rs b/crates/bevy_pbr/src/render/mesh_view_bindings.rs index 78e7f3e6d7dcb..3ce3578b12e2f 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.rs @@ -45,11 +45,12 @@ use crate::{ }, prepass, resources::{AtmosphereBuffer, AtmosphereData, AtmosphereSampler, AtmosphereTextures}, - EnvironmentMapUniformBuffer, ExtractedAtmosphere, FogMeta, GlobalClusterableObjectMeta, - GpuClusterableObjects, GpuFog, GpuLights, LightMeta, LightProbesBuffer, LightProbesUniform, - MeshPipeline, MeshPipelineKey, RenderViewLightProbes, ScreenSpaceAmbientOcclusionResources, - ScreenSpaceReflectionsBuffer, ScreenSpaceReflectionsUniform, ShadowSamplers, - ViewClusterBindings, ViewShadowBindings, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, + Bluenoise, EnvironmentMapUniformBuffer, ExtractedAtmosphere, FogMeta, + GlobalClusterableObjectMeta, GpuClusterableObjects, GpuFog, GpuLights, LightMeta, + LightProbesBuffer, LightProbesUniform, MeshPipeline, MeshPipelineKey, RenderViewLightProbes, + ScreenSpaceAmbientOcclusionResources, ScreenSpaceReflectionsBuffer, + ScreenSpaceReflectionsUniform, ShadowSamplers, ViewClusterBindings, ViewShadowBindings, + CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, }; #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] @@ -84,6 +85,7 @@ bitflags::bitflags! { const DEFERRED_PREPASS = 1 << 4; const OIT_ENABLED = 1 << 5; const ATMOSPHERE = 1 << 6; + const BLUE_NOISE_TEXTURE = 1 << 7; } } @@ -96,7 +98,7 @@ impl MeshPipelineViewLayoutKey { use MeshPipelineViewLayoutKey as Key; format!( - "mesh_view_layout{}{}{}{}{}{}{}", + "mesh_view_layout{}{}{}{}{}{}{}{}", if self.contains(Key::MULTISAMPLED) { "_multisampled" } else { @@ -132,6 +134,11 @@ impl MeshPipelineViewLayoutKey { } else { Default::default() }, + if self.contains(Key::BLUE_NOISE_TEXTURE) { + "_stbn" + } else { + Default::default() + }, ) } } @@ -162,6 +169,10 @@ impl From for MeshPipelineViewLayoutKey { result |= MeshPipelineViewLayoutKey::ATMOSPHERE; } + if cfg!(feature = "bluenoise_texture") { + result |= MeshPipelineViewLayoutKey::BLUE_NOISE_TEXTURE; + } + result } } @@ -410,6 +421,14 @@ fn layout_entries( )); } + // Blue noise + if layout_key.contains(MeshPipelineViewLayoutKey::BLUE_NOISE_TEXTURE) { + entries = entries.extend_with_indices((( + 33, + texture_2d_array(TextureSampleType::Float { filterable: false }), + ),)); + } + let mut binding_array_entries = DynamicBindGroupLayoutEntries::new(ShaderStages::FRAGMENT); binding_array_entries = binding_array_entries.extend_with_indices(( (0, environment_map_entries[0]), @@ -599,11 +618,12 @@ pub fn prepare_mesh_view_bind_groups( visibility_ranges: Res, ssr_buffer: Res, oit_buffers: Res, - (decals_buffer, render_decals, atmosphere_buffer, atmosphere_sampler): ( + (decals_buffer, render_decals, atmosphere_buffer, atmosphere_sampler, blue_noise): ( Res, Res, Option>, Option>, + Res, ), ) { if let ( @@ -659,6 +679,9 @@ pub fn prepare_mesh_view_bind_groups( if has_atmosphere { layout_key |= MeshPipelineViewLayoutKey::ATMOSPHERE; } + if cfg!(feature = "bluenoise_texture") { + layout_key |= MeshPipelineViewLayoutKey::BLUE_NOISE_TEXTURE; + } let layout = mesh_pipeline.get_view_layout(layout_key); @@ -752,6 +775,14 @@ pub fn prepare_mesh_view_bind_groups( )); } + if layout_key.contains(MeshPipelineViewLayoutKey::BLUE_NOISE_TEXTURE) { + let stbn_view = &images + .get(&blue_noise.texture) + .expect("STBN texture is added unconditionally with at least a placeholder") + .texture_view; + entries = entries.extend_with_indices(((33, stbn_view),)); + } + let mut entries_binding_array = DynamicBindGroupEntries::new(); let environment_map_bind_group_entries = RenderViewEnvironmentMapBindGroupEntries::get( diff --git a/crates/bevy_pbr/src/render/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index 112556ee1eb76..0c2a027f6b4e5 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -155,7 +155,12 @@ struct LightProbes { // For more information on these settings, see the documentation for // `bevy_pbr::ssr::ScreenSpaceReflections`. struct ScreenSpaceReflectionsSettings { - perceptual_roughness_threshold: f32, + min_perceptual_roughness: f32, + min_perceptual_roughness_fully_active: f32, + max_perceptual_roughness_starts_to_fade: f32, + max_perceptual_roughness: f32, + edge_fadeout_fully_active: f32, + edge_fadeout_no_longer_active: f32, thickness: f32, linear_steps: u32, linear_march_exponent: f32, diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index c53820ab0ba0a..c74eb0dfca5ba 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -607,32 +607,34 @@ fn apply_pbr_lighting( // Environment map light (indirect) #ifdef ENVIRONMENT_MAP - // If screen space reflections are going to be used for this material, don't - // accumulate environment map light yet. The SSR shader will do it. + // If screen space reflections are going to be used for this material, only + // accumulate the diffuse part of the environment map light. The SSR shader + // will accumulate the specular part (including the environment map fallback + // if SSR misses). #ifdef SCREEN_SPACE_REFLECTIONS - let use_ssr = perceptual_roughness <= - view_bindings::ssr_settings.perceptual_roughness_threshold; + let use_ssr = perceptual_roughness <= view_bindings::ssr_settings.max_perceptual_roughness + && perceptual_roughness >= view_bindings::ssr_settings.min_perceptual_roughness; #else // SCREEN_SPACE_REFLECTIONS let use_ssr = false; #endif // SCREEN_SPACE_REFLECTIONS - if (!use_ssr) { #ifdef STANDARD_MATERIAL_ANISOTROPY - var bent_normal_lighting_input = lighting_input; - bend_normal_for_anisotropy(&bent_normal_lighting_input); - let environment_map_lighting_input = &bent_normal_lighting_input; + var bent_normal_lighting_input = lighting_input; + bend_normal_for_anisotropy(&bent_normal_lighting_input); + let environment_map_lighting_input = &bent_normal_lighting_input; #else // STANDARD_MATERIAL_ANISOTROPY - let environment_map_lighting_input = &lighting_input; + let environment_map_lighting_input = &lighting_input; #endif // STANDARD_MATERIAL_ANISOTROPY - let environment_light = environment_map::environment_map_light( - environment_map_lighting_input, - &clusterable_object_index_ranges, - found_diffuse_indirect, - ); + let environment_light = environment_map::environment_map_light( + environment_map_lighting_input, + &clusterable_object_index_ranges, + found_diffuse_indirect, + ); - indirect_light += environment_light.diffuse * diffuse_occlusion + - environment_light.specular * specular_occlusion; + indirect_light += environment_light.diffuse * diffuse_occlusion; + if (!use_ssr) { + indirect_light += environment_light.specular * specular_occlusion; } #endif // ENVIRONMENT_MAP diff --git a/crates/bevy_pbr/src/ssr/mod.rs b/crates/bevy_pbr/src/ssr/mod.rs index 7bd9d02c529de..207062c70c526 100644 --- a/crates/bevy_pbr/src/ssr/mod.rs +++ b/crates/bevy_pbr/src/ssr/mod.rs @@ -1,5 +1,7 @@ //! Screen space reflections implemented via raymarching. +use core::ops::Range; + use bevy_app::{App, Plugin}; use bevy_asset::{load_embedded_asset, AssetServer, Handle}; use bevy_core_pipeline::{ @@ -27,6 +29,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ diagnostic::RecordDiagnostics, extract_component::{ExtractComponent, ExtractComponentPlugin}, + render_asset::RenderAssets, render_graph::{ NodeRunError, RenderGraph, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner, }, @@ -36,9 +39,11 @@ use bevy_render::{ DynamicUniformBuffer, FilterMode, FragmentState, Operations, PipelineCache, RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages, ShaderType, SpecializedRenderPipeline, - SpecializedRenderPipelines, TextureFormat, TextureSampleType, + SpecializedRenderPipelines, TextureFormat, TextureSampleType, TextureViewDescriptor, + TextureViewDimension, }, renderer::{RenderAdapter, RenderContext, RenderDevice, RenderQueue}, + texture::GpuImage, view::{ExtractedView, Msaa, ViewTarget, ViewUniformOffset}, Render, RenderApp, RenderStartup, RenderSystems, }; @@ -47,8 +52,8 @@ use bevy_utils::{once, prelude::default}; use tracing::info; use crate::{ - binding_arrays_are_usable, graph::NodePbr, ExtractedAtmosphere, MeshPipelineViewLayoutKey, - MeshPipelineViewLayouts, MeshViewBindGroup, RenderViewLightProbes, + binding_arrays_are_usable, graph::NodePbr, Bluenoise, ExtractedAtmosphere, + MeshPipelineViewLayoutKey, MeshPipelineViewLayouts, MeshViewBindGroup, RenderViewLightProbes, ViewEnvironmentMapUniformOffset, ViewFogUniformOffset, ViewLightProbesUniformOffset, ViewLightsUniformOffset, }; @@ -65,10 +70,7 @@ pub struct ScreenSpaceReflectionsPlugin; /// components, which are inserted automatically, /// but deferred rendering itself is not automatically enabled. /// -/// SSR currently performs no roughness filtering for glossy reflections, so -/// only very smooth surfaces will reflect objects in screen space. You can -/// adjust the `perceptual_roughness_threshold` in order to tune the threshold -/// below which screen-space reflections will be traced. +/// Enable the `bluenoise_texture` feature to improve the quality of noise on rough reflections. /// /// As with all screen-space techniques, SSR can only reflect objects on screen. /// When objects leave the camera, they will disappear from reflections. @@ -82,14 +84,22 @@ pub struct ScreenSpaceReflectionsPlugin; /// Screen-space reflections are presently unsupported on WebGL 2 because of a /// bug whereby Naga doesn't generate correct GLSL when sampling depth buffers, /// which is required for screen-space raymarching. -#[derive(Clone, Copy, Component, Reflect)] +#[derive(Clone, Component, Reflect)] #[reflect(Component, Default, Clone)] #[require(DepthPrepass, DeferredPrepass)] #[doc(alias = "Ssr")] pub struct ScreenSpaceReflections { - /// The maximum PBR roughness level that will enable screen space - /// reflections. - pub perceptual_roughness_threshold: f32, + /// The perceptual roughness range over which SSR begins to fade in. + /// + /// The first value is the roughness at which SSR begins to appear; the + /// second value is the roughness at which SSR is fully active. + pub min_perceptual_roughness: Range, + + /// The perceptual roughness range over which SSR begins to fade out. + /// + /// The first value is the roughness at which SSR begins to fade out; the + /// second value is the roughness at which SSR is no longer active. + pub max_perceptual_roughness: Range, /// When marching the depth buffer, we only have 2.5D information and don't /// know how thick surfaces are. We shall assume that the depth buffer @@ -115,6 +125,14 @@ pub struct ScreenSpaceReflections { /// as 1 or 2. pub linear_march_exponent: f32, + /// The range over which SSR begins to fade out at the edges of the screen, + /// in terms of a percentage of the screen dimensions. + /// + /// The first value is the percentage from the edge at which SSR is no + /// longer active; the second value is the percentage at which SSR is fully + /// active. + pub edge_fadeout: Range, + /// Number of steps in a bisection (binary search) to perform once the /// linear search has found an intersection. Helps narrow down the hit, /// increasing the chance of the secant method finding an accurate hit @@ -133,7 +151,12 @@ pub struct ScreenSpaceReflections { /// [`ScreenSpaceReflections`]. #[derive(Clone, Copy, Component, ShaderType)] pub struct ScreenSpaceReflectionsUniform { - perceptual_roughness_threshold: f32, + min_perceptual_roughness: f32, + min_perceptual_roughness_fully_active: f32, + max_perceptual_roughness_starts_to_fade: f32, + max_perceptual_roughness: f32, + edge_fadeout_fully_active: f32, + edge_fadeout_no_longer_active: f32, thickness: f32, linear_steps: u32, linear_march_exponent: f32, @@ -240,12 +263,14 @@ impl Default for ScreenSpaceReflections { // . fn default() -> Self { Self { - perceptual_roughness_threshold: 0.1, - linear_steps: 16, - bisection_steps: 4, + min_perceptual_roughness: 0.08..0.12, + max_perceptual_roughness: 0.55..0.6, + linear_steps: 10, + bisection_steps: 5, use_secant: true, thickness: 0.25, linear_march_exponent: 1.0, + edge_fadeout: 0.0..0.0, } } } @@ -293,6 +318,17 @@ impl ViewNode for ScreenSpaceReflectionsNode { // Create the bind group for this view. let ssr_pipeline = world.resource::(); + let bluenoise = world.resource::(); + let render_images = world.resource::>(); + let Some(stbn_texture) = render_images.get(&bluenoise.texture) else { + return Ok(()); + }; + let stbn_view = stbn_texture.texture.create_view(&TextureViewDescriptor { + label: Some("ssr_stbn_view"), + dimension: Some(TextureViewDimension::D2Array), + ..default() + }); + let ssr_bind_group = render_context.render_device().create_bind_group( "SSR bind group", &pipeline_cache.get_bind_group_layout(&ssr_pipeline.bind_group_layout), @@ -301,6 +337,7 @@ impl ViewNode for ScreenSpaceReflectionsNode { &ssr_pipeline.color_sampler, &ssr_pipeline.depth_linear_sampler, &ssr_pipeline.depth_nearest_sampler, + &stbn_view, )), ); @@ -363,6 +400,7 @@ pub fn init_screen_space_reflections_pipeline( binding_types::sampler(SamplerBindingType::Filtering), binding_types::sampler(SamplerBindingType::Filtering), binding_types::sampler(SamplerBindingType::NonFiltering), + binding_types::texture_2d_array(TextureSampleType::Float { filterable: false }), ), ), ); @@ -455,6 +493,9 @@ pub fn prepare_ssr_pipelines( has_motion_vector_prepass, ); mesh_pipeline_view_key.set(MeshPipelineViewLayoutKey::ATMOSPHERE, has_atmosphere); + if cfg!(feature = "bluenoise_texture") { + mesh_pipeline_view_key |= MeshPipelineViewLayoutKey::BLUE_NOISE_TEXTURE; + } // Build the pipeline. let pipeline_id = pipelines.specialize( @@ -517,7 +558,7 @@ impl ExtractComponent for ScreenSpaceReflections { return None; } - Some((*settings).into()) + Some(settings.clone().into()) } } @@ -552,6 +593,10 @@ impl SpecializedRenderPipeline for ScreenSpaceReflectionsPipeline { shader_defs.push("ATMOSPHERE".into()); } + if cfg!(feature = "bluenoise_texture") { + shader_defs.push("BLUE_NOISE_TEXTURE".into()); + } + #[cfg(not(target_arch = "wasm32"))] shader_defs.push("USE_DEPTH_SAMPLERS".into()); @@ -581,7 +626,12 @@ impl SpecializedRenderPipeline for ScreenSpaceReflectionsPipeline { impl From for ScreenSpaceReflectionsUniform { fn from(settings: ScreenSpaceReflections) -> Self { Self { - perceptual_roughness_threshold: settings.perceptual_roughness_threshold, + min_perceptual_roughness: settings.min_perceptual_roughness.start, + min_perceptual_roughness_fully_active: settings.min_perceptual_roughness.end, + max_perceptual_roughness_starts_to_fade: settings.max_perceptual_roughness.start, + max_perceptual_roughness: settings.max_perceptual_roughness.end, + edge_fadeout_no_longer_active: settings.edge_fadeout.start, + edge_fadeout_fully_active: settings.edge_fadeout.end, thickness: settings.thickness, linear_steps: settings.linear_steps, linear_march_exponent: settings.linear_march_exponent, diff --git a/crates/bevy_pbr/src/ssr/ssr.wgsl b/crates/bevy_pbr/src/ssr/ssr.wgsl index d646ac69febf9..2839f67738ef7 100644 --- a/crates/bevy_pbr/src/ssr/ssr.wgsl +++ b/crates/bevy_pbr/src/ssr/ssr.wgsl @@ -7,7 +7,13 @@ clustered_forward, lighting, lighting::{LAYER_BASE, LAYER_CLEARCOAT}, - mesh_view_bindings::{view, depth_prepass_texture, deferred_prepass_texture, ssr_settings}, + mesh_view_bindings::{ + view, + globals, + depth_prepass_texture, + deferred_prepass_texture, + ssr_settings + }, pbr_deferred_functions::pbr_input_from_deferred_gbuffer, pbr_deferred_types, pbr_functions, @@ -29,7 +35,10 @@ position_world_to_view, }, } -#import bevy_render::view::View +#import bevy_render::{ + view::View, + maths::orthonormalize, +} #ifdef ENVIRONMENT_MAP #import bevy_pbr::environment_map @@ -43,6 +52,36 @@ // Group 1, bindings 2 and 3 are in `raymarch.wgsl`. +@group(2) @binding(4) var stbn_texture: texture_2d_array; + +struct BrdfSample { + wi: vec3, + value_over_pdf: vec3, +} + +fn sample_specular_brdf(wo: vec3, roughness: f32, F0: vec3, urand: vec2, N: vec3) -> BrdfSample { + var brdf_sample: BrdfSample; + + // Use VNDF sampling for the half-vector. + let wi = lighting::sample_visible_ggx(urand, roughness, N, wo); + let H = normalize(wo + wi); + let NdotL = max(dot(N, wi), 0.0001); + let NdotV = max(dot(N, wo), 0.0001); + let VdotH = max(dot(wo, H), 0.0001); + + let F = lighting::F_Schlick_vec(F0, 1.0, VdotH); + + // Height-correlated Smith G2 / G1(V) + let a2 = roughness * roughness; + let lambdaV = NdotL * sqrt((NdotV - a2 * NdotV) * NdotV + a2); + let lambdaL = NdotV * sqrt((NdotL - a2 * NdotL) * NdotL + a2); + + brdf_sample.wi = wi; + brdf_sample.value_over_pdf = F * (NdotV * NdotL + lambdaV) / (lambdaV + lambdaL); + + return brdf_sample; +} + // Returns the reflected color in the RGB channel and the specular occlusion in // the alpha channel. // @@ -56,8 +95,11 @@ // // * `P_world`: The current position in world space. // +// * `jitter`: Jitter to apply to the first step of the linear search; 0..=1 +// range. +// // [1]: https://lettier.github.io/3d-game-shaders-for-beginners/screen-space-reflection.html -fn evaluate_ssr(R_world: vec3, P_world: vec3) -> vec4 { +fn evaluate_ssr(R_world: vec3, P_world: vec3, jitter: f32) -> vec4 { let depth_size = vec2(textureDimensions(depth_prepass_texture)); var raymarch = depth_ray_march_new_from_depth(depth_size); @@ -67,7 +109,7 @@ fn evaluate_ssr(R_world: vec3, P_world: vec3) -> vec4 { raymarch.bisection_steps = ssr_settings.bisection_steps; raymarch.use_secant = ssr_settings.use_secant != 0u; raymarch.depth_thickness_linear_z = ssr_settings.thickness; - raymarch.jitter = 1.0; // Disable jitter for now. + raymarch.jitter = jitter; raymarch.march_behind_surfaces = false; let raymarch_result = depth_ray_march_march(&raymarch); @@ -92,10 +134,49 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { let gbuffer = textureLoad(deferred_prepass_texture, vec2(frag_coord.xy), 0); let pbr_input = pbr_input_from_deferred_gbuffer(frag_coord, gbuffer); - // Don't do anything if the surface is too rough, since we can't blur or do - // temporal accumulation yet. + // Don't do anything if the surface is too rough or too smooth let perceptual_roughness = pbr_input.material.perceptual_roughness; - if (perceptual_roughness > ssr_settings.perceptual_roughness_threshold) { + + var min_fade: f32; + if (ssr_settings.min_perceptual_roughness >= ssr_settings.min_perceptual_roughness_fully_active) { + min_fade = step(ssr_settings.min_perceptual_roughness, perceptual_roughness); + } else { + min_fade = smoothstep( + ssr_settings.min_perceptual_roughness, + ssr_settings.min_perceptual_roughness_fully_active, + perceptual_roughness + ); + } + + var max_fade: f32; + if (ssr_settings.max_perceptual_roughness_starts_to_fade >= ssr_settings.max_perceptual_roughness) { + max_fade = step(perceptual_roughness, ssr_settings.max_perceptual_roughness); + } else { + max_fade = 1.0 - smoothstep( + ssr_settings.max_perceptual_roughness_starts_to_fade, + ssr_settings.max_perceptual_roughness, + perceptual_roughness + ); + } + + var fade = saturate(min_fade) * saturate(max_fade); + + let ndc_position = frag_coord_to_ndc(vec4(in.position.xy, frag_coord.z, 1.0)); + let uv = ndc_to_uv(ndc_position.xy); + let dist = min(uv, vec2(1.0) - uv); + var fade_xy: vec2; + if (ssr_settings.edge_fadeout_no_longer_active >= ssr_settings.edge_fadeout_fully_active) { + fade_xy = step(vec2(ssr_settings.edge_fadeout_no_longer_active), dist); + } else { + fade_xy = smoothstep( + vec2(ssr_settings.edge_fadeout_no_longer_active), + vec2(ssr_settings.edge_fadeout_fully_active), + dist + ); + } + fade *= fade_xy.x * fade_xy.y; + + if (perceptual_roughness > ssr_settings.max_perceptual_roughness) { return fragment; } @@ -105,18 +186,57 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { let N = pbr_input.N; let V = pbr_input.V; - // Calculate the reflection vector. - let R = reflect(-V, N); + // Build a basis for sampling the BRDF. + let tangent_to_world = orthonormalize(N); + + let roughness = lighting::perceptualRoughnessToRoughness(perceptual_roughness); + let F0 = pbr_functions::calculate_F0(pbr_input.material.base_color.rgb, pbr_input.material.metallic, pbr_input.material.reflectance); + + // Get some random numbers. If the spatio-temporal blue noise (STBN) texture + // is available (i.e. not the 1x1 placeholder), we use it. Otherwise, we + // fall back to procedural noise. + let stbn_dims = textureDimensions(stbn_texture); + var urand: vec2; + var raymarch_jitter: f32; + if (all(stbn_dims > vec2(1u))) { + let stbn_layers = textureNumLayers(stbn_texture); + let stbn_noise = textureLoad( + stbn_texture, + vec2(in.position.xy) % stbn_dims, + i32(globals.frame_count % u32(stbn_layers)), + 0 + ); + urand = stbn_noise.xy; + // Use the third channel for jitter to avoid correlation with BRDF sampling. + raymarch_jitter = stbn_noise.z; + } else { + // Fallback to PCG-based procedural noise. + // We use a XOR-sum of products with large primes to decorrelate the + // seed from the screen-space coordinates and frame count, avoiding + // visible "crawling" artifacts. + var state = (u32(in.position.x) * 2131358057u) ^ + (u32(in.position.y) * 3416869721u) ^ + (globals.frame_count * 1199786941u); + urand = utils::rand_vec2f(&state); + raymarch_jitter = utils::rand_f(&state); + } + + // Sample the BRDF. + let N_tangent = vec3(0.0, 0.0, 1.0); + let V_tangent = V * tangent_to_world; + + let brdf_sample = sample_specular_brdf(V_tangent, roughness, F0, urand, N_tangent); + let R_stochastic = tangent_to_world * brdf_sample.wi; + let brdf_sample_value_over_pdf = brdf_sample.value_over_pdf; // Do the raymarching. - let ssr_specular = evaluate_ssr(R, world_position); - var indirect_light = ssr_specular.rgb; - specular_occlusion *= ssr_specular.a; + let ssr_specular = evaluate_ssr(R_stochastic, world_position, raymarch_jitter); + var indirect_light = ssr_specular.rgb * brdf_sample_value_over_pdf * fade; + specular_occlusion = mix(specular_occlusion, specular_occlusion * ssr_specular.a, fade); // Sample the environment map if necessary. // - // This will take the specular part of the environment map into account if - // the ray missed. Otherwise, it only takes the diffuse part. + // This will take the specular part of the environment map into account. // // TODO: Merge this with the duplicated code in `apply_pbr_lighting`. #ifdef ENVIRONMENT_MAP @@ -126,7 +246,6 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { let reflectance = pbr_input.material.reflectance; let specular_transmission = pbr_input.material.specular_transmission; let diffuse_transmission = pbr_input.material.diffuse_transmission; - let diffuse_occlusion = pbr_input.diffuse_occlusion; #ifdef STANDARD_MATERIAL_CLEARCOAT // Do the above calculations again for the clearcoat layer. Remember that @@ -140,7 +259,7 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { #endif // STANDARD_MATERIAL_CLEARCOAT // Calculate various other values needed for environment mapping. - let roughness = lighting::perceptualRoughnessToRoughness(perceptual_roughness); + let env_roughness = lighting::perceptualRoughnessToRoughness(perceptual_roughness); let diffuse_color = pbr_functions::calculate_diffuse_color( base_color, metallic, @@ -149,7 +268,11 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { ); let NdotV = max(dot(N, V), 0.0001); let F_ab = lighting::F_AB(perceptual_roughness, NdotV); - let F0 = pbr_functions::calculate_F0(base_color, metallic, reflectance); + let F0_env = pbr_functions::calculate_F0(base_color, metallic, reflectance); + + // Don't add stochastic noise to hits that sample the prefiltered env map. + // The prefiltered env map already accounts for roughness. + let R = reflect(-V, N); // Pack all the values into a structure. var lighting_input: lighting::LightingInput; @@ -157,11 +280,11 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { lighting_input.layers[LAYER_BASE].N = N; lighting_input.layers[LAYER_BASE].R = R; lighting_input.layers[LAYER_BASE].perceptual_roughness = perceptual_roughness; - lighting_input.layers[LAYER_BASE].roughness = roughness; + lighting_input.layers[LAYER_BASE].roughness = env_roughness; lighting_input.P = world_position.xyz; lighting_input.V = V; lighting_input.diffuse_color = diffuse_color; - lighting_input.F0_ = F0; + lighting_input.F0_ = F0_env; lighting_input.F_ab = F_ab; #ifdef STANDARD_MATERIAL_CLEARCOAT lighting_input.layers[LAYER_CLEARCOAT].NdotV = clearcoat_NdotV; @@ -180,13 +303,15 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { clustered_forward::unpack_clusterable_object_index_ranges(cluster_index); // Sample the environment map. + // + // We pass `true` for `found_diffuse_indirect` here because we only want + // the specular part; the diffuse part was already accumulated in the + // main PBR pass. let environment_light = environment_map::environment_map_light( - &lighting_input, &clusterable_object_index_ranges, false); + &lighting_input, &clusterable_object_index_ranges, true); // Accumulate the environment map light. - indirect_light += view.exposure * - (environment_light.diffuse * diffuse_occlusion + - environment_light.specular * specular_occlusion); + indirect_light += view.exposure * environment_light.specular * specular_occlusion; #endif // Write the results. diff --git a/crates/bevy_pbr/src/volumetric_fog/render.rs b/crates/bevy_pbr/src/volumetric_fog/render.rs index e0ddc3ccefe74..b9820b562629d 100644 --- a/crates/bevy_pbr/src/volumetric_fog/render.rs +++ b/crates/bevy_pbr/src/volumetric_fog/render.rs @@ -668,6 +668,9 @@ pub fn prepare_volumetric_fog_pipelines( deferred_prepass, ); mesh_pipeline_view_key.set(MeshPipelineViewLayoutKey::ATMOSPHERE, atmosphere); + if cfg!(feature = "bluenoise_texture") { + mesh_pipeline_view_key |= MeshPipelineViewLayoutKey::BLUE_NOISE_TEXTURE; + } let mut textureless_flags = VolumetricFogPipelineKeyFlags::empty(); textureless_flags.set(VolumetricFogPipelineKeyFlags::HDR, view.hdr); diff --git a/examples/3d/atmosphere.rs b/examples/3d/atmosphere.rs index e264716cc3714..e9ae1e8bb4e74 100644 --- a/examples/3d/atmosphere.rs +++ b/examples/3d/atmosphere.rs @@ -4,7 +4,7 @@ use bevy::camera_controller::free_camera::{FreeCamera, FreeCameraPlugin}; use std::f32::consts::PI; use bevy::{ - anti_alias::fxaa::Fxaa, + anti_alias::taa::TemporalAntiAliasing, camera::Exposure, color::palettes::css::BLACK, core_pipeline::tonemapping::Tonemapping, @@ -128,7 +128,7 @@ fn setup_camera_fog( ..default() }, Msaa::Off, - Fxaa::default(), + TemporalAntiAliasing::default(), ScreenSpaceReflections::default(), )); } diff --git a/examples/3d/ssr.rs b/examples/3d/ssr.rs index 9b98a01ca418e..eedcbaf640b20 100644 --- a/examples/3d/ssr.rs +++ b/examples/3d/ssr.rs @@ -3,7 +3,7 @@ use std::ops::Range; use bevy::{ - anti_alias::fxaa::Fxaa, + anti_alias::taa::TemporalAntiAliasing, color::palettes::css::{BLACK, WHITE}, core_pipeline::Skybox, image::{ @@ -13,7 +13,8 @@ use bevy::{ input::mouse::MouseWheel, math::{vec3, vec4}, pbr::{ - DefaultOpaqueRendererMethod, ExtendedMaterial, MaterialExtension, ScreenSpaceReflections, + DefaultOpaqueRendererMethod, ExtendedMaterial, MaterialExtension, + ScreenSpaceAmbientOcclusion, ScreenSpaceReflections, }, prelude::*, render::{ @@ -39,7 +40,11 @@ static TURN_SSR_ON_HELP_TEXT: &str = "Press Space to turn screen-space reflectio static MOVE_CAMERA_HELP_TEXT: &str = "Press WASD or use the mouse wheel to pan and orbit the camera"; static SWITCH_TO_FLIGHT_HELMET_HELP_TEXT: &str = "Press Enter to switch to the flight helmet model"; -static SWITCH_TO_CUBE_HELP_TEXT: &str = "Press Enter to switch to the cube model"; +static SWITCH_TO_CAPSULES_HELP_TEXT: &str = "Press Enter to switch to the row of capsules model"; +static SWITCH_TO_CUBE_HELP_TEXT: &str = "Press Enter to switch to the single cube model"; +static MIN_ROUGHNESS_HELP_TEXT: &str = "Press U/I and O/P to adjust the minimum roughness range"; +static MAX_ROUGHNESS_HELP_TEXT: &str = "Press H/J and K/L to adjust the maximum roughness range"; +static EDGE_FADEOUT_HELP_TEXT: &str = "Press N/M and ,/. to adjust the edge fadeout range"; /// A custom [`ExtendedMaterial`] that creates animated water ripples. #[derive(Asset, TypePath, AsBindGroup, Debug, Clone)] @@ -75,6 +80,12 @@ struct AppSettings { ssr_on: bool, /// Which model is being displayed. displayed_model: DisplayedModel, + /// The perceptual roughness range over which SSR begins to fade in. + min_perceptual_roughness: Range, + /// The perceptual roughness range over which SSR begins to fade out. + max_perceptual_roughness: Range, + /// The range over which SSR begins to fade out at the edges of the screen. + edge_fadeout: Range, } /// Which model is being displayed. @@ -85,9 +96,11 @@ enum DisplayedModel { Cube, /// The flight helmet is being displayed. FlightHelmet, + /// The capsules are being displayed. + Capsules, } -/// A marker component for the cube model. +/// A marker component for the single cube model. #[derive(Component)] struct CubeModel; @@ -95,6 +108,14 @@ struct CubeModel; #[derive(Component)] struct FlightHelmetModel; +/// A marker component for the row of capsules model. +#[derive(Component)] +struct CapsuleModel; + +/// A marker component for the row of capsules parent. +#[derive(Component)] +struct CapsulesParent; + fn main() { // Enable deferred rendering, which is necessary for screen-space // reflections at this time. Disable multisampled antialiasing, as deferred @@ -133,13 +154,14 @@ fn setup( &mut standard_materials, ); spawn_flight_helmet(&mut commands, &asset_server); + spawn_capsules(&mut commands, &mut meshes, &mut standard_materials); spawn_water( &mut commands, &asset_server, &mut meshes, &mut water_materials, ); - spawn_camera(&mut commands, &asset_server); + spawn_camera(&mut commands, &asset_server, &app_settings); spawn_text(&mut commands, &app_settings); } @@ -176,6 +198,39 @@ fn spawn_flight_helmet(commands: &mut Commands, asset_server: &AssetServer) { )); } +// Spawns the row of capsules. +fn spawn_capsules( + commands: &mut Commands, + meshes: &mut Assets, + standard_materials: &mut Assets, +) { + let capsule_mesh = meshes.add(Capsule3d::new(0.4, 0.5)); + let parent = commands + .spawn(( + Transform::from_xyz(0.0, 0.5, 0.0), + Visibility::Hidden, + CapsulesParent, + )) + .id(); + + for i in 0..5 { + let roughness = i as f32 * 0.25; + let child = commands + .spawn(( + Mesh3d(capsule_mesh.clone()), + MeshMaterial3d(standard_materials.add(StandardMaterial { + base_color: Color::BLACK, + perceptual_roughness: roughness, + ..default() + })), + Transform::from_xyz(i as f32 * 1.1 - (1.1 * 2.0), 0.5, 0.0), + CapsuleModel, + )) + .id(); + commands.entity(parent).add_child(child); + } +} + // Spawns the water plane. fn spawn_water( commands: &mut Commands, @@ -188,7 +243,7 @@ fn spawn_water( MeshMaterial3d(water_materials.add(ExtendedMaterial { base: StandardMaterial { base_color: BLACK.into(), - perceptual_roughness: 0.0, + perceptual_roughness: 0.09, ..default() }, extension: Water { @@ -222,31 +277,36 @@ fn spawn_water( } // Spawns the camera. -fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) { +fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer, app_settings: &AppSettings) { // Create the camera. Add an environment map and skybox so the water has // something interesting to reflect, other than the cube. Enable deferred // rendering by adding depth and deferred prepasses. Turn on FXAA to make // the scene look a little nicer. Finally, add screen space reflections. - commands - .spawn(( - Camera3d::default(), - Transform::from_translation(vec3(-1.25, 2.25, 4.5)).looking_at(Vec3::ZERO, Vec3::Y), - Hdr, - Msaa::Off, - )) - .insert(EnvironmentMapLight { + commands.spawn(( + Camera3d::default(), + Transform::from_translation(vec3(-1.25, 2.25, 4.5)).looking_at(Vec3::ZERO, Vec3::Y), + Hdr, + Msaa::Off, + TemporalAntiAliasing::default(), + ScreenSpaceReflections { + min_perceptual_roughness: app_settings.min_perceptual_roughness.clone(), + max_perceptual_roughness: app_settings.max_perceptual_roughness.clone(), + edge_fadeout: app_settings.edge_fadeout.clone(), + ..default() + }, + ScreenSpaceAmbientOcclusion::default(), + EnvironmentMapLight { diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), intensity: 5000.0, ..default() - }) - .insert(Skybox { + }, + Skybox { image: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), brightness: 5000.0, ..default() - }) - .insert(ScreenSpaceReflections::default()) - .insert(Fxaa::default()); + }, + )); } // Spawns the help text. @@ -265,17 +325,27 @@ fn spawn_text(commands: &mut Commands, app_settings: &AppSettings) { // Creates or recreates the help text. fn create_text(app_settings: &AppSettings) -> Text { format!( - "{}\n{}\n{}", + "{}\n{}\n{}\n{}\n{}\n{}\nSSR min roughness: {:.2}..{:.2}\nSSR max roughness: {:.2}..{:.2}\nSSR edge fadeout: {:.2}..{:.2}", match app_settings.displayed_model { DisplayedModel::Cube => SWITCH_TO_FLIGHT_HELMET_HELP_TEXT, - DisplayedModel::FlightHelmet => SWITCH_TO_CUBE_HELP_TEXT, + DisplayedModel::FlightHelmet => SWITCH_TO_CAPSULES_HELP_TEXT, + DisplayedModel::Capsules => SWITCH_TO_CUBE_HELP_TEXT, }, if app_settings.ssr_on { TURN_SSR_OFF_HELP_TEXT } else { TURN_SSR_ON_HELP_TEXT }, - MOVE_CAMERA_HELP_TEXT + MOVE_CAMERA_HELP_TEXT, + MIN_ROUGHNESS_HELP_TEXT, + MAX_ROUGHNESS_HELP_TEXT, + EDGE_FADEOUT_HELP_TEXT, + app_settings.min_perceptual_roughness.start, + app_settings.min_perceptual_roughness.end, + app_settings.max_perceptual_roughness.start, + app_settings.max_perceptual_roughness.end, + app_settings.edge_fadeout.start, + app_settings.edge_fadeout.end, ) .into() } @@ -292,7 +362,8 @@ fn rotate_model( time: Res