@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,474 @@
1
+ //! EN-025 — ragdolls.
2
+ //!
3
+ //! Jolt has shipped `Ragdoll.cpp` in the vendored submodule since day one; what
4
+ //! was missing was any way to reach it. This is that. It does NOT use Jolt's
5
+ //! `Ragdoll` class, though — that wants a `RagdollSettings` asset authored
6
+ //! alongside the skeleton, and we don't have one. Instead it builds the ragdoll
7
+ //! from the skeleton we DO have, at runtime: one capsule per bone, one limited
8
+ //! six-DOF joint per articulation. Same result, no new authoring step, and it
9
+ //! works for any skinned model the game already loads.
10
+ //!
11
+ //! # The coordinate problem, and how it is solved
12
+ //!
13
+ //! Physics runs in world space. Skinning runs in model space. The bridge is the
14
+ //! model transform `M` (scale, position, yaw) the game was drawing with at the
15
+ //! moment of death — and the key decision here is that **`M` is frozen at
16
+ //! activation**. The corpse does not follow the enemy's old position, because
17
+ //! the enemy is gone; the bodies carry the motion now, and `M` is only a fixed
18
+ //! frame to express them in.
19
+ //!
20
+ //! So for each bodied joint we store, at activation, the joint's transform in
21
+ //! the *body's* frame:
22
+ //!
23
+ //! ```text
24
+ //! offset_j = (M⁻¹ · B_j(0))⁻¹ · jointWorld_j(0)
25
+ //! ```
26
+ //!
27
+ //! and every frame after, recover the pose by pushing the simulated body back
28
+ //! through it:
29
+ //!
30
+ //! ```text
31
+ //! jointWorld_j(t) = (M⁻¹ · B_j(t)) · offset_j
32
+ //! ```
33
+ //!
34
+ //! Joints with no body of their own (fingers, tails, the long chains that would
35
+ //! cost bodies and buy nothing) simply ride their nearest bodied ancestor at
36
+ //! their rest pose. That is what `maxBodies` is really buying: a coarse
37
+ //! skeleton that behaves, instead of a fine one that jitters.
38
+
39
+ use crate::models::{ModelAnimation, SkeletonData};
40
+
41
+ /// One simulated bone.
42
+ struct Bone {
43
+ /// Joint this body drives.
44
+ joint: usize,
45
+ /// Physics body handle (as the f64 the physics registry uses).
46
+ body: f64,
47
+ /// Joint transform expressed in the body's frame — see the module note.
48
+ offset: [[f32; 4]; 4],
49
+ }
50
+
51
+ pub struct Ragdoll {
52
+ pub active: bool,
53
+ bones: Vec<Bone>,
54
+ constraints: Vec<f64>,
55
+ /// Model transform frozen at activation, and its inverse.
56
+ model: [[f32; 4]; 4],
57
+ inv_model: [[f32; 4]; 4],
58
+ /// The (scale, pos, rot) that `model` was built from — the joint upload
59
+ /// needs them again in exactly that form.
60
+ scale: f32,
61
+ pos: [f32; 3],
62
+ rot: f32,
63
+ /// Seconds since activation, so the game can settle/despawn on a timer.
64
+ pub age: f32,
65
+ }
66
+
67
+ impl Ragdoll {
68
+ pub fn new() -> Self {
69
+ Self {
70
+ active: false,
71
+ bones: Vec::new(),
72
+ constraints: Vec::new(),
73
+ model: mat4_identity(),
74
+ inv_model: mat4_identity(),
75
+ scale: 1.0,
76
+ pos: [0.0; 3],
77
+ rot: 0.0,
78
+ age: 0.0,
79
+ }
80
+ }
81
+ }
82
+
83
+ /// Which joints get a body.
84
+ ///
85
+ /// Breadth-first from the roots, taking any joint whose bone (parent → self) is
86
+ /// long enough to be worth simulating, until `max_bodies` is reached. BFS is the
87
+ /// point: it spends the budget on the spine and the limbs — the things whose
88
+ /// motion you actually read — and runs out before it reaches the fingers.
89
+ fn select_bones(skel: &SkeletonData, min_len: f32, max_bodies: usize,
90
+ joint_world: &[[[f32; 4]; 4]]) -> Vec<usize> {
91
+ let n = skel.joints.len();
92
+ let mut parent = vec![usize::MAX; n];
93
+ for (i, j) in skel.joints.iter().enumerate() {
94
+ for &c in &j.children {
95
+ if c < n { parent[c] = i; }
96
+ }
97
+ }
98
+
99
+ let mut order: Vec<usize> = Vec::new();
100
+ let mut queue: std::collections::VecDeque<usize> = skel.root_joints.iter().copied().collect();
101
+ while let Some(j) = queue.pop_front() {
102
+ order.push(j);
103
+ for &c in &skel.joints[j].children {
104
+ if c < n { queue.push_back(c); }
105
+ }
106
+ }
107
+
108
+ let mut picked = Vec::new();
109
+ for &j in &order {
110
+ if picked.len() >= max_bodies { break; }
111
+ let p = parent[j];
112
+ if p == usize::MAX { continue; } // roots carry no bone
113
+ let a = translation(&joint_world[p]);
114
+ let b = translation(&joint_world[j]);
115
+ let len = dist(a, b);
116
+ if len >= min_len {
117
+ picked.push(j);
118
+ }
119
+ }
120
+ picked
121
+ }
122
+
123
+ pub struct RagdollBuild {
124
+ pub joint: usize,
125
+ /// Capsule half-height and radius, in WORLD units (model scale applied).
126
+ pub half_height: f32,
127
+ pub radius: f32,
128
+ /// World transform of the capsule at activation (column-major 4x4).
129
+ pub world: [[f32; 4]; 4],
130
+ /// Parent bone index within the build list, or usize::MAX for none.
131
+ pub parent_bone: usize,
132
+ /// World-space anchor for the joint to the parent bone.
133
+ pub anchor: [f32; 3],
134
+ }
135
+
136
+ /// Compute everything the physics side needs to create the bodies. Kept separate
137
+ /// from body creation so this module never has to know about JoltPhysics — the
138
+ /// FFI layer owns that, and this stays testable.
139
+ #[allow(clippy::too_many_arguments)]
140
+ pub fn plan(
141
+ anim: &ModelAnimation,
142
+ scale: f32, pos: [f32; 3], rot: f32,
143
+ max_bodies: usize,
144
+ radius_scale: f32,
145
+ ) -> Vec<RagdollBuild> {
146
+ let Some(skel) = &anim.skeleton else { return Vec::new() };
147
+ if anim.joint_world.len() != skel.joints.len() { return Vec::new() }
148
+
149
+ let model = compose(scale, pos, rot);
150
+ let picked = select_bones(skel, 0.06, max_bodies, &anim.joint_world);
151
+
152
+ // joint -> index in `picked`, so a bone can find its parent bone.
153
+ let mut bone_of = vec![usize::MAX; skel.joints.len()];
154
+ for (bi, &j) in picked.iter().enumerate() { bone_of[j] = bi; }
155
+
156
+ let n = skel.joints.len();
157
+ let mut parent = vec![usize::MAX; n];
158
+ for (i, j) in skel.joints.iter().enumerate() {
159
+ for &c in &j.children { if c < n { parent[c] = i; } }
160
+ }
161
+
162
+ let mut out = Vec::with_capacity(picked.len());
163
+ for &j in &picked {
164
+ let p = parent[j];
165
+ let a_local = translation(&anim.joint_world[p]);
166
+ let b_local = translation(&anim.joint_world[j]);
167
+
168
+ // World-space endpoints of the bone.
169
+ let a = xform_point(&model, a_local);
170
+ let b = xform_point(&model, b_local);
171
+ let len = dist(a, b);
172
+ if len < 1e-4 { continue; }
173
+
174
+ // Capsule spans the bone: centre at the midpoint, +Y down its length
175
+ // (Jolt capsules are Y-aligned), radius a fraction of the length.
176
+ let mid = [(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5, (a[2] + b[2]) * 0.5];
177
+ let dir = normalize([b[0] - a[0], b[1] - a[1], b[2] - a[2]]);
178
+ let radius = (len * radius_scale).clamp(0.02, len * 0.45);
179
+ let half_height = (len * 0.5 - radius).max(0.01);
180
+
181
+ // Rotation taking +Y onto the bone direction.
182
+ let world = compose_from_dir(mid, dir);
183
+
184
+ // Find the nearest ANCESTOR that has a body — not necessarily the
185
+ // immediate parent joint, since that one may not have been picked.
186
+ let mut anc = p;
187
+ while anc != usize::MAX && bone_of[anc] == usize::MAX {
188
+ anc = parent[anc];
189
+ }
190
+ let parent_bone = if anc == usize::MAX { usize::MAX } else { bone_of[anc] };
191
+
192
+ out.push(RagdollBuild {
193
+ joint: j,
194
+ half_height,
195
+ radius,
196
+ world,
197
+ parent_bone,
198
+ // Joint sits at the bone's proximal end — that IS the articulation.
199
+ anchor: a,
200
+ });
201
+ }
202
+ out
203
+ }
204
+
205
+ impl Ragdoll {
206
+ /// Called by the FFI once the bodies exist. `bodies[i]` pairs with
207
+ /// `builds[i]`.
208
+ pub fn attach(&mut self, builds: &[RagdollBuild], bodies: &[f64],
209
+ constraints: Vec<f64>,
210
+ anim: &ModelAnimation,
211
+ scale: f32, pos: [f32; 3], rot: f32) {
212
+ self.model = compose(scale, pos, rot);
213
+ self.inv_model = invert_rigid(&self.model);
214
+ self.scale = scale;
215
+ self.pos = pos;
216
+ self.rot = rot;
217
+ self.constraints = constraints;
218
+ self.bones.clear();
219
+ self.age = 0.0;
220
+
221
+ for (i, b) in builds.iter().enumerate() {
222
+ if i >= bodies.len() || bodies[i] == 0.0 { continue; }
223
+ // offset = (M⁻¹ · B(0))⁻¹ · jointWorld(0)
224
+ let body_model = mat4_mul(&self.inv_model, &b.world);
225
+ let offset = mat4_mul(&invert_rigid(&body_model), &anim.joint_world[b.joint]);
226
+ self.bones.push(Bone { joint: b.joint, body: bodies[i], offset });
227
+ }
228
+ self.active = true;
229
+ }
230
+
231
+ pub fn bodies(&self) -> Vec<f64> { self.bones.iter().map(|b| b.body).collect() }
232
+ pub fn constraint_handles(&self) -> &[f64] { &self.constraints }
233
+
234
+ pub fn upload_params(&self) -> (f32, [f32; 3], f32) { (self.scale, self.pos, self.rot) }
235
+
236
+ /// Rebuild `anim.joint_matrices` from the simulated bodies.
237
+ /// `body_world[i]` is the world transform of `self.bones[i]`'s body.
238
+ pub fn apply(&self, anim: &mut ModelAnimation, body_world: &[[[f32; 4]; 4]]) {
239
+ let Some(skel) = &anim.skeleton else { return };
240
+ let n = skel.joints.len();
241
+ if anim.joint_world.len() != n { return }
242
+
243
+ // 1. Bodied joints: push the body back through the stored offset.
244
+ let mut have = vec![false; n];
245
+ let mut world = vec![mat4_identity(); n];
246
+ for (i, bone) in self.bones.iter().enumerate() {
247
+ if i >= body_world.len() { break; }
248
+ let body_model = mat4_mul(&self.inv_model, &body_world[i]);
249
+ world[bone.joint] = mat4_mul(&body_model, &bone.offset);
250
+ have[bone.joint] = true;
251
+ }
252
+
253
+ // 2. Everything else rides its parent at the rest pose. Walk from the
254
+ // roots so a parent is always resolved before its children.
255
+ let mut stack: Vec<(usize, [[f32; 4]; 4])> =
256
+ skel.root_joints.iter().map(|&r| (r, mat4_identity())).collect();
257
+ while let Some((j, parent_world)) = stack.pop() {
258
+ if j >= n { continue; }
259
+ if !have[j] {
260
+ let local = mat4_from_trs(
261
+ &skel.joints[j].rest_translation,
262
+ &skel.joints[j].rest_rotation,
263
+ &skel.joints[j].rest_scale,
264
+ );
265
+ world[j] = mat4_mul(&parent_world, &local);
266
+ }
267
+ for &c in &skel.joints[j].children {
268
+ stack.push((c, world[j]));
269
+ }
270
+ }
271
+
272
+ for i in 0..n {
273
+ anim.joint_matrices[i] = mat4_mul(&world[i], &skel.joints[i].inverse_bind);
274
+ }
275
+ anim.joint_world.copy_from_slice(&world);
276
+ }
277
+ }
278
+
279
+ // ---- registry ----------------------------------------------------------------
280
+
281
+ pub struct RagdollManager {
282
+ pub slots: Vec<Option<Ragdoll>>,
283
+ }
284
+
285
+ impl RagdollManager {
286
+ pub fn new() -> Self { Self { slots: Vec::new() } }
287
+ pub fn create(&mut self) -> u32 {
288
+ self.slots.push(Some(Ragdoll::new()));
289
+ self.slots.len() as u32
290
+ }
291
+ pub fn get_mut(&mut self, h: u32) -> Option<&mut Ragdoll> {
292
+ if h == 0 { return None }
293
+ self.slots.get_mut(h as usize - 1)?.as_mut()
294
+ }
295
+ pub fn get(&self, h: u32) -> Option<&Ragdoll> {
296
+ if h == 0 { return None }
297
+ self.slots.get(h as usize - 1)?.as_ref()
298
+ }
299
+ }
300
+
301
+ impl Default for RagdollManager {
302
+ fn default() -> Self { Self::new() }
303
+ }
304
+
305
+ // ---- math --------------------------------------------------------------------
306
+
307
+ fn mat4_identity() -> [[f32; 4]; 4] {
308
+ [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]]
309
+ }
310
+
311
+ fn mat4_mul(a: &[[f32; 4]; 4], b: &[[f32; 4]; 4]) -> [[f32; 4]; 4] {
312
+ let mut o = [[0.0f32; 4]; 4];
313
+ for col in 0..4 {
314
+ for row in 0..4 {
315
+ o[col][row] = a[0][row] * b[col][0] + a[1][row] * b[col][1]
316
+ + a[2][row] * b[col][2] + a[3][row] * b[col][3];
317
+ }
318
+ }
319
+ o
320
+ }
321
+
322
+ fn translation(m: &[[f32; 4]; 4]) -> [f32; 3] { [m[3][0], m[3][1], m[3][2]] }
323
+
324
+ fn xform_point(m: &[[f32; 4]; 4], p: [f32; 3]) -> [f32; 3] {
325
+ [
326
+ m[0][0] * p[0] + m[1][0] * p[1] + m[2][0] * p[2] + m[3][0],
327
+ m[0][1] * p[0] + m[1][1] * p[1] + m[2][1] * p[2] + m[3][1],
328
+ m[0][2] * p[0] + m[1][2] * p[1] + m[2][2] * p[2] + m[3][2],
329
+ ]
330
+ }
331
+
332
+ fn dist(a: [f32; 3], b: [f32; 3]) -> f32 {
333
+ let d = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
334
+ (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
335
+ }
336
+
337
+ fn normalize(v: [f32; 3]) -> [f32; 3] {
338
+ let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
339
+ if l > 1e-6 { [v[0] / l, v[1] / l, v[2] / l] } else { [0.0, 1.0, 0.0] }
340
+ }
341
+
342
+ fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
343
+ [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
344
+ }
345
+
346
+ /// Model transform: translate · rotateY · scale.
347
+ pub fn compose(scale: f32, pos: [f32; 3], rot: f32) -> [[f32; 4]; 4] {
348
+ let (s, c) = (rot.sin(), rot.cos());
349
+ [
350
+ [c * scale, 0.0, -s * scale, 0.0],
351
+ [0.0, scale, 0.0, 0.0],
352
+ [s * scale, 0.0, c * scale, 0.0],
353
+ [pos[0], pos[1], pos[2], 1.0],
354
+ ]
355
+ }
356
+
357
+ /// A rigid frame whose +Y points along `dir`, centred at `p`.
358
+ fn compose_from_dir(p: [f32; 3], dir: [f32; 3]) -> [[f32; 4]; 4] {
359
+ let up = dir;
360
+ // Any axis not parallel to `up` works as the seed for the basis.
361
+ let seed = if up[1].abs() > 0.99 { [1.0, 0.0, 0.0] } else { [0.0, 1.0, 0.0] };
362
+ let right = normalize(cross(seed, up));
363
+ let fwd = cross(up, right);
364
+ [
365
+ [right[0], right[1], right[2], 0.0],
366
+ [up[0], up[1], up[2], 0.0],
367
+ [fwd[0], fwd[1], fwd[2], 0.0],
368
+ [p[0], p[1], p[2], 1.0],
369
+ ]
370
+ }
371
+
372
+ /// Inverse of a transform with uniform scale + rotation + translation.
373
+ /// (A general inverse would be wasted here and less numerically pleasant.)
374
+ fn invert_rigid(m: &[[f32; 4]; 4]) -> [[f32; 4]; 4] {
375
+ // Recover uniform scale from the first basis vector.
376
+ let s2 = m[0][0] * m[0][0] + m[0][1] * m[0][1] + m[0][2] * m[0][2];
377
+ let s = s2.sqrt().max(1e-8);
378
+ let inv_s = 1.0 / s;
379
+ // R⁻¹ = Rᵀ, with the scale divided out twice (once for the transpose's own
380
+ // scale, once for the inverse scale).
381
+ let r = [
382
+ [m[0][0] * inv_s * inv_s, m[1][0] * inv_s * inv_s, m[2][0] * inv_s * inv_s],
383
+ [m[0][1] * inv_s * inv_s, m[1][1] * inv_s * inv_s, m[2][1] * inv_s * inv_s],
384
+ [m[0][2] * inv_s * inv_s, m[1][2] * inv_s * inv_s, m[2][2] * inv_s * inv_s],
385
+ ];
386
+ let t = [m[3][0], m[3][1], m[3][2]];
387
+ let it = [
388
+ -(r[0][0] * t[0] + r[1][0] * t[1] + r[2][0] * t[2]),
389
+ -(r[0][1] * t[0] + r[1][1] * t[1] + r[2][1] * t[2]),
390
+ -(r[0][2] * t[0] + r[1][2] * t[1] + r[2][2] * t[2]),
391
+ ];
392
+ [
393
+ [r[0][0], r[0][1], r[0][2], 0.0],
394
+ [r[1][0], r[1][1], r[1][2], 0.0],
395
+ [r[2][0], r[2][1], r[2][2], 0.0],
396
+ [it[0], it[1], it[2], 1.0],
397
+ ]
398
+ }
399
+
400
+ fn mat4_from_trs(t: &[f32; 3], q: &[f32; 4], s: &[f32; 3]) -> [[f32; 4]; 4] {
401
+ let (x, y, z, w) = (q[0], q[1], q[2], q[3]);
402
+ let (x2, y2, z2) = (x + x, y + y, z + z);
403
+ let (xx, xy, xz) = (x * x2, x * y2, x * z2);
404
+ let (yy, yz, zz) = (y * y2, y * z2, z * z2);
405
+ let (wx, wy, wz) = (w * x2, w * y2, w * z2);
406
+ [
407
+ [(1.0 - (yy + zz)) * s[0], (xy + wz) * s[0], (xz - wy) * s[0], 0.0],
408
+ [(xy - wz) * s[1], (1.0 - (xx + zz)) * s[1], (yz + wx) * s[1], 0.0],
409
+ [(xz + wy) * s[2], (yz - wx) * s[2], (1.0 - (xx + yy)) * s[2], 0.0],
410
+ [t[0], t[1], t[2], 1.0],
411
+ ]
412
+ }
413
+
414
+ /// Build a world transform from a physics body's position + quaternion.
415
+ pub fn from_pos_quat(p: [f32; 3], q: [f32; 4]) -> [[f32; 4]; 4] {
416
+ mat4_from_trs(&p, &q, &[1.0, 1.0, 1.0])
417
+ }
418
+
419
+ /// Quaternion (x, y, z, w) from an orthonormal rotation matrix. Shepperd's
420
+ /// method — pick the largest diagonal term so the division never blows up on a
421
+ /// 180° rotation, which the naive `w = sqrt(1+trace)/2` form does.
422
+ pub fn quat_from_mat(m: &[[f32; 4]; 4]) -> [f32; 4] {
423
+ let (m00, m01, m02) = (m[0][0], m[0][1], m[0][2]);
424
+ let (m10, m11, m12) = (m[1][0], m[1][1], m[1][2]);
425
+ let (m20, m21, m22) = (m[2][0], m[2][1], m[2][2]);
426
+ let trace = m00 + m11 + m22;
427
+
428
+ if trace > 0.0 {
429
+ let s = (trace + 1.0).sqrt() * 2.0;
430
+ [(m12 - m21) / s, (m20 - m02) / s, (m01 - m10) / s, 0.25 * s]
431
+ } else if m00 > m11 && m00 > m22 {
432
+ let s = (1.0 + m00 - m11 - m22).sqrt() * 2.0;
433
+ [0.25 * s, (m10 + m01) / s, (m20 + m02) / s, (m12 - m21) / s]
434
+ } else if m11 > m22 {
435
+ let s = (1.0 + m11 - m00 - m22).sqrt() * 2.0;
436
+ [(m10 + m01) / s, 0.25 * s, (m21 + m12) / s, (m20 - m02) / s]
437
+ } else {
438
+ let s = (1.0 + m22 - m00 - m11).sqrt() * 2.0;
439
+ [(m20 + m02) / s, (m21 + m12) / s, 0.25 * s, (m01 - m10) / s]
440
+ }
441
+ }
442
+
443
+ #[cfg(test)]
444
+ mod tests {
445
+ use super::*;
446
+
447
+ #[test]
448
+ fn invert_rigid_round_trips() {
449
+ let m = compose(2.5, [1.0, -3.0, 4.0], 0.7);
450
+ let i = invert_rigid(&m);
451
+ let id = mat4_mul(&m, &i);
452
+ for c in 0..4 {
453
+ for r in 0..4 {
454
+ let want = if c == r { 1.0 } else { 0.0 };
455
+ assert!((id[c][r] - want).abs() < 1e-3,
456
+ "M·M⁻¹ is not identity at [{c}][{r}]: {}", id[c][r]);
457
+ }
458
+ }
459
+ }
460
+
461
+ #[test]
462
+ fn compose_from_dir_points_y_along_the_bone() {
463
+ let dir = normalize([0.3, -0.9, 0.2]);
464
+ let m = compose_from_dir([1.0, 2.0, 3.0], dir);
465
+ // Column 1 is the +Y basis vector.
466
+ assert!((m[1][0] - dir[0]).abs() < 1e-5);
467
+ assert!((m[1][1] - dir[1]).abs() < 1e-5);
468
+ assert!((m[1][2] - dir[2]).abs() < 1e-5);
469
+ // And the frame stays orthonormal, or the capsule shears.
470
+ let right = [m[0][0], m[0][1], m[0][2]];
471
+ let dot = right[0] * dir[0] + right[1] * dir[1] + right[2] * dir[2];
472
+ assert!(dot.abs() < 1e-5, "basis is not orthogonal: {dot}");
473
+ }
474
+ }