//! 3D 数学基础:向量、仿射矩阵、摄像机基向量、投影射线和三角形相交。 //! 矩阵组合顺序决定局部变换如何进入父坐标系;接近退化的逆矩阵、法线和射线使用 Option 拒绝无效计算。 //! use serde::{Deserialize, Serialize}; use std::ops::{Add, Mul, Sub}; #[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)] /// 三维数学向量;构造不校验分量,需在输入边界显式调用 valid,不是托管 Vec3 对象。 pub struct Vec3 { pub x: f32, pub y: f32, pub z: f32, } impl Vec3 { /// 直接保存三个分量,不隐式归一化或拒绝非有限值。 pub const fn new(x: f32, y: f32, z: f32) -> Self { Self { x, y, z } } /// 计算向量点积,用于方向投影、光照和相交判断。 pub fn dot(self, b: Self) -> f32 { self.x * b.x + self.y * b.y + self.z * b.z } /// 计算右手叉积,用于摄像机基向量和三角形法线。 pub fn cross(self, b: Self) -> Self { Self::new( self.y * b.z - self.z * b.y, self.z * b.x - self.x * b.z, self.x * b.y - self.y * b.x, ) } /// 计算向量长度;归一化前还需检查是否接近零。 pub fn length(self) -> f32 { self.dot(self).sqrt() } /// 仅在长度有效且不退化时返回单位向量,避免除零产生非有限结果。 pub fn normalized(self) -> Option { // 先求模长,拒绝非有限值和长度不大于 1e-6 的向量;返回 None 让相机/法线调用方停止退化计算。 let length = self.length(); (length.is_finite() && length > 1e-6).then(|| self * (1.0 / length)) } /// 要求三维向量各分量有限、绝对值不超过一百万;构造和算术运算不会自动调用此检查。 pub fn valid(self) -> bool { // 构造函数与算术运算不会自动验证;此入口显式要求每个分量有限且绝对值至多一百万。 [self.x, self.y, self.z] .iter() .all(|v| v.is_finite() && v.abs() <= 1_000_000.0) } } impl Add for Vec3 { type Output = Self; /// 按分量执行向量加减或标量乘法,保持数学运算不依赖 VM 状态。 fn add(self, b: Self) -> Self { Self::new(self.x + b.x, self.y + b.y, self.z + b.z) } } impl Sub for Vec3 { type Output = Self; /// 按分量执行向量加减或标量乘法,保持数学运算不依赖 VM 状态。 fn sub(self, b: Self) -> Self { Self::new(self.x - b.x, self.y - b.y, self.z - b.z) } } impl Mul for Vec3 { type Output = Self; /// 按分量执行向量加减或标量乘法,保持数学运算不依赖 VM 状态。 fn mul(self, b: f32) -> Self { Self::new(self.x * b, self.y * b, self.z * b) } } #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] /// 仿射 4×4 变换矩阵,支持局部到世界的组合与求逆。 pub struct Matrix(pub [f32; 16]); impl Default for Matrix { /// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。 fn default() -> Self { Self([ 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., ]) } } impl Matrix { /// 求仿射世界矩阵的逆;退化或不可逆时返回空值。 pub fn inverse(self) -> Option { // 这里按仿射矩阵处理:最后一行假设为 [0,0,0,1],只逆左上 3×3,不实现一般透视矩阵求逆。 let m = self.0; let determinant = m[0] * (m[5] * m[10] - m[6] * m[9]) - m[1] * (m[4] * m[10] - m[6] * m[8]) + m[2] * (m[4] * m[9] - m[5] * m[8]); // 接近零行列式意味着某方向被压扁或数值不稳定;不能继续除以 determinant。 if !determinant.is_finite() || determinant.abs() < 1e-9 { return None; } // 用伴随矩阵除行列式得到线性部分逆矩阵;先把逆平移留为零。 let d = 1. / determinant; let mut out = Self([ (m[5] * m[10] - m[6] * m[9]) * d, (m[2] * m[9] - m[1] * m[10]) * d, (m[1] * m[6] - m[2] * m[5]) * d, 0., (m[6] * m[8] - m[4] * m[10]) * d, (m[0] * m[10] - m[2] * m[8]) * d, (m[2] * m[4] - m[0] * m[6]) * d, 0., (m[4] * m[9] - m[5] * m[8]) * d, (m[1] * m[8] - m[0] * m[9]) * d, (m[0] * m[5] - m[1] * m[4]) * d, 0., 0., 0., 0., 1., ]); // 仿射逆平移为 -A⁻¹t。此时 out 平移为零,所以 point 只应用刚求得的线性逆变换。 let translation = out.point(Vec3::new(m[3], m[7], m[11])) * -1.; out.0[3] = translation.x; out.0[7] = translation.y; out.0[11] = translation.z; Some(out) } /// 组合父子仿射变换,调用顺序决定局部坐标最终落在哪个世界位置。 pub fn compose(self, local: Self) -> Self { // 行主序存储,点视作列向量;self.compose(local) = self × local,即先应用 local 再应用 self。 let mut out = [0.; 16]; for row in 0..4 { for col in 0..4 { out[row * 4 + col] = (0..4) .map(|k| self.0[row * 4 + k] * local.0[k * 4 + col]) .sum(); } } Self(out) } /// 对一个位置应用矩阵,包括平移项;不要用它代替纯方向变换。 pub fn point(self, p: Vec3) -> Vec3 { Vec3::new( self.0[0] * p.x + self.0[1] * p.y + self.0[2] * p.z + self.0[3], self.0[4] * p.x + self.0[5] * p.y + self.0[6] * p.z + self.0[7], self.0[8] * p.x + self.0[9] * p.y + self.0[10] * p.z + self.0[11], ) } /// 由平移、角度旋转和缩放构造局部仿射矩阵。 pub fn trs(position: Vec3, rotation: Vec3, scale: Vec3) -> Self { // 输入欧拉角单位为度,先转换为弧度。组合为 T × Rz × Ry × Rx × S,列分别乘对应缩放轴。 let (sx, cx) = rotation.x.to_radians().sin_cos(); let (sy, cy) = rotation.y.to_radians().sin_cos(); let (sz, cz) = rotation.z.to_radians().sin_cos(); // Local X, then Y, then Z rotation; scale first, translation last. Self([ cz * cy * scale.x, (cz * sy * sx - sz * cx) * scale.y, (cz * sy * cx + sz * sx) * scale.z, position.x, sz * cy * scale.x, (sz * sy * sx + cz * cx) * scale.y, (sz * sy * cx - cz * sx) * scale.z, position.y, -sy * scale.x, cy * sx * scale.y, cy * cx * scale.z, position.z, 0., 0., 0., 1., ]) } } #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] /// 投影策略:透视视场角或正交可见高度。 pub enum Projection { // fov 表示垂直视场角(度),水平范围再乘视口宽高比。 Perspective { fov: f32 }, // height 是相机空间可见总高度,与深度无关;宽度为 height × aspect。 Orthographic { height: f32 }, } #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] /// 原生摄像机描述,包括观察基、近远面和投影参数;宽高比由视口提供。 pub struct Camera { pub position: Vec3, pub target: Vec3, pub up: Vec3, pub near: f32, pub far: f32, pub projection: Projection, } impl Default for Camera { /// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。 fn default() -> Self { Self { position: Vec3::new(0., 0., 5.), target: Vec3::default(), up: Vec3::new(0., 1., 0.), near: 0.1, far: 1000., projection: Projection::Perspective { fov: 50. }, } } } #[derive(Debug, Clone, Copy)] /// 摄像机的右、上、前单位基向量。 pub struct Basis { pub right: Vec3, pub up: Vec3, pub forward: Vec3, } impl Camera { /// 从观察位置、目标和上方向建立正交相机基;方向退化时拒绝。 pub fn basis(self) -> Option { // forward 指向观察目标;up 输入只提供大致上方向,叉积后重新正交化。 // 位置等于目标、up 为零或与前向平行时,normalized 返回 None。 let forward = (self.target - self.position).normalized()?; let right = forward.cross(self.up).normalized()?; let up = right.cross(forward).normalized()?; Some(Basis { right, up, forward }) } /// 验证相机向量/观察基、裁剪面次序和投影参数;退化方向、非法视场角或正交高度均返回 false。 pub fn valid(self) -> bool { // 除了向量范围,还要求有效观察基、0 < near < far,以及合法视场角或正交高度。 self.position.valid() && self.target.valid() && self.up.valid() && self.basis().is_some() && self.near.is_finite() && self.far.is_finite() && self.near > 0. && self.far > self.near && self.far <= 1_000_000. && match self.projection { Projection::Perspective { fov } => fov.is_finite() && (1.0..=175.0).contains(&fov), Projection::Orthographic { height } => { height.is_finite() && height > 0. && height <= 1_000_000. } } } /// 把世界点投影到相机的右/上/前基向量坐标。 pub fn view(self, basis: Basis, p: Vec3) -> Vec3 { // 先把世界点相对化,再点乘正交基;这里前方 z 为正,与某些图形 API 的负 z 约定不同。 let p = p - self.position; Vec3::new(p.dot(basis.right), p.dot(basis.up), p.dot(basis.forward)) } /// 从视口归一化坐标生成透视或正交射线;宽高比由具体视口提供。 pub fn ray(self, u: f32, v: f32, aspect: f32) -> Option { let basis = self.basis()?; // u/v 通常是视口内 [0,1],映射到 [-1,1] 并翻转屏幕 y。 // 此函数不钳制输入,捕获拖动可以从视口外继续生成射线;aspect 由调用方保证有效。 let (x, y) = (u * 2. - 1., 1. - v * 2.); match self.projection { Projection::Perspective { fov } => { // 透视射线共用相机原点,方向沿前向加上视平面偏移,最后归一化。 let half = (fov.to_radians() / 2.).tan(); Some(Ray { origin: self.position, direction: (basis.forward + basis.right * (x * half * aspect) + basis.up * (y * half)) .normalized()?, }) } // 正交射线方向都为 forward,只改变起点在相机右/上平面中的位置。 Projection::Orthographic { height } => Some(Ray { origin: self.position + basis.right * (x * height * aspect / 2.) + basis.up * (y * height / 2.), direction: basis.forward, }), } } } #[derive(Debug, Clone, Copy)] /// 世界坐标中的射线原点与方向,供网格和面板求交。 pub struct Ray { pub origin: Vec3, pub direction: Vec3, } impl Ray { /// 求射线与三角形相交距离和重心坐标,拒绝退化三角形及射线起点后方的交点。 pub fn triangle(self, a: Vec3, b: Vec3, c: Vec3) -> Option<(f32, [f32; 3])> { // Möller–Trumbore 求交:使用两条三角形边与叉积解重心系数,不先构造平面方程。 let ab = b - a; let ac = c - a; let p = self.direction.cross(ac); let determinant = ab.dot(p); // 近零表示射线与面平行或三角形退化;保留正负行列式,因此不做背面剔除。 if determinant.abs() < 1e-7 { return None; } let inverse = 1.0 / determinant; let t = self.origin - a; // u 是顶点 b 的重心权重,必须位于 [0,1];v 是 c 的权重,a 的权重由 1-u-v 得到。 let u = t.dot(p) * inverse; if !(0.0..=1.0).contains(&u) { return None; } let q = t.cross(ab); let v = self.direction.dot(q) * inverse; if v < 0. || u + v > 1. { return None; } // 返回的是射线参数 t,方向为单位向量时才等于世界距离;负值在射线起点后方,被拒绝。 let distance = ac.dot(q) * inverse; (distance >= 0.).then_some((distance, [1. - u - v, u, v])) } }