he3d_physics/contact.rs
1use he3d_math::Vec3;
2
3use crate::body::BodyId;
4use crate::static_geometry::StaticMeshId;
5
6/// The maximum number of points a single pair of Bodies can produce.
7/// 一对 Body 最多能产生的接触点数。
8///
9/// Clipping one convex face against another yields at most one vertex per edge of
10/// the reference face, so a box-on-box manifold never exceeds four; and four
11/// coplanar points are the minimum needed to pin a resting face against rocking
12/// about either axis. The constraint arrays are sized to this, so it is also the
13/// compile-time capacity.
14/// 把一个凸面裁剪到另一个上,最多为参考面的每条棱产生一个顶点,因此盒对盒的流形不会超过
15/// 四个;而四个共面点正是把一个静止面在两个轴上都钉住、不让它摇摆所需的最小数量。约束数组
16/// 按此分配,因此它同时也是编译期容量。
17pub(crate) const MAX_MANIFOLD_POINTS: usize = 4;
18
19/// Whether a Contact is appearing, persisting, or disappearing this step.
20/// 该 Contact 本步是正在出现、持续存在,还是正在消失。
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22#[non_exhaustive]
23pub enum ContactPhase {
24 /// The pair touched this step and did not touch last step.
25 /// 本步相碰,上一步未相碰。
26 Begin,
27 /// The pair touched last step and still touches.
28 /// 上一步相碰,本步仍相碰。
29 Persist,
30 /// The pair touched last step and does not touch now.
31 /// 上一步相碰,本步不再相碰。
32 End,
33}
34
35/// What lies on the far side of a Contact's normal.
36/// Contact 法线另一侧的东西。
37///
38/// A Contact is always anchored on a Body; the other side is either a second Body
39/// or a piece of Static geometry, which has no Body of its own.
40/// 一个 Contact 总以某个 Body 为一端;另一端要么是另一个 Body,要么是一块本身没有 Body
41/// 的 Static geometry。
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43#[non_exhaustive]
44pub enum ContactPartner {
45 /// Another Body.
46 /// 另一个 Body。
47 Body(BodyId),
48 /// A registered piece of Static geometry.
49 /// 一块已注册的 Static geometry。
50 Static(StaticMeshId),
51}
52
53/// A single point at which two things touch.
54/// 两样东西相互接触的单个点。
55///
56/// The normal points from `a` toward `b`; `depth` is how far the two shapes
57/// overlap along it. A pair that separates still reports an `End` contact built
58/// from the last state in which it overlapped, so an application can always
59/// observe both ends of a contact's lifetime.
60/// 法线由 `a` 指向 `b`;`depth` 是两形状沿该方向的重叠量。已经分离的一对仍会报出一个
61/// 由最后一次重叠状态构造的 `End` 接触,因此应用总能观察到一段接触生命周期的两端。
62#[derive(Clone, Copy, Debug, PartialEq)]
63#[non_exhaustive]
64pub struct Contact {
65 /// The Body the normal points away from.
66 /// 法线背离的那个 Body。
67 pub a: BodyId,
68 /// What the normal points toward.
69 /// 法线指向的东西。
70 pub b: ContactPartner,
71 /// The contact point, in world space.
72 /// 接触点(世界空间)。
73 pub point: Vec3,
74 /// The unit contact normal, pointing from `a` toward `b`.
75 /// 单位接触法线,由 `a` 指向 `b`。
76 pub normal: Vec3,
77 /// Penetration depth along the normal. Always positive for a live Contact.
78 /// 沿法线的穿透深度。存活接触恒为正。
79 pub depth: f32,
80 /// Where this Contact is in its lifetime.
81 /// 该接触在其生命周期中的位置。
82 pub phase: ContactPhase,
83}
84
85impl Contact {
86 /// Whether the Contact has the given Body on either side.
87 /// 该 Contact 的任意一侧是否为给定的 Body。
88 #[must_use]
89 pub fn involves(&self, id: BodyId) -> bool {
90 self.a == id || self.b == ContactPartner::Body(id)
91 }
92}
93
94/// One point of a manifold, before it becomes a solved constraint.
95/// 流形中的一个点,尚未成为已求解的约束。
96#[derive(Clone, Copy, Debug, Default)]
97pub(crate) struct ManifoldPoint {
98 /// World-space contact position.
99 pub position: Vec3,
100 /// Penetration depth along the manifold normal.
101 pub depth: f32,
102 /// A stable identifier for warm-start matching between steps.
103 pub feature: u32,
104}
105
106/// The points at which two shapes overlap, with the normal between them.
107/// 两个形状重叠处的接触点,以及它们之间的法线。
108#[derive(Clone, Copy, Debug)]
109pub(crate) struct Manifold {
110 /// Unit normal pointing from the first shape toward the second.
111 pub normal: Vec3,
112 /// The contact points, in a deterministic order.
113 pub points: [ManifoldPoint; MAX_MANIFOLD_POINTS],
114 /// How many entries of `points` are live.
115 pub count: usize,
116}
117
118impl Default for Manifold {
119 fn default() -> Self {
120 Self {
121 normal: Vec3::Z,
122 points: [ManifoldPoint::default(); MAX_MANIFOLD_POINTS],
123 count: 0,
124 }
125 }
126}
127
128impl Manifold {
129 /// An empty manifold.
130 pub(crate) fn empty() -> Self {
131 Self::default()
132 }
133
134 /// Append a point, ignoring anything past the manifold's capacity.
135 pub(crate) fn push(&mut self, point: ManifoldPoint) {
136 if self.count < MAX_MANIFOLD_POINTS {
137 self.points[self.count] = point;
138 self.count += 1;
139 }
140 }
141}
142
143/// One point of a contact after the solver has claimed it.
144/// 求解器接管后的单个接触点。
145///
146/// `anchor_a` and `anchor_b` are the contact position relative to each Body's
147/// center of mass, captured once so that every solver iteration applies its
148/// impulse at exactly the same point.
149/// `anchor_a` 与 `anchor_b` 是接触点相对于各 Body 质心的位置,只捕获一次,使每次求解
150/// 迭代都在完全相同的点上施加冲量。
151#[derive(Clone, Copy, Debug, Default)]
152pub(crate) struct ConstraintPoint {
153 /// World-space contact position, captured when the constraint was built.
154 pub position: Vec3,
155 /// Contact position relative to Body `a`'s center.
156 pub anchor_a: Vec3,
157 /// Contact position relative to Body `b`'s center.
158 pub anchor_b: Vec3,
159 /// Penetration depth at the start of the step.
160 pub depth: f32,
161 /// Feature id, used to carry impulses across steps.
162 pub feature: u32,
163 /// Accumulated normal impulse.
164 pub normal_impulse: f32,
165 /// Accumulated impulse along each friction tangent.
166 pub tangent_impulse: [f32; 2],
167 /// Effective mass along the normal.
168 pub normal_mass: f32,
169 /// Effective mass along each friction tangent.
170 pub tangent_mass: [f32; 2],
171 /// The angular half of the effective mass: `inv_inertia(anchor × axis)` for
172 /// each of the three solve axes, per side.
173 ///
174 /// The velocity solver applies every impulse along one of these three axes, and
175 /// the inertia tensor is linear, so
176 /// `inv_inertia(anchor × (axis * delta)) == inv_inertia(anchor × axis) * delta`.
177 /// Keeping the product of the first two lets an iteration apply an impulse with
178 /// one scalar multiply where it would otherwise do two quaternion rotations —
179 /// and a four-point constraint does twenty-four of those per iteration otherwise.
180 /// The position solve reuses the normal entry.
181 /// 有效质量的角速度部分:三条求解轴上各自的 `inv_inertia(anchor × axis)`,按侧存放。
182 /// 速度求解器施加的每个冲量都沿这三条轴之一,而惯量张量是线性的,因此
183 /// `inv_inertia(anchor × (axis * delta)) == inv_inertia(anchor × axis) * delta`。把前
184 /// 两者相乘的结果存下来,每次迭代施加冲量就只需一次标量乘法,而不必做两次四元数旋转——否则
185 /// 四点的约束每次迭代要做二十四次。位置求解复用其中的法线项。
186 ///
187 /// Indexed `[side][axis]`, where the axes are the normal and then the two
188 /// tangents, and the second side is zero for Static geometry.
189 /// 索引为 `[side][axis]`:轴依次是法线与两条切线;第二侧为 Static geometry 时为零。
190 pub angular_response: [[Vec3; 3]; 2],
191 /// Velocity bias from restitution, applied to real velocities.
192 pub bias: f32,
193 /// Penetration bias, applied only to pseudo-velocities so that removing
194 /// overlap never leaves kinetic energy behind.
195 pub position_bias: f32,
196 /// Accumulated pseudo-impulse that removes penetration.
197 pub position_impulse: f32,
198}
199
200/// A solved constraint: one normal, two friction tangents, and up to four points.
201/// 一条已求解的约束:一条法线、两条摩擦切线,以及至多四个接触点。
202#[derive(Clone, Copy, Debug)]
203pub(crate) struct ContactConstraint {
204 /// The handle of the first Body, kept so an `End` contact can still name it
205 /// after the Body itself has been removed.
206 /// 第一个 Body 的句柄。保留它是为了在 Body 已被移除之后,`End` 接触仍能指代它。
207 pub id_a: BodyId,
208 /// What lies on the other side.
209 /// 另一侧是什么。
210 pub id_b: ContactPartner,
211 /// Slot index of the first Body.
212 /// 第一个 Body 的槽位索引。
213 pub a: u32,
214 /// Slot index of the other Body, or [`crate::storage::STATIC_PARTNER`] when the
215 /// other side is Static geometry.
216 /// 另一侧 Body 的槽位索引;当另一侧是 Static geometry 时为
217 /// [`crate::storage::STATIC_PARTNER`]。
218 pub b: u32,
219 /// Distinguishes constraints that share a Body pair.
220 ///
221 /// All of a Body's contacts against Static geometry share the same `(a, b)`, so
222 /// this carries the mesh and triangle identity instead. It is zero for a
223 /// Body-versus-Body constraint.
224 /// 区分共享同一对 `(a, b)` 的约束。一个 Body 与 Static geometry 的所有接触共享同一个
225 /// `(a, b)`,因此这里改为携带网格与三角形的身份。Body 对 Body 的约束此值为零。
226 pub partner_key: u64,
227 /// Unit normal from `a` toward `b`.
228 /// 由 `a` 指向 `b` 的单位法线。
229 pub normal: Vec3,
230 /// The two friction tangents, orthogonal to the normal and to each other.
231 /// 两条摩擦切线,与法线正交且彼此正交。
232 pub tangents: [Vec3; 2],
233 /// Combined Coulomb friction coefficient.
234 /// 合并后的库仑摩擦系数。
235 pub friction: f32,
236 /// How many entries of `points` are live.
237 /// `points` 中存活的条目数。
238 pub point_count: u8,
239 /// The contact points.
240 /// 接触点。
241 pub points: [ConstraintPoint; MAX_MANIFOLD_POINTS],
242}
243
244impl Default for ContactConstraint {
245 fn default() -> Self {
246 Self {
247 id_a: BodyId::new(0, 0),
248 id_b: ContactPartner::Body(BodyId::new(0, 0)),
249 a: 0,
250 b: 0,
251 partner_key: 0,
252 normal: Vec3::Z,
253 tangents: [Vec3::X, Vec3::Y],
254 friction: 0.0,
255 point_count: 0,
256 points: [ConstraintPoint::default(); MAX_MANIFOLD_POINTS],
257 }
258 }
259}
260
261impl ContactConstraint {
262 /// The live points.
263 /// 存活的接触点。
264 pub(crate) fn points(&self) -> &[ConstraintPoint] {
265 &self.points[..usize::from(self.point_count)]
266 }
267
268 /// The live points, mutably.
269 /// 存活的接触点(可变)。
270 pub(crate) fn points_mut(&mut self) -> &mut [ConstraintPoint] {
271 let count = usize::from(self.point_count);
272 &mut self.points[..count]
273 }
274
275 /// The key that identifies this constraint across steps.
276 ///
277 /// Both constraint lists are sorted by it, so warm starting can match a pair
278 /// with a binary search.
279 /// 跨步识别这条约束的键。两份约束列表都按它排序,因此 warm starting 可以用二分查找
280 /// 匹配同一对。
281 pub(crate) fn sort_key(&self) -> (u32, u32, u64) {
282 (self.a, self.b, self.partner_key)
283 }
284}
285
286/// Build an orthonormal basis around `normal`, returned as the two tangents.
287/// 围绕 `normal` 构造一组正交基,返回两条切线。
288pub(crate) fn tangent_basis(normal: Vec3) -> [Vec3; 2] {
289 // Pick the axis least aligned with the normal so the cross product is stable.
290 // 选取与法线最不共线的轴,使叉积保持数值稳定。
291 let helper = if normal.x.abs() > normal.y.abs() {
292 Vec3::new(0.0, 1.0, 0.0)
293 } else {
294 Vec3::new(1.0, 0.0, 0.0)
295 };
296 let first = normal.cross(helper).normalize_or_zero();
297 let second = normal.cross(first);
298 [first, second]
299}
300
301#[cfg(test)]
302// Test fixtures turn small loop counters into depths; the values sit far inside
303// the range f32 represents exactly.
304// 测试夹具把小整数循环计数转换为深度;这些值远在 f32 能精确表示的范围内。
305#[allow(clippy::cast_precision_loss)]
306mod tests {
307 use super::{ContactConstraint, Manifold, ManifoldPoint, tangent_basis};
308 use he3d_math::Vec3;
309
310 #[test]
311 fn a_manifold_stops_at_its_capacity() {
312 let mut manifold = Manifold::empty();
313 for index in 0..10 {
314 manifold.push(ManifoldPoint {
315 depth: index as f32,
316 ..ManifoldPoint::default()
317 });
318 }
319 assert_eq!(manifold.count, 4);
320 }
321
322 #[test]
323 fn tangents_are_orthonormal_to_the_normal() {
324 for normal in [
325 Vec3::X,
326 Vec3::Y,
327 Vec3::Z,
328 Vec3::new(1.0, 2.0, 3.0).normalize(),
329 ] {
330 let [t0, t1] = tangent_basis(normal);
331 assert!(normal.dot(t0).abs() < 1e-5, "t0 not orthogonal");
332 assert!(normal.dot(t1).abs() < 1e-5, "t1 not orthogonal");
333 assert!(t0.dot(t1).abs() < 1e-5, "tangents not orthogonal");
334 assert!((t0.length() - 1.0).abs() < 1e-5);
335 assert!((t1.length() - 1.0).abs() < 1e-5);
336 }
337 }
338
339 #[test]
340 fn an_empty_constraint_has_no_points() {
341 let constraint = ContactConstraint::default();
342 assert!(constraint.points().is_empty());
343 }
344}