@bornengine/engine 0.4.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/native/android/Cargo.lock +1848 -0
- package/native/android/Cargo.toml +24 -0
- package/native/android/src/lib.rs +702 -0
- package/native/ios/Cargo.lock +1690 -0
- package/native/ios/Cargo.toml +32 -0
- package/native/ios/src/lib.rs +1267 -0
- package/native/linux/Cargo.lock +3279 -0
- package/native/linux/Cargo.toml +29 -0
- package/native/linux/src/lib.rs +1331 -0
- package/native/macos/Cargo.lock +3310 -0
- package/native/macos/Cargo.toml +46 -0
- package/native/macos/src/lib.rs +1302 -0
- package/native/shared/Cargo.lock +1899 -0
- package/native/shared/Cargo.toml +62 -0
- package/native/shared/assets/default_font.ttf +0 -0
- package/native/shared/build.rs +270 -0
- package/native/shared/shaders/common/clouds.wgsl +122 -0
- package/native/shared/shaders/common/fog.wgsl +16 -0
- package/native/shared/shaders/common/foliage_wind.wgsl +98 -0
- package/native/shared/shaders/common/imposter.wgsl +112 -0
- package/native/shared/shaders/common/pbr.wgsl +186 -0
- package/native/shared/shaders/common/shadows.wgsl +186 -0
- package/native/shared/shaders/common/sky.wgsl +8 -0
- package/native/shared/shaders/common/tonemap.wgsl +25 -0
- package/native/shared/shaders/impulse_field.wgsl +57 -0
- package/native/shared/shaders/material_abi.wgsl +383 -0
- package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
- package/native/shared/src/anim_mixer.rs +61 -0
- package/native/shared/src/attach.rs +263 -0
- package/native/shared/src/audio/decode.rs +123 -0
- package/native/shared/src/audio/mod.rs +863 -0
- package/native/shared/src/audio/render.rs +892 -0
- package/native/shared/src/audio/spsc.rs +156 -0
- package/native/shared/src/audio/stream.rs +226 -0
- package/native/shared/src/custom_shaders.rs +104 -0
- package/native/shared/src/decals.rs +245 -0
- package/native/shared/src/drs.rs +211 -0
- package/native/shared/src/engine.rs +261 -0
- package/native/shared/src/ffi.rs +116 -0
- package/native/shared/src/ffi_core/assets.rs +388 -0
- package/native/shared/src/ffi_core/audio_ffi.rs +184 -0
- package/native/shared/src/ffi_core/draw.rs +334 -0
- package/native/shared/src/ffi_core/game_loop.rs +577 -0
- package/native/shared/src/ffi_core/input.rs +234 -0
- package/native/shared/src/ffi_core/mod.rs +127 -0
- package/native/shared/src/ffi_core/models.rs +1154 -0
- package/native/shared/src/ffi_core/ragdoll_ffi.rs +261 -0
- package/native/shared/src/ffi_core/scene.rs +626 -0
- package/native/shared/src/ffi_core/vfx.rs +212 -0
- package/native/shared/src/ffi_core/visual.rs +691 -0
- package/native/shared/src/frame_callbacks.rs +122 -0
- package/native/shared/src/geometry.rs +236 -0
- package/native/shared/src/handles.rs +182 -0
- package/native/shared/src/input.rs +448 -0
- package/native/shared/src/jolt_sys.rs +822 -0
- package/native/shared/src/lib.rs +55 -0
- package/native/shared/src/models.rs +1093 -0
- package/native/shared/src/models_gltf.rs +1280 -0
- package/native/shared/src/particles.rs +391 -0
- package/native/shared/src/physics_jolt.rs +1908 -0
- package/native/shared/src/picking.rs +298 -0
- package/native/shared/src/postfx.rs +345 -0
- package/native/shared/src/profiler.rs +492 -0
- package/native/shared/src/ragdoll.rs +474 -0
- package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
- package/native/shared/src/renderer/brdf_lut.rs +154 -0
- package/native/shared/src/renderer/draw2d.rs +143 -0
- package/native/shared/src/renderer/formats.rs +822 -0
- package/native/shared/src/renderer/froxel.rs +421 -0
- package/native/shared/src/renderer/gi_bake.rs +653 -0
- package/native/shared/src/renderer/graph.rs +462 -0
- package/native/shared/src/renderer/hiz.rs +269 -0
- package/native/shared/src/renderer/hot_reload.rs +390 -0
- package/native/shared/src/renderer/impulse_field.rs +456 -0
- package/native/shared/src/renderer/lighting.rs +154 -0
- package/native/shared/src/renderer/material_instancing.rs +171 -0
- package/native/shared/src/renderer/material_pipeline.rs +700 -0
- package/native/shared/src/renderer/material_system.rs +1996 -0
- package/native/shared/src/renderer/material_system_tests.rs +601 -0
- package/native/shared/src/renderer/material_system_wasm.rs +41 -0
- package/native/shared/src/renderer/mod.rs +12556 -0
- package/native/shared/src/renderer/model_draw.rs +641 -0
- package/native/shared/src/renderer/occlusion.rs +429 -0
- package/native/shared/src/renderer/planar_pass.rs +593 -0
- package/native/shared/src/renderer/planar_reflection.rs +499 -0
- package/native/shared/src/renderer/post_pass.rs +249 -0
- package/native/shared/src/renderer/postfx_chain.rs +728 -0
- package/native/shared/src/renderer/pt_pass.rs +577 -0
- package/native/shared/src/renderer/scene_pass.rs +607 -0
- package/native/shared/src/renderer/shader_include.rs +205 -0
- package/native/shared/src/renderer/shader_library.rs +135 -0
- package/native/shared/src/renderer/shaders/ao.rs +570 -0
- package/native/shared/src/renderer/shaders/core.rs +1243 -0
- package/native/shared/src/renderer/shaders/env.rs +907 -0
- package/native/shared/src/renderer/shaders/gi.rs +810 -0
- package/native/shared/src/renderer/shaders/mod.rs +19 -0
- package/native/shared/src/renderer/shaders/post.rs +1558 -0
- package/native/shared/src/renderer/shaders/pt.rs +1859 -0
- package/native/shared/src/renderer/shaders/ssgi.rs +1586 -0
- package/native/shared/src/renderer/shadow_pass.rs +731 -0
- package/native/shared/src/renderer/ssgi_pass.rs +392 -0
- package/native/shared/src/renderer/ssr_pass.rs +188 -0
- package/native/shared/src/renderer/texture_store.rs +473 -0
- package/native/shared/src/renderer/transient.rs +591 -0
- package/native/shared/src/renderer/types.rs +941 -0
- package/native/shared/src/renderer/util.rs +152 -0
- package/native/shared/src/scene.rs +1362 -0
- package/native/shared/src/sdf_cache.rs +274 -0
- package/native/shared/src/shadows.rs +1036 -0
- package/native/shared/src/staging.rs +102 -0
- package/native/shared/src/string_header.rs +266 -0
- package/native/shared/src/text_renderer.rs +502 -0
- package/native/shared/src/textures.rs +197 -0
- package/native/tvos/Cargo.lock +1693 -0
- package/native/tvos/Cargo.toml +36 -0
- package/native/tvos/metal-patched/Cargo.toml +178 -0
- package/native/tvos/metal-patched/LICENSE-APACHE +201 -0
- package/native/tvos/metal-patched/LICENSE-MIT +25 -0
- package/native/tvos/metal-patched/src/acceleration_structure.rs +667 -0
- package/native/tvos/metal-patched/src/acceleration_structure_pass.rs +108 -0
- package/native/tvos/metal-patched/src/argument.rs +366 -0
- package/native/tvos/metal-patched/src/blitpass.rs +102 -0
- package/native/tvos/metal-patched/src/buffer.rs +71 -0
- package/native/tvos/metal-patched/src/capturedescriptor.rs +76 -0
- package/native/tvos/metal-patched/src/capturemanager.rs +113 -0
- package/native/tvos/metal-patched/src/commandbuffer.rs +192 -0
- package/native/tvos/metal-patched/src/commandqueue.rs +44 -0
- package/native/tvos/metal-patched/src/computepass.rs +107 -0
- package/native/tvos/metal-patched/src/constants.rs +152 -0
- package/native/tvos/metal-patched/src/counters.rs +119 -0
- package/native/tvos/metal-patched/src/depthstencil.rs +190 -0
- package/native/tvos/metal-patched/src/device.rs +2134 -0
- package/native/tvos/metal-patched/src/drawable.rs +39 -0
- package/native/tvos/metal-patched/src/encoder.rs +2041 -0
- package/native/tvos/metal-patched/src/heap.rs +281 -0
- package/native/tvos/metal-patched/src/indirect_encoder.rs +344 -0
- package/native/tvos/metal-patched/src/lib.rs +657 -0
- package/native/tvos/metal-patched/src/library.rs +902 -0
- package/native/tvos/metal-patched/src/mps.rs +575 -0
- package/native/tvos/metal-patched/src/pipeline/compute.rs +475 -0
- package/native/tvos/metal-patched/src/pipeline/mod.rs +71 -0
- package/native/tvos/metal-patched/src/pipeline/render.rs +762 -0
- package/native/tvos/metal-patched/src/renderpass.rs +443 -0
- package/native/tvos/metal-patched/src/resource.rs +182 -0
- package/native/tvos/metal-patched/src/sampler.rs +165 -0
- package/native/tvos/metal-patched/src/sync.rs +178 -0
- package/native/tvos/metal-patched/src/texture.rs +352 -0
- package/native/tvos/metal-patched/src/types.rs +90 -0
- package/native/tvos/metal-patched/src/vertexdescriptor.rs +250 -0
- package/native/tvos/src/audio_backend.rs +197 -0
- package/native/tvos/src/lib.rs +1891 -0
- package/native/visionos/Cargo.lock +1693 -0
- package/native/visionos/Cargo.toml +40 -0
- package/native/visionos/src/audio_backend.rs +197 -0
- package/native/visionos/src/lib.rs +1887 -0
- package/native/watchos/Cargo.lock +16 -0
- package/native/watchos/Cargo.toml +19 -0
- package/native/watchos/shaders/bloom_postfx.metal +99 -0
- package/native/watchos/src/BloomWatchApp.swift +1267 -0
- package/native/watchos/src/BloomWatchAudio.swift +179 -0
- package/native/watchos/src/audio.rs +55 -0
- package/native/watchos/src/draw_list.rs +229 -0
- package/native/watchos/src/ffi_stubs.rs +915 -0
- package/native/watchos/src/ffi_stubs_manual.rs +35 -0
- package/native/watchos/src/lib.rs +1124 -0
- package/native/watchos/src/models.rs +746 -0
- package/native/watchos/src/postfx.rs +95 -0
- package/native/watchos/src/scene.rs +534 -0
- package/native/watchos/src/textures.rs +184 -0
- package/native/web/Cargo.lock +1657 -0
- package/native/web/Cargo.toml +43 -0
- package/native/web/bloom_glue.js +695 -0
- package/native/web/build.sh +131 -0
- package/native/web/index.html +35 -0
- package/native/web/jolt_bridge.js +1519 -0
- package/native/web/src/input_ffi.rs +286 -0
- package/native/web/src/lib.rs +1796 -0
- package/native/web/src/material_ffi.rs +710 -0
- package/native/web/src/parity_ffi.rs +343 -0
- package/native/web/src/physics_ffi.rs +643 -0
- package/native/web/src/ragdoll_ffi.rs +250 -0
- package/native/web/src/render_settings.rs +98 -0
- package/native/windows/Cargo.lock +1815 -0
- package/native/windows/Cargo.toml +68 -0
- package/native/windows/src/lib.rs +1486 -0
- package/package.json +4279 -0
- package/src/audio/index.ts +315 -0
- package/src/core/colors.ts +63 -0
- package/src/core/index.ts +1206 -0
- package/src/core/keys.ts +63 -0
- package/src/core/types.ts +104 -0
- package/src/index.ts +171 -0
- package/src/math/index.ts +516 -0
- package/src/mobile/index.ts +294 -0
- package/src/models/index.ts +1258 -0
- package/src/physics/index.ts +1134 -0
- package/src/scene/index.ts +698 -0
- package/src/shapes/index.ts +120 -0
- package/src/text/index.ts +48 -0
- package/src/textures/index.ts +187 -0
- package/src/vfx/index.ts +191 -0
- package/src/world/index.ts +24 -0
- package/src/world/loader.ts +423 -0
- package/src/world/prefab.ts +217 -0
- package/src/world/render.ts +172 -0
- package/src/world/saver.ts +108 -0
- package/src/world/serialize.ts +301 -0
- package/src/world/terrain.ts +355 -0
- package/src/world/types.ts +160 -0
- package/src/world/validate.ts +319 -0
- package/src/world/version.ts +114 -0
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
//! Hi-Z linear-depth pyramid + GTAO and its bilateral blur.
|
|
2
|
+
//! Split from renderer/shaders.rs.
|
|
3
|
+
|
|
4
|
+
/// GTAO (Ground Truth Ambient Occlusion) shader.
|
|
5
|
+
///
|
|
6
|
+
/// Horizon-based AO: for each pixel, reconstruct view-space position
|
|
7
|
+
/// from depth + inverse projection, then march 8 directions around
|
|
8
|
+
/// the pixel (4 steps each). At each step, compute the horizon angle
|
|
9
|
+
/// (elevation of the sample above the surface tangent plane). The
|
|
10
|
+
/// final AO is the average fraction of the hemisphere that is
|
|
11
|
+
/// unoccluded across all directions.
|
|
12
|
+
///
|
|
13
|
+
/// Uses interleaved gradient noise (IGN) for per-pixel direction
|
|
14
|
+
/// jitter to break banding. Output is Rg8Unorm at half resolution
|
|
15
|
+
/// — R = GTAO occlusion, G = contact-shadow factor (both 1 =
|
|
16
|
+
/// unoccluded, 0 = fully occluded).
|
|
17
|
+
/// Linearize the hardware depth buffer into mip 0 of the Hi-Z
|
|
18
|
+
/// pyramid. Stores *positive* view-space distance (|view_z|); sky
|
|
19
|
+
/// pixels (depth ≥ 0.9999) receive the sentinel `HIZ_SKY_Z` so a
|
|
20
|
+
/// subsequent `min` downsample never picks sky over real geometry.
|
|
21
|
+
pub(in crate::renderer) const HIZ_LINEARIZE_SHADER_WGSL: &str = "
|
|
22
|
+
struct Params {
|
|
23
|
+
/// xy = inv_size (1/mip0_w, 1/mip0_h)
|
|
24
|
+
/// z = proj[2][2]
|
|
25
|
+
/// w = proj[3][2]
|
|
26
|
+
params: vec4<f32>,
|
|
27
|
+
/// xy = mip-0 size (u32). zw unused.
|
|
28
|
+
size: vec4<u32>,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const HIZ_SKY_Z: f32 = 10000.0;
|
|
32
|
+
|
|
33
|
+
@group(0) @binding(0) var<uniform> u: Params;
|
|
34
|
+
@group(0) @binding(1) var depth_tex: texture_depth_2d;
|
|
35
|
+
@group(0) @binding(2) var depth_samp: sampler;
|
|
36
|
+
@group(0) @binding(3) var hiz_out: texture_storage_2d<r32float, write>;
|
|
37
|
+
|
|
38
|
+
@compute @workgroup_size(8, 8, 1)
|
|
39
|
+
fn cs_main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
40
|
+
let px = gid.xy;
|
|
41
|
+
if (px.x >= u.size.x || px.y >= u.size.y) { return; }
|
|
42
|
+
let uv = (vec2<f32>(px) + vec2<f32>(0.5)) * u.params.xy;
|
|
43
|
+
let d = textureSampleLevel(depth_tex, depth_samp, uv, 0);
|
|
44
|
+
var linear_z: f32;
|
|
45
|
+
// Sky = the cleared depth (exactly 1.0; one-ulp slack for driver
|
|
46
|
+
// rounding). The previous 0.9999 threshold was calibrated for a
|
|
47
|
+
// reversed-Z mindset: with this forward-Z projection, depth crams
|
|
48
|
+
// toward 1.0 so fast that 0.9999 already fires at ~35 m — every
|
|
49
|
+
// mid/far surface was written as 'sky' (10000).
|
|
50
|
+
if (d >= 0.99999994) {
|
|
51
|
+
linear_z = HIZ_SKY_Z;
|
|
52
|
+
} else {
|
|
53
|
+
// view_z = p32 / (d + p22); store |view_z|. For this projection
|
|
54
|
+
// (p22 = far/(near-far), p32 = near*far/(near-far), both negative)
|
|
55
|
+
// the numerator and denominator are BOTH negative for geometry, so
|
|
56
|
+
// the quotient is the positive view depth. The previous leading
|
|
57
|
+
// minus flipped every geometry pixel negative, and the max() then
|
|
58
|
+
// clamped the whole depth pyramid to 0.0001 — GTAO's horizon scan
|
|
59
|
+
// and the occlusion grid have been reading a flat 0.0001/10000
|
|
60
|
+
// two-value field instead of real depth.
|
|
61
|
+
linear_z = u.params.w / (d + u.params.z);
|
|
62
|
+
linear_z = max(linear_z, 0.0001);
|
|
63
|
+
}
|
|
64
|
+
textureStore(hiz_out, vec2<i32>(px), vec4<f32>(linear_z, 0.0, 0.0, 0.0));
|
|
65
|
+
}
|
|
66
|
+
";
|
|
67
|
+
|
|
68
|
+
/// Downsample one Hi-Z mip into the next. Uses `min` so the coarser
|
|
69
|
+
/// mip reports the nearest occluder in its footprint — exactly what
|
|
70
|
+
/// the GTAO horizon scan wants when picking a coarser mip for a
|
|
71
|
+
/// far step.
|
|
72
|
+
pub(in crate::renderer) const HIZ_DOWNSAMPLE_SHADER_WGSL: &str = "
|
|
73
|
+
struct Params {
|
|
74
|
+
/// xy = dst-mip size (u32). zw unused.
|
|
75
|
+
size: vec4<u32>,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
@group(0) @binding(0) var<uniform> u: Params;
|
|
79
|
+
@group(0) @binding(1) var src_tex: texture_2d<f32>;
|
|
80
|
+
@group(0) @binding(2) var dst_tex: texture_storage_2d<r32float, write>;
|
|
81
|
+
|
|
82
|
+
@compute @workgroup_size(8, 8, 1)
|
|
83
|
+
fn cs_main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
84
|
+
let dst = gid.xy;
|
|
85
|
+
if (dst.x >= u.size.x || dst.y >= u.size.y) { return; }
|
|
86
|
+
let src = vec2<i32>(dst * 2u);
|
|
87
|
+
let a = textureLoad(src_tex, src + vec2<i32>(0, 0), 0).r;
|
|
88
|
+
let b = textureLoad(src_tex, src + vec2<i32>(1, 0), 0).r;
|
|
89
|
+
let c = textureLoad(src_tex, src + vec2<i32>(0, 1), 0).r;
|
|
90
|
+
let d = textureLoad(src_tex, src + vec2<i32>(1, 1), 0).r;
|
|
91
|
+
let m = min(min(a, b), min(c, d));
|
|
92
|
+
textureStore(dst_tex, vec2<i32>(dst), vec4<f32>(m, 0.0, 0.0, 0.0));
|
|
93
|
+
}
|
|
94
|
+
";
|
|
95
|
+
|
|
96
|
+
/// GTAO compute shader — same 8-dir × 8-step horizon scan +
|
|
97
|
+
/// 12-step contact-shadow ray march as ticket 002, but:
|
|
98
|
+
/// 1. Samples a hierarchical linear-depth pyramid instead of the
|
|
99
|
+
/// raw depth buffer. Each scan step `s` picks its mip from
|
|
100
|
+
/// `log2(step_pixels)`, so far steps hit progressively coarser
|
|
101
|
+
/// (cache-resident) data. This is the win Apple TBDR wants to
|
|
102
|
+
/// compensate for the free tile-memory depth cache we lost
|
|
103
|
+
/// going compute.
|
|
104
|
+
/// 2. Linear depth lives in the pyramid directly, so `view_pos`
|
|
105
|
+
/// skips the per-sample `-p32 / (depth + p22)` reconstruction.
|
|
106
|
+
/// 3. Contact-shadow march is gated on `dot(N, light_vs) > 0.1` —
|
|
107
|
+
/// back-facing pixels skip the whole 12-step march.
|
|
108
|
+
/// Output layout is unchanged: R = noisy AO, G = contact shadow.
|
|
109
|
+
pub(in crate::renderer) const SSAO_SHADER_WGSL: &str = "
|
|
110
|
+
struct SsaoParams {
|
|
111
|
+
/// xy = inv_size (1/half_w, 1/half_h), z = radius_ws, w = strength
|
|
112
|
+
params: vec4<f32>,
|
|
113
|
+
/// x = proj[0][0], y = proj[1][1], z = proj[2][0], w = proj[2][1]
|
|
114
|
+
proj_row01: vec4<f32>,
|
|
115
|
+
/// x = proj[2][2], y = proj[3][2] (unused here — Hi-Z already
|
|
116
|
+
/// linearized; kept so host-side SsaoParams stays symmetric),
|
|
117
|
+
/// z = 1/proj[0][0], w = 1/proj[1][1] (contact-shadow NDC->UV).
|
|
118
|
+
proj_z: vec4<f32>,
|
|
119
|
+
light_dir_vs: vec4<f32>,
|
|
120
|
+
/// x = half_w, y = half_h, z = frame_phase (0..3), w = first_frames
|
|
121
|
+
/// flag (non-zero → force alpha=1 so history seeds from the current
|
|
122
|
+
/// frame instead of a stale clear).
|
|
123
|
+
size: vec4<u32>,
|
|
124
|
+
/// x = temporal blend alpha (steady state, 4-frame EMA ≈ 0.25);
|
|
125
|
+
/// y = per-frame Halton-5 rotation offset for direction basis
|
|
126
|
+
/// (uncorrelated with TAA's Halton-2/3 pixel jitter so the two
|
|
127
|
+
/// patterns don't resonate); zw unused.
|
|
128
|
+
temporal: vec4<f32>,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const HIZ_SKY_Z: f32 = 10000.0;
|
|
132
|
+
|
|
133
|
+
@group(0) @binding(0) var<uniform> u: SsaoParams;
|
|
134
|
+
@group(0) @binding(1) var ao_out: texture_storage_2d<rgba8unorm, write>;
|
|
135
|
+
@group(0) @binding(2) var hiz_samp: sampler;
|
|
136
|
+
@group(0) @binding(3) var hiz0: texture_2d<f32>;
|
|
137
|
+
@group(0) @binding(4) var hiz1: texture_2d<f32>;
|
|
138
|
+
@group(0) @binding(5) var hiz2: texture_2d<f32>;
|
|
139
|
+
@group(0) @binding(6) var hiz3: texture_2d<f32>;
|
|
140
|
+
@group(0) @binding(7) var hiz4: texture_2d<f32>;
|
|
141
|
+
@group(0) @binding(8) var velocity_tex: texture_2d<f32>;
|
|
142
|
+
@group(0) @binding(9) var history_in: texture_2d<f32>;
|
|
143
|
+
@group(0) @binding(10) var filt_samp: sampler;
|
|
144
|
+
@group(0) @binding(11) var history_out: texture_storage_2d<rgba8unorm, write>;
|
|
145
|
+
|
|
146
|
+
// Temporal accumulation fan-out: every frame scans 2 of 8 directions;
|
|
147
|
+
// 4 frames rotate through all 8. The 4-frame EMA folded in history
|
|
148
|
+
// reconstructs the full 8-dir × 8-step signal at steady state for a
|
|
149
|
+
// nominal 4× perf win on the GTAO pass itself.
|
|
150
|
+
const N_DIRS_TOTAL: u32 = 8u;
|
|
151
|
+
const N_DIRS_PER_FRAME: u32 = 2u;
|
|
152
|
+
const N_PHASES: u32 = 4u;
|
|
153
|
+
const N_STEPS: u32 = 8u;
|
|
154
|
+
const PI: f32 = 3.14159265;
|
|
155
|
+
const HIZ_MAX_MIP: i32 = 4;
|
|
156
|
+
// Screen-space contact shadows (the SSAO 'G' channel). Disabled: this
|
|
157
|
+
// 12-step march is half-res, unblurred, and — unlike the horizon AO — has
|
|
158
|
+
// NO temporal accumulation, so over the dense wind-animated grass it hits
|
|
159
|
+
// swaying blades a little differently every frame, producing a sharp,
|
|
160
|
+
// pixelated, dark flickering layer. It is also independent of ssao_radius /
|
|
161
|
+
// ssao_strength, which is why tuning those changed nothing. Off until it can
|
|
162
|
+
// be gated to exclude foliage. Flip to true to restore.
|
|
163
|
+
const CONTACT_SHADOWS_ENABLED: bool = false;
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
fn hiz_sample(uv: vec2<f32>, mip: i32) -> f32 {
|
|
167
|
+
switch (clamp(mip, 0, HIZ_MAX_MIP)) {
|
|
168
|
+
case 0: { return textureSampleLevel(hiz0, hiz_samp, uv, 0.0).r; }
|
|
169
|
+
case 1: { return textureSampleLevel(hiz1, hiz_samp, uv, 0.0).r; }
|
|
170
|
+
case 2: { return textureSampleLevel(hiz2, hiz_samp, uv, 0.0).r; }
|
|
171
|
+
case 3: { return textureSampleLevel(hiz3, hiz_samp, uv, 0.0).r; }
|
|
172
|
+
default: { return textureSampleLevel(hiz4, hiz_samp, uv, 0.0).r; }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fn view_pos_from_linear(uv: vec2<f32>, linear_z: f32) -> vec3<f32> {
|
|
177
|
+
let ndc_x = uv.x * 2.0 - 1.0;
|
|
178
|
+
let ndc_y = 1.0 - uv.y * 2.0;
|
|
179
|
+
let p00 = u.proj_row01.x;
|
|
180
|
+
let p11 = u.proj_row01.y;
|
|
181
|
+
let p20 = u.proj_row01.z;
|
|
182
|
+
let p21 = u.proj_row01.w;
|
|
183
|
+
let view_z = -linear_z;
|
|
184
|
+
let view_x = -(ndc_x + p20) * view_z / p00;
|
|
185
|
+
let view_y = -(ndc_y + p21) * view_z / p11;
|
|
186
|
+
return vec3<f32>(view_x, view_y, view_z);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Mip level per march step — depends only on step_size, so it is computed
|
|
190
|
+
// once per pixel in cs_main and read by every scan_direction call.
|
|
191
|
+
var<private> mip_per_step: array<i32, 8>;
|
|
192
|
+
|
|
193
|
+
// March one horizon direction (both signs) and return its visibility
|
|
194
|
+
// contribution in [0, 1]. Factored out of cs_main so the adaptive full-scan
|
|
195
|
+
// fallback below can reuse it for the directions outside this frame's phase.
|
|
196
|
+
fn scan_direction(angle: f32, P: vec3<f32>, N: vec3<f32>, uv: vec2<f32>,
|
|
197
|
+
step_size: f32, inv_radius: f32) -> f32 {
|
|
198
|
+
let dir = vec2<f32>(cos(angle), sin(angle));
|
|
199
|
+
|
|
200
|
+
var max_horizon_pos = -1.0;
|
|
201
|
+
var max_horizon_neg = -1.0;
|
|
202
|
+
var pos_on_sky = false;
|
|
203
|
+
var neg_on_sky = false;
|
|
204
|
+
|
|
205
|
+
for (var s = 1u; s <= N_STEPS; s = s + 1u) {
|
|
206
|
+
let offset = dir * step_size * f32(s);
|
|
207
|
+
let mip = mip_per_step[s - 1u];
|
|
208
|
+
|
|
209
|
+
if (!pos_on_sky) {
|
|
210
|
+
let uv_pos = uv + offset;
|
|
211
|
+
let z_pos = hiz_sample(uv_pos, mip);
|
|
212
|
+
if (z_pos >= HIZ_SKY_Z * 0.5) {
|
|
213
|
+
pos_on_sky = true;
|
|
214
|
+
} else {
|
|
215
|
+
let S_pos = view_pos_from_linear(uv_pos, z_pos);
|
|
216
|
+
let diff_pos = S_pos - P;
|
|
217
|
+
let dist_pos = length(diff_pos);
|
|
218
|
+
if (dist_pos > 0.001) {
|
|
219
|
+
let h_pos = dot(diff_pos, N) / dist_pos;
|
|
220
|
+
let atten = saturate(1.0 - dist_pos * inv_radius);
|
|
221
|
+
max_horizon_pos = max(max_horizon_pos, mix(-1.0, h_pos, atten));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!neg_on_sky) {
|
|
227
|
+
let uv_neg = uv - offset;
|
|
228
|
+
let z_neg = hiz_sample(uv_neg, mip);
|
|
229
|
+
if (z_neg >= HIZ_SKY_Z * 0.5) {
|
|
230
|
+
neg_on_sky = true;
|
|
231
|
+
} else {
|
|
232
|
+
let S_neg = view_pos_from_linear(uv_neg, z_neg);
|
|
233
|
+
let diff_neg = S_neg - P;
|
|
234
|
+
let dist_neg = length(diff_neg);
|
|
235
|
+
if (dist_neg > 0.001) {
|
|
236
|
+
let h_neg = dot(diff_neg, N) / dist_neg;
|
|
237
|
+
let atten = saturate(1.0 - dist_neg * inv_radius);
|
|
238
|
+
max_horizon_neg = max(max_horizon_neg, mix(-1.0, h_neg, atten));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (pos_on_sky && neg_on_sky) { break; }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let vis_pos = 1.0 - saturate(max_horizon_pos);
|
|
247
|
+
let vis_neg = 1.0 - saturate(max_horizon_neg);
|
|
248
|
+
return (vis_pos + vis_neg) * 0.5;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
@compute @workgroup_size(8, 8, 1)
|
|
252
|
+
fn cs_main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
253
|
+
let px = gid.xy;
|
|
254
|
+
let in_bounds = px.x < u.size.x && px.y < u.size.y;
|
|
255
|
+
|
|
256
|
+
let inv_sz = u.params.xy;
|
|
257
|
+
let uv = (vec2<f32>(px) + vec2<f32>(0.5)) * inv_sz;
|
|
258
|
+
|
|
259
|
+
if (!in_bounds) { return; }
|
|
260
|
+
|
|
261
|
+
let center_z = hiz_sample(uv, 0);
|
|
262
|
+
// Sky early-out: write full visibility (no AO, no shadow) both
|
|
263
|
+
// to the bilateral-blur input and to history. Seeding history
|
|
264
|
+
// with 1.0 keeps next-frame's 4-tap cross clamp well-behaved
|
|
265
|
+
// where sky neighbours geometry.
|
|
266
|
+
if (center_z >= HIZ_SKY_Z * 0.5) {
|
|
267
|
+
textureStore(ao_out, vec2<i32>(px), vec4<f32>(1.0, 1.0, 0.0, 1.0));
|
|
268
|
+
textureStore(history_out, vec2<i32>(px), vec4<f32>(1.0, 1.0, 0.0, 1.0));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let P = view_pos_from_linear(uv, center_z);
|
|
273
|
+
let uv_r = uv + vec2<f32>(inv_sz.x, 0.0);
|
|
274
|
+
let uv_u = uv + vec2<f32>(0.0, -inv_sz.y);
|
|
275
|
+
let P_r = view_pos_from_linear(uv_r, hiz_sample(uv_r, 0));
|
|
276
|
+
let P_u = view_pos_from_linear(uv_u, hiz_sample(uv_u, 0));
|
|
277
|
+
let N = normalize(cross(P_r - P, P_u - P));
|
|
278
|
+
|
|
279
|
+
let coord = vec2<f32>(px);
|
|
280
|
+
let ign = fract(52.9829189 * fract(0.06711056 * coord.x + 0.00583715 * coord.y));
|
|
281
|
+
// Per-pixel IGN rotation plus a per-frame Halton-5 rotation of
|
|
282
|
+
// the direction basis. Halton base 5 is uncorrelated with TAA's
|
|
283
|
+
// Halton 2/3 pixel jitter so the two noise patterns don't
|
|
284
|
+
// resonate into a visible crawling artefact.
|
|
285
|
+
let jitter_angle = ign * PI + u.temporal.y * PI;
|
|
286
|
+
|
|
287
|
+
let radius_ws = u.params.z;
|
|
288
|
+
let strength = u.params.w;
|
|
289
|
+
|
|
290
|
+
let proj_scale_x = abs(u.proj_row01.x);
|
|
291
|
+
let proj_scale_y = abs(u.proj_row01.y);
|
|
292
|
+
let screen_radius = radius_ws * 0.5 * (proj_scale_x + proj_scale_y) / abs(P.z);
|
|
293
|
+
// Cap the screen-space radius. The old 0.25 ceiling let a near-field
|
|
294
|
+
// pixel gather horizon occlusion across a QUARTER of the screen — over
|
|
295
|
+
// dense instanced grass that pulls thousands of blades into every AO
|
|
296
|
+
// sample, painting a broad, blotchy dark layer that also shimmers with
|
|
297
|
+
// the grass wind. 0.05 keeps AO a local contact/crevice term (what it's
|
|
298
|
+
// for) instead of a scene-wide dimming wash.
|
|
299
|
+
let clamped_radius = clamp(screen_radius, 2.0 * max(inv_sz.x, inv_sz.y), 0.05);
|
|
300
|
+
|
|
301
|
+
var ao_sum = 0.0;
|
|
302
|
+
let step_size = clamped_radius / f32(N_STEPS);
|
|
303
|
+
let inv_radius = 1.0 / radius_ws;
|
|
304
|
+
|
|
305
|
+
// Mip-per-step: step s covers ~s * step_size * mip0_w pixels;
|
|
306
|
+
// take floor(log2(that)). Precomputed once per pixel.
|
|
307
|
+
let step_pixels = step_size * f32(u.size.x);
|
|
308
|
+
for (var s = 0u; s < N_STEPS; s = s + 1u) {
|
|
309
|
+
let d_px = step_pixels * f32(s + 1u);
|
|
310
|
+
mip_per_step[s] = clamp(i32(floor(log2(max(d_px, 1.0)))), 0, HIZ_MAX_MIP);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Split the 8-direction scan across 4 frames. Frame `phase = k`
|
|
314
|
+
// samples directions { phase, phase + 4 } — complementary angles
|
|
315
|
+
// in the 0..π range so every frame covers roughly the full
|
|
316
|
+
// horizon and history doesn't bias toward one hemisphere between
|
|
317
|
+
// refreshes. Over 4 frames the full 8-direction set is sampled.
|
|
318
|
+
// Flicker fix: dither the phase spatially in a 2×2 pixel quad so
|
|
319
|
+
// every quad covers all 4 phases EVERY frame. With a globally
|
|
320
|
+
// shared phase, the per-frame direction bias was identical across
|
|
321
|
+
// whole surfaces — when a phase pair landed badly on
|
|
322
|
+
// high-frequency geometry, the entire surface's AO pulsed in
|
|
323
|
+
// lockstep once per 4-frame cycle (periodic wall-wide flicker).
|
|
324
|
+
// Dithered, the same bias becomes 2×2 spatial noise that the
|
|
325
|
+
// bilateral blur and the EMA absorb.
|
|
326
|
+
let quad_offset = (px.x & 1u) + ((px.y & 1u) << 1u);
|
|
327
|
+
let phase = (u.size.z + quad_offset) % N_PHASES;
|
|
328
|
+
var scanned = 0u;
|
|
329
|
+
var vis_min = 1.0;
|
|
330
|
+
var vis_max = 0.0;
|
|
331
|
+
for (var k = 0u; k < N_DIRS_PER_FRAME; k = k + 1u) {
|
|
332
|
+
let d = phase + k * N_PHASES;
|
|
333
|
+
let angle = (f32(d) / f32(N_DIRS_TOTAL)) * PI + jitter_angle;
|
|
334
|
+
let vis = scan_direction(angle, P, N, uv, step_size, inv_radius);
|
|
335
|
+
ao_sum = ao_sum + vis;
|
|
336
|
+
scanned = scanned + 1u;
|
|
337
|
+
vis_min = min(vis_min, vis);
|
|
338
|
+
vis_max = max(vis_max, vis);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Adaptive full scan — the checkerboard fix. The quad dither above
|
|
342
|
+
// assumes its 2×2 phase noise is ABSORBED: spatially by the bilateral
|
|
343
|
+
// blur, temporally by the EMA. At alpha-cutout silhouettes (leaf cards
|
|
344
|
+
// up close) both absorbers fail at once — the blur edge-stops on the
|
|
345
|
+
// huge leaf-to-background depth delta, and wind sway keeps the history
|
|
346
|
+
// reprojecting onto what was sky or another card last frame — so the
|
|
347
|
+
// per-phase disagreement showed through as a stationary pale
|
|
348
|
+
// checkerboard at every leaf edge (scattered into 'white snow-dots' by
|
|
349
|
+
// TSR at renderScale < 1). Reconstruction can't be saved there, so
|
|
350
|
+
// remove the thing being reconstructed: when this pixel's own two
|
|
351
|
+
// directions disagree strongly (the anisotropy signature of a thin
|
|
352
|
+
// cutout edge — one axis runs along the card, the other falls off to
|
|
353
|
+
// sky/background), or the surface is in the near field (where the
|
|
354
|
+
// screen-radius clamp makes per-direction variance extreme), scan the
|
|
355
|
+
// remaining six directions too. The pixel then carries the FULL
|
|
356
|
+
// 8-direction estimate this frame: nothing left to disagree with its
|
|
357
|
+
// quad neighbours, checkerboard gone by construction. Flat walls,
|
|
358
|
+
// ground and grass-over-ground keep the cheap 2-direction path (their
|
|
359
|
+
// directions agree), so the 4× fan-out cost stays foliage-local.
|
|
360
|
+
let anisotropic = (vis_max - vis_min) > 0.3;
|
|
361
|
+
let near_field = (-P.z) < 1.5;
|
|
362
|
+
if (anisotropic || near_field) {
|
|
363
|
+
for (var d = 0u; d < N_DIRS_TOTAL; d = d + 1u) {
|
|
364
|
+
if ((d % N_PHASES) == phase) { continue; }
|
|
365
|
+
let angle = (f32(d) / f32(N_DIRS_TOTAL)) * PI + jitter_angle;
|
|
366
|
+
ao_sum = ao_sum + scan_direction(angle, P, N, uv, step_size, inv_radius);
|
|
367
|
+
scanned = scanned + 1u;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Partial-scan AO: normalise by the directions actually scanned.
|
|
372
|
+
// For 2-direction pixels the temporal blend below reconstructs the
|
|
373
|
+
// full 8-direction signal via the 4-frame EMA; full-scan pixels
|
|
374
|
+
// already carry it.
|
|
375
|
+
let ao_raw = ao_sum / f32(scanned);
|
|
376
|
+
|
|
377
|
+
// --- Screen-space contact shadows ---
|
|
378
|
+
// Gate on surface/light orientation — a back-facing pixel has no
|
|
379
|
+
// contact shadow to find, skip the 12-step march entirely.
|
|
380
|
+
// Contact shadow runs at its full 12-step count every frame (no
|
|
381
|
+
// temporal accumulation) because the directional light can change
|
|
382
|
+
// between frames and stale history here would trail behind.
|
|
383
|
+
let light_vs = normalize(u.light_dir_vs.xyz);
|
|
384
|
+
var contact = 1.0;
|
|
385
|
+
let n_dot_l = dot(N, light_vs);
|
|
386
|
+
if (CONTACT_SHADOWS_ENABLED && n_dot_l > 0.1) {
|
|
387
|
+
let cs_steps = 12u;
|
|
388
|
+
let cs_max_dist = 0.2;
|
|
389
|
+
let cs_step = cs_max_dist / f32(cs_steps);
|
|
390
|
+
let inv_p00 = u.proj_z.z;
|
|
391
|
+
let inv_p11 = u.proj_z.w;
|
|
392
|
+
for (var i = 1u; i <= cs_steps; i = i + 1u) {
|
|
393
|
+
let march_pos = P + light_vs * cs_step * f32(i);
|
|
394
|
+
let ndc_x = march_pos.x / ((-march_pos.z) * inv_p00);
|
|
395
|
+
let ndc_y = march_pos.y / ((-march_pos.z) * inv_p11);
|
|
396
|
+
let march_uv = vec2<f32>(ndc_x * 0.5 + 0.5, 1.0 - (ndc_y * 0.5 + 0.5));
|
|
397
|
+
if (march_uv.x < 0.0 || march_uv.x > 1.0 || march_uv.y < 0.0 || march_uv.y > 1.0) {
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
let march_z = hiz_sample(march_uv, 0);
|
|
401
|
+
if (march_z >= HIZ_SKY_Z * 0.5) { continue; }
|
|
402
|
+
let scene_pos = view_pos_from_linear(march_uv, march_z);
|
|
403
|
+
if (scene_pos.z > march_pos.z + 0.01) {
|
|
404
|
+
let t = f32(i) / f32(cs_steps);
|
|
405
|
+
contact = min(contact, t);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// --- Temporal accumulation ---
|
|
411
|
+
// Reproject previous-frame history via the velocity buffer. Use
|
|
412
|
+
// `textureLoad` (nearest, no sampler — velocity's bind-group
|
|
413
|
+
// entry is filterable:false). With TSR on (the default since
|
|
414
|
+
// ticket 001), the velocity RT is created at `render_extent()`
|
|
415
|
+
// = half-res, matching the SSAO pass dimensions 1:1 — so the
|
|
416
|
+
// integer coord is just `px`, NOT `px*2`. The earlier `px*2`
|
|
417
|
+
// read velocity at 2× offset, which sent 75% of SSAO pixels
|
|
418
|
+
// out of bounds (returns zero → history reprojected from the
|
|
419
|
+
// same screen pixel even during camera motion → stale geometry
|
|
420
|
+
// blended in over 4 frames → scene-wide darkening while
|
|
421
|
+
// turning + per-pixel floor sparkle from stale-history noise).
|
|
422
|
+
let vel = textureLoad(velocity_tex, vec2<i32>(px), 0).rg;
|
|
423
|
+
let prev_uv = vec2<f32>(uv.x - vel.x, uv.y + vel.y);
|
|
424
|
+
let reproj_oob = prev_uv.x < 0.0 || prev_uv.x > 1.0
|
|
425
|
+
|| prev_uv.y < 0.0 || prev_uv.y > 1.0;
|
|
426
|
+
|
|
427
|
+
var history_ao = ao_raw;
|
|
428
|
+
if (!reproj_oob) {
|
|
429
|
+
history_ao = textureSampleLevel(history_in, filt_samp, prev_uv, 0.0).r;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Temporal stability (flicker fix). The old `ao_delta > 0.35 →
|
|
433
|
+
// alpha 1` HARD refresh fired on a perfectly static camera: each
|
|
434
|
+
// frame samples only 2 of 8 directions, and when that pair lands
|
|
435
|
+
// badly on high-frequency geometry the delta spikes across the
|
|
436
|
+
// whole surface at once (the direction phase is shared by every
|
|
437
|
+
// pixel) — converged history got replaced with the noisiest
|
|
438
|
+
// possible single-frame estimate, a region-wide AO pulse that read
|
|
439
|
+
// as periodic flicker on detailed walls. On a static scene the
|
|
440
|
+
// HISTORY is the trustworthy estimate, so instead: clamp how far a
|
|
441
|
+
// single frame's raw sample can pull (±0.15 around history) and
|
|
442
|
+
// keep the plain EMA. A sustained true occlusion change still
|
|
443
|
+
// tracks at ~0.04 AO/frame (0.35 in ~10 frames — imperceptible
|
|
444
|
+
// lag), while correlated sampling spikes are capped at an
|
|
445
|
+
// invisible single-frame step. Hard refresh only on real
|
|
446
|
+
// reprojection breaks (off-screen history / first frames).
|
|
447
|
+
let force_refresh = reproj_oob || u.size.w != 0u;
|
|
448
|
+
let ao_in = clamp(ao_raw, history_ao - 0.15, history_ao + 0.15);
|
|
449
|
+
let alpha = select(u.temporal.x, 1.0, force_refresh);
|
|
450
|
+
let ao = mix(history_ao, select(ao_in, ao_raw, force_refresh), alpha);
|
|
451
|
+
|
|
452
|
+
// Contrast + floor (exact curve preserved).
|
|
453
|
+
let ao_contrasted = pow(ao, 2.0);
|
|
454
|
+
let ao_floored = max(ao_contrasted, 0.15);
|
|
455
|
+
let final_ao = mix(1.0, ao_floored, strength);
|
|
456
|
+
let contact_scaled = mix(0.1, 1.0, contact);
|
|
457
|
+
|
|
458
|
+
// Write the blended *pre-contrast* AO to history so next frame's
|
|
459
|
+
// blend stays in linear AO space (otherwise `pow(pow(x,2),2)` on
|
|
460
|
+
// every frame collapses toward black). The bilateral-blur input
|
|
461
|
+
// (ao_out) gets the contrasted + strength-modulated value so the
|
|
462
|
+
// composite pass stays visually identical to pre-temporal SSAO.
|
|
463
|
+
textureStore(
|
|
464
|
+
ao_out,
|
|
465
|
+
vec2<i32>(px),
|
|
466
|
+
vec4<f32>(saturate(final_ao), saturate(contact_scaled), 0.0, 1.0),
|
|
467
|
+
);
|
|
468
|
+
textureStore(
|
|
469
|
+
history_out,
|
|
470
|
+
vec2<i32>(px),
|
|
471
|
+
vec4<f32>(saturate(ao), saturate(contact_scaled), 0.0, 1.0),
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
";
|
|
475
|
+
|
|
476
|
+
/// Bilateral blur applied to the raw GTAO output.
|
|
477
|
+
///
|
|
478
|
+
/// A 5×5 cross-bilateral filter: for each tap we weight by a spatial
|
|
479
|
+
/// Gaussian AND by depth similarity so the blur stops at depth edges,
|
|
480
|
+
/// preserving contact-shadow / crease detail while suppressing the
|
|
481
|
+
/// per-pixel noise introduced by the horizon-sampling in GTAO.
|
|
482
|
+
///
|
|
483
|
+
/// Bindings:
|
|
484
|
+
/// 0 – uniform (SsaoBlurParams)
|
|
485
|
+
/// 1 – ssao_rt (Rg8Unorm: R = noisy GTAO, G = contact shadow)
|
|
486
|
+
/// 2 – ao sampler (filtering)
|
|
487
|
+
/// 3 – depth_tex (Depth32Float for edge-stopping)
|
|
488
|
+
/// 4 – depth sampler (non-filtering)
|
|
489
|
+
pub(in crate::renderer) const SSAO_BLUR_SHADER_WGSL: &str = "
|
|
490
|
+
struct SsaoBlurParams {
|
|
491
|
+
// xy = texel_size (1/w, 1/h of the SSAO RT, i.e. half-res)
|
|
492
|
+
// z = depth_sigma (edge-stop threshold, ~0.01–0.1 in NDC depth)
|
|
493
|
+
// w = unused
|
|
494
|
+
params: vec4<f32>,
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
@group(0) @binding(0) var<uniform> u: SsaoBlurParams;
|
|
498
|
+
@group(0) @binding(1) var ao_tex: texture_2d<f32>;
|
|
499
|
+
@group(0) @binding(2) var ao_samp: sampler;
|
|
500
|
+
@group(0) @binding(3) var depth_tex: texture_depth_2d;
|
|
501
|
+
@group(0) @binding(4) var depth_samp: sampler;
|
|
502
|
+
|
|
503
|
+
struct VsOut {
|
|
504
|
+
@builtin(position) clip_pos: vec4<f32>,
|
|
505
|
+
@location(0) uv: vec2<f32>,
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
@vertex
|
|
509
|
+
fn vs_main(@builtin(vertex_index) vid: u32) -> VsOut {
|
|
510
|
+
let x = f32((vid & 1u) * 4u) - 1.0;
|
|
511
|
+
let y = f32((vid >> 1u) * 4u) - 1.0;
|
|
512
|
+
var out: VsOut;
|
|
513
|
+
out.clip_pos = vec4<f32>(x, y, 0.0, 1.0);
|
|
514
|
+
out.uv = vec2<f32>((x + 1.0) * 0.5, (1.0 - y) * 0.5);
|
|
515
|
+
return out;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Pre-computed Gaussian weights for a 5-tap 1-D kernel (sigma ≈ 1.4).
|
|
519
|
+
// Offsets: -2, -1, 0, +1, +2
|
|
520
|
+
const GAUSS5: array<f32, 5> = array<f32, 5>(
|
|
521
|
+
0.0625, 0.25, 0.375, 0.25, 0.0625
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
@fragment
|
|
525
|
+
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
|
526
|
+
let texel = u.params.xy;
|
|
527
|
+
let d_sigma = u.params.z;
|
|
528
|
+
|
|
529
|
+
let center_depth = textureSampleLevel(depth_tex, depth_samp, in.uv, 0u);
|
|
530
|
+
|
|
531
|
+
// Sky pixels: no occlusion, no contact shadow.
|
|
532
|
+
if (center_depth >= 0.9999) {
|
|
533
|
+
return vec4<f32>(1.0, 1.0, 0.0, 1.0);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
var ao_sum = 0.0;
|
|
537
|
+
var weight_sum = 0.0;
|
|
538
|
+
|
|
539
|
+
// 5×5 separable-style bilateral gather on the AO (R) channel
|
|
540
|
+
// only. Contact shadows (G) pass through from the center tap
|
|
541
|
+
// untouched — they're a sharp binary ray-march result and any
|
|
542
|
+
// smoothing here erases the fine-detail shadows the pass is
|
|
543
|
+
// there to capture.
|
|
544
|
+
for (var dy: i32 = -2; dy <= 2; dy = dy + 1) {
|
|
545
|
+
for (var dx: i32 = -2; dx <= 2; dx = dx + 1) {
|
|
546
|
+
let offset = vec2<f32>(f32(dx), f32(dy)) * texel;
|
|
547
|
+
let s_uv = in.uv + offset;
|
|
548
|
+
|
|
549
|
+
let s_ao = textureSampleLevel(ao_tex, ao_samp, s_uv, 0.0).r;
|
|
550
|
+
let s_depth = textureSampleLevel(depth_tex, depth_samp, s_uv, 0u);
|
|
551
|
+
|
|
552
|
+
let gx = GAUSS5[dx + 2];
|
|
553
|
+
let gy = GAUSS5[dy + 2];
|
|
554
|
+
let spatial = gx * gy;
|
|
555
|
+
|
|
556
|
+
let depth_diff = abs(center_depth - s_depth);
|
|
557
|
+
let range_w = exp(-depth_diff / d_sigma);
|
|
558
|
+
|
|
559
|
+
let w = spatial * range_w;
|
|
560
|
+
ao_sum += s_ao * w;
|
|
561
|
+
weight_sum += w;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
let center = textureSampleLevel(ao_tex, ao_samp, in.uv, 0.0);
|
|
566
|
+
let ao_blurred = select(center.r, ao_sum / weight_sum, weight_sum > 0.0001);
|
|
567
|
+
return vec4<f32>(ao_blurred, center.g, 0.0, 1.0);
|
|
568
|
+
}
|
|
569
|
+
";
|
|
570
|
+
|