Files
endorphin-tessera-gui/src/render/scene3d/math.rs
T

306 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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<Self> {
// 先求模长,拒绝非有限值和长度不大于 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<f32> 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<Self> {
// 这里按仿射矩阵处理:最后一行假设为 [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<Basis> {
// 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<Ray> {
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]))
}
}