Skip to main content

he3d_physics/
shape.rs

1use core::f32::consts::PI;
2
3use he3d_math::{Quat, Vec3};
4
5use crate::aabb::Aabb;
6
7/// How a Body's mass is distributed: inverse mass and inverse diagonal inertia.
8/// Body 的质量分布:逆质量与逆惯量对角阵。
9///
10/// The inertia is stored as three inverse values along the Body's **local** axes.
11/// Every supported Shape is symmetric about its local axes, so a diagonal tensor
12/// is exact and a full 3x3 matrix is never needed.
13/// 惯量以沿 Body **局部**轴的三组逆值存储。所有受支持的 Shape 都关于局部轴对称,因此
14/// 对角阵是精确的,永远不需要完整的 3x3 矩阵。
15#[derive(Clone, Copy, Debug, PartialEq)]
16pub struct MassProperties {
17   /// Inverse mass. Zero means nothing can accelerate the Body.
18   /// 逆质量。为零表示没有任何东西能加速该 Body。
19   pub inv_mass: f32,
20   /// Inverse inertia per local axis. A zero component means the Body does not
21   /// resist rotation about that axis.
22   /// 局部各轴的逆惯量。某分量为零表示 Body 不抵抗绕该轴的旋转。
23   pub inv_inertia: Vec3,
24}
25
26impl MassProperties {
27   /// The properties of a Body that nothing can accelerate.
28   /// 没有任何东西能加速的 Body 的质量属性。
29   pub const IMMOVABLE: Self = Self {
30      inv_mass: 0.0,
31      inv_inertia: Vec3::ZERO,
32   };
33
34   /// Whether this Body can be accelerated at all.
35   /// 该 Body 是否根本不能被加速。
36   #[must_use]
37   pub fn is_immovable(&self) -> bool {
38      self.inv_mass == 0.0 && self.inv_inertia == Vec3::ZERO
39   }
40}
41
42/// A convex collision shape.
43/// 凸碰撞形状。
44///
45/// Cylinder, cone, and convex hull are planned but not implemented yet; see
46/// `docs/physics-spec.md` §4.
47/// 圆柱、圆锥与凸包已规划但尚未实现;见 `docs/physics-spec.md` §4。
48#[derive(Clone, Copy, Debug, PartialEq)]
49#[non_exhaustive]
50pub enum Shape {
51   /// An axis-aligned box described by its half extents.
52   /// 由半长描述的轴对齐盒。
53   Box {
54      /// Half the box's size on each local axis.
55      /// 盒在局部各轴上尺寸的一半。
56      half_extents: Vec3,
57   },
58   /// A sphere described by its radius.
59   /// 由半径描述的球。
60   Sphere {
61      /// The sphere's radius.
62      /// 球的半径。
63      radius: f32,
64   },
65}
66
67impl Shape {
68   /// An axis-aligned box with the given half extents.
69   /// 给定半长的轴对齐盒。
70   ///
71   /// Half extents are clamped to be non-negative, so a negative input yields a
72   /// degenerate (zero-volume) box rather than an inverted one.
73   /// 半长被钳制为非负,因此负输入得到退化的(零体积)盒,而不是内外翻转的盒。
74   #[must_use]
75   pub fn cuboid(half_extents: Vec3) -> Self {
76      Self::Box {
77         half_extents: half_extents.max(Vec3::ZERO),
78      }
79   }
80
81   /// A sphere with the given radius, clamped to be non-negative.
82   /// 给定半径的球;半径被钳制为非负。
83   #[must_use]
84   pub fn sphere(radius: f32) -> Self {
85      Self::Sphere {
86         radius: radius.max(0.0),
87      }
88   }
89
90   /// The shape's volume, used to sanity-check mass input.
91   /// 形状的体积,用于校验质量输入。
92   #[must_use]
93   pub fn volume(&self) -> f32 {
94      match *self {
95         Self::Box { half_extents } => 8.0 * half_extents.x * half_extents.y * half_extents.z,
96         Self::Sphere { radius } => 4.0 / 3.0 * PI * radius * radius * radius,
97      }
98   }
99
100   /// The local inertia tensor's diagonal for a given total mass.
101   /// 给定总质量时,局部惯量张量的对角元。
102   #[must_use]
103   pub fn local_inertia(&self, mass: f32) -> Vec3 {
104      match *self {
105         Self::Box { half_extents } => {
106            let h = half_extents;
107            // A solid box: I_xx = m/3 * (hy^2 + hz^2), and cyclically.
108            // 实心长方体:I_xx = m/3 * (hy^2 + hz^2),其余按轮换。
109            Vec3::new(
110               mass / 3.0 * (h.y * h.y + h.z * h.z),
111               mass / 3.0 * (h.x * h.x + h.z * h.z),
112               mass / 3.0 * (h.x * h.x + h.y * h.y),
113            )
114         }
115         Self::Sphere { radius } => {
116            // A solid sphere is isotropic: I = 2/5 * m * r^2.
117            // 实心球是各向同性的:I = 2/5 * m * r^2。
118            Vec3::splat(0.4 * mass * radius * radius)
119         }
120      }
121   }
122
123   /// Inverse mass properties for a given total mass.
124   /// 给定总质量时的逆质量属性。
125   ///
126   /// A non-finite or non-positive `mass` yields [`MassProperties::IMMOVABLE`]. A
127   /// component of the inertia tensor that is zero (a degenerate shape) yields a
128   /// zero inverse on that axis, so the Body simply never resists that rotation
129   /// instead of producing `inf`.
130   /// 非有限或非正的 `mass` 返回 [`MassProperties::IMMOVABLE`]。惯量张量的某个分量为零
131   /// (退化形状)时,该轴的逆值为零,于是 Body 只是不抵抗该轴旋转,而不会产生 `inf`。
132   #[must_use]
133   pub fn mass_properties(&self, mass: f32) -> MassProperties {
134      if !mass.is_finite() || mass <= 0.0 {
135         return MassProperties::IMMOVABLE;
136      }
137      let inertia = self.local_inertia(mass);
138      MassProperties {
139         inv_mass: 1.0 / mass,
140         inv_inertia: Vec3::new(
141            inverse_or_zero(inertia.x),
142            inverse_or_zero(inertia.y),
143            inverse_or_zero(inertia.z),
144         ),
145      }
146   }
147
148   /// The world-space bounding box of this shape placed at `position` with
149   /// `orientation`.
150   /// 该形状以 `position` 与 `orientation` 摆放时的世界空间包围盒。
151   #[must_use]
152   pub fn aabb(&self, position: Vec3, orientation: Quat) -> Aabb {
153      match *self {
154         Self::Box { half_extents } => {
155            // Rotating the local basis and taking absolute values gives the
156            // world-axis extents directly: extent_i = sum_j |R[i][j]| * h_j.
157            // 旋转局部基向量并取绝对值即可直接得到世界轴向上的半长:
158            // extent_i = sum_j |R[i][j]| * h_j。
159            let x = orientation * Vec3::X;
160            let y = orientation * Vec3::Y;
161            let z = orientation * Vec3::Z;
162            let abs = |v: Vec3| Vec3::new(v.x.abs(), v.y.abs(), v.z.abs());
163            let (ax, ay, az) = (abs(x), abs(y), abs(z));
164            let extent = Vec3::new(
165               ax.x * half_extents.x + ay.x * half_extents.y + az.x * half_extents.z,
166               ax.y * half_extents.x + ay.y * half_extents.y + az.y * half_extents.z,
167               ax.z * half_extents.x + ay.z * half_extents.y + az.z * half_extents.z,
168            );
169            Aabb::from_center_half_extents(position, extent)
170         }
171         Self::Sphere { radius } => Aabb::from_center_half_extents(position, Vec3::splat(radius)),
172      }
173   }
174}
175
176/// `1.0 / value`, or zero when the value is not positive and finite.
177/// `1.0 / value`,当值非正或非有限时返回零。
178fn inverse_or_zero(value: f32) -> f32 {
179   if value.is_finite() && value > 0.0 {
180      1.0 / value
181   } else {
182      0.0
183   }
184}
185
186#[cfg(test)]
187mod tests {
188   use super::{MassProperties, Shape};
189   use he3d_math::{Quat, Vec3};
190
191   #[test]
192   fn cuboid_inertia_matches_the_solid_box_formula() {
193      let shape = Shape::cuboid(Vec3::new(1.0, 2.0, 3.0));
194      let inertia = shape.local_inertia(12.0);
195      // m/3 * (hy^2 + hz^2) = 4 * (4 + 9) = 52.
196      assert!((inertia.x - 52.0).abs() < 1e-4);
197      // m/3 * (hx^2 + hz^2) = 4 * (1 + 9) = 40.
198      assert!((inertia.y - 40.0).abs() < 1e-4);
199      // m/3 * (hx^2 + hy^2) = 4 * (1 + 4) = 20.
200      assert!((inertia.z - 20.0).abs() < 1e-4);
201   }
202
203   #[test]
204   fn sphere_inertia_is_isotropic() {
205      let shape = Shape::sphere(2.0);
206      let inertia = shape.local_inertia(5.0);
207      let expected = 0.4 * 5.0 * 4.0;
208      assert!((inertia.x - expected).abs() < 1e-4);
209      assert!((inertia.y - expected).abs() < 1e-4);
210      assert!((inertia.z - expected).abs() < 1e-4);
211   }
212
213   #[test]
214   fn non_positive_mass_is_immovable() {
215      assert_eq!(
216         Shape::sphere(1.0).mass_properties(0.0),
217         MassProperties::IMMOVABLE
218      );
219      assert_eq!(
220         Shape::sphere(1.0).mass_properties(f32::NAN),
221         MassProperties::IMMOVABLE
222      );
223      assert!(Shape::sphere(1.0).mass_properties(0.0).is_immovable());
224   }
225
226   #[test]
227   fn degenerate_shape_yields_zero_inverse_inertia() {
228      let flat = Shape::cuboid(Vec3::new(1.0, 0.0, 1.0));
229      let props = flat.mass_properties(2.0);
230      // Zero extent on Y means the box has no resistance about Y; no infinities.
231      assert!(props.inv_inertia.y.is_finite());
232      assert!(props.inv_inertia.x.is_finite());
233      assert!(props.inv_mass.is_finite());
234   }
235
236   #[test]
237   fn axis_aligned_aabb_is_exact() {
238      let shape = Shape::cuboid(Vec3::new(1.0, 2.0, 3.0));
239      let aabb = shape.aabb(Vec3::new(10.0, 0.0, 0.0), Quat::IDENTITY);
240      assert!((aabb.min.x - 9.0).abs() < 1e-5);
241      assert!((aabb.max.x - 11.0).abs() < 1e-5);
242      assert!((aabb.max.y - 2.0).abs() < 1e-5);
243      assert!((aabb.max.z - 3.0).abs() < 1e-5);
244   }
245
246   #[test]
247   fn rotated_aabb_grows_to_cover_the_corners() {
248      let shape = Shape::cuboid(Vec3::splat(1.0));
249      // A 45 degree turn about Z makes the diagonal the X/Y extent.
250      let aabb = shape.aabb(
251         Vec3::ZERO,
252         Quat::from_rotation_z(core::f32::consts::FRAC_PI_4),
253      );
254      let expected = 2.0_f32.sqrt();
255      assert!((aabb.max.x - expected).abs() < 1e-4);
256      assert!((aabb.max.y - expected).abs() < 1e-4);
257      assert!((aabb.max.z - 1.0).abs() < 1e-5);
258   }
259
260   #[test]
261   fn negative_extents_are_clamped_not_inverted() {
262      assert_eq!(
263         Shape::cuboid(Vec3::splat(-1.0)),
264         Shape::Box {
265            half_extents: Vec3::ZERO
266         }
267      );
268      assert_eq!(Shape::sphere(-2.0), Shape::Sphere { radius: 0.0 });
269   }
270}