Skip to content

Commit 7a94dc4

Browse files
tim-blackbirdjames7132
authored andcommitted
Add Camera::viewport_to_world (bevyengine#6126)
# Objective Add a method for getting a world space ray from a viewport position. Opted to add a `Ray` type to `bevy_math` instead of returning a tuple of `Vec3`'s as this is clearer and easier to document The docs on `viewport_to_world` are okay, but I'm not super happy with them. ## Changelog * Add `Camera::viewport_to_world` * Add `Camera::ndc_to_world` * Add `Ray` to `bevy_math` * Some doc tweaks Co-authored-by: devil-ira <justthecooldude@gmail.com>
1 parent 43b57b3 commit 7a94dc4

File tree

3 files changed

+63
-10
lines changed

3 files changed

+63
-10
lines changed

crates/bevy_math/src/lib.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,18 @@
66
77
#![warn(missing_docs)]
88

9+
mod ray;
910
mod rect;
1011

12+
pub use ray::Ray;
1113
pub use rect::Rect;
1214

1315
/// The `bevy_math` prelude.
1416
pub mod prelude {
1517
#[doc(hidden)]
1618
pub use crate::{
17-
BVec2, BVec3, BVec4, EulerRot, IVec2, IVec3, IVec4, Mat2, Mat3, Mat4, Quat, Rect, UVec2,
18-
UVec3, UVec4, Vec2, Vec3, Vec4,
19+
BVec2, BVec3, BVec4, EulerRot, IVec2, IVec3, IVec4, Mat2, Mat3, Mat4, Quat, Ray, Rect,
20+
UVec2, UVec3, UVec4, Vec2, Vec3, Vec4,
1921
};
2022
}
2123

crates/bevy_math/src/ray.rs

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use crate::Vec3;
2+
use serde::{Deserialize, Serialize};
3+
4+
/// A ray is an infinite line starting at `origin`, going in `direction`.
5+
#[derive(Default, Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
6+
pub struct Ray {
7+
/// The origin of the ray.
8+
pub origin: Vec3,
9+
/// The direction of the ray.
10+
pub direction: Vec3,
11+
}

crates/bevy_render/src/camera/camera.rs

+48-8
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use bevy_ecs::{
1717
reflect::ReflectComponent,
1818
system::{Commands, ParamSet, Query, Res},
1919
};
20-
use bevy_math::{Mat4, UVec2, UVec4, Vec2, Vec3};
20+
use bevy_math::{Mat4, Ray, UVec2, UVec4, Vec2, Vec3};
2121
use bevy_reflect::prelude::*;
2222
use bevy_reflect::FromReflect;
2323
use bevy_transform::components::GlobalTransform;
@@ -212,26 +212,66 @@ impl Camera {
212212
Some((ndc_space_coords.truncate() + Vec2::ONE) / 2.0 * target_size)
213213
}
214214

215+
/// Returns a ray originating from the camera, that passes through everything beyond `viewport_position`.
216+
///
217+
/// The resulting ray starts on the near plane of the camera.
218+
///
219+
/// If the camera's projection is orthographic the direction of the ray is always equal to `camera_transform.forward()`.
220+
///
221+
/// To get the world space coordinates with Normalized Device Coordinates, you should use
222+
/// [`ndc_to_world`](Self::ndc_to_world).
223+
pub fn viewport_to_world(
224+
&self,
225+
camera_transform: &GlobalTransform,
226+
viewport_position: Vec2,
227+
) -> Option<Ray> {
228+
let target_size = self.logical_viewport_size()?;
229+
let ndc = viewport_position * 2. / target_size - Vec2::ONE;
230+
231+
let world_near_plane = self.ndc_to_world(camera_transform, ndc.extend(1.))?;
232+
// Using EPSILON because passing an ndc with Z = 0 returns NaNs.
233+
let world_far_plane = self.ndc_to_world(camera_transform, ndc.extend(f32::EPSILON))?;
234+
235+
Some(Ray {
236+
origin: world_near_plane,
237+
direction: (world_far_plane - world_near_plane).normalize(),
238+
})
239+
}
240+
215241
/// Given a position in world space, use the camera's viewport to compute the Normalized Device Coordinates.
216242
///
217-
/// Values returned will be between -1.0 and 1.0 when the position is within the viewport.
243+
/// When the position is within the viewport the values returned will be between -1.0 and 1.0 on the X and Y axes,
244+
/// and between 0.0 and 1.0 on the Z axis.
218245
/// To get the coordinates in the render target's viewport dimensions, you should use
219246
/// [`world_to_viewport`](Self::world_to_viewport).
220247
pub fn world_to_ndc(
221248
&self,
222249
camera_transform: &GlobalTransform,
223250
world_position: Vec3,
224251
) -> Option<Vec3> {
225-
// Build a transform to convert from world to NDC using camera data
252+
// Build a transformation matrix to convert from world space to NDC using camera data
226253
let world_to_ndc: Mat4 =
227254
self.computed.projection_matrix * camera_transform.compute_matrix().inverse();
228255
let ndc_space_coords: Vec3 = world_to_ndc.project_point3(world_position);
229256

230-
if !ndc_space_coords.is_nan() {
231-
Some(ndc_space_coords)
232-
} else {
233-
None
234-
}
257+
(!ndc_space_coords.is_nan()).then_some(ndc_space_coords)
258+
}
259+
260+
/// Given a position in Normalized Device Coordinates,
261+
/// use the camera's viewport to compute the world space position.
262+
///
263+
/// When the position is within the viewport the values returned will be between -1.0 and 1.0 on the X and Y axes,
264+
/// and between 0.0 and 1.0 on the Z axis.
265+
/// To get the world space coordinates with the viewport position, you should use
266+
/// [`world_to_viewport`](Self::world_to_viewport).
267+
pub fn ndc_to_world(&self, camera_transform: &GlobalTransform, ndc: Vec3) -> Option<Vec3> {
268+
// Build a transformation matrix to convert from NDC to world space using camera data
269+
let ndc_to_world =
270+
camera_transform.compute_matrix() * self.computed.projection_matrix.inverse();
271+
272+
let world_space_coords = ndc_to_world.project_point3(ndc);
273+
274+
(!world_space_coords.is_nan()).then_some(world_space_coords)
235275
}
236276
}
237277

0 commit comments

Comments
 (0)