bevy_rapier碰撞检测的源码拆解

啃了点源码记录一下。
日常
学习笔记
Author

Midstream

Published

June 30, 2023

啃了点源码记录一下。

前言

项目需要搭建一个自定义的物理环境,选择使用bevy引擎进行可视化。

因为依然处于开发阶段,在bevy引擎里,如果想实现物理效果,需要引入第三方库,其中最成熟的是dimforge的Rapier物理引擎,运行中Rapier会自动检测所有实体的碰撞并自动处理碰撞事件。

然而,我需要的物理环境里有许多特殊的情况,要求特定的两个或数个实体之间不进行碰撞,如——有众多Joint链接的情况下,相互连接的个体之间可以穿透。举个例子就是,一列火车中,第一节和第二节车厢不会发生碰撞,但第一节和其他所有车厢的碰撞都是正常的。实现这种效果,就需要在rapier进行碰撞处理的时候对其进行自定义,Rapier提供了PhysicsHook控件用于魔改碰撞处理,很友善。

但是接下来就是问题出现的地方了。

因为bevy和rapier的不断的持续更新与引擎本身的小众,互联网上本就不多的信息早已全部过时。bevy本身在今年三月份更新了0.10版本,bevy_rapier也在二月份的一个pull request中更新了PhysicsHook的获取信息的方式。于是,BevyPhysicsHooks的使用方式甚至再没有例子可循,文档中也只有简短的说明而没有使用范例。

那就只好去拆解bevy_rapier的源码了。

拆解

首先找到RapierPhysicsPlugin的内容,这是为了让Rapier引擎能够以plugin的形式在bevy中运行的API。在plugin::RapierPhysicsPlugin::build方法里我们看到这样一行

app.add_systems(
    Self::get_systems(PhysicsSet::StepSimulation)
        .in_base_set(PhysicsSet::StepSimulation),
);

我们可以发现add_system将步骤模拟system加入了plugin, 而在方法add_system中,三个方法被返回,其中两个update_system 被要求在step_simulation之前调用。

PhysicsSet::StepSimulation => (
    systems::step_simulation::<PhysicsHooks>,
    Events::<CollisionEvent>::update_system
        .before(systems::step_simulation::<PhysicsHooks>),
    Events::<ContactForceEvent>::update_system
        .before(systems::step_simulation::<PhysicsHooks>),
)
    .into_configs(),

step_simulation参数如下和部分内容如下,其中的collision_events被送至RapierContext::step_simulation继续处理

pub fn step_simulation<Hooks>(
    mut context: ResMut<RapierContext>,
    config: Res<RapierConfiguration>,
    hooks: StaticSystemParam<Hooks>,
    time: Res<Time>,
    mut sim_to_render_time: ResMut<SimulationToRenderTime>,
    collision_events: EventWriter<CollisionEvent>,
    contact_force_events: EventWriter<ContactForceEvent>,
    interpolation_query: Query<(&RapierRigidBodyHandle, &mut TransformInterpolation)>,
) where
    Hooks: 'static + BevyPhysicsHooks,
    for<'w, 's> SystemParamItem<'w, 's, Hooks>: BevyPhysicsHooks,

