From 90e9e56ab160532207e8ddf23978cfe1a57182d1 Mon Sep 17 00:00:00 2001 From: Christophe Dehais Date: Mon, 8 Jun 2026 10:07:49 +0200 Subject: [PATCH 1/5] Use IOR to parameterize dielectrics specular reflectance --- crates/bevy_gltf/src/loader/mod.rs | 4 +- crates/bevy_gltf/src/material.rs | 13 ++--- .../src/deferred/pbr_deferred_functions.wgsl | 21 +++++--- .../src/deferred/pbr_deferred_types.wgsl | 39 ++++++++++++++ crates/bevy_pbr/src/gltf.rs | 2 +- .../src/light_probe/environment_map.wgsl | 14 ++--- crates/bevy_pbr/src/pbr_material.rs | 53 ++++++++----------- crates/bevy_pbr/src/render/pbr_fragment.wgsl | 17 +++--- crates/bevy_pbr/src/render/pbr_functions.wgsl | 42 +++++++++------ crates/bevy_pbr/src/render/pbr_lighting.wgsl | 34 +++++++----- crates/bevy_pbr/src/render/pbr_types.wgsl | 6 ++- .../src/transmission/transmission.wgsl | 4 +- 12 files changed, 150 insertions(+), 99 deletions(-) diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index 5781bfbf82d08..94be18f35ffc1 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -1500,9 +1500,7 @@ fn load_material( anisotropy_channel: anisotropy.anisotropy_channel, #[cfg(feature = "pbr_anisotropy_texture")] anisotropy_texture: anisotropy.anisotropy_texture, - // From the `KHR_materials_specular` spec: - // - reflectance: specular.specular_factor.unwrap_or(1.0) as f32 * 0.5, + specular: specular.specular_factor.unwrap_or(1.0) as f32, #[cfg(feature = "pbr_specular_textures")] specular_channel: specular.specular_channel, #[cfg(feature = "pbr_specular_textures")] diff --git a/crates/bevy_gltf/src/material.rs b/crates/bevy_gltf/src/material.rs index 724bad7edf13d..79ca9c5b87b22 100644 --- a/crates/bevy_gltf/src/material.rs +++ b/crates/bevy_gltf/src/material.rs @@ -43,8 +43,8 @@ pub struct GltfMaterial { /// Metallic and roughness maps, stored as a single texture. pub metallic_roughness_texture: Option>, - /// Specular intensity for non-metals on a linear scale of `[0.0, 1.0]`. - pub reflectance: f32, + /// Specular strength for non-metals on a linear scale of `[0.0, 1.0]`. + pub specular: f32, /// The UV channel to use for the [`GltfMaterial::specular_texture`]. #[cfg(feature = "pbr_specular_textures")] @@ -54,8 +54,8 @@ pub struct GltfMaterial { #[cfg(feature = "pbr_specular_textures")] pub specular_texture: Option>, - /// A color with which to modulate the [`GltfMaterial::reflectance`] for - /// non-metals. + /// A color with which to modulate the default reflectance at normal incidence (which is computed from [`GltfMaterial::ior`]). + /// Only affects non-metals. pub specular_tint: Color, /// The UV channel to use for the @@ -206,10 +206,6 @@ impl Default for GltfMaterial { metallic: 0.0, metallic_roughness_channel: UvChannel::Uv0, metallic_roughness_texture: None, - // Minimum real-world reflectance is 2%, most materials between 2-5% - // Expressed in a linear scale and equivalent to 4% reflectance see - // - reflectance: 0.5, specular_transmission: 0.0, #[cfg(feature = "pbr_transmission_textures")] specular_transmission_channel: UvChannel::Uv0, @@ -227,6 +223,7 @@ impl Default for GltfMaterial { occlusion_texture: None, normal_map_channel: UvChannel::Uv0, normal_map_texture: None, + specular: 1.0, #[cfg(feature = "pbr_specular_textures")] specular_channel: UvChannel::Uv0, #[cfg(feature = "pbr_specular_textures")] diff --git a/crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl b/crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl index 5ff9c31f8b075..0af6696310d33 100644 --- a/crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl +++ b/crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl @@ -30,21 +30,23 @@ fn deferred_gbuffer_from_pbr_input(in: PbrInput) -> vec4 { // https://en.wikipedia.org/wiki/Rec._709 let rec_709_coeffs = vec3(0.2126, 0.7152, 0.0722); let diffuse_occlusion = dot(in.diffuse_occlusion, rec_709_coeffs); - // Only monochrome specular supported. - let reflectance = dot(in.material.reflectance, rec_709_coeffs); #ifdef WEBGL2 // More crunched for webgl so we can also fit depth. - var props = deferred_types::pack_unorm3x4_plus_unorm_20_(vec4( - reflectance, + let ior_specular = deferred_types::pack_4bit_ior_specular(in.material.ior, in.material.specular_weight); + let ior_specular_props = f32(ior_specular) / 15.0; + var props = deferred_types::pack_unorm3x4_unorm_20_(vec4( + ior_specular_props, in.material.metallic, diffuse_occlusion, in.frag_coord.z)); #else + let ior_specular = deferred_types::pack_8bit_ior_specular(in.material.ior, in.material.specular_weight); + let ior_specular_props = f32(ior_specular) / 255.0; let clearcoat = u32(round(saturate(in.material.clearcoat) * 15.0)); let clearcoat_perceptual_roughness = u32(round(clamp(in.material.clearcoat_perceptual_roughness, 0.0, 0.5) * 30.0)); let clearcoat_props = f32(clearcoat | (clearcoat_perceptual_roughness << 4u)) / 255.0; var props = deferred_types::pack_unorm4x8_(vec4( - reflectance, // could be fewer bits + ior_specular_props, in.material.metallic, // could be fewer bits diffuse_occlusion, // is this worth including? clearcoat_props)); @@ -106,15 +108,18 @@ fn pbr_input_from_deferred_gbuffer(frag_coord: vec4, gbuffer: vec4) -> } #ifdef WEBGL2 // More crunched for webgl so we can also fit depth. let props = deferred_types::unpack_unorm3x4_plus_unorm_20_(gbuffer.b); - // Bias to 0.5 since that's the value for almost all materials. - pbr.material.reflectance = vec3(saturate(props.r - 0.03333333333)); + let ior_specular_props = u32(rounds(props.g * 15.0)); + let ior_specular = deferred_types::unpack_4bit_ior_specular(ior_specular_props); #else let props = deferred_types::unpack_unorm4x8_(gbuffer.b); + let ior_specular_props = u32(round(props.r * 255.0)); + let ior_specular = deferred_types::unpack_8bit_ior_specular(ior_specular_props); let clearcoat_props = u32(round(props.a * 255.0)); - pbr.material.reflectance = vec3(props.r); pbr.material.clearcoat = f32(clearcoat_props & 0x0fu) / 15.0; pbr.material.clearcoat_perceptual_roughness = f32(clearcoat_props >> 4u) / 30.0; #endif // WEBGL2 + pbr.material.ior = ior_specular.x; + pbr.material.specular_weight = ior_specular.y; pbr.material.metallic = props.g; pbr.diffuse_occlusion = vec3(props.b); let octahedral_normal = deferred_types::unpack_24bit_normal(gbuffer.a); diff --git a/crates/bevy_pbr/src/deferred/pbr_deferred_types.wgsl b/crates/bevy_pbr/src/deferred/pbr_deferred_types.wgsl index fb4def94ce777..8621940d01cec 100644 --- a/crates/bevy_pbr/src/deferred/pbr_deferred_types.wgsl +++ b/crates/bevy_pbr/src/deferred/pbr_deferred_types.wgsl @@ -52,6 +52,45 @@ fn unpack_flags(packed: u32) -> u32 { return (packed >> 24u) & 0xFFu; } + +#ifdef WEBGL2 +// Pack ior and specular strength into 4 bits. +// With so little space, specular is made binary, and all values except specular = 0.0 are mapped to 1 +// IOR values outside [1.33, 2.33] are clamped. +fn pack_4bit_ior_specular(ior: f32, specular: f32) -> u32 { + var ior_remapped = saturate(ior - 1.33); + // give more range to lower values + ior_remapped = sqrt(ior_remapped); + let ior3 = u32(ior_remapped * 7.0 + 0.5); + return (u32(specular > 0.0) << 3u) | ior3; +} + +fn unpack_4bit_ior_specular(v: u32) -> vec2 { + let specular = f32(v >> 3u); + var ior = f32(v & 0x7u) / 7.0; + ior = ior * ior + 1.33; + return vec2(ior, specular); +} +#endif // WEBGL2 + +// Pack ior and specular strength into 8 bits. Allocation is IOR: 5 bits / specular: 3 bits +// IOR values outside [1.0. 3.0] are clamped +fn pack_8bit_ior_specular(ior: f32, specular: f32) -> u32 { + var ior_remapped = saturate((ior - 1.0) / 2.0); + // give more range to lower values + ior_remapped = sqrt(ior_remapped); + let ior5 = u32(ior_remapped * 31.0 + 0.5); + let specular3 = u32(saturate(specular) * 7.0 + 0.5); + return (specular3 << 5u) | ior5; +} + +fn unpack_8bit_ior_specular(v: u32) -> vec2 { + let specular = f32(v >> 5u) / 7.0; + var ior = f32(v & 0x1Fu) / 31.0; + ior = ior * ior * 2.0 + 1.0; + return vec2(ior, specular); +} + // The builtin one didn't work in webgl. // "'unpackUnorm4x8' : no matching overloaded function found" // https://github.com/gfx-rs/naga/issues/2006 diff --git a/crates/bevy_pbr/src/gltf.rs b/crates/bevy_pbr/src/gltf.rs index af9bbf7f64c35..a26a914d22f04 100644 --- a/crates/bevy_pbr/src/gltf.rs +++ b/crates/bevy_pbr/src/gltf.rs @@ -42,7 +42,7 @@ pub fn standard_material_from_gltf_material(material: &GltfMaterial) -> Standard metallic: material.metallic, metallic_roughness_channel: material.metallic_roughness_channel.clone(), metallic_roughness_texture: material.metallic_roughness_texture.clone(), - reflectance: material.reflectance, + specular: material.specular, specular_tint: material.specular_tint, specular_transmission: material.specular_transmission, #[cfg(feature = "pbr_transmission_textures")] diff --git a/crates/bevy_pbr/src/light_probe/environment_map.wgsl b/crates/bevy_pbr/src/light_probe/environment_map.wgsl index 15e7aaec33fb3..8d52fc7f581d4 100644 --- a/crates/bevy_pbr/src/light_probe/environment_map.wgsl +++ b/crates/bevy_pbr/src/light_probe/environment_map.wgsl @@ -329,9 +329,8 @@ fn compute_multiscatter( F0: vec3, F_ab: vec2, Ems: f32, - specular_occlusion: f32, ) -> MultiscatterResult { - let FssEss = (F0 * F_ab.x + F_ab.y) * specular_occlusion; + let FssEss = (F0 * F_ab.x + F_ab.y); let Favg = F0 + (1.0 - F0) / 21.0; let FmsEms = FssEss * Favg / (1.0 - Ems * Favg) * Ems; let Edss = 1.0 - (FssEss + FmsEms); @@ -352,6 +351,7 @@ fn environment_map_light( let F_ab = (*input).F_ab; let F0_dielectric = (*input).F0_dielectric; let F0_metallic = (*input).F0_metallic; + let specular_weight = (*input).specular_weight; let world_position = (*input).P; var out: EnvironmentMapLight; @@ -369,18 +369,14 @@ fn environment_map_light( return out; } - // No real world material has specular values under 0.02, so we use this range as a - // "pre-baked specular occlusion" that extinguishes the fresnel term, for artistic control. - // See: https://google.github.io/filament/Filament.md.html#specularocclusion let F0_surface = mix(F0_dielectric, F0_metallic, metallic); - let specular_occlusion = saturate(dot(F0_surface, vec3(50.0 * 0.33))); // Compute per-material (dielectric and metallic separately) then mix the results. // We can't use F0 directly as the multiscattering term is nonlinear. let Ems = 1.0 - (F_ab.x + F_ab.y); - let ms_dielectric = compute_multiscatter(F0_dielectric, F_ab, Ems, specular_occlusion); - let ms_metallic = compute_multiscatter(F0_metallic, F_ab, Ems, specular_occlusion); + let ms_dielectric = compute_multiscatter(F0_dielectric, F_ab, Ems); + let ms_metallic = compute_multiscatter(F0_metallic, F_ab, Ems); let FssEss = mix(ms_dielectric.FssEss, ms_metallic.FssEss, metallic); let FmsEms = mix(ms_dielectric.FmsEms, ms_metallic.FmsEms, metallic); @@ -392,7 +388,7 @@ fn environment_map_light( out.diffuse = vec3(0.0); } - out.specular = FssEss * radiances.radiance; + out.specular = specular_weight * FssEss * radiances.radiance; #ifdef STANDARD_MATERIAL_CLEARCOAT environment_map_light_clearcoat( diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index ed69dc0979acc..887f4f7bfc09d 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -169,24 +169,22 @@ pub struct StandardMaterial { #[dependency] pub metallic_roughness_texture: Option>, - /// Specular intensity for non-metals on a linear scale of `[0.0, 1.0]`. + /// Specular strength for non-metals on a linear scale of `[0.0, 1.0]`. /// - /// Use the value as a way to control the intensity of the - /// specular highlight of the material, i.e. how reflective is the material, - /// rather than the physical property "reflectance." + /// For non metals the specular reflectance at normal incidence is governed by the [`StandardMaterial::ior`] parameter. /// - /// Set to `0.0`, no specular highlight is visible, the highlight is strongest - /// when `reflectance` is set to `1.0`. + /// Use this value as a way to scale down this default reflectance, and thus the intensity of the + /// specular highlight of the material, i.e. how reflective the material ultimately is. /// - /// Defaults to `0.5` which is mapped to 4% reflectance in the shader. + /// Set to `0.0`, no specular highlight is visible. The highlight is strongest when `specular` is set to `1.0`. + /// + /// Defaults to `1.0`, which will compute the normal incidence reflectance according to the material's IOR. #[doc(alias = "specular_intensity")] - pub reflectance: f32, + pub specular: f32, - /// A color with which to modulate the [`StandardMaterial::reflectance`] for - /// non-metals. + /// A color with which to modulate the specular reflectance for non-metals. /// - /// The specular highlights and reflection are tinted with this color. Note - /// that it has no effect for non-metals. + /// The specular highlights and reflection are tinted with this color. /// /// This feature is currently unsupported in the deferred rendering path, in /// order to reduce the size of the geometry buffers. @@ -444,19 +442,15 @@ pub struct StandardMaterial { #[cfg(feature = "pbr_specular_textures")] pub specular_channel: UvChannel, - /// A map that specifies reflectance for non-metallic materials. + /// A map that adjusts the strength of the highlights and reflection for non-metallic materials. /// - /// Alpha values from [0.0, 1.0] in this texture are linearly mapped to - /// reflectance values of [0.0, 0.5] and multiplied by the constant - /// [`StandardMaterial::reflectance`] value. This follows the - /// `KHR_materials_specular` specification. The map will have no effect if + /// Alpha values from [0.0, 1.0] in this texture will be multiplied with the constant + /// [`StandardMaterial::specular`] value, to obtain a strength factor that will linearly + /// linearly scale the default specular reflectance of the material. + /// This follows the `KHR_materials_specular` specification. The map will have no effect if /// the material is fully metallic. /// - /// When using this map, you may wish to set the - /// [`StandardMaterial::reflectance`] value to 2.0 so that this map can - /// express the full [0.0, 1.0] range of values. - /// - /// Note that, because the reflectance is stored in the alpha channel, and + /// Note that, because the specular strength is stored in the alpha channel, and /// the [`StandardMaterial::specular_tint_texture`] has no alpha value, it /// may be desirable to pack the values together and supply the same /// texture to both fields. @@ -866,10 +860,6 @@ impl Default for StandardMaterial { metallic: 0.0, metallic_roughness_channel: UvChannel::Uv0, metallic_roughness_texture: None, - // Minimum real-world reflectance is 2%, most materials between 2-5% - // Expressed in a linear scale and equivalent to 4% reflectance see - // - reflectance: 0.5, diffuse_transmission: 0.0, #[cfg(feature = "pbr_transmission_textures")] diffuse_transmission_channel: UvChannel::Uv0, @@ -892,6 +882,7 @@ impl Default for StandardMaterial { occlusion_texture: None, normal_map_channel: UvChannel::Uv0, normal_map_texture: None, + specular: 1.0, #[cfg(feature = "pbr_specular_textures")] specular_channel: UvChannel::Uv0, #[cfg(feature = "pbr_specular_textures")] @@ -1020,9 +1011,10 @@ pub struct StandardMaterialUniform { pub attenuation_color: Vec4, /// The transform applied to the UVs corresponding to `ATTRIBUTE_UV_0` on the mesh before sampling. Default is identity. pub uv_transform: Mat3, - /// Specular intensity for non-metals on a linear scale of [0.0, 1.0] - /// defaults to 0.5 which is mapped to 4% reflectance in the shader - pub reflectance: Vec3, + /// Specular tint modulating non-metals specular reflectance. + pub specular_tint: Vec3, + /// Specular strength for non-metals on a linear scale of [0.0, 1.0]. Default is 1.0. + pub specular_weight: f32, /// Linear perceptual roughness, clamped to [0.089, 1.0] in the shader /// Defaults to minimum of 0.089 pub roughness: f32, @@ -1187,7 +1179,8 @@ impl AsBindGroupShaderType for StandardMaterial { emissive, roughness: self.perceptual_roughness, metallic: self.metallic, - reflectance: LinearRgba::from(self.specular_tint).to_vec3() * self.reflectance, + specular_tint: LinearRgba::from(self.specular_tint).to_vec3(), + specular_weight: self.specular, clearcoat: self.clearcoat, clearcoat_perceptual_roughness: self.clearcoat_perceptual_roughness, anisotropy_strength: self.anisotropy_strength, diff --git a/crates/bevy_pbr/src/render/pbr_fragment.wgsl b/crates/bevy_pbr/src/render/pbr_fragment.wgsl index b6d58a44146b9..aa85fd9dc4436 100644 --- a/crates/bevy_pbr/src/render/pbr_fragment.wgsl +++ b/crates/bevy_pbr/src/render/pbr_fragment.wgsl @@ -257,12 +257,15 @@ pbr_input.material.uv_transform = uv_transform; pbr_input.material.attenuation_distance = pbr_bindings::material.attenuation_distance; #endif // BINDLESS - // reflectance + // Specular reflectance #ifdef BINDLESS - pbr_input.material.reflectance = - pbr_bindings::material_array[material_indices[slot].material].reflectance; + pbr_input.material.specular_tint = + pbr_bindings::material_array[material_indices[slot].material].specular_tint; + pbr_input.material.specular_weight = + pbr_bindings::material_array[material_indices[slot].material].specular_weight; #else // BINDLESS - pbr_input.material.reflectance = pbr_bindings::material.reflectance; + pbr_input.material.specular_tint = pbr_bindings::material.specular_tint; + pbr_input.material.specular_weight = pbr_bindings::material.specular_weight; #endif // BINDLESS #ifdef PBR_SPECULAR_TEXTURES_SUPPORTED @@ -295,9 +298,7 @@ pbr_input.material.uv_transform = uv_transform; bias.mip_bias, #endif // MESHLET_MESH_MATERIAL_PASS ).a; - // This 0.5 factor is from the `KHR_materials_specular` specification: - // - pbr_input.material.reflectance *= specular * 0.5; + pbr_input.material.specular_weight *= specular; } // Specular tint texture @@ -327,7 +328,7 @@ pbr_input.material.uv_transform = uv_transform; bias.mip_bias, #endif // MESHLET_MESH_MATERIAL_PASS ).rgb; - pbr_input.material.reflectance *= specular_tint; + pbr_input.material.specular_tint *= specular_tint; } #endif // VERTEX_UVS diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index e3ce30015a779..36098ac88e82a 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -291,15 +291,13 @@ fn calculate_diffuse_color( (1.0 - diffuse_transmission); } -// Remapping [0,1] reflectance to F0 for dielectrics -fn calculate_F0_dielectric(reflectance: vec3) -> vec3 { - return 0.16 * reflectance * reflectance; -} - -// Remapping [0,1] reflectance to F0 -// See https://google.github.io/filament/Filament.md.html#materialsystem/parameterization/remapping -fn calculate_F0(base_color: vec3, metallic: f32, reflectance: vec3) -> vec3 { - return mix(calculate_F0_dielectric(reflectance), base_color, metallic); +// Reflectance at normal incidence for dielectrics is computed from IOR, then optionally tinted (non-physically). +fn calculate_F0_dielectric(ior: f32, specular_tint: vec3) -> vec3 { + // Physical reflectance at normal incidence + var f0 = max(0.0, (ior - 1.0) / (ior + 1.0)); + f0 *= f0; + // Artistic tint + return min(f0 * specular_tint, vec3(1.0)); } #ifdef CONTACT_SHADOWS @@ -349,13 +347,13 @@ fn apply_pbr_lighting( let emissive = in.material.emissive; - // calculate non-linear roughness from linear perceptualRoughness let metallic = in.material.metallic; + let specular_weight = in.material.specular_weight; + + // calculate non-linear roughness from linear perceptualRoughness let perceptual_roughness = in.material.perceptual_roughness; let roughness = lighting::perceptualRoughnessToRoughness(perceptual_roughness); - let ior = in.material.ior; let thickness = in.material.thickness; - let reflectance = in.material.reflectance; let diffuse_transmission = in.material.diffuse_transmission; let specular_transmission = in.material.specular_transmission; @@ -377,6 +375,10 @@ fn apply_pbr_lighting( let clearcoat_N = in.clearcoat_N; let clearcoat_NdotV = max(dot(clearcoat_N, in.V), 0.0001); let clearcoat_R = reflect(-in.V, clearcoat_N); + // Correct the base layer's IOR to account for the change of interface from air (IOR = 1) to the clearcoat layer (IOR = 1.5). + let ior = in.material.ior / mix(1.0, 1.5, clearcoat); +#else + let ior = in.material.ior; #endif // STANDARD_MATERIAL_CLEARCOAT let diffuse_color = calculate_diffuse_color( @@ -392,7 +394,13 @@ fn apply_pbr_lighting( // Calculate the world position of the second Lambertian lobe used for diffuse transmission, by subtracting material thickness let diffuse_transmissive_lobe_world_position = in.world_position - vec4(in.world_normal, 0.0) * thickness; - let F0 = calculate_F0(output_color.rgb, metallic, reflectance); + let F0_dielectric = calculate_F0_dielectric(ior, in.material.specular_tint); + + // Reflectance at normal incidence for pure metals is the base color + let F0_metallic = output_color.rgb; + + let F0 = mix(F0_dielectric, F0_metallic, metallic); + let F_ab = lighting::F_AB(perceptual_roughness, NdotV); var direct_light: vec3 = vec3(0.0); @@ -411,8 +419,9 @@ fn apply_pbr_lighting( lighting_input.V = in.V; lighting_input.diffuse_color = diffuse_color; lighting_input.metallic = metallic; - lighting_input.F0_dielectric = calculate_F0_dielectric(reflectance); - lighting_input.F0_metallic = output_color.rgb; + lighting_input.specular_weight = specular_weight; + lighting_input.F0_dielectric = F0_dielectric; + lighting_input.F0_metallic = F0_metallic; lighting_input.F_ab = F_ab; #ifdef STANDARD_MATERIAL_CLEARCOAT lighting_input.layers[LAYER_CLEARCOAT].NdotV = clearcoat_NdotV; @@ -440,6 +449,7 @@ fn apply_pbr_lighting( transmissive_lighting_input.V = -in.V; transmissive_lighting_input.diffuse_color = diffuse_transmissive_color; transmissive_lighting_input.metallic = 0.0; + transmissive_lighting_input.specular_weight = 1.0; transmissive_lighting_input.F0_dielectric = vec3(0.0); transmissive_lighting_input.F0_metallic = vec3(0.0); transmissive_lighting_input.F_ab = vec2(0.1); @@ -849,7 +859,7 @@ fn apply_pbr_lighting( emissive_light = emissive_light * mix(1.0, view_bindings::view.exposure, emissive.a); #ifdef STANDARD_MATERIAL_SPECULAR_TRANSMISSION - transmitted_light += transmission::specular_transmissive_light(in.world_position, in.frag_coord.xyz, view_z, in.N, in.V, F0, ior, thickness, perceptual_roughness, specular_transmissive_color, specular_transmitted_environment_light).rgb; + transmitted_light += transmission::specular_transmissive_light(in.world_position, in.frag_coord.xyz, view_z, in.N, in.V, F0, specular_weight, ior, thickness, perceptual_roughness, specular_transmissive_color, specular_transmitted_environment_light).rgb; if (in.material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_ATTENUATION_ENABLED_BIT) != 0u { // We reuse the `atmospheric_fog()` function here, as it's fundamentally diff --git a/crates/bevy_pbr/src/render/pbr_lighting.wgsl b/crates/bevy_pbr/src/render/pbr_lighting.wgsl index e9fb29ee34d20..d1648f30071ca 100644 --- a/crates/bevy_pbr/src/render/pbr_lighting.wgsl +++ b/crates/bevy_pbr/src/render/pbr_lighting.wgsl @@ -81,6 +81,9 @@ struct LightingInput { // The 0-1 metallic factor of the material. metallic: f32, + // Specular weight for dielectric materials + specular_weight: f32, + // Specular reflectance at the normal incidence angle. F0_dielectric: vec3, F0_metallic: vec3, @@ -315,13 +318,6 @@ fn F_Schlick(f0: f32, f90: f32, VdotH: f32) -> f32 { return f0 + (f90 - f0) * pow(1.0 - VdotH, 5.0); } -fn fresnel(f0: vec3, LdotH: f32) -> vec3 { - // f_90 suitable for ambient occlusion - // see https://google.github.io/filament/Filament.md.html#lighting/occlusion - let f90 = saturate(dot(f0, vec3(50.0 * 0.33))); - return F_Schlick_vec(f0, f90, LdotH); -} - // Given distribution, visibility, and Fresnel term, calculates the final // specular light. // @@ -419,7 +415,7 @@ fn specular( // Calculate visibility. let V = V_SmithGGXCorrelated(roughness, NdotV, NdotL); // Calculate the Fresnel term. - let F = fresnel(F0, LdotH); + let F = F_Schlick_vec(F0, 1.0, LdotH); // Calculate the specular light. let Fr = specular_multiscatter(D, V, F, F0, (*input).F_ab, specular_intensity); @@ -486,7 +482,7 @@ fn specular_anisotropy( let Da = D_GGX_anisotropic(at, ab, NdotH, TdotH, BdotH); let Va = V_GGX_anisotropic(at, ab, NdotL, NdotV, BdotV, TdotV, TdotL, BdotL); - let Fa = fresnel(F0, LdotH); + let Fa = F_Schlick_vec(F0, 1.0, LdotH); // Calculate the specular light. let Fr = specular_multiscatter(Da, Va, Fa, F0, (*input).F_ab, specular_intensity); @@ -651,6 +647,8 @@ fn point_light( let distance = sqrt(distance_square); let rangeAttenuation = getDistanceAttenuation(distance_square, (*light).color_inverse_square_range.w); + let specular_weight = (*input).specular_weight; + // Base layer let a = (*input).layers[LAYER_BASE].roughness; @@ -667,7 +665,11 @@ fn point_light( var specular_derived_input = derive_lighting_input(N, V, L_spec); let normalizationFactor = a / a_prime; - let specular_intensity = normalizationFactor * normalizationFactor; + var specular_intensity = normalizationFactor * normalizationFactor; + + // Scale the specular reflectance for dialectrics (following KHR_materials_specular). + // Contrary to only scaling F0 this scales the whole Fresnel response and can also cut grazing reflections (F90). + specular_intensity *= mix(specular_weight, 1.0, (*input).metallic); let brdf_roughness = mix(a, a_prime, specular_fix_remap(a)); @@ -737,6 +739,10 @@ fn point_light( var derived_input = derive_lighting_input(N, V, L); var diffuse = vec3(0.0); if (enable_diffuse) { + // KHR_materials_specular describes how the specular strength also influences the diffuse reflectance. + // We can't reproduce it however because we (following Filament) do not explicitly couple the diffuse and + // specular reflectances like glTF does, and have no term for specular compensation. + // See https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#coupling-diffuse-and-specular-reflection. diffuse = diffuse_color * Fd_Burley(input, &derived_input); } @@ -855,10 +861,14 @@ fn directional_light( diffuse = diffuse_color * Fd_Burley(input, &derived_input); } + // Scale the specular reflectance for dialectrics (following KHR_materials_specular). + // Contrary to only scaling F0 this scales the whole Fresnel response and can also cut grazing reflections (F90). + let specular_intensity = (*input).specular_weight; + #ifdef STANDARD_MATERIAL_ANISOTROPY - let specular_light = specular_anisotropy(input, &derived_input, L, roughness, 1.0); + let specular_light = specular_anisotropy(input, &derived_input, L, roughness, specular_intensity); #else // STANDARD_MATERIAL_ANISOTROPY - let specular_light = specular(input, &derived_input, roughness, 1.0); + let specular_light = specular(input, &derived_input, roughness, specular_intensity); #endif // STANDARD_MATERIAL_ANISOTROPY #ifdef STANDARD_MATERIAL_CLEARCOAT diff --git a/crates/bevy_pbr/src/render/pbr_types.wgsl b/crates/bevy_pbr/src/render/pbr_types.wgsl index b8b51c577ecef..20a488349b788 100644 --- a/crates/bevy_pbr/src/render/pbr_types.wgsl +++ b/crates/bevy_pbr/src/render/pbr_types.wgsl @@ -7,7 +7,8 @@ struct StandardMaterial { emissive: vec4, attenuation_color: vec4, uv_transform: mat3x3, - reflectance: vec3, + specular_tint: vec3, + specular_weight: f32, perceptual_roughness: f32, metallic: f32, diffuse_transmission: f32, @@ -73,7 +74,8 @@ fn standard_material_new() -> StandardMaterial { material.emissive = vec4(0.0, 0.0, 0.0, 1.0); material.perceptual_roughness = 0.5; material.metallic = 0.00; - material.reflectance = vec3(0.5); + material.specular_weight = 1.0; + material.specular_tint = vec3(1.0); material.diffuse_transmission = 0.0; material.specular_transmission = 0.0; material.thickness = 0.0; diff --git a/crates/bevy_pbr/src/transmission/transmission.wgsl b/crates/bevy_pbr/src/transmission/transmission.wgsl index 06e3c0e4512b1..6efb88ee920e3 100644 --- a/crates/bevy_pbr/src/transmission/transmission.wgsl +++ b/crates/bevy_pbr/src/transmission/transmission.wgsl @@ -23,7 +23,7 @@ fn ior_corrected_roughness(roughness: f32, ior: f32) -> f32 { return roughness * clamp(ior * 2.0 - 2.0, 0.0, 1.0); } -fn specular_transmissive_light(world_position: vec4, frag_coord: vec3, view_z: f32, N: vec3, V: vec3, F0: vec3, ior: f32, thickness: f32, perceptual_roughness: f32, specular_transmissive_color: vec3, transmitted_environment_light_specular: vec3) -> vec3 { +fn specular_transmissive_light(world_position: vec4, frag_coord: vec3, view_z: f32, N: vec3, V: vec3, F0: vec3, specular_weight: f32, ior: f32, thickness: f32, perceptual_roughness: f32, specular_transmissive_color: vec3, transmitted_environment_light_specular: vec3) -> vec3 { // Calculate the ratio between refraction indexes. Assume air/vacuum for the space outside the mesh let eta = 1.0 / ior; @@ -61,7 +61,7 @@ fn specular_transmissive_light(world_position: vec4, frag_coord: vec3, let MinusNdotT = dot(-N, T); // Calculate 1.0 - fresnel factor (how much light is _NOT_ reflected, i.e. how much is transmitted) - let F = vec3(1.0) - lighting::fresnel(F0, MinusNdotT); + let F = vec3(1.0) - specular_weight * lighting::F_Schlick_vec(F0, 1.0, MinusNdotT); // Calculate final color by applying fresnel multiplied specular transmissive color to a mix of background color and transmitted specular environment light return F * specular_transmissive_color * mix(transmitted_environment_light_specular, background_color.rgb, background_color.a); From c17beaac2edc0eae48ab1b0df7dd660cbddbe2d3 Mon Sep 17 00:00:00 2001 From: Christophe Dehais Date: Mon, 8 Jun 2026 10:07:49 +0200 Subject: [PATCH 2/5] Fix examples --- crates/bevy_pbr/src/ssr/ssr.wgsl | 14 +++++++++----- examples/3d/fog.rs | 2 +- examples/3d/light_probe_blending.rs | 1 - examples/3d/motion_blur.rs | 2 +- examples/3d/parallax_mapping.rs | 2 +- examples/3d/pccm.rs | 1 - examples/3d/render_to_texture.rs | 4 ++-- examples/3d/specular_tint.rs | 7 ++----- examples/3d/ssao.rs | 4 ++-- examples/3d/transmission.rs | 22 +++++++++++----------- examples/ui/render_ui_to_texture.rs | 2 +- tests/3d/test_invalid_skinned_mesh.rs | 4 ++-- 12 files changed, 32 insertions(+), 33 deletions(-) diff --git a/crates/bevy_pbr/src/ssr/ssr.wgsl b/crates/bevy_pbr/src/ssr/ssr.wgsl index d6e27f8219755..c7bd90bf82351 100644 --- a/crates/bevy_pbr/src/ssr/ssr.wgsl +++ b/crates/bevy_pbr/src/ssr/ssr.wgsl @@ -61,7 +61,7 @@ struct BrdfSample { 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); @@ -186,7 +186,10 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { 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); + + let F0_dielectric = pbr_functions::calculate_F0_dielectric(pbr_input.material.ior, pbr_input.material.specular_tint); + let F0_metallic = pbr_input.material.base_color.rgb; + let F0 = mix(F0_dielectric, F0_metallic, pbr_input.material.metallic); // 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 @@ -220,7 +223,7 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { // 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; @@ -238,7 +241,8 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { // Unpack values required for environment mapping. let base_color = pbr_input.material.base_color.rgb; let metallic = pbr_input.material.metallic; - let reflectance = pbr_input.material.reflectance; + let ior = pbr_input.material.ior; + let specular_tint = pbr_input.material.specular_tint; let specular_transmission = pbr_input.material.specular_transmission; let diffuse_transmission = pbr_input.material.diffuse_transmission; @@ -263,7 +267,7 @@ 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_dielectric = pbr_functions::calculate_F0_dielectric(reflectance); + let F0_dielectric = pbr_functions::calculate_F0_dielectric(ior, specular_tint); // Don't add stochastic noise to hits that sample the prefiltered env map. // The prefiltered env map already accounts for roughness. diff --git a/examples/3d/fog.rs b/examples/3d/fog.rs index d33968cddda94..504d754efa39e 100644 --- a/examples/3d/fog.rs +++ b/examples/3d/fog.rs @@ -78,7 +78,7 @@ fn setup_pyramid_scene( Mesh3d(meshes.add(Sphere::default())), MeshMaterial3d(materials.add(StandardMaterial { base_color: Srgba::hex("126212CC").unwrap().into(), - reflectance: 1.0, + ior: 2.33, perceptual_roughness: 0.0, metallic: 0.5, alpha_mode: AlphaMode::Blend, diff --git a/examples/3d/light_probe_blending.rs b/examples/3d/light_probe_blending.rs index 535e11accf10b..fc3b6357cb1fe 100644 --- a/examples/3d/light_probe_blending.rs +++ b/examples/3d/light_probe_blending.rs @@ -237,7 +237,6 @@ fn create_reflective_material( materials.add(StandardMaterial { base_color: WHITE.into(), metallic: 1.0, - reflectance: 1.0, perceptual_roughness: 0.0, ..default() }) diff --git a/examples/3d/motion_blur.rs b/examples/3d/motion_blur.rs index d5a03cadefcdb..63a80ba73db0e 100644 --- a/examples/3d/motion_blur.rs +++ b/examples/3d/motion_blur.rs @@ -180,7 +180,7 @@ fn spawn_barriers( let capsule = meshes.add(Capsule3d::default()); let matl = materials.add(StandardMaterial { base_color: Color::srgb_u8(255, 87, 51), - reflectance: 1.0, + specular_tint: Color::linear_rgb(4.0, 4.0, 4.0), ..default() }); let mut spawn_with_offset = |offset: f32| { diff --git a/examples/3d/parallax_mapping.rs b/examples/3d/parallax_mapping.rs index ef7e7cb4c1fb5..53d2172419b02 100644 --- a/examples/3d/parallax_mapping.rs +++ b/examples/3d/parallax_mapping.rs @@ -242,7 +242,7 @@ fn setup( // standard material derived from dark green, but // with roughness and reflectance set. perceptual_roughness: 0.45, - reflectance: 0.18, + specular_tint: Color::linear_rgb(0.77, 0.77, 0.77), ..Color::srgb_u8(0, 80, 0).into() })), Transform::from_xyz(0.0, -1.0, 0.0), diff --git a/examples/3d/pccm.rs b/examples/3d/pccm.rs index 79f8ffb80b2f8..6119ba7a1d522 100644 --- a/examples/3d/pccm.rs +++ b/examples/3d/pccm.rs @@ -118,7 +118,6 @@ fn spawn_inner_cube( let cube_material = materials.add(StandardMaterial { base_color: Color::WHITE, metallic: 1.0, - reflectance: 1.0, perceptual_roughness: 0.0, ..default() }); diff --git a/examples/3d/render_to_texture.rs b/examples/3d/render_to_texture.rs index 2c3339cdf4265..9552202b5db9d 100644 --- a/examples/3d/render_to_texture.rs +++ b/examples/3d/render_to_texture.rs @@ -40,7 +40,7 @@ fn setup( let cube_handle = meshes.add(Cuboid::new(4.0, 4.0, 4.0)); let cube_material_handle = materials.add(StandardMaterial { base_color: Color::srgb(0.8, 0.7, 0.6), - reflectance: 0.02, + specular: 0.04, unlit: false, ..default() }); @@ -86,7 +86,7 @@ fn setup( // This material has the texture that has been rendered. let material_handle = materials.add(StandardMaterial { base_color_texture: Some(image_handle), - reflectance: 0.02, + specular: 0.04, unlit: false, ..default() }); diff --git a/examples/3d/specular_tint.rs b/examples/3d/specular_tint.rs index 81655cfebd570..ba6dfa5ab061c 100644 --- a/examples/3d/specular_tint.rs +++ b/examples/3d/specular_tint.rs @@ -107,7 +107,7 @@ fn setup( // We want only reflected specular light here, so we set the base // color as black. base_color: Color::BLACK, - reflectance: 1.0, + specular: 2.0, specular_tint: Color::hsva(app_status.hue, 1.0, 1.0, 1.0), // The object must not be metallic, or else the reflectance is // ignored per the Filament spec: @@ -199,13 +199,10 @@ fn toggle_specular_map( // Adjust the tint type. match app_status.tint_type { TintType::Solid => { - material.reflectance = 1.0; + material.specular_tint = Color::linear_rgb(2.0, 2.0, 2.0); material.specular_tint_texture = None; } TintType::Map => { - // Set reflectance to 2.0 to spread out the map's reflectance - // range from the default [0.0, 0.5] to [0.0, 1.0]. - material.reflectance = 2.0; // As the tint map is multiplied by the tint color, we set the // latter to white so that only the map has an effect. material.specular_tint = WHITE.into(); diff --git a/examples/3d/ssao.rs b/examples/3d/ssao.rs index 3de273ed71889..036f3394a2371 100644 --- a/examples/3d/ssao.rs +++ b/examples/3d/ssao.rs @@ -39,7 +39,7 @@ fn setup( let material = materials.add(StandardMaterial { base_color: Color::srgb(0.5, 0.5, 0.5), perceptual_roughness: 1.0, - reflectance: 0.0, + specular_tint: Color::linear_rgb(0.0, 0.0, 0.0), ..default() }); commands.spawn(( @@ -62,7 +62,7 @@ fn setup( MeshMaterial3d(materials.add(StandardMaterial { base_color: Color::srgb(0.4, 0.4, 0.4), perceptual_roughness: 1.0, - reflectance: 0.0, + specular_tint: Color::linear_rgb(0.0, 0.0, 0.0), ..default() })), SphereMarker, diff --git a/examples/3d/transmission.rs b/examples/3d/transmission.rs index 3e969ffdfa3ac..8ca6c79e9a6ad 100644 --- a/examples/3d/transmission.rs +++ b/examples/3d/transmission.rs @@ -11,7 +11,7 @@ //! | `A` / `S` | Decrease / Increase Thickness | //! | `Z` / `X` | Decrease / Increase IOR | //! | `E` / `R` | Decrease / Increase Perceptual Roughness | -//! | `U` / `I` | Decrease / Increase Reflectance | +//! | `U` / `I` | Decrease / Increase Specular strength | //! | Arrow Keys | Control Camera | //! | `C` | Randomize Colors | //! | `H` | Toggle HDR + Bloom | @@ -226,14 +226,14 @@ fn setup( // Chessboard Plane let black_material = materials.add(StandardMaterial { base_color: Color::BLACK, - reflectance: 0.3, + specular: 0.85, perceptual_roughness: 0.8, ..default() }); let white_material = materials.add(StandardMaterial { base_color: Color::WHITE, - reflectance: 0.3, + specular: 0.85, perceptual_roughness: 0.8, ..default() }); @@ -264,7 +264,7 @@ fn setup( base_color: Color::WHITE, diffuse_transmission: 0.6, perceptual_roughness: 0.8, - reflectance: 1.0, + specular_tint: Color::linear_rgb(4.0, 4.0, 4.0), double_sided: true, cull_mode: None, ..default() @@ -351,7 +351,7 @@ struct ExampleState { thickness: f32, ior: f32, perceptual_roughness: f32, - reflectance: f32, + specular: f32, auto_camera: bool, } @@ -366,7 +366,7 @@ impl Default for ExampleState { thickness: 1.8, ior: 1.5, perceptual_roughness: 0.12, - reflectance: 0.5, + specular: 1.0, auto_camera: true, } } @@ -417,9 +417,9 @@ fn example_control_system( } if input.pressed(KeyCode::KeyI) { - state.reflectance = (state.reflectance + time.delta_secs()).min(1.0); + state.specular = (state.specular + time.delta_secs()).min(1.0); } else if input.pressed(KeyCode::KeyU) { - state.reflectance = (state.reflectance - time.delta_secs()).max(0.0); + state.specular = (state.specular - time.delta_secs()).max(0.0); } if input.pressed(KeyCode::KeyR) { @@ -437,7 +437,7 @@ fn example_control_system( material.thickness = state.thickness; material.ior = state.ior; material.perceptual_roughness = state.perceptual_roughness; - material.reflectance = state.reflectance; + material.specular = state.specular; } if controls.diffuse_transmission { @@ -550,7 +550,7 @@ fn example_control_system( " A / S Thickness: {:.2}\n", " Z / X IOR: {:.2}\n", " E / R Perceptual Roughness: {:.2}\n", - " U / I Reflectance: {:.2}\n", + " U / I Specular: {:.2}\n", " Arrow Keys Control Camera\n", " C Randomize Colors\n", " H HDR + Bloom: {}\n", @@ -564,7 +564,7 @@ fn example_control_system( state.thickness, state.ior, state.perceptual_roughness, - state.reflectance, + state.specular, if hdr { "ON " } else { "OFF" }, if cfg!(any(feature = "webgpu", not(target_arch = "wasm32"))) { if depth_prepass.is_some() { diff --git a/examples/ui/render_ui_to_texture.rs b/examples/ui/render_ui_to_texture.rs index 304e299e781fa..48a85798da72a 100644 --- a/examples/ui/render_ui_to_texture.rs +++ b/examples/ui/render_ui_to_texture.rs @@ -120,7 +120,7 @@ fn setup( // This material has the texture that has been rendered. let material_handle = materials.add(StandardMaterial { base_color_texture: Some(image_handle), - reflectance: 0.02, + specular: 0.04, unlit: false, ..default() }); diff --git a/tests/3d/test_invalid_skinned_mesh.rs b/tests/3d/test_invalid_skinned_mesh.rs index cbd1082fcdb37..46ca35213496b 100644 --- a/tests/3d/test_invalid_skinned_mesh.rs +++ b/tests/3d/test_invalid_skinned_mesh.rs @@ -83,7 +83,7 @@ fn setup_environment( Mesh3d(mesh_assets.add(Plane3d::default().mesh().size(100.0, 100.0).normal(Dir3::Z))), MeshMaterial3d(material_assets.add(StandardMaterial { base_color: Color::srgb(0.05, 0.05, 0.15), - reflectance: 0.2, + specular: 0.4, ..default() })), )); @@ -149,7 +149,7 @@ fn setup_meshes( let background_material_handle = material_assets.add(StandardMaterial { base_color: Color::srgb(0.05, 0.15, 0.05), - reflectance: 0.2, + specular: 0.4, ..default() }); From dcbccf73f4d44bcba540f8e6d244672d014b6fa1 Mon Sep 17 00:00:00 2001 From: Christophe Dehais Date: Mon, 8 Jun 2026 10:07:49 +0200 Subject: [PATCH 3/5] Fix Solari --- .../src/realtime/resolve_dlss_rr_textures.wgsl | 6 ++++-- crates/bevy_solari/src/realtime/specular_gi.wgsl | 6 ++++-- crates/bevy_solari/src/scene/binder.rs | 2 +- crates/bevy_solari/src/scene/brdf.wgsl | 8 ++++---- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/crates/bevy_solari/src/realtime/resolve_dlss_rr_textures.wgsl b/crates/bevy_solari/src/realtime/resolve_dlss_rr_textures.wgsl index 066cc8b56f288..5e0be9a5872e0 100644 --- a/crates/bevy_solari/src/realtime/resolve_dlss_rr_textures.wgsl +++ b/crates/bevy_solari/src/realtime/resolve_dlss_rr_textures.wgsl @@ -1,7 +1,7 @@ enable wgpu_ray_query; #define_import_path bevy_solari::resolve_dlss_rr_textures -#import bevy_pbr::pbr_functions::{calculate_diffuse_color, calculate_F0} +#import bevy_pbr::pbr_functions::{calculate_diffuse_color, calculate_F0_dielectric} #import bevy_render::view::View #import bevy_solari::gbuffer_utils::gpixel_resolve #import bevy_solari::realtime_bindings::{gbuffer, depth_buffer, motion_vectors, view, diffuse_albedo, specular_albedo, normal_roughness, specular_motion_vectors} @@ -22,7 +22,9 @@ fn resolve_dlss_rr_textures(@builtin(global_invocation_id) global_id: vec3) } let surface = gpixel_resolve(textureLoad(gbuffer, pixel_id, 0), depth, pixel_id, view.main_pass_viewport.zw, view.world_from_clip); - let F0 = calculate_F0(surface.material.base_color, surface.material.metallic, vec3(surface.material.reflectance)); + let F0_metallic = surface.material.base_color; + let F0_dielectric = calculate_F0_dielectric(1.5, vec3(1.0)); + let F0 = mix(F0_dielectric, F0_metallic, surface.material.metallic); let wo = normalize(view.world_position - surface.world_position); textureStore(diffuse_albedo, pixel_id, vec4(calculate_diffuse_color(surface.material.base_color, surface.material.metallic, 0.0, 0.0), 0.0)); diff --git a/crates/bevy_solari/src/realtime/specular_gi.wgsl b/crates/bevy_solari/src/realtime/specular_gi.wgsl index 205aedcdaaf8f..b4c9f3c430f4c 100644 --- a/crates/bevy_solari/src/realtime/specular_gi.wgsl +++ b/crates/bevy_solari/src/realtime/specular_gi.wgsl @@ -3,7 +3,7 @@ enable wgpu_ray_query; #define_import_path bevy_solari::specular_gi #import bevy_core_pipeline::tonemapping::tonemapping_luminance as luminance -#import bevy_pbr::pbr_functions::{calculate_diffuse_color, calculate_F0} +#import bevy_pbr::pbr_functions::{calculate_diffuse_color, calculate_F0_dielectric} #import bevy_pbr::utils::rand_f #import bevy_render::maths::{orthonormalize, PI} #import bevy_render::view::View @@ -220,7 +220,9 @@ fn replace_primary_surface(pixel_id: vec2, ray_hit: ResolvedRayHitFull, mir let virtual_previous_frame_position = (mirror_rotations * (ray_hit.previous_frame_world_position - primary_surface_world_position)) + primary_surface_world_position; let specular_motion_vector = calculate_motion_vector(virtual_position, virtual_previous_frame_position); - let F0 = calculate_F0(ray_hit.material.base_color, ray_hit.material.metallic, vec3(ray_hit.material.reflectance)); + let F0_metallic = ray_hit.material.base_color; + let F0_dielectric = calculate_F0_dielectric(1.5, vec3(1.0)); + let F0 = mix(F0_dielectric, F0_metallic, ray_hit.material.metallic); let wo = normalize(view.world_position - virtual_position); let virtual_normal = normalize(mirror_rotations * ray_hit.world_normal); diff --git a/crates/bevy_solari/src/scene/binder.rs b/crates/bevy_solari/src/scene/binder.rs index 4cb9cfd205aad..3374703ae251f 100644 --- a/crates/bevy_solari/src/scene/binder.rs +++ b/crates/bevy_solari/src/scene/binder.rs @@ -132,7 +132,7 @@ pub fn prepare_raytracing_scene_bindings( perceptual_roughness: material.perceptual_roughness, emissive: material.emissive.to_vec3(), metallic: material.metallic, - reflectance: material.reflectance, + reflectance: 1.0, // material.reflectance, _padding: Default::default(), }); diff --git a/crates/bevy_solari/src/scene/brdf.wgsl b/crates/bevy_solari/src/scene/brdf.wgsl index cc347d0021ac1..e607e77f65e12 100644 --- a/crates/bevy_solari/src/scene/brdf.wgsl +++ b/crates/bevy_solari/src/scene/brdf.wgsl @@ -42,7 +42,7 @@ fn evaluate_and_sample_brdf( let NdotV = dot(world_normal, wo); if NdotV < 0.0001 { return EvaluateAndSampleBrdfResult(vec3(0.0), vec3(0.0), 0.0); } let F0_metal = material.base_color; - let F0_dielectric = calculate_F0_dielectric(vec3(material.reflectance)); + let F0_dielectric = calculate_F0_dielectric(1.5, vec3(1.0)); let rho = lobe_reflectances(F0_metal, F0_dielectric, material, F_ab); let specular_weight = luminance(rho.specular) / luminance(rho.specular + rho.diffuse); let diffuse_weight = 1.0 - specular_weight; @@ -99,7 +99,7 @@ fn evaluate_diffuse_brdf(wo: vec3, wi: vec3, world_normal: vec3, let NdotV = dot(world_normal, wo); if NdotL < 0.0001 || NdotV < 0.0001 { return vec3(0.0); } let F0_metal = material.base_color; - let F0_dielectric = calculate_F0_dielectric(vec3(material.reflectance)); + let F0_dielectric = calculate_F0_dielectric(1.5, vec3(1.0)); let rho = lobe_reflectances(F0_metal, F0_dielectric, material, F_ab); return rho.diffuse / PI * NdotL; } @@ -113,7 +113,7 @@ fn evaluate_specular_brdf(wo: vec3, wi: vec3, world_normal: vec3, if NdotL < 0.0001 || NdotH < 0.0001 || LdotH < 0.0001 || NdotV < 0.0001 { return vec3(0.0); } let F0_metal = material.base_color; - let F0_dielectric = calculate_F0_dielectric(vec3(material.reflectance)); + let F0_dielectric = calculate_F0_dielectric(1.5, vec3(1.0)); if material.roughness <= MIRROR_ROUGHNESS_THRESHOLD { if abs(NdotH - 1.0) < 0.0001 { @@ -137,7 +137,7 @@ fn evaluate_specular_brdf(wo: vec3, wi: vec3, world_normal: vec3, fn brdf_pdf(wo: vec3, wi: vec3, world_normal: vec3, material: ResolvedMaterial, F_ab: vec2) -> f32 { let NdotV = max(dot(world_normal, wo), 0.0001); let F0_metal = material.base_color; - let F0_dielectric = calculate_F0_dielectric(vec3(material.reflectance)); + let F0_dielectric = calculate_F0_dielectric(1.5, vec3(1.0)); let rho = lobe_reflectances(F0_metal, F0_dielectric, material, F_ab); let specular_weight = luminance(rho.specular) / luminance(rho.specular + rho.diffuse); let diffuse_weight = 1.0 - specular_weight; From f71ea44e30a92512e258e15fa7f85902f7edcdd6 Mon Sep 17 00:00:00 2001 From: Christophe Dehais Date: Mon, 8 Jun 2026 17:20:24 +0200 Subject: [PATCH 4/5] Fix WebGL2 --- crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl b/crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl index 0af6696310d33..5e051e2faedec 100644 --- a/crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl +++ b/crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl @@ -33,7 +33,7 @@ fn deferred_gbuffer_from_pbr_input(in: PbrInput) -> vec4 { #ifdef WEBGL2 // More crunched for webgl so we can also fit depth. let ior_specular = deferred_types::pack_4bit_ior_specular(in.material.ior, in.material.specular_weight); let ior_specular_props = f32(ior_specular) / 15.0; - var props = deferred_types::pack_unorm3x4_unorm_20_(vec4( + var props = deferred_types::unpack_unorm3x4_plus_unorm_20_(vec4( ior_specular_props, in.material.metallic, diffuse_occlusion, @@ -64,7 +64,7 @@ fn deferred_gbuffer_from_pbr_input(in: PbrInput) -> vec4 { } // Utilize the emissive channel to transmit the lightmap data. To ensure - // it matches the output in forward shading, pre-multiply it with the + // it matches the output in forward shading, pre-multiply it with the // calculated diffuse color. let base_color = in.material.base_color.rgb; let metallic = in.material.metallic; @@ -108,7 +108,7 @@ fn pbr_input_from_deferred_gbuffer(frag_coord: vec4, gbuffer: vec4) -> } #ifdef WEBGL2 // More crunched for webgl so we can also fit depth. let props = deferred_types::unpack_unorm3x4_plus_unorm_20_(gbuffer.b); - let ior_specular_props = u32(rounds(props.g * 15.0)); + let ior_specular_props = u32(round(props.r * 15.0)); let ior_specular = deferred_types::unpack_4bit_ior_specular(ior_specular_props); #else let props = deferred_types::unpack_unorm4x8_(gbuffer.b); From 671d13f2905efe2ff9a6cffad7a2fd0851a847fd Mon Sep 17 00:00:00 2001 From: Christophe Dehais Date: Mon, 8 Jun 2026 17:32:43 +0200 Subject: [PATCH 5/5] Remove stale comment about KHR_materials_specular --- .../src/loader/extensions/khr_materials_specular.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/crates/bevy_gltf/src/loader/extensions/khr_materials_specular.rs b/crates/bevy_gltf/src/loader/extensions/khr_materials_specular.rs index c0309ad5efcb6..3b900984482be 100644 --- a/crates/bevy_gltf/src/loader/extensions/khr_materials_specular.rs +++ b/crates/bevy_gltf/src/loader/extensions/khr_materials_specular.rs @@ -10,18 +10,6 @@ use {crate::loader::gltf_ext::material::parse_material_extension_texture, bevy_m /// Parsed data from the `KHR_materials_specular` extension. /// -/// We currently don't parse `specularFactor` and `specularTexture`, since -/// they're incompatible with Filament. -/// -/// Note that the map is a *specular map*, not a *reflectance map*. In Bevy and -/// Filament terms, the reflectance values in the specular map range from [0.0, -/// 0.5], rather than [0.0, 1.0]. This is an unfortunate -/// `KHR_materials_specular` specification requirement that stems from the fact -/// that glTF is specified in terms of a specular strength model, not the -/// reflectance model that Filament and Bevy use. A workaround, which is noted -/// in the [`StandardMaterial`](https://docs.rs/bevy/latest/bevy/pbr/struct.StandardMaterial.html) documentation, is to set the reflectance value -/// to 2.0, which spreads the specular map range from [0.0, 1.0] as normal. -/// /// See the specification: /// #[derive(Default)]