Skip to main content

game_toolkit_gfx/
camera3d.rs

1//! Perspective 3D camera producing a column-major `view_proj` for the same camera uniform
2//! the 2D path uses. Right-handed, looking down -Z, with wgpu's `[0, 1]` depth range.
3
4use crate::transform::{Mat4, mul};
5
6/// A camera with a `view_proj` matrix, implemented by both [`crate::Camera2D`] and
7/// [`Camera3D`] so a pipeline can be fed by either.
8pub trait Camera {
9    fn view_proj(&self) -> Mat4;
10}
11
12/// Perspective camera. Set [`Camera3D::eye`]/[`Camera3D::target`] to move it.
13#[derive(Copy, Clone, Debug)]
14pub struct Camera3D {
15    pub eye: [f32; 3],
16    pub target: [f32; 3],
17    pub up: [f32; 3],
18    /// Vertical field of view, in radians.
19    pub fov_y: f32,
20    pub aspect: f32,
21    pub near: f32,
22    pub far: f32,
23}
24
25impl Camera3D {
26    pub fn new(aspect: f32) -> Self {
27        Self {
28            eye: [0.0, 0.0, 5.0],
29            target: [0.0, 0.0, 0.0],
30            up: [0.0, 1.0, 0.0],
31            fov_y: 60.0_f32.to_radians(),
32            aspect,
33            near: 0.1,
34            far: 100.0,
35        }
36    }
37
38    pub fn resize(&mut self, w: f32, h: f32) {
39        if h > 0.0 {
40            self.aspect = w / h;
41        }
42    }
43}
44
45impl Camera for Camera3D {
46    fn view_proj(&self) -> Mat4 {
47        mul(
48            &perspective(self.fov_y, self.aspect, self.near, self.far),
49            &look_at(self.eye, self.target, self.up),
50        )
51    }
52}
53
54impl Camera for crate::Camera2D {
55    fn view_proj(&self) -> Mat4 {
56        crate::Camera2D::view_proj(self)
57    }
58}
59
60/// Right-handed perspective with a `[0, 1]` depth range (wgpu/Vulkan/Metal convention).
61fn perspective(fov_y: f32, aspect: f32, near: f32, far: f32) -> Mat4 {
62    let f = 1.0 / (fov_y * 0.5).tan();
63    [
64        [f / aspect, 0.0, 0.0, 0.0],
65        [0.0, f, 0.0, 0.0],
66        [0.0, 0.0, far / (near - far), -1.0],
67        [0.0, 0.0, (near * far) / (near - far), 0.0],
68    ]
69}
70
71/// Right-handed look-at view matrix.
72fn look_at(eye: [f32; 3], target: [f32; 3], up: [f32; 3]) -> Mat4 {
73    let z = normalize(sub(eye, target)); // forward is -z, so this points back toward the eye
74    let x = normalize(cross(up, z));
75    let y = cross(z, x);
76    [
77        [x[0], y[0], z[0], 0.0],
78        [x[1], y[1], z[1], 0.0],
79        [x[2], y[2], z[2], 0.0],
80        [-dot(x, eye), -dot(y, eye), -dot(z, eye), 1.0],
81    ]
82}
83
84fn sub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
85    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
86}
87fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
88    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
89}
90fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
91    [
92        a[1] * b[2] - a[2] * b[1],
93        a[2] * b[0] - a[0] * b[2],
94        a[0] * b[1] - a[1] * b[0],
95    ]
96}
97fn normalize(v: [f32; 3]) -> [f32; 3] {
98    let len = dot(v, v).sqrt().max(1e-6);
99    [v[0] / len, v[1] / len, v[2] / len]
100}