@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,810 @@
1
+ //! Surface-cache GI: mesh cards, SDF bakes, WSRC.
2
+ //! Split from renderer/shaders.rs.
3
+
4
+ /// Ticket 013 V3 — Mesh-Cards capture shader with dual render targets.
5
+ ///
6
+ /// Location 0 (albedo) + Location 1 (emissive). Rasterises a mesh
7
+ /// orthographically along its assigned signed axis into the card slot,
8
+ /// writing baked albedo and emissive into their respective atlases in
9
+ /// one draw. Emissive reads the material's emissive texture (if any)
10
+ /// and multiplies by the `emissive_factor`. Flat `base_color_factor` +
11
+ /// a 1×1 white fallback texture cover the case where the mesh has
12
+ /// only a scalar material.
13
+ pub(in crate::renderer) const CARD_CAPTURE_WGSL: &str = "
14
+ struct CaptureParams {
15
+ ortho_vp: mat4x4<f32>,
16
+ // Mesh's base_color_factor (xyz) + `has_base_texture` flag (w).
17
+ base_color: vec4<f32>,
18
+ // Mesh's emissive_factor (xyz) + `has_emissive_texture` flag (w).
19
+ emissive: vec4<f32>,
20
+ };
21
+
22
+ struct VertexIn {
23
+ @location(0) position: vec3<f32>,
24
+ @location(1) normal: vec3<f32>,
25
+ @location(2) color: vec4<f32>,
26
+ @location(3) uv: vec2<f32>,
27
+ @location(4) joints: vec4<f32>,
28
+ @location(5) weights: vec4<f32>,
29
+ };
30
+
31
+ struct VsOut {
32
+ @builtin(position) clip_pos: vec4<f32>,
33
+ @location(0) uv: vec2<f32>,
34
+ };
35
+
36
+ struct FsOut {
37
+ @location(0) albedo: vec4<f32>,
38
+ @location(1) emissive: vec4<f32>,
39
+ };
40
+
41
+ @group(0) @binding(0) var<uniform> u: CaptureParams;
42
+ @group(1) @binding(0) var albedo_tex: texture_2d<f32>;
43
+ @group(1) @binding(1) var albedo_samp: sampler;
44
+ @group(1) @binding(2) var emissive_tex: texture_2d<f32>;
45
+
46
+ @vertex
47
+ fn vs_main(v: VertexIn) -> VsOut {
48
+ var out: VsOut;
49
+ out.clip_pos = u.ortho_vp * vec4<f32>(v.position, 1.0);
50
+ out.uv = v.uv;
51
+ return out;
52
+ }
53
+
54
+ @fragment
55
+ fn fs_main(in: VsOut) -> FsOut {
56
+ var albedo = u.base_color.rgb;
57
+ if (u.base_color.w > 0.5) {
58
+ albedo = albedo * textureSample(albedo_tex, albedo_samp, in.uv).rgb;
59
+ }
60
+
61
+ var emissive = u.emissive.rgb;
62
+ if (u.emissive.w > 0.5) {
63
+ emissive = emissive * textureSample(emissive_tex, albedo_samp, in.uv).rgb;
64
+ }
65
+
66
+ var out: FsOut;
67
+ out.albedo = vec4<f32>(albedo, 1.0);
68
+ // Rgba8UnormSrgb clamps emissive at 1.0 per channel; that's fine
69
+ // for Sponza-scale lanterns. Pre-divide by EMISSIVE_SCALE at write
70
+ // and multiply back at sample if HDR emissive becomes necessary.
71
+ out.emissive = vec4<f32>(clamp(emissive, vec3<f32>(0.0), vec3<f32>(1.0)), 1.0);
72
+ return out;
73
+ }
74
+ ";
75
+
76
+ /// Ticket 014 V1 — per-mesh unsigned distance field bake.
77
+ ///
78
+ /// One compute invocation per voxel (32×32×32 = 32768 per mesh). Each
79
+ /// lane reads the mesh's vertex + index buffers as storage, iterates
80
+ /// all triangles, and computes `min(point-triangle distance)` from the
81
+ /// voxel centre. Output is R16Float distance (unsigned — we don't
82
+ /// attempt inside/outside classification, which is brittle on open
83
+ /// Sponza meshes; sphere-trace works with UDF either way because each
84
+ /// march step advances by `d` regardless of sign).
85
+ ///
86
+ /// Vertex stride matches `Vertex3D` (12 f32 = 48 bytes). Only the
87
+ /// first 3 floats (position) are read.
88
+ pub(in crate::renderer) const SDF_BAKE_WGSL: &str = "
89
+ struct SdfBakeParams {
90
+ aabb_min: vec4<f32>,
91
+ aabb_max: vec4<f32>,
92
+ // x = triangle_count, y = sdf_resolution, zw unused
93
+ counts: vec4<u32>,
94
+ };
95
+
96
+ @group(0) @binding(0) var<uniform> u: SdfBakeParams;
97
+ @group(0) @binding(1) var<storage, read> vertex_buf: array<f32>;
98
+ @group(0) @binding(2) var<storage, read> index_buf: array<u32>;
99
+ @group(0) @binding(3) var sdf_out: texture_storage_3d<r32float, write>;
100
+
101
+ const VERTEX_STRIDE_F32: u32 = 12u; // Vertex3D: pos(3) + normal(3) + color(4) + uv(2) = 12 f32
102
+
103
+ fn vtx_pos(idx: u32) -> vec3<f32> {
104
+ let base = idx * VERTEX_STRIDE_F32;
105
+ return vec3<f32>(vertex_buf[base], vertex_buf[base + 1u], vertex_buf[base + 2u]);
106
+ }
107
+
108
+ // Point-triangle distance, clamped-edge form. From Ericson, Real-Time
109
+ // Collision Detection. Returns unsigned distance to the closest point
110
+ // on triangle abc from point p.
111
+ fn point_triangle_distance(p: vec3<f32>, a: vec3<f32>, b: vec3<f32>, c: vec3<f32>) -> f32 {
112
+ let ab = b - a;
113
+ let ac = c - a;
114
+ let ap = p - a;
115
+ let d1 = dot(ab, ap);
116
+ let d2 = dot(ac, ap);
117
+ if (d1 <= 0.0 && d2 <= 0.0) { return length(ap); }
118
+ let bp = p - b;
119
+ let d3 = dot(ab, bp);
120
+ let d4 = dot(ac, bp);
121
+ if (d3 >= 0.0 && d4 <= d3) { return length(bp); }
122
+ let vc = d1 * d4 - d3 * d2;
123
+ if (vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0) {
124
+ let v = d1 / (d1 - d3);
125
+ return length(p - (a + v * ab));
126
+ }
127
+ let cp = p - c;
128
+ let d5 = dot(ab, cp);
129
+ let d6 = dot(ac, cp);
130
+ if (d6 >= 0.0 && d5 <= d6) { return length(cp); }
131
+ let vb = d5 * d2 - d1 * d6;
132
+ if (vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0) {
133
+ let w = d2 / (d2 - d6);
134
+ return length(p - (a + w * ac));
135
+ }
136
+ let va = d3 * d6 - d5 * d4;
137
+ if (va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0) {
138
+ let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
139
+ return length(p - (b + w * (c - b)));
140
+ }
141
+ // Closest point in face interior.
142
+ let denom = 1.0 / (va + vb + vc);
143
+ let v = vb * denom;
144
+ let w = vc * denom;
145
+ return length(p - (a + ab * v + ac * w));
146
+ }
147
+
148
+ @compute @workgroup_size(4, 4, 4)
149
+ fn cs_main(@builtin(global_invocation_id) gid: vec3<u32>) {
150
+ let res = u.counts.y;
151
+ if (gid.x >= res || gid.y >= res || gid.z >= res) { return; }
152
+
153
+ // Voxel centre in object space.
154
+ let uvw = (vec3<f32>(gid) + vec3<f32>(0.5)) / f32(res);
155
+ let voxel_os = mix(u.aabb_min.xyz, u.aabb_max.xyz, uvw);
156
+
157
+ var min_dist = 1e9;
158
+ let tri_count = u.counts.x;
159
+ for (var t: u32 = 0u; t < tri_count; t = t + 1u) {
160
+ let i0 = index_buf[t * 3u + 0u];
161
+ let i1 = index_buf[t * 3u + 1u];
162
+ let i2 = index_buf[t * 3u + 2u];
163
+ let a = vtx_pos(i0);
164
+ let b = vtx_pos(i1);
165
+ let c = vtx_pos(i2);
166
+ let d = point_triangle_distance(voxel_os, a, b, c);
167
+ min_dist = min(min_dist, d);
168
+ }
169
+
170
+ textureStore(sdf_out, vec3<i32>(gid), vec4<f32>(min_dist, 0.0, 0.0, 0.0));
171
+ }
172
+ ";
173
+
174
+ /// Fullscreen-lag fix — scene-clipmap variant of the SDF bake.
175
+ ///
176
+ /// The original clipmap bake reused `SDF_BAKE_WGSL`: every one of the
177
+ /// 64³ voxels looped over EVERY scene triangle, and the whole volume
178
+ /// baked in a single dispatch. On integrated GPUs that one dispatch
179
+ /// stalled the end-of-frame submit for seconds whenever the camera
180
+ /// drifted 10 m. This variant fixes both axes of that cost:
181
+ ///
182
+ /// - Triangles are binned on the CPU into `counts.x` cells per axis,
183
+ /// each cell's list pre-expanded by one cell in every direction. A
184
+ /// voxel only tests its own cell's list. Distances are clamped at
185
+ /// one cell width (`band`); because of the one-cell expansion the
186
+ /// clamp never OVERestimates the distance to the nearest surface,
187
+ /// which keeps sphere-trace steps conservative. Empty-air cells skip
188
+ /// the triangle loop entirely.
189
+ /// - `counts.z` carries a voxel Z offset so the caller can bake a few
190
+ /// Z-layers per frame into a staging texture instead of the whole
191
+ /// volume at once.
192
+ pub(in crate::renderer) const SDF_CLIPMAP_BAKE_WGSL: &str = "
193
+ struct SdfClipmapBakeParams {
194
+ aabb_min: vec4<f32>,
195
+ aabb_max: vec4<f32>,
196
+ // x = bin cells per axis, y = sdf resolution,
197
+ // z = voxel Z offset of this slice batch, w unused
198
+ counts: vec4<u32>,
199
+ };
200
+
201
+ @group(0) @binding(0) var<uniform> u: SdfClipmapBakeParams;
202
+ @group(0) @binding(1) var<storage, read> vertex_buf: array<f32>;
203
+ @group(0) @binding(2) var<storage, read> index_buf: array<u32>;
204
+ @group(0) @binding(3) var sdf_out: texture_storage_3d<r32float, write>;
205
+ @group(0) @binding(4) var<storage, read> cell_offsets: array<u32>;
206
+ @group(0) @binding(5) var<storage, read> cell_tris: array<u32>;
207
+
208
+ const VERTEX_STRIDE_F32: u32 = 12u; // Vertex3D: pos(3) + normal(3) + color(4) + uv(2)
209
+
210
+ fn vtx_pos(idx: u32) -> vec3<f32> {
211
+ let base = idx * VERTEX_STRIDE_F32;
212
+ return vec3<f32>(vertex_buf[base], vertex_buf[base + 1u], vertex_buf[base + 2u]);
213
+ }
214
+
215
+ // Point-triangle distance, clamped-edge form (Ericson).
216
+ fn point_triangle_distance(p: vec3<f32>, a: vec3<f32>, b: vec3<f32>, c: vec3<f32>) -> f32 {
217
+ let ab = b - a;
218
+ let ac = c - a;
219
+ let ap = p - a;
220
+ let d1 = dot(ab, ap);
221
+ let d2 = dot(ac, ap);
222
+ if (d1 <= 0.0 && d2 <= 0.0) { return length(ap); }
223
+ let bp = p - b;
224
+ let d3 = dot(ab, bp);
225
+ let d4 = dot(ac, bp);
226
+ if (d3 >= 0.0 && d4 <= d3) { return length(bp); }
227
+ let vc = d1 * d4 - d3 * d2;
228
+ if (vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0) {
229
+ let v = d1 / (d1 - d3);
230
+ return length(p - (a + v * ab));
231
+ }
232
+ let cp = p - c;
233
+ let d5 = dot(ab, cp);
234
+ let d6 = dot(ac, cp);
235
+ if (d6 >= 0.0 && d5 <= d6) { return length(cp); }
236
+ let vb = d5 * d2 - d1 * d6;
237
+ if (vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0) {
238
+ let w = d2 / (d2 - d6);
239
+ return length(p - (a + w * ac));
240
+ }
241
+ let va = d3 * d6 - d5 * d4;
242
+ if (va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0) {
243
+ let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
244
+ return length(p - (b + w * (c - b)));
245
+ }
246
+ let denom = 1.0 / (va + vb + vc);
247
+ let v = vb * denom;
248
+ let w = vc * denom;
249
+ return length(p - (a + ab * v + ac * w));
250
+ }
251
+
252
+ @compute @workgroup_size(4, 4, 4)
253
+ fn cs_main(@builtin(global_invocation_id) gid: vec3<u32>) {
254
+ let res = u.counts.y;
255
+ let vox = vec3<u32>(gid.x, gid.y, gid.z + u.counts.z);
256
+ if (vox.x >= res || vox.y >= res || vox.z >= res) { return; }
257
+
258
+ let uvw = (vec3<f32>(vox) + vec3<f32>(0.5)) / f32(res);
259
+ let voxel_ws = mix(u.aabb_min.xyz, u.aabb_max.xyz, uvw);
260
+
261
+ let cells = u.counts.x;
262
+ let vpc = res / cells;
263
+ let cell = vox / vpc;
264
+ let ci = (cell.z * cells + cell.y) * cells + cell.x;
265
+ let start = cell_offsets[ci];
266
+ let end = cell_offsets[ci + 1u];
267
+
268
+ let band = (u.aabb_max.x - u.aabb_min.x) / f32(cells);
269
+ var min_dist = band;
270
+ for (var k: u32 = start; k < end; k = k + 1u) {
271
+ let t = cell_tris[k];
272
+ let a = vtx_pos(index_buf[t * 3u + 0u]);
273
+ let b = vtx_pos(index_buf[t * 3u + 1u]);
274
+ let c = vtx_pos(index_buf[t * 3u + 2u]);
275
+ min_dist = min(min_dist, point_triangle_distance(voxel_ws, a, b, c));
276
+ }
277
+
278
+ textureStore(sdf_out, vec3<i32>(vox), vec4<f32>(min_dist, 0.0, 0.0, 0.0));
279
+ }
280
+ ";
281
+
282
+ /// Ticket 013 V3 — per-frame card-lighting compute pass with
283
+ /// shadow-cascade sampling + emissive contribution.
284
+ ///
285
+ /// Per texel: reconstruct the world-space position of the card point
286
+ /// (slot metadata's aabb + axis + mesh transform), sample the shadow
287
+ /// cascade at that point, compute direct + sky + emissive. Writes to
288
+ /// the radiance atlas. Shadow lookup makes indirect bounce meaningfully
289
+ /// darker on sun-occluded card faces (arches, column undersides).
290
+ pub(in crate::renderer) const CARD_LIGHT_WGSL: &str = "
291
+ struct SlotMeta {
292
+ // xyz = world-space card-face normal, w = signed axis (0..6 as f32)
293
+ normal_ws: vec4<f32>,
294
+ aabb_min: vec4<f32>,
295
+ aabb_max: vec4<f32>,
296
+ transform: mat4x4<f32>,
297
+ };
298
+
299
+ struct CardLightParams {
300
+ sun_dir: vec4<f32>,
301
+ sun_color: vec4<f32>,
302
+ sky_color: vec4<f32>,
303
+ atlas_info: vec4<u32>,
304
+ shadow_vps: array<mat4x4<f32>, 3>,
305
+ shadow_splits: vec4<f32>,
306
+ view_matrix: mat4x4<f32>,
307
+ flags: vec4<f32>,
308
+ };
309
+
310
+ @group(0) @binding(0) var<uniform> u: CardLightParams;
311
+ @group(0) @binding(1) var albedo_atlas: texture_2d<f32>;
312
+ @group(0) @binding(2) var atlas_samp: sampler;
313
+ @group(0) @binding(3) var<storage, read> slot_meta: array<SlotMeta>;
314
+ @group(0) @binding(4) var radiance_out: texture_storage_2d<rgba16float, write>;
315
+ @group(0) @binding(5) var emissive_atlas: texture_2d<f32>;
316
+ @group(0) @binding(6) var shadow_atlas_0: texture_depth_2d;
317
+ @group(0) @binding(7) var shadow_atlas_1: texture_depth_2d;
318
+ @group(0) @binding(8) var shadow_atlas_2: texture_depth_2d;
319
+ @group(0) @binding(9) var shadow_samp: sampler_comparison;
320
+
321
+ fn sample_cascade(cascade: i32, pos_ws: vec3<f32>, bias: f32) -> f32 {
322
+ var clip: vec4<f32>;
323
+ if (cascade == 0) {
324
+ clip = u.shadow_vps[0] * vec4<f32>(pos_ws, 1.0);
325
+ } else if (cascade == 1) {
326
+ clip = u.shadow_vps[1] * vec4<f32>(pos_ws, 1.0);
327
+ } else {
328
+ clip = u.shadow_vps[2] * vec4<f32>(pos_ws, 1.0);
329
+ }
330
+ let ndc = clip.xyz / clip.w;
331
+ // Outside the cascade frustum → treat as lit (no shadow).
332
+ if (ndc.x < -1.0 || ndc.x > 1.0 || ndc.y < -1.0 || ndc.y > 1.0 || ndc.z < 0.0 || ndc.z > 1.0) {
333
+ return 1.0;
334
+ }
335
+ let shadow_uv = vec2<f32>(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
336
+ let ref_depth = ndc.z - bias;
337
+ if (cascade == 0) {
338
+ return textureSampleCompareLevel(shadow_atlas_0, shadow_samp, shadow_uv, ref_depth);
339
+ } else if (cascade == 1) {
340
+ return textureSampleCompareLevel(shadow_atlas_1, shadow_samp, shadow_uv, ref_depth);
341
+ } else {
342
+ return textureSampleCompareLevel(shadow_atlas_2, shadow_samp, shadow_uv, ref_depth);
343
+ }
344
+ }
345
+
346
+ @compute @workgroup_size(8, 8, 1)
347
+ fn cs_main(@builtin(global_invocation_id) gid: vec3<u32>) {
348
+ let px = gid.xy;
349
+ let atlas_sz = u.atlas_info.x;
350
+ if (px.x >= atlas_sz || px.y >= atlas_sz) { return; }
351
+
352
+ let slot_sz = u.atlas_info.y;
353
+ let slots_per_row = u.atlas_info.z;
354
+ let active_count = u.atlas_info.w;
355
+
356
+ let slot_x = px.x / slot_sz;
357
+ let slot_y = px.y / slot_sz;
358
+ let slot_idx = slot_y * slots_per_row + slot_x;
359
+
360
+ if (slot_idx >= active_count) {
361
+ textureStore(radiance_out, vec2<i32>(px), vec4<f32>(0.0));
362
+ return;
363
+ }
364
+
365
+ let uv = (vec2<f32>(px) + vec2<f32>(0.5)) / f32(atlas_sz);
366
+ let albedo = textureSampleLevel(albedo_atlas, atlas_samp, uv, 0.0).rgb;
367
+ let emissive = textureSampleLevel(emissive_atlas, atlas_samp, uv, 0.0).rgb;
368
+
369
+ let slot_m = slot_meta[slot_idx];
370
+ let n_ws = slot_m.normal_ws.xyz;
371
+ let axis = u32(slot_m.normal_ws.w);
372
+
373
+ // Local slot UV (0..1 inside this slot) → object-space card-plane
374
+ // position. Signed-axis-aware u-flip matches the capture pass's
375
+ // projection so the texel we light is the one the capture baked.
376
+ let sx = f32((px.x) % slot_sz);
377
+ let sy = f32((px.y) % slot_sz);
378
+ let sd = f32(slot_sz);
379
+ var u_norm = (sx + 0.5) / sd;
380
+ let v_norm = (sy + 0.5) / sd;
381
+ var pos_os = vec3<f32>(0.0);
382
+ if (axis == 0u || axis == 1u) {
383
+ // Card plane at x = bmax.x (+X) or bmin.x (-X); u=y, v=z.
384
+ if (axis == 1u) { u_norm = 1.0 - u_norm; }
385
+ pos_os.y = mix(slot_m.aabb_min.y, slot_m.aabb_max.y, u_norm);
386
+ pos_os.z = mix(slot_m.aabb_min.z, slot_m.aabb_max.z, v_norm);
387
+ if (axis == 0u) { pos_os.x = slot_m.aabb_max.x; } else { pos_os.x = slot_m.aabb_min.x; }
388
+ } else if (axis == 2u || axis == 3u) {
389
+ if (axis == 3u) { u_norm = 1.0 - u_norm; }
390
+ pos_os.x = mix(slot_m.aabb_min.x, slot_m.aabb_max.x, u_norm);
391
+ pos_os.z = mix(slot_m.aabb_min.z, slot_m.aabb_max.z, v_norm);
392
+ if (axis == 2u) { pos_os.y = slot_m.aabb_max.y; } else { pos_os.y = slot_m.aabb_min.y; }
393
+ } else {
394
+ if (axis == 5u) { u_norm = 1.0 - u_norm; }
395
+ pos_os.x = mix(slot_m.aabb_min.x, slot_m.aabb_max.x, u_norm);
396
+ pos_os.y = mix(slot_m.aabb_min.y, slot_m.aabb_max.y, v_norm);
397
+ if (axis == 4u) { pos_os.z = slot_m.aabb_max.z; } else { pos_os.z = slot_m.aabb_min.z; }
398
+ }
399
+ let pos_ws = (slot_m.transform * vec4<f32>(pos_os, 1.0)).xyz;
400
+
401
+ // Shadow. Cascade selection uses view-space Z against splits.
402
+ var shadow: f32 = 1.0;
403
+ if (u.flags.y > 0.5) {
404
+ let view_z = -(u.view_matrix * vec4<f32>(pos_ws, 1.0)).z;
405
+ var cascade: i32 = 2;
406
+ if (view_z <= u.shadow_splits.x) {
407
+ cascade = 0;
408
+ } else if (view_z <= u.shadow_splits.y) {
409
+ cascade = 1;
410
+ }
411
+ shadow = sample_cascade(cascade, pos_ws, u.flags.x);
412
+ }
413
+
414
+ let ndotl = max(dot(n_ws, u.sun_dir.xyz), 0.0);
415
+ let direct = u.sun_color.xyz * ndotl * shadow;
416
+ let ndotup = max(dot(n_ws, vec3<f32>(0.0, 1.0, 0.0)), 0.0);
417
+ let sky = u.sky_color.xyz * ndotup;
418
+
419
+ let lit = albedo * (direct + sky) + emissive;
420
+ textureStore(radiance_out, vec2<i32>(px), vec4<f32>(lit, 1.0));
421
+ }
422
+ ";
423
+
424
+ /// Ticket 014 V6 — World-Space Radiance Cache bake.
425
+ ///
426
+ /// One workgroup per probe (16×16×16 probes on a 120 m camera-following
427
+ /// cube), 8×8 threads per workgroup = one octel texel each. Output is
428
+ /// the flat rgba16float atlas read by the SDF miss path.
429
+ ///
430
+ /// Per texel: analytic sun term for the octel direction, gated by a
431
+ /// shadow-cascade lookup at the probe's world position (position-
432
+ /// varying occlusion — the crucial signal that makes the cache worth
433
+ /// having; flat analytic would mean every probe holds the same
434
+ /// content). Analytic sky hemisphere for up-biased directions. No
435
+ /// per-direction geometry trace — this is the "distant envelope",
436
+ /// not another SDF march.
437
+ pub(in crate::renderer) const WSRC_BAKE_WGSL: &str = "
438
+ struct WsrcBakeParams {
439
+ sun_dir: vec4<f32>,
440
+ sun_color: vec4<f32>,
441
+ sky_color: vec4<f32>,
442
+ // xyz = cascade origin (world-space cube centre), w = full extent
443
+ grid: vec4<f32>,
444
+ shadow_vps: array<mat4x4<f32>, 3>,
445
+ shadow_splits: vec4<f32>,
446
+ // x = shadow bias, y = shadows-enabled flag, z = cascade index
447
+ // (0..WSRC_CASCADE_COUNT), w unused. Cascade index offsets the
448
+ // output z-slice so this pipeline can be dispatched once per
449
+ // cascade with the same layout.
450
+ flags: vec4<f32>,
451
+ // EN-023 — xyz = scene-average albedo for the ground-bounce term.
452
+ ground_albedo: vec4<f32>,
453
+ };
454
+
455
+ @group(0) @binding(0) var<uniform> u: WsrcBakeParams;
456
+ @group(0) @binding(1) var shadow_atlas_0: texture_depth_2d;
457
+ @group(0) @binding(2) var shadow_atlas_1: texture_depth_2d;
458
+ @group(0) @binding(3) var shadow_atlas_2: texture_depth_2d;
459
+ @group(0) @binding(4) var shadow_samp: sampler_comparison;
460
+ @group(0) @binding(5) var wsrc_out: texture_storage_3d<rgba16float, write>;
461
+
462
+ fn wsrc_sample_cascade(cascade: i32, pos_ws: vec3<f32>, bias: f32) -> f32 {
463
+ var clip: vec4<f32>;
464
+ if (cascade == 0) {
465
+ clip = u.shadow_vps[0] * vec4<f32>(pos_ws, 1.0);
466
+ } else if (cascade == 1) {
467
+ clip = u.shadow_vps[1] * vec4<f32>(pos_ws, 1.0);
468
+ } else {
469
+ clip = u.shadow_vps[2] * vec4<f32>(pos_ws, 1.0);
470
+ }
471
+ let ndc = clip.xyz / clip.w;
472
+ if (ndc.x < -1.0 || ndc.x > 1.0 || ndc.y < -1.0 || ndc.y > 1.0 || ndc.z < 0.0 || ndc.z > 1.0) {
473
+ return 1.0;
474
+ }
475
+ let shadow_uv = vec2<f32>(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
476
+ let ref_depth = ndc.z - bias;
477
+ if (cascade == 0) {
478
+ return textureSampleCompareLevel(shadow_atlas_0, shadow_samp, shadow_uv, ref_depth);
479
+ } else if (cascade == 1) {
480
+ return textureSampleCompareLevel(shadow_atlas_1, shadow_samp, shadow_uv, ref_depth);
481
+ } else {
482
+ return textureSampleCompareLevel(shadow_atlas_2, shadow_samp, shadow_uv, ref_depth);
483
+ }
484
+ }
485
+
486
+ // V10 — workgroup writes the 10×10 padded slab per probe. Thread
487
+ // (lid.x, lid.y) writes texel (wg.*10 + lid) in the atlas; border
488
+ // threads (lid on 0 or 9) shade for the nearest INSIDE octel
489
+ // direction so the sampler's edge-extend behaviour is baked into
490
+ // the data. The 1-texel border is what lets the hardware bilinear
491
+ // sampler do octel smoothing without leaking into adjacent probes.
492
+ const WSRC_OCT_PADDED_SIZE: u32 = 10u;
493
+
494
+ @compute @workgroup_size(10, 10, 1)
495
+ fn cs_main(
496
+ @builtin(workgroup_id) wg: vec3<u32>,
497
+ @builtin(local_invocation_id) lid: vec3<u32>,
498
+ ) {
499
+ let grid_res: u32 = 16u;
500
+ if (wg.x >= grid_res || wg.y >= grid_res || wg.z >= grid_res) { return; }
501
+ if (lid.x >= WSRC_OCT_PADDED_SIZE || lid.y >= WSRC_OCT_PADDED_SIZE) { return; }
502
+
503
+ // Probe world-space centre — cell-centre within the grid cube.
504
+ let extent = u.grid.w;
505
+ let cell = extent / f32(grid_res);
506
+ let probe_pos = u.grid.xyz
507
+ - vec3<f32>(extent * 0.5)
508
+ + (vec3<f32>(f32(wg.x), f32(wg.y), f32(wg.z)) + vec3<f32>(0.5)) * cell;
509
+
510
+ // V11 — map padded octel → real octel with true octahedral
511
+ // silhouette wrap on the 4 edges. Beyond v<0 or v>1 in octel uv
512
+ // space the octahedron folds onto itself with u ↔ 1-u; likewise
513
+ // u<0 or u>1 folds with v ↔ 1-v. Corners (both axes out) keep
514
+ // the V10 edge-extend fill since the double-fold has two valid
515
+ // representations and the exact corner only matters when the
516
+ // sampler bilinear-weights it near zero anyway.
517
+ let px = i32(lid.x);
518
+ let py = i32(lid.y);
519
+ let is_edge_x = px == 0 || px == 9;
520
+ let is_edge_y = py == 0 || py == 9;
521
+ var real_ox: i32;
522
+ var real_oy: i32;
523
+ if (is_edge_x && is_edge_y) {
524
+ // Corner — nearest-inside (edge-extend).
525
+ real_ox = clamp(px - 1, 0, 7);
526
+ real_oy = clamp(py - 1, 0, 7);
527
+ } else if (is_edge_y) {
528
+ // Top/bottom border: mirror x across the edge, same row.
529
+ real_ox = 8 - px;
530
+ real_oy = clamp(py - 1, 0, 7);
531
+ } else if (is_edge_x) {
532
+ // Left/right border: same column, mirror y across the edge.
533
+ real_ox = clamp(px - 1, 0, 7);
534
+ real_oy = 8 - py;
535
+ } else {
536
+ // Interior — direct mapping.
537
+ real_ox = px - 1;
538
+ real_oy = py - 1;
539
+ }
540
+ let dir = octel_direction(vec2<u32>(u32(real_ox), u32(real_oy)));
541
+
542
+ // Shadow at the probe position (cascade 2 — widest, covers the
543
+ // full 120 m cube without per-probe cascade selection).
544
+ var shadow: f32 = 1.0;
545
+ if (u.flags.y > 0.5) {
546
+ shadow = wsrc_sample_cascade(2, probe_pos, u.flags.x);
547
+ }
548
+
549
+ let ndotl = max(dot(dir, u.sun_dir.xyz), 0.0);
550
+ let sun = u.sun_color.xyz * ndotl * shadow;
551
+ let up = clamp(dir.y * 0.5 + 0.5, 0.0, 1.0);
552
+ let sky = u.sky_color.xyz * up * up;
553
+
554
+ // EN-023 — ground bounce for below-horizon octels. This envelope
555
+ // was light-source-only: any miss ray pointing downward returned
556
+ // ~black, so shaded receivers lost the strongest real bounce
557
+ // source (sunlit ground). Approximate it as the scene-average
558
+ // albedo lit by the sun (shadowed at the probe) + half the sky.
559
+ let down = clamp(-dir.y, 0.0, 1.0);
560
+ let ground_irr = u.sun_color.xyz * max(u.sun_dir.y, 0.0) * shadow
561
+ + u.sky_color.xyz * 0.5;
562
+ let ground = u.ground_albedo.xyz * ground_irr * down * down;
563
+
564
+ let radiance = sun + sky + ground;
565
+
566
+ // V13 — cascade index in flags.z offsets the output z-slice.
567
+ let cascade_idx = u32(u.flags.z);
568
+ let tex_coord = vec3<i32>(
569
+ i32(wg.x * WSRC_OCT_PADDED_SIZE + lid.x),
570
+ i32(wg.y * WSRC_OCT_PADDED_SIZE + lid.y),
571
+ i32(cascade_idx * grid_res + wg.z),
572
+ );
573
+ textureStore(wsrc_out, tex_coord, vec4<f32>(radiance, 1.0));
574
+ }
575
+ ";
576
+
577
+ /// Ticket 014 V14 — HW-ray-traced WSRC bake for RT-capable adapters.
578
+ ///
579
+ /// Same probe grid + padded octel layout as the SW bake; the
580
+ /// difference is the radiance computation. For each probe-octel
581
+ /// texel:
582
+ /// 1. Fire a ray from `probe_pos` in `dir` (octel direction) with
583
+ /// `t_max = extent * 0.5` — long enough for a probe to reach
584
+ /// the cascade boundary, short enough that each cascade stays
585
+ /// in its spatial regime.
586
+ /// 2. On hit: sample the Mesh Cards radiance atlas at the hit
587
+ /// point (already pre-lit each frame with sun × shadow +
588
+ /// emissive by `card_light_pass`, so the bake propagates
589
+ /// one-bounce shaded radiance into WSRC — effectively 2-bounce
590
+ /// when the SSGI probe then samples WSRC on miss).
591
+ /// 3. On miss: analytic sun (shadow-sampled at the probe, not
592
+ /// per-direction) + hemisphere sky — same fallback as the V13
593
+ /// SW path, so escaped rays still carry a useful envelope.
594
+ ///
595
+ /// Border texels still use the V11 octahedral wrap — same rule as
596
+ /// the SW bake. The cascade index comes from `flags.z` and offsets
597
+ /// the output z slice so one pipeline covers all 3 cascades via
598
+ /// per-dispatch uniform.
599
+ pub(in crate::renderer) const WSRC_BAKE_HW_WGSL: &str = "
600
+ struct WsrcBakeParams {
601
+ sun_dir: vec4<f32>,
602
+ sun_color: vec4<f32>,
603
+ sky_color: vec4<f32>,
604
+ grid: vec4<f32>,
605
+ shadow_vps: array<mat4x4<f32>, 3>,
606
+ shadow_splits: vec4<f32>,
607
+ flags: vec4<f32>,
608
+ // EN-023 — layout mirror; the HW bake traces real geometry and
609
+ // ignores the scene-average ground albedo.
610
+ ground_albedo: vec4<f32>,
611
+ };
612
+
613
+ struct HwBakeInstanceGiData {
614
+ albedo: vec3<f32>,
615
+ emissive_luma: f32,
616
+ normal_ws: vec3<f32>,
617
+ _pad0: f32,
618
+ card_slot: vec4<f32>,
619
+ card_aabb_min: vec4<f32>,
620
+ card_aabb_max: vec4<f32>,
621
+ // EN-023 — world-space AABB (SDF path only; layout mirror).
622
+ world_aabb_min: vec4<f32>,
623
+ world_aabb_max: vec4<f32>,
624
+ // PT-2 — layout mirror only; the WSRC bake ignores both fields.
625
+ geo: vec4<u32>,
626
+ mat_params: vec4<f32>,
627
+ };
628
+
629
+ const HW_BAKE_CARD_SLOTS_PER_ROW: f32 = 64.0;
630
+ const HW_BAKE_OCT_PADDED: u32 = 10u;
631
+
632
+ @group(0) @binding(0) var<uniform> u: WsrcBakeParams;
633
+ @group(0) @binding(1) var shadow_atlas_0: texture_depth_2d;
634
+ @group(0) @binding(2) var shadow_atlas_1: texture_depth_2d;
635
+ @group(0) @binding(3) var shadow_atlas_2: texture_depth_2d;
636
+ @group(0) @binding(4) var shadow_samp: sampler_comparison;
637
+ @group(0) @binding(5) var wsrc_out: texture_storage_3d<rgba16float, write>;
638
+ @group(0) @binding(6) var accel: acceleration_structure;
639
+ @group(0) @binding(7) var<storage, read> instance_data: array<HwBakeInstanceGiData>;
640
+ @group(0) @binding(8) var card_atlas: texture_2d<f32>;
641
+ @group(0) @binding(9) var card_samp: sampler;
642
+
643
+ fn hw_bake_sample_cascade(cascade: i32, pos_ws: vec3<f32>, bias: f32) -> f32 {
644
+ var clip: vec4<f32>;
645
+ if (cascade == 0) { clip = u.shadow_vps[0] * vec4<f32>(pos_ws, 1.0); }
646
+ else if (cascade == 1) { clip = u.shadow_vps[1] * vec4<f32>(pos_ws, 1.0); }
647
+ else { clip = u.shadow_vps[2] * vec4<f32>(pos_ws, 1.0); }
648
+ let ndc = clip.xyz / clip.w;
649
+ if (ndc.x < -1.0 || ndc.x > 1.0 || ndc.y < -1.0 || ndc.y > 1.0 || ndc.z < 0.0 || ndc.z > 1.0) {
650
+ return 1.0;
651
+ }
652
+ let shadow_uv = vec2<f32>(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
653
+ let ref_depth = ndc.z - bias;
654
+ if (cascade == 0) { return textureSampleCompareLevel(shadow_atlas_0, shadow_samp, shadow_uv, ref_depth); }
655
+ else if (cascade == 1) { return textureSampleCompareLevel(shadow_atlas_1, shadow_samp, shadow_uv, ref_depth); }
656
+ else { return textureSampleCompareLevel(shadow_atlas_2, shadow_samp, shadow_uv, ref_depth); }
657
+ }
658
+
659
+ @compute @workgroup_size(10, 10, 1)
660
+ fn cs_main(
661
+ @builtin(workgroup_id) wg: vec3<u32>,
662
+ @builtin(local_invocation_id) lid: vec3<u32>,
663
+ ) {
664
+ let grid_res: u32 = 16u;
665
+ if (wg.x >= grid_res || wg.y >= grid_res || wg.z >= grid_res) { return; }
666
+ if (lid.x >= HW_BAKE_OCT_PADDED || lid.y >= HW_BAKE_OCT_PADDED) { return; }
667
+
668
+ let extent = u.grid.w;
669
+ let cell = extent / f32(grid_res);
670
+ let probe_pos = u.grid.xyz
671
+ - vec3<f32>(extent * 0.5)
672
+ + (vec3<f32>(f32(wg.x), f32(wg.y), f32(wg.z)) + vec3<f32>(0.5)) * cell;
673
+
674
+ // V11 octahedral wrap for the padded borders.
675
+ let px = i32(lid.x);
676
+ let py = i32(lid.y);
677
+ let is_edge_x = px == 0 || px == 9;
678
+ let is_edge_y = py == 0 || py == 9;
679
+ var real_ox: i32;
680
+ var real_oy: i32;
681
+ if (is_edge_x && is_edge_y) {
682
+ real_ox = clamp(px - 1, 0, 7);
683
+ real_oy = clamp(py - 1, 0, 7);
684
+ } else if (is_edge_y) {
685
+ real_ox = 8 - px;
686
+ real_oy = clamp(py - 1, 0, 7);
687
+ } else if (is_edge_x) {
688
+ real_ox = clamp(px - 1, 0, 7);
689
+ real_oy = 8 - py;
690
+ } else {
691
+ real_ox = px - 1;
692
+ real_oy = py - 1;
693
+ }
694
+ let dir = octel_direction(vec2<u32>(u32(real_ox), u32(real_oy)));
695
+
696
+ // V14 — fire a short ray from the probe centre. Ray length
697
+ // scales with the cascade extent so each cascade's rays stay
698
+ // in its resolution regime (near: ~15 m, mid: ~60 m, far:
699
+ // ~250 m).
700
+ let ray_length = extent * 0.5;
701
+ var rq: ray_query;
702
+ rayQueryInitialize(&rq, accel, RayDesc(
703
+ 0u,
704
+ 0xFFu,
705
+ 0.01,
706
+ ray_length,
707
+ probe_pos,
708
+ dir,
709
+ ));
710
+ loop {
711
+ if (!rayQueryProceed(&rq)) { break; }
712
+ }
713
+ let hit = rayQueryGetCommittedIntersection(&rq);
714
+
715
+ var radiance = vec3<f32>(0.0);
716
+ if (hit.kind != RAY_QUERY_INTERSECTION_NONE) {
717
+ let inst = instance_data[hit.instance_custom_data];
718
+ if (inst.card_slot.w > 0.5) {
719
+ // Sample Mesh Cards pre-lit radiance at hit. Same
720
+ // projection math as SSGI_PROBE_TRACE_HW_WGSL's hit
721
+ // branch.
722
+ let hit_world = probe_pos + dir * hit.t;
723
+ let hit_os = (hit.world_to_object * vec4<f32>(hit_world, 1.0)).xyz;
724
+ let abs_d = abs(dir);
725
+ var axis_idx: u32 = 0u;
726
+ if (abs_d.y >= abs_d.x && abs_d.y >= abs_d.z) {
727
+ axis_idx = 2u;
728
+ } else if (abs_d.z >= abs_d.x) {
729
+ axis_idx = 4u;
730
+ }
731
+ var signed_axis: u32 = axis_idx;
732
+ if (axis_idx == 0u && dir.x > 0.0) { signed_axis = 1u; }
733
+ else if (axis_idx == 2u && dir.y > 0.0) { signed_axis = 3u; }
734
+ else if (axis_idx == 4u && dir.z > 0.0) { signed_axis = 5u; }
735
+
736
+ let first_slot = u32(inst.card_slot.x);
737
+ let slot = first_slot + signed_axis;
738
+ let slot_x = slot % 64u;
739
+ let slot_y = slot / 64u;
740
+
741
+ let bmin = inst.card_aabb_min.xyz;
742
+ let bmax = inst.card_aabb_max.xyz;
743
+ var u_os: f32;
744
+ var v_os: f32;
745
+ var u_lo: f32; var u_hi: f32;
746
+ var v_lo: f32; var v_hi: f32;
747
+ var u_flip: f32 = 1.0;
748
+ if (signed_axis == 0u || signed_axis == 1u) {
749
+ u_os = hit_os.y; v_os = hit_os.z;
750
+ u_lo = bmin.y; u_hi = bmax.y; v_lo = bmin.z; v_hi = bmax.z;
751
+ if (signed_axis == 1u) { u_flip = -1.0; }
752
+ } else if (signed_axis == 2u || signed_axis == 3u) {
753
+ u_os = hit_os.x; v_os = hit_os.z;
754
+ u_lo = bmin.x; u_hi = bmax.x; v_lo = bmin.z; v_hi = bmax.z;
755
+ if (signed_axis == 3u) { u_flip = -1.0; }
756
+ } else {
757
+ u_os = hit_os.x; v_os = hit_os.y;
758
+ u_lo = bmin.x; u_hi = bmax.x; v_lo = bmin.y; v_hi = bmax.y;
759
+ if (signed_axis == 5u) { u_flip = -1.0; }
760
+ }
761
+ var u_norm = clamp((u_os - u_lo) / max(u_hi - u_lo, 1e-4), 0.0, 1.0);
762
+ let v_norm = clamp((v_os - v_lo) / max(v_hi - v_lo, 1e-4), 0.0, 1.0);
763
+ if (u_flip < 0.0) { u_norm = 1.0 - u_norm; }
764
+
765
+ let slot_size_uv = 1.0 / HW_BAKE_CARD_SLOTS_PER_ROW;
766
+ let texel_in_slot = slot_size_uv / f32(64);
767
+ let slot_u0 = f32(slot_x) * slot_size_uv + texel_in_slot;
768
+ let slot_v0 = f32(slot_y) * slot_size_uv + texel_in_slot;
769
+ let slot_span = slot_size_uv - 2.0 * texel_in_slot;
770
+ let atlas_uv = vec2<f32>(
771
+ slot_u0 + u_norm * slot_span,
772
+ slot_v0 + v_norm * slot_span,
773
+ );
774
+ radiance = textureSampleLevel(card_atlas, card_samp, atlas_uv, 0.0).rgb;
775
+ } else {
776
+ // Instance without a card — shade analytically using
777
+ // its flat normal + albedo.
778
+ let hit_n = inst.normal_ws;
779
+ let ndotl = max(dot(hit_n, u.sun_dir.xyz), 0.0);
780
+ let direct = u.sun_color.xyz * ndotl;
781
+ let ndotup = max(dot(hit_n, vec3<f32>(0.0, 1.0, 0.0)), 0.0);
782
+ let sky = u.sky_color.xyz * ndotup;
783
+ radiance = inst.albedo * (direct + sky)
784
+ + inst.albedo * inst.emissive_luma;
785
+ }
786
+ } else {
787
+ // Miss — V13 analytic fallback (shadow-sampled sun +
788
+ // hemisphere sky). The ray went past `extent * 0.5` without
789
+ // hitting anything, so this is the open-sky direction at
790
+ // probe scale.
791
+ var shadow: f32 = 1.0;
792
+ if (u.flags.y > 0.5) {
793
+ shadow = hw_bake_sample_cascade(2, probe_pos, u.flags.x);
794
+ }
795
+ let ndotl = max(dot(dir, u.sun_dir.xyz), 0.0);
796
+ let sun = u.sun_color.xyz * ndotl * shadow;
797
+ let up = clamp(dir.y * 0.5 + 0.5, 0.0, 1.0);
798
+ let sky = u.sky_color.xyz * up * up;
799
+ radiance = sun + sky;
800
+ }
801
+
802
+ let cascade_idx = u32(u.flags.z);
803
+ let tex_coord = vec3<i32>(
804
+ i32(wg.x * HW_BAKE_OCT_PADDED + lid.x),
805
+ i32(wg.y * HW_BAKE_OCT_PADDED + lid.y),
806
+ i32(cascade_idx * grid_res + wg.z),
807
+ );
808
+ textureStore(wsrc_out, tex_coord, vec4<f32>(radiance, 1.0));
809
+ }
810
+ ";