bevy_xpbd_3d/constraints/joints/
spherical.rs

1//! [`SphericalJoint`] component.
2
3use crate::prelude::*;
4use bevy::{
5    ecs::entity::{EntityMapper, MapEntities},
6    prelude::*,
7};
8
9/// A spherical joint prevents relative translation of the attached bodies while allowing rotation around all axes.
10///
11/// Spherical joints can be useful for things like pendula, chains, ragdolls etc.
12#[derive(Component, Clone, Copy, Debug, PartialEq)]
13#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
14pub struct SphericalJoint {
15    /// First entity constrained by the joint.
16    pub entity1: Entity,
17    /// Second entity constrained by the joint.
18    pub entity2: Entity,
19    /// Attachment point on the first body.
20    pub local_anchor1: Vector,
21    /// Attachment point on the second body.
22    pub local_anchor2: Vector,
23    /// An axis that the attached bodies can swing around. This is normally the x-axis.
24    pub swing_axis: Vector3,
25    /// An axis that the attached bodies can twist around. This is normally the y-axis.
26    pub twist_axis: Vector3,
27    /// The extents of the allowed relative rotation of the bodies around the `swing_axis`.
28    pub swing_limit: Option<AngleLimit>,
29    /// The extents of the allowed relative rotation of the bodies around the `twist_axis`.
30    pub twist_limit: Option<AngleLimit>,
31    /// Linear damping applied by the joint.
32    pub damping_linear: Scalar,
33    /// Angular damping applied by the joint.
34    pub damping_angular: Scalar,
35    /// Lagrange multiplier for the positional correction.
36    pub position_lagrange: Scalar,
37    /// Lagrange multiplier for the angular correction caused by the swing limits.
38    pub swing_lagrange: Scalar,
39    /// Lagrange multiplier for the angular correction caused by the twist limits.
40    pub twist_lagrange: Scalar,
41    /// The joint's compliance, the inverse of stiffness, has the unit meters / Newton.
42    pub compliance: Scalar,
43    /// The force exerted by the joint.
44    pub force: Vector,
45    /// The torque exerted by the joint when limiting the relative rotation of the bodies around the `swing_axis`.
46    pub swing_torque: Torque,
47    /// The torque exerted by the joint when limiting the relative rotation of the bodies around the `twist_axis`.
48    pub twist_torque: Torque,
49}
50
51impl XpbdConstraint<2> for SphericalJoint {
52    fn entities(&self) -> [Entity; 2] {
53        [self.entity1, self.entity2]
54    }
55
56    fn clear_lagrange_multipliers(&mut self) {
57        self.position_lagrange = 0.0;
58        self.swing_lagrange = 0.0;
59        self.twist_lagrange = 0.0;
60    }
61
62    fn solve(&mut self, bodies: [&mut RigidBodyQueryItem; 2], dt: Scalar) {
63        let [body1, body2] = bodies;
64        let compliance = self.compliance;
65
66        // Align positions
67        let mut lagrange = self.position_lagrange;
68        self.force = self.align_position(
69            body1,
70            body2,
71            self.local_anchor1,
72            self.local_anchor2,
73            &mut lagrange,
74            compliance,
75            dt,
76        );
77        self.position_lagrange = lagrange;
78
79        // Apply swing limits
80        self.swing_torque = self.apply_swing_limits(body1, body2, dt);
81
82        // Apply twist limits
83        self.twist_torque = self.apply_twist_limits(body1, body2, dt);
84    }
85}
86
87impl Joint for SphericalJoint {
88    fn new(entity1: Entity, entity2: Entity) -> Self {
89        Self {
90            entity1,
91            entity2,
92            local_anchor1: Vector::ZERO,
93            local_anchor2: Vector::ZERO,
94            swing_axis: Vector3::X,
95            twist_axis: Vector3::Y,
96            swing_limit: None,
97            twist_limit: None,
98            damping_linear: 1.0,
99            damping_angular: 1.0,
100            position_lagrange: 0.0,
101            swing_lagrange: 0.0,
102            twist_lagrange: 0.0,
103            compliance: 0.0,
104            force: Vector::ZERO,
105            #[cfg(feature = "2d")]
106            swing_torque: 0.0,
107            #[cfg(feature = "3d")]
108            swing_torque: Vector::ZERO,
109            #[cfg(feature = "2d")]
110            twist_torque: 0.0,
111            #[cfg(feature = "3d")]
112            twist_torque: Vector::ZERO,
113        }
114    }
115
116    fn with_compliance(self, compliance: Scalar) -> Self {
117        Self { compliance, ..self }
118    }
119
120    fn with_local_anchor_1(self, anchor: Vector) -> Self {
121        Self {
122            local_anchor1: anchor,
123            ..self
124        }
125    }
126
127    fn with_local_anchor_2(self, anchor: Vector) -> Self {
128        Self {
129            local_anchor2: anchor,
130            ..self
131        }
132    }
133
134    fn with_linear_velocity_damping(self, damping: Scalar) -> Self {
135        Self {
136            damping_linear: damping,
137            ..self
138        }
139    }
140
141    fn with_angular_velocity_damping(self, damping: Scalar) -> Self {
142        Self {
143            damping_angular: damping,
144            ..self
145        }
146    }
147
148    fn local_anchor_1(&self) -> Vector {
149        self.local_anchor1
150    }
151
152    fn local_anchor_2(&self) -> Vector {
153        self.local_anchor2
154    }
155
156    fn damping_linear(&self) -> Scalar {
157        self.damping_linear
158    }
159
160    fn damping_angular(&self) -> Scalar {
161        self.damping_angular
162    }
163}
164
165impl SphericalJoint {
166    /// Sets the limits of the allowed relative rotation around the `swing_axis`.
167    pub fn with_swing_limits(self, min: Scalar, max: Scalar) -> Self {
168        Self {
169            swing_limit: Some(AngleLimit::new(min, max)),
170            ..self
171        }
172    }
173
174    /// Sets the limits of the allowed relative rotation around the `twist_axis`.
175    #[cfg(feature = "3d")]
176    pub fn with_twist_limits(self, min: Scalar, max: Scalar) -> Self {
177        Self {
178            twist_limit: Some(AngleLimit::new(min, max)),
179            ..self
180        }
181    }
182
183    /// Applies angle limits to limit the relative rotation of the bodies around the `swing_axis`.
184    fn apply_swing_limits(
185        &mut self,
186        body1: &mut RigidBodyQueryItem,
187        body2: &mut RigidBodyQueryItem,
188        dt: Scalar,
189    ) -> Torque {
190        if let Some(joint_limit) = self.swing_limit {
191            let a1 = body1.rotation.rotate_vec3(self.swing_axis);
192            let a2 = body2.rotation.rotate_vec3(self.swing_axis);
193
194            let n = a1.cross(a2);
195            let n_magnitude = n.length();
196
197            if n_magnitude <= Scalar::EPSILON {
198                return Torque::ZERO;
199            }
200
201            let n = n / n_magnitude;
202
203            if let Some(dq) = joint_limit.compute_correction(n, a1, a2, PI) {
204                let mut lagrange = self.swing_lagrange;
205                let torque =
206                    self.align_orientation(body1, body2, dq, &mut lagrange, self.compliance, dt);
207                self.swing_lagrange = lagrange;
208                return torque;
209            }
210        }
211        Torque::ZERO
212    }
213
214    /// Applies angle limits to limit the relative rotation of the bodies around the `twist_axis`.
215    fn apply_twist_limits(
216        &mut self,
217        body1: &mut RigidBodyQueryItem,
218        body2: &mut RigidBodyQueryItem,
219        dt: Scalar,
220    ) -> Torque {
221        if let Some(joint_limit) = self.twist_limit {
222            let a1 = body1.rotation.rotate_vec3(self.swing_axis);
223            let a2 = body2.rotation.rotate_vec3(self.swing_axis);
224
225            let b1 = body1.rotation.rotate_vec3(self.twist_axis);
226            let b2 = body2.rotation.rotate_vec3(self.twist_axis);
227
228            let n = a1 + a2;
229            let n_magnitude = n.length();
230
231            if n_magnitude <= Scalar::EPSILON {
232                return Torque::ZERO;
233            }
234
235            let n = n / n_magnitude;
236
237            let n1 = b1 - n.dot(b1) * n;
238            let n2 = b2 - n.dot(b2) * n;
239            let n1_magnitude = n1.length();
240            let n2_magnitude = n2.length();
241
242            if n1_magnitude <= Scalar::EPSILON || n2_magnitude <= Scalar::EPSILON {
243                return Torque::ZERO;
244            }
245
246            let n1 = n1 / n1_magnitude;
247            let n2 = n2 / n2_magnitude;
248
249            let max_correction = if a1.dot(a2) > -0.5 { 2.0 * PI } else { dt };
250
251            if let Some(dq) = joint_limit.compute_correction(n, n1, n2, max_correction) {
252                let mut lagrange = self.twist_lagrange;
253                let torque =
254                    self.align_orientation(body1, body2, dq, &mut lagrange, self.compliance, dt);
255                self.twist_lagrange = lagrange;
256                return torque;
257            }
258        }
259        Torque::ZERO
260    }
261}
262
263impl PositionConstraint for SphericalJoint {}
264
265impl AngularConstraint for SphericalJoint {}
266
267impl MapEntities for SphericalJoint {
268    fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) {
269        self.entity1 = entity_mapper.map_entity(self.entity1);
270        self.entity2 = entity_mapper.map_entity(self.entity2);
271    }
272}