@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,641 @@
1
+ //! Model draw submission: cached-model draw commands and the
2
+ //! immediate-mode mesh path (incl. GPU-skinning joint-pose plumbing).
3
+ //! Split from renderer/mod.rs (2000-line file policy — same pattern as
4
+ //! shadow_pass.rs).
5
+
6
+ use super::*;
7
+
8
+ impl Renderer {
9
+ /// Record a cached model draw command. The actual rendering happens in end_frame().
10
+ pub fn draw_model_cached(&mut self, handle_bits: u64, position: [f32; 3], scale: f32, tint: [f32; 4]) {
11
+ let mesh_count = match self.model_gpu_cache.get(&handle_bits) {
12
+ Some(Some(meshes)) => meshes.len(),
13
+ _ => return,
14
+ };
15
+ // Foliage wind amount for this model (0 = not a plant). Rides in misc.z.
16
+ let foliage = self.foliage_wind.get(&handle_bits).copied().unwrap_or(0.0);
17
+
18
+ for mesh_idx in 0..mesh_count {
19
+ let slot = self.next_model_uniform_slot;
20
+ self.next_model_uniform_slot += 1;
21
+
22
+ // Grow uniform pool if needed
23
+ self.ensure_model_uniform_slot(slot);
24
+
25
+ // Compute model MVP: VP * translate(position) * scale(s)
26
+ let model_matrix = mat4_multiply(
27
+ mat4_translate(IDENTITY_MAT4, position),
28
+ mat4_scale(IDENTITY_MAT4, [scale, scale, scale]),
29
+ );
30
+ let model_mvp = mat4_multiply(self.current_vp_matrix, model_matrix);
31
+
32
+ // Stage uniform for this draw (flushed in one write at end-frame)
33
+ self.stage_model_uniform(slot, &Uniforms3D {
34
+ mvp: model_mvp, model: model_matrix,
35
+ prev_mvp: model_mvp, model_tint: tint,
36
+ misc: [0.0, 0.0, foliage, 0.0],
37
+ });
38
+
39
+ self.model_draw_commands.push(CachedModelDraw {
40
+ uniform_slot: slot,
41
+ cache_handle: handle_bits,
42
+ mesh_idx,
43
+ model: model_matrix,
44
+ skinned: false,
45
+ joint_offset: 0.0,
46
+ bounds_override: None,
47
+ });
48
+ }
49
+ }
50
+
51
+ /// `draw_model_cached` with a Y-axis rotation folded into the model
52
+ /// matrix (translate ∘ rotY ∘ scale). Backs `bloom_draw_model_rotated`
53
+ /// for static models — previously that FFI only had the immediate-mode
54
+ /// vertex path, which bypasses the scene pipeline entirely (no alpha
55
+ /// cutout, no normal/MR maps, no foliage wind or transmission, no
56
+ /// cutout shadows) and re-transforms every vertex on the CPU each
57
+ /// frame. Alpha-cutout foliage drawn through it rendered its cards'
58
+ /// transparent texels as opaque.
59
+ pub fn draw_model_cached_rotated(
60
+ &mut self,
61
+ handle_bits: u64,
62
+ position: [f32; 3],
63
+ scale: f32,
64
+ rot_y: f32,
65
+ tint: [f32; 4],
66
+ ) {
67
+ let mesh_count = match self.model_gpu_cache.get(&handle_bits) {
68
+ Some(Some(meshes)) => meshes.len(),
69
+ _ => return,
70
+ };
71
+
72
+ let foliage = self.foliage_wind.get(&handle_bits).copied().unwrap_or(0.0);
73
+ let (s, c) = rot_y.sin_cos();
74
+ // Column-major rotY (matches mat4_translate / mat4_scale layout).
75
+ let rot: [[f32; 4]; 4] = [
76
+ [c, 0.0, -s, 0.0],
77
+ [0.0, 1.0, 0.0, 0.0],
78
+ [s, 0.0, c, 0.0],
79
+ [0.0, 0.0, 0.0, 1.0],
80
+ ];
81
+ let model_matrix = mat4_multiply(
82
+ mat4_translate(IDENTITY_MAT4, position),
83
+ mat4_multiply(rot, mat4_scale(IDENTITY_MAT4, [scale, scale, scale])),
84
+ );
85
+
86
+ for mesh_idx in 0..mesh_count {
87
+ let slot = self.next_model_uniform_slot;
88
+ self.next_model_uniform_slot += 1;
89
+ self.ensure_model_uniform_slot(slot);
90
+
91
+ let model_mvp = mat4_multiply(self.current_vp_matrix, model_matrix);
92
+ self.stage_model_uniform(slot, &Uniforms3D {
93
+ mvp: model_mvp, model: model_matrix,
94
+ prev_mvp: model_mvp, model_tint: tint,
95
+ misc: [0.0, 0.0, foliage, 0.0],
96
+ });
97
+
98
+ self.model_draw_commands.push(CachedModelDraw {
99
+ uniform_slot: slot,
100
+ cache_handle: handle_bits,
101
+ mesh_idx,
102
+ model: model_matrix,
103
+ skinned: false,
104
+ joint_offset: 0.0,
105
+ bounds_override: None,
106
+ });
107
+ }
108
+ }
109
+
110
+ /// EN-039 — `draw_model_cached` taking the model matrix WHOLE, instead of
111
+ /// composing it from a position/scale/yaw. `draw_model_cached_rotated` above
112
+ /// can only express a Y rotation, so an immediate-mode caller could not
113
+ /// pitch or roll: the shooter's weapon stays level while the camera looks up
114
+ /// or down, and the same wall hits any held prop, thrown object or debris
115
+ /// chunk. Everything downstream (alpha cutout, normal/MR maps, foliage wind,
116
+ /// cutout shadows, planar reflections) is identical — this only skips the
117
+ /// composition step and trusts the caller's matrix.
118
+ pub fn draw_model_cached_transform(
119
+ &mut self,
120
+ handle_bits: u64,
121
+ model_matrix: [[f32; 4]; 4],
122
+ tint: [f32; 4],
123
+ ) {
124
+ let mesh_count = match self.model_gpu_cache.get(&handle_bits) {
125
+ Some(Some(meshes)) => meshes.len(),
126
+ _ => return,
127
+ };
128
+
129
+ let foliage = self.foliage_wind.get(&handle_bits).copied().unwrap_or(0.0);
130
+
131
+ for mesh_idx in 0..mesh_count {
132
+ let slot = self.next_model_uniform_slot;
133
+ self.next_model_uniform_slot += 1;
134
+ self.ensure_model_uniform_slot(slot);
135
+
136
+ let model_mvp = mat4_multiply(self.current_vp_matrix, model_matrix);
137
+ self.stage_model_uniform(slot, &Uniforms3D {
138
+ mvp: model_mvp, model: model_matrix,
139
+ prev_mvp: model_mvp, model_tint: tint,
140
+ misc: [0.0, 0.0, foliage, 0.0],
141
+ });
142
+
143
+ self.model_draw_commands.push(CachedModelDraw {
144
+ uniform_slot: slot,
145
+ cache_handle: handle_bits,
146
+ mesh_idx,
147
+ model: model_matrix,
148
+ skinned: false,
149
+ joint_offset: 0.0,
150
+ bounds_override: None,
151
+ });
152
+ }
153
+ }
154
+
155
+ /// Cached-path draw for a SKINNED model: the bind-pose VB/IB stay
156
+ /// GPU-resident (raw joint indices) and the scene VS skins from the
157
+ /// shared joint buffer — replacing the immediate path's per-frame
158
+ /// CPU re-transform + whole-batch vertex re-upload. Consumes the
159
+ /// staged pose ONCE for the whole model and shares its joint-buffer
160
+ /// offset across every primitive via the per-draw uniform's misc.x
161
+ /// (per-primitive pops starved multi-primitive models onto offset 0
162
+ /// — the marauder/tyrant blob; see `take_staged_skin_offset`).
163
+ ///
164
+ /// Rotation-less by design: joint matrices already bake the model's
165
+ /// world orientation (same contract as the old immediate path).
166
+ pub fn draw_model_cached_skinned(
167
+ &mut self,
168
+ handle_bits: u64,
169
+ position: [f32; 3],
170
+ scale: f32,
171
+ tint: [f32; 4],
172
+ ) {
173
+ // Union of the cached meshes' local AABBs = the model's rest-pose
174
+ // AABB (sentinel min > max when every mesh is empty).
175
+ let (mesh_count, rest_min, rest_max) = match self.model_gpu_cache.get(&handle_bits) {
176
+ Some(Some(meshes)) => {
177
+ let mut rmin = [f32::MAX; 3];
178
+ let mut rmax = [f32::MIN; 3];
179
+ for mesh in meshes.iter() {
180
+ if mesh.local_min[0] > mesh.local_max[0] { continue; }
181
+ for a in 0..3 {
182
+ if mesh.local_min[a] < rmin[a] { rmin[a] = mesh.local_min[a]; }
183
+ if mesh.local_max[a] > rmax[a] { rmax[a] = mesh.local_max[a]; }
184
+ }
185
+ }
186
+ (meshes.len(), rmin, rmax)
187
+ }
188
+ _ => return,
189
+ };
190
+ if mesh_count == 0 { return; }
191
+
192
+ let joint_offset = self.take_staged_skin_offset().unwrap_or(0.0);
193
+
194
+ // Model matrix places the rare rigid (weightless) verts; weighted
195
+ // verts get their world placement from the joint matrices.
196
+ let model_matrix = mat4_multiply(
197
+ mat4_translate(IDENTITY_MAT4, position),
198
+ mat4_scale(IDENTITY_MAT4, [scale, scale, scale]),
199
+ );
200
+
201
+ // Rigorous world AABB: a skinned vertex is a convex blend of its
202
+ // per-joint transforms of ONE rest position, so the union of every
203
+ // joint matrix applied to the rest AABB bounds all weighted verts
204
+ // (same argument as `finish_model_segment`). Union in the plain
205
+ // model-matrix transform too so rigid verts are covered. The
206
+ // model's joints occupy frame_joint_data[joint_offset..len] right
207
+ // now: its group was staged just above and the next model's group
208
+ // hasn't been staged yet.
209
+ let (mut wmin, mut wmax) = super::transform_aabb(&model_matrix, rest_min, rest_max);
210
+ let jstart = (joint_offset.max(0.0) as usize).min(self.frame_joint_data.len());
211
+ for j in jstart..self.frame_joint_data.len() {
212
+ let (m0, m1) = super::transform_aabb(
213
+ &self.frame_joint_data[j], rest_min, rest_max,
214
+ );
215
+ for a in 0..3 {
216
+ if m0[a] < wmin[a] { wmin[a] = m0[a]; }
217
+ if m1[a] > wmax[a] { wmax[a] = m1[a]; }
218
+ }
219
+ }
220
+ // Sentinel (min > max) → no bounds; the passes treat the draw as
221
+ // uncullable rather than culling it away.
222
+ let bounds_override = if wmin[0] <= wmax[0] { Some((wmin, wmax)) } else { None };
223
+
224
+ let vp = self.current_vp_matrix;
225
+ for mesh_idx in 0..mesh_count {
226
+ let slot = self.next_model_uniform_slot;
227
+ self.next_model_uniform_slot += 1;
228
+ self.ensure_model_uniform_slot(slot);
229
+
230
+ // Skinned draws: mvp is the BARE view-projection — the
231
+ // joint matrices bake world placement, so the VS goes
232
+ // joint-space → world → clip without a model term.
233
+ // prev_mvp is the EN-022 velocity-reference VP (previous
234
+ // frame, jitter-cancelled): with the previous palette in
235
+ // the VS this yields REAL skeletal+locomotion velocity
236
+ // (it used to be the current VP -> exactly zero velocity,
237
+ // which is why enemies ghosted under TAA/TSR).
238
+ self.stage_model_uniform(slot, &Uniforms3D {
239
+ mvp: vp, model: model_matrix,
240
+ prev_mvp: self.velocity_ref_vp, model_tint: tint,
241
+ misc: [joint_offset, 1.0, 0.0, 0.0],
242
+ });
243
+
244
+ self.model_draw_commands.push(CachedModelDraw {
245
+ uniform_slot: slot,
246
+ cache_handle: handle_bits,
247
+ mesh_idx,
248
+ model: model_matrix,
249
+ skinned: true,
250
+ joint_offset,
251
+ bounds_override,
252
+ });
253
+
254
+ // PT-6 — register the mesh for the dynamic TLAS path: a
255
+ // compute pass skins it into the PT megabuffer and a
256
+ // per-frame BLAS build makes it visible to every ray
257
+ // (traced shadows, bounce light, reflections).
258
+ if self.hw_rt_enabled {
259
+ let (wmin, wmax) = bounds_override
260
+ .unwrap_or(([0.0; 3], [-1.0; 3]));
261
+ self.pt_dynamic_draws.push(super::PtDynamicDraw {
262
+ cache_handle: handle_bits,
263
+ mesh_idx,
264
+ joint_offset,
265
+ model: model_matrix,
266
+ wmin,
267
+ wmax,
268
+ });
269
+ }
270
+ }
271
+ }
272
+
273
+ /// Bind group for one 256 B slot of the pooled model uniform buffer.
274
+ pub(super) fn model_uniform_bg_for_slot(
275
+ device: &wgpu::Device,
276
+ layout: &wgpu::BindGroupLayout,
277
+ pool: &wgpu::Buffer,
278
+ slot: usize,
279
+ ) -> wgpu::BindGroup {
280
+ device.create_bind_group(&wgpu::BindGroupDescriptor {
281
+ label: Some("model_uniform_bg"),
282
+ layout,
283
+ entries: &[wgpu::BindGroupEntry {
284
+ binding: 0,
285
+ resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
286
+ buffer: pool,
287
+ offset: (slot * MODEL_UNIFORM_STRIDE) as u64,
288
+ size: std::num::NonZeroU64::new(
289
+ std::mem::size_of::<Uniforms3D>() as u64,
290
+ ),
291
+ }),
292
+ }],
293
+ })
294
+ }
295
+
296
+ /// Pack a draw's uniforms into the CPU scratch; `flush_model_uniforms`
297
+ /// uploads the whole used range in one `write_buffer` at end-frame.
298
+ fn stage_model_uniform(&mut self, slot: usize, uniforms: &Uniforms3D) {
299
+ self.ensure_model_uniform_slot(slot);
300
+ let off = slot * MODEL_UNIFORM_STRIDE;
301
+ if self.model_uniform_scratch.len() < off + MODEL_UNIFORM_STRIDE {
302
+ self.model_uniform_scratch.resize(off + MODEL_UNIFORM_STRIDE, 0);
303
+ }
304
+ self.model_uniform_scratch[off..off + std::mem::size_of::<Uniforms3D>()]
305
+ .copy_from_slice(bytemuck::bytes_of(uniforms));
306
+ }
307
+
308
+ /// Upload every staged cached-model uniform in one write. Called once
309
+ /// per frame from the end-frame paths, before passes execute (queued
310
+ /// writes land at submit, ahead of all encoded passes).
311
+ pub(super) fn flush_model_uniforms(&mut self) {
312
+ if self.next_model_uniform_slot == 0 { return; }
313
+ let used = (self.next_model_uniform_slot * MODEL_UNIFORM_STRIDE)
314
+ .min(self.model_uniform_scratch.len());
315
+ if used > 0 {
316
+ self.queue.write_buffer(
317
+ &self.model_uniform_pool,
318
+ 0,
319
+ &self.model_uniform_scratch[..used],
320
+ );
321
+ }
322
+ }
323
+
324
+ fn ensure_model_uniform_slot(&mut self, slot: usize) {
325
+ if slot < self.model_uniform_pool_capacity {
326
+ return;
327
+ }
328
+ // Grow by doubling: new pool + rebuild every slot bind group
329
+ // (rare — only when a scene exceeds the previous high-water mark).
330
+ let new_cap = (slot + 1)
331
+ .next_power_of_two()
332
+ .max(self.model_uniform_pool_capacity * 2);
333
+ self.model_uniform_pool = self.device.create_buffer(&wgpu::BufferDescriptor {
334
+ label: Some("model_uniform_pool"),
335
+ size: (new_cap * MODEL_UNIFORM_STRIDE) as u64,
336
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
337
+ mapped_at_creation: false,
338
+ });
339
+ self.model_uniform_bind_groups = (0..new_cap)
340
+ .map(|s| Self::model_uniform_bg_for_slot(
341
+ &self.device, &self.uniform_3d_layout, &self.model_uniform_pool, s,
342
+ ))
343
+ .collect();
344
+ self.model_uniform_pool_capacity = new_cap;
345
+ }
346
+
347
+ pub fn draw_model_mesh(&mut self, vertices: &[Vertex3D], indices: &[u32], position: [f32; 3], scale: f32) {
348
+ self.draw_model_mesh_tinted(vertices, indices, position, scale, [1.0, 1.0, 1.0, 1.0], 0);
349
+ }
350
+
351
+ /// True if any vertex carries skin weights (same test the cache and
352
+ /// the per-vertex draw loops use).
353
+ fn mesh_has_skin(vertices: &[Vertex3D]) -> bool {
354
+ vertices.iter().any(|v|
355
+ v.weights[0] + v.weights[1] + v.weights[2] + v.weights[3] > 0.01)
356
+ }
357
+
358
+ /// Pop the next staged skin pose (FIFO) and pack it into the frame
359
+ /// joint accumulator, returning the base slot offset its matrices
360
+ /// were packed at. `None` when nothing is staged.
361
+ ///
362
+ /// drawModel-level callers must call this ONCE per model and share
363
+ /// the offset across every primitive: when each primitive popped its
364
+ /// own entry, the second primitive of a multi-primitive skinned model
365
+ /// (marauder, tyrant) found the FIFO empty, fell back to offset 0 and
366
+ /// got skinned by whatever pose lives at the start of the joint
367
+ /// buffer — the player's — rendering as a giant mangled blob glued to
368
+ /// the player's position.
369
+ pub fn take_staged_skin_offset(&mut self) -> Option<f32> {
370
+ if self.pending_skin_groups.is_empty() {
371
+ return None;
372
+ }
373
+ let group = self.pending_skin_groups.remove(0);
374
+ // PT-7 — the prev-palette queue moves in lockstep so both
375
+ // arenas assign the SAME offset to this draw. A length
376
+ // mismatch (or an unpaired group from a legacy caller) falls
377
+ // back to the current pose = zero skeletal velocity.
378
+ let prev_group = if !self.pending_skin_groups_prev.is_empty() {
379
+ let p = self.pending_skin_groups_prev.remove(0);
380
+ if p.len() == group.len() { p } else { group.clone() }
381
+ } else {
382
+ group.clone()
383
+ };
384
+ let start = self.frame_joint_data.len();
385
+ // Cap at the 1024-slot buffer. Overflowing poses land at offset 0,
386
+ // which at least avoids an out-of-range read — the model will look
387
+ // mis-posed but not corrupt memory.
388
+ if start + group.len() <= 1024 {
389
+ self.frame_joint_data.extend_from_slice(&group);
390
+ self.frame_joint_data_prev.extend_from_slice(&prev_group);
391
+ Some(start as f32)
392
+ } else {
393
+ Some(0.0)
394
+ }
395
+ }
396
+
397
+ /// Same as `draw_model_mesh_tinted` but applies a Y-axis rotation
398
+ /// (radians) to the mesh local space before scale + translate.
399
+ /// Skinned meshes ignore the rotation here — pose joints already
400
+ /// drive their orientation. CPU-side baking mirrors the unrotated
401
+ /// path so callers can mix rotated and unrotated draws freely
402
+ /// without extra GPU state.
403
+ pub fn draw_model_mesh_tinted_rotated(&mut self, vertices: &[Vertex3D], indices: &[u32], position: [f32; 3], scale: f32, tint: [f32; 4], texture_idx: u32, rot_y: f32) {
404
+ // Mirror the joint-pose plumbing in the non-rotated path so a
405
+ // skinned mesh drawn here still consumes its pending pose.
406
+ let joint_offset = if Self::mesh_has_skin(vertices) {
407
+ self.take_staged_skin_offset()
408
+ } else {
409
+ None
410
+ };
411
+ self.draw_model_mesh_tinted_rotated_with_joints(
412
+ vertices, indices, position, scale, tint, texture_idx, rot_y, joint_offset);
413
+ }
414
+
415
+ /// Rotated-path body with an explicit joint-buffer offset. Callers
416
+ /// that draw multiple primitives of ONE skinned model pop the staged
417
+ /// pose once (`take_staged_skin_offset`) and pass the same offset to
418
+ /// every primitive.
419
+ pub fn draw_model_mesh_tinted_rotated_with_joints(&mut self, vertices: &[Vertex3D], indices: &[u32], position: [f32; 3], scale: f32, tint: [f32; 4], texture_idx: u32, rot_y: f32, joint_offset: Option<f32>) {
420
+ // Own bounded segment (even if the texture matches) so the shadow
421
+ // pass can cull + cache this draw independently of neighbours.
422
+ self.push_draw_call_3d(texture_idx, true);
423
+ let joint_offset: f32 = joint_offset.unwrap_or(0.0);
424
+
425
+ let cos_y = rot_y.cos();
426
+ let sin_y = rot_y.sin();
427
+ let base = self.vertices_3d.len() as u32;
428
+ let mut seg = SegBounds::new();
429
+ for v in vertices {
430
+ let is_skinned = v.weights[0] + v.weights[1] + v.weights[2] + v.weights[3] > 0.01;
431
+ let pos = if is_skinned {
432
+ v.position
433
+ } else {
434
+ // Rotate local-space position around Y, then scale + translate.
435
+ let lx = v.position[0];
436
+ let ly = v.position[1];
437
+ let lz = v.position[2];
438
+ let rx = cos_y * lx + sin_y * lz;
439
+ let rz = -sin_y * lx + cos_y * lz;
440
+ [rx * scale + position[0],
441
+ ly * scale + position[1],
442
+ rz * scale + position[2]]
443
+ };
444
+ // Rotate the surface normal too so lighting matches the new
445
+ // orientation. Y-axis rotation leaves normal.y untouched.
446
+ let n = v.normal;
447
+ let normal = if is_skinned {
448
+ n
449
+ } else {
450
+ [ cos_y * n[0] + sin_y * n[2],
451
+ n[1],
452
+ -sin_y * n[0] + cos_y * n[2] ]
453
+ };
454
+ // Rotate tangent.xyz the same way; preserve handedness in w.
455
+ let t = v.tangent;
456
+ let tangent = if is_skinned {
457
+ t
458
+ } else {
459
+ [ cos_y * t[0] + sin_y * t[2],
460
+ t[1],
461
+ -sin_y * t[0] + cos_y * t[2],
462
+ t[3] ]
463
+ };
464
+ let joints_out = if is_skinned {
465
+ [v.joints[0] + joint_offset,
466
+ v.joints[1] + joint_offset,
467
+ v.joints[2] + joint_offset,
468
+ v.joints[3] + joint_offset]
469
+ } else {
470
+ v.joints
471
+ };
472
+ seg.note(is_skinned, if is_skinned { v.position } else { pos });
473
+ self.vertices_3d.push(Vertex3D {
474
+ position: pos,
475
+ normal,
476
+ color: [
477
+ v.color[0] * tint[0],
478
+ v.color[1] * tint[1],
479
+ v.color[2] * tint[2],
480
+ v.color[3] * tint[3],
481
+ ],
482
+ uv: v.uv,
483
+ joints: joints_out,
484
+ weights: v.weights,
485
+ tangent,
486
+ });
487
+ }
488
+ for &idx in indices {
489
+ self.indices_3d.push(base + idx);
490
+ }
491
+ self.finish_model_segment(seg, joint_offset);
492
+ }
493
+
494
+ pub fn draw_model_mesh_tinted(&mut self, vertices: &[Vertex3D], indices: &[u32], position: [f32; 3], scale: f32, tint: [f32; 4], texture_idx: u32) {
495
+ // If this mesh is skinned, consume the next pending pose
496
+ // (FIFO) and pack its matrices into the frame accumulator at
497
+ // the current cursor. Each vertex's joint indices then get
498
+ // shifted by that cursor so the shader samples this mesh's
499
+ // slice of the shared joint buffer. With a 1024-slot buffer,
500
+ // multiple skinned models can coexist in one frame.
501
+ let joint_offset = if Self::mesh_has_skin(vertices) {
502
+ self.take_staged_skin_offset()
503
+ } else {
504
+ None
505
+ };
506
+ self.draw_model_mesh_tinted_with_joints(
507
+ vertices, indices, position, scale, tint, texture_idx, joint_offset);
508
+ }
509
+
510
+ /// Body of `draw_model_mesh_tinted` with an explicit joint-buffer
511
+ /// offset. Callers that draw multiple primitives of ONE skinned model
512
+ /// pop the staged pose once (`take_staged_skin_offset`) and pass the
513
+ /// same offset to every primitive.
514
+ pub fn draw_model_mesh_tinted_with_joints(&mut self, vertices: &[Vertex3D], indices: &[u32], position: [f32; 3], scale: f32, tint: [f32; 4], texture_idx: u32, joint_offset: Option<f32>) {
515
+ // Own bounded segment — see the rotated variant.
516
+ self.push_draw_call_3d(texture_idx, true);
517
+ let joint_offset: f32 = joint_offset.unwrap_or(0.0);
518
+
519
+ let base = self.vertices_3d.len() as u32;
520
+ let mut seg = SegBounds::new();
521
+ for v in vertices {
522
+ // Check if vertex is skinned (has non-zero weights)
523
+ let is_skinned = v.weights[0] + v.weights[1] + v.weights[2] + v.weights[3] > 0.01;
524
+ let pos = if is_skinned {
525
+ // Skinned: pass raw bind-pose positions — joint matrices handle transform
526
+ v.position
527
+ } else {
528
+ // Unskinned: apply CPU-side position + scale
529
+ [v.position[0] * scale + position[0],
530
+ v.position[1] * scale + position[1],
531
+ v.position[2] * scale + position[2]]
532
+ };
533
+ let joints_out = if is_skinned {
534
+ [v.joints[0] + joint_offset,
535
+ v.joints[1] + joint_offset,
536
+ v.joints[2] + joint_offset,
537
+ v.joints[3] + joint_offset]
538
+ } else {
539
+ v.joints
540
+ };
541
+ seg.note(is_skinned, if is_skinned { v.position } else { pos });
542
+ self.vertices_3d.push(Vertex3D {
543
+ position: pos,
544
+ normal: v.normal,
545
+ color: [
546
+ v.color[0] * tint[0],
547
+ v.color[1] * tint[1],
548
+ v.color[2] * tint[2],
549
+ v.color[3] * tint[3],
550
+ ],
551
+ uv: v.uv,
552
+ joints: joints_out,
553
+ weights: v.weights,
554
+ tangent: v.tangent,
555
+ });
556
+ }
557
+ for &idx in indices {
558
+ self.indices_3d.push(base + idx);
559
+ }
560
+ self.finish_model_segment(seg, joint_offset);
561
+ }
562
+
563
+ /// Fold a model draw's accumulated bounds into its (freshly-pushed)
564
+ /// segment. Skinned content is bounded by the union of its joint
565
+ /// matrices applied to the rest-pose AABB — rigorous, because a
566
+ /// skinned vertex is a convex combination of its per-joint
567
+ /// transforms of one rest position, and a convex combination of
568
+ /// points inside a set of AABBs stays inside their union AABB.
569
+ fn finish_model_segment(&mut self, seg: SegBounds, joint_offset: f32) {
570
+ let mut wmin = seg.world_min;
571
+ let mut wmax = seg.world_max;
572
+ if seg.any_skinned && seg.rest_min[0] <= seg.rest_max[0] {
573
+ // The model's joints occupy frame_joint_data[joint_offset..len]
574
+ // at this point: its group was staged immediately before its
575
+ // primitives and the next model's group hasn't been staged yet.
576
+ let jstart = (joint_offset.max(0.0) as usize).min(self.frame_joint_data.len());
577
+ for j in jstart..self.frame_joint_data.len() {
578
+ let (m0, m1) = super::transform_aabb(
579
+ &self.frame_joint_data[j], seg.rest_min, seg.rest_max,
580
+ );
581
+ for a in 0..3 {
582
+ if m0[a] < wmin[a] { wmin[a] = m0[a]; }
583
+ if m1[a] > wmax[a] { wmax[a] = m1[a]; }
584
+ }
585
+ }
586
+ }
587
+ let call = self.draw_calls_3d.last_mut()
588
+ .expect("finish_model_segment: segment was pushed at fn start");
589
+ call.has_skinned = seg.any_skinned;
590
+ call.content_hash = seg.hash;
591
+ // Sentinel stays (min > max) when nothing contributed bounds —
592
+ // the shadow pass then treats the segment as uncullable.
593
+ if wmin[0] <= wmax[0] {
594
+ call.wmin = wmin;
595
+ call.wmax = wmax;
596
+ }
597
+ }
598
+ }
599
+
600
+ /// Per-model-draw bounds accumulator: world AABB + content hash for the
601
+ /// non-skinned verts (already world-space at append time), rest-pose
602
+ /// AABB for the skinned ones (their world placement lives in the joint
603
+ /// matrices — see `finish_model_segment`).
604
+ struct SegBounds {
605
+ world_min: [f32; 3],
606
+ world_max: [f32; 3],
607
+ rest_min: [f32; 3],
608
+ rest_max: [f32; 3],
609
+ any_skinned: bool,
610
+ hash: u64,
611
+ }
612
+
613
+ impl SegBounds {
614
+ fn new() -> Self {
615
+ SegBounds {
616
+ world_min: [f32::MAX; 3],
617
+ world_max: [f32::MIN; 3],
618
+ rest_min: [f32::MAX; 3],
619
+ rest_max: [f32::MIN; 3],
620
+ any_skinned: false,
621
+ hash: super::types::FNV_OFFSET,
622
+ }
623
+ }
624
+
625
+ #[inline]
626
+ fn note(&mut self, is_skinned: bool, pos: [f32; 3]) {
627
+ if is_skinned {
628
+ self.any_skinned = true;
629
+ for a in 0..3 {
630
+ if pos[a] < self.rest_min[a] { self.rest_min[a] = pos[a]; }
631
+ if pos[a] > self.rest_max[a] { self.rest_max[a] = pos[a]; }
632
+ }
633
+ } else {
634
+ for a in 0..3 {
635
+ if pos[a] < self.world_min[a] { self.world_min[a] = pos[a]; }
636
+ if pos[a] > self.world_max[a] { self.world_max[a] = pos[a]; }
637
+ }
638
+ self.hash = super::types::fnv1a_bytes(self.hash, bytemuck::bytes_of(&pos));
639
+ }
640
+ }
641
+ }