// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// A camera looking at a scene: what a world point lands on, in screen space,
/// what of the scene survives the frustum, and what a screen point sees when the
/// ray is traced back out into the world.
///
/// moon run src/examples/02-camera
#coverage.skip
fn main {
let width : Float = 1280.0
let height : Float = 720.0
let eye = @moonmath.Vec3::new(0.0, 2.0, 6.0)
let target = @moonmath.Vec3::new(0.0, 1.0, 0.0)
let up = @moonmath.Vec3::unit_y()
let projection = @moonmath.Mat4::perspective(
@moonmath.to_radians(50.0),
width / height,
0.1,
200.0,
)
let view = @moonmath.Mat4::look_at(eye, target, up)
let view_projection = projection * view
println("-- where things land on screen")
let scene = [
("in front", @moonmath.Vec3::new(0.0, 1.0, 0.0)),
("to the right", @moonmath.Vec3::new(2.0, 1.0, 0.0)),
("above", @moonmath.Vec3::new(0.0, 4.0, 0.0)),
("behind the camera", @moonmath.Vec3::new(0.0, 1.0, 12.0)),
]
for item in scene {
let (name, point) = item
let clip = view_projection.mul_vec4(point.to_vec4(1.0))
if clip.w <= 0.0 {
println("\{name}: behind the camera")
} else {
let ndc = clip.xyz().divide(clip.w)
let half : Float = 0.5
let one : Float = 1.0
let x = (ndc.x * half + half) * width
let y = (one - (ndc.y * half + half)) * height
println("\{name}: pixel (\{x}, \{y}), depth \{ndc.z}")
}
}
println("\n-- what the frustum keeps")
let frustum = @moonmath.Frustum::from_matrix(view_projection).unwrap()
let objects = [
(
"crate",
@moonmath.Aabb3::new(
@moonmath.Vec3::new(-1.0, 0.0, -1.0),
@moonmath.Vec3::new(0.0, 1.0, 0.0),
),
),
(
"far tower",
@moonmath.Aabb3::new(
@moonmath.Vec3::new(9.0, 0.0, -40.0),
@moonmath.Vec3::new(11.0, 30.0, -38.0),
),
),
(
"behind us",
@moonmath.Aabb3::new(
@moonmath.Vec3::new(-1.0, 0.0, 8.0),
@moonmath.Vec3::new(1.0, 1.0, 10.0),
),
),
(
"sun",
@moonmath.Aabb3::new(
@moonmath.Vec3::new(-1.0, 0.0, -100.0),
@moonmath.Vec3::new(1.0, 2.0, -98.0),
),
),
]
for object in objects {
let (name, box) = object
let verdict = if frustum.contains_aabb(box) {
"fully visible"
} else if frustum.intersects_aabb(box) {
"partly visible"
} else {
"culled"
}
println("\{name}: \{verdict}")
}
println("\n-- what a click on the screen hits")
let inverse = view_projection.inverse().unwrap()
let pick = @moonmath.Vec2::new(width * 0.5, height * 0.5)
// Screen to clip space: the depth is the near plane, which is what a click
// knows and all it needs, since the ray is what the two points define.
let near = @moonmath.Vec4::new(
pick.x / width * 2.0 - 1.0,
1.0 - pick.y / height * 2.0,
0.0,
1.0,
)
let far = @moonmath.Vec4::new(near.x, near.y, 1.0, 1.0)
let start = inverse.transform_point(near.xyz().divide(near.w))
let end = inverse.transform_point(far.xyz().divide(far.w))
let ray = @moonmath.Ray3::new(start, (end - start).normalize())
println("ray from \{ray.origin} towards \{ray.direction}")
let ground = @moonmath.Plane::new(@moonmath.Vec3::unit_y(), 0.0)
match ray.intersect_plane(ground) {
Some(t) => println("it meets the ground at \{ray.at(t)}")
None => println("it never meets the ground")
}
}