128 lines
4.1 KiB
Rust
128 lines
4.1 KiB
Rust
//! Minimale f32-Matrix-Mathematik für den Renderer — bewusst handgerollt
|
|
//! statt glam: PS1-Style braucht nur Multiply, View und Perspective.
|
|
//! (Die Spiellogik bekommt später ihr eigenes Q16.16 wie irl3d; das hier
|
|
//! ist nur der GPU-Pfad.)
|
|
//!
|
|
//! Konventionen:
|
|
//! - rechtshändig, Kamera blickt -Z, +Y oben (wie Blender-OBJ-Export)
|
|
//! - Spaltenvektoren, Speicher column-major — `Mat4.0[spalte][zeile]`,
|
|
//! bytemuck-kompatibel zu WGSL `mat4x4f`
|
|
//! - Clip-Z in [0,1] (wgpu/WebGPU, nicht GL-[-1,1])
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
|
pub struct Mat4(pub [[f32; 4]; 4]);
|
|
|
|
impl Mat4 {
|
|
pub const IDENT: Mat4 = Mat4([
|
|
[1.0, 0.0, 0.0, 0.0],
|
|
[0.0, 1.0, 0.0, 0.0],
|
|
[0.0, 0.0, 1.0, 0.0],
|
|
[0.0, 0.0, 0.0, 1.0],
|
|
]);
|
|
|
|
pub fn mul(&self, rhs: &Mat4) -> Mat4 {
|
|
let mut out = [[0.0f32; 4]; 4];
|
|
for c in 0..4 {
|
|
for r in 0..4 {
|
|
out[c][r] = (0..4).map(|k| self.0[k][r] * rhs.0[c][k]).sum();
|
|
}
|
|
}
|
|
Mat4(out)
|
|
}
|
|
|
|
/// Punkt-Transformation auf der CPU — bisher nur von den Tests
|
|
/// gebraucht; der Renderer transformiert auf der GPU.
|
|
#[cfg(test)]
|
|
pub fn transform(&self, v: [f32; 4]) -> [f32; 4] {
|
|
let mut out = [0.0f32; 4];
|
|
for r in 0..4 {
|
|
out[r] = (0..4).map(|k| self.0[k][r] * v[k]).sum();
|
|
}
|
|
out
|
|
}
|
|
|
|
pub fn translate(x: f32, y: f32, z: f32) -> Mat4 {
|
|
let mut m = Mat4::IDENT;
|
|
m.0[3] = [x, y, z, 1.0];
|
|
m
|
|
}
|
|
|
|
pub fn rot_x(a: f32) -> Mat4 {
|
|
let (s, c) = a.sin_cos();
|
|
let mut m = Mat4::IDENT;
|
|
m.0[1] = [0.0, c, s, 0.0];
|
|
m.0[2] = [0.0, -s, c, 0.0];
|
|
m
|
|
}
|
|
|
|
pub fn rot_y(a: f32) -> Mat4 {
|
|
let (s, c) = a.sin_cos();
|
|
let mut m = Mat4::IDENT;
|
|
m.0[0] = [c, 0.0, -s, 0.0];
|
|
m.0[2] = [s, 0.0, c, 0.0];
|
|
m
|
|
}
|
|
|
|
/// FPS-View: erst Kamera-Position abziehen, dann Yaw, dann Pitch
|
|
/// herausdrehen. Yaw/Pitch wie die irl3d-Kamera: yaw=0 blickt -Z,
|
|
/// positiver Pitch hebt den Blick.
|
|
pub fn view(pos: [f32; 3], yaw: f32, pitch: f32) -> Mat4 {
|
|
Mat4::rot_x(-pitch)
|
|
.mul(&Mat4::rot_y(-yaw))
|
|
.mul(&Mat4::translate(-pos[0], -pos[1], -pos[2]))
|
|
}
|
|
|
|
/// Perspektive mit Clip-Z in [0,1]. `fovy` in Radiant.
|
|
pub fn perspective(fovy: f32, aspect: f32, near: f32, far: f32) -> Mat4 {
|
|
let f = 1.0 / (fovy * 0.5).tan();
|
|
Mat4([
|
|
[f / aspect, 0.0, 0.0, 0.0],
|
|
[0.0, f, 0.0, 0.0],
|
|
[0.0, 0.0, far / (near - far), -1.0],
|
|
[0.0, 0.0, near * far / (near - far), 0.0],
|
|
])
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn close(a: [f32; 4], b: [f32; 4]) -> bool {
|
|
a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-5)
|
|
}
|
|
|
|
#[test]
|
|
fn view_translates_origin_in_front() {
|
|
// Kamera bei z=+5, Blick -Z → Origin liegt 5 vor der Kamera.
|
|
let v = Mat4::view([0.0, 0.0, 5.0], 0.0, 0.0);
|
|
assert!(close(v.transform([0.0, 0.0, 0.0, 1.0]), [0.0, 0.0, -5.0, 1.0]));
|
|
}
|
|
|
|
#[test]
|
|
fn view_yaw_quarter_turn_looks_minus_x() {
|
|
// yaw=90°: Blick Richtung -X (irl3d-Konvention) — ein Punkt auf
|
|
// -X liegt dann vor der Kamera.
|
|
let v = Mat4::view([0.0, 0.0, 0.0], std::f32::consts::FRAC_PI_2, 0.0);
|
|
assert!(close(v.transform([-2.0, 0.0, 0.0, 1.0]), [0.0, 0.0, -2.0, 1.0]));
|
|
}
|
|
|
|
#[test]
|
|
fn view_pitch_up_looks_plus_y() {
|
|
let v = Mat4::view([0.0, 0.0, 0.0], 0.0, std::f32::consts::FRAC_PI_2);
|
|
assert!(close(v.transform([0.0, 3.0, 0.0, 1.0]), [0.0, 0.0, -3.0, 1.0]));
|
|
}
|
|
|
|
#[test]
|
|
fn perspective_maps_near_far_to_0_1() {
|
|
let p = Mat4::perspective(1.0, 4.0 / 3.0, 0.1, 100.0);
|
|
let n = p.transform([0.0, 0.0, -0.1, 1.0]);
|
|
let f = p.transform([0.0, 0.0, -100.0, 1.0]);
|
|
assert!((n[2] / n[3] - 0.0).abs() < 1e-5);
|
|
assert!((f[2] / f[3] - 1.0).abs() < 1e-4);
|
|
// w = Abstand vor der Kamera
|
|
assert!((n[3] - 0.1).abs() < 1e-6 && (f[3] - 100.0).abs() < 1e-4);
|
|
}
|
|
}
|