//! 3D 数学基础:向量、仿射矩阵、摄像机基向量、投影射线和三角形相交。 //! 矩阵组合顺序决定局部变换如何进入父坐标系;接近退化的逆矩阵、法线和射线使用 Option 拒绝无效计算。 //! use serde::{Deserialize, Serialize}; use std::ops::{Add, Mul, Sub}; #[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)] /// 三维数学向量;构造不校验分量,需在输入边界显式调用 valid,不是托管 Vec3 对象。 // model/scene3d 用它读取场景变换,原生光栅化、网格拾取和 panel 坐标映射共同复用此类型。 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; /// 按分量相加;摄像机射线用它叠加前向与视平面偏移。 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; /// 按分量相减;观察基与三角形求交用它得到相对位置和边向量。 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; /// 每个分量乘同一个标量;归一化、射线步进和光照计算用它调整向量长度。 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 变换矩阵,支持局部到世界的组合与求逆。 // model/scene3d 从 TRS 构造并组合世界矩阵;panel 使用其逆矩阵将射线交点还原为局部 UI 位置。 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]; // 逐行逐列计算输出的 16 个元素;k 遍历 self 的行与 local 的列,累加四项点积。 for row in 0..4 { // row/col 都属于输出矩阵;不能交换两个乘数,否则父子变换顺序会反转。 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(); // 点先缩放,再依次绕局部 X、Y、Z 旋转,最后加平移;这里直接展开组合矩阵以避免临时矩阵。 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)] /// 原生摄像机描述,包括观察基、近远面和投影参数;宽高比由视口提供。 // 场景发布时由 model 验证参数;View 渲染/拾取与 panel 映射随后使用同一份相机描述。 pub struct Camera { // 世界空间眼睛位置;透视投影的全部拾取射线从这里出发。 pub position: Vec3, // 世界空间观察目标,用 target-position 确定前向,而不是直接保存欧拉角。 pub target: Vec3, // 提供上方向参考,basis 会重新正交化;与前向平行时无法形成观察基。 pub up: Vec3, // 近远面按相机正向深度裁剪;valid 要求 0 < near < far。 pub near: f32, pub far: f32, pub projection: Projection, } impl Default for Camera { /// 返回从世界 z=5 观察原点的透视相机,供缺省场景与原生测试构造使用。 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()?; // 前向叉上方向产生右向;再次归一化,消除输入 up 长度对后续投影的影响。 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. // 投影类型决定最后的合法性条件;透视角度受限以避免极端 tan,正交高度必须为正。 && 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 { // View::pick 和 panel::Mapping 的逆投影都从这里生成射线;退化观察基直接向上传播 None。 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 { /// 求射线与三角形相交距离和重心坐标,拒绝退化三角形及射线起点后方的交点。 // View::pick 遍历网格三角形时调用,使用返回的射线参数选最近命中,重心权重还可插值纹理坐标。 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; } // 复用行列式倒数以解重心系数;t 此处是从顶点 a 指向射线起点的向量。 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; } // 第二次叉积用于同时求顶点 c 的权重和射线参数,避免另外求解完整线性方程组。 let q = t.cross(ab); let v = self.direction.dot(q) * inverse; // v 为负或 b/c 权重和超过 1 都表示交点落在三角形外,即使它仍位于三角形所在平面。 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])) } }