啃了点源码记录一下。
前言
项目需要搭建一个自定义的物理环境,选择使用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
方法里我们看到这样一行
.add_systems(
appSelf::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>,
: Res<RapierConfiguration>,
config: StaticSystemParam<Hooks>,
hooks: Res<Time>,
timemut sim_to_render_time: ResMut<SimulationToRenderTime>,
: EventWriter<CollisionEvent>,
collision_events: EventWriter<ContactForceEvent>,
contact_force_events: Query<(&RapierRigidBodyHandle, &mut TransformInterpolation)>,
interpolation_querywhere
) : 'static + BevyPhysicsHooks,
Hooksfor<'w, 's> SystemParamItem<'w, 's, Hooks>: BevyPhysicsHooks,
// 省略部分
if config.physics_pipeline_active {
.step_simulation(
context.gravity,
config.timestep_mode,
configSome((collision_events, contact_force_events)),
&hooks_adapter,
&time,
&mut sim_to_render_time,
Some(interpolation_query),
;
).deleted_colliders.clear(); context
可以看到,collision_events
和Hooks
在RapierContext::step_simulation
中进行运算,值得注意的是,这里的Hooks
是BevyPhysicsHooks
类型而不是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::NarrowPhase
的compute_contacts
方法,这就是一切碰撞计算发生的地方。在这里,hooks中可以自定义的三个方法会被依次调用,用于修改碰撞的计算方法。
我在这里贴上完整源码:
pub(crate) fn compute_contacts(
&mut self,
: Real,
prediction_distance: &RigidBodySet,
bodies: &ColliderSet,
colliders: &ImpulseJointSet,
impulse_joints: &MultibodyJointSet,
multibody_joints: &[ColliderHandle],
modified_colliders: &dyn PhysicsHooks,
hooks: &dyn EventHandler,
events{
) 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 {
= bodies[co_parent1.handle].body_type;
rb_type1 }
if let Some(co_parent2) = &co2.parent {
= bodies[co_parent2.handle].body_type;
rb_type2 }
// Deal with contacts disabled between bodies attached by joints.
if let (Some(co_parent1), Some(co_parent2)) = (&co1.parent, &co2.parent) {
for (_, joint) in
.joints_between(co_parent1.handle, co_parent2.handle)
impulse_joints{
if !joint.data.contacts_enabled {
.clear();
pairbreak 'emit_events;
}
}
if let Some((_, _, mb_link)) =
.joint_between(co_parent1.handle, co_parent2.handle)
multibody_joints{
if !mb_link.joint.data.contacts_enabled {
.clear();
pairbreak '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)
{
.clear();
pairbreak 'emit_events;
}
// Filter based on collision groups.
if !co1.flags.collision_groups.test(co2.flags.collision_groups) {
.clear();
pairbreak '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: co1.parent.map(|p| p.handle),
rigid_body1: co2.parent.map(|p| p.handle),
rigid_body2: pair.collider1,
collider1: pair.collider2,
collider2};
if let Some(solver_flags) = hooks.filter_contact_pair(&context) {
solver_flags} else {
// No contact allowed.
.clear();
pairbreak 'emit_events;
}
} else {
SolverFlags::default()
};
if !co1.flags.solver_groups.test(co2.flags.solver_groups) {
.remove(SolverFlags::COMPUTE_IMPULSES);
solver_flags}
if co1.changes.contains(ColliderChanges::SHAPE)
|| co2.changes.contains(ColliderChanges::SHAPE)
{
// The shape changed so the workspace is no longer valid.
.workspace = None;
pair}
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(
.material.friction,
co1.material.friction,
co2.material.friction_combine_rule as u8,
co1.material.friction_combine_rule as u8,
co2;
)let restitution = CoefficientCombineRule::combine(
.material.restitution,
co1.material.restitution,
co2.material.restitution_combine_rule as u8,
co1.material.restitution_combine_rule as u8,
co2;
)
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);
.has_any_active_contact = false;
pair
for manifold in &mut pair.manifolds {
let world_pos1 = manifold.subshape_pos1.prepend_to(&co1.pos);
.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)
manifold- dominance2.effective_group(&rb_type2);
.data.normal = world_pos1 * manifold.local_n1;
manifold
// Generate solver contacts.
for (contact_id, contact) in manifold.points.iter().enumerate() {
assert!(
<= u8::MAX as usize,
contact_id "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 as u8,
contact_id: world_pos1 * contact.local_p1
point+ manifold.data.normal * contact.dist / 2.0,
: contact.dist,
dist,
friction,
restitution: Vector::zeros(),
tangent_velocity: contact.data.impulse == 0.0,
is_new};
.data.solver_contacts.push(solver_contact);
manifold.has_any_active_contact = true;
pair}
}
// 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: co1.parent.map(|p| p.handle),
rigid_body1: co2.parent.map(|p| p.handle),
rigid_body2: pair.collider1,
collider1: pair.collider2,
collider2,
manifold: &mut modifiable_solver_contacts,
solver_contacts: &mut modifiable_normal,
normal: &mut modifiable_user_data,
user_data};
.modify_solver_contacts(&mut context);
hooks
.data.solver_contacts = modifiable_solver_contacts;
manifold.data.normal = modifiable_normal;
manifold.data.user_data = modifiable_user_data;
manifold}
}
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 {
.emit_start_event(bodies, colliders, events);
pair} else {
.emit_stop_event(bodies, colliders, events);
pair}
}
}
});
}
然后呢?
然后我就看到了这一段:
// Deal with contacts disabled between bodies attached by joints.
if let (Some(co_parent1), Some(co_parent2)) = (&co1.parent, &co2.parent) {
for (_, joint) in
.joints_between(co_parent1.handle, co_parent2.handle)
impulse_joints{
if !joint.data.contacts_enabled {
.clear();
pairbreak 'emit_events;
}
}
if let Some((_, _, mb_link)) =
.joint_between(co_parent1.handle, co_parent2.handle)
multibody_joints{
if !mb_link.joint.data.contacts_enabled {
.clear();
pairbreak '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 = ()> {
: f32,
physics_scale: bool,
default_system_setup: PhantomData<PhysicsHooks>,
_phantom}
PhysicsHooks
竟然作为是一个类型给到RapierPhysicsPlugin
的,一开始我忽视了这一点,以为只要注册了SystemParam
就可以被rapier获取到,浪费了许多时间。
另外,在浏览hooks的源码的过程中还发现,在physics_hooks::ContactModificationContext
中竟然很贴心的内置了一个方法update_as_oneway_platform
,可以直接在hook中使用,让复杂繁琐的单项平台可以可以几行搞定。