// 省略部分

    if config.physics_pipeline_active {
        context.step_simulation(
            config.gravity,
            config.timestep_mode,
            Some((collision_events, contact_force_events)),
            &hooks_adapter,
            &time,
            &mut sim_to_render_time,
            Some(interpolation_query),
        );
        context.deleted_colliders.clear();

可以看到,collision_eventsHooksRapierContext::step_simulation中进行运算,值得注意的是,这里的HooksBevyPhysicsHooks类型而不是PhysicsHooks, 前者多了一个SystemParam作为supertrait。

而在RapierContext::step_simulation中,方法self.pipeline.step被调用了。这里的step方法已经不再是bevy_rapier2d里的方法了,而是rapier2d的方法。同时,在这一步里,之前一直被传递的碰撞事件也和其他事件一同被打包进rapier2d::pipeline::event_handler里面。在rapier中非常重要的两个概念Broad Phrase和Narrow Phrase也将在RapierContext被实例化的同时完成初始化,并且作为参数和events,hooks一同传递给PhysicsPipeline::step

step中会一件一件地处理一帧中发生的所有事件,是rapier引擎的核心loop。在每个step和substep中都会经过一系列的方法调用,最终会调用至位于geometry::narrow_phase::NarrowPhasecompute_contacts方法,这就是一切碰撞计算发生的地方。在这里,hooks中可以自定义的三个方法会被依次调用,用于修改碰撞的计算方法。

我在这里贴上完整源码:

pub(crate) fn compute_contacts(
    &mut self,
    prediction_distance: Real,
    bodies: &RigidBodySet,
    colliders: &ColliderSet,
    impulse_joints: &ImpulseJointSet,
    multibody_joints: &MultibodyJointSet,
    modified_colliders: &[ColliderHandle],
    hooks: &dyn PhysicsHooks,
    events: &dyn EventHandler,
) {
    if modified_colliders.is_empty() {
        return;
    }

    let query_dispatcher = &*self.query_dispatcher;

    // TODO: don't iterate on all the edges.
    par_iter_mut!(&mut self.contact_graph.graph.edges).for_each(|edge| {
        let pair = &mut edge.weight;
        let had_any_active_contact = pair.has_any_active_contact;
        let co1 = &colliders[pair.collider1];
        let co2 = &colliders[pair.collider2];

        // TODO: remove the `loop` once labels on blocks are supported.
        'emit_events: loop {
            if !co1.changes.needs_narrow_phase_update()
                && !co2.changes.needs_narrow_phase_update()
            {
                // No update needed for these colliders.
                return;
            }

            // TODO: avoid lookup into bodies.
            let mut rb_type1 = RigidBodyType::Fixed;
            let mut rb_type2 = RigidBodyType::Fixed;

            if let Some(co_parent1) = &co1.parent {
                rb_type1 = bodies[co_parent1.handle].body_type;
            }

            if let Some(co_parent2) = &co2.parent {
                rb_type2 = bodies[co_parent2.handle].body_type;
            }

            // Deal with contacts disabled between bodies attached by joints.
            if let (Some(co_parent1), Some(co_parent2)) = (&co1.parent, &co2.parent) {
                for (_, joint) in
                    impulse_joints.joints_between(co_parent1.handle, co_parent2.handle)
                {
                    if !joint.data.contacts_enabled {
                        pair.clear();
                        break 'emit_events;
                    }
                }

                if let Some((_, _, mb_link)) =
                    multibody_joints.joint_between(co_parent1.handle, co_parent2.handle)
                {
                    if !mb_link.joint.data.contacts_enabled {
                        pair.clear();
                        break 'emit_events;
                    }
                }
            }

            // Filter based on the rigid-body types.
            if !co1.flags.active_collision_types.test(rb_type1, rb_type2)
                && !co2.flags.active_collision_types.test(rb_type1, rb_type2)
            {
                pair.clear();
                break 'emit_events;
            }

            // Filter based on collision groups.
            if !co1.flags.collision_groups.test(co2.flags.collision_groups) {
                pair.clear();
                break 'emit_events;
            }

            let active_hooks = co1.flags.active_hooks | co2.flags.active_hooks;

            let mut solver_flags = if active_hooks.contains(ActiveHooks::FILTER_CONTACT_PAIRS) {
                let context = PairFilterContext {
                    bodies,
                    colliders,
                    rigid_body1: co1.parent.map(|p| p.handle),
                    rigid_body2: co2.parent.map(|p| p.handle),
                    collider1: pair.collider1,
                    collider2: pair.collider2,
                };

                if let Some(solver_flags) = hooks.filter_contact_pair(&context) {
                    solver_flags
                } else {
                    // No contact allowed.
                    pair.clear();
                    break 'emit_events;
                }
            } else {
                SolverFlags::default()
            };

            if !co1.flags.solver_groups.test(co2.flags.solver_groups) {
                solver_flags.remove(SolverFlags::COMPUTE_IMPULSES);
            }

            if co1.changes.contains(ColliderChanges::SHAPE)
                || co2.changes.contains(ColliderChanges::SHAPE)
            {
                // The shape changed so the workspace is no longer valid.
                pair.workspace = None;
            }

            let pos12 = co1.pos.inv_mul(&co2.pos);
            let _ = query_dispatcher.contact_manifolds(
                &pos12,
                &*co1.shape,
                &*co2.shape,
                prediction_distance,
                &mut pair.manifolds,
                &mut pair.workspace,
            );

            let friction = CoefficientCombineRule::combine(
                co1.material.friction,
                co2.material.friction,
                co1.material.friction_combine_rule as u8,
                co2.material.friction_combine_rule as u8,
            );
            let restitution = CoefficientCombineRule::combine(
                co1.material.restitution,
                co2.material.restitution,
                co1.material.restitution_combine_rule as u8,
                co2.material.restitution_combine_rule as u8,
            );

            let zero = RigidBodyDominance(0); // The value doesn't matter, it will be MAX because of the effective groups.
            let dominance1 = co1
                .parent
                .map(|p1| bodies[p1.handle].dominance)
                .unwrap_or(zero);
            let dominance2 = co2
                .parent
                .map(|p2| bodies[p2.handle].dominance)
                .unwrap_or(zero);

            pair.has_any_active_contact = false;

            for manifold in &mut pair.manifolds {
                let world_pos1 = manifold.subshape_pos1.prepend_to(&co1.pos);
                manifold.data.solver_contacts.clear();
                manifold.data.rigid_body1 = co1.parent.map(|p| p.handle);
                manifold.data.rigid_body2 = co2.parent.map(|p| p.handle);
                manifold.data.solver_flags = solver_flags;
                manifold.data.relative_dominance = dominance1.effective_group(&rb_type1)
                    - dominance2.effective_group(&rb_type2);
                manifold.data.normal = world_pos1 * manifold.local_n1;

                // Generate solver contacts.
                for (contact_id, contact) in manifold.points.iter().enumerate() {
                    assert!(
                        contact_id <= u8::MAX as usize,
                        "A contact manifold cannot contain more than 255 contacts currently."
                    );

                    if contact.dist < prediction_distance {
                        // Generate the solver contact.
                        let solver_contact = SolverContact {
                            contact_id: contact_id as u8,
                            point: world_pos1 * contact.local_p1
                                + manifold.data.normal * contact.dist / 2.0,
                            dist: contact.dist,
                            friction,
                            restitution,
                            tangent_velocity: Vector::zeros(),
                            is_new: contact.data.impulse == 0.0,
                        };

                        manifold.data.solver_contacts.push(solver_contact);
                        pair.has_any_active_contact = true;
                    }
                }

                // Apply the user-defined contact modification.
                if active_hooks.contains(ActiveHooks::MODIFY_SOLVER_CONTACTS) {
                    let mut modifiable_solver_contacts =
                        std::mem::replace(&mut manifold.data.solver_contacts, Vec::new());
                    let mut modifiable_user_data = manifold.data.user_data;
                    let mut modifiable_normal = manifold.data.normal;

                    let mut context = ContactModificationContext {
                        bodies,
                        colliders,
                        rigid_body1: co1.parent.map(|p| p.handle),
                        rigid_body2: co2.parent.map(|p| p.handle),
                        collider1: pair.collider1,
                        collider2: pair.collider2,
                        manifold,
                        solver_contacts: &mut modifiable_solver_contacts,
                        normal: &mut modifiable_normal,
                        user_data: &mut modifiable_user_data,
                    };

                    hooks.modify_solver_contacts(&mut context);

                    manifold.data.solver_contacts = modifiable_solver_contacts;
                    manifold.data.normal = modifiable_normal;
                    manifold.data.user_data = modifiable_user_data;
                }
            }

            break 'emit_events;
        }

        let active_events = co1.flags.active_events | co2.flags.active_events;

        if pair.has_any_active_contact != had_any_active_contact {
            if active_events.contains(ActiveEvents::COLLISION_EVENTS) {
                if pair.has_any_active_contact {
                    pair.emit_start_event(bodies, colliders, events);
                } else {
                    pair.emit_stop_event(bodies, colliders, events);
                }
            }
        }
    });
}

