Skip to content

Commit 85de616

Browse files
LuracasmusJondolf
andauthored
Make 2D picking Z plane configurable (#982)
# Objective - Even after #962, 2D picking only works reliably when pickable colliders are located on the Z = 0 plane when e.g. using perspective projection or a rotated camera. Allowing this Z coordinate to be configured could be useful. ## Solution - Make the Z level used for picking ray intersection configurable by adding a new field to `PhysicsPickingSettings`. - Extend the `picking_2d_in_perspective` example with controls for moving the physics picking Z plane, rotating the camera, and switching projection type, demonstrating this new functionality. - Write a migration guide entry for the changes in this PR and #962. ## Testing - The extended `picking_2d_in_perspective` example seems to work properly. - The solution works in my use case, picking from a 3D camera with perspective projection and colliders located far away in negative Z, which was previously broken. - `cargo test` returns 4 failures, but I get the same result on the latest commit before my changes. ```log ---- dynamics::rigid_body::mass_properties::tests::mass_properties_add_remove_collider stdout ---- thread 'dynamics::rigid_body::mass_properties::tests::mass_properties_add_remove_collider' (22023) panicked at crates/avian2d/../../src/dynamics/rigid_body/mass_properties/mod.rs:736:9: assertion `left == right` failed left: 15.707963 right: 15.707964 ---- dynamics::rigid_body::mass_properties::tests::mass_properties_no_auto_mass_add_remove stdout ---- thread 'dynamics::rigid_body::mass_properties::tests::mass_properties_no_auto_mass_add_remove' (22026) panicked at crates/avian2d/../../src/dynamics/rigid_body/mass_properties/mod.rs:890:9: assertion `left == right` failed left: 14.999999 right: 15.0 ---- dynamics::rigid_body::mass_properties::tests::mass_properties_move_child_collider stdout ---- thread 'dynamics::rigid_body::mass_properties::tests::mass_properties_move_child_collider' (22025) panicked at crates/avian2d/../../src/dynamics/rigid_body/mass_properties/mod.rs:843:9: assertion `left == right` failed left: 14.999999 right: 15.0 ---- dynamics::rigid_body::mass_properties::tests::mass_properties_rb_collider_with_set_mass_and_child_collider_with_set_mass stdout ---- thread 'dynamics::rigid_body::mass_properties::tests::mass_properties_rb_collider_with_set_mass_and_child_collider_with_set_mass' (22033) panicked at crates/avian2d/../../src/dynamics/rigid_body/mass_properties/mod.rs:629:9: assertion `left == right` failed left: 14.999999 right: 15.0 ``` All testing done was on x86_64-unknown-linux-gnu (latest CachyOS Desktop as of writing, using the [rust](https://packages.cachyos.org/package/cachyos-extra-znver4/x86_64_v4/rust) package from the `cachyos-extra-znver4` distribution repository). --------- Co-authored-by: Joona Aalto <jondolf.dev@gmail.com>
1 parent 3a385ff commit 85de616

3 files changed

Lines changed: 241 additions & 157 deletions

File tree

Lines changed: 180 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use avian2d::{math::*, prelude::*};
2-
use bevy::prelude::*;
2+
use bevy::{camera::ScalingMode, prelude::*};
33
use examples_common_2d::ExampleCommonPlugin;
44

55
fn main() {
@@ -13,10 +13,14 @@ fn main() {
1313
.insert_resource(ClearColor(Color::srgb(0.05, 0.05, 0.1)))
1414
.insert_resource(Gravity(Vector::NEG_Y))
1515
.add_systems(Startup, setup)
16-
.add_systems(Update, move_camera)
16+
.add_systems(Update, control_camera_and_plane)
1717
.run();
1818
}
1919

20+
#[derive(Component)]
21+
#[component(immutable)]
22+
struct PickingPlane;
23+
2024
fn setup(
2125
mut commands: Commands,
2226
mut materials: ResMut<Assets<ColorMaterial>>,
@@ -29,154 +33,200 @@ fn setup(
2933
Transform::from_xyz(0.0, 0.0, 1.0), // We have to start slightly away from the Z axis, since now everything is drawing at Z = 0
3034
));
3135

32-
let square_sprite = Sprite {
33-
color: Color::srgb(0.7, 0.7, 0.8),
34-
custom_size: Some(Vec2::splat(0.05)),
35-
..default()
36-
};
37-
38-
// Ceiling
39-
commands.spawn((
40-
square_sprite.clone(),
41-
Transform::from_xyz(0.0, 0.05 * 6.0, 0.0).with_scale(Vec3::new(20.0, 1.0, 1.0)),
42-
RigidBody::Static,
43-
Collider::rectangle(0.05, 0.05),
44-
));
45-
// Floor
46-
commands.spawn((
47-
square_sprite.clone(),
48-
Transform::from_xyz(0.0, -0.05 * 6.0, 0.0).with_scale(Vec3::new(20.0, 1.0, 1.0)),
49-
RigidBody::Static,
50-
Collider::rectangle(0.05, 0.05),
51-
));
52-
// Left wall
53-
commands.spawn((
54-
square_sprite.clone(),
55-
Transform::from_xyz(-0.05 * 9.5, 0.0, 0.0).with_scale(Vec3::new(1.0, 11.0, 1.0)),
56-
RigidBody::Static,
57-
Collider::rectangle(0.05, 0.05),
58-
));
59-
// Right wall
60-
commands.spawn((
61-
square_sprite,
62-
Transform::from_xyz(0.05 * 9.5, 0.0, 0.0).with_scale(Vec3::new(1.0, 11.0, 1.0)),
63-
RigidBody::Static,
64-
Collider::rectangle(0.05, 0.05),
65-
));
36+
// For example purposes, define a custom picking plane that can be moved with the E and Q keys.
37+
// Using a picking plane like this is not required, but the Z coordinates of 2D colliders
38+
// should match `PhysicsPickingSettings::z_plane` for them to be picked at the correct XY coordinates.
39+
commands
40+
.spawn((PickingPlane, Visibility::Visible, Transform::default()))
41+
.with_children(|child_spawner_commands| {
42+
let square_sprite = Sprite {
43+
color: Color::srgb(0.7, 0.7, 0.8),
44+
custom_size: Some(Vec2::splat(0.05)),
45+
..default()
46+
};
6647

67-
for flip in [-1.0, 1.0] {
68-
// Static anchor for the churner
69-
let velocity_anchor = commands
70-
.spawn((
71-
Sprite {
72-
color: Color::srgb(0.5, 0.5, 0.5),
73-
custom_size: Some(Vec2::splat(0.01)),
74-
..default()
75-
},
76-
Transform::from_xyz(-0.3 * flip, -0.15, 0.0),
48+
// Ceiling
49+
child_spawner_commands.spawn((
50+
square_sprite.clone(),
51+
Transform::from_xyz(0.0, 0.05 * 6.0, 0.0).with_scale(Vec3::new(20.0, 1.0, 1.0)),
7752
RigidBody::Static,
78-
))
79-
.id();
80-
81-
// Spinning churner
82-
let velocity_wheel = commands
83-
.spawn((
84-
Sprite {
85-
color: Color::srgb(0.9, 0.3, 0.3),
86-
custom_size: Some(vec2(0.24, 0.02)),
87-
..default()
88-
},
89-
Transform::from_xyz(-0.3 * flip, -0.15, 0.0),
90-
RigidBody::Dynamic,
91-
Mass(1.0),
92-
AngularInertia(1.0),
93-
SleepingDisabled, // Prevent sleeping so motor can always control it
94-
Collider::rectangle(0.24, 0.02),
95-
))
96-
.id();
97-
98-
// Revolute joint with velocity-controlled motor
99-
// Default anchors are at body centers (Vector::ZERO)
100-
commands.spawn((
101-
RevoluteJoint::new(velocity_anchor, velocity_wheel).with_motor(AngularMotor {
102-
target_velocity: 5.0 * flip.adjust_precision(),
103-
max_torque: 1000.0,
104-
motor_model: MotorModel::AccelerationBased {
105-
stiffness: 0.0,
106-
damping: 1.0,
107-
},
108-
..default()
109-
}),
110-
));
111-
}
53+
Collider::rectangle(0.05, 0.05),
54+
));
55+
// Floor
56+
child_spawner_commands.spawn((
57+
square_sprite.clone(),
58+
Transform::from_xyz(0.0, -0.05 * 6.0, 0.0).with_scale(Vec3::new(20.0, 1.0, 1.0)),
59+
RigidBody::Static,
60+
Collider::rectangle(0.05, 0.05),
61+
));
62+
// Left wall
63+
child_spawner_commands.spawn((
64+
square_sprite.clone(),
65+
Transform::from_xyz(-0.05 * 9.5, 0.0, 0.0).with_scale(Vec3::new(1.0, 11.0, 1.0)),
66+
RigidBody::Static,
67+
Collider::rectangle(0.05, 0.05),
68+
));
69+
// Right wall
70+
child_spawner_commands.spawn((
71+
square_sprite,
72+
Transform::from_xyz(0.05 * 9.5, 0.0, 0.0).with_scale(Vec3::new(1.0, 11.0, 1.0)),
73+
RigidBody::Static,
74+
Collider::rectangle(0.05, 0.05),
75+
));
11276

113-
let circle = Circle::new(0.0075);
114-
let collider = circle.collider();
115-
let mesh = meshes.add(circle);
116-
117-
let ball_color = Color::srgb(0.29, 0.33, 0.64);
118-
119-
for x in -12i32..=12 {
120-
for y in -1_i32..=8 {
121-
commands
122-
.spawn((
123-
Mesh2d(mesh.clone()),
124-
MeshMaterial2d(materials.add(ball_color)),
125-
Transform::from_xyz(x as f32 * 0.025, y as f32 * 0.025, 0.0),
126-
collider.clone(),
127-
RigidBody::Dynamic,
128-
Friction::new(0.1),
129-
))
130-
.observe(
131-
|over: On<Pointer<Over>>,
132-
mut materials: ResMut<Assets<ColorMaterial>>,
133-
balls: Query<&MeshMaterial2d<ColorMaterial>>| {
134-
materials
135-
.get_mut(balls.get(over.entity).unwrap())
136-
.unwrap()
137-
.color = Color::WHITE;
138-
},
139-
)
140-
.observe(
141-
move |out: On<Pointer<Out>>,
142-
mut materials: ResMut<Assets<ColorMaterial>>,
143-
balls: Query<&MeshMaterial2d<ColorMaterial>>| {
144-
materials
145-
.get_mut(balls.get(out.entity).unwrap())
146-
.unwrap()
147-
.color = ball_color;
148-
},
149-
);
150-
}
151-
}
77+
for flip in [-1.0, 1.0] {
78+
// Static anchor for the churner
79+
let velocity_anchor = child_spawner_commands
80+
.spawn((
81+
Sprite {
82+
color: Color::srgb(0.5, 0.5, 0.5),
83+
custom_size: Some(Vec2::splat(0.01)),
84+
..default()
85+
},
86+
Transform::from_xyz(-0.3 * flip, -0.15, 0.0),
87+
RigidBody::Static,
88+
))
89+
.id();
90+
91+
// Spinning churner
92+
let velocity_wheel = child_spawner_commands
93+
.spawn((
94+
Sprite {
95+
color: Color::srgb(0.9, 0.3, 0.3),
96+
custom_size: Some(vec2(0.24, 0.02)),
97+
..default()
98+
},
99+
Transform::from_xyz(-0.3 * flip, -0.15, 0.0),
100+
RigidBody::Dynamic,
101+
Mass(1.0),
102+
AngularInertia(1.0),
103+
SleepingDisabled, // Prevent sleeping so motor can always control it
104+
Collider::rectangle(0.24, 0.02),
105+
))
106+
.id();
107+
108+
// Revolute joint with velocity-controlled motor
109+
// Default anchors are at body centers (Vector::ZERO)
110+
child_spawner_commands.spawn((RevoluteJoint::new(velocity_anchor, velocity_wheel)
111+
.with_motor(AngularMotor {
112+
target_velocity: 5.0 * flip.adjust_precision(),
113+
max_torque: 1000.0,
114+
motor_model: MotorModel::AccelerationBased {
115+
stiffness: 0.0,
116+
damping: 1.0,
117+
},
118+
..default()
119+
}),));
120+
}
121+
122+
let circle = Circle::new(0.0075);
123+
let collider = circle.collider();
124+
let mesh = meshes.add(circle);
125+
126+
let ball_color = Color::srgb(0.29, 0.33, 0.64);
127+
128+
for x in -12i32..=12 {
129+
for y in -1_i32..=8 {
130+
child_spawner_commands
131+
.spawn((
132+
Mesh2d(mesh.clone()),
133+
MeshMaterial2d(materials.add(ball_color)),
134+
Transform::from_xyz(x as f32 * 0.025, y as f32 * 0.025, 0.0),
135+
collider.clone(),
136+
RigidBody::Dynamic,
137+
Friction::new(0.1),
138+
))
139+
.observe(
140+
|over: On<Pointer<Over>>,
141+
mut materials: ResMut<Assets<ColorMaterial>>,
142+
balls: Query<&MeshMaterial2d<ColorMaterial>>| {
143+
materials
144+
.get_mut(balls.get(over.entity).unwrap())
145+
.unwrap()
146+
.color = Color::WHITE;
147+
},
148+
)
149+
.observe(
150+
move |out: On<Pointer<Out>>,
151+
mut materials: ResMut<Assets<ColorMaterial>>,
152+
balls: Query<&MeshMaterial2d<ColorMaterial>>| {
153+
materials
154+
.get_mut(balls.get(out.entity).unwrap())
155+
.unwrap()
156+
.color = ball_color;
157+
},
158+
);
159+
}
160+
}
161+
});
152162
}
153163

154164
// TODO: if anyone would like to improve this example, a freecam could be fun to mess around with :D
155-
fn move_camera(
156-
mut camera: Single<&mut Transform, With<Camera>>,
165+
fn control_camera_and_plane(
166+
mut camera: Single<(&mut Transform, &mut Projection), With<Camera>>,
167+
mut picking_plane: Single<&mut Transform, (With<PickingPlane>, Without<Camera>)>,
168+
mut physics_picking_settings: ResMut<PhysicsPickingSettings>,
157169
input: Res<ButtonInput<KeyCode>>,
158170
time: Res<Time>,
159171
) {
160-
const MOVE_SCALE: f32 = 1.0;
172+
const CAMERA_MOVE_SCALE: f32 = 1.0;
173+
const CAMERA_ROTATE_SCALE: f32 = 1.0;
174+
const PICKING_MOVE_SCALE: f32 = 1.0;
161175

162-
let mut delta = Vec3::ZERO;
176+
let mut linear_delta = Vec3::ZERO;
163177
if input.pressed(KeyCode::KeyW) {
164-
delta.z -= time.delta_secs() * MOVE_SCALE;
178+
linear_delta.z -= CAMERA_MOVE_SCALE;
165179
}
166180
if input.pressed(KeyCode::KeyS) {
167-
delta.z += time.delta_secs() * MOVE_SCALE;
181+
linear_delta.z += CAMERA_MOVE_SCALE;
168182
}
169183
if input.pressed(KeyCode::KeyA) {
170-
delta.x -= time.delta_secs() * MOVE_SCALE;
184+
linear_delta.x -= CAMERA_MOVE_SCALE;
171185
}
172186
if input.pressed(KeyCode::KeyD) {
173-
delta.x += time.delta_secs() * MOVE_SCALE;
187+
linear_delta.x += CAMERA_MOVE_SCALE;
174188
}
175189
if input.pressed(KeyCode::ShiftLeft) {
176-
delta.y -= time.delta_secs() * MOVE_SCALE;
190+
linear_delta.y -= CAMERA_MOVE_SCALE;
177191
}
178192
if input.pressed(KeyCode::Space) {
179-
delta.y += time.delta_secs() * MOVE_SCALE;
193+
linear_delta.y += CAMERA_MOVE_SCALE;
194+
}
195+
let camera_rotation = camera.0.rotation;
196+
camera.0.translation += camera_rotation * (time.delta_secs() * linear_delta);
197+
198+
let mut angular_delta = 0.0;
199+
if input.pressed(KeyCode::ArrowRight) {
200+
angular_delta -= CAMERA_ROTATE_SCALE;
201+
}
202+
if input.pressed(KeyCode::ArrowLeft) {
203+
angular_delta += CAMERA_ROTATE_SCALE;
204+
}
205+
camera.0.rotate_y(time.delta_secs() * angular_delta);
206+
207+
let mut picking_plane_delta = 0.0;
208+
if input.pressed(KeyCode::KeyE) {
209+
picking_plane_delta += PICKING_MOVE_SCALE
210+
}
211+
if input.pressed(KeyCode::KeyQ) {
212+
picking_plane_delta -= PICKING_MOVE_SCALE;
213+
}
214+
picking_plane_delta *= time.delta_secs();
215+
216+
physics_picking_settings.z_plane += picking_plane_delta;
217+
picking_plane.translation.z += picking_plane_delta;
218+
219+
if input.just_pressed(KeyCode::KeyR) {
220+
*camera.1 = if let Projection::Perspective(_) = *camera.1 {
221+
Projection::Orthographic(OrthographicProjection {
222+
scaling_mode: ScalingMode::AutoMin {
223+
min_width: 1.0,
224+
min_height: 1.0,
225+
},
226+
..OrthographicProjection::default_3d()
227+
})
228+
} else {
229+
Projection::Perspective(PerspectiveProjection::default())
230+
}
180231
}
181-
camera.translation += delta;
182232
}

migration-guides/0.6-to-main.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@ simply ignore the new parameter with `_`.
1717
Similar to many other plugins, the `SpatialQueryPlugin` now stores a schedule label,
1818
which by default is also passed via `PhysicsPlugins::new`.
1919

20+
## 2D picking works in more 3D situations
21+
22+
PRs: [#982](https://github.com/avianphysics/avian/pull/982)
23+
24+
`PhysicsPickingSettings` in 2D now contains a `z_plane` that determines the world space Z coordinate
25+
of the plane that rays cast from the picking `Camera` intersect, affecting the world space XY coordinates
26+
used when querying for colliders to pick.
27+
28+
This allows 2D physics picking to work properly with cameras using perspective projection
29+
or rotated away from the default forward direction, as long as all pickable colliders
30+
are located at the `z_plane` world space Z coordinate.
31+
32+
The Z component of the `HitData::position` produced when picking in 2D is now equal to `PhysicsPickingSettings::z_plane`,
33+
and `HitData::depth` is equal to the distance along each ray to its intersection point on the plane.
34+
2035
## Physics Debug Gizmo Mesh Visibility
2136

2237
In past releases, `PhysicsGizmos::hide_meshes` forced all meshes to be visible when it is `false`.

0 commit comments

Comments
 (0)