Skip to main content

rapier2d/control/
pid_controller.rs

1use crate::dynamics::{AxesMask, RigidBody, RigidBodyPosition, RigidBodyVelocity};
2use crate::math::{AngVector, Pose, Real, Rotation, Vector};
3
4/// A Proportional-Derivative (PD) controller.
5///
6/// This is useful for controlling a rigid-body at the velocity level so it matches a target
7/// pose.
8///
9/// This is a [PID controller](https://en.wikipedia.org/wiki/Proportional%E2%80%93integral%E2%80%93derivative_controller)
10/// without the Integral part to keep the API immutable, while having a behaviour generally
11/// sufficient for games.
12#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
13#[derive(Debug, Copy, Clone, PartialEq)]
14pub struct PdController {
15    /// The Proportional gain applied to the instantaneous linear position errors.
16    ///
17    /// This is usually set to a multiple of the inverse of simulation step time
18    /// (e.g. `60` if the delta-time is `1.0 / 60.0`).
19    pub lin_kp: Vector,
20    /// The Derivative gain applied to the instantaneous linear velocity errors.
21    ///
22    /// This is usually set to a value in `[0.0, 1.0]` where `0.0` implies no damping
23    /// (no correction of velocity errors) and `1.0` implies complete damping (velocity errors
24    /// are corrected in a single simulation step).
25    pub lin_kd: Vector,
26    /// The Proportional gain applied to the instantaneous angular position errors.
27    ///
28    /// This is usually set to a multiple of the inverse of simulation step time
29    /// (e.g. `60` if the delta-time is `1.0 / 60.0`).
30    pub ang_kp: AngVector,
31    /// The Derivative gain applied to the instantaneous angular velocity errors.
32    ///
33    /// This is usually set to a value in `[0.0, 1.0]` where `0.0` implies no damping
34    /// (no correction of velocity errors) and `1.0` implies complete damping (velocity errors
35    /// are corrected in a single simulation step).
36    pub ang_kd: AngVector,
37    /// The axes affected by this controller.
38    ///
39    /// Only coordinate axes with a bit flags set to `true` will be taken into
40    /// account when calculating the errors and corrections.
41    pub axes: AxesMask,
42}
43
44impl Default for PdController {
45    fn default() -> Self {
46        Self::new(60.0, 0.8, AxesMask::all())
47    }
48}
49
50/// A Proportional-Integral-Derivative (PID) controller.
51///
52/// For video games, the Proportional-Derivative [`PdController`] is generally sufficient and
53/// offers an immutable API.
54#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
55#[derive(Debug, Copy, Clone, PartialEq)]
56pub struct PidController {
57    /// The Proportional-Derivative (PD) part of this PID controller.
58    pub pd: PdController,
59    /// The translational error accumulated through time for the Integral part of the PID controller.
60    pub lin_integral: Vector,
61    /// The angular error accumulated through time for the Integral part of the PID controller.
62    pub ang_integral: AngVector,
63    /// The linear gain applied to the Integral part of the PID controller.
64    pub lin_ki: Vector,
65    /// The angular gain applied to the Integral part of the PID controller.
66    pub ang_ki: AngVector,
67}
68
69impl Default for PidController {
70    fn default() -> Self {
71        Self::new(60.0, 1.0, 0.8, AxesMask::all())
72    }
73}
74
75/// Position or velocity errors measured for PID control.
76pub struct PdErrors {
77    /// The linear (translational) part of the error.
78    pub linear: Vector,
79    /// The angular (rotational) part of the error.
80    pub angular: AngVector,
81}
82
83impl From<RigidBodyVelocity<Real>> for PdErrors {
84    fn from(vels: RigidBodyVelocity<Real>) -> Self {
85        Self {
86            #[cfg(feature = "dim2")]
87            linear: Vector::new(vels.linvel.x, vels.linvel.y),
88            #[cfg(feature = "dim3")]
89            linear: Vector::new(vels.linvel.x, vels.linvel.y, vels.linvel.z),
90            #[cfg(feature = "dim2")]
91            angular: vels.angvel,
92            #[cfg(feature = "dim3")]
93            angular: AngVector::new(vels.angvel.x, vels.angvel.y, vels.angvel.z),
94        }
95    }
96}
97
98impl PdController {
99    /// Initialized the PD controller with uniform gain.
100    ///
101    /// The same gain are applied on all axes. To configure per-axes gains, construct
102    /// the [`PdController`] by setting its fields explicitly instead.
103    ///
104    /// Only the axes specified in `axes` will be enabled (but the gain values are set
105    /// on all axes regardless).
106    pub fn new(kp: Real, kd: Real, axes: AxesMask) -> PdController {
107        #[cfg(feature = "dim2")]
108        return Self {
109            lin_kp: Vector::splat(kp),
110            lin_kd: Vector::splat(kd),
111            ang_kp: kp,
112            ang_kd: kd,
113            axes,
114        };
115
116        #[cfg(feature = "dim3")]
117        return Self {
118            lin_kp: Vector::splat(kp),
119            lin_kd: Vector::splat(kd),
120            ang_kp: AngVector::splat(kp),
121            ang_kd: AngVector::splat(kd),
122            axes,
123        };
124    }
125
126    /// Calculates the linear correction from positional and velocity errors calculated automatically
127    /// from a rigid-body and the desired positions/velocities.
128    ///
129    /// The unit of the returned value depends on the gain values. In general, `kd` is proportional to
130    /// the inverse of the simulation step so the returned value is a linear rigid-body velocity
131    /// change.
132    pub fn linear_rigid_body_correction(
133        &self,
134        rb: &RigidBody,
135        target_pos: Vector,
136        target_linvel: Vector,
137    ) -> Vector {
138        let angvel = rb.angvel();
139
140        self.rigid_body_correction(
141            rb,
142            Pose::from_translation(target_pos),
143            RigidBodyVelocity {
144                linvel: target_linvel,
145                angvel,
146            },
147        )
148        .linvel
149    }
150
151    /// Calculates the angular correction from positional and velocity errors calculated automatically
152    /// from a rigid-body and the desired positions/velocities.
153    ///
154    /// The unit of the returned value depends on the gain values. In general, `kd` is proportional to
155    /// the inverse of the simulation step so the returned value is an angular rigid-body velocity
156    /// change.
157    pub fn angular_rigid_body_correction(
158        &self,
159        rb: &RigidBody,
160        target_rot: Rotation,
161        target_angvel: AngVector,
162    ) -> AngVector {
163        self.rigid_body_correction(
164            rb,
165            Pose::from_parts(Vector::ZERO, target_rot),
166            RigidBodyVelocity {
167                linvel: rb.linvel(),
168                angvel: target_angvel,
169            },
170        )
171        .angvel
172    }
173
174    /// Calculates the linear and angular  correction from positional and velocity errors calculated
175    /// automatically from a rigid-body and the desired poses/velocities.
176    ///
177    /// The unit of the returned value depends on the gain values. In general, `kd` is proportional to
178    /// the inverse of the simulation step so the returned value is a rigid-body velocity
179    /// change.
180    pub fn rigid_body_correction(
181        &self,
182        rb: &RigidBody,
183        target_pose: Pose,
184        target_vels: RigidBodyVelocity<Real>,
185    ) -> RigidBodyVelocity<Real> {
186        let pose_errors = RigidBodyPosition {
187            position: rb.pos.position,
188            next_position: target_pose,
189        }
190        .pose_errors(rb.local_center_of_mass());
191        let vels_errors = target_vels - rb.vels;
192        self.correction(&pose_errors, &vels_errors.into())
193    }
194
195    /// Mask where each component is 1.0 or 0.0 depending on whether
196    /// the corresponding linear axis is enabled.
197    fn lin_mask(&self) -> Vector {
198        #[cfg(feature = "dim2")]
199        return Vector::new(
200            self.axes.contains(AxesMask::LIN_X) as u32 as Real,
201            self.axes.contains(AxesMask::LIN_Y) as u32 as Real,
202        );
203        #[cfg(feature = "dim3")]
204        return Vector::new(
205            self.axes.contains(AxesMask::LIN_X) as u32 as Real,
206            self.axes.contains(AxesMask::LIN_Y) as u32 as Real,
207            self.axes.contains(AxesMask::LIN_Z) as u32 as Real,
208        );
209    }
210
211    /// Mask where each component is 1.0 or 0.0 depending on whether
212    /// the corresponding angular axis is enabled.
213    fn ang_mask(&self) -> AngVector {
214        #[cfg(feature = "dim2")]
215        return self.axes.contains(AxesMask::ANG_Z) as u32 as Real;
216        #[cfg(feature = "dim3")]
217        return Vector::new(
218            self.axes.contains(AxesMask::ANG_X) as u32 as Real,
219            self.axes.contains(AxesMask::ANG_Y) as u32 as Real,
220            self.axes.contains(AxesMask::ANG_Z) as u32 as Real,
221        );
222    }
223
224    /// Calculates the linear and angular correction from the given positional and velocity errors.
225    ///
226    /// The unit of the returned value depends on the gain values. In general, `kd` is proportional to
227    /// the inverse of the simulation step so the returned value is a rigid-body velocity
228    /// change.
229    pub fn correction(
230        &self,
231        pose_errors: &PdErrors,
232        vel_errors: &PdErrors,
233    ) -> RigidBodyVelocity<Real> {
234        let lin_mask = self.lin_mask();
235        let ang_mask = self.ang_mask();
236
237        let linvel =
238            (pose_errors.linear * self.lin_kp + vel_errors.linear * self.lin_kd) * lin_mask;
239        let angvel =
240            (pose_errors.angular * self.ang_kp + vel_errors.angular * self.ang_kd) * ang_mask;
241
242        RigidBodyVelocity { linvel, angvel }
243    }
244}
245
246impl PidController {
247    /// Initialized the PDI controller with uniform gain.
248    ///
249    /// The same gain are applied on all axes. To configure per-axes gains, construct
250    /// the [`PidController`] by setting its fields explicitly instead.
251    ///
252    /// Only the axes specified in `axes` will be enabled (but the gain values are set
253    /// on all axes regardless).
254    pub fn new(kp: Real, ki: Real, kd: Real, axes: AxesMask) -> PidController {
255        #[cfg(feature = "dim2")]
256        return Self {
257            pd: PdController::new(kp, kd, axes),
258            lin_integral: Vector::ZERO,
259            ang_integral: 0.0,
260            lin_ki: Vector::splat(ki),
261            ang_ki: ki,
262        };
263
264        #[cfg(feature = "dim3")]
265        return Self {
266            pd: PdController::new(kp, kd, axes),
267            lin_integral: Vector::ZERO,
268            ang_integral: AngVector::ZERO,
269            lin_ki: Vector::splat(ki),
270            ang_ki: AngVector::splat(ki),
271        };
272    }
273
274    /// Set the axes errors and corrections are computed for.
275    ///
276    /// This doesn’t modify any of the gains.
277    pub fn set_axes(&mut self, axes: AxesMask) {
278        self.pd.axes = axes;
279    }
280
281    /// Get the axes errors and corrections are computed for.
282    pub fn axes(&self) -> AxesMask {
283        self.pd.axes
284    }
285
286    /// Resets to zero the accumulated linear and angular errors used by
287    /// the Integral part of the controller.
288    pub fn reset_integrals(&mut self) {
289        self.lin_integral = Vector::ZERO;
290        #[cfg(feature = "dim2")]
291        {
292            self.ang_integral = 0.0;
293        }
294        #[cfg(feature = "dim3")]
295        {
296            self.ang_integral = AngVector::ZERO;
297        }
298    }
299
300    /// Calculates the linear correction from positional and velocity errors calculated automatically
301    /// from a rigid-body and the desired positions/velocities.
302    ///
303    /// The unit of the returned value depends on the gain values. In general, `kd` is proportional to
304    /// the inverse of the simulation step so the returned value is a linear rigid-body velocity
305    /// change.
306    ///
307    /// This method is mutable because of the need to update the accumulated positional
308    /// errors for the Integral part of this controller. Prefer the [`PdController`] instead if
309    /// an immutable API is needed.
310    pub fn linear_rigid_body_correction(
311        &mut self,
312        dt: Real,
313        rb: &RigidBody,
314        target_pos: Vector,
315        target_linvel: Vector,
316    ) -> Vector {
317        self.rigid_body_correction(
318            dt,
319            rb,
320            Pose::from_translation(target_pos),
321            RigidBodyVelocity {
322                linvel: target_linvel,
323                angvel: rb.angvel(),
324            },
325        )
326        .linvel
327    }
328
329    /// Calculates the angular correction from positional and velocity errors calculated automatically
330    /// from a rigid-body and the desired positions/velocities.
331    ///
332    /// The unit of the returned value depends on the gain values. In general, `kd` is proportional to
333    /// the inverse of the simulation step so the returned value is an angular rigid-body velocity
334    /// change.
335    ///
336    /// This method is mutable because of the need to update the accumulated positional
337    /// errors for the Integral part of this controller. Prefer the [`PdController`] instead if
338    /// an immutable API is needed.
339    pub fn angular_rigid_body_correction(
340        &mut self,
341        dt: Real,
342        rb: &RigidBody,
343        target_rot: Rotation,
344        target_angvel: AngVector,
345    ) -> AngVector {
346        self.rigid_body_correction(
347            dt,
348            rb,
349            Pose::from_parts(Vector::ZERO, target_rot),
350            RigidBodyVelocity {
351                linvel: rb.linvel(),
352                angvel: target_angvel,
353            },
354        )
355        .angvel
356    }
357
358    /// Calculates the linear and angular  correction from positional and velocity errors calculated
359    /// automatically from a rigid-body and the desired poses/velocities.
360    ///
361    /// The unit of the returned value depends on the gain values. In general, `kd` is proportional to
362    /// the inverse of the simulation step so the returned value is a rigid-body velocity
363    /// change.
364    ///
365    /// This method is mutable because of the need to update the accumulated positional
366    /// errors for the Integral part of this controller. Prefer the [`PdController`] instead if
367    /// an immutable API is needed.
368    pub fn rigid_body_correction(
369        &mut self,
370        dt: Real,
371        rb: &RigidBody,
372        target_pose: Pose,
373        target_vels: RigidBodyVelocity<Real>,
374    ) -> RigidBodyVelocity<Real> {
375        let pose_errors = RigidBodyPosition {
376            position: rb.pos.position,
377            next_position: target_pose,
378        }
379        .pose_errors(rb.local_center_of_mass());
380        let vels_errors = target_vels - rb.vels;
381        self.correction(dt, &pose_errors, &vels_errors.into())
382    }
383
384    /// Calculates the linear and angular correction from the given positional and velocity errors.
385    ///
386    /// The unit of the returned value depends on the gain values. In general, `kd` is proportional to
387    /// the inverse of the simulation step so the returned value is a rigid-body velocity
388    /// change.
389    ///
390    /// This method is mutable because of the need to update the accumulated positional
391    /// errors for the Integral part of this controller. Prefer the [`PdController`] instead if
392    /// an immutable API is needed.
393    pub fn correction(
394        &mut self,
395        dt: Real,
396        pose_errors: &PdErrors,
397        vel_errors: &PdErrors,
398    ) -> RigidBodyVelocity<Real> {
399        self.lin_integral += pose_errors.linear * dt;
400        self.ang_integral += pose_errors.angular * dt;
401
402        let lin_mask = self.pd.lin_mask();
403        let ang_mask = self.pd.ang_mask();
404
405        let linvel = (pose_errors.linear * self.pd.lin_kp
406            + vel_errors.linear * self.pd.lin_kd
407            + self.lin_integral * self.lin_ki)
408            * lin_mask;
409        #[cfg(feature = "dim2")]
410        let angvel = (pose_errors.angular * self.pd.ang_kp
411            + vel_errors.angular * self.pd.ang_kd
412            + self.ang_integral * self.ang_ki)
413            * ang_mask;
414        #[cfg(feature = "dim3")]
415        let angvel = (pose_errors.angular * self.pd.ang_kp
416            + vel_errors.angular * self.pd.ang_kd
417            + self.ang_integral * self.ang_ki)
418            * ang_mask;
419
420        RigidBodyVelocity { linvel, angvel }
421    }
422}