Skip to main content

he3d_physics/
world.rs

1use alloc::vec::Vec;
2
3use gp3d::{BodyDesc, BodyHandle, Motion};
4use he3d_core::{Mesh, ObjectId, Scene, Transform};
5use he3d_math::Vec3;
6
7use crate::StepResult;
8
9/// Default gravity, matching the reference engine's `(0, -9.8, 0)`.
10/// 默认重力,与参考引擎的 `(0, -9.8, 0)` 一致。
11const DEFAULT_GRAVITY: Vec3 = Vec3::new(0.0, -9.8, 0.0);
12
13/// A physics world mapping render objects by stable [`ObjectId`] onto the
14/// standalone [`gp3d`] engine's generational body handles.
15/// 一个通过稳定的 [`ObjectId`] 将渲染对象映射到独立 [`gp3d`] 引擎代际刚体句柄上的物理世界。
16///
17/// Bodies and colliders are owned by gp3d.
18/// 刚体和碰撞体由 gp3d 持有。
19/// After stepping, call [`PhysicsWorld::synchronize`] to write body transforms back into the scene so the renderer sees the physics state.
20/// 步进后,调用 [`PhysicsWorld::synchronize`] 将刚体变换写回场景,使渲染器看到物理状态。
21///
22/// gp3d and `he3d-math` both wrap `glam`, so `Vec3`/`Quat` interoperate directly.
23/// gp3d 与 `he3d-math` 都包装 `glam`,因此 `Vec3`/`Quat` 可直接互操作。
24pub struct PhysicsWorld {
25    scene: gp3d::PhysicsScene,
26    bodies: Vec<(ObjectId, BodyHandle)>,
27}
28
29impl PhysicsWorld {
30    pub fn new() -> Self {
31        let mut scene = gp3d::PhysicsScene::new();
32        scene.set_gravity(DEFAULT_GRAVITY);
33        Self {
34            scene,
35            bodies: Vec::new(),
36        }
37    }
38
39    /// Override the world gravity vector. 覆盖世界重力向量。
40    pub fn set_gravity(&mut self, gravity: Vec3) {
41        self.scene.set_gravity(gravity);
42    }
43
44    /// The current world gravity. 当前世界重力。
45    pub fn gravity(&self) -> Vec3 {
46        self.scene.gravity()
47    }
48
49    /// Returns `false` if the object already has a body, leaving the world unchanged.
50    /// 若对象已拥有刚体则返回 `false`,世界保持不变。
51    pub fn add_dynamic_box(&mut self, object: ObjectId, half_extents: Vec3, transform: Transform) -> bool {
52        self.add_box(object, half_extents, transform, Motion::Dynamic)
53    }
54
55    /// Returns `false` if the object already has a body, leaving the world unchanged.
56    /// 若对象已拥有刚体则返回 `false`,世界保持不变。
57    pub fn add_static_box(&mut self, object: ObjectId, half_extents: Vec3, transform: Transform) -> bool {
58        self.add_box(object, half_extents, transform, Motion::Static)
59    }
60
61    /// Returns `false` if the object already has a body, leaving the world unchanged.
62    /// 若对象已拥有刚体则返回 `false`,世界保持不变。
63    pub fn add_kinematic_box(&mut self, object: ObjectId, half_extents: Vec3, transform: Transform) -> bool {
64        self.add_box(object, half_extents, transform, Motion::Kinematic)
65    }
66
67    /// Register a body, rejecting a duplicate `ObjectId` (the reference `AddBody`
68    /// also rejects already-registered objects).
69    /// 注册刚体;拒绝重复的 `ObjectId`(参考实现 `AddBody` 同样拒绝已注册对象)。
70    fn add_box(&mut self, object: ObjectId, half_extents: Vec3, transform: Transform, motion: Motion) -> bool {
71        if self.handle(object).is_some() {
72            return false;
73        }
74        let desc = BodyDesc {
75            motion,
76            collider: gp3d::Collider::Box { half_extents },
77            position: transform.position,
78            orientation: transform.orientation,
79            ..BodyDesc::default()
80        };
81        match self.scene.add_body(desc) {
82            Ok(handle) => {
83                self.bodies.push((object, handle));
84                true
85            }
86            Err(_) => false,
87        }
88    }
89
90    /// Add a static triangle-mesh collider from a [`Mesh`]. The mesh vertices are
91    /// copied; the reference `MeshCollider` reads live mesh geometry, and gp3d
92    /// stores its own copy, so re-adding after the mesh changes is the norm.
93    /// 从 [`Mesh`] 添加静态三角网格碰撞体。网格顶点会被复制;参考 `MeshCollider`
94    /// 读取实时网格几何,而 gp3d 保存自己的副本,因此网格变化后重新添加是常态。
95    pub fn add_static_mesh(&mut self, object: ObjectId, mesh: &Mesh, transform: Transform) -> bool {
96        if self.handle(object).is_some() {
97            return false;
98        }
99        let vertices = mesh.vertices().to_vec();
100        if vertices.is_empty() || vertices.len() % 3 != 0 {
101            return false;
102        }
103        match self
104            .scene
105            .add_static_mesh(vertices, transform.position, transform.orientation)
106        {
107            Ok(handle) => {
108                self.bodies.push((object, handle));
109                true
110            }
111            Err(_) => false,
112        }
113    }
114
115    /// Teleport a body, keeping its velocity. Returns `false` for unknown objects.
116    /// 瞬移刚体并保持其速度。对未知对象返回 `false`。
117    pub fn set_transform(&mut self, object: ObjectId, transform: Transform) -> bool {
118        match self.handle(object) {
119            Some(handle) => {
120                self.scene
121                    .set_transform(handle, transform.position, transform.orientation)
122            }
123            None => false,
124        }
125    }
126
127    /// Read the current body transform. Returns `None` for unknown objects.
128    /// 读取当前刚体变换。对未知对象返回 `None`。
129    pub fn transform(&self, object: ObjectId) -> Option<Transform> {
130        let handle = self.handle(object)?;
131        let position = self.scene.position(handle)?;
132        let orientation = self.scene.orientation(handle)?;
133        Some(Transform::from_position_orientation(position, orientation))
134    }
135
136    /// Override the body's friction and restitution coefficients.
137    /// 覆盖刚体的摩擦系数和恢复系数。
138    pub fn set_material(&mut self, object: ObjectId, friction: f32, restitution: f32) -> bool {
139        match self.handle(object) {
140            Some(handle) => self.scene.set_material(handle, friction, restitution),
141            None => false,
142        }
143    }
144
145    /// Override the body's linear and angular damping. 覆盖刚体的线性和角向阻尼。
146    pub fn set_damping(&mut self, object: ObjectId, linear: f32, angular: f32) -> bool {
147        let Some(handle) = self.handle(object) else {
148            return false;
149        };
150        self.scene.set_linear_damping(handle, linear);
151        self.scene.set_angular_damping(handle, angular);
152        true
153    }
154
155    /// Whether the body is currently touching another collider.
156    /// 刚体当前是否与另一碰撞体接触。
157    pub fn has_contacts(&self, object: ObjectId) -> bool {
158        match self.handle(object) {
159            Some(handle) => self.scene.has_contacts(handle),
160            None => false,
161        }
162    }
163
164    pub fn remove_body(&mut self, object: ObjectId) -> bool {
165        let Some(position) = self.bodies.iter().position(|(id, _)| *id == object) else {
166            return false;
167        };
168        let (_, handle) = self.bodies.swap_remove(position);
169        self.scene.remove_body(handle)
170    }
171
172    pub fn has_body(&self, object: ObjectId) -> bool {
173        self.handle(object).is_some()
174    }
175
176    /// Override the body's linear velocity. 覆盖刚体线速度。
177    pub fn set_velocity(&mut self, object: ObjectId, velocity: Vec3) -> bool {
178        match self.handle(object) {
179            Some(handle) => self.scene.set_velocity(handle, velocity),
180            None => false,
181        }
182    }
183
184    pub fn velocity(&self, object: ObjectId) -> Vec3 {
185        match self.handle(object) {
186            Some(handle) => self.scene.velocity(handle),
187            None => Vec3::ZERO,
188        }
189    }
190
191    /// Override the body's angular velocity. 覆盖刚体角速度。
192    pub fn set_angular_velocity(&mut self, object: ObjectId, velocity: Vec3) -> bool {
193        match self.handle(object) {
194            Some(handle) => self.scene.set_angular_velocity(handle, velocity),
195            None => false,
196        }
197    }
198
199    pub fn angular_velocity(&self, object: ObjectId) -> Vec3 {
200        match self.handle(object) {
201            Some(handle) => self.scene.angular_velocity(handle),
202            None => Vec3::ZERO,
203        }
204    }
205
206    pub fn add_force(&mut self, object: ObjectId, force: Vec3) -> bool {
207        match self.handle(object) {
208            Some(handle) => self.scene.add_force(handle, force),
209            None => false,
210        }
211    }
212
213    pub fn add_torque(&mut self, object: ObjectId, torque: Vec3) -> bool {
214        match self.handle(object) {
215            Some(handle) => self.scene.add_torque(handle, torque),
216            None => false,
217        }
218    }
219
220    pub fn add_impulse(&mut self, object: ObjectId, impulse: Vec3) -> bool {
221        match self.handle(object) {
222            Some(handle) => self.scene.add_impulse(handle, impulse),
223            None => false,
224        }
225    }
226
227    pub fn add_angular_impulse(&mut self, object: ObjectId, impulse: Vec3) -> bool {
228        match self.handle(object) {
229            Some(handle) => self.scene.add_angular_impulse(handle, impulse),
230            None => false,
231        }
232    }
233
234    /// Advance the simulation by one fixed step using the reference default of 4
235    /// solver iterations. Forces and torques applied with [`PhysicsWorld::add_force`]
236    /// / [`PhysicsWorld::add_torque`] act for one step only and are then cleared.
237    /// 以参考默认的 4 次求解器迭代推进模拟。由 [`PhysicsWorld::add_force`] /
238    /// [`PhysicsWorld::add_torque`] 施加的力与力矩只作用一步并被清除。
239    pub fn step(&mut self, delta_time: f32) -> StepResult {
240        self.scene.step(delta_time, 4)
241    }
242
243    /// Write body transforms back into the scene's render objects.
244    /// 将刚体变换写回场景的渲染对象。
245    pub fn synchronize(&mut self, scene: &mut Scene) {
246        for &(object, handle) in &self.bodies {
247            let Some(render_object) = scene.objects.get_mut(object) else {
248                continue;
249            };
250            let Some(position) = self.scene.position(handle) else {
251                continue;
252            };
253            let Some(orientation) = self.scene.orientation(handle) else {
254                continue;
255            };
256            render_object.transform.position = position;
257            render_object.transform.orientation = orientation;
258        }
259    }
260
261    fn handle(&self, object: ObjectId) -> Option<BodyHandle> {
262        self.bodies
263            .iter()
264            .find(|(id, _)| *id == object)
265            .map(|(_, handle)| *handle)
266    }
267}
268
269impl Default for PhysicsWorld {
270    fn default() -> Self {
271        Self::new()
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use alloc::vec;
278
279    use super::*;
280    use he3d_core::{ColorRgba8, Material, Mesh, RenderObject, Rgb, Texture};
281    use he3d_math::Quat;
282
283    const DT: f32 = 1.0 / 60.0;
284
285    fn spawn_box(scene: &mut Scene, position: Vec3) -> ObjectId {
286        let mesh = scene.meshes.insert(Mesh::cube(1.0, 1.0, 1.0).expect("cube"));
287        let solid = scene.textures.insert(Texture::solid(ColorRgba8::WHITE));
288        scene.spawn(RenderObject {
289            mesh,
290            texture: Some(solid),
291            transform: Transform::from_position(position),
292            material: Material::new(Rgb::WHITE),
293            visible: true,
294        })
295    }
296
297    /// A flat quad mesh floor with a render object; use with `add_static_mesh`.
298    /// 一个带渲染对象的平坦四边形网格地面;配合 `add_static_mesh` 使用。
299    fn spawn_quad_floor(scene: &mut Scene, half: f32) -> (ObjectId, Mesh) {
300        let h = half;
301        let quad = Mesh::from_vertices(vec![
302            Vec3::new(-h, 0.0, -h),
303            Vec3::new(h, 0.0, h),
304            Vec3::new(h, 0.0, -h),
305            Vec3::new(-h, 0.0, -h),
306            Vec3::new(-h, 0.0, h),
307            Vec3::new(h, 0.0, h),
308        ])
309        .expect("quad");
310        let mesh_id = scene.meshes.insert(quad.clone());
311        let solid = scene.textures.insert(Texture::solid(ColorRgba8::from_rgb(77, 179, 77)));
312        let object = scene.spawn(RenderObject {
313            mesh: mesh_id,
314            texture: Some(solid),
315            transform: Transform::default(),
316            material: Material::new(Rgb::WHITE),
317            visible: true,
318        });
319        (object, quad)
320    }
321
322    fn run(world: &mut PhysicsWorld, steps: usize) {
323        for _ in 0..steps {
324            assert_eq!(world.step(DT), StepResult::Completed);
325        }
326    }
327
328    #[test]
329    fn registration_and_removal() {
330        let mut scene = Scene::new();
331        let cube = spawn_box(&mut scene, Vec3::ZERO);
332        let mut world = PhysicsWorld::new();
333
334        assert!(world.add_dynamic_box(cube, Vec3::splat(0.5), Transform::default()));
335        assert!(world.has_body(cube));
336        // Duplicate registration is rejected. 重复注册被拒绝。
337        assert!(!world.add_dynamic_box(cube, Vec3::splat(0.5), Transform::default()));
338        assert!(!world.add_static_box(cube, Vec3::splat(0.5), Transform::default()));
339
340        assert!(world.remove_body(cube));
341        assert!(!world.has_body(cube));
342        assert!(!world.remove_body(cube));
343        assert!(!world.add_force(cube, Vec3::X));
344    }
345
346    #[test]
347    fn transform_and_velocity_passthrough() {
348        let mut scene = Scene::new();
349        let cube = spawn_box(&mut scene, Vec3::new(0.0, 1.0, 0.0));
350        let mut world = PhysicsWorld::new();
351        world.add_dynamic_box(cube, Vec3::splat(0.5), Transform::from_position(Vec3::new(0.0, 1.0, 0.0)));
352
353        assert!(world.set_velocity(cube, Vec3::new(2.0, 0.0, 0.0)));
354        assert_eq!(world.velocity(cube), Vec3::new(2.0, 0.0, 0.0));
355
356        // Teleport keeps velocity. 瞬移保持速度。
357        let teleport = Transform::from_position_orientation(
358            Vec3::new(3.0, 4.0, 5.0),
359            Quat::from_rotation_y(1.0),
360        );
361        assert!(world.set_transform(cube, teleport));
362        assert_eq!(world.transform(cube), Some(teleport));
363        assert_eq!(world.velocity(cube), Vec3::new(2.0, 0.0, 0.0));
364
365        assert!(world.set_angular_velocity(cube, Vec3::new(0.0, 0.0, 1.0)));
366        assert_eq!(world.angular_velocity(cube), Vec3::new(0.0, 0.0, 1.0));
367
368        // Unknown objects are no-ops. 未知对象为无操作。
369        let stale = spawn_box(&mut scene, Vec3::ZERO);
370        scene.objects.remove(stale);
371        assert!(!world.set_transform(stale, Transform::default()));
372        assert_eq!(world.transform(stale), None);
373    }
374
375    #[test]
376    fn gravity_pulls_dynamic_body_down() {
377        let mut scene = Scene::new();
378        let cube = spawn_box(&mut scene, Vec3::new(0.0, 10.0, 0.0));
379        let mut world = PhysicsWorld::new();
380        world.add_dynamic_box(
381            cube,
382            Vec3::splat(0.5),
383            Transform::from_position(Vec3::new(0.0, 10.0, 0.0)),
384        );
385        run(&mut world, 60);
386        world.synchronize(&mut scene);
387        let y = scene.objects.get(cube).unwrap().transform.position.y;
388        assert!(y < 9.0, "body should have fallen, got y={y}");
389        assert!(y > 0.0, "body should not have fallen through, got y={y}");
390    }
391
392    #[test]
393    fn cube_rests_on_static_mesh_ground() {
394        let mut scene = Scene::new();
395        let cube = spawn_box(&mut scene, Vec3::new(0.0, 5.0, 0.0));
396        let (floor, floor_mesh) = spawn_quad_floor(&mut scene, 3.0);
397        let mut world = PhysicsWorld::new();
398        world.add_dynamic_box(
399            cube,
400            Vec3::splat(0.5),
401            Transform::from_position(Vec3::new(0.0, 5.0, 0.0)),
402        );
403        assert!(world.add_static_mesh(
404            floor,
405            &floor_mesh,
406            Transform::from_position(Vec3::new(0.0, -0.4, 0.0)),
407        ));
408
409        run(&mut world, 300);
410        world.synchronize(&mut scene);
411        let y = scene.objects.get(cube).unwrap().transform.position.y;
412        assert!((y - 0.1).abs() < 0.05, "cube should rest near y=0.1, got y={y}");
413    }
414
415    #[test]
416    fn restitution_bounces_on_fast_impact() {
417        let mut scene = Scene::new();
418        let cube = spawn_box(&mut scene, Vec3::new(0.0, 5.0, 0.0));
419        let (floor, floor_mesh) = spawn_quad_floor(&mut scene, 6.0);
420        let mut world = PhysicsWorld::new();
421        world.add_dynamic_box(
422            cube,
423            Vec3::splat(0.5),
424            Transform::from_position(Vec3::new(0.0, 5.0, 0.0)),
425        );
426        world.set_material(cube, 0.5, 0.5);
427        world.add_static_mesh(floor, &floor_mesh, Transform::from_position(Vec3::new(0.0, -0.4, 0.0)));
428
429        let mut max_vy = 0.0f32;
430        for _ in 0..120 {
431            world.step(DT);
432            max_vy = max_vy.max(world.velocity(cube).y);
433        }
434        assert!(max_vy > 0.5, "fast impact must bounce, got max_vy={max_vy}");
435    }
436
437    #[test]
438    fn force_acts_for_one_step_then_is_cleared() {
439        let mut scene = Scene::new();
440        let cube = spawn_box(&mut scene, Vec3::ZERO);
441        let mut world = PhysicsWorld::new();
442        world.set_gravity(Vec3::ZERO);
443        world.add_dynamic_box(cube, Vec3::splat(0.5), Transform::default());
444
445        assert!(world.add_force(cube, Vec3::new(60.0, 0.0, 0.0)));
446        assert_eq!(world.step(DT), StepResult::Completed);
447        let after_force = world.velocity(cube).x;
448        assert!(after_force > 0.9, "got {after_force}");
449
450        assert_eq!(world.step(DT), StepResult::Completed);
451        let after_idle = world.velocity(cube).x;
452        assert!(after_idle < after_force);
453        assert!(after_idle > after_force * 0.99);
454    }
455
456    #[test]
457    fn damping_decays_velocity() {
458        let mut scene = Scene::new();
459        let cube = spawn_box(&mut scene, Vec3::ZERO);
460        let mut world = PhysicsWorld::new();
461        world.set_gravity(Vec3::ZERO);
462        world.add_dynamic_box(cube, Vec3::splat(0.5), Transform::default());
463        assert!(world.set_damping(cube, 0.5, 0.5));
464        assert!(world.set_velocity(cube, Vec3::new(10.0, 0.0, 0.0)));
465        assert_eq!(world.step(DT), StepResult::Completed);
466        let vx = world.velocity(cube).x;
467        assert!(vx > 9.0 && vx < 10.0, "got {vx}");
468    }
469
470    #[test]
471    fn contacts_detect_overlap_and_separation() {
472        let mut scene = Scene::new();
473        let cube = spawn_box(&mut scene, Vec3::ZERO);
474        let other = spawn_box(&mut scene, Vec3::ZERO);
475        let mut world = PhysicsWorld::new();
476        world.add_dynamic_box(cube, Vec3::splat(0.5), Transform::default());
477        world.add_dynamic_box(other, Vec3::splat(0.5), Transform::default());
478
479        assert!(world.has_contacts(cube));
480        assert!(world.has_contacts(other));
481
482        assert!(world.set_transform(
483            other,
484            Transform::from_position(Vec3::new(10.0, 0.0, 0.0)),
485        ));
486        assert!(!world.has_contacts(cube));
487    }
488
489    #[test]
490    fn kinematic_body_moves_and_ignores_gravity() {
491        let mut scene = Scene::new();
492        let mover = spawn_box(&mut scene, Vec3::ZERO);
493        let mut world = PhysicsWorld::new();
494        world.add_kinematic_box(mover, Vec3::splat(0.5), Transform::default());
495        assert!(world.set_velocity(mover, Vec3::new(1.0, 0.0, 0.0)));
496        run(&mut world, 10);
497        let position = world.transform(mover).unwrap().position;
498        assert!((position.x - 10.0 * DT).abs() < 1e-4, "got x={}", position.x);
499        assert_eq!(position.y, 0.0);
500    }
501
502    #[test]
503    fn step_rejects_invalid_input() {
504        let mut world = PhysicsWorld::new();
505        assert_eq!(world.step(0.0), StepResult::InvalidInput);
506        assert_eq!(world.step(-1.0), StepResult::InvalidInput);
507        assert_eq!(world.step(f32::NAN), StepResult::InvalidInput);
508    }
509
510    #[test]
511    fn synchronize_writes_orientation_and_position() {
512        let mut scene = Scene::new();
513        let cube = spawn_box(&mut scene, Vec3::new(0.0, 2.0, 0.0));
514        let mut world = PhysicsWorld::new();
515        world.add_dynamic_box(cube, Vec3::splat(0.5), Transform::from_position(Vec3::new(0.0, 2.0, 0.0)));
516        assert!(world.set_angular_velocity(cube, Vec3::new(0.0, 0.0, 2.0)));
517        run(&mut world, 30);
518        world.synchronize(&mut scene);
519        let orientation = scene.objects.get(cube).unwrap().transform.orientation;
520        assert_ne!(orientation, Quat::IDENTITY, "cube should have rotated");
521        assert!(orientation.is_finite());
522    }
523
524    #[test]
525    fn add_static_mesh_registers_and_rejects_duplicates() {
526        let mut scene = Scene::new();
527        let cube = spawn_box(&mut scene, Vec3::new(0.0, 5.0, 0.0));
528        let (floor, floor_mesh) = spawn_quad_floor(&mut scene, 3.0);
529        let mut world = PhysicsWorld::new();
530        assert!(world.add_static_mesh(
531            floor,
532            &floor_mesh,
533            Transform::from_position(Vec3::new(0.0, -0.4, 0.0)),
534        ));
535        assert!(world.has_body(floor));
536        // Duplicate registration for the same object is rejected.
537        // 同一对象的重复注册被拒绝。
538        assert!(!world.add_static_mesh(
539            floor,
540            &floor_mesh,
541            Transform::from_position(Vec3::new(0.0, -0.4, 0.0)),
542        ));
543        assert!(world.add_dynamic_box(
544            cube,
545            Vec3::splat(0.5),
546            Transform::from_position(Vec3::new(0.0, 5.0, 0.0)),
547        ));
548        run(&mut world, 300);
549        world.synchronize(&mut scene);
550        let y = scene.objects.get(cube).unwrap().transform.position.y;
551        assert!((y - 0.1).abs() < 0.05, "cube should rest near y=0.1, got y={y}");
552    }
553}
554