#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub struct Aabb {
pub min: [f64; 3],
pub max: [f64; 3],
}
impl Aabb {
pub fn empty() -> Self {
Self {
min: [f64::INFINITY; 3],
max: [f64::NEG_INFINITY; 3],
}
}
pub fn is_empty(&self) -> bool {
(0..3).any(|i| self.min[i] > self.max[i])
}
pub fn expand(&mut self, p: [f64; 3]) {
for i in 0..3 {
self.min[i] = self.min[i].min(p[i]);
self.max[i] = self.max[i].max(p[i]);
}
}
pub fn union(&mut self, other: &Aabb) {
if other.is_empty() {
return;
}
self.expand(other.min);
self.expand(other.max);
}
pub fn center(&self) -> [f64; 3] {
[
(self.min[0] + self.max[0]) * 0.5,
(self.min[1] + self.max[1]) * 0.5,
(self.min[2] + self.max[2]) * 0.5,
]
}
pub fn size(&self) -> [f64; 3] {
[
self.max[0] - self.min[0],
self.max[1] - self.min[1],
self.max[2] - self.min[2],
]
}
}
fn norm(v: [f64; 3]) -> [f64; 3] {
let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
if len <= 0.0 {
return [0.0, 0.0, 1.0];
}
[v[0] / len, v[1] / len, v[2] / len]
}
fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[derive(Debug, Clone, Copy)]
pub struct Camera {
pub view_proj: [[f32; 4]; 4],
pub forward: [f32; 3],
}
fn look_at(eye: [f64; 3], target: [f64; 3], up: [f64; 3]) -> [[f64; 4]; 4] {
let f = norm([target[0] - eye[0], target[1] - eye[1], target[2] - eye[2]]);
let r = norm(cross(f, up));
let u = cross(r, f);
[
[r[0], u[0], -f[0], 0.0],
[r[1], u[1], -f[1], 0.0],
[r[2], u[2], -f[2], 0.0],
[-dot(r, eye), -dot(u, eye), dot(f, eye), 1.0],
]
}
fn ortho(l: f64, r: f64, b: f64, t: f64, near: f64, far: f64) -> [[f64; 4]; 4] {
let sx = 2.0 / (r - l);
let sy = 2.0 / (t - b);
let sz = -1.0 / (far - near);
[
[sx, 0.0, 0.0, 0.0],
[0.0, sy, 0.0, 0.0],
[0.0, 0.0, sz, 0.0],
[
-(r + l) / (r - l),
-(t + b) / (t - b),
-near / (far - near),
1.0,
],
]
}
fn mul(a: [[f64; 4]; 4], b: [[f64; 4]; 4]) -> [[f64; 4]; 4] {
let mut out = [[0.0; 4]; 4];
for col in 0..4 {
for row in 0..4 {
let mut sum = 0.0;
for k in 0..4 {
sum += a[k][row] * b[col][k];
}
out[col][row] = sum;
}
}
out
}
fn to_f32(m: [[f64; 4]; 4]) -> [[f32; 4]; 4] {
let mut out = [[0.0f32; 4]; 4];
for c in 0..4 {
for r in 0..4 {
out[c][r] = m[c][r] as f32;
}
}
out
}
pub fn ortho_camera(
eye: [f64; 3],
target: [f64; 3],
up: [f64; 3],
half_width: f64,
half_height: f64,
near: f64,
far: f64,
) -> Camera {
let view = look_at(eye, target, up);
let proj = ortho(-half_width, half_width, -half_height, half_height, near, far);
let forward = norm([
target[0] - eye[0],
target[1] - eye[1],
target[2] - eye[2],
]);
Camera {
view_proj: to_f32(mul(proj, view)),
forward: [forward[0] as f32, forward[1] as f32, forward[2] as f32],
}
}
pub fn artifact_camera(bbox: &Aabb, width: u32, height: u32) -> Camera {
let bbox = if bbox.is_empty() {
Aabb {
min: [-1.0, -1.0, -1.0],
max: [1.0, 1.0, 1.0],
}
} else {
*bbox
};
let center = bbox.center();
let size = bbox.size();
let radius = size[0].max(size[1]).max(size[2]).max(1e-9) * 0.75;
let dir = norm([1.0, -1.2, 0.9]);
let eye = [
center[0] + dir[0] * radius * 6.0,
center[1] + dir[1] * radius * 6.0,
center[2] + dir[2] * radius * 6.0,
];
let aspect = width.max(1) as f64 / height.max(1) as f64;
ortho_camera(
eye,
center,
[0.0, 0.0, 1.0],
radius * aspect * 1.15,
radius * 1.15,
0.01,
radius * 20.0,
)
}
#[derive(Debug, Clone, Copy)]
pub struct Orbit {
pub azimuth: f64,
pub elevation: f64,
pub zoom: f64,
}
impl Default for Orbit {
fn default() -> Self {
let dir = norm([1.0, -1.2, 0.9]);
Self {
azimuth: dir[1].atan2(dir[0]),
elevation: dir[2].asin(),
zoom: 1.0,
}
}
}
impl Orbit {
pub fn camera(&self, bbox: &Aabb, width: u32, height: u32) -> Camera {
let bbox = if bbox.is_empty() {
Aabb {
min: [-1.0, -1.0, -1.0],
max: [1.0, 1.0, 1.0],
}
} else {
*bbox
};
let center = bbox.center();
let size = bbox.size();
let radius = size[0].max(size[1]).max(size[2]).max(1e-9) * 0.75;
let el = self.elevation.clamp(-1.55, 1.55);
let dir = [
el.cos() * self.azimuth.cos(),
el.cos() * self.azimuth.sin(),
el.sin(),
];
let eye = [
center[0] + dir[0] * radius * 6.0,
center[1] + dir[1] * radius * 6.0,
center[2] + dir[2] * radius * 6.0,
];
let aspect = width.max(1) as f64 / height.max(1) as f64;
let half_h = radius * 1.15 * self.zoom;
ortho_camera(
eye,
center,
[0.0, 0.0, 1.0],
half_h * aspect,
half_h,
0.01,
radius * 20.0,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn artifact_camera_centers_bbox() {
let bbox = Aabb {
min: [0.0, 0.0, 0.0],
max: [10.0, 10.0, 10.0],
};
let cam = artifact_camera(&bbox, 640, 480);
let c = [5.0f32, 5.0, 5.0, 1.0];
let m = cam.view_proj;
let mut clip = [0.0f32; 4];
for row in 0..4 {
clip[row] = m[0][row] * c[0] + m[1][row] * c[1] + m[2][row] * c[2] + m[3][row] * c[3];
}
assert!((clip[0] / clip[3]).abs() < 1e-5);
assert!((clip[1] / clip[3]).abs() < 1e-5);
let z = clip[2] / clip[3];
assert!((0.0..=1.0).contains(&z), "z {z}");
for corner in [[0.0f32, 0.0, 0.0, 1.0], [10.0, 10.0, 10.0, 1.0]] {
let mut clip = [0.0f32; 4];
for row in 0..4 {
clip[row] = m[0][row] * corner[0]
+ m[1][row] * corner[1]
+ m[2][row] * corner[2]
+ m[3][row] * corner[3];
}
assert!((clip[0] / clip[3]).abs() <= 1.0);
assert!((clip[1] / clip[3]).abs() <= 1.0);
}
}
}