diff --git a/Cargo.toml b/Cargo.toml index da1ccc9f9aecb..7d62a46b5b47d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4679,6 +4679,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_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index 28b25ac2fdd2b..b738d56e98aef 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -157,7 +157,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 e321607dca6d9..20ffcd8b225e4 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -678,32 +678,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 3984b82933685..920ced509d8f7 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, }; @@ -48,9 +53,9 @@ use tracing::info; use crate::{ binding_arrays_are_usable, contact_shadows::ViewContactShadowsUniformOffset, graph::NodePbr, - ExtractedAtmosphere, MeshPipelineViewLayoutKey, MeshPipelineViewLayouts, MeshViewBindGroup, - RenderViewLightProbes, ViewEnvironmentMapUniformOffset, ViewFogUniformOffset, - ViewLightProbesUniformOffset, ViewLightsUniformOffset, + Bluenoise, ExtractedAtmosphere, MeshPipelineViewLayoutKey, MeshPipelineViewLayouts, + MeshViewBindGroup, RenderViewLightProbes, ViewEnvironmentMapUniformOffset, + ViewFogUniformOffset, ViewLightProbesUniformOffset, ViewLightsUniformOffset, }; /// Enables screen-space reflections for a camera. @@ -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, } } } @@ -295,6 +320,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), @@ -303,6 +339,7 @@ impl ViewNode for ScreenSpaceReflectionsNode { &ssr_pipeline.color_sampler, &ssr_pipeline.depth_linear_sampler, &ssr_pipeline.depth_nearest_sampler, + &stbn_view, )), ); @@ -366,6 +403,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 }), ), ), ); @@ -458,6 +496,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::STBN; + } // Build the pipeline. let pipeline_id = pipelines.specialize( @@ -520,7 +561,7 @@ impl ExtractComponent for ScreenSpaceReflections { return None; } - Some((*settings).into()) + Some(settings.clone().into()) } } @@ -555,6 +596,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()); @@ -584,7 +629,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..f6338106c5149 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 (fade <= 0.0 || 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) * fade; #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 44f6b81d0ce82..2f77d52f7f977 100644 --- a/crates/bevy_pbr/src/volumetric_fog/render.rs +++ b/crates/bevy_pbr/src/volumetric_fog/render.rs @@ -671,6 +671,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::STBN; + } 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 be13b7383dac4..da412072a850b 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/contact_shadows.rs b/examples/3d/contact_shadows.rs index e57d2c3b3a457..eb02105157ce8 100644 --- a/examples/3d/contact_shadows.rs +++ b/examples/3d/contact_shadows.rs @@ -116,7 +116,7 @@ fn setup(mut commands: Commands, asset_server: Res) { Bloom::default(), Hdr, Skybox { - brightness: 500.0, + brightness: 1000.0, image: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), ..default() }, diff --git a/examples/3d/ssr.rs b/examples/3d/ssr.rs index 9b98a01ca418e..b2c8c6eae7d30 100644 --- a/examples/3d/ssr.rs +++ b/examples/3d/ssr.rs @@ -1,9 +1,10 @@ //! Demonstrates screen space reflections in deferred rendering. +use std::fmt; 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 +14,8 @@ use bevy::{ input::mouse::MouseWheel, math::{vec3, vec4}, pbr::{ - DefaultOpaqueRendererMethod, ExtendedMaterial, MaterialExtension, ScreenSpaceReflections, + DefaultOpaqueRendererMethod, ExtendedMaterial, MaterialExtension, + ScreenSpaceAmbientOcclusion, ScreenSpaceReflections, }, prelude::*, render::{ @@ -23,6 +25,15 @@ use bevy::{ shader::ShaderRef, }; +#[path = "../helpers/widgets.rs"] +mod widgets; + +use widgets::{ + handle_ui_interactions, main_ui_node, option_buttons, update_ui_radio_button, + update_ui_radio_button_text, RadioButton, RadioButtonText, WidgetClickEvent, WidgetClickSender, + BUTTON_BORDER, BUTTON_BORDER_COLOR, BUTTON_BORDER_RADIUS_SIZE, BUTTON_PADDING, +}; + /// This example uses a shader source file from the assets subdirectory const SHADER_ASSET_PATH: &str = "shaders/water_material.wgsl"; @@ -34,13 +45,6 @@ const CAMERA_MOUSE_WHEEL_ZOOM_SPEED: f32 = 0.25; // We clamp camera distances to this range. const CAMERA_ZOOM_RANGE: Range = 2.0..12.0; -static TURN_SSR_OFF_HELP_TEXT: &str = "Press Space to turn screen-space reflections off"; -static TURN_SSR_ON_HELP_TEXT: &str = "Press Space to turn screen-space reflections on"; -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"; - /// A custom [`ExtendedMaterial`] that creates animated water ripples. #[derive(Asset, TypePath, AsBindGroup, Debug, Clone)] struct Water { @@ -75,19 +79,56 @@ 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. -#[derive(Default)] +#[derive(Default, PartialEq, Copy, Clone)] enum DisplayedModel { /// The cube is being displayed. #[default] Cube, /// The flight helmet is being displayed. FlightHelmet, + /// The capsules are being displayed. + Capsules, +} + +impl fmt::Display for DisplayedModel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match self { + DisplayedModel::Cube => "Cube", + DisplayedModel::FlightHelmet => "Flight Helmet", + DisplayedModel::Capsules => "Capsules", + }; + write!(f, "{}", name) + } +} + +#[derive(Clone, Copy, PartialEq)] +enum ExampleSetting { + Ssr(bool), + Model(DisplayedModel), + MinRoughnessStart(Adjustment), + MinRoughnessEnd(Adjustment), + MaxRoughnessStart(Adjustment), + MaxRoughnessEnd(Adjustment), + EdgeFadeoutStart(Adjustment), + EdgeFadeoutEnd(Adjustment), +} + +#[derive(Clone, Copy, PartialEq)] +enum Adjustment { + Increase, + Decrease, } -/// A marker component for the cube model. +/// A marker component for the single cube model. #[derive(Component)] struct CubeModel; @@ -95,6 +136,25 @@ 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; + +/// A marker component for the text that displays a range value. +#[derive(Component)] +enum RangeValueText { + MinRoughnessStart, + MinRoughnessEnd, + MaxRoughnessStart, + MaxRoughnessEnd, + EdgeFadeoutStart, + EdgeFadeoutEnd, +} + fn main() { // Enable deferred rendering, which is necessary for screen-space // reflections at this time. Disable multisampled antialiasing, as deferred @@ -110,10 +170,12 @@ fn main() { ..default() })) .add_plugins(MaterialPlugin::>::default()) + .add_message::>() .add_systems(Startup, setup) .add_systems(Update, rotate_model) .add_systems(Update, move_camera) .add_systems(Update, adjust_app_settings) + .add_systems(Update, handle_ui_interactions::) .run(); } @@ -133,14 +195,15 @@ 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_text(&mut commands, &app_settings); + spawn_camera(&mut commands, &asset_server, &app_settings); + spawn_buttons(&mut commands, &app_settings); } // Spawns the rotating cube. @@ -176,6 +239,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 +284,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,77 +318,215 @@ 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. -fn spawn_text(commands: &mut Commands, app_settings: &AppSettings) { - commands.spawn(( - create_text(app_settings), +fn spawn_buttons(commands: &mut Commands, app_settings: &AppSettings) { + commands.spawn(main_ui_node()).with_children(|parent| { + parent.spawn(option_buttons( + "SSR", + &[ + (ExampleSetting::Ssr(true), "On"), + (ExampleSetting::Ssr(false), "Off"), + ], + )); + + parent.spawn(option_buttons( + "Model", + &[ + (ExampleSetting::Model(DisplayedModel::Cube), "Cube"), + ( + ExampleSetting::Model(DisplayedModel::FlightHelmet), + "Flight Helmet", + ), + (ExampleSetting::Model(DisplayedModel::Capsules), "Capsules"), + ], + )); + + parent.spawn(range_row( + "Min Roughness", + app_settings.min_perceptual_roughness.start, + app_settings.min_perceptual_roughness.end, + RangeValueText::MinRoughnessStart, + RangeValueText::MinRoughnessEnd, + ExampleSetting::MinRoughnessStart(Adjustment::Decrease), + ExampleSetting::MinRoughnessStart(Adjustment::Increase), + ExampleSetting::MinRoughnessEnd(Adjustment::Decrease), + ExampleSetting::MinRoughnessEnd(Adjustment::Increase), + )); + + parent.spawn(range_row( + "Max Roughness", + app_settings.max_perceptual_roughness.start, + app_settings.max_perceptual_roughness.end, + RangeValueText::MaxRoughnessStart, + RangeValueText::MaxRoughnessEnd, + ExampleSetting::MaxRoughnessStart(Adjustment::Decrease), + ExampleSetting::MaxRoughnessStart(Adjustment::Increase), + ExampleSetting::MaxRoughnessEnd(Adjustment::Decrease), + ExampleSetting::MaxRoughnessEnd(Adjustment::Increase), + )); + + parent.spawn(range_row( + "Edge Fadeout", + app_settings.edge_fadeout.start, + app_settings.edge_fadeout.end, + RangeValueText::EdgeFadeoutStart, + RangeValueText::EdgeFadeoutEnd, + ExampleSetting::EdgeFadeoutStart(Adjustment::Decrease), + ExampleSetting::EdgeFadeoutStart(Adjustment::Increase), + ExampleSetting::EdgeFadeoutEnd(Adjustment::Decrease), + ExampleSetting::EdgeFadeoutEnd(Adjustment::Increase), + )); + }); +} + +fn range_row( + title: &str, + start_value: f32, + end_value: f32, + start_marker: RangeValueText, + end_marker: RangeValueText, + start_dec: ExampleSetting, + start_inc: ExampleSetting, + end_dec: ExampleSetting, + end_inc: ExampleSetting, +) -> impl Bundle { + ( Node { - position_type: PositionType::Absolute, - bottom: px(12), - left: px(12), + align_items: AlignItems::Center, ..default() }, - )); + Children::spawn(( + Spawn(( + widgets::ui_text(title, Color::WHITE), + Node { + width: px(150), + ..default() + }, + )), + Spawn(range_controls( + start_value, + start_marker, + start_dec, + start_inc, + )), + Spawn(( + widgets::ui_text("to", Color::WHITE), + Node { + margin: UiRect::horizontal(px(10)), + ..default() + }, + )), + Spawn(range_controls(end_value, end_marker, end_dec, end_inc)), + )), + ) } -// Creates or recreates the help text. -fn create_text(app_settings: &AppSettings) -> Text { - format!( - "{}\n{}\n{}", - match app_settings.displayed_model { - DisplayedModel::Cube => SWITCH_TO_FLIGHT_HELMET_HELP_TEXT, - DisplayedModel::FlightHelmet => SWITCH_TO_CUBE_HELP_TEXT, - }, - if app_settings.ssr_on { - TURN_SSR_OFF_HELP_TEXT - } else { - TURN_SSR_ON_HELP_TEXT +fn range_controls( + value: f32, + marker: RangeValueText, + dec_setting: ExampleSetting, + inc_setting: ExampleSetting, +) -> impl Bundle { + ( + Node { + align_items: AlignItems::Center, + ..default() }, - MOVE_CAMERA_HELP_TEXT + Children::spawn(( + Spawn(adjustment_button(dec_setting, "<", Some(true))), + Spawn(( + Node { + width: px(50), + height: px(33), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + border: BUTTON_BORDER.with_left(px(0)).with_right(px(0)), + ..default() + }, + BackgroundColor(Color::WHITE), + BUTTON_BORDER_COLOR, + marker, + children![(widgets::ui_text(&format!("{:.2}", value), Color::BLACK))], + )), + Spawn(adjustment_button(inc_setting, ">", Some(false))), + )), ) - .into() } -impl MaterialExtension for Water { - fn deferred_fragment_shader() -> ShaderRef { - SHADER_ASSET_PATH.into() - } +fn adjustment_button( + setting: ExampleSetting, + label: &str, + is_left_right: Option, +) -> impl Bundle { + ( + Button, + Node { + height: px(33), + border: if let Some(is_left) = is_left_right { + if is_left { + BUTTON_BORDER.with_right(px(0)) + } else { + BUTTON_BORDER.with_left(px(0)) + } + } else { + BUTTON_BORDER + }, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + padding: BUTTON_PADDING, + border_radius: match is_left_right { + Some(true) => BorderRadius::ZERO.with_left(BUTTON_BORDER_RADIUS_SIZE), + Some(false) => BorderRadius::ZERO.with_right(BUTTON_BORDER_RADIUS_SIZE), + None => BorderRadius::all(BUTTON_BORDER_RADIUS_SIZE), + }, + ..default() + }, + BUTTON_BORDER_COLOR, + BackgroundColor(Color::BLACK), + RadioButton, + WidgetClickSender(setting), + children![(widgets::ui_text(label, Color::WHITE), RadioButtonText)], + ) } -/// Rotates the model on the Y axis a bit every frame. fn rotate_model( mut query: Query<&mut Transform, Or<(With, With)>>, time: Res