1use crate::assets::Assets;
27use crate::components::{DynamicMesh, MeshRenderer};
28use crate::mesh::MeshData;
29use glam::{Mat4, Quat, Vec3};
30use kvas_ecs::{Entity, World};
31use serde::{Deserialize, Serialize};
32use std::collections::HashMap;
33use std::sync::{Arc, Mutex};
34
35#[derive(Clone, Copy, Debug, PartialEq)]
37pub struct Trs {
38 pub t: Vec3,
40 pub r: Quat,
42 pub s: Vec3,
44}
45
46impl Default for Trs {
47 fn default() -> Self {
48 Self { t: Vec3::ZERO, r: Quat::IDENTITY, s: Vec3::ONE }
49 }
50}
51
52impl Trs {
53 pub fn from_mat(m: Mat4) -> Self {
55 let (s, r, t) = m.to_scale_rotation_translation();
56 Self { t, r: r.normalize(), s }
57 }
58 pub fn mat(&self) -> Mat4 {
60 Mat4::from_scale_rotation_translation(self.s, self.r, self.t)
61 }
62 pub fn lerp(&self, o: &Trs, k: f32) -> Trs {
64 Trs { t: self.t.lerp(o.t, k), r: self.r.slerp(o.r, k).normalize(), s: self.s.lerp(o.s, k) }
65 }
66}
67
68#[derive(Clone, Debug)]
70pub struct SkelNode {
71 pub name: String,
73 pub parent: Option<usize>,
75 pub rest: Mat4,
77}
78
79#[derive(Clone, Debug)]
82pub struct Joint {
83 pub node: usize,
85 pub inverse_bind: Mat4,
87}
88
89#[derive(Clone, Debug)]
91pub struct Clip {
92 pub name: String,
94 pub duration: f32,
96 pub fps: f32,
98 pub frames: Vec<Vec<Trs>>,
100}
101
102#[derive(Clone)]
104pub struct SkinnedModel {
105 pub mesh: MeshData,
107 pub bind: Vec<(Vec3, Vec3)>,
109 pub joints: Vec<[u16; 4]>,
111 pub weights: Vec<[f32; 4]>,
113 pub nodes: Vec<SkelNode>,
115 pub skin: Vec<Joint>,
117 pub clips: Vec<Clip>,
119 pub humanoid: Option<crate::humanoid::HumanoidBones>,
121 pub children: Vec<Vec<usize>>,
123}
124
125fn norm_name(s: &str) -> String {
126 s.chars().filter(|c| c.is_alphanumeric()).collect::<String>().to_lowercase()
127}
128
129impl SkinnedModel {
130 pub fn clip(&self, name: &str) -> Option<&Clip> {
132 let n = norm_name(name);
133 self.clips
134 .iter()
135 .find(|c| c.name == name)
136 .or_else(|| self.clips.iter().find(|c| norm_name(&c.name) == n))
137 .or_else(|| self.clips.iter().find(|c| norm_name(c.name.rsplit('|').next().unwrap_or("")) == n))
138 }
139
140 pub fn rest_pose(&self) -> Vec<Trs> {
142 self.nodes.iter().map(|n| Trs::from_mat(n.rest)).collect()
143 }
144
145 pub fn sample(&self, clip: &Clip, time: f32, looping: bool, out: &mut Vec<Trs>) {
147 out.clear();
148 let n = clip.frames.len();
149 if n == 0 {
150 out.extend(self.rest_pose());
151 return;
152 }
153 let t = if looping && clip.duration > 0.0 { time.rem_euclid(clip.duration) } else { time.clamp(0.0, clip.duration) };
154 let f = t * clip.fps;
155 let a = (f.floor() as usize).min(n - 1);
156 let b = if a + 1 < n { a + 1 } else if looping { 0 } else { a };
157 let k = (f - f.floor()).clamp(0.0, 1.0);
158 out.extend(clip.frames[a].iter().zip(&clip.frames[b]).map(|(x, y)| x.lerp(y, k)));
159 }
160
161 pub fn skin_into(&self, pose: &[Trs], out: &mut MeshData) {
163 let mats: Vec<Mat4> = self.skin.iter().map(|j| pose.get(j.node).map_or(Mat4::IDENTITY, Trs::mat) * j.inverse_bind).collect();
164 for (i, v) in out.vertices.iter_mut().enumerate() {
165 let (Some(&(p, n)), Some(js), Some(ws)) = (self.bind.get(i), self.joints.get(i), self.weights.get(i)) else { continue };
166 let (mut pos, mut nrm) = (Vec3::ZERO, Vec3::ZERO);
167 for k in 0..4 {
168 let w = ws[k];
169 if w <= 0.0 {
170 continue;
171 }
172 let m = mats.get(js[k] as usize).copied().unwrap_or(Mat4::IDENTITY);
173 pos += m.transform_point3(p) * w;
174 nrm += m.transform_vector3(n) * w;
175 }
176 v.pos = pos.into();
177 v.normal = nrm.normalize_or_zero().into();
178 }
179 }
180
181 fn node_named(&self, names: &[&str]) -> Option<usize> {
182 names.iter().find_map(|want| self.nodes.iter().position(|n| norm_name(&n.name) == *want))
183 }
184
185 fn in_place(&mut self) {
187 let Some(hips) = self.node_named(&["hips", "pelvis", "hip", "torso"]) else { return };
188 for clip in &mut self.clips {
189 let (Some(first), Some(last)) = (clip.frames.first(), clip.frames.last()) else { continue };
190 let drift = (last[hips].t - first[hips].t) * Vec3::new(1.0, 0.0, 1.0);
191 if drift.length() < 0.05 {
192 continue;
193 }
194 let n = (clip.frames.len() - 1).max(1) as f32;
195 for (f, frame) in clip.frames.iter_mut().enumerate() {
196 let off = drift * (f as f32 / n);
197 for trs in frame.iter_mut() {
198 trs.t -= off;
199 }
200 }
201 }
202 }
203}
204
205fn top4(mut list: Vec<(u16, f32)>) -> ([u16; 4], [f32; 4]) {
206 list.sort_by(|a, b| b.1.total_cmp(&a.1));
207 list.truncate(4);
208 let sum: f32 = list.iter().map(|w| w.1).sum();
209 let (mut j, mut w) = ([0u16; 4], [0f32; 4]);
210 for (i, (jj, ww)) in list.iter().enumerate() {
211 j[i] = *jj;
212 w[i] = if sum > 0.0 { ww / sum } else { 0.0 };
213 }
214 (j, w)
215}
216
217fn fbx_mat(g: &ufbx::Matrix) -> Mat4 {
220 Mat4::from_cols_array(&[
221 g.m00 as f32, g.m10 as f32, g.m20 as f32, 0.0, g.m01 as f32, g.m11 as f32, g.m21 as f32, 0.0, g.m02 as f32, g.m12 as f32, g.m22 as f32, 0.0, g.m03 as f32,
222 g.m13 as f32, g.m23 as f32, 1.0,
223 ])
224}
225
226fn load_fbx(assets: &Assets, path: &str, bytes: &[u8]) -> Result<SkinnedModel, String> {
227 use ufbx::{CoordinateAxes, CoordinateAxis};
228 let opts = ufbx::LoadOpts {
230 target_axes: CoordinateAxes { right: CoordinateAxis::PositiveX, up: CoordinateAxis::PositiveY, front: CoordinateAxis::PositiveZ },
231 target_unit_meters: 1.0,
232 generate_missing_normals: true,
233 ..Default::default()
234 };
235 let scene = ufbx::load_memory(bytes, opts).map_err(|e| format!("{e:?}"))?;
236 let mesh = MeshData::load(assets, path)?;
237 let mut index_of: HashMap<u32, usize> = HashMap::new();
239 let mut nodes: Vec<SkelNode> = Vec::new();
240 fn add(node: &ufbx::Node, index_of: &mut HashMap<u32, usize>, nodes: &mut Vec<SkelNode>) -> usize {
241 if let Some(&i) = index_of.get(&node.element.typed_id) {
242 return i;
243 }
244 let parent = node.parent.as_ref().map(|p| add(p, index_of, nodes));
245 nodes.push(SkelNode { name: node.element.name.to_string(), parent, rest: fbx_mat(&node.node_to_world) });
246 index_of.insert(node.element.typed_id, nodes.len() - 1);
247 nodes.len() - 1
248 }
249 let mut skin = Vec::new();
250 let (mut bind, mut joints, mut weights) = (Vec::new(), Vec::new(), Vec::new());
251 for node in scene.nodes.iter() {
252 let Some(m) = &node.mesh else { continue };
253 let deformer = m.skin_deformers.first();
254 let cluster_joint: Vec<u16> = match deformer {
256 Some(d) => d
257 .clusters
258 .iter()
259 .map(|c| {
260 let bone = c.bone_node.as_ref().map(|b| add(b, &mut index_of, &mut nodes)).unwrap_or_else(|| add(node, &mut index_of, &mut nodes));
261 nodes[bone].rest = fbx_mat(&c.bind_to_world);
264 skin.push(Joint { node: bone, inverse_bind: fbx_mat(&c.geometry_to_bone) });
265 (skin.len() - 1) as u16
266 })
267 .collect(),
268 None => {
269 let own = add(node, &mut index_of, &mut nodes);
270 skin.push(Joint { node: own, inverse_bind: fbx_mat(&node.geometry_to_node) });
271 vec![(skin.len() - 1) as u16]
272 }
273 };
274 for i in 0..m.num_indices {
275 let p = m.vertex_position[i];
276 let n = if m.vertex_normal.exists { m.vertex_normal[i] } else { ufbx::Vec3 { x: 0.0, y: 1.0, z: 0.0 } };
277 bind.push((Vec3::new(p.x as f32, p.y as f32, p.z as f32), Vec3::new(n.x as f32, n.y as f32, n.z as f32)));
278 let (j, w) = match deformer {
279 Some(d) => {
280 let vi = m.vertex_indices[i] as usize;
281 let list: Vec<(u16, f32)> = d
282 .vertices
283 .get(vi)
284 .map(|sv| {
285 (sv.weight_begin..sv.weight_begin + sv.num_weights)
286 .filter_map(|k| d.weights.get(k as usize))
287 .filter_map(|w| Some((*cluster_joint.get(w.cluster_index as usize)?, w.weight as f32)))
288 .collect()
289 })
290 .unwrap_or_default();
291 if list.is_empty() {
292 ([0; 4], [0.0; 4])
293 } else {
294 top4(list)
295 }
296 }
297 None => ([cluster_joint[0], 0, 0, 0], [1.0, 0.0, 0.0, 0.0]),
298 };
299 joints.push(j);
300 weights.push(w);
301 }
302 }
303 if bind.len() != mesh.vertices.len() {
304 return Err(format!("{path}: skinned vertex count {} differs from the mesh ({})", bind.len(), mesh.vertices.len()));
305 }
306 let mut clips = Vec::new();
308 for stack in scene.anim_stacks.iter() {
309 let duration = (stack.time_end - stack.time_begin).max(0.0) as f32;
310 let fps = 30.0;
311 let count = (duration * fps).ceil() as usize + 1;
312 let mut frames = Vec::with_capacity(count);
313 for f in 0..count {
314 let t = stack.time_begin + (f as f64 / fps as f64).min(duration as f64);
315 let eval = ufbx::evaluate_scene(&scene, &stack.anim, t, ufbx::EvaluateOpts::default()).map_err(|e| format!("{e:?}"))?;
316 let mut pose = vec![Trs::default(); nodes.len()];
317 for (&typed, &i) in &index_of {
318 if let Some(n) = eval.nodes.get(typed as usize) {
319 pose[i] = Trs::from_mat(fbx_mat(&n.node_to_world));
320 }
321 }
322 frames.push(pose);
323 }
324 let name = stack.element.name.to_string();
325 clips.push(Clip { name: name.rsplit('|').next().unwrap_or(&name).to_string(), duration, fps, frames });
326 }
327 Ok(SkinnedModel { mesh, bind, joints, weights, nodes, skin, clips, humanoid: None, children: Vec::new() })
328}
329
330fn load_gltf(assets: &Assets, path: &str, bytes: &[u8]) -> Result<SkinnedModel, String> {
333 let crate::mesh::ImportedGltf { document: doc, buffers, images, .. } = crate::mesh::import_gltf(assets, path, bytes)?;
334 let get = |b: gltf::Buffer| buffers.get(b.index()).map(|d| d.0.as_slice());
335 let count = doc.nodes().len();
337 let mut parent = vec![None; count];
338 for n in doc.nodes() {
339 for c in n.children() {
340 parent[c.index()] = Some(n.index());
341 }
342 }
343 let local_rest: Vec<(Vec3, Quat, Vec3)> = doc
344 .nodes()
345 .map(|n| {
346 let (t, r, s) = n.transform().decomposed();
347 (Vec3::from(t), Quat::from_array(r).normalize(), Vec3::from(s))
348 })
349 .collect();
350 let world_of = |local: &[(Vec3, Quat, Vec3)]| -> Vec<Mat4> {
351 let mut world: Vec<Option<Mat4>> = vec![None; count];
352 fn resolve(i: usize, parent: &[Option<usize>], local: &[(Vec3, Quat, Vec3)], world: &mut Vec<Option<Mat4>>, depth: usize) -> Mat4 {
353 if let Some(m) = world[i] {
354 return m;
355 }
356 let (t, r, s) = local[i];
357 let own = Mat4::from_scale_rotation_translation(s, r, t);
358 let m = match parent[i] {
359 Some(p) if depth < 64 => resolve(p, parent, local, world, depth + 1) * own,
360 _ => own,
361 };
362 world[i] = Some(m);
363 m
364 }
365 (0..count).map(|i| resolve(i, &parent, local, &mut world, 0)).collect()
366 };
367 let rest_world = world_of(&local_rest);
368 let nodes: Vec<SkelNode> = doc.nodes().map(|n| SkelNode { name: n.name().unwrap_or("").to_string(), parent: parent[n.index()], rest: rest_world[n.index()] }).collect();
369
370 let mut mesh = MeshData::default();
371 let (mut bind, mut joints, mut weights, mut skin) = (Vec::new(), Vec::new(), Vec::new(), Vec::<Joint>::new());
372 let mut parts = Vec::new();
373 for node in doc.nodes() {
374 let Some(gm) = node.mesh() else { continue };
375 let joint_base = skin.len() as u16;
377 match node.skin() {
378 Some(s) => {
379 let ibms: Vec<Mat4> = s.reader(get).read_inverse_bind_matrices().map(|it| it.map(|m| Mat4::from_cols_array_2d(&m)).collect()).unwrap_or_default();
380 for (k, j) in s.joints().enumerate() {
381 skin.push(Joint { node: j.index(), inverse_bind: ibms.get(k).copied().unwrap_or(Mat4::IDENTITY) });
382 }
383 }
384 None => skin.push(Joint { node: node.index(), inverse_bind: Mat4::IDENTITY }),
385 }
386 let skinned = node.skin().is_some();
387 for prim in gm.primitives() {
388 if prim.mode() != gltf::mesh::Mode::Triangles {
389 continue;
390 }
391 let r = prim.reader(get);
392 let Some(pos) = r.read_positions() else { continue };
393 let pos: Vec<Vec3> = pos.map(Vec3::from).collect();
394 let nrm: Vec<Vec3> = r.read_normals().map(|n| n.map(Vec3::from).collect()).unwrap_or_else(|| vec![Vec3::Y; pos.len()]);
395 let uvs: Vec<[f32; 2]> = r.read_tex_coords(0).map(|t| t.into_f32().collect()).unwrap_or_else(|| vec![[0.0; 2]; pos.len()]);
396 let js: Vec<[u16; 4]> = r.read_joints(0).map(|j| j.into_u16().collect()).unwrap_or_default();
397 let ws: Vec<[f32; 4]> = r.read_weights(0).map(|w| w.into_f32().collect()).unwrap_or_default();
398 let base = mesh.vertices.len() as u32;
399 let rest_rigid = rest_world[node.index()];
400 for (i, p) in pos.iter().enumerate() {
401 let n = nrm.get(i).copied().unwrap_or(Vec3::Y);
402 let (j, w) = if skinned && i < js.len() && i < ws.len() {
403 let list: Vec<(u16, f32)> = (0..4).filter(|k| ws[i][*k] > 0.0).map(|k| (joint_base + js[i][k], ws[i][k])).collect();
404 top4(list)
405 } else {
406 ([joint_base, 0, 0, 0], [1.0, 0.0, 0.0, 0.0])
407 };
408 let m: Mat4 = (0..4).filter(|k| w[*k] > 0.0).map(|k| {
410 let jt = &skin[j[k] as usize];
411 (rest_world[jt.node] * jt.inverse_bind) * w[k]
412 }).fold(Mat4::ZERO, |a, b| a + b);
413 let m = if skinned { m } else { rest_rigid };
414 mesh.vertices.push(crate::mesh::Vertex { pos: m.transform_point3(*p).into(), normal: m.transform_vector3(n).normalize_or_zero().into(), uv: uvs.get(i).copied().unwrap_or([0.0; 2]) });
415 bind.push((*p, n));
416 joints.push(j);
417 weights.push(w);
418 }
419 let first = mesh.indices.len() as u32;
420 match r.read_indices() {
421 Some(ix) => mesh.indices.extend(ix.into_u32().map(|i| base + i)),
422 None => mesh.indices.extend((0..pos.len() as u32).map(|i| base + i)),
423 }
424 let albedo = crate::mesh::material_albedo(&images, &prim.material().pbr_metallic_roughness());
425 if mesh.albedo.is_none() {
426 mesh.albedo = albedo.clone();
427 }
428 parts.push(crate::mesh::MeshPart { first_index: first, index_count: mesh.indices.len() as u32 - first, albedo, normal: None, orm: None });
429 }
430 }
431 if parts.len() > 1 {
432 mesh.parts = parts;
433 }
434 if mesh.indices.is_empty() {
435 return Err(format!("{path}: no triangles"));
436 }
437 let mut clips = Vec::new();
439 for anim in doc.animations() {
440 let mut tracks = Vec::new();
441 let mut duration = 0.0f32;
442 for ch in anim.channels() {
443 let r = ch.reader(get);
444 let times: Vec<f32> = r.read_inputs().map(|t| t.collect()).unwrap_or_default();
445 duration = duration.max(times.last().copied().unwrap_or(0.0));
446 let cubic = ch.sampler().interpolation() == gltf::animation::Interpolation::CubicSpline;
447 use gltf::animation::util::ReadOutputs;
448 let values: Vec<[f32; 4]> = match r.read_outputs() {
449 Some(ReadOutputs::Translations(v)) => v.map(|x| [x[0], x[1], x[2], 0.0]).collect(),
450 Some(ReadOutputs::Scales(v)) => v.map(|x| [x[0], x[1], x[2], 0.0]).collect(),
451 Some(ReadOutputs::Rotations(v)) => v.into_f32().collect(),
452 _ => continue,
453 };
454 let values: Vec<[f32; 4]> = if cubic { values.chunks(3).filter_map(|c| c.get(1).copied()).collect() } else { values };
456 tracks.push((ch.target().node().index(), ch.target().property(), times, values));
457 }
458 let fps = 30.0;
459 let frames_n = (duration * fps).ceil() as usize + 1;
460 let mut frames = Vec::with_capacity(frames_n);
461 for f in 0..frames_n {
462 let t = f as f32 / fps;
463 let mut local = local_rest.clone();
464 for (node, prop, times, values) in &tracks {
465 if times.is_empty() || values.is_empty() {
466 continue;
467 }
468 let k = times.partition_point(|x| *x <= t).saturating_sub(1).min(values.len() - 1);
469 let k2 = (k + 1).min(values.len() - 1).min(times.len() - 1);
470 let span = times[k2] - times[k];
471 let a = if span > 0.0 { ((t - times[k]) / span).clamp(0.0, 1.0) } else { 0.0 };
472 let (v0, v1) = (values[k], values[k2]);
473 use gltf::animation::Property;
474 match prop {
475 Property::Translation => local[*node].0 = Vec3::new(v0[0], v0[1], v0[2]).lerp(Vec3::new(v1[0], v1[1], v1[2]), a),
476 Property::Scale => local[*node].2 = Vec3::new(v0[0], v0[1], v0[2]).lerp(Vec3::new(v1[0], v1[1], v1[2]), a),
477 Property::Rotation => local[*node].1 = Quat::from_array(v0).normalize().slerp(Quat::from_array(v1).normalize(), a),
478 _ => {}
479 }
480 }
481 frames.push(world_of(&local).into_iter().map(Trs::from_mat).collect());
482 }
483 clips.push(Clip { name: anim.name().unwrap_or("Clip").to_string(), duration, fps, frames });
484 }
485 Ok(SkinnedModel { mesh, bind, joints, weights, nodes, skin, clips, humanoid: None, children: Vec::new() })
486}
487
488pub fn load(assets: &Assets, path: &str) -> Result<SkinnedModel, String> {
490 let bytes = assets.read(path).map_err(|e| format!("{path}: {e}"))?;
491 let lower = path.to_ascii_lowercase();
492 let mut model = if lower.ends_with(".fbx") {
493 load_fbx(assets, path, &bytes)?
494 } else if lower.ends_with(".glb") || lower.ends_with(".gltf") {
495 load_gltf(assets, path, &bytes)?
496 } else {
497 return Err(format!("{path}: skinning needs an FBX or glTF model"));
498 };
499 model.in_place();
500 crate::humanoid::add_clips(&mut model);
501 model.humanoid = crate::humanoid::find_bones(&model);
502 model.children = vec![Vec::new(); model.nodes.len()];
503 for (i, n) in model.nodes.iter().enumerate() {
504 if let Some(p) = n.parent {
505 model.children[p].push(i);
506 }
507 }
508 Ok(model)
509}
510
511pub fn model(path: &str) -> Option<Arc<SkinnedModel>> {
514 static CACHE: Mutex<Option<HashMap<String, Option<Arc<SkinnedModel>>>>> = Mutex::new(None);
515 let mut cache = CACHE.lock().ok()?;
516 cache
517 .get_or_insert_with(HashMap::new)
518 .entry(path.to_string())
519 .or_insert_with(|| {
520 let assets = crate::meshmod::assets().unwrap_or(Assets::Dir(std::path::PathBuf::new()));
521 load(&assets, path).ok().map(Arc::new)
522 })
523 .clone()
524}
525
526#[derive(Clone, Serialize, Deserialize)]
530#[serde(default)]
531pub struct Animator {
532 pub clip: String,
534 pub speed: f32,
536 pub looping: bool,
538 pub playing: bool,
540 pub fade: f32,
542 #[serde(skip)]
544 pub clips: Vec<String>,
545 #[serde(skip)]
547 pub time: f32,
548}
549
550impl Default for Animator {
551 fn default() -> Self {
552 Self { clip: String::new(), speed: 1.0, looping: true, playing: true, fade: 0.25, clips: Vec::new(), time: 0.0 }
553 }
554}
555
556impl crate::components::Component for Animator {
557 fn fields(&mut self) -> Vec<crate::reflect::Field<'_>> {
558 use crate::reflect::Field;
559 vec![Field::text("clip", &mut self.clip), Field::f32("speed", &mut self.speed), Field::bool("looping", &mut self.looping), Field::bool("playing", &mut self.playing), Field::f32("fade", &mut self.fade)]
560 }
561}
562
563#[derive(Clone, Default)]
565pub struct AnimatorState {
566 current: String,
567 time: f32,
568 previous: Option<(String, f32)>,
569 fade_t: f32,
570 fade_len: f32,
571 wall: f32,
572 version: u64,
573 pose: Vec<Trs>,
574 blend: Vec<Trs>,
575 ik: crate::ik::IkState,
576 ragdoll: crate::ragdoll::RagdollState,
577}
578
579pub type GroundQuery<'a> = &'a dyn Fn(Vec3, f32, Entity) -> Option<f32>;
581
582pub fn update(world: &mut World, time: f32, dt: f32, simulate: bool) {
586 update_with(world, time, dt, simulate, None);
587}
588
589pub fn update_with(world: &mut World, time: f32, dt: f32, simulate: bool, ground: Option<GroundQuery>) {
593 let stale: Vec<Entity> = world.iter::<AnimatorState>().map(|(e, _)| e).filter(|e| !world.has::<Animator>(*e)).collect();
595 for e in stale {
596 if let Some(mut st) = world.remove::<AnimatorState>(e) {
597 crate::ragdoll::deactivate(world, &mut st.ragdoll);
598 }
599 world.remove::<DynamicMesh>(e);
600 }
601 let list: Vec<(Entity, Animator, String)> = world.iter::<Animator>().filter_map(|(e, a)| Some((e, a.clone(), world.get::<MeshRenderer>(e)?.mesh.clone()))).collect();
602 for (e, mut anim, mesh) in list {
603 let Some(model) = model(&mesh) else { continue };
604 let mut st = world.get::<AnimatorState>(e).cloned().unwrap_or_default();
605 let step = if simulate { dt } else { (time - st.wall).clamp(0.0, 0.1) };
606 st.wall = time;
607 let wanted = if anim.clip.is_empty() { model.clips.first().map(|c| c.name.clone()).unwrap_or_default() } else { anim.clip.clone() };
608 if wanted != st.current {
609 if !st.current.is_empty() && anim.fade > 0.0 {
610 st.previous = Some((st.current.clone(), st.time));
611 st.fade_t = 0.0;
612 st.fade_len = anim.fade;
613 }
614 st.current = wanted;
615 st.time = 0.0;
616 }
617 let advance = if anim.playing { step * anim.speed } else { 0.0 };
618 st.time += advance;
619 let mut pose = std::mem::take(&mut st.pose);
620 match model.clip(&st.current) {
621 Some(c) => model.sample(c, st.time, anim.looping, &mut pose),
622 None => pose = model.rest_pose(),
623 }
624 if let Some((prev, t)) = st.previous.clone() {
625 st.fade_t += step;
626 let k = (st.fade_t / st.fade_len.max(1e-3)).clamp(0.0, 1.0);
627 let k = k * k * (3.0 - 2.0 * k);
628 if k >= 1.0 {
629 st.previous = None;
630 } else if let Some(c) = model.clip(&prev) {
631 let mut from = std::mem::take(&mut st.blend);
632 model.sample(c, t + st.fade_t * anim.speed, true, &mut from);
633 for (p, f) in pose.iter_mut().zip(&from) {
634 *p = f.lerp(p, k);
635 }
636 st.blend = from;
637 }
638 }
639 let owner_world = world.get::<crate::components::GlobalTransform>(e).map_or_else(|| crate::hierarchy::global_matrix(world, e), |g| g.0);
641 let rd = world.get::<crate::ragdoll::Ragdoll>(e).cloned();
642 let want_rag = simulate && model.humanoid.is_some() && rd.as_ref().is_some_and(|r| r.active);
643 if want_rag && !st.ragdoll.is_active() {
644 let velocity = world.get::<crate::components::RigidBody>(e).map_or(Vec3::ZERO, |r| r.velocity);
645 st.ragdoll = crate::ragdoll::activate(world, e, &model, &pose, owner_world, rd.as_ref().unwrap(), velocity);
646 } else if !want_rag && st.ragdoll.is_active() {
647 crate::ragdoll::deactivate(world, &mut st.ragdoll);
648 }
649 if st.ragdoll.is_active() {
650 crate::ragdoll::pose(world, &model, &st.ragdoll, owner_world, &mut pose);
651 } else if let Some(ik) = world.get::<crate::ik::CharacterIk>(e).cloned() {
652 let target = ik.look_point.or_else(|| {
653 let t = (!ik.look_at.is_empty()).then(|| crate::hierarchy::find_by_name(world, &ik.look_at)).flatten()?;
654 Some(world.get::<crate::components::GlobalTransform>(t).map_or_else(|| crate::hierarchy::global_matrix(world, t), |g| g.0).w_axis.truncate())
655 });
656 let probe = ground.map(|g| move |from: Vec3, len: f32| g(from, len, e));
657 let probe_ref = probe.as_ref().map(|p| p as &dyn Fn(Vec3, f32) -> Option<f32>);
658 crate::ik::apply(&model, &mut pose, owner_world, &ik, target, probe_ref, &mut st.ik, if simulate { dt } else { step });
659 }
660 let mut data = model.mesh.clone();
661 model.skin_into(&pose, &mut data);
662 st.pose = pose;
663 st.version += 1;
664 let version = st.version;
665 anim.time = st.time;
666 if anim.clips.len() != model.clips.len() {
667 anim.clips = model.clips.iter().map(|c| c.name.clone()).collect();
668 }
669 if let Some(a) = world.get_mut::<Animator>(e) {
670 a.time = anim.time;
671 if a.clips.len() != anim.clips.len() {
672 a.clips = anim.clips;
673 }
674 }
675 world.insert(e, st);
676 world.insert(e, DynamicMesh { data: Arc::new(data), version });
677 }
678}
679
680#[cfg(test)]
681mod tests {
682 use super::*;
683
684 #[test]
685 fn trs_round_trip_and_blend() {
686 let m = Mat4::from_scale_rotation_translation(Vec3::splat(2.0), Quat::from_rotation_y(0.7), Vec3::new(1.0, 2.0, 3.0));
687 let t = Trs::from_mat(m);
688 assert!(t.mat().abs_diff_eq(m, 1e-4));
689 let half = Trs::default().lerp(&t, 0.5);
690 assert!((half.t - Vec3::new(0.5, 1.0, 1.5)).length() < 1e-5);
691 }
692}