Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a function for computing a world point from a screen point #1799

Closed
wants to merge 14 commits into from

Conversation

anchpop
Copy link
Contributor

@anchpop anchpop commented Apr 2, 2021

Uses geometric primitives from #1621 (so get that merged first haha). The example at screen_to_world_positioning contains a demonstration

This is a feature that a large proportion of 3D games will need at some point, so it's useful to have it in Bevy proper (rather than in a 3rd-party extension like bevy_mod_raycast from which some of the code was borrowed).

Thank you to @aevyrie and @alice-i-cecile for helping me out with this one in the Discord :P

@alice-i-cecile alice-i-cecile added C-Usability A simple quality-of-life change that makes Bevy easier to use A-Core Common functionality for all bevy apps labels Apr 2, 2021
@alice-i-cecile
Copy link
Member

Closes #620. Related to #197 and #615.

@alice-i-cecile
Copy link
Member

@jamadazi once this is merged this will obsolete https://bevy-cheatbook.github.io/cookbook/cursor2world.html :)


fn follow(
mut q: Query<&mut Transform, With<Follow>>,
q_camera: Query<(&Camera, &GlobalTransform)>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably want to tweak this to exclude UI cameras to make it easier to port to user code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't played with UI yet, tip for what struct exactly I should exclude?

@alice-i-cecile
Copy link
Member

I would very much like to see a 2D example of this as well :) It's not obvious to many beginners that 3D code tends to also work in 2D.

Remember to add new examples to examples/README.md as well!

@anchpop
Copy link
Contributor Author

anchpop commented Apr 2, 2021

I would very much like to see a 2D example of this as well :) It's not obvious to many beginners that 3D code tends to also work in 2D.

Remember to add new examples to examples/README.md as well!

I was actually curious about your thoughts on this. Should there be a function specialized for 2D users, since we can assume they probably want to use the plane that passes through the origin and faces the camera? If so, how should we prevent 3D users from accidentally using the 2D version and vice/versa?

@@ -61,6 +62,48 @@ impl Camera {
let screen_space_coords = (ndc_space_coords.truncate() + Vec2::ONE) / 2.0 * window_size;
Some(screen_space_coords)
}

/// Given a position in screen space, compute the world-space line that corresponds to it.
pub fn screen_to_world_line(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both of these functions feel like they should be a method on the Camera, not a function that takes a camera as the argument.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See: #1258

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote it like that originally, but it felt weird that it was a method on the camera but still required the camera's global transform. I thought it was cleaner as a function but I'm happy to change it if you think it's better or more consistent that way (I'm not sure how this is normally handled in bevy).

@aevyrie
Copy link
Member

aevyrie commented Apr 2, 2021

You could argue that there is no such thing as 2D in bevy, considering layers are positioned with real z-depth, and some games use the fact that 2d is actually 3d as a feature for rendering effects.

That said, if someone is truly making a 2D-only game, they probably only need the cursor coordinates.

@alice-i-cecile
Copy link
Member

Should there be a function specialized for 2D users, since we can assume they probably want to use the plane that passes through the origin and faces the camera?

Yes, this sounds very useful. And it's particularly helpful for beginners.

If so, how should we prevent 3D users from accidentally using the 2D version and vice/versa?

The simplest solution would be to extend Camera::screen_to_point_on_plane with a parallel Camera::screen_to_point_2d function, with the docs pointing to the alternative version.

The other idea would be to inspect the properties of the camera, and then determine which approach to take based on their properties. I don't think this is the right approach though: the extra check will be wasted work and the 2D version needs significantly less information than the 3D version.

@alice-i-cecile
Copy link
Member

That said, if someone is truly making a 2D-only game, they probably only need the cursor coordinates.

I'd totally forgotten that #1258 was merged. It's probably saner to just use that for 2D. I still think it deserves an example for discoverability (and API consistency and doc crosslinking).

IMO that's within scope for this PR, since it will make the big picture much clearer for the end user.

@anchpop
Copy link
Contributor Author

anchpop commented Apr 2, 2021

I would very much like to see a 2D example of this as well :) It's not obvious to many beginners that 3D code tends to also work in 2D.

Done!

Remember to add new examples to examples/README.md as well!

Done!

I'd totally forgotten that #1258 was merged. It's probably saner to just use that for 2D. I still think it deserves an example for discoverability (and API consistency and doc crosslinking).

