Skip to content

Commit

Permalink
Add Camera::viewport_to_world (#6126)
Browse files Browse the repository at this point in the history
# 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>
  • Loading branch information
tim-blackbird and tim-blackbird committed Oct 5, 2022
1 parent 2362ceb commit 37860a0
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 10 deletions.
6 changes: 4 additions & 2 deletions crates/bevy_math/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@
#![warn(missing_docs)]

mod ray;
mod rect;

pub use ray::Ray;
pub use rect::Rect;

/// The `bevy_math` prelude.
pub mod prelude {
#[doc(hidden)]
pub use crate::{
BVec2, BVec3, BVec4, EulerRot, IVec2, IVec3, IVec4, Mat2, Mat3, Mat4, Quat, Rect, UVec2,
UVec3, UVec4, Vec2, Vec3, Vec4,
BVec2, BVec3, BVec4, EulerRot, IVec2, IVec3, IVec4, Mat2, Mat3, Mat4, Quat, Ray, Rect,
UVec2, UVec3, UVec4, Vec2, Vec3, Vec4,
};
}

Expand Down
11 changes: 11 additions & 0 deletions crates/bevy_math/src/ray.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use crate::Vec3;
use serde::{Deserialize, Serialize};

/// A ray is an infinite line starting at `origin`, going in `direction`.
#[derive(Default, Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
pub struct Ray {
/// The origin of the ray.
pub origin: Vec3,
/// The direction of the ray.
pub direction: Vec3,
}
56 changes: 48 additions & 8 deletions crates/bevy_render/src/camera/camera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use bevy_ecs::{
reflect::ReflectComponent,
system::{Commands, ParamSet, Query, Res},
};
use bevy_math::{Mat4, UVec2, UVec4, Vec2, Vec3};
use bevy_math::{Mat4, Ray, UVec2, UVec4, Vec2, Vec3};
use bevy_reflect::prelude::*;
use bevy_reflect::FromReflect;
use bevy_transform::components::GlobalTransform;
Expand Down Expand Up @@ -212,26 +212,66 @@ impl Camera {
Some((ndc_space_coords.truncate() + Vec2::ONE) / 2.0 * target_size)
}

/// Returns a ray originating from the camera, that passes through everything beyond `viewport_position`.
///
/// The resulting ray starts on the near plane of the camera.
///
/// If the camera's projection is orthographic the direction of the ray is always equal to `camera_transform.forward()`.
///
/// To get the world space coordinates with Normalized Device Coordinates, you should use
/// [`ndc_to_world`](Self::ndc_to_world).
pub fn viewport_to_world(
&self,
camera_transform: &GlobalTransform,
viewport_position: Vec2,
) -> Option<Ray> {
let target_size = self.logical_viewport_size()?;
let ndc = viewport_position * 2. / target_size - Vec2::ONE;

let world_near_plane = self.ndc_to_world(camera_transform, ndc.extend(1.))?;
// Using EPSILON because passing an ndc with Z = 0 returns NaNs.
let world_far_plane = self.ndc_to_world(camera_transform, ndc.extend(f32::EPSILON))?;

Some(Ray {
origin: world_near_plane,
direction: (world_far_plane - world_near_plane).normalize(),
})
}

/// Given a position in world space, use the camera's viewport to compute the Normalized Device Coordinates.
///
/// Values returned will be between -1.0 and 1.0 when the position is within the viewport.
/// When the position is within the viewport the values returned will be between -1.0 and 1.0 on the X and Y axes,
/// and between 0.0 and 1.0 on the Z axis.
/// To get the coordinates in the render target's viewport dimensions, you should use
/// [`world_to_viewport`](Self::world_to_viewport).
pub fn world_to_ndc(
&self,
camera_transform: &GlobalTransform,
world_position: Vec3,
) -> Option<Vec3> {
// Build a transform to convert from world to NDC using camera data
// Build a transformation matrix to convert from world space to NDC using camera data
let world_to_ndc: Mat4 =
self.computed.projection_matrix * camera_transform.compute_matrix().inverse();
let ndc_space_coords: Vec3 = world_to_ndc.project_point3(world_position);

if !ndc_space_coords.is_nan() {
Some(ndc_space_coords)
} else {
None
}
(!ndc_space_coords.is_nan()).then_some(ndc_space_coords)
}

/// Given a position in Normalized Device Coordinates,
/// use the camera's viewport to compute the world space position.
///
/// When the position is within the viewport the values returned will be between -1.0 and 1.0 on the X and Y axes,
/// and between 0.0 and 1.0 on the Z axis.
/// To get the world space coordinates with the viewport position, you should use
/// [`world_to_viewport`](Self::world_to_viewport).
pub fn ndc_to_world(&self, camera_transform: &GlobalTransform, ndc: Vec3) -> Option<Vec3> {
// Build a transformation matrix to convert from NDC to world space using camera data
let ndc_to_world =
camera_transform.compute_matrix() * self.computed.projection_matrix.inverse();

let world_space_coords = ndc_to_world.project_point3(ndc);

(!world_space_coords.is_nan()).then_some(world_space_coords)
}
}

Expand Down

0 comments on commit 37860a0

Please sign in to comment.