///|
/// Perspective camera with the glTF-compatible Y-up, right-handed convention.
pub(all) struct PerspectiveCamera {
fov_y_radians : Double
aspect : Double
near : Double
far : Double
} derive(Eq, Debug, ToJson)
///|
pub extend PerspectiveCamera with Debug::{to_repr}
///|
pub extend PerspectiveCamera with Eq::{not_equal, equal}
///|
pub extend PerspectiveCamera with ToJson::{to_json}
///|
pub fn PerspectiveCamera::new(
fov_y_radians? : Double = 0.7853981633974483,
aspect? : Double = 1.0,
near? : Double = 0.01,
far? : Double = 1000.0,
) -> PerspectiveCamera {
{
fov_y_radians: clamp(fov_y_radians, 0.05, 3.0),
aspect: if aspect <= 0.0 {
1.0
} else {
aspect
},
near: if near <= 0.0 {
0.01
} else {
near
},
far: if far <= near {
near + 1.0
} else {
far
},
}
}
///|
pub fn PerspectiveCamera::projection(self : PerspectiveCamera) -> Mat4 {
Mat4::perspective(
fov_y_radians=self.fov_y_radians,
aspect=self.aspect,
near=self.near,
far=self.far,
)
}
///|
pub fn PerspectiveCamera::with_aspect(
self : PerspectiveCamera,
aspect : Double,
) -> PerspectiveCamera {
{ ..self, aspect: if aspect <= 0.0 { 1.0 } else { aspect }, }
}
///|
pub fn PerspectiveCamera::fov_y_radians(self : PerspectiveCamera) -> Double {
self.fov_y_radians
}
///|
pub fn PerspectiveCamera::near(self : PerspectiveCamera) -> Double {
self.near
}
///|
pub fn PerspectiveCamera::far(self : PerspectiveCamera) -> Double {
self.far
}
///|
/// Orbit camera state. `polar` is measured from the positive Y axis and is
/// clamped away from the poles so orbiting cannot flip the view upside down.
pub(all) struct OrbitCamera {
target : Vec3
radius : Double
azimuth : Double
polar : Double
perspective : PerspectiveCamera
} derive(Eq, Debug, ToJson)
///|
pub extend OrbitCamera with Debug::{to_repr}
///|
pub extend OrbitCamera with Eq::{not_equal, equal}
///|
pub extend OrbitCamera with ToJson::{to_json}
///|
pub fn OrbitCamera::new(
target? : Vec3 = Vec3::zero(),
radius? : Double = 3.0,
azimuth? : Double = 0.0,
polar? : Double = 1.1,
perspective? : PerspectiveCamera = PerspectiveCamera::new(),
) -> OrbitCamera {
{
target,
radius: clamp(radius, 0.01, 1.0e9),
azimuth,
polar: clamp(polar, 0.01, @math.PI - 0.01),
perspective,
}
}
///|
pub fn OrbitCamera::target(self : OrbitCamera) -> Vec3 {
self.target
}
///|
pub fn OrbitCamera::radius(self : OrbitCamera) -> Double {
self.radius
}
///|
pub fn OrbitCamera::azimuth(self : OrbitCamera) -> Double {
self.azimuth
}
///|
pub fn OrbitCamera::polar(self : OrbitCamera) -> Double {
self.polar
}
///|
pub fn OrbitCamera::perspective(self : OrbitCamera) -> PerspectiveCamera {
self.perspective
}
///|
pub fn OrbitCamera::position(self : OrbitCamera) -> Vec3 {
let sine = @math.sin(self.polar)
Vec3::new(
x=self.target.x + self.radius * sine * @math.sin(self.azimuth),
y=self.target.y + self.radius * @math.cos(self.polar),
z=self.target.z + self.radius * sine * @math.cos(self.azimuth),
)
}
///|
pub fn OrbitCamera::view(self : OrbitCamera) -> Mat4 {
Mat4::look_at(
eye=self.position(),
target=self.target,
up=Vec3::new(x=0.0, y=1.0, z=0.0),
)
}
///|
pub fn OrbitCamera::projection(self : OrbitCamera) -> Mat4 {
self.perspective.projection()
}
///|
pub fn OrbitCamera::orbit(
self : OrbitCamera,
delta_azimuth : Double,
delta_polar : Double,
) -> OrbitCamera {
{
..self,
azimuth: self.azimuth + delta_azimuth,
polar: clamp(self.polar + delta_polar, 0.01, @math.PI - 0.01),
}
}
///|
pub fn OrbitCamera::zoom(self : OrbitCamera, amount : Double) -> OrbitCamera {
{ ..self, radius: clamp(self.radius * amount, 0.01, 1.0e9), }
}
///|
pub fn OrbitCamera::pan(self : OrbitCamera, delta : Vec2) -> OrbitCamera {
let forward = self.target.sub(self.position()).normalized()
let right = forward.cross(Vec3::new(x=0.0, y=1.0, z=0.0)).normalized()
let up = right.cross(forward).normalized()
let scale = self.radius * 0.0025
{
..self,
target: self.target
.add(right.scale(-delta.x * scale))
.add(up.scale(delta.y * scale)),
}
}
///|
pub fn OrbitCamera::with_aspect(
self : OrbitCamera,
aspect : Double,
) -> OrbitCamera {
{ ..self, perspective: self.perspective.with_aspect(aspect), }
}
///|
pub fn OrbitCamera::fit(
self : OrbitCamera,
bounds : Aabb,
padding? : Double = 1.2,
) -> OrbitCamera {
if !bounds.is_valid() {
self
} else {
let radius = clamp(bounds.radius() * clamp(padding, 1.0, 10.0), 0.01, 1.0e9)
let half_fov = self.perspective.fov_y_radians() / 2.0
let distance = radius / @math.sin(half_fov)
{ ..self, target: bounds.center(), radius: clamp(distance, 0.01, 1.0e9), }
}
}
///|
/// Construct a world-space ray through a logical viewport point.
pub fn OrbitCamera::ray_for_viewport(
self : OrbitCamera,
point : Vec2,
viewport : Vec2,
) -> Ray {
let width = if viewport.x <= 0.0 { 1.0 } else { viewport.x }
let height = if viewport.y <= 0.0 { 1.0 } else { viewport.y }
let normalized_x = point.x / width * 2.0 - 1.0
let normalized_y = 1.0 - point.y / height * 2.0
let forward = self.target.sub(self.position()).normalized()
let right = forward.cross(Vec3::new(x=0.0, y=1.0, z=0.0)).normalized()
let up = right.cross(forward).normalized()
let half_fov = self.perspective.fov_y_radians() / 2.0
let half_height = @math.tan(half_fov)
let half_width = half_height * width / height
let direction = forward
.add(right.scale(normalized_x * half_width))
.add(up.scale(normalized_y * half_height))
.normalized()
Ray::new(origin=self.position(), direction~)
}
///|
fn clamp(value : Double, minimum : Double, maximum : Double) -> Double {
if value < minimum {
minimum
} else if value > maximum {
maximum
} else {
value
}
}