I didn't see the code for #1258 in my branch - not sure if it got moved or if my branch just isn't up to date. Either way, I think the implementation of the 2D version should just call into the 3D version so I've rewritten it, but the interface doesn't need to change (although it's different right now because mine is a function and the other is a method).

Thoughts?

@anchpop anchpop force-pushed the screen_point_to_ray branch 5 times, most recently from 34709ba to c22b434 Compare April 2, 2021 15:54
@anchpop
Copy link
Contributor Author

anchpop commented Apr 2, 2021

I'd totally forgotten that #1258 was merged. It's probably saner to just use that for 2D. I still think it deserves an example for discoverability (and API consistency and doc crosslinking).

I just realized something. #1258 converts world-space coordinates to screen-space, my function does the opposite. So there's need for both.

@inodentry
Copy link
Contributor

Happy to see a proper solution making its way into bevy, for something so common. I really don't like having that ugly cursor2world cheatsheet page in cheatbook. 😄

@anchpop anchpop force-pushed the screen_point_to_ray branch 3 times, most recently from 7b61246 to 96645dd Compare May 17, 2021 18:10
bors bot pushed a commit that referenced this pull request May 17, 2021
This can save users from having to type `&*X` all the time at the cost of some complexity in the type signature. For instance, this allows me to accommodate @jakobhellermann's suggestion in #1799 without requiring users to type `&*windows` 99% of the time.
@anchpop
Copy link
Contributor Author

anchpop commented May 18, 2021

You probably want to tweak this to exclude UI cameras to make it easier to port to user code.

@alice-i-cecile Do you think Bevy's UICameraBundle should come with a UICamera marker component? I think almost all uses of the UI camera will require distinguishing it from other cameras, and like you mentioned it would allow the example code to exclude UI cameras (which would be the correct behavior 99% of the time).

@anchpop anchpop force-pushed the screen_point_to_ray branch 2 times, most recently from ba6da4a to 5b263ff Compare May 18, 2021 18:53
@alice-i-cecile
Copy link
Member

@alice-i-cecile Do you think Bevy's UICameraBundle should come with a UICamera marker component? I think almost all uses of the UI camera will require distinguishing it from other cameras, and like you mentioned it would allow the example code to exclude UI cameras (which would be the correct behavior 99% of the time).

Quite strongly yes :) I would love to see that change, but I think it should be part of a seperate PR.

@wilk10
Copy link
Contributor

wilk10 commented Jun 28, 2021

Is there an update on this? I'd love too see it merged, if it gets approved. Do you need help with fixing conflicts / get CI to pass? (No hurry though)

Comment on lines +16 to +18
}
impl Error for PrimitiveError {}
impl fmt::Display for PrimitiveError {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
impl Error for PrimitiveError {}
impl fmt::Display for PrimitiveError {
}
impl Error for PrimitiveError {}
impl fmt::Display for PrimitiveError {

just a small nit-pick, add some newlines here :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Also this is here in a few other places, so adding newlines between impls would be nice)

radius: f32,
}

impl Sphere {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These seem like quite simple almost unnecessary getters/setters for Sphere.
IMO just having the Sphere's origin and radius field be public would be better here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto for a lot of the other simple structs in the PR

Comment on lines +138 to +143
for vertex in self.vertices().iter() {
if plane.distance_to_point(*vertex) <= 0.0 {
return false;
}
}
true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if self.vertices()
    .iter()
    .find(|vert| plane.distance_to_point(*vertex) <= 0.0)
    .map_or(true, |_| false)

I would personally prefer using find() then map here.

Err(PrimitiveError::MinGreaterThanMax)
}
}
/// Construct an [AxisAlignedBox] from the origin at the minimum corner, and the extents - the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Construct an [AxisAlignedBox] from the origin at the minimum corner, and the extents - the
/// Construct an [`AxisAlignedBox`] from the origin at the minimum corner, and the extents - the

