@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,1093 @@
|
|
|
1
|
+
use crate::handles::HandleRegistry;
|
|
2
|
+
use crate::renderer::Vertex3D;
|
|
3
|
+
use std::sync::Arc;
|
|
4
|
+
|
|
5
|
+
#[cfg(debug_assertions)]
|
|
6
|
+
static DEBUG_PRINTED: std::sync::Once = std::sync::Once::new();
|
|
7
|
+
|
|
8
|
+
pub struct MeshData {
|
|
9
|
+
pub vertices: Vec<Vertex3D>,
|
|
10
|
+
pub indices: Vec<u32>,
|
|
11
|
+
pub texture_idx: Option<u32>,
|
|
12
|
+
pub normal_texture_idx: Option<u32>,
|
|
13
|
+
pub metallic_roughness_texture_idx: Option<u32>,
|
|
14
|
+
pub emissive_texture_idx: Option<u32>,
|
|
15
|
+
pub occlusion_texture_idx: Option<u32>,
|
|
16
|
+
pub metallic_factor: f32,
|
|
17
|
+
pub roughness_factor: f32,
|
|
18
|
+
pub emissive_factor: [f32; 3],
|
|
19
|
+
/// glTF alpha cutoff for MASK mode — fragments with base-colour
|
|
20
|
+
/// alpha below this are discarded. 0.0 means OPAQUE mode (no
|
|
21
|
+
/// discard); glTF spec default for MASK is 0.5. BLEND mode is
|
|
22
|
+
/// currently treated as MASK @ 0.5 since we don't have a sorted
|
|
23
|
+
/// transparent pipeline yet.
|
|
24
|
+
pub alpha_cutoff: f32,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
pub struct ModelData {
|
|
28
|
+
pub meshes: Vec<MeshData>,
|
|
29
|
+
pub bbox_min: [f32; 3],
|
|
30
|
+
pub bbox_max: [f32; 3],
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pub struct JointData {
|
|
34
|
+
pub inverse_bind: [[f32; 4]; 4],
|
|
35
|
+
pub children: Vec<usize>,
|
|
36
|
+
pub name: String,
|
|
37
|
+
pub rest_translation: [f32; 3],
|
|
38
|
+
pub rest_rotation: [f32; 4],
|
|
39
|
+
pub rest_scale: [f32; 3],
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pub struct AnimationChannel {
|
|
43
|
+
pub joint_index: usize,
|
|
44
|
+
pub timestamps: Vec<f32>,
|
|
45
|
+
pub translations: Vec<[f32; 3]>,
|
|
46
|
+
pub rotation_timestamps: Vec<f32>,
|
|
47
|
+
pub rotations: Vec<[f32; 4]>,
|
|
48
|
+
pub scale_timestamps: Vec<f32>,
|
|
49
|
+
pub scales: Vec<[f32; 3]>,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
pub struct AnimationData {
|
|
53
|
+
pub channels: Vec<AnimationChannel>,
|
|
54
|
+
pub duration: f32,
|
|
55
|
+
pub name: String,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
pub struct SkeletonData {
|
|
59
|
+
pub joints: Vec<JointData>,
|
|
60
|
+
pub root_joints: Vec<usize>,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
pub use crate::anim_mixer::AnimMixer;
|
|
64
|
+
|
|
65
|
+
// glTF/GLB parsing lives in models_gltf.rs, models3d-gated so builds
|
|
66
|
+
// without the feature drop the gltf/image_dds deps (EN-014, re-fixed by
|
|
67
|
+
// EN-063). Everything else in this file compiles in every configuration.
|
|
68
|
+
#[cfg(feature = "models3d")]
|
|
69
|
+
#[path = "models_gltf.rs"]
|
|
70
|
+
mod gltf_load;
|
|
71
|
+
#[cfg(feature = "models3d")]
|
|
72
|
+
pub use gltf_load::load_gltf_staged;
|
|
73
|
+
|
|
74
|
+
pub struct ModelAnimation {
|
|
75
|
+
/// EN-055 — the parsed clip data (skeleton, keyframe tracks, rest
|
|
76
|
+
/// rotations) is IMMUTABLE after load and shared between instances via
|
|
77
|
+
/// `Arc`: `instantiate_animation` clones the handles, not the data. Only
|
|
78
|
+
/// the fields below the shared block are per-instance state.
|
|
79
|
+
pub skeleton: Option<Arc<SkeletonData>>,
|
|
80
|
+
pub animations: Arc<Vec<AnimationData>>,
|
|
81
|
+
/// Reference rest-pose rotations (from first animation, sampled at t=0).
|
|
82
|
+
/// Used for retargeting when multiple armatures have different rest orientations.
|
|
83
|
+
pub ref_rest_rotations: Option<Arc<Vec<[f32; 4]>>>,
|
|
84
|
+
// ---- per-instance state from here down ---------------------------------
|
|
85
|
+
pub joint_matrices: Vec<[[f32; 4]; 4]>,
|
|
86
|
+
/// EN-028 mixer state.
|
|
87
|
+
pub mixer: AnimMixer,
|
|
88
|
+
/// EN-033 — joint world transforms *before* the inverse-bind multiply.
|
|
89
|
+
/// `joint_matrices` is skinning-space and useless for attaching props;
|
|
90
|
+
/// this is the model-space transform a socket actually wants.
|
|
91
|
+
pub joint_world: Vec<[[f32; 4]; 4]>,
|
|
92
|
+
/// Cached per-joint layer weights, rebuilt when `layer_mask_root` changes.
|
|
93
|
+
pub mask_weights: Vec<f32>,
|
|
94
|
+
pub mask_cached_root: i32,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
pub struct ModelManager {
|
|
98
|
+
pub models: HandleRegistry<ModelData>,
|
|
99
|
+
pub animations: HandleRegistry<ModelAnimation>,
|
|
100
|
+
/// Scratch buffers for the array-free mesh-upload path. Perry 0.5.1171
|
|
101
|
+
/// rejects passing a JS `number[]` to a native `i64` pointer param
|
|
102
|
+
/// (strict safe-integer check), so `createMesh` instead pushes vertex
|
|
103
|
+
/// floats / indices one scalar at a time through `mesh_scratch_push_*`
|
|
104
|
+
/// (all `f64` ABI) and then builds the mesh from these. Mirrors the
|
|
105
|
+
/// physics subsystem's `scratch_*` shape-upload path.
|
|
106
|
+
pub scratch_f32: Vec<f32>,
|
|
107
|
+
pub scratch_u32: Vec<u32>,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
impl ModelManager {
|
|
111
|
+
pub fn new() -> Self {
|
|
112
|
+
Self {
|
|
113
|
+
models: HandleRegistry::new(),
|
|
114
|
+
animations: HandleRegistry::new(),
|
|
115
|
+
scratch_f32: Vec::new(),
|
|
116
|
+
scratch_u32: Vec::new(),
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
pub fn mesh_scratch_reset(&mut self) {
|
|
121
|
+
self.scratch_f32.clear();
|
|
122
|
+
self.scratch_u32.clear();
|
|
123
|
+
}
|
|
124
|
+
pub fn mesh_scratch_push_f32(&mut self, v: f32) { self.scratch_f32.push(v); }
|
|
125
|
+
pub fn mesh_scratch_push_u32(&mut self, v: u32) { self.scratch_u32.push(v); }
|
|
126
|
+
|
|
127
|
+
/// Build a mesh from the scratch buffers: `vertex_count` vertices of 12
|
|
128
|
+
/// floats each in `scratch_f32`, `index_count` indices in `scratch_u32`.
|
|
129
|
+
pub fn create_mesh_from_scratch(&mut self, vertex_count: u32, index_count: u32) -> f64 {
|
|
130
|
+
let need_f = vertex_count as usize * 12;
|
|
131
|
+
let need_u = index_count as usize;
|
|
132
|
+
if vertex_count == 0 || self.scratch_f32.len() < need_f || self.scratch_u32.len() < need_u {
|
|
133
|
+
return 0.0;
|
|
134
|
+
}
|
|
135
|
+
// Clone out so create_mesh's &self borrow doesn't alias scratch.
|
|
136
|
+
let verts: Vec<f32> = self.scratch_f32[..need_f].to_vec();
|
|
137
|
+
let inds: Vec<u32> = self.scratch_u32[..need_u].to_vec();
|
|
138
|
+
self.create_mesh(&verts, &inds)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Read-only view of the f32 scratch buffer, for consumers that lay their
|
|
142
|
+
/// own data out in it (the spline ribbon packs positions then widths).
|
|
143
|
+
pub fn scratch_floats(&self) -> &[f32] {
|
|
144
|
+
&self.scratch_f32
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// Take the scratch buffers as raw vertex floats + indices, for callers
|
|
148
|
+
/// that build something other than a Model out of them (the scene graph's
|
|
149
|
+
/// `update_geometry`). Same 12-floats-per-vertex layout as
|
|
150
|
+
/// `create_mesh_from_scratch`; returns None if the scratch is short.
|
|
151
|
+
pub fn take_scratch_geometry(
|
|
152
|
+
&self,
|
|
153
|
+
vertex_count: u32,
|
|
154
|
+
index_count: u32,
|
|
155
|
+
) -> Option<(Vec<f32>, Vec<u32>)> {
|
|
156
|
+
let need_f = vertex_count as usize * 12;
|
|
157
|
+
let need_u = index_count as usize;
|
|
158
|
+
if vertex_count == 0 || self.scratch_f32.len() < need_f || self.scratch_u32.len() < need_u {
|
|
159
|
+
return None;
|
|
160
|
+
}
|
|
161
|
+
Some((
|
|
162
|
+
self.scratch_f32[..need_f].to_vec(),
|
|
163
|
+
self.scratch_u32[..need_u].to_vec(),
|
|
164
|
+
))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[cfg(feature = "models3d")]
|
|
168
|
+
pub fn load_model(&mut self, file_data: &[u8]) -> f64 {
|
|
169
|
+
match gltf_load::load_gltf(file_data) {
|
|
170
|
+
Some(model) => self.models.alloc(model),
|
|
171
|
+
None => 0.0,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#[cfg(feature = "models3d")]
|
|
176
|
+
pub fn load_model_with_textures(&mut self, file_data: &[u8], renderer: &mut crate::renderer::Renderer) -> f64 {
|
|
177
|
+
match gltf_load::load_gltf_with_textures(file_data, renderer, None) {
|
|
178
|
+
Some(model) => self.models.alloc(model),
|
|
179
|
+
None => 0.0,
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// Like `load_model_with_textures` but also resolves external `.bin`
|
|
184
|
+
/// and image URIs relative to `base_dir` — required for loose glTF
|
|
185
|
+
/// files (as opposed to single-file .glb). Intel Sponza etc.
|
|
186
|
+
#[cfg(feature = "models3d")]
|
|
187
|
+
pub fn load_model_with_textures_from_path(
|
|
188
|
+
&mut self,
|
|
189
|
+
file_data: &[u8],
|
|
190
|
+
base_dir: &std::path::Path,
|
|
191
|
+
renderer: &mut crate::renderer::Renderer,
|
|
192
|
+
) -> f64 {
|
|
193
|
+
match gltf_load::load_gltf_with_textures(file_data, renderer, Some(base_dir)) {
|
|
194
|
+
Some(model) => self.models.alloc(model),
|
|
195
|
+
None => 0.0,
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
pub fn get(&self, handle: f64) -> Option<&ModelData> {
|
|
200
|
+
self.models.get(handle)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/// Return the axis-aligned bounding box of a loaded model as
|
|
204
|
+
/// `(min_xyz, max_xyz)`. Used by editors to size move/rotate gizmos,
|
|
205
|
+
/// auto-frame the camera on selection, and snap placed entities onto
|
|
206
|
+
/// terrain. Returns the origin for unknown handles so callers can read
|
|
207
|
+
/// without checking for None.
|
|
208
|
+
pub fn get_bounds(&self, handle: f64) -> ([f32; 3], [f32; 3]) {
|
|
209
|
+
match self.models.get(handle) {
|
|
210
|
+
Some(model) => (model.bbox_min, model.bbox_max),
|
|
211
|
+
None => ([0.0, 0.0, 0.0], [0.0, 0.0, 0.0]),
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/// EN-025 — the ragdoll writes joint matrices directly, bypassing the
|
|
216
|
+
/// sampler entirely: once a thing is dead, physics owns its pose.
|
|
217
|
+
pub fn get_animation_mut(&mut self, handle: f64) -> Option<&mut ModelAnimation> {
|
|
218
|
+
self.animations.get_mut(handle)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
pub fn get_animation(&self, handle: f64) -> Option<&ModelAnimation> {
|
|
222
|
+
self.animations.get(handle)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
pub fn unload_model(&mut self, handle: f64) {
|
|
226
|
+
self.models.free(handle);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
pub fn gen_mesh_cube(&mut self, w: f32, h: f32, d: f32) -> f64 {
|
|
230
|
+
let hw = w * 0.5;
|
|
231
|
+
let hh = h * 0.5;
|
|
232
|
+
let hd = d * 0.5;
|
|
233
|
+
let white = [1.0, 1.0, 1.0, 1.0];
|
|
234
|
+
|
|
235
|
+
#[rustfmt::skip]
|
|
236
|
+
let faces: &[([f32; 3], [f32; 3], [f32; 2])] = &[
|
|
237
|
+
// Front face (+Z)
|
|
238
|
+
([-hw, -hh, hd], [0.0, 0.0, 1.0], [0.0, 1.0]),
|
|
239
|
+
([ hw, -hh, hd], [0.0, 0.0, 1.0], [1.0, 1.0]),
|
|
240
|
+
([ hw, hh, hd], [0.0, 0.0, 1.0], [1.0, 0.0]),
|
|
241
|
+
([-hw, hh, hd], [0.0, 0.0, 1.0], [0.0, 0.0]),
|
|
242
|
+
// Back face (-Z)
|
|
243
|
+
([ hw, -hh, -hd], [0.0, 0.0, -1.0], [0.0, 1.0]),
|
|
244
|
+
([-hw, -hh, -hd], [0.0, 0.0, -1.0], [1.0, 1.0]),
|
|
245
|
+
([-hw, hh, -hd], [0.0, 0.0, -1.0], [1.0, 0.0]),
|
|
246
|
+
([ hw, hh, -hd], [0.0, 0.0, -1.0], [0.0, 0.0]),
|
|
247
|
+
// Right face (+X)
|
|
248
|
+
([ hw, -hh, hd], [1.0, 0.0, 0.0], [0.0, 1.0]),
|
|
249
|
+
([ hw, -hh, -hd], [1.0, 0.0, 0.0], [1.0, 1.0]),
|
|
250
|
+
([ hw, hh, -hd], [1.0, 0.0, 0.0], [1.0, 0.0]),
|
|
251
|
+
([ hw, hh, hd], [1.0, 0.0, 0.0], [0.0, 0.0]),
|
|
252
|
+
// Left face (-X)
|
|
253
|
+
([-hw, -hh, -hd], [-1.0, 0.0, 0.0], [0.0, 1.0]),
|
|
254
|
+
([-hw, -hh, hd], [-1.0, 0.0, 0.0], [1.0, 1.0]),
|
|
255
|
+
([-hw, hh, hd], [-1.0, 0.0, 0.0], [1.0, 0.0]),
|
|
256
|
+
([-hw, hh, -hd], [-1.0, 0.0, 0.0], [0.0, 0.0]),
|
|
257
|
+
// Top face (+Y)
|
|
258
|
+
([-hw, hh, hd], [0.0, 1.0, 0.0], [0.0, 1.0]),
|
|
259
|
+
([ hw, hh, hd], [0.0, 1.0, 0.0], [1.0, 1.0]),
|
|
260
|
+
([ hw, hh, -hd], [0.0, 1.0, 0.0], [1.0, 0.0]),
|
|
261
|
+
([-hw, hh, -hd], [0.0, 1.0, 0.0], [0.0, 0.0]),
|
|
262
|
+
// Bottom face (-Y)
|
|
263
|
+
([-hw, -hh, -hd], [0.0, -1.0, 0.0], [0.0, 1.0]),
|
|
264
|
+
([ hw, -hh, -hd], [0.0, -1.0, 0.0], [1.0, 1.0]),
|
|
265
|
+
([ hw, -hh, hd], [0.0, -1.0, 0.0], [1.0, 0.0]),
|
|
266
|
+
([-hw, -hh, hd], [0.0, -1.0, 0.0], [0.0, 0.0]),
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
let vertices: Vec<Vertex3D> = faces.iter().map(|(pos, norm, uv)| Vertex3D {
|
|
270
|
+
position: *pos,
|
|
271
|
+
normal: *norm,
|
|
272
|
+
color: white,
|
|
273
|
+
uv: *uv,
|
|
274
|
+
joints: [0.0; 4],
|
|
275
|
+
weights: [0.0; 4],
|
|
276
|
+
tangent: [0.0; 4],
|
|
277
|
+
}).collect();
|
|
278
|
+
|
|
279
|
+
let mut indices = Vec::with_capacity(36);
|
|
280
|
+
for face in 0..6u32 {
|
|
281
|
+
let base = face * 4;
|
|
282
|
+
indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let model = ModelData {
|
|
286
|
+
meshes: vec![MeshData { vertices, indices, texture_idx: None, normal_texture_idx: None, metallic_roughness_texture_idx: None, emissive_texture_idx: None, occlusion_texture_idx: None, metallic_factor: 0.0, roughness_factor: 1.0, emissive_factor: [0.0; 3], alpha_cutoff: 0.0 }],
|
|
287
|
+
bbox_min: [-hw, -hh, -hd],
|
|
288
|
+
bbox_max: [hw, hh, hd],
|
|
289
|
+
};
|
|
290
|
+
self.models.alloc(model)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
pub fn gen_mesh_heightmap(&mut self, image_data: &[u8], img_w: u32, img_h: u32, size_x: f32, size_y: f32, size_z: f32) -> f64 {
|
|
294
|
+
let cols = img_w as usize;
|
|
295
|
+
let rows = img_h as usize;
|
|
296
|
+
if cols < 2 || rows < 2 { return 0.0; }
|
|
297
|
+
|
|
298
|
+
let mut vertices = Vec::with_capacity(cols * rows);
|
|
299
|
+
let white = [1.0, 1.0, 1.0, 1.0];
|
|
300
|
+
|
|
301
|
+
for z in 0..rows {
|
|
302
|
+
for x in 0..cols {
|
|
303
|
+
let pixel_idx = (z * cols + x) * 4;
|
|
304
|
+
let luminance = if pixel_idx + 2 < image_data.len() {
|
|
305
|
+
(image_data[pixel_idx] as f32 * 0.299
|
|
306
|
+
+ image_data[pixel_idx + 1] as f32 * 0.587
|
|
307
|
+
+ image_data[pixel_idx + 2] as f32 * 0.114) / 255.0
|
|
308
|
+
} else {
|
|
309
|
+
0.0
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
let px = (x as f32 / (cols - 1) as f32 - 0.5) * size_x;
|
|
313
|
+
let py = luminance * size_y;
|
|
314
|
+
let pz = (z as f32 / (rows - 1) as f32 - 0.5) * size_z;
|
|
315
|
+
let u = x as f32 / (cols - 1) as f32;
|
|
316
|
+
let v = z as f32 / (rows - 1) as f32;
|
|
317
|
+
|
|
318
|
+
vertices.push(Vertex3D {
|
|
319
|
+
position: [px, py, pz],
|
|
320
|
+
normal: [0.0, 1.0, 0.0],
|
|
321
|
+
color: white,
|
|
322
|
+
uv: [u, v],
|
|
323
|
+
joints: [0.0; 4],
|
|
324
|
+
weights: [0.0; 4],
|
|
325
|
+
tangent: [0.0; 4],
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Compute normals from neighboring heights
|
|
331
|
+
for z in 0..rows {
|
|
332
|
+
for x in 0..cols {
|
|
333
|
+
let idx = z * cols + x;
|
|
334
|
+
let left = if x > 0 { vertices[z * cols + x - 1].position[1] } else { vertices[idx].position[1] };
|
|
335
|
+
let right = if x < cols - 1 { vertices[z * cols + x + 1].position[1] } else { vertices[idx].position[1] };
|
|
336
|
+
let up = if z > 0 { vertices[(z - 1) * cols + x].position[1] } else { vertices[idx].position[1] };
|
|
337
|
+
let down = if z < rows - 1 { vertices[(z + 1) * cols + x].position[1] } else { vertices[idx].position[1] };
|
|
338
|
+
let sx = size_x / (cols - 1) as f32;
|
|
339
|
+
let sz = size_z / (rows - 1) as f32;
|
|
340
|
+
let nx = (left - right) / (2.0 * sx);
|
|
341
|
+
let nz = (up - down) / (2.0 * sz);
|
|
342
|
+
let len = (nx * nx + 1.0 + nz * nz).sqrt();
|
|
343
|
+
vertices[idx].normal = [nx / len, 1.0 / len, nz / len];
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
let mut indices = Vec::with_capacity((cols - 1) * (rows - 1) * 6);
|
|
348
|
+
for z in 0..rows - 1 {
|
|
349
|
+
for x in 0..cols - 1 {
|
|
350
|
+
let tl = (z * cols + x) as u32;
|
|
351
|
+
let tr = tl + 1;
|
|
352
|
+
let bl = ((z + 1) * cols + x) as u32;
|
|
353
|
+
let br = bl + 1;
|
|
354
|
+
indices.extend_from_slice(&[tl, bl, tr, tr, bl, br]);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let model = ModelData {
|
|
359
|
+
meshes: vec![MeshData { vertices, indices, texture_idx: None, normal_texture_idx: None, metallic_roughness_texture_idx: None, emissive_texture_idx: None, occlusion_texture_idx: None, metallic_factor: 0.0, roughness_factor: 1.0, emissive_factor: [0.0; 3], alpha_cutoff: 0.0 }],
|
|
360
|
+
bbox_min: [-size_x * 0.5, 0.0, -size_z * 0.5],
|
|
361
|
+
bbox_max: [size_x * 0.5, size_y, size_z * 0.5],
|
|
362
|
+
};
|
|
363
|
+
self.models.alloc(model)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/// Create a mesh from raw float data passed from TS.
|
|
367
|
+
/// vertex_data layout: [x,y,z, nx,ny,nz, r,g,b,a, u,v] per vertex (12 floats each)
|
|
368
|
+
pub fn create_mesh(&mut self, vertex_data: &[f32], index_data: &[u32]) -> f64 {
|
|
369
|
+
let floats_per_vert = 12;
|
|
370
|
+
let vert_count = vertex_data.len() / floats_per_vert;
|
|
371
|
+
if vert_count == 0 { return 0.0; }
|
|
372
|
+
|
|
373
|
+
let mut vertices = Vec::with_capacity(vert_count);
|
|
374
|
+
let mut bbox_min = [f32::MAX; 3];
|
|
375
|
+
let mut bbox_max = [f32::MIN; 3];
|
|
376
|
+
|
|
377
|
+
for i in 0..vert_count {
|
|
378
|
+
let o = i * floats_per_vert;
|
|
379
|
+
let pos = [vertex_data[o], vertex_data[o+1], vertex_data[o+2]];
|
|
380
|
+
for k in 0..3 {
|
|
381
|
+
if pos[k] < bbox_min[k] { bbox_min[k] = pos[k]; }
|
|
382
|
+
if pos[k] > bbox_max[k] { bbox_max[k] = pos[k]; }
|
|
383
|
+
}
|
|
384
|
+
vertices.push(Vertex3D {
|
|
385
|
+
position: pos,
|
|
386
|
+
normal: [vertex_data[o+3], vertex_data[o+4], vertex_data[o+5]],
|
|
387
|
+
color: [vertex_data[o+6], vertex_data[o+7], vertex_data[o+8], vertex_data[o+9]],
|
|
388
|
+
uv: [vertex_data[o+10], vertex_data[o+11]],
|
|
389
|
+
joints: [0.0; 4],
|
|
390
|
+
weights: [0.0; 4],
|
|
391
|
+
tangent: [0.0; 4],
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
let indices = index_data.to_vec();
|
|
396
|
+
let model = ModelData {
|
|
397
|
+
meshes: vec![MeshData { vertices, indices, texture_idx: None, normal_texture_idx: None, metallic_roughness_texture_idx: None, emissive_texture_idx: None, occlusion_texture_idx: None, metallic_factor: 0.0, roughness_factor: 1.0, emissive_factor: [0.0; 3], alpha_cutoff: 0.0 }],
|
|
398
|
+
bbox_min,
|
|
399
|
+
bbox_max,
|
|
400
|
+
};
|
|
401
|
+
self.models.alloc(model)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/// Q9: Generate a ribbon mesh along a Catmull-Rom spline. Used by the
|
|
405
|
+
/// editor's river tool. `points` is flat [x0,y0,z0, x1,y1,z1, ...],
|
|
406
|
+
/// `widths` has one width per control point.
|
|
407
|
+
pub fn gen_mesh_spline_ribbon(&mut self, points: &[f32], widths: &[f32]) -> f64 {
|
|
408
|
+
let n = points.len() / 3;
|
|
409
|
+
if n < 2 || widths.len() < n { return 0.0; }
|
|
410
|
+
|
|
411
|
+
// Evaluate Catmull-Rom at fine intervals.
|
|
412
|
+
let segments = (n - 1) * 8; // 8 subdivisions per segment.
|
|
413
|
+
let mut center_pts: Vec<[f32; 3]> = Vec::with_capacity(segments + 1);
|
|
414
|
+
let mut center_widths: Vec<f32> = Vec::with_capacity(segments + 1);
|
|
415
|
+
|
|
416
|
+
for i in 0..n - 1 {
|
|
417
|
+
for sub in 0..8 {
|
|
418
|
+
let t = sub as f32 / 8.0;
|
|
419
|
+
let p = catmull_rom_point(points, n, i, t);
|
|
420
|
+
let w = widths[i] * (1.0 - t) + widths[i + 1] * t;
|
|
421
|
+
center_pts.push(p);
|
|
422
|
+
center_widths.push(w);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// Add the last point.
|
|
426
|
+
let last = n - 1;
|
|
427
|
+
center_pts.push([points[last * 3], points[last * 3 + 1], points[last * 3 + 2]]);
|
|
428
|
+
center_widths.push(widths[last]);
|
|
429
|
+
|
|
430
|
+
// Build ribbon vertices (two per center point: left and right).
|
|
431
|
+
let ribbon_len = center_pts.len();
|
|
432
|
+
let mut vertices = Vec::with_capacity(ribbon_len * 2);
|
|
433
|
+
let mut bbox_min = [f32::MAX; 3];
|
|
434
|
+
let mut bbox_max = [f32::MIN; 3];
|
|
435
|
+
let white = [0.3, 0.5, 0.8, 0.7]; // Water-blue tint.
|
|
436
|
+
|
|
437
|
+
for i in 0..ribbon_len {
|
|
438
|
+
// Tangent direction.
|
|
439
|
+
let tangent = if i < ribbon_len - 1 {
|
|
440
|
+
let dx = center_pts[i + 1][0] - center_pts[i][0];
|
|
441
|
+
let dz = center_pts[i + 1][2] - center_pts[i][2];
|
|
442
|
+
let len = (dx * dx + dz * dz).sqrt().max(1e-6);
|
|
443
|
+
[dx / len, dz / len]
|
|
444
|
+
} else if i > 0 {
|
|
445
|
+
let dx = center_pts[i][0] - center_pts[i - 1][0];
|
|
446
|
+
let dz = center_pts[i][2] - center_pts[i - 1][2];
|
|
447
|
+
let len = (dx * dx + dz * dz).sqrt().max(1e-6);
|
|
448
|
+
[dx / len, dz / len]
|
|
449
|
+
} else {
|
|
450
|
+
[0.0, 1.0]
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// Perpendicular in XZ plane (rotate tangent 90 degrees).
|
|
454
|
+
let perp = [-tangent[1], tangent[0]];
|
|
455
|
+
let hw = center_widths[i] * 0.5;
|
|
456
|
+
let cp = center_pts[i];
|
|
457
|
+
let u = i as f32 / (ribbon_len - 1).max(1) as f32;
|
|
458
|
+
|
|
459
|
+
// Left vertex.
|
|
460
|
+
let lx = cp[0] + perp[0] * hw;
|
|
461
|
+
let ly = cp[1];
|
|
462
|
+
let lz = cp[2] + perp[1] * hw;
|
|
463
|
+
update_bounds(&mut bbox_min, &mut bbox_max, lx, ly, lz);
|
|
464
|
+
vertices.push(Vertex3D {
|
|
465
|
+
position: [lx, ly, lz],
|
|
466
|
+
normal: [0.0, 1.0, 0.0],
|
|
467
|
+
color: white,
|
|
468
|
+
uv: [u, 0.0],
|
|
469
|
+
joints: [0.0; 4],
|
|
470
|
+
weights: [0.0; 4],
|
|
471
|
+
tangent: [0.0; 4],
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// Right vertex.
|
|
475
|
+
let rx = cp[0] - perp[0] * hw;
|
|
476
|
+
let ry = cp[1];
|
|
477
|
+
let rz = cp[2] - perp[1] * hw;
|
|
478
|
+
update_bounds(&mut bbox_min, &mut bbox_max, rx, ry, rz);
|
|
479
|
+
vertices.push(Vertex3D {
|
|
480
|
+
position: [rx, ry, rz],
|
|
481
|
+
normal: [0.0, 1.0, 0.0],
|
|
482
|
+
color: white,
|
|
483
|
+
uv: [u, 1.0],
|
|
484
|
+
joints: [0.0; 4],
|
|
485
|
+
weights: [0.0; 4],
|
|
486
|
+
tangent: [0.0; 4],
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Triangle strip indices.
|
|
491
|
+
let mut indices = Vec::with_capacity((ribbon_len - 1) * 6);
|
|
492
|
+
for i in 0..(ribbon_len - 1) as u32 {
|
|
493
|
+
let bl = i * 2;
|
|
494
|
+
let br = bl + 1;
|
|
495
|
+
let tl = bl + 2;
|
|
496
|
+
let tr = bl + 3;
|
|
497
|
+
indices.extend_from_slice(&[bl, tl, br, br, tl, tr]);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if vertices.is_empty() {
|
|
501
|
+
bbox_min = [0.0; 3];
|
|
502
|
+
bbox_max = [0.0; 3];
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
let model = ModelData {
|
|
506
|
+
meshes: vec![MeshData { vertices, indices, texture_idx: None, normal_texture_idx: None, metallic_roughness_texture_idx: None, emissive_texture_idx: None, occlusion_texture_idx: None, metallic_factor: 0.0, roughness_factor: 1.0, emissive_factor: [0.0; 3], alpha_cutoff: 0.0 }],
|
|
507
|
+
bbox_min,
|
|
508
|
+
bbox_max,
|
|
509
|
+
};
|
|
510
|
+
self.models.alloc(model)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
#[cfg(feature = "models3d")]
|
|
514
|
+
pub fn load_model_animation(&mut self, file_data: &[u8]) -> f64 {
|
|
515
|
+
match gltf_load::load_gltf_animation(file_data) {
|
|
516
|
+
Some(anim) => self.animations.alloc(anim),
|
|
517
|
+
None => 0.0,
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/// EN-055 — a new animation INSTANCE over an already-loaded clip set.
|
|
522
|
+
/// The parsed data (skeleton, keyframe tracks, rest rotations) is shared
|
|
523
|
+
/// via `Arc`; the mixer, joint matrices and mask cache are fresh — so N
|
|
524
|
+
/// characters get independent clocks/fades without N GLB re-parses
|
|
525
|
+
/// (which was 5.5 s of the shooter's 8 s boot). Returns 0 for a dead
|
|
526
|
+
/// source handle, mirroring load's failure convention.
|
|
527
|
+
pub fn instantiate_animation(&mut self, src: f64) -> f64 {
|
|
528
|
+
let inst = match self.animations.get(src) {
|
|
529
|
+
Some(a) => ModelAnimation {
|
|
530
|
+
skeleton: a.skeleton.clone(),
|
|
531
|
+
animations: a.animations.clone(),
|
|
532
|
+
ref_rest_rotations: a.ref_rest_rotations.clone(),
|
|
533
|
+
joint_matrices: a.joint_matrices.clone(),
|
|
534
|
+
mixer: AnimMixer::default(),
|
|
535
|
+
joint_world: a.joint_world.clone(),
|
|
536
|
+
mask_weights: vec![0.0; a.mask_weights.len()],
|
|
537
|
+
mask_cached_root: -1,
|
|
538
|
+
},
|
|
539
|
+
None => return 0.0,
|
|
540
|
+
};
|
|
541
|
+
self.animations.alloc(inst)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
pub fn update_model_animation(&mut self, handle: f64, anim_index: usize, time: f32) {
|
|
545
|
+
if let Some(model_anim) = self.animations.get_mut(handle) {
|
|
546
|
+
let skeleton = match &model_anim.skeleton {
|
|
547
|
+
Some(s) => s,
|
|
548
|
+
None => return,
|
|
549
|
+
};
|
|
550
|
+
if anim_index >= model_anim.animations.len() { return; }
|
|
551
|
+
#[cfg(debug_assertions)]
|
|
552
|
+
let joint_count = skeleton.joints.len();
|
|
553
|
+
|
|
554
|
+
let pose = sample_local_pose(skeleton, &model_anim.animations[anim_index], time, true);
|
|
555
|
+
model_anim.apply_pose(&pose);
|
|
556
|
+
|
|
557
|
+
#[cfg(debug_assertions)]
|
|
558
|
+
{
|
|
559
|
+
if joint_count > 0 {
|
|
560
|
+
DEBUG_PRINTED.call_once(|| {
|
|
561
|
+
eprintln!(
|
|
562
|
+
"[anim] joints={}, t={:.3}, anim_index={}",
|
|
563
|
+
joint_count, time, anim_index
|
|
564
|
+
);
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// ---- EN-028 mixer -----------------------------------------------------
|
|
572
|
+
|
|
573
|
+
/// Start a transition to `clip`. Re-requesting the clip already playing is
|
|
574
|
+
/// a no-op, so game code can call this unconditionally every frame ("I
|
|
575
|
+
/// want to be walking") instead of tracking edges itself — which is how
|
|
576
|
+
/// this gets used in practice and where the pops came from before.
|
|
577
|
+
pub fn anim_play(&mut self, handle: f64, clip: usize, fade: f32, speed: f32, looping: bool) {
|
|
578
|
+
if let Some(ma) = self.animations.get_mut(handle) {
|
|
579
|
+
if clip >= ma.animations.len() { return; }
|
|
580
|
+
let m = &mut ma.mixer;
|
|
581
|
+
// Re-requesting the clip that is ALREADY current is a no-op by
|
|
582
|
+
// contract ("safe to call every frame with the clip you want"), and
|
|
583
|
+
// that has to hold DURING a fade too — a fade is a fade *to*
|
|
584
|
+
// cur_clip, so the right thing is to let it finish.
|
|
585
|
+
//
|
|
586
|
+
// This used to also require `m.fade_dur <= 0.0`. A caller doing the
|
|
587
|
+
// documented thing then fell through to the restart path on every
|
|
588
|
+
// frame of the fade: it re-seeded the fade (fade_t = 0, so the fade
|
|
589
|
+
// could never complete) and reset cur_time = 0. The base clip was
|
|
590
|
+
// pinned at t≈0 forever, from the first clip change onward — every
|
|
591
|
+
// animated character in a game froze the moment it stopped idling.
|
|
592
|
+
if m.started && m.cur_clip == clip {
|
|
593
|
+
m.cur_speed = speed;
|
|
594
|
+
m.cur_loop = looping;
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if m.started && fade > 0.0 {
|
|
598
|
+
m.prev_clip = m.cur_clip;
|
|
599
|
+
m.prev_time = m.cur_time;
|
|
600
|
+
m.prev_speed = m.cur_speed;
|
|
601
|
+
m.prev_loop = m.cur_loop;
|
|
602
|
+
m.fade_dur = fade;
|
|
603
|
+
m.fade_t = 0.0;
|
|
604
|
+
} else {
|
|
605
|
+
m.fade_dur = 0.0;
|
|
606
|
+
m.fade_t = 0.0;
|
|
607
|
+
}
|
|
608
|
+
m.cur_clip = clip;
|
|
609
|
+
m.cur_time = 0.0;
|
|
610
|
+
m.cur_speed = speed;
|
|
611
|
+
m.cur_loop = looping;
|
|
612
|
+
m.finished = false;
|
|
613
|
+
m.started = true;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/// Masked layer: `clip` drives every joint at or below `mask_root`,
|
|
618
|
+
/// blended in by `weight`. weight <= 0 (or clip < 0) turns it off.
|
|
619
|
+
pub fn anim_set_layer(&mut self, handle: f64, clip: i32, weight: f32, mask_root: i32, speed: f32, looping: bool) {
|
|
620
|
+
if let Some(ma) = self.animations.get_mut(handle) {
|
|
621
|
+
let m = &mut ma.mixer;
|
|
622
|
+
let off = clip < 0 || weight <= 0.0 || (clip as usize) >= ma.animations.len();
|
|
623
|
+
if off {
|
|
624
|
+
m.layer_clip = -1;
|
|
625
|
+
m.layer_weight = 0.0;
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if m.layer_clip != clip {
|
|
629
|
+
m.layer_time = 0.0;
|
|
630
|
+
m.layer_clip = clip;
|
|
631
|
+
}
|
|
632
|
+
m.layer_weight = weight.clamp(0.0, 1.0);
|
|
633
|
+
m.layer_mask_root = mask_root;
|
|
634
|
+
m.layer_speed = speed;
|
|
635
|
+
m.layer_loop = looping;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
pub fn anim_set_root_motion(&mut self, handle: f64, on: bool) {
|
|
640
|
+
if let Some(ma) = self.animations.get_mut(handle) {
|
|
641
|
+
ma.mixer.root_motion = on;
|
|
642
|
+
ma.mixer.root_delta = [0.0; 3];
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
pub fn anim_finished(&self, handle: f64) -> bool {
|
|
647
|
+
self.animations.get(handle).map(|m| m.mixer.finished).unwrap_or(true)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
pub fn anim_clip_duration(&self, handle: f64, clip: usize) -> f32 {
|
|
651
|
+
self.animations.get(handle)
|
|
652
|
+
.and_then(|m| m.animations.get(clip))
|
|
653
|
+
.map(|a| a.duration)
|
|
654
|
+
.unwrap_or(0.0)
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
pub fn anim_root_delta(&self, handle: f64) -> [f32; 3] {
|
|
658
|
+
self.animations.get(handle).map(|m| m.mixer.root_delta).unwrap_or([0.0; 3])
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
pub fn find_joint(&self, handle: f64, name: &str) -> i32 {
|
|
662
|
+
if let Some(ma) = self.animations.get(handle) {
|
|
663
|
+
if let Some(sk) = &ma.skeleton {
|
|
664
|
+
for (i, j) in sk.joints.iter().enumerate() {
|
|
665
|
+
if j.name == name { return i as i32; }
|
|
666
|
+
}
|
|
667
|
+
// Fall back to a case-insensitive contains match: exporters
|
|
668
|
+
// decorate names ("mixamorig:Hand_R", "Bip01 R Hand") often
|
|
669
|
+
// enough that an exact match is the exception, not the rule.
|
|
670
|
+
let want = name.to_ascii_lowercase();
|
|
671
|
+
for (i, j) in sk.joints.iter().enumerate() {
|
|
672
|
+
if j.name.to_ascii_lowercase().contains(&want) { return i as i32; }
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
-1
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/// Model-space transform of a joint (EN-033 sockets). Valid after the
|
|
680
|
+
/// frame's `advance_and_update`.
|
|
681
|
+
pub fn joint_world(&self, handle: f64, joint: usize) -> Option<[[f32; 4]; 4]> {
|
|
682
|
+
self.animations.get(handle)
|
|
683
|
+
.and_then(|m| m.joint_world.get(joint))
|
|
684
|
+
.copied()
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/// Advance every mixer clock by `dt` and rebuild the pose. One call per
|
|
688
|
+
/// model per frame; the game never touches clip time.
|
|
689
|
+
pub fn advance_and_update(&mut self, handle: f64, dt: f32) {
|
|
690
|
+
if let Some(ma) = self.animations.get_mut(handle) {
|
|
691
|
+
if ma.skeleton.is_none() || ma.animations.is_empty() { return; }
|
|
692
|
+
if !ma.mixer.started { ma.mixer.started = true; }
|
|
693
|
+
|
|
694
|
+
// --- advance clocks
|
|
695
|
+
let cur_dur = ma.animations.get(ma.mixer.cur_clip).map(|a| a.duration).unwrap_or(0.0);
|
|
696
|
+
let t_before = ma.mixer.cur_time;
|
|
697
|
+
let t_raw = ma.mixer.cur_time + dt * ma.mixer.cur_speed;
|
|
698
|
+
let mut wrapped = false;
|
|
699
|
+
ma.mixer.cur_time = if cur_dur <= 0.0 {
|
|
700
|
+
0.0
|
|
701
|
+
} else if ma.mixer.cur_loop {
|
|
702
|
+
if t_raw >= cur_dur { wrapped = true; }
|
|
703
|
+
t_raw.rem_euclid(cur_dur)
|
|
704
|
+
} else if t_raw >= cur_dur {
|
|
705
|
+
ma.mixer.finished = true;
|
|
706
|
+
cur_dur
|
|
707
|
+
} else {
|
|
708
|
+
t_raw
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
if ma.mixer.fade_dur > 0.0 {
|
|
712
|
+
let prev_dur = ma.animations.get(ma.mixer.prev_clip).map(|a| a.duration).unwrap_or(0.0);
|
|
713
|
+
let pt = ma.mixer.prev_time + dt * ma.mixer.prev_speed;
|
|
714
|
+
ma.mixer.prev_time = if prev_dur <= 0.0 {
|
|
715
|
+
0.0
|
|
716
|
+
} else if ma.mixer.prev_loop {
|
|
717
|
+
pt.rem_euclid(prev_dur)
|
|
718
|
+
} else {
|
|
719
|
+
pt.min(prev_dur)
|
|
720
|
+
};
|
|
721
|
+
ma.mixer.fade_t += dt;
|
|
722
|
+
if ma.mixer.fade_t >= ma.mixer.fade_dur {
|
|
723
|
+
ma.mixer.fade_dur = 0.0;
|
|
724
|
+
ma.mixer.fade_t = 0.0;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if ma.mixer.layer_clip >= 0 {
|
|
729
|
+
let li = ma.mixer.layer_clip as usize;
|
|
730
|
+
let ldur = ma.animations.get(li).map(|a| a.duration).unwrap_or(0.0);
|
|
731
|
+
let lt = ma.mixer.layer_time + dt * ma.mixer.layer_speed;
|
|
732
|
+
ma.mixer.layer_time = if ldur <= 0.0 {
|
|
733
|
+
0.0
|
|
734
|
+
} else if ma.mixer.layer_loop {
|
|
735
|
+
lt.rem_euclid(ldur)
|
|
736
|
+
} else {
|
|
737
|
+
lt.min(ldur)
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// --- sample + blend the base track
|
|
742
|
+
let skel = ma.skeleton.as_ref().unwrap();
|
|
743
|
+
let strip_root = !ma.mixer.root_motion;
|
|
744
|
+
let mut pose = sample_local_pose(skel, &ma.animations[ma.mixer.cur_clip], ma.mixer.cur_time, strip_root);
|
|
745
|
+
|
|
746
|
+
if ma.mixer.fade_dur > 0.0 {
|
|
747
|
+
let w = (ma.mixer.fade_t / ma.mixer.fade_dur).clamp(0.0, 1.0);
|
|
748
|
+
// Smoothstep the fade — a linear pose blend still reads as a
|
|
749
|
+
// slope discontinuity at both ends of the transition.
|
|
750
|
+
let w = w * w * (3.0 - 2.0 * w);
|
|
751
|
+
let prev = sample_local_pose(skel, &ma.animations[ma.mixer.prev_clip], ma.mixer.prev_time, strip_root);
|
|
752
|
+
blend_pose(&mut pose, &prev, 1.0 - w, None);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// --- root motion: delta of the root joint's authored translation
|
|
756
|
+
if ma.mixer.root_motion && !skel.joints.is_empty() {
|
|
757
|
+
let cd = cur_dur;
|
|
758
|
+
let anim = &ma.animations[ma.mixer.cur_clip];
|
|
759
|
+
let p_now = root_translation_at(skel, anim, ma.mixer.cur_time);
|
|
760
|
+
let p_old = root_translation_at(skel, anim, t_before);
|
|
761
|
+
let d = if wrapped && cd > 0.0 {
|
|
762
|
+
let p_end = root_translation_at(skel, anim, cd);
|
|
763
|
+
let p_start = root_translation_at(skel, anim, 0.0);
|
|
764
|
+
[
|
|
765
|
+
(p_end[0] - p_old[0]) + (p_now[0] - p_start[0]),
|
|
766
|
+
(p_end[1] - p_old[1]) + (p_now[1] - p_start[1]),
|
|
767
|
+
(p_end[2] - p_old[2]) + (p_now[2] - p_start[2]),
|
|
768
|
+
]
|
|
769
|
+
} else {
|
|
770
|
+
[p_now[0] - p_old[0], p_now[1] - p_old[1], p_now[2] - p_old[2]]
|
|
771
|
+
};
|
|
772
|
+
ma.mixer.root_delta = d;
|
|
773
|
+
// The delta is handed to the character controller, so the pose
|
|
774
|
+
// itself must not also carry it or the model double-moves.
|
|
775
|
+
pose.0[0] = skel.joints[0].rest_translation;
|
|
776
|
+
} else {
|
|
777
|
+
ma.mixer.root_delta = [0.0; 3];
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// --- masked layer over the top
|
|
781
|
+
if ma.mixer.layer_clip >= 0 && ma.mixer.layer_weight > 0.0 {
|
|
782
|
+
let root = ma.mixer.layer_mask_root;
|
|
783
|
+
if ma.mask_cached_root != root {
|
|
784
|
+
ma.mask_weights = build_mask_weights(skel, root);
|
|
785
|
+
ma.mask_cached_root = root;
|
|
786
|
+
}
|
|
787
|
+
let li = ma.mixer.layer_clip as usize;
|
|
788
|
+
let lpose = sample_local_pose(skel, &ma.animations[li], ma.mixer.layer_time, true);
|
|
789
|
+
let w = ma.mixer.layer_weight;
|
|
790
|
+
let mask = ma.mask_weights.clone();
|
|
791
|
+
blend_pose(&mut pose, &lpose, w, Some(&mask));
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
ma.apply_pose(&pose);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
type LocalPose = (Vec<[f32; 3]>, Vec<[f32; 4]>, Vec<[f32; 3]>);
|
|
800
|
+
|
|
801
|
+
impl ModelAnimation {
|
|
802
|
+
/// Local TRS pose -> world transforms -> skinning matrices. Keeps the
|
|
803
|
+
/// world transforms around too, because that is what sockets read.
|
|
804
|
+
fn apply_pose(&mut self, pose: &LocalPose) {
|
|
805
|
+
let skeleton = match &self.skeleton { Some(s) => s, None => return };
|
|
806
|
+
let joint_count = skeleton.joints.len();
|
|
807
|
+
if self.joint_matrices.len() != joint_count {
|
|
808
|
+
self.joint_matrices = vec![mat4_identity(); joint_count];
|
|
809
|
+
}
|
|
810
|
+
if self.joint_world.len() != joint_count {
|
|
811
|
+
self.joint_world = vec![mat4_identity(); joint_count];
|
|
812
|
+
}
|
|
813
|
+
let mut world = vec![mat4_identity(); joint_count];
|
|
814
|
+
for &root in &skeleton.root_joints {
|
|
815
|
+
compute_joint_transforms(skeleton, root, &mat4_identity(), &pose.0, &pose.1, &pose.2, &mut world);
|
|
816
|
+
}
|
|
817
|
+
for i in 0..joint_count {
|
|
818
|
+
self.joint_matrices[i] = mat4_mul(&world[i], &skeleton.joints[i].inverse_bind);
|
|
819
|
+
}
|
|
820
|
+
self.joint_world.copy_from_slice(&world);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/// Sample one clip into a local TRS pose, rest pose as the fallback for
|
|
825
|
+
/// joints the clip does not animate.
|
|
826
|
+
fn sample_local_pose(skeleton: &SkeletonData, anim: &AnimationData, time: f32, strip_root: bool) -> LocalPose {
|
|
827
|
+
let joint_count = skeleton.joints.len();
|
|
828
|
+
let mut t: Vec<[f32; 3]> = skeleton.joints.iter().map(|j| j.rest_translation).collect();
|
|
829
|
+
let mut r: Vec<[f32; 4]> = skeleton.joints.iter().map(|j| j.rest_rotation).collect();
|
|
830
|
+
let mut s: Vec<[f32; 3]> = skeleton.joints.iter().map(|j| j.rest_scale).collect();
|
|
831
|
+
|
|
832
|
+
let time = if anim.duration > 0.0 { time.rem_euclid(anim.duration) } else { 0.0 };
|
|
833
|
+
|
|
834
|
+
for channel in &anim.channels {
|
|
835
|
+
let ji = channel.joint_index;
|
|
836
|
+
if ji >= joint_count { continue; }
|
|
837
|
+
if !channel.translations.is_empty() && !channel.timestamps.is_empty() {
|
|
838
|
+
t[ji] = sample_vec3(&channel.timestamps, &channel.translations, time);
|
|
839
|
+
}
|
|
840
|
+
if !channel.rotations.is_empty() {
|
|
841
|
+
let ts = if !channel.rotation_timestamps.is_empty() { &channel.rotation_timestamps } else { &channel.timestamps };
|
|
842
|
+
if !ts.is_empty() { r[ji] = sample_quat(ts, &channel.rotations, time); }
|
|
843
|
+
}
|
|
844
|
+
if !channel.scales.is_empty() {
|
|
845
|
+
let ts = if !channel.scale_timestamps.is_empty() { &channel.scale_timestamps } else { &channel.timestamps };
|
|
846
|
+
if !ts.is_empty() { s[ji] = sample_vec3(ts, &channel.scales, time); }
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if strip_root && joint_count > 0 {
|
|
851
|
+
t[0] = skeleton.joints[0].rest_translation;
|
|
852
|
+
}
|
|
853
|
+
(t, r, s)
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/// The root joint's authored translation at `time` — the raw channel value,
|
|
857
|
+
/// *not* the rest-locked one, which is the whole point of root motion.
|
|
858
|
+
fn root_translation_at(skeleton: &SkeletonData, anim: &AnimationData, time: f32) -> [f32; 3] {
|
|
859
|
+
if skeleton.joints.is_empty() { return [0.0; 3]; }
|
|
860
|
+
let time = if anim.duration > 0.0 { time.clamp(0.0, anim.duration) } else { 0.0 };
|
|
861
|
+
for channel in &anim.channels {
|
|
862
|
+
if channel.joint_index == 0 && !channel.translations.is_empty() && !channel.timestamps.is_empty() {
|
|
863
|
+
return sample_vec3(&channel.timestamps, &channel.translations, time);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
skeleton.joints[0].rest_translation
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/// `dst = lerp(dst, src, w * mask[j])`. Rotations use nlerp with a
|
|
870
|
+
/// hemisphere fix — without the dot-sign flip, two clips whose quaternions
|
|
871
|
+
/// land on opposite hemispheres blend the *long* way round and the limb
|
|
872
|
+
/// visibly swings through the body.
|
|
873
|
+
fn blend_pose(dst: &mut LocalPose, src: &LocalPose, w: f32, mask: Option<&[f32]>) {
|
|
874
|
+
let n = dst.0.len().min(src.0.len());
|
|
875
|
+
for j in 0..n {
|
|
876
|
+
let jw = match mask {
|
|
877
|
+
Some(m) => w * m.get(j).copied().unwrap_or(0.0),
|
|
878
|
+
None => w,
|
|
879
|
+
};
|
|
880
|
+
if jw <= 0.0 { continue; }
|
|
881
|
+
let jw = jw.min(1.0);
|
|
882
|
+
for k in 0..3 {
|
|
883
|
+
dst.0[j][k] = dst.0[j][k] + (src.0[j][k] - dst.0[j][k]) * jw;
|
|
884
|
+
dst.2[j][k] = dst.2[j][k] + (src.2[j][k] - dst.2[j][k]) * jw;
|
|
885
|
+
}
|
|
886
|
+
let a = dst.1[j];
|
|
887
|
+
let mut b = src.1[j];
|
|
888
|
+
let dot = a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3];
|
|
889
|
+
if dot < 0.0 { b = [-b[0], -b[1], -b[2], -b[3]]; }
|
|
890
|
+
let mut q = [
|
|
891
|
+
a[0] + (b[0] - a[0]) * jw,
|
|
892
|
+
a[1] + (b[1] - a[1]) * jw,
|
|
893
|
+
a[2] + (b[2] - a[2]) * jw,
|
|
894
|
+
a[3] + (b[3] - a[3]) * jw,
|
|
895
|
+
];
|
|
896
|
+
let len = (q[0]*q[0] + q[1]*q[1] + q[2]*q[2] + q[3]*q[3]).sqrt();
|
|
897
|
+
if len > 1e-6 {
|
|
898
|
+
q = [q[0]/len, q[1]/len, q[2]/len, q[3]/len];
|
|
899
|
+
} else {
|
|
900
|
+
q = a;
|
|
901
|
+
}
|
|
902
|
+
dst.1[j] = q;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/// 1.0 for every joint at or below `root`, 0.0 elsewhere. `root < 0` means
|
|
907
|
+
/// "whole skeleton" so a layer with no mask is a plain full-body override.
|
|
908
|
+
fn build_mask_weights(skeleton: &SkeletonData, root: i32) -> Vec<f32> {
|
|
909
|
+
let n = skeleton.joints.len();
|
|
910
|
+
if root < 0 || (root as usize) >= n { return vec![1.0; n]; }
|
|
911
|
+
let mut w = vec![0.0f32; n];
|
|
912
|
+
let mut stack = vec![root as usize];
|
|
913
|
+
while let Some(j) = stack.pop() {
|
|
914
|
+
if j >= n || w[j] > 0.0 { continue; }
|
|
915
|
+
w[j] = 1.0;
|
|
916
|
+
for &c in &skeleton.joints[j].children { stack.push(c); }
|
|
917
|
+
}
|
|
918
|
+
w
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// ============================================================
|
|
922
|
+
// Matrix / quaternion helpers for skeletal animation
|
|
923
|
+
// ============================================================
|
|
924
|
+
|
|
925
|
+
fn mat4_identity() -> [[f32; 4]; 4] {
|
|
926
|
+
[
|
|
927
|
+
[1.0, 0.0, 0.0, 0.0],
|
|
928
|
+
[0.0, 1.0, 0.0, 0.0],
|
|
929
|
+
[0.0, 0.0, 1.0, 0.0],
|
|
930
|
+
[0.0, 0.0, 0.0, 1.0],
|
|
931
|
+
]
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
fn mat4_mul(a: &[[f32; 4]; 4], b: &[[f32; 4]; 4]) -> [[f32; 4]; 4] {
|
|
935
|
+
let mut out = [[0.0f32; 4]; 4];
|
|
936
|
+
for col in 0..4 {
|
|
937
|
+
for row in 0..4 {
|
|
938
|
+
out[col][row] = a[0][row]*b[col][0] + a[1][row]*b[col][1] + a[2][row]*b[col][2] + a[3][row]*b[col][3];
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
out
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
fn mat4_from_trs(t: &[f32; 3], r: &[f32; 4], s: &[f32; 3]) -> [[f32; 4]; 4] {
|
|
945
|
+
let (x, y, z, w) = (r[0], r[1], r[2], r[3]);
|
|
946
|
+
let x2 = x + x; let y2 = y + y; let z2 = z + z;
|
|
947
|
+
let xx = x * x2; let xy = x * y2; let xz = x * z2;
|
|
948
|
+
let yy = y * y2; let yz = y * z2; let zz = z * z2;
|
|
949
|
+
let wx = w * x2; let wy = w * y2; let wz = w * z2;
|
|
950
|
+
|
|
951
|
+
// Column-major: m[col][row]
|
|
952
|
+
[
|
|
953
|
+
[(1.0 - (yy + zz)) * s[0], (xy + wz) * s[0], (xz - wy) * s[0], 0.0], // column 0
|
|
954
|
+
[(xy - wz) * s[1], (1.0 - (xx + zz)) * s[1], (yz + wx) * s[1], 0.0], // column 1
|
|
955
|
+
[(xz + wy) * s[2], (yz - wx) * s[2], (1.0 - (xx + yy)) * s[2], 0.0], // column 2
|
|
956
|
+
[t[0], t[1], t[2], 1.0], // column 3 (translation)
|
|
957
|
+
]
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
fn quat_slerp(a: &[f32; 4], b: &[f32; 4], t: f32) -> [f32; 4] {
|
|
961
|
+
let mut dot = a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3];
|
|
962
|
+
let mut b2 = *b;
|
|
963
|
+
if dot < 0.0 {
|
|
964
|
+
dot = -dot;
|
|
965
|
+
b2 = [-b[0], -b[1], -b[2], -b[3]];
|
|
966
|
+
}
|
|
967
|
+
if dot > 0.9995 {
|
|
968
|
+
let mut out = [
|
|
969
|
+
a[0] + t * (b2[0] - a[0]),
|
|
970
|
+
a[1] + t * (b2[1] - a[1]),
|
|
971
|
+
a[2] + t * (b2[2] - a[2]),
|
|
972
|
+
a[3] + t * (b2[3] - a[3]),
|
|
973
|
+
];
|
|
974
|
+
let len = (out[0]*out[0] + out[1]*out[1] + out[2]*out[2] + out[3]*out[3]).sqrt();
|
|
975
|
+
if len > 0.0 { for v in &mut out { *v /= len; } }
|
|
976
|
+
return out;
|
|
977
|
+
}
|
|
978
|
+
let theta = dot.acos();
|
|
979
|
+
let sin_theta = theta.sin();
|
|
980
|
+
let wa = ((1.0 - t) * theta).sin() / sin_theta;
|
|
981
|
+
let wb = (t * theta).sin() / sin_theta;
|
|
982
|
+
[
|
|
983
|
+
wa * a[0] + wb * b2[0],
|
|
984
|
+
wa * a[1] + wb * b2[1],
|
|
985
|
+
wa * a[2] + wb * b2[2],
|
|
986
|
+
wa * a[3] + wb * b2[3],
|
|
987
|
+
]
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
fn lerp_vec3(a: &[f32; 3], b: &[f32; 3], t: f32) -> [f32; 3] {
|
|
991
|
+
[
|
|
992
|
+
a[0] + t * (b[0] - a[0]),
|
|
993
|
+
a[1] + t * (b[1] - a[1]),
|
|
994
|
+
a[2] + t * (b[2] - a[2]),
|
|
995
|
+
]
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
fn find_keyframe_pair(timestamps: &[f32], time: f32) -> (usize, usize, f32) {
|
|
999
|
+
if timestamps.len() <= 1 {
|
|
1000
|
+
return (0, 0, 0.0);
|
|
1001
|
+
}
|
|
1002
|
+
if time <= timestamps[0] {
|
|
1003
|
+
return (0, 0, 0.0);
|
|
1004
|
+
}
|
|
1005
|
+
if time >= timestamps[timestamps.len() - 1] {
|
|
1006
|
+
let last = timestamps.len() - 1;
|
|
1007
|
+
return (last, last, 0.0);
|
|
1008
|
+
}
|
|
1009
|
+
for i in 0..timestamps.len() - 1 {
|
|
1010
|
+
if time >= timestamps[i] && time < timestamps[i + 1] {
|
|
1011
|
+
let dt = timestamps[i + 1] - timestamps[i];
|
|
1012
|
+
let t = if dt > 0.0 { (time - timestamps[i]) / dt } else { 0.0 };
|
|
1013
|
+
return (i, i + 1, t);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
let last = timestamps.len() - 1;
|
|
1017
|
+
(last, last, 0.0)
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
fn sample_vec3(timestamps: &[f32], values: &[[f32; 3]], time: f32) -> [f32; 3] {
|
|
1021
|
+
if values.is_empty() { return [0.0; 3]; }
|
|
1022
|
+
if values.len() == 1 { return values[0]; }
|
|
1023
|
+
let (i0, i1, t) = find_keyframe_pair(timestamps, time);
|
|
1024
|
+
if i0 >= values.len() { return values[values.len() - 1]; }
|
|
1025
|
+
if i1 >= values.len() { return values[values.len() - 1]; }
|
|
1026
|
+
lerp_vec3(&values[i0], &values[i1], t)
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
fn sample_quat(timestamps: &[f32], values: &[[f32; 4]], time: f32) -> [f32; 4] {
|
|
1030
|
+
if values.is_empty() { return [0.0, 0.0, 0.0, 1.0]; }
|
|
1031
|
+
if values.len() == 1 { return values[0]; }
|
|
1032
|
+
let (i0, i1, t) = find_keyframe_pair(timestamps, time);
|
|
1033
|
+
if i0 >= values.len() { return values[values.len() - 1]; }
|
|
1034
|
+
if i1 >= values.len() { return values[values.len() - 1]; }
|
|
1035
|
+
quat_slerp(&values[i0], &values[i1], t)
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
fn compute_joint_transforms(
|
|
1039
|
+
skeleton: &SkeletonData,
|
|
1040
|
+
joint_idx: usize,
|
|
1041
|
+
parent_transform: &[[f32; 4]; 4],
|
|
1042
|
+
translations: &[[f32; 3]],
|
|
1043
|
+
rotations: &[[f32; 4]],
|
|
1044
|
+
scales: &[[f32; 3]],
|
|
1045
|
+
world_transforms: &mut [[[f32; 4]; 4]],
|
|
1046
|
+
) {
|
|
1047
|
+
if joint_idx >= skeleton.joints.len() { return; }
|
|
1048
|
+
let local = mat4_from_trs(&translations[joint_idx], &rotations[joint_idx], &scales[joint_idx]);
|
|
1049
|
+
let world = mat4_mul(parent_transform, &local);
|
|
1050
|
+
world_transforms[joint_idx] = world;
|
|
1051
|
+
let children = skeleton.joints[joint_idx].children.clone();
|
|
1052
|
+
for &child in &children {
|
|
1053
|
+
compute_joint_transforms(skeleton, child, &world, translations, rotations, scales, world_transforms);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// ---- Catmull-Rom spline helpers (Q9) ----
|
|
1058
|
+
|
|
1059
|
+
fn catmull_rom_point(points: &[f32], n: usize, segment: usize, t: f32) -> [f32; 3] {
|
|
1060
|
+
// Indices: p0 = segment - 1, p1 = segment, p2 = segment + 1, p3 = segment + 2.
|
|
1061
|
+
// Clamp at boundaries.
|
|
1062
|
+
let i0 = if segment > 0 { segment - 1 } else { 0 };
|
|
1063
|
+
let i1 = segment;
|
|
1064
|
+
let i2 = if segment + 1 < n { segment + 1 } else { n - 1 };
|
|
1065
|
+
let i3 = if segment + 2 < n { segment + 2 } else { n - 1 };
|
|
1066
|
+
|
|
1067
|
+
let p0 = [points[i0 * 3], points[i0 * 3 + 1], points[i0 * 3 + 2]];
|
|
1068
|
+
let p1 = [points[i1 * 3], points[i1 * 3 + 1], points[i1 * 3 + 2]];
|
|
1069
|
+
let p2 = [points[i2 * 3], points[i2 * 3 + 1], points[i2 * 3 + 2]];
|
|
1070
|
+
let p3 = [points[i3 * 3], points[i3 * 3 + 1], points[i3 * 3 + 2]];
|
|
1071
|
+
|
|
1072
|
+
let t2 = t * t;
|
|
1073
|
+
let t3 = t2 * t;
|
|
1074
|
+
let mut out = [0.0f32; 3];
|
|
1075
|
+
for k in 0..3 {
|
|
1076
|
+
out[k] = 0.5 * (
|
|
1077
|
+
(2.0 * p1[k]) +
|
|
1078
|
+
(-p0[k] + p2[k]) * t +
|
|
1079
|
+
(2.0 * p0[k] - 5.0 * p1[k] + 4.0 * p2[k] - p3[k]) * t2 +
|
|
1080
|
+
(-p0[k] + 3.0 * p1[k] - 3.0 * p2[k] + p3[k]) * t3
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
out
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
fn update_bounds(bmin: &mut [f32; 3], bmax: &mut [f32; 3], x: f32, y: f32, z: f32) {
|
|
1087
|
+
if x < bmin[0] { bmin[0] = x; }
|
|
1088
|
+
if y < bmin[1] { bmin[1] = y; }
|
|
1089
|
+
if z < bmin[2] { bmin[2] = z; }
|
|
1090
|
+
if x > bmax[0] { bmax[0] = x; }
|
|
1091
|
+
if y > bmax[1] { bmax[1] = y; }
|
|
1092
|
+
if z > bmax[2] { bmax[2] = z; }
|
|
1093
|
+
}
|