Skip to main content

kvas_engine/
physics.rs

1//! Rapier integration. ECS components are the source of truth: bodies, colliders and joints are
2//! created from them (also for entities spawned later), and dynamic results are written back.
3//!
4//! Structures: a collider without its own RigidBody attaches to the nearest ancestor that has one,
5//! so a hierarchy of colliders under one body moves as a single compound body.
6
7use crate::components::*;
8use crate::hierarchy;
9use crate::scene::Scene;
10use glam::{Mat4, Quat, Vec3};
11use kvas_ecs::{Entity, World};
12use rapier3d::prelude as r;
13use std::collections::HashMap;
14use std::sync::{Arc, Mutex};
15
16/// Closest solid-collider hit. Distances are metres along a normalized ray.
17#[derive(Clone, Copy, Debug)]
18pub struct RayHit {
19    /// Entity carrying the hit collider (may be a child of the body owner).
20    pub entity: Entity,
21    /// World-space hit position.
22    pub point: Vec3,
23    /// World-space surface normal reported by Rapier.
24    pub normal: Vec3,
25    /// Non-negative distance from the ray origin.
26    pub distance: f32,
27}
28
29/// Filtering policy for world-space physics queries.
30/// Layer masks use the same symmetric rule as collider/collider interactions.
31#[derive(Clone, Copy, Debug)]
32pub struct QueryOptions {
33    /// Collider entity to skip; if it owns a body, all colliders of that body are skipped.
34    pub exclude: Option<Entity>,
35    /// Whether sensor volumes participate in the query. Defaults to false.
36    pub include_triggers: bool,
37    /// Only sensor volumes are hit; solid geometry is ignored (e.g. finding "use" areas behind a
38    /// thin rim, then checking line of sight with a second, solid-only query).
39    pub only_triggers: bool,
40    /// Query membership bits used against collider filter masks.
41    pub memberships: u32,
42    /// Collider membership bits accepted by this query.
43    pub filter: u32,
44}
45
46impl Default for QueryOptions {
47    fn default() -> Self {
48        Self { exclude: None, include_triggers: false, only_triggers: false, memberships: u32::MAX, filter: u32::MAX }
49    }
50}
51
52fn groups(memberships: u32, filter: u32) -> r::InteractionGroups {
53    r::InteractionGroups::new(r::Group::from_bits_retain(memberships), r::Group::from_bits_retain(filter), r::InteractionTestMode::And)
54}
55
56/// Host-owned Rapier state mirrored from ECS components during [`Self::step`].
57/// Queries reflect the most recently completed physics step, not pending ECS edits.
58/// Use [`Self::reset`] to explicitly rebuild after wholesale scene replacement.
59pub struct Physics {
60    world: r::PhysicsWorld,
61    bodies: HashMap<Entity, r::RigidBodyHandle>,
62    colliders: HashMap<Entity, r::ColliderHandle>,
63    joints: HashMap<Entity, r::ImpulseJointHandle>,
64    owners: HashMap<r::ColliderHandle, Entity>,
65    anchors: HashMap<Entity, r::RigidBodyHandle>,
66    shapes: HashMap<Entity, ShapeKey>,
67    joint_configs: HashMap<Entity, (u64, Option<r::RigidBodyHandle>)>,
68}
69
70fn pose_mat(m: Mat4) -> (Mat4, Vec3) {
71    let (scale, rot, pos) = m.to_scale_rotation_translation();
72    (Mat4::from_rotation_translation(rot, pos), scale)
73}
74
75fn pose(m: Mat4) -> r::Pose {
76    let (_, rot, pos) = m.to_scale_rotation_translation();
77    r::Pose::from_parts(pos, rot)
78}
79
80fn global(world: &World, e: Entity) -> Mat4 {
81    world.get::<GlobalTransform>(e).map_or_else(|| hierarchy::global_matrix(world, e), |transform| transform.0)
82}
83
84/// What a collider's Rapier shape was built from; a change rebuilds it.
85type ShapeKey = (ShapeKind, Vec3, Vec3, String);
86
87type Triangles = Arc<(Vec<Vec3>, Vec<[u32; 3]>)>;
88
89/// Positions and triangles of a model file, loaded once per path for mesh colliders.
90fn model_triangles(path: &str) -> Option<Triangles> {
91    static CACHE: Mutex<Option<HashMap<String, Option<Triangles>>>> = Mutex::new(None);
92    let mut cache = CACHE.lock().ok()?;
93    cache
94        .get_or_insert_with(HashMap::new)
95        .entry(path.to_string())
96        .or_insert_with(|| {
97            let assets = crate::meshmod::assets().unwrap_or(crate::assets::Assets::Dir(std::path::PathBuf::new()));
98            let mesh = crate::mesh::MeshData::load(&assets, path).ok()?;
99            let points: Vec<Vec3> = mesh.vertices.iter().map(|v| Vec3::from(v.pos)).collect();
100            let tris: Vec<[u32; 3]> = mesh
101                .indices
102                .chunks_exact(3)
103                .map(|t| [t[0], t[1], t[2]])
104                .filter(|t| t.iter().all(|&i| (i as usize) < points.len()) && t[0] != t[1] && t[1] != t[2] && t[0] != t[2])
105                .collect();
106            (!tris.is_empty()).then(|| Arc::new((points, tris)))
107        })
108        .clone()
109}
110
111fn shape_key(world: &World, e: Entity, c: &Collider, scale: Vec3) -> ShapeKey {
112    let mesh = match c.shape {
113        ShapeKind::Mesh | ShapeKind::Hull => world.get::<MeshRenderer>(e).map(|m| m.mesh.clone()).unwrap_or_default(),
114        _ => String::new(),
115    };
116    (c.shape, c.size, scale, mesh)
117}
118
119/// Exact triangles for static geometry, a convex hull for moving props; `None` if the model is missing.
120fn mesh_shape(c: &Collider, scale: Vec3, mesh: &str) -> Option<r::ColliderBuilder> {
121    let tris = model_triangles(mesh)?;
122    let s = c.size.abs() * scale.abs();
123    let points: Vec<Vec3> = tris.0.iter().map(|p| *p * s).collect();
124    if c.shape == ShapeKind::Hull {
125        return r::ColliderBuilder::convex_hull(&points);
126    }
127    let flags = r::TriMeshFlags::FIX_INTERNAL_EDGES | r::TriMeshFlags::MERGE_DUPLICATE_VERTICES;
128    r::ColliderBuilder::trimesh_with_flags(points.clone(), tris.1.clone(), flags)
129        .or_else(|_| r::ColliderBuilder::trimesh(points, tris.1.clone()))
130        .ok()
131}
132
133fn make_shape(c: &Collider, scale: Vec3, mesh: &str) -> r::ColliderBuilder {
134    let s = scale.abs();
135    let size = c.size.abs().max(Vec3::splat(0.001)) * s;
136    let radius = size.x.max(size.z) * 0.5;
137    match c.shape {
138        ShapeKind::Mesh | ShapeKind::Hull => {
139            mesh_shape(c, scale, mesh).unwrap_or_else(|| r::ColliderBuilder::cuboid(size.x * 0.5, size.y * 0.5, size.z * 0.5))
140        }
141        ShapeKind::Box => r::ColliderBuilder::cuboid(size.x * 0.5, size.y * 0.5, size.z * 0.5),
142        ShapeKind::Sphere => r::ColliderBuilder::ball(size.x.max(size.y).max(size.z) * 0.5),
143        ShapeKind::Capsule => r::ColliderBuilder::capsule_y((size.y * 0.5 - radius).max(0.0), radius),
144        ShapeKind::Cylinder => r::ColliderBuilder::cylinder(size.y * 0.5, radius),
145    }
146}
147
148impl Physics {
149    /// Creates an empty simulation. ECS objects appear on the first valid step.
150    pub fn new() -> Self {
151        Self {
152            world: r::PhysicsWorld::default(),
153            bodies: HashMap::new(),
154            colliders: HashMap::new(),
155            joints: HashMap::new(),
156            owners: HashMap::new(),
157            anchors: HashMap::new(),
158            shapes: HashMap::new(),
159            joint_configs: HashMap::new(),
160        }
161    }
162
163    /// Closest solid hit, ignoring trigger volumes. `dir` need not be normalized.
164    /// Invalid origins/directions, zero direction and invalid ranges return `None`.
165    pub fn raycast(&self, origin: Vec3, dir: Vec3, max_distance: f32) -> Option<RayHit> {
166        self.raycast_excluding(origin, dir, max_distance, None)
167    }
168
169    /// Like `raycast`, but ignores trigger volumes and the collider of `exclude` (the caster itself).
170    pub fn raycast_excluding(&self, origin: Vec3, dir: Vec3, max_distance: f32, exclude: Option<Entity>) -> Option<RayHit> {
171        self.raycast_with_options(origin, dir, max_distance, QueryOptions { exclude, ..Default::default() })
172    }
173
174    /// Closest ray hit under explicit sensor, layer and owner-exclusion filters.
175    /// Direction is normalized internally; distance is in metres. Invalid input returns `None`.
176    pub fn raycast_with_options(&self, origin: Vec3, dir: Vec3, max_distance: f32, options: QueryOptions) -> Option<RayHit> {
177        if !origin.is_finite() || !dir.is_finite() || !max_distance.is_finite() || max_distance < 0.0 {
178            return None;
179        }
180        let dir = crate::spatial::Ray::new(origin, dir)?.direction();
181        let ray = r::Ray::new(origin, dir);
182        let filter = self.query_filter(options);
183        let (handle, hit) = self.world.cast_ray_and_get_normal(&ray, max_distance, true, filter)?;
184        Some(RayHit {
185            entity: *self.owners.get(&handle)?,
186            point: origin + dir * hit.time_of_impact,
187            normal: hit.normal,
188            distance: hit.time_of_impact,
189        })
190    }
191
192    fn query_filter(&self, options: QueryOptions) -> r::QueryFilter<'_> {
193        let mut filter = r::QueryFilter::default().groups(groups(options.memberships, options.filter));
194        if options.only_triggers {
195            filter = filter.exclude_solids();
196        } else if !options.include_triggers {
197            filter = filter.exclude_sensors();
198        }
199        if let Some(handle) = options.exclude.and_then(|e| self.colliders.get(&e)) {
200            filter = filter.exclude_collider(*handle);
201        }
202        if let Some(handle) = options.exclude.and_then(|e| self.bodies.get(&e)) {
203            filter = filter.exclude_rigid_body(*handle);
204        }
205        filter
206    }
207
208    /// Clears all Rapier objects and queries; ECS data is left untouched.
209    pub fn reset(&mut self) {
210        *self = Self::new();
211    }
212
213    /// Number of mirrored ECS rigid bodies (world joint anchors are excluded).
214    pub fn body_count(&self) -> usize {
215        self.bodies.len()
216    }
217    /// Number of mirrored colliders.
218    pub fn collider_count(&self) -> usize {
219        self.colliders.len()
220    }
221    /// Number of mirrored impulse joints.
222    pub fn joint_count(&self) -> usize {
223        self.joints.len()
224    }
225
226    /// All collider entities intersecting a world-space sphere, including triggers.
227    /// Returns a sorted unique list; invalid centres/radii return an empty list.
228    pub fn overlap_sphere(&self, center: Vec3, radius: f32) -> Vec<Entity> {
229        self.overlap_sphere_with_options(center, radius, QueryOptions { include_triggers: true, ..Default::default() })
230    }
231
232    /// Sphere-overlap query with explicit layer, trigger and owner filters.
233    /// Results are unique collider entity handles in index order.
234    pub fn overlap_sphere_with_options(&self, center: Vec3, radius: f32, options: QueryOptions) -> Vec<Entity> {
235        if !center.is_finite() || !radius.is_finite() || radius <= 0.0 {
236            return Vec::new();
237        }
238        let shape = r::Ball::new(radius);
239        let pose = r::Pose::from_translation(center);
240        let mut entities: Vec<_> = self
241            .world
242            .intersect_shape(pose, &shape, self.query_filter(options))
243            .filter_map(|(handle, _)| self.owners.get(&handle).copied())
244            .collect();
245        entities.sort();
246        entities.dedup();
247        entities
248    }
249
250    /// Entities with active solid contact or sensor overlap against this collider.
251    /// A compound body's children must be queried by their collider entity handles.
252    /// Returns a sorted unique list from the last completed simulation step.
253    pub fn touching(&self, entity: Entity) -> Vec<Entity> {
254        let Some(&handle) = self.colliders.get(&entity) else {
255            return Vec::new();
256        };
257        let contacts = self.world.contact_pairs_with(handle).filter(|p| p.has_any_active_contact()).map(|p| {
258            if p.collider1 == handle {
259                p.collider2
260            } else {
261                p.collider1
262            }
263        });
264        let sensors = self.world.intersection_pairs_with(handle).filter(|p| p.4).map(|(a, _, b, _, _)| if a == handle { b } else { a });
265        let mut entities: Vec<_> = contacts.chain(sensors).filter_map(|h| self.owners.get(&h).copied()).collect();
266        entities.sort();
267        entities.dedup();
268        entities
269    }
270
271    /// Nearest entity at or above `e` that owns a rigid body.
272    fn body_owner(&self, world: &World, mut e: Entity) -> Option<Entity> {
273        let mut seen = std::collections::HashSet::new();
274        loop {
275            if !seen.insert(e) {
276                return None;
277            }
278            if self.bodies.contains_key(&e) {
279                return Some(e);
280            }
281            e = hierarchy::parent_of(world, e)?;
282        }
283    }
284
285    fn remove_joint(&mut self, entity: Entity) {
286        if let Some(handle) = self.joints.remove(&entity) {
287            self.world.remove_impulse_joint(handle);
288        }
289        if let Some(anchor) = self.anchors.remove(&entity) {
290            self.world.remove_body(anchor);
291        }
292        self.joint_configs.remove(&entity);
293    }
294
295    /// Creates physics objects for new entities and drops those of removed ones.
296    fn reconcile(&mut self, world: &World) {
297        let gone: Vec<_> = self
298            .joints
299            .iter()
300            .filter(|(e, h)| !world.has::<Joint>(**e) || !world.has::<RigidBody>(**e) || self.world.impulse_joints.get(**h).is_none())
301            .map(|(&e, _)| e)
302            .collect();
303        for e in gone {
304            self.remove_joint(e);
305        }
306        let gone: Vec<_> = self.bodies.keys().copied().filter(|&e| !world.has::<RigidBody>(e)).collect();
307        for e in gone {
308            if let Some(h) = self.bodies.remove(&e) {
309                self.world.remove_body(h);
310            }
311        }
312        let gone: Vec<_> = self
313            .colliders
314            .iter()
315            .filter(|(e, h)| !world.has::<Collider>(**e) || self.world.colliders.get(**h).is_none())
316            .map(|(&e, _)| e)
317            .collect();
318        for e in gone {
319            if let Some(h) = self.colliders.remove(&e) {
320                self.owners.remove(&h);
321                self.world.remove_collider(h);
322            }
323            self.shapes.remove(&e);
324        }
325
326        for (e, rb) in world.iter::<RigidBody>() {
327            if self.bodies.contains_key(&e) {
328                continue;
329            }
330            let b = match rb.kind {
331                BodyKind::Dynamic => r::RigidBodyBuilder::dynamic(),
332                BodyKind::Fixed => r::RigidBodyBuilder::fixed(),
333                BodyKind::Kinematic => r::RigidBodyBuilder::kinematic_position_based(),
334            }
335            .pose(pose(global(world, e)))
336            .gravity_scale(rb.gravity_scale)
337            .linear_damping(rb.linear_damping)
338            .angular_damping(rb.angular_damping)
339            .ccd_enabled(rb.ccd)
340            .additional_mass(rb.mass.max(0.001));
341            let b = if rb.lock_rotation { b.lock_rotations() } else { b };
342            self.bodies.insert(e, self.world.insert_body(b));
343        }
344
345        for (e, c) in world.iter::<Collider>() {
346            if self.colliders.contains_key(&e) {
347                let handle = self.colliders[&e];
348                let owner = self.body_owner(world, e);
349                let parent = owner.and_then(|p| self.bodies.get(&p).copied());
350                let old_parent = self.world.colliders.get(handle).and_then(|c| c.parent());
351                if old_parent != parent {
352                    self.world.colliders.set_parent(handle, parent, &mut self.world.bodies);
353                }
354                let g = global(world, e);
355                let (_, scale) = pose_mat(g);
356                let shape_key = shape_key(world, e, c, scale);
357                let shape_changed = self.shapes.get(&e) != Some(&shape_key);
358                if let Some(collider) = self.world.colliders.get_mut(handle) {
359                    if shape_changed {
360                        collider.set_shape(make_shape(c, scale, &shape_key.3).build().shared_shape().clone());
361                    }
362                    collider.set_friction(c.friction);
363                    collider.set_restitution(c.restitution);
364                    collider.set_sensor(c.is_trigger);
365                    collider.set_collision_groups(groups(c.memberships, c.filter));
366                    if let Some(owner) = owner {
367                        let (owner_pose, _) = pose_mat(global(world, owner));
368                        collider.set_position_wrt_parent(pose(owner_pose.inverse() * g));
369                    } else {
370                        collider.set_position(pose(g));
371                    }
372                }
373                self.shapes.insert(e, shape_key);
374                continue;
375            }
376            let g = global(world, e);
377            let (_, scale) = pose_mat(g);
378            let key = shape_key(world, e, c, scale);
379            let builder = make_shape(c, scale, &key.3)
380                .friction(c.friction)
381                .restitution(c.restitution)
382                .sensor(c.is_trigger)
383                .collision_groups(groups(c.memberships, c.filter))
384                .density(0.0);
385            let handle = match self.body_owner(world, e) {
386                Some(owner) => {
387                    let (owner_pose, _) = pose_mat(global(world, owner));
388                    let (_, rot, pos) = (owner_pose.inverse() * g).to_scale_rotation_translation();
389                    let builder = builder.position(r::Pose::from_parts(pos, rot));
390                    self.world.insert_collider(builder, Some(self.bodies[&owner]))
391                }
392                None => self.world.insert_collider(builder.position(pose(g)), None),
393            };
394            self.colliders.insert(e, handle);
395            self.owners.insert(handle, e);
396            self.shapes.insert(e, key);
397        }
398
399        // Bodies can have disappeared above, and Inspector/scripts may edit
400        // constraint settings or replace a named target while the entity lives.
401        let changed: Vec<_> = self
402            .joints
403            .iter()
404            .filter_map(|(&entity, &handle)| {
405                let joint = world.get::<Joint>(entity)?;
406                let target = if joint.target.is_empty() {
407                    None
408                } else {
409                    hierarchy::find_by_name(world, &joint.target).and_then(|e| self.bodies.get(&e).copied())
410                };
411                let key = (crate::procedural::signature(joint), target);
412                (self.world.impulse_joints.get(handle).is_none() || self.joint_configs.get(&entity) != Some(&key)).then_some(entity)
413            })
414            .collect();
415        for entity in changed {
416            self.remove_joint(entity);
417        }
418
419        for (e, j) in world.iter::<Joint>() {
420            if self.joints.contains_key(&e) {
421                continue;
422            }
423            let Some(&b1) = self.bodies.get(&e) else { continue };
424            let (target_body, target_mat) = if j.target.is_empty() {
425                (None, Mat4::IDENTITY)
426            } else {
427                match hierarchy::find_by_name(world, &j.target)
428                    .and_then(|t| Some((self.bodies.get(&t).copied()?, pose_mat(global(world, t)).0)))
429                {
430                    Some((h, m)) => (Some(h), m),
431                    None => continue, // target not spawned (yet)
432                }
433            };
434            if target_body == Some(b1) {
435                continue;
436            } // self-joints are invalid constraints.
437            let (m1, scale) = pose_mat(global(world, e));
438            // Pivot joints attach at `anchor`; a spring hangs from its far end at `anchor`.
439            let pivot = j.anchor * scale;
440            let a1 = if j.kind == JointKind::Spring { Vec3::ZERO } else { pivot };
441            let a2 = target_mat.inverse().transform_point3(m1.transform_point3(pivot));
442            if let Some(joint) = build_joint(j, m1, target_mat, a1, a2) {
443                let b2 = target_body.unwrap_or_else(|| {
444                    let anchor = self.world.insert_body(r::RigidBodyBuilder::fixed());
445                    self.anchors.insert(e, anchor);
446                    anchor
447                });
448                self.joints.insert(e, self.world.insert_impulse_joint(b1, b2, joint));
449                self.joint_configs.insert(e, (crate::procedural::signature(j), target_body));
450            }
451        }
452    }
453
454    /// Reconciles ECS objects, advances physics and writes back dynamic transforms.
455    /// Invalid/non-positive deltas are ignored. Apply impulses before this call.
456    /// Removing a physics component removes its Rapier counterpart on this step.
457    pub fn step(&mut self, scene: &mut Scene, dt: f32) {
458        if !dt.is_finite() || dt <= 0.0 {
459            return;
460        }
461        // Scripts and modifiers may have edited local poses since the last
462        // frame. Refresh once before any reconciliation/query of cached poses.
463        hierarchy::update_globals(&mut scene.world);
464        self.reconcile(&scene.world);
465        self.world.gravity = scene.settings.gravity;
466        self.world.integration_parameters.dt = dt;
467
468        for (&e, &h) in &self.bodies {
469            let g = global(&scene.world, e);
470            let Some(rb) = scene.world.get_mut::<RigidBody>(e) else { continue };
471            let Some(body) = self.world.bodies.get_mut(h) else { continue };
472            let kind = match rb.kind {
473                BodyKind::Dynamic => r::RigidBodyType::Dynamic,
474                BodyKind::Fixed => r::RigidBodyType::Fixed,
475                BodyKind::Kinematic => r::RigidBodyType::KinematicPositionBased,
476            };
477            if body.body_type() != kind {
478                body.set_body_type(kind, true);
479            }
480            body.set_gravity_scale(rb.gravity_scale, false);
481            body.set_linear_damping(rb.linear_damping);
482            body.set_angular_damping(rb.angular_damping);
483            body.enable_ccd(rb.ccd);
484            if (body.mass() - rb.mass.max(0.001)).abs() > f32::EPSILON {
485                body.set_additional_mass(rb.mass.max(0.001), true);
486            }
487            body.lock_rotations(rb.lock_rotation, false);
488            if std::mem::take(&mut rb.teleport) {
489                body.set_position(pose(g), true);
490            }
491            match rb.kind {
492                BodyKind::Kinematic => body.set_next_kinematic_position(pose(g)),
493                BodyKind::Dynamic => {
494                    if let Some(v) = rb.set_velocity.take() {
495                        body.set_linvel(v, true);
496                    }
497                    if let Some(w) = rb.set_angular_velocity.take() {
498                        body.set_angvel(w, true);
499                    }
500                    if rb.impulse != Vec3::ZERO {
501                        body.apply_impulse(std::mem::take(&mut rb.impulse), true);
502                    }
503                }
504                BodyKind::Fixed => body.set_position(pose(g), false),
505            }
506        }
507
508        self.world.step();
509
510        // Write results back, parents before children so local transforms see fresh ancestors.
511        let mut dynamic: Vec<(Entity, r::RigidBodyHandle)> = self
512            .bodies
513            .iter()
514            .filter(|(e, _)| scene.world.get::<RigidBody>(**e).is_some_and(|rb| rb.kind == BodyKind::Dynamic))
515            .map(|(&e, &h)| (e, h))
516            .collect();
517        dynamic.sort_by_key(|&(e, _)| (hierarchy::ancestors_count(&scene.world, e), e));
518        for (e, h) in dynamic {
519            let Some(body) = self.world.bodies.get(h) else { continue };
520            let p = body.position();
521            let (_, scale) = pose_mat(global(&scene.world, e));
522            let new_global = Mat4::from_scale_rotation_translation(scale, p.rotation, p.translation);
523            let parent_m = hierarchy::parent_of(&scene.world, e).map_or(Mat4::IDENTITY, |pa| hierarchy::global_matrix(&scene.world, pa));
524            if let Some(t) = scene.world.get_mut::<Transform>(e) {
525                *t = Transform::from_matrix(parent_m.inverse() * new_global);
526            }
527            if let Some(rb) = scene.world.get_mut::<RigidBody>(e) {
528                rb.velocity = body.linvel();
529            }
530        }
531        hierarchy::update_globals(&mut scene.world);
532    }
533}
534
535impl Default for Physics {
536    fn default() -> Self {
537        Self::new()
538    }
539}
540
541fn build_joint(j: &Joint, m1: Mat4, m2: Mat4, a1: Vec3, a2: Vec3) -> Option<r::GenericJoint> {
542    Some(match j.kind {
543        JointKind::Fixed => {
544            // Weld at the current relative pose.
545            let frame = m1.inverse() * m2 * Mat4::from_translation(a2);
546            let (_, rot, pos) = frame.to_scale_rotation_translation();
547            r::FixedJointBuilder::new().local_frame1(r::Pose::from_parts(pos, rot)).local_anchor2(a2).build().into()
548        }
549        JointKind::Ball => r::SphericalJointBuilder::new().local_anchor1(a1).local_anchor2(a2).build().into(),
550        JointKind::Hinge => {
551            let axis = j.axis.try_normalize()?;
552            let axis2 = Quat::from_mat4(&m2).inverse() * (Quat::from_mat4(&m1) * axis);
553            let mut b = r::RevoluteJointBuilder::new(axis).local_anchor1(a1).local_anchor2(a2);
554            if j.use_limits {
555                b = b.limits([j.limit_min.to_radians(), j.limit_max.to_radians()]);
556            }
557            let mut joint: r::GenericJoint = b.build().into();
558            joint.set_local_axis2(axis2);
559            joint
560        }
561        JointKind::Spring => {
562            r::SpringJointBuilder::new(j.rest_length, j.stiffness, j.damping).local_anchor1(a1).local_anchor2(a2).build().into()
563        }
564    })
565}