@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,421 @@
1
+ //! Froxel light clustering — task #23 of the architecture audit.
2
+ //!
3
+ //! The 8+256 light-cap raise removed the capability ceiling but left
4
+ //! the scene shader paying O(live point lights) per fragment. This
5
+ //! module restores O(cluster lights): a compute pass assigns the point
6
+ //! lights (read from the same lighting UBO the shaders already use) to
7
+ //! a 16×9×24 view-frustum froxel grid each frame, and a clustered
8
+ //! variant of the scene shader loops only its froxel's index list.
9
+ //!
10
+ //! Backend split, by capability rather than cfg: storage buffers in
11
+ //! fragment shaders don't exist on WebGL2, so [`FroxelPass::supported`]
12
+ //! gates on the device limits. Unsupported backends keep the plain
13
+ //! count-driven loop (the semantic reference — the clustered path must
14
+ //! match it exactly, which the many_point_lights golden enforces).
15
+ //!
16
+ //! Memory: counts 3456×4 B ≈ 14 KB; index list 3456×256×4 B ≈ 3.5 MB
17
+ //! (256 = worst-case every light in one froxel — exact parity with the
18
+ //! reference loop, no truncation).
19
+
20
+ use wgpu::util::DeviceExt;
21
+
22
+ pub(super) const GRID_X: u32 = 16;
23
+ pub(super) const GRID_Y: u32 = 9;
24
+ pub(super) const GRID_Z: u32 = 24;
25
+ pub(super) const CLUSTER_COUNT: u32 = GRID_X * GRID_Y * GRID_Z;
26
+ pub(super) const MAX_LIGHTS_PER_CLUSTER: u32 = 256;
27
+
28
+ /// Uniform parameters shared by the assignment compute pass and the
29
+ /// clustered fragment loop. Layout mirrored in WGSL below and in the
30
+ /// fragment include.
31
+ #[repr(C)]
32
+ #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
33
+ pub(super) struct FroxelParams {
34
+ /// View matrix (world → view) for light-position transform.
35
+ pub view: [[f32; 4]; 4],
36
+ /// x = grid_x, y = grid_y, z = grid_z, w = live point-light count.
37
+ pub grid: [u32; 4],
38
+ /// x = znear, y = zfar, z = log(zfar/znear), w = unused.
39
+ pub depth_range: [f32; 4],
40
+ /// x = 1/tile_w_px, y = 1/tile_h_px (fragment tile lookup),
41
+ /// z = p22, w = p32 (depth linearization, same convention as Hi-Z).
42
+ pub screen: [f32; 4],
43
+ /// Inverse projection — froxel corner reconstruction in the
44
+ /// assignment pass.
45
+ pub inv_proj: [[f32; 4]; 4],
46
+ }
47
+
48
+ const ASSIGN_SHADER: &str = "
49
+ struct FroxelParams {
50
+ view: mat4x4<f32>,
51
+ grid: vec4<u32>,
52
+ depth_range: vec4<f32>,
53
+ screen: vec4<f32>,
54
+ inv_proj: mat4x4<f32>,
55
+ };
56
+ struct PointLight { position: vec4<f32>, color: vec4<f32> };
57
+ struct Lights {
58
+ // Mirrors the tail of the Lighting UBO relevant here. The host
59
+ // binds a dedicated compact UBO (positions+ranges only would do,
60
+ // but reusing PointLight keeps one struct).
61
+ count: vec4<f32>,
62
+ lights: array<PointLight, 256>,
63
+ };
64
+
65
+ @group(0) @binding(0) var<uniform> p: FroxelParams;
66
+ @group(0) @binding(1) var<uniform> l: Lights;
67
+ @group(0) @binding(2) var<storage, read_write> cluster_counts: array<u32>;
68
+ @group(0) @binding(3) var<storage, read_write> cluster_indices: array<u32>;
69
+
70
+ // View-space Z of slice boundary k (logarithmic distribution).
71
+ fn slice_z(k: u32) -> f32 {
72
+ let t = f32(k) / f32(p.grid.z);
73
+ return p.depth_range.x * exp(t * p.depth_range.z);
74
+ }
75
+
76
+ @compute @workgroup_size(4, 4, 4)
77
+ fn cs_main(@builtin(global_invocation_id) gid: vec3<u32>) {
78
+ if (gid.x >= p.grid.x || gid.y >= p.grid.y || gid.z >= p.grid.z) { return; }
79
+ let cluster = gid.x + gid.y * p.grid.x + gid.z * p.grid.x * p.grid.y;
80
+
81
+ // Froxel AABB in view space: reconstruct the tile's corner rays on
82
+ // the near plane of the projection and scale to the slice depths.
83
+ // NDC tile extents:
84
+ let x0 = (f32(gid.x) / f32(p.grid.x)) * 2.0 - 1.0;
85
+ let x1 = (f32(gid.x + 1u) / f32(p.grid.x)) * 2.0 - 1.0;
86
+ // NDC y is up; tile row 0 is the TOP of the screen.
87
+ let y1 = 1.0 - (f32(gid.y) / f32(p.grid.y)) * 2.0;
88
+ let y0 = 1.0 - (f32(gid.y + 1u) / f32(p.grid.y)) * 2.0;
89
+
90
+ // Unproject the four corners at an arbitrary depth and normalize to
91
+ // rays through the camera (view space, looking down -Z).
92
+ var mn = vec3<f32>( 1e30, 1e30, 1e30);
93
+ var mx = vec3<f32>(-1e30, -1e30, -1e30);
94
+ let z_near_s = slice_z(gid.z);
95
+ let z_far_s = slice_z(gid.z + 1u);
96
+ for (var cx = 0u; cx < 2u; cx++) {
97
+ for (var cy = 0u; cy < 2u; cy++) {
98
+ let nx = select(x0, x1, cx == 1u);
99
+ let ny = select(y0, y1, cy == 1u);
100
+ let h = p.inv_proj * vec4<f32>(nx, ny, 0.5, 1.0);
101
+ let dir = h.xyz / h.w; // a point on the ray (view space)
102
+ let ray = dir / max(-dir.z, 1e-6); // scale so z == -1
103
+ // corner at both slice depths (view z is negative forward)
104
+ let a = ray * z_near_s;
105
+ let b = ray * z_far_s;
106
+ mn = min(mn, min(vec3<f32>(a.xy, -z_near_s), vec3<f32>(b.xy, -z_far_s)));
107
+ mx = max(mx, max(vec3<f32>(a.xy, -z_near_s), vec3<f32>(b.xy, -z_far_s)));
108
+ }
109
+ }
110
+
111
+ // Sphere/AABB tests against every live light.
112
+ var count = 0u;
113
+ let n = u32(l.count.x);
114
+ let base = cluster * 256u;
115
+ for (var i = 0u; i < n; i++) {
116
+ let pos_w = l.lights[i].position;
117
+ let pos_v = (p.view * vec4<f32>(pos_w.xyz, 1.0)).xyz;
118
+ let r = pos_w.w;
119
+ let closest = clamp(pos_v, mn, mx);
120
+ let d = pos_v - closest;
121
+ if (dot(d, d) <= r * r) {
122
+ cluster_indices[base + count] = i;
123
+ count++;
124
+ }
125
+ }
126
+ cluster_counts[cluster] = count;
127
+ }
128
+ ";
129
+
130
+ /// The fragment-side replacement for the plain point-light loop, plus
131
+ /// the bindings it needs. Spliced into SCENE_SHADER between the
132
+ /// BEGIN/END-POINT-LIGHT-LOOP markers by [`clustered_scene_shader`].
133
+ const CLUSTERED_BINDINGS: &str = "
134
+ struct FroxelParams {
135
+ view: mat4x4<f32>,
136
+ grid: vec4<u32>,
137
+ depth_range: vec4<f32>,
138
+ screen: vec4<f32>,
139
+ inv_proj: mat4x4<f32>,
140
+ };
141
+ @group(1) @binding(10) var<uniform> froxel: FroxelParams;
142
+ @group(1) @binding(11) var<storage, read> cluster_counts: array<u32>;
143
+ @group(1) @binding(12) var<storage, read> cluster_indices: array<u32>;
144
+ ";
145
+
146
+ const CLUSTERED_LOOP: &str = "
147
+ // Froxel-clustered point lights: identical shading math to the
148
+ // reference loop, restricted to this fragment's cluster list.
149
+ let view_z = -froxel.screen.w / (in.clip_position.z + froxel.screen.z);
150
+ let slice = clamp(
151
+ u32(log(max(view_z, froxel.depth_range.x) / froxel.depth_range.x)
152
+ / froxel.depth_range.z * f32(froxel.grid.z)),
153
+ 0u, froxel.grid.z - 1u);
154
+ let tile_x = min(u32(in.clip_position.x * froxel.screen.x), froxel.grid.x - 1u);
155
+ let tile_y = min(u32(in.clip_position.y * froxel.screen.y), froxel.grid.y - 1u);
156
+ let cluster = tile_x + tile_y * froxel.grid.x + slice * froxel.grid.x * froxel.grid.y;
157
+ let cl_count = cluster_counts[cluster];
158
+ let cl_base = cluster * 256u;
159
+ for (var ci = 0u; ci < cl_count; ci++) {
160
+ let pl = lighting.point_lights[cluster_indices[cl_base + ci]];
161
+ let to_light = pl.position.xyz - in.world_pos;
162
+ let dist = length(to_light);
163
+ let range = pl.position.w;
164
+ if (dist < range && dist > 0.0) {
165
+ let l = to_light / dist;
166
+ let atten = 1.0 - (dist / range);
167
+ let atten2 = atten * atten;
168
+ lit += shade_pbr(n, v, l, pl.color.rgb, pl.color.w * atten2,
169
+ base_color, metallic, roughness);
170
+ }
171
+ }
172
+ ";
173
+
174
+ /// Build the clustered SCENE_SHADER variant from the canonical source.
175
+ pub(super) fn clustered_scene_shader(source: &str) -> String {
176
+ let begin = source
177
+ .find("// BEGIN-POINT-LIGHT-LOOP")
178
+ .expect("scene shader missing BEGIN-POINT-LIGHT-LOOP marker");
179
+ let end_marker = "// END-POINT-LIGHT-LOOP";
180
+ let end = source.find(end_marker).expect("scene shader missing END marker") + end_marker.len();
181
+ format!(
182
+ "{}{}{}{}",
183
+ CLUSTERED_BINDINGS,
184
+ &source[..begin],
185
+ CLUSTERED_LOOP,
186
+ &source[end..]
187
+ )
188
+ }
189
+
190
+ /// The three entries appended to `lighting_layout` (group 1) when the
191
+ /// device supports the clustered path. Pipelines whose shaders don't
192
+ /// reference them (SHADER_3D's pipeline_3d) are unaffected — extra
193
+ /// layout entries are legal as long as the bind group provides them.
194
+ pub(super) fn extra_lighting_layout_entries() -> [wgpu::BindGroupLayoutEntry; 3] {
195
+ let storage_ro = wgpu::BindingType::Buffer {
196
+ ty: wgpu::BufferBindingType::Storage { read_only: true },
197
+ has_dynamic_offset: false,
198
+ min_binding_size: None,
199
+ };
200
+ [
201
+ wgpu::BindGroupLayoutEntry {
202
+ binding: 10,
203
+ visibility: wgpu::ShaderStages::FRAGMENT,
204
+ ty: wgpu::BindingType::Buffer {
205
+ ty: wgpu::BufferBindingType::Uniform,
206
+ has_dynamic_offset: false,
207
+ min_binding_size: None,
208
+ },
209
+ count: None,
210
+ },
211
+ wgpu::BindGroupLayoutEntry { binding: 11, visibility: wgpu::ShaderStages::FRAGMENT, ty: storage_ro, count: None },
212
+ wgpu::BindGroupLayoutEntry { binding: 12, visibility: wgpu::ShaderStages::FRAGMENT, ty: storage_ro, count: None },
213
+ ]
214
+ }
215
+
216
+ pub struct FroxelPass {
217
+ pub assign_pipeline: wgpu::ComputePipeline,
218
+ pub assign_layout: wgpu::BindGroupLayout,
219
+ pub params_buffer: wgpu::Buffer,
220
+ /// Compact point-light UBO for the compute pass (count + 256 lights).
221
+ pub lights_buffer: wgpu::Buffer,
222
+ pub counts_buffer: wgpu::Buffer,
223
+ pub indices_buffer: wgpu::Buffer,
224
+ assign_bg: wgpu::BindGroup,
225
+ }
226
+
227
+ impl FroxelPass {
228
+ /// Storage buffers must be available in BOTH compute and fragment
229
+ /// stages (WebGL2 has neither). `BLOOM_DISABLE_FROXEL=1` forces the
230
+ /// reference loop — used to (re)generate the clustered-parity
231
+ /// golden and to bisect suspected clustering bugs in the field.
232
+ pub fn supported(device: &wgpu::Device) -> bool {
233
+ if std::env::var_os("BLOOM_DISABLE_FROXEL").is_some_and(|v| v == "1") {
234
+ return false;
235
+ }
236
+ let l = device.limits();
237
+ l.max_storage_buffers_per_shader_stage >= 2
238
+ && l.max_storage_buffer_binding_size as u64
239
+ >= (CLUSTER_COUNT * MAX_LIGHTS_PER_CLUSTER * 4) as u64
240
+ }
241
+
242
+ pub fn new(device: &wgpu::Device) -> Self {
243
+ let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
244
+ label: Some("froxel_assign_shader"),
245
+ source: wgpu::ShaderSource::Wgsl(ASSIGN_SHADER.into()),
246
+ });
247
+ let assign_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
248
+ label: Some("froxel_assign_layout"),
249
+ entries: &[
250
+ wgpu::BindGroupLayoutEntry {
251
+ binding: 0,
252
+ visibility: wgpu::ShaderStages::COMPUTE,
253
+ ty: wgpu::BindingType::Buffer {
254
+ ty: wgpu::BufferBindingType::Uniform,
255
+ has_dynamic_offset: false,
256
+ min_binding_size: None,
257
+ },
258
+ count: None,
259
+ },
260
+ wgpu::BindGroupLayoutEntry {
261
+ binding: 1,
262
+ visibility: wgpu::ShaderStages::COMPUTE,
263
+ ty: wgpu::BindingType::Buffer {
264
+ ty: wgpu::BufferBindingType::Uniform,
265
+ has_dynamic_offset: false,
266
+ min_binding_size: None,
267
+ },
268
+ count: None,
269
+ },
270
+ wgpu::BindGroupLayoutEntry {
271
+ binding: 2,
272
+ visibility: wgpu::ShaderStages::COMPUTE,
273
+ ty: wgpu::BindingType::Buffer {
274
+ ty: wgpu::BufferBindingType::Storage { read_only: false },
275
+ has_dynamic_offset: false,
276
+ min_binding_size: None,
277
+ },
278
+ count: None,
279
+ },
280
+ wgpu::BindGroupLayoutEntry {
281
+ binding: 3,
282
+ visibility: wgpu::ShaderStages::COMPUTE,
283
+ ty: wgpu::BindingType::Buffer {
284
+ ty: wgpu::BufferBindingType::Storage { read_only: false },
285
+ has_dynamic_offset: false,
286
+ min_binding_size: None,
287
+ },
288
+ count: None,
289
+ },
290
+ ],
291
+ });
292
+ let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
293
+ label: Some("froxel_assign_pl"),
294
+ bind_group_layouts: &[Some(&assign_layout)],
295
+ ..Default::default()
296
+ });
297
+ let assign_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
298
+ label: Some("froxel_assign_pipeline"),
299
+ layout: Some(&pl),
300
+ module: &shader,
301
+ entry_point: Some("cs_main"),
302
+ compilation_options: Default::default(),
303
+ cache: None,
304
+ });
305
+ let params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
306
+ label: Some("froxel_params"),
307
+ contents: &[0u8; std::mem::size_of::<FroxelParams>()],
308
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
309
+ });
310
+ // count vec4 + 256 lights × 2 vec4
311
+ let lights_buffer = device.create_buffer(&wgpu::BufferDescriptor {
312
+ label: Some("froxel_lights"),
313
+ size: 16 + 256 * 32,
314
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
315
+ mapped_at_creation: false,
316
+ });
317
+ let counts_buffer = device.create_buffer(&wgpu::BufferDescriptor {
318
+ label: Some("froxel_counts"),
319
+ size: (CLUSTER_COUNT * 4) as u64,
320
+ usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
321
+ mapped_at_creation: false,
322
+ });
323
+ let indices_buffer = device.create_buffer(&wgpu::BufferDescriptor {
324
+ label: Some("froxel_indices"),
325
+ size: (CLUSTER_COUNT * MAX_LIGHTS_PER_CLUSTER * 4) as u64,
326
+ usage: wgpu::BufferUsages::STORAGE,
327
+ mapped_at_creation: false,
328
+ });
329
+ let assign_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
330
+ label: Some("froxel_assign_bg"),
331
+ layout: &assign_layout,
332
+ entries: &[
333
+ wgpu::BindGroupEntry { binding: 0, resource: params_buffer.as_entire_binding() },
334
+ wgpu::BindGroupEntry { binding: 1, resource: lights_buffer.as_entire_binding() },
335
+ wgpu::BindGroupEntry { binding: 2, resource: counts_buffer.as_entire_binding() },
336
+ wgpu::BindGroupEntry { binding: 3, resource: indices_buffer.as_entire_binding() },
337
+ ],
338
+ });
339
+ Self {
340
+ assign_pipeline,
341
+ assign_layout,
342
+ params_buffer,
343
+ lights_buffer,
344
+ counts_buffer,
345
+ indices_buffer,
346
+ assign_bg,
347
+ }
348
+ }
349
+
350
+ /// The bind-group entries matching [`extra_lighting_layout_entries`],
351
+ /// appended to every lighting bind group the renderer builds.
352
+ pub(super) fn extra_lighting_bind_entries(&self) -> [wgpu::BindGroupEntry<'_>; 3] {
353
+ [
354
+ wgpu::BindGroupEntry { binding: 10, resource: self.params_buffer.as_entire_binding() },
355
+ wgpu::BindGroupEntry { binding: 11, resource: self.counts_buffer.as_entire_binding() },
356
+ wgpu::BindGroupEntry { binding: 12, resource: self.indices_buffer.as_entire_binding() },
357
+ ]
358
+ }
359
+
360
+ /// Record the per-frame assignment dispatch. The caller uploads
361
+ /// params + lights first (see Renderer::record_froxel_assign).
362
+ pub fn record(&self, encoder: &mut wgpu::CommandEncoder) {
363
+ let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
364
+ label: Some("froxel_assign_pass"),
365
+ timestamp_writes: None,
366
+ });
367
+ pass.set_pipeline(&self.assign_pipeline);
368
+ pass.set_bind_group(0, &self.assign_bg, &[]);
369
+ pass.dispatch_workgroups(GRID_X / 4, GRID_Y.div_ceil(4), GRID_Z / 4);
370
+ }
371
+ }
372
+
373
+ impl super::Renderer {
374
+ /// Upload froxel params + the compact light list and dispatch the
375
+ /// assignment pass. Runs every 3D frame on supported devices —
376
+ /// even with zero lights, so `cluster_counts` never carries stale
377
+ /// data from a previous frame's camera.
378
+ pub(super) fn record_froxel_assign(&mut self, encoder: &mut wgpu::CommandEncoder) {
379
+ let Some(froxel) = &self.froxel else { return };
380
+
381
+ let proj = self.current_proj_matrix;
382
+ let p22 = proj[2][2];
383
+ let p32 = proj[3][2];
384
+ // Same linearization as Hi-Z: view_z(depth) = -p32/(depth + p22),
385
+ // positive forward. Evaluate at depth 0 and 1; min/max makes
386
+ // this hold for reversed-Z too, and the clamps keep an
387
+ // infinite-far projection (division by ~0) finite.
388
+ let z_at = |d: f32| -p32 / (d + p22);
389
+ let (z0, z1) = (z_at(0.0), z_at(1.0));
390
+ let znear = z0.min(z1).max(1e-3);
391
+ let zfar = z0.max(z1).clamp(znear * 1.001, 1e9);
392
+
393
+ // clip_position.xy is in render-target pixels — the HDR scene
394
+ // pass runs at render_extent (render_scale-aware), not surface
395
+ // size.
396
+ let (rw, rh) = self.render_extent();
397
+ let n = (self.lighting_uniforms.point_light_count[0] as u32)
398
+ .min(MAX_LIGHTS_PER_CLUSTER);
399
+ let params = FroxelParams {
400
+ view: self.current_view_matrix,
401
+ grid: [GRID_X, GRID_Y, GRID_Z, n],
402
+ depth_range: [znear, zfar, (zfar / znear).ln(), 0.0],
403
+ screen: [
404
+ GRID_X as f32 / rw.max(1) as f32,
405
+ GRID_Y as f32 / rh.max(1) as f32,
406
+ p22,
407
+ p32,
408
+ ],
409
+ inv_proj: self.current_inv_proj_matrix,
410
+ };
411
+ self.queue.write_buffer(&froxel.params_buffer, 0, bytemuck::bytes_of(&params));
412
+ let count = [n as f32, 0.0, 0.0, 0.0_f32];
413
+ self.queue.write_buffer(&froxel.lights_buffer, 0, bytemuck::bytes_of(&count));
414
+ self.queue.write_buffer(
415
+ &froxel.lights_buffer,
416
+ 16,
417
+ bytemuck::cast_slice(&self.lighting_uniforms.point_lights),
418
+ );
419
+ froxel.record(encoder);
420
+ }
421
+ }