@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.
Files changed (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +231 -0
  3. package/native/android/Cargo.lock +1848 -0
  4. package/native/android/Cargo.toml +24 -0
  5. package/native/android/src/lib.rs +702 -0
  6. package/native/ios/Cargo.lock +1690 -0
  7. package/native/ios/Cargo.toml +32 -0
  8. package/native/ios/src/lib.rs +1267 -0
  9. package/native/linux/Cargo.lock +3279 -0
  10. package/native/linux/Cargo.toml +29 -0
  11. package/native/linux/src/lib.rs +1331 -0
  12. package/native/macos/Cargo.lock +3310 -0
  13. package/native/macos/Cargo.toml +46 -0
  14. package/native/macos/src/lib.rs +1302 -0
  15. package/native/shared/Cargo.lock +1899 -0
  16. package/native/shared/Cargo.toml +62 -0
  17. package/native/shared/assets/default_font.ttf +0 -0
  18. package/native/shared/build.rs +270 -0
  19. package/native/shared/shaders/common/clouds.wgsl +122 -0
  20. package/native/shared/shaders/common/fog.wgsl +16 -0
  21. package/native/shared/shaders/common/foliage_wind.wgsl +98 -0
  22. package/native/shared/shaders/common/imposter.wgsl +112 -0
  23. package/native/shared/shaders/common/pbr.wgsl +186 -0
  24. package/native/shared/shaders/common/shadows.wgsl +186 -0
  25. package/native/shared/shaders/common/sky.wgsl +8 -0
  26. package/native/shared/shaders/common/tonemap.wgsl +25 -0
  27. package/native/shared/shaders/impulse_field.wgsl +57 -0
  28. package/native/shared/shaders/material_abi.wgsl +383 -0
  29. package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
  30. package/native/shared/src/anim_mixer.rs +61 -0
  31. package/native/shared/src/attach.rs +263 -0
  32. package/native/shared/src/audio/decode.rs +123 -0
  33. package/native/shared/src/audio/mod.rs +863 -0
  34. package/native/shared/src/audio/render.rs +892 -0
  35. package/native/shared/src/audio/spsc.rs +156 -0
  36. package/native/shared/src/audio/stream.rs +226 -0
  37. package/native/shared/src/custom_shaders.rs +104 -0
  38. package/native/shared/src/decals.rs +245 -0
  39. package/native/shared/src/drs.rs +211 -0
  40. package/native/shared/src/engine.rs +261 -0
  41. package/native/shared/src/ffi.rs +116 -0
  42. package/native/shared/src/ffi_core/assets.rs +388 -0
  43. package/native/shared/src/ffi_core/audio_ffi.rs +184 -0
  44. package/native/shared/src/ffi_core/draw.rs +334 -0
  45. package/native/shared/src/ffi_core/game_loop.rs +577 -0
  46. package/native/shared/src/ffi_core/input.rs +234 -0
  47. package/native/shared/src/ffi_core/mod.rs +127 -0
  48. package/native/shared/src/ffi_core/models.rs +1154 -0
  49. package/native/shared/src/ffi_core/ragdoll_ffi.rs +261 -0
  50. package/native/shared/src/ffi_core/scene.rs +626 -0
  51. package/native/shared/src/ffi_core/vfx.rs +212 -0
  52. package/native/shared/src/ffi_core/visual.rs +691 -0
  53. package/native/shared/src/frame_callbacks.rs +122 -0
  54. package/native/shared/src/geometry.rs +236 -0
  55. package/native/shared/src/handles.rs +182 -0
  56. package/native/shared/src/input.rs +448 -0
  57. package/native/shared/src/jolt_sys.rs +822 -0
  58. package/native/shared/src/lib.rs +55 -0
  59. package/native/shared/src/models.rs +1093 -0
  60. package/native/shared/src/models_gltf.rs +1280 -0
  61. package/native/shared/src/particles.rs +391 -0
  62. package/native/shared/src/physics_jolt.rs +1908 -0
  63. package/native/shared/src/picking.rs +298 -0
  64. package/native/shared/src/postfx.rs +345 -0
  65. package/native/shared/src/profiler.rs +492 -0
  66. package/native/shared/src/ragdoll.rs +474 -0
  67. package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
  68. package/native/shared/src/renderer/brdf_lut.rs +154 -0
  69. package/native/shared/src/renderer/draw2d.rs +143 -0
  70. package/native/shared/src/renderer/formats.rs +822 -0
  71. package/native/shared/src/renderer/froxel.rs +421 -0
  72. package/native/shared/src/renderer/gi_bake.rs +653 -0
  73. package/native/shared/src/renderer/graph.rs +462 -0
  74. package/native/shared/src/renderer/hiz.rs +269 -0
  75. package/native/shared/src/renderer/hot_reload.rs +390 -0
  76. package/native/shared/src/renderer/impulse_field.rs +456 -0
  77. package/native/shared/src/renderer/lighting.rs +154 -0
  78. package/native/shared/src/renderer/material_instancing.rs +171 -0
  79. package/native/shared/src/renderer/material_pipeline.rs +700 -0
  80. package/native/shared/src/renderer/material_system.rs +1996 -0
  81. package/native/shared/src/renderer/material_system_tests.rs +601 -0
  82. package/native/shared/src/renderer/material_system_wasm.rs +41 -0
  83. package/native/shared/src/renderer/mod.rs +12556 -0
  84. package/native/shared/src/renderer/model_draw.rs +641 -0
  85. package/native/shared/src/renderer/occlusion.rs +429 -0
  86. package/native/shared/src/renderer/planar_pass.rs +593 -0
  87. package/native/shared/src/renderer/planar_reflection.rs +499 -0
  88. package/native/shared/src/renderer/post_pass.rs +249 -0
  89. package/native/shared/src/renderer/postfx_chain.rs +728 -0
  90. package/native/shared/src/renderer/pt_pass.rs +577 -0
  91. package/native/shared/src/renderer/scene_pass.rs +607 -0
  92. package/native/shared/src/renderer/shader_include.rs +205 -0
  93. package/native/shared/src/renderer/shader_library.rs +135 -0
  94. package/native/shared/src/renderer/shaders/ao.rs +570 -0
  95. package/native/shared/src/renderer/shaders/core.rs +1243 -0
  96. package/native/shared/src/renderer/shaders/env.rs +907 -0
  97. package/native/shared/src/renderer/shaders/gi.rs +810 -0
  98. package/native/shared/src/renderer/shaders/mod.rs +19 -0
  99. package/native/shared/src/renderer/shaders/post.rs +1558 -0
  100. package/native/shared/src/renderer/shaders/pt.rs +1859 -0
  101. package/native/shared/src/renderer/shaders/ssgi.rs +1586 -0
  102. package/native/shared/src/renderer/shadow_pass.rs +731 -0
  103. package/native/shared/src/renderer/ssgi_pass.rs +392 -0
  104. package/native/shared/src/renderer/ssr_pass.rs +188 -0
  105. package/native/shared/src/renderer/texture_store.rs +473 -0
  106. package/native/shared/src/renderer/transient.rs +591 -0
  107. package/native/shared/src/renderer/types.rs +941 -0
  108. package/native/shared/src/renderer/util.rs +152 -0
  109. package/native/shared/src/scene.rs +1362 -0
  110. package/native/shared/src/sdf_cache.rs +274 -0
  111. package/native/shared/src/shadows.rs +1036 -0
  112. package/native/shared/src/staging.rs +102 -0
  113. package/native/shared/src/string_header.rs +266 -0
  114. package/native/shared/src/text_renderer.rs +502 -0
  115. package/native/shared/src/textures.rs +197 -0
  116. package/native/tvos/Cargo.lock +1693 -0
  117. package/native/tvos/Cargo.toml +36 -0
  118. package/native/tvos/metal-patched/Cargo.toml +178 -0
  119. package/native/tvos/metal-patched/LICENSE-APACHE +201 -0
  120. package/native/tvos/metal-patched/LICENSE-MIT +25 -0
  121. package/native/tvos/metal-patched/src/acceleration_structure.rs +667 -0
  122. package/native/tvos/metal-patched/src/acceleration_structure_pass.rs +108 -0
  123. package/native/tvos/metal-patched/src/argument.rs +366 -0
  124. package/native/tvos/metal-patched/src/blitpass.rs +102 -0
  125. package/native/tvos/metal-patched/src/buffer.rs +71 -0
  126. package/native/tvos/metal-patched/src/capturedescriptor.rs +76 -0
  127. package/native/tvos/metal-patched/src/capturemanager.rs +113 -0
  128. package/native/tvos/metal-patched/src/commandbuffer.rs +192 -0
  129. package/native/tvos/metal-patched/src/commandqueue.rs +44 -0
  130. package/native/tvos/metal-patched/src/computepass.rs +107 -0
  131. package/native/tvos/metal-patched/src/constants.rs +152 -0
  132. package/native/tvos/metal-patched/src/counters.rs +119 -0
  133. package/native/tvos/metal-patched/src/depthstencil.rs +190 -0
  134. package/native/tvos/metal-patched/src/device.rs +2134 -0
  135. package/native/tvos/metal-patched/src/drawable.rs +39 -0
  136. package/native/tvos/metal-patched/src/encoder.rs +2041 -0
  137. package/native/tvos/metal-patched/src/heap.rs +281 -0
  138. package/native/tvos/metal-patched/src/indirect_encoder.rs +344 -0
  139. package/native/tvos/metal-patched/src/lib.rs +657 -0
  140. package/native/tvos/metal-patched/src/library.rs +902 -0
  141. package/native/tvos/metal-patched/src/mps.rs +575 -0
  142. package/native/tvos/metal-patched/src/pipeline/compute.rs +475 -0
  143. package/native/tvos/metal-patched/src/pipeline/mod.rs +71 -0
  144. package/native/tvos/metal-patched/src/pipeline/render.rs +762 -0
  145. package/native/tvos/metal-patched/src/renderpass.rs +443 -0
  146. package/native/tvos/metal-patched/src/resource.rs +182 -0
  147. package/native/tvos/metal-patched/src/sampler.rs +165 -0
  148. package/native/tvos/metal-patched/src/sync.rs +178 -0
  149. package/native/tvos/metal-patched/src/texture.rs +352 -0
  150. package/native/tvos/metal-patched/src/types.rs +90 -0
  151. package/native/tvos/metal-patched/src/vertexdescriptor.rs +250 -0
  152. package/native/tvos/src/audio_backend.rs +197 -0
  153. package/native/tvos/src/lib.rs +1891 -0
  154. package/native/visionos/Cargo.lock +1693 -0
  155. package/native/visionos/Cargo.toml +40 -0
  156. package/native/visionos/src/audio_backend.rs +197 -0
  157. package/native/visionos/src/lib.rs +1887 -0
  158. package/native/watchos/Cargo.lock +16 -0
  159. package/native/watchos/Cargo.toml +19 -0
  160. package/native/watchos/shaders/bloom_postfx.metal +99 -0
  161. package/native/watchos/src/BloomWatchApp.swift +1267 -0
  162. package/native/watchos/src/BloomWatchAudio.swift +179 -0
  163. package/native/watchos/src/audio.rs +55 -0
  164. package/native/watchos/src/draw_list.rs +229 -0
  165. package/native/watchos/src/ffi_stubs.rs +915 -0
  166. package/native/watchos/src/ffi_stubs_manual.rs +35 -0
  167. package/native/watchos/src/lib.rs +1124 -0
  168. package/native/watchos/src/models.rs +746 -0
  169. package/native/watchos/src/postfx.rs +95 -0
  170. package/native/watchos/src/scene.rs +534 -0
  171. package/native/watchos/src/textures.rs +184 -0
  172. package/native/web/Cargo.lock +1657 -0
  173. package/native/web/Cargo.toml +43 -0
  174. package/native/web/bloom_glue.js +695 -0
  175. package/native/web/build.sh +131 -0
  176. package/native/web/index.html +35 -0
  177. package/native/web/jolt_bridge.js +1519 -0
  178. package/native/web/src/input_ffi.rs +286 -0
  179. package/native/web/src/lib.rs +1796 -0
  180. package/native/web/src/material_ffi.rs +710 -0
  181. package/native/web/src/parity_ffi.rs +343 -0
  182. package/native/web/src/physics_ffi.rs +643 -0
  183. package/native/web/src/ragdoll_ffi.rs +250 -0
  184. package/native/web/src/render_settings.rs +98 -0
  185. package/native/windows/Cargo.lock +1815 -0
  186. package/native/windows/Cargo.toml +68 -0
  187. package/native/windows/src/lib.rs +1486 -0
  188. package/package.json +4279 -0
  189. package/src/audio/index.ts +315 -0
  190. package/src/core/colors.ts +63 -0
  191. package/src/core/index.ts +1206 -0
  192. package/src/core/keys.ts +63 -0
  193. package/src/core/types.ts +104 -0
  194. package/src/index.ts +171 -0
  195. package/src/math/index.ts +516 -0
  196. package/src/mobile/index.ts +294 -0
  197. package/src/models/index.ts +1258 -0
  198. package/src/physics/index.ts +1134 -0
  199. package/src/scene/index.ts +698 -0
  200. package/src/shapes/index.ts +120 -0
  201. package/src/text/index.ts +48 -0
  202. package/src/textures/index.ts +187 -0
  203. package/src/vfx/index.ts +191 -0
  204. package/src/world/index.ts +24 -0
  205. package/src/world/loader.ts +423 -0
  206. package/src/world/prefab.ts +217 -0
  207. package/src/world/render.ts +172 -0
  208. package/src/world/saver.ts +108 -0
  209. package/src/world/serialize.ts +301 -0
  210. package/src/world/terrain.ts +355 -0
  211. package/src/world/types.ts +160 -0
  212. package/src/world/validate.ts +319 -0
  213. 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
+