having the ticks (`) in the doc comments helps highlight the fact this is a type :)

}
impl Primitive3d for Frustum {
fn outside_plane(&self, plane: Plane) -> bool {
for vertex in self.vertices().iter() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto here with the find() then map_or

pub fn intersection_line(&self, line: &Line) -> Option<Vec3> {
let d = line.direction.dot(self.normal);
if d == 0. {
// Should probably check if they're approximately equal, not strictly equal
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, I agree with this

}
impl Primitive3d for OBB {
fn outside_plane(&self, plane: Plane) -> bool {
for vertex in self.vertices().iter() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto find/map_or

MinGreaterThanMax,
NonPositiveExtents,
}
impl Error for PrimitiveError {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of implementing Error and Display for this type.
You can use the thiserror crate. (we use it in other places of the bevy code base)

#[derive(Error, Debug, Clone)]
pub enum PrimitiveError {
    #[error("this is the error message")]
    MinGreaterThanMax,
    #[error("this is another error message")]
    NonPositiveExtents,
}

@anchpop
Copy link
Contributor Author

anchpop commented Jun 28, 2021

@NathanSWard Most of those comments are related to #1621, pinging @aevyrie to make sure they see them

@cart cart added the S-Pre-Relicense This PR was made before Bevy added the Apache license. Cannot be merged or used for other work label Jul 23, 2021
@mockersf mockersf removed the S-Pre-Relicense This PR was made before Bevy added the Apache license. Cannot be merged or used for other work label Jul 24, 2021
ostwilkens pushed a commit to ostwilkens/bevy that referenced this pull request Jul 27, 2021
This can save users from having to type `&*X` all the time at the cost of some complexity in the type signature. For instance, this allows me to accommodate @jakobhellermann's suggestion in bevyengine#1799 without requiring users to type `&*windows` 99% of the time.
@AThilenius
Copy link

Are there plans for this PR?


IMO this should focus exclusively on space transformation and not 'object picking' which the presence of the geometry primitives seems to imply. That's a much harder problem to solve for arbitrary geometry and generally relies on either a physics engine ray-casting or color-readback from offscreen buffers using an "object ID" shader.

This (below) seems sufficient. Or possibly return a Ray with a point_at_distance(&self, distance_from_start: f32) -> Vec3 function if you want to get fancy. Space transformation with a non-ortho camera is of limited use; it quickly gets into the aforementioned ray-casting territory for anything but the most trivial of cases.

pub fn screen_to_world_point_at_distance(
    pos_screen: Vec2,
    window: &Window,
    camera: &Camera,
    camera_transform: &GlobalTransform,
    distance: f32,
) -> Vec3 {
    let camera_position = camera_transform.compute_matrix();
    let screen_size = Vec2::from([window.width() as f32, window.height() as f32]);
    let projection_matrix = camera.projection_matrix;

    // Normalized device coordinate cursor position from (-1, -1, -1) to (1, 1, 1)
    let cursor_ndc = (pos_screen / screen_size) * 2.0 - Vec2::from([1.0, 1.0]);
    let cursor_pos_ndc_near: Vec3 = cursor_ndc.extend(-1.0);
    let cursor_pos_ndc_far: Vec3 = cursor_ndc.extend(1.0);

    // Use near and far ndc points to generate a ray in world space
    // This method is more robust than using the location of the camera as the start of
    // the ray, because ortho cameras have a focal point at infinity!
    let ndc_to_world: Mat4 = camera_position * projection_matrix.inverse();
    let cursor_pos_near: Vec3 = ndc_to_world.project_point3(cursor_pos_ndc_near);
    let cursor_pos_far: Vec3 = ndc_to_world.project_point3(cursor_pos_ndc_far);
    let ray_direction = cursor_pos_far - cursor_pos_near;

    cursor_pos_near + (ray_direction * distance)
}

@anchpop
Copy link
Contributor Author

anchpop commented Sep 6, 2021

@AThilenius

IMO this should focus exclusively on space transformation and not 'object picking' which the presence of the geometry primitives seems to imply. That's a much harder problem to solve for arbitrary geometry and generally relies on either a physics engine ray-casting or color-readback from offscreen buffers using an "object ID" shader.

This PR doesn't have anything to do with object picking or raycasting (as it's normally interpreted). The issue is that any screen pixel corresponds to a line in world space (really a ray). It's often useful to have that line, and it's often useful to see where that line intersects with a plane. For example, imagine a top-down shooter like hotline miami, but with a perspective camera at a slight tilt. To determine where the player is aiming their gun, you need to find the plane parallel to the ground that intersects the player, then calculate the intersection of that plane and the line corresponding to the pixel the user's mouse is on.

@AThilenius
Copy link

AThilenius commented Sep 6, 2021

@anchpop Indeed, that's what I said 😋

A space transform from screen to world creates a ray (hence "or possibly return a Ray") but where that ray interests with geometry (ie. "it's often useful to see where that line intersects with...") is the very definition of ray casting and IMO outside the scope of a space transformation. In other words this seems to combine two problems: transforming a pixel into a world Ray, and seeing where that Ray intersects with geometry. The former is a space transformation, the latter is ray casting and is typically delegated to a physics engine's spatial querying ability.

This certainly isn't meant ad hominem of course! Both are needed, I'm just casting a vote to keep those separate and distinct concepts instead of munging them together 😁

@rope-hmg
Copy link

rope-hmg commented Mar 7, 2022

Hi all, is there any chance of this getting added soon, or should I implement my own for now?
Thanks

@alice-i-cecile
Copy link
Member

Hi all, is there any chance of this getting added soon, or should I implement my own for now?

I'm guiding @omarbassam88 through creating a scoped version of this. However, there's no guarantee on when that will be merged in, so I would suggest unblocking yourself by coding your own simple implementation (or using bevy_mod_picking).

@alice-i-cecile alice-i-cecile added the X-Controversial There is active debate or serious implications around merging this PR label Apr 25, 2022
@alice-i-cecile
Copy link
Member

Closing in favor of the fresher and more scoped #4177.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Core Common functionality for all bevy apps C-Usability A simple quality-of-life change that makes Bevy easier to use X-Controversial There is active debate or serious implications around merging this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.