然后呢?

然后我就看到了这一段:

// Deal with contacts disabled between bodies attached by joints.
if let (Some(co_parent1), Some(co_parent2)) = (&co1.parent, &co2.parent) {
    for (_, joint) in
        impulse_joints.joints_between(co_parent1.handle, co_parent2.handle)
    {
        if !joint.data.contacts_enabled {
            pair.clear();
            break 'emit_events;
        }
    }

    if let Some((_, _, mb_link)) =
        multibody_joints.joint_between(co_parent1.handle, co_parent2.handle)
    {
        if !mb_link.joint.data.contacts_enabled {
            pair.clear();
            break 'emit_events;
        }
    }
}

淦。

原来,在joint里有一个flag叫做contacts_enabled,这段代码会专门对这个flag进行判断,如果打开了,那么joint连接的两端的collider之间的碰撞就会在计算之前从query中被删除。。。。。。。。。。

然后,好像我不需要用hooks了。只要在建立连接的时候把这个开关关上就好了。。。。。。

可是为什么joint的User Guides里完全没讲还有这个功能。。。。。。

别的

虽然现在有更简单的方法来实现我需要的功能了,但是新的hook到底该如何使用呢?

其实很简单,而且比以前更加简单,只需要将一个实现了hook内部方法的struct添加到SystemParam就行了。

再就是,记得在添加RapierPhysicsPlugin到bevy app的时候,把hook注册进去。因为plugin的定义是这样的:

pub struct RapierPhysicsPlugin<PhysicsHooks = ()> {
    physics_scale: f32,
    default_system_setup: bool,
    _phantom: PhantomData<PhysicsHooks>,
}

PhysicsHooks竟然作为是一个类型给到RapierPhysicsPlugin的,一开始我忽视了这一点,以为只要注册了SystemParam就可以被rapier获取到,浪费了许多时间。

另外,在浏览hooks的源码的过程中还发现,在physics_hooks::ContactModificationContext中竟然很贴心的内置了一个方法update_as_oneway_platform,可以直接在hook中使用,让复杂繁琐的单项平台可以可以几行搞定。