@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.
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/native/android/Cargo.lock +1848 -0
- package/native/android/Cargo.toml +24 -0
- package/native/android/src/lib.rs +702 -0
- package/native/ios/Cargo.lock +1690 -0
- package/native/ios/Cargo.toml +32 -0
- package/native/ios/src/lib.rs +1267 -0
- package/native/linux/Cargo.lock +3279 -0
- package/native/linux/Cargo.toml +29 -0
- package/native/linux/src/lib.rs +1331 -0
- package/native/macos/Cargo.lock +3310 -0
- package/native/macos/Cargo.toml +46 -0
- package/native/macos/src/lib.rs +1302 -0
- package/native/shared/Cargo.lock +1899 -0
- package/native/shared/Cargo.toml +62 -0
- package/native/shared/assets/default_font.ttf +0 -0
- package/native/shared/build.rs +270 -0
- package/native/shared/shaders/common/clouds.wgsl +122 -0
- package/native/shared/shaders/common/fog.wgsl +16 -0
- package/native/shared/shaders/common/foliage_wind.wgsl +98 -0
- package/native/shared/shaders/common/imposter.wgsl +112 -0
- package/native/shared/shaders/common/pbr.wgsl +186 -0
- package/native/shared/shaders/common/shadows.wgsl +186 -0
- package/native/shared/shaders/common/sky.wgsl +8 -0
- package/native/shared/shaders/common/tonemap.wgsl +25 -0
- package/native/shared/shaders/impulse_field.wgsl +57 -0
- package/native/shared/shaders/material_abi.wgsl +383 -0
- package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
- package/native/shared/src/anim_mixer.rs +61 -0
- package/native/shared/src/attach.rs +263 -0
- package/native/shared/src/audio/decode.rs +123 -0
- package/native/shared/src/audio/mod.rs +863 -0
- package/native/shared/src/audio/render.rs +892 -0
- package/native/shared/src/audio/spsc.rs +156 -0
- package/native/shared/src/audio/stream.rs +226 -0
- package/native/shared/src/custom_shaders.rs +104 -0
- package/native/shared/src/decals.rs +245 -0
- package/native/shared/src/drs.rs +211 -0
- package/native/shared/src/engine.rs +261 -0
- package/native/shared/src/ffi.rs +116 -0
- package/native/shared/src/ffi_core/assets.rs +388 -0
- package/native/shared/src/ffi_core/audio_ffi.rs +184 -0
- package/native/shared/src/ffi_core/draw.rs +334 -0
- package/native/shared/src/ffi_core/game_loop.rs +577 -0
- package/native/shared/src/ffi_core/input.rs +234 -0
- package/native/shared/src/ffi_core/mod.rs +127 -0
- package/native/shared/src/ffi_core/models.rs +1154 -0
- package/native/shared/src/ffi_core/ragdoll_ffi.rs +261 -0
- package/native/shared/src/ffi_core/scene.rs +626 -0
- package/native/shared/src/ffi_core/vfx.rs +212 -0
- package/native/shared/src/ffi_core/visual.rs +691 -0
- package/native/shared/src/frame_callbacks.rs +122 -0
- package/native/shared/src/geometry.rs +236 -0
- package/native/shared/src/handles.rs +182 -0
- package/native/shared/src/input.rs +448 -0
- package/native/shared/src/jolt_sys.rs +822 -0
- package/native/shared/src/lib.rs +55 -0
- package/native/shared/src/models.rs +1093 -0
- package/native/shared/src/models_gltf.rs +1280 -0
- package/native/shared/src/particles.rs +391 -0
- package/native/shared/src/physics_jolt.rs +1908 -0
- package/native/shared/src/picking.rs +298 -0
- package/native/shared/src/postfx.rs +345 -0
- package/native/shared/src/profiler.rs +492 -0
- package/native/shared/src/ragdoll.rs +474 -0
- package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
- package/native/shared/src/renderer/brdf_lut.rs +154 -0
- package/native/shared/src/renderer/draw2d.rs +143 -0
- package/native/shared/src/renderer/formats.rs +822 -0
- package/native/shared/src/renderer/froxel.rs +421 -0
- package/native/shared/src/renderer/gi_bake.rs +653 -0
- package/native/shared/src/renderer/graph.rs +462 -0
- package/native/shared/src/renderer/hiz.rs +269 -0
- package/native/shared/src/renderer/hot_reload.rs +390 -0
- package/native/shared/src/renderer/impulse_field.rs +456 -0
- package/native/shared/src/renderer/lighting.rs +154 -0
- package/native/shared/src/renderer/material_instancing.rs +171 -0
- package/native/shared/src/renderer/material_pipeline.rs +700 -0
- package/native/shared/src/renderer/material_system.rs +1996 -0
- package/native/shared/src/renderer/material_system_tests.rs +601 -0
- package/native/shared/src/renderer/material_system_wasm.rs +41 -0
- package/native/shared/src/renderer/mod.rs +12556 -0
- package/native/shared/src/renderer/model_draw.rs +641 -0
- package/native/shared/src/renderer/occlusion.rs +429 -0
- package/native/shared/src/renderer/planar_pass.rs +593 -0
- package/native/shared/src/renderer/planar_reflection.rs +499 -0
- package/native/shared/src/renderer/post_pass.rs +249 -0
- package/native/shared/src/renderer/postfx_chain.rs +728 -0
- package/native/shared/src/renderer/pt_pass.rs +577 -0
- package/native/shared/src/renderer/scene_pass.rs +607 -0
- package/native/shared/src/renderer/shader_include.rs +205 -0
- package/native/shared/src/renderer/shader_library.rs +135 -0
- package/native/shared/src/renderer/shaders/ao.rs +570 -0
- package/native/shared/src/renderer/shaders/core.rs +1243 -0
- package/native/shared/src/renderer/shaders/env.rs +907 -0
- package/native/shared/src/renderer/shaders/gi.rs +810 -0
- package/native/shared/src/renderer/shaders/mod.rs +19 -0
- package/native/shared/src/renderer/shaders/post.rs +1558 -0
- package/native/shared/src/renderer/shaders/pt.rs +1859 -0
- package/native/shared/src/renderer/shaders/ssgi.rs +1586 -0
- package/native/shared/src/renderer/shadow_pass.rs +731 -0
- package/native/shared/src/renderer/ssgi_pass.rs +392 -0
- package/native/shared/src/renderer/ssr_pass.rs +188 -0
- package/native/shared/src/renderer/texture_store.rs +473 -0
- package/native/shared/src/renderer/transient.rs +591 -0
- package/native/shared/src/renderer/types.rs +941 -0
- package/native/shared/src/renderer/util.rs +152 -0
- package/native/shared/src/scene.rs +1362 -0
- package/native/shared/src/sdf_cache.rs +274 -0
- package/native/shared/src/shadows.rs +1036 -0
- package/native/shared/src/staging.rs +102 -0
- package/native/shared/src/string_header.rs +266 -0
- package/native/shared/src/text_renderer.rs +502 -0
- package/native/shared/src/textures.rs +197 -0
- package/native/tvos/Cargo.lock +1693 -0
- package/native/tvos/Cargo.toml +36 -0
- package/native/tvos/metal-patched/Cargo.toml +178 -0
- package/native/tvos/metal-patched/LICENSE-APACHE +201 -0
- package/native/tvos/metal-patched/LICENSE-MIT +25 -0
- package/native/tvos/metal-patched/src/acceleration_structure.rs +667 -0
- package/native/tvos/metal-patched/src/acceleration_structure_pass.rs +108 -0
- package/native/tvos/metal-patched/src/argument.rs +366 -0
- package/native/tvos/metal-patched/src/blitpass.rs +102 -0
- package/native/tvos/metal-patched/src/buffer.rs +71 -0
- package/native/tvos/metal-patched/src/capturedescriptor.rs +76 -0
- package/native/tvos/metal-patched/src/capturemanager.rs +113 -0
- package/native/tvos/metal-patched/src/commandbuffer.rs +192 -0
- package/native/tvos/metal-patched/src/commandqueue.rs +44 -0
- package/native/tvos/metal-patched/src/computepass.rs +107 -0
- package/native/tvos/metal-patched/src/constants.rs +152 -0
- package/native/tvos/metal-patched/src/counters.rs +119 -0
- package/native/tvos/metal-patched/src/depthstencil.rs +190 -0
- package/native/tvos/metal-patched/src/device.rs +2134 -0
- package/native/tvos/metal-patched/src/drawable.rs +39 -0
- package/native/tvos/metal-patched/src/encoder.rs +2041 -0
- package/native/tvos/metal-patched/src/heap.rs +281 -0
- package/native/tvos/metal-patched/src/indirect_encoder.rs +344 -0
- package/native/tvos/metal-patched/src/lib.rs +657 -0
- package/native/tvos/metal-patched/src/library.rs +902 -0
- package/native/tvos/metal-patched/src/mps.rs +575 -0
- package/native/tvos/metal-patched/src/pipeline/compute.rs +475 -0
- package/native/tvos/metal-patched/src/pipeline/mod.rs +71 -0
- package/native/tvos/metal-patched/src/pipeline/render.rs +762 -0
- package/native/tvos/metal-patched/src/renderpass.rs +443 -0
- package/native/tvos/metal-patched/src/resource.rs +182 -0
- package/native/tvos/metal-patched/src/sampler.rs +165 -0
- package/native/tvos/metal-patched/src/sync.rs +178 -0
- package/native/tvos/metal-patched/src/texture.rs +352 -0
- package/native/tvos/metal-patched/src/types.rs +90 -0
- package/native/tvos/metal-patched/src/vertexdescriptor.rs +250 -0
- package/native/tvos/src/audio_backend.rs +197 -0
- package/native/tvos/src/lib.rs +1891 -0
- package/native/visionos/Cargo.lock +1693 -0
- package/native/visionos/Cargo.toml +40 -0
- package/native/visionos/src/audio_backend.rs +197 -0
- package/native/visionos/src/lib.rs +1887 -0
- package/native/watchos/Cargo.lock +16 -0
- package/native/watchos/Cargo.toml +19 -0
- package/native/watchos/shaders/bloom_postfx.metal +99 -0
- package/native/watchos/src/BloomWatchApp.swift +1267 -0
- package/native/watchos/src/BloomWatchAudio.swift +179 -0
- package/native/watchos/src/audio.rs +55 -0
- package/native/watchos/src/draw_list.rs +229 -0
- package/native/watchos/src/ffi_stubs.rs +915 -0
- package/native/watchos/src/ffi_stubs_manual.rs +35 -0
- package/native/watchos/src/lib.rs +1124 -0
- package/native/watchos/src/models.rs +746 -0
- package/native/watchos/src/postfx.rs +95 -0
- package/native/watchos/src/scene.rs +534 -0
- package/native/watchos/src/textures.rs +184 -0
- package/native/web/Cargo.lock +1657 -0
- package/native/web/Cargo.toml +43 -0
- package/native/web/bloom_glue.js +695 -0
- package/native/web/build.sh +131 -0
- package/native/web/index.html +35 -0
- package/native/web/jolt_bridge.js +1519 -0
- package/native/web/src/input_ffi.rs +286 -0
- package/native/web/src/lib.rs +1796 -0
- package/native/web/src/material_ffi.rs +710 -0
- package/native/web/src/parity_ffi.rs +343 -0
- package/native/web/src/physics_ffi.rs +643 -0
- package/native/web/src/ragdoll_ffi.rs +250 -0
- package/native/web/src/render_settings.rs +98 -0
- package/native/windows/Cargo.lock +1815 -0
- package/native/windows/Cargo.toml +68 -0
- package/native/windows/src/lib.rs +1486 -0
- package/package.json +4279 -0
- package/src/audio/index.ts +315 -0
- package/src/core/colors.ts +63 -0
- package/src/core/index.ts +1206 -0
- package/src/core/keys.ts +63 -0
- package/src/core/types.ts +104 -0
- package/src/index.ts +171 -0
- package/src/math/index.ts +516 -0
- package/src/mobile/index.ts +294 -0
- package/src/models/index.ts +1258 -0
- package/src/physics/index.ts +1134 -0
- package/src/scene/index.ts +698 -0
- package/src/shapes/index.ts +120 -0
- package/src/text/index.ts +48 -0
- package/src/textures/index.ts +187 -0
- package/src/vfx/index.ts +191 -0
- package/src/world/index.ts +24 -0
- package/src/world/loader.ts +423 -0
- package/src/world/prefab.ts +217 -0
- package/src/world/render.ts +172 -0
- package/src/world/saver.ts +108 -0
- package/src/world/serialize.ts +301 -0
- package/src/world/terrain.ts +355 -0
- package/src/world/types.ts +160 -0
- package/src/world/validate.ts +319 -0
- 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
|
+
}
|