Repository navigation
renderer: a mobile scene shading, the default on Android #193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| //! The mobile scene shading: on mobile GPUs the scene shader's | ||
| //! `shade_main_scene` is replaced by `shaders/scene_mobile.wgsl`, a lighter | ||
| //! forward base pass (as Unreal's mobile renderer and Unity URP have). Same | ||
| //! inputs, outputs, lights and shadows (the sun's with the 4-tap tent | ||
| //! filter only, chosen when the shader is built); the full shader's normal-variance | ||
| //! roughness, BRDF LUT (an analytic fit instead), coverage-dithered masks, cloud | ||
| //! shadows and firefly caps are left out. Those cost a mobile GPU most of the | ||
| //! main pass through register pressure, not arithmetic (Pixel 4a, Adreno | ||
| //! 618). | ||
| //! | ||
| //! Applied to the main scene shader before its froxel and virtual-shadow | ||
| //! specializations (the mobile function keeps their markers), so the scene | ||
| //! pipelines, their prepassed variant and the GPU-driven variant all shade | ||
| //! with it. The layered-material and layered-refraction shaders keep the full | ||
| //! shading. | ||
|
|
||
| use std::borrow::Cow; | ||
|
|
||
| const MOBILE_SHADE_MAIN_SCENE: &str = include_str!("shaders/scene_mobile.wgsl"); | ||
| const SHADE_MAIN_SCENE: &str = | ||
| "fn shade_main_scene(in: VertexOutputScene, front_facing: bool) -> SceneOut {"; | ||
| const AFTER_SHADE_MAIN_SCENE: &str = "@fragment\nfn fs_main_scene("; | ||
| const SAMPLE_CASCADE: &str = | ||
| "fn sample_cascade(cascade: i32, shadow_uv: vec2<f32>, depth_ref: f32) -> f32 {"; | ||
| const MOBILE_SAMPLE_CASCADE: &str = | ||
| "fn sample_cascade(cascade: i32, shadow_uv: vec2<f32>, depth_ref: f32) -> f32 { | ||
| let texel = 1.0 / vec2<f32>(textureDimensions(shadow_tex)); | ||
| return pcf_tent4(cascade, shadow_uv, texel, depth_ref) * 0.25; | ||
| } | ||
| "; | ||
|
|
||
| /// Mobile shading on Android by default; `BLOOM_MOBILE_SHADING=0|1` overrides | ||
| /// it (to compare the two on a device, or to try the mobile shading on a | ||
| /// desktop GPU). | ||
| pub(super) fn mobile_shading_enabled() -> bool { | ||
| match std::env::var("BLOOM_MOBILE_SHADING").as_deref() { | ||
| Ok("1") => true, | ||
| Ok("0") => false, | ||
| _ => cfg!(target_os = "android"), | ||
| } | ||
| } | ||
|
|
||
| /// `source` with the mobile `shade_main_scene` in place of the full one, when | ||
| /// mobile shading is on. | ||
| pub(super) fn scene_shader(source: &'static str) -> Cow<'static, str> { | ||
| if !mobile_shading_enabled() { | ||
| return source.into(); | ||
| } | ||
| with_mobile_shading(source).into() | ||
| } | ||
|
|
||
| fn with_mobile_shading(source: &str) -> String { | ||
| let start = source | ||
| .find(SHADE_MAIN_SCENE) | ||
| .expect("scene shader missing shade_main_scene"); | ||
| let end = start | ||
| + source[start..] | ||
| .find(AFTER_SHADE_MAIN_SCENE) | ||
| .expect("scene shader missing fs_main_scene after shade_main_scene"); | ||
| let source = format!( | ||
| "{}{}{}", | ||
| &source[..start], | ||
| MOBILE_SHADE_MAIN_SCENE, | ||
| &source[end..] | ||
| ); | ||
| // The sun shadow takes the 4-tap tent only. The 16-tap Poisson disc | ||
| // behind the runtime filter switch costs a mobile GPU even when it never | ||
| // runs (its code is the pass's register budget): 10 ms of a 36 ms main | ||
| // pass on a Pixel 4a. | ||
| let start = source | ||
| .find(SAMPLE_CASCADE) | ||
| .expect("scene shader missing sample_cascade"); | ||
| let end = start | ||
| + source[start..] | ||
| .find("\n}\n") | ||
| .expect("sample_cascade without an end") | ||
| + "\n}\n".len(); | ||
| format!( | ||
| "{}{}{}", | ||
| &source[..start], | ||
| MOBILE_SAMPLE_CASCADE, | ||
| &source[end..] | ||
| ) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::super::shaders::SCENE_SHADER; | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn mobile_scene_shader_validates_and_replaces_the_full_shading() { | ||
| let mobile = with_mobile_shading(SCENE_SHADER); | ||
| let module = wgpu::naga::front::wgsl::parse_str(&mobile) | ||
| .unwrap_or_else(|error| panic!("mobile scene WGSL failed: {error:?}")); | ||
| wgpu::naga::valid::Validator::new( | ||
| wgpu::naga::valid::ValidationFlags::all(), | ||
| wgpu::naga::valid::Capabilities::all(), | ||
| ) | ||
| .validate(&module) | ||
| .unwrap_or_else(|error| panic!("mobile scene WGSL invalid: {error:?}")); | ||
| assert_eq!(mobile.matches(SHADE_MAIN_SCENE).count(), 1); | ||
| // the full shading's LUT and variance terms are gone | ||
| assert!(!mobile.contains("brdf_lut_tex, brdf_lut_samp, vec2<f32>(n_dot_v_ibl")); | ||
| assert!(!mobile.contains("sigma2_toksvig")); | ||
| // the shadow filter is the tent alone | ||
| assert!(!mobile.contains("pcf_poisson16(cascade")); | ||
| assert_eq!(mobile.matches(SAMPLE_CASCADE).count(), 1); | ||
| // the markers the prepass, froxel and VSM variants rely on | ||
| for marker in [ | ||
| "//PREPASS_STRIP_BEGIN", | ||
| "//PREPASS_STRIP_END", | ||
| "//IBL_STRIP_BEGIN", | ||
| "//IBL_STRIP_END", | ||
| "// BEGIN-POINT-LIGHT-LOOP", | ||
| "// END-POINT-LIGHT-LOOP", | ||
| "pl.color.w * atten2", | ||
| ] { | ||
| assert_eq!(mobile.matches(marker).count(), 1, "{marker}"); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn mobile_scene_shader_takes_the_ibl_strip_diagnostic() { | ||
| // As BLOOM_SKIP=ibl does in specialized_scene_shader_source_from. | ||
| let mobile = with_mobile_shading(SCENE_SHADER); | ||
| let begin = mobile.find("//IBL_STRIP_BEGIN").unwrap(); | ||
| let end = mobile.find("//IBL_STRIP_END").unwrap() + "//IBL_STRIP_END".len(); | ||
| let stripped = format!( | ||
| "{}\n let ibl_diffuse = base_color * (1.0 - metallic) * occlusion\n * lighting.camera_pos.w * 0.35;\n let ibl_spec = vec3<f32>(0.0);\n let ibl_clearcoat = vec3<f32>(0.0);\n{}", | ||
| &mobile[..begin], | ||
| &mobile[end..] | ||
| ); | ||
| assert!(!stripped.contains("env_sample_lod(reflect(-v, n)")); | ||
| wgpu::naga::front::wgsl::parse_str(&stripped) | ||
| .unwrap_or_else(|error| panic!("IBL-stripped mobile WGSL failed: {error:?}")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn mobile_scene_shader_takes_the_froxel_and_gpu_driven_variants() { | ||
| let mobile = with_mobile_shading(SCENE_SHADER); | ||
| let clustered = super::super::froxel::clustered_scene_shader(&mobile); | ||
| wgpu::naga::front::wgsl::parse_str(&clustered) | ||
| .unwrap_or_else(|error| panic!("clustered mobile WGSL failed: {error:?}")); | ||
| let gpu = super::super::gpu_driven::make_gpu_scene_shader(&mobile); | ||
| wgpu::naga::front::wgsl::parse_str(&gpu) | ||
| .unwrap_or_else(|error| panic!("GPU-driven mobile WGSL failed: {error:?}")); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| // The mobile scene shading (renderer/mobile_shading.rs): replaces | ||
| // shade_main_scene in the scene shader on mobile GPUs, the way Unreal's mobile | ||
| // renderer and Unity URP have their own forward base pass. Same inputs, | ||
| // outputs and lights; dropped: normal-map variance and curvature roughness | ||
| // (Toksvig / LEADR / screen-space kernel), the BRDF LUT (Karis's analytic | ||
| // environment BRDF instead, as Unreal mobile), | ||
| // coverage-dithered alpha masks (a plain alpha test), cloud shadows and the | ||
| // specular firefly caps. The texture reads are the canonical ones, so the | ||
| // GPU-driven variant rewrites them like the full shader's. | ||
| fn shade_main_scene(in: VertexOutputScene, front_facing: bool) -> SceneOut { | ||
| var n = normalize(in.normal); | ||
| let base_uv = material_uv( | ||
| in.uv, material.uv_transforms[0], material.uv_transforms[1], | ||
| ); | ||
| let normal_uv = material_uv( | ||
| in.uv, material.uv_transforms[2], material.uv_transforms[3], | ||
| ); | ||
| let mr_uv = material_uv( | ||
| in.uv, material.uv_transforms[4], material.uv_transforms[5], | ||
| ); | ||
| let emissive_uv = material_uv( | ||
| in.uv, material.uv_transforms[6], material.uv_transforms[7], | ||
| ); | ||
| let occlusion_uv = material_uv( | ||
| in.uv, material.uv_transforms[8], material.uv_transforms[9], | ||
| ); | ||
|
|
||
| let lod_bias = lighting.shadow_cascade_splits.w; | ||
| let nm_sample4 = textureSampleBias(normal_tex, normal_samp, normal_uv, 0.25 + lod_bias); | ||
| let nm_sample = normalize((nm_sample4.xyz * 2.0 - 1.0) * vec3<f32>( | ||
| material.uv_transforms[2].w, | ||
| material.uv_transforms[3].w, | ||
| 1.0, | ||
| )); | ||
| if (dot(in.tangent.xyz, in.tangent.xyz) > 0.0001) { | ||
| let t = normalize(in.tangent.xyz); | ||
| let t_ortho = normalize(t - n * dot(n, t)); | ||
| let b = cross(n, t_ortho) * in.tangent.w; | ||
| n = normalize(t_ortho * nm_sample.x + b * nm_sample.y + n * nm_sample.z); | ||
| } else { | ||
| let tbn = compute_tbn( | ||
| dpdx(in.world_pos), dpdy(in.world_pos), dpdx(normal_uv), dpdy(normal_uv), n, | ||
| ); | ||
| n = normalize(tbn * nm_sample); | ||
| } | ||
|
|
||
| let base_tex = textureSampleBias(base_color_tex, base_color_samp, base_uv, lod_bias); | ||
| let base_color = srgb_to_linear_v(base_tex.rgb) * in.color.rgb; | ||
| let base_alpha = base_tex.a * in.color.a; | ||
| let alpha_cutoff = material.metal_rough.w; | ||
| //PREPASS_STRIP_BEGIN — removed in the prepassed-pipeline variant. | ||
| if (alpha_cutoff > 0.0 && base_alpha < alpha_cutoff) { | ||
| discard; | ||
| } | ||
| //PREPASS_STRIP_END | ||
| if (alpha_cutoff > 0.0 && dot(n, lighting.camera_pos.xyz - in.world_pos) < 0.0) { | ||
| n = -n; | ||
| } | ||
| if (alpha_cutoff < 0.0 && !front_facing) { | ||
| n = -n; | ||
| } | ||
|
|
||
| let mr_tex_sample = textureSample(mr_tex, mr_samp, mr_uv); | ||
| let has_mr = material.metal_rough.z > 0.5 && material.metal_rough.z < 1.5; | ||
| let has_spec_gloss = material.metal_rough.z > 1.5; | ||
| let metallic = select( | ||
| select( | ||
| clamp(material.metal_rough.x, 0.0, 1.0), | ||
| clamp(mr_tex_sample.b * material.metal_rough.x, 0.0, 1.0), | ||
| has_mr, | ||
| ), | ||
| 0.0, | ||
| has_spec_gloss, | ||
| ); | ||
| let roughness_raw = select( | ||
| select( | ||
| clamp(material.metal_rough.y, 0.045, 1.0), | ||
| clamp(mr_tex_sample.g * material.metal_rough.y, 0.045, 1.0), | ||
| has_mr, | ||
| ), | ||
| clamp(1.0 - mr_tex_sample.a * material.spec_gloss.a, 0.045, 1.0), | ||
| has_spec_gloss, | ||
| ); | ||
| let roughness = max(roughness_raw, select(0.15, 0.045, has_spec_gloss) * (1.0 - metallic)); | ||
| // The lights' view of either workflow (see shade_pbr_f0). | ||
| let authored_specular = srgb_to_linear_v(mr_tex_sample.rgb) * material.spec_gloss.rgb; | ||
| let light_diffuse = select(base_color * (1.0 - metallic), base_color, has_spec_gloss); | ||
| let light_f0 = select( | ||
| mix(vec3<f32>(0.04), base_color, metallic), | ||
| authored_specular, | ||
| has_spec_gloss, | ||
| ); | ||
|
|
||
| let em_tex_sample = textureSample(em_tex, em_samp, emissive_uv); | ||
| let emissive = srgb_to_linear_v(em_tex_sample.rgb) * material.emissive.rgb; | ||
| let sampled_occlusion = textureSample(occ_tex, occ_samp, occlusion_uv).r; | ||
| let occlusion = mix(1.0, sampled_occlusion, material.uv_transforms[8].w); | ||
|
|
||
| // --- Direct lighting --- | ||
| let v = normalize(lighting.camera_pos.xyz - in.world_pos); | ||
| var lit = lighting.ambient.rgb * lighting.ambient.a * base_color; | ||
| let shadow_factor = sample_shadow(in.world_pos, normalize(in.normal), in.clip_position.xy); | ||
| let direct_shadow = mix(0.03, 1.0, shadow_factor); | ||
| let legacy_dir = normalize(lighting.light_dir.xyz); | ||
| if (alpha_cutoff > 0.0) { | ||
| // Foliage: wrap diffuse plus transmission, no specular. | ||
| let wrap = 0.45; | ||
| let ndl_wrap = clamp((dot(n, legacy_dir) + wrap) / ((1.0 + wrap) * (1.0 + wrap)), 0.0, 1.0); | ||
| let trans = pow(max(dot(v, -legacy_dir), 0.0), 3.0) * 0.85; | ||
| lit += base_color * lighting.light_color.rgb * lighting.light_dir.w | ||
| * (ndl_wrap / PI + trans) * direct_shadow; | ||
| } else { | ||
| lit += shade_pbr_f0(n, v, legacy_dir, lighting.light_color.rgb, | ||
| lighting.light_dir.w, light_diffuse, light_f0, roughness) | ||
| * direct_shadow; | ||
| } | ||
| let dir_count = u32(lighting.dir_light_count.x); | ||
| for (var i = 0u; i < dir_count; i++) { | ||
| let dl = lighting.dir_lights[i]; | ||
| let l = normalize(dl.direction.xyz); | ||
| lit += shade_pbr_f0(n, v, l, dl.color.rgb, dl.direction.w, | ||
| light_diffuse, light_f0, roughness); | ||
| } | ||
| // BEGIN-POINT-LIGHT-LOOP (the froxel-clustered variant replaces this | ||
| // loop, as in the full shader) | ||
| let pt_count = u32(lighting.point_light_count.x); | ||
| for (var i = 0u; i < pt_count; i++) { | ||
| let pl = lighting.point_lights[i]; | ||
| let to_light = pl.position.xyz - in.world_pos; | ||
| let dist = length(to_light); | ||
| let range = pl.position.w; | ||
| if (dist < range && dist > 0.0) { | ||
| let l = to_light / dist; | ||
| let atten = 1.0 - (dist / range); | ||
| let atten2 = atten * atten; | ||
| lit += shade_pbr_f0(n, v, l, pl.color.rgb, pl.color.w * atten2, | ||
| light_diffuse, light_f0, roughness); | ||
| } | ||
| } | ||
| // END-POINT-LIGHT-LOOP | ||
|
|
||
| // --- Image-based lighting --- | ||
| //IBL_STRIP_BEGIN — BLOOM_SKIP=ibl replaces this block with a flat ambient | ||
| // (see mod.rs); only ibl_diffuse/ibl_spec escape it. | ||
| // Karis's analytic fit of the split-sum environment BRDF ("Physically | ||
| // Based Shading on Mobile", the one Unreal's mobile renderer uses) in | ||
| // place of the BRDF LUT. | ||
| let n_dot_v = max(dot(n, v), 0.0); | ||
| let c0 = vec4<f32>(-1.0, -0.0275, -0.572, 0.022); | ||
| let c1 = vec4<f32>(1.0, 0.0425, 1.04, -0.04); | ||
| let rc = roughness * c0 + c1; | ||
| let a004 = min(rc.x * rc.x, exp2(-9.28 * n_dot_v)) * rc.x + rc.y; | ||
| let env_ab = vec2<f32>(-1.04, 1.04) * a004 + rc.zw; | ||
| let env_brdf = light_f0 * env_ab.x + vec3<f32>(env_ab.y); | ||
| // Multi-scatter compensation from the fit's energy, as the full shader | ||
| // does from the LUT's (Fdez-Aguera 2019). | ||
| let ess = env_ab.x + env_ab.y; | ||
| let f_avg = light_f0 + (vec3<f32>(1.0) - light_f0) * (1.0 / 21.0); | ||
| let ms = f_avg * ess / (vec3<f32>(1.0) - f_avg * (1.0 - ess)) * (1.0 - ess); | ||
|
|
||
| let irradiance = textureSampleLevel( | ||
| env_diffuse_tex, env_samp, seamless_equirect_uv(dir_to_equirect_uv(n)), 0.0, | ||
| ).rgb * lighting.camera_pos.w; | ||
| let ibl_diffuse = irradiance * light_diffuse * (vec3<f32>(1.0) - env_brdf) * occlusion; | ||
| let max_spec_mip = max(f32(textureNumLevels(env_tex)) - 1.0, 0.0); | ||
| let prefiltered_env = env_sample_lod(reflect(-v, n), roughness * max_spec_mip); | ||
| // SSR owns the smooth lobes when it runs (dir_light_count.z), as in the | ||
| // full shader. | ||
| let ssr_own = clamp( | ||
| lighting.dir_light_count.z * (1.0 - smoothstep(0.45, 0.70, roughness)), | ||
| 0.0, 1.0); | ||
| let spec_occ = clamp( | ||
| pow(n_dot_v + occlusion, exp2(-16.0 * roughness - 1.0)) - 1.0 + occlusion, | ||
| 0.0, 1.0, | ||
| ); | ||
| let ibl_spec = prefiltered_env * (env_brdf + ms) * spec_occ * (1.0 - ssr_own); | ||
| //IBL_STRIP_END | ||
| let indirect_shadow = mix(0.35, 1.0, shadow_factor); | ||
|
|
||
| let hdr_raw = lit + (ibl_diffuse + ibl_spec) * indirect_shadow + emissive; | ||
| let hdr = select(vec3<f32>(0.0), hdr_raw, hdr_raw == hdr_raw); | ||
|
|
||
| let curr_ndc = in.curr_clip.xy / in.curr_clip.w; | ||
| let prev_ndc = in.prev_clip.xy / in.prev_clip.w; | ||
| let vel = (curr_ndc - prev_ndc) * 0.5; | ||
| let non_opaque_alpha = select(base_alpha, 1.0, alpha_cutoff > 0.0); | ||
| let out_alpha = select(in.color.a, non_opaque_alpha, alpha_cutoff != 0.0); | ||
| return SceneOut( | ||
| vec4<f32>(hdr, out_alpha), | ||
|
Comment on lines
+187
to
+189
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| vec2<f32>(metallic, roughness), | ||
| vel, | ||
| vec4<f32>(base_color, 1.0 - shadow_factor), | ||
| ); | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.