diff --git a/native/shared/src/renderer/froxel.rs b/native/shared/src/renderer/froxel.rs index 200ad9e1..dcb8cf17 100644 --- a/native/shared/src/renderer/froxel.rs +++ b/native/shared/src/renderer/froxel.rs @@ -167,15 +167,8 @@ const CLUSTERED_LOOP: &str = " let l = to_light / dist; let atten = 1.0 - (dist / range); let atten2 = atten * atten; - if (has_spec_gloss) { - lit += shade_specular_glossiness_pbr( - n, v, l, pl.color.rgb, pl.color.w * atten2, - base_color, authored_specular, roughness, - ); - } else { - lit += shade_pbr(n, v, l, pl.color.rgb, pl.color.w * atten2, - base_color, metallic, roughness); - } + lit += shade_pbr_f0(n, v, l, pl.color.rgb, pl.color.w * atten2, + light_diffuse, light_f0, roughness); } } "; @@ -191,10 +184,15 @@ pub(super) fn clustered_scene_shader(source: &str) -> String { .expect("scene shader missing END marker") + end_marker.len(); let clustered_loop = if source.contains("fn shade_layered_base_pbr(") { - CLUSTERED_LOOP.replace( - "shade_pbr(n, v,", - "shade_layered_pbr(layered_surface, n, v,", - ) + CLUSTERED_LOOP + .replace( + "shade_pbr_f0(n, v,", + "shade_layered_pbr(layered_surface, n, v,", + ) + .replace( + "light_diffuse, light_f0, roughness", + "base_color, metallic, roughness", + ) } else { CLUSTERED_LOOP.to_owned() }; diff --git a/native/shared/src/renderer/layered_pbr_scene.rs b/native/shared/src/renderer/layered_pbr_scene.rs index 0962b843..743375af 100644 --- a/native/shared/src/renderer/layered_pbr_scene.rs +++ b/native/shared/src/renderer/layered_pbr_scene.rs @@ -377,10 +377,17 @@ fn layered_secondary_uv(in: VertexOutputScene) -> vec2 { "direct diffuse complement", ); + // The direct lights take the layered BRDF, from the base colour and + // metallic (glTF's layered extensions are metallic/roughness only; the + // specular/glossiness extension doesn't combine with them). source = source.replace( - "shade_pbr(n, v,", + "shade_pbr_f0(n, v,", "shade_layered_pbr(layered_surface, n, v,", ); + source = source.replace( + "light_diffuse, light_f0, roughness", + "base_color, metallic, roughness", + ); assert_eq!( source .matches("shade_layered_pbr(layered_surface, n, v,") diff --git a/native/shared/src/renderer/shaders/core.rs b/native/shared/src/renderer/shaders/core.rs index 7ef75b84..ecacabe4 100644 --- a/native/shared/src/renderer/shaders/core.rs +++ b/native/shared/src/renderer/shaders/core.rs @@ -712,7 +712,14 @@ fn shade_pbr( // failure case. Keeping the authored F0 here preserves both lobes while // sharing the established GGX distribution, visibility, energy bound, and // roughness policy with the ordinary material path. -fn shade_specular_glossiness_pbr( +// Direct GGX for a surface given as a diffuse colour and an F0: both the +// specular/glossiness workflow (authored) and metallic/roughness (diffuse = +// base * (1 - metallic), F0 = mix(0.04, base, metallic), exactly shade_pbr) +// light through this one function, so the scene shader inlines one BRDF per +// light instead of one per workflow (the second copy was 8 ms of a 38 ms +// main pass on a mobile GPU, Pixel 4a). shade_pbr stays the metallic form +// the layered-material variant is derived from. +fn shade_pbr_f0( n: vec3, v: vec3, l_dir: vec3, @@ -1032,6 +1039,14 @@ fn shade_main_scene(in: VertexOutputScene, front_facing: bool) -> SceneOut { let dielectric_floor = select(0.15, 0.045, has_spec_gloss); var roughness = max(roughness_raw, dielectric_floor * (1.0 - metallic)); + // What the lights see of either workflow: a diffuse colour and an F0 + // (see shade_pbr_f0). + let light_diffuse = select(base_color * (1.0 - metallic), base_color, has_spec_gloss); + let light_f0 = select( + mix(vec3(0.04), base_color, metallic), + authored_specular, + has_spec_gloss, + ); // Specular antialiasing. Two sources of variance are folded into // GGX α² as additive corrections: @@ -1120,16 +1135,9 @@ fn shade_main_scene(in: VertexOutputScene, front_facing: bool) -> SceneOut { lit += base_color / PI * lighting.light_color.rgb * lighting.light_dir.w * ndl_wrap * direct_shadow; } else { - if (has_spec_gloss) { - lit += shade_specular_glossiness_pbr( - n, v, legacy_dir, lighting.light_color.rgb, - lighting.light_dir.w, base_color, authored_specular, roughness, - ) * direct_shadow; - } else { - lit += shade_pbr(n, v, legacy_dir, lighting.light_color.rgb, - lighting.light_dir.w, base_color, metallic, roughness) - * direct_shadow; - } + lit += shade_pbr_f0(n, v, legacy_dir, lighting.light_color.rgb, + lighting.light_dir.w, light_diffuse, light_f0, roughness) + * direct_shadow; } // Foliage backlit transmission — sun bleeding THROUGH alpha-cut leaf cards @@ -1149,15 +1157,8 @@ fn shade_main_scene(in: VertexOutputScene, front_facing: bool) -> SceneOut { for (var i = 0u; i < dir_count; i++) { let dl = lighting.dir_lights[i]; let l = normalize(dl.direction.xyz); - if (has_spec_gloss) { - lit += shade_specular_glossiness_pbr( - n, v, l, dl.color.rgb, dl.direction.w, - base_color, authored_specular, roughness, - ); - } else { - lit += shade_pbr(n, v, l, dl.color.rgb, dl.direction.w, - base_color, metallic, roughness); - } + lit += shade_pbr_f0(n, v, l, dl.color.rgb, dl.direction.w, + light_diffuse, light_f0, roughness); } // BEGIN-POINT-LIGHT-LOOP (replaced by the froxel-clustered variant @@ -1174,15 +1175,8 @@ fn shade_main_scene(in: VertexOutputScene, front_facing: bool) -> SceneOut { let l = to_light / dist; let atten = 1.0 - (dist / range); let atten2 = atten * atten; - if (has_spec_gloss) { - lit += shade_specular_glossiness_pbr( - n, v, l, pl.color.rgb, pl.color.w * atten2, - base_color, authored_specular, roughness, - ); - } else { - lit += shade_pbr(n, v, l, pl.color.rgb, pl.color.w * atten2, - base_color, metallic, roughness); - } + lit += shade_pbr_f0(n, v, l, pl.color.rgb, pl.color.w * atten2, + light_diffuse, light_f0, roughness); } } // END-POINT-LIGHT-LOOP diff --git a/native/shared/src/renderer/shaders/core_tests.rs b/native/shared/src/renderer/shaders/core_tests.rs index 93bd593f..d8a636f7 100644 --- a/native/shared/src/renderer/shaders/core_tests.rs +++ b/native/shared/src/renderer/shaders/core_tests.rs @@ -21,7 +21,18 @@ fn scene_vertex_shader_preserves_the_missing_tangent_sentinel() { fn textured_specular_glossiness_keeps_authored_diffuse_and_f0_independent() { wgpu::naga::front::wgsl::parse_str(SCENE_SHADER) .unwrap_or_else(|error| panic!("ordinary scene WGSL failed: {error:?}")); - assert!(SCENE_SHADER.contains("fn shade_specular_glossiness_pbr(")); + // Both workflows light through one BRDF taking a diffuse colour and an F0. + assert!(SCENE_SHADER.contains("fn shade_pbr_f0(")); + assert!(!SCENE_SHADER.contains("fn shade_specular_glossiness_pbr(")); + assert!(SCENE_SHADER.contains( + "let light_diffuse = select(base_color * (1.0 - metallic), base_color, has_spec_gloss);" + )); + assert_eq!( + SCENE_SHADER + .matches("light_diffuse, light_f0, roughness") + .count(), + 3 + ); assert!(SCENE_SHADER.contains("let f0 = select(mr_f0, authored_specular, has_spec_gloss);")); assert!( SCENE_SHADER.contains("let diffuse_weight = select(1.0 - metallic, 1.0, has_spec_gloss);")