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

286 lines
11 KiB
Rust

//! 不可变世界空间 BVH。用网格变换后的包围盒快速筛选视锥与射线候选。
//! 空间索引只做宽阶段筛选;最终拾取仍检查真实三角形。相机变化可复用索引,几何或变换变化需要新索引。
//!
//! Immutable world-space BVH. Camera changes reuse it; it contains no VM data.
use super::{
Mesh,
math::{Basis, Camera, Ray, Vec3},
};
#[derive(Debug, Clone, Copy)]
/// 变换后网格的世界空间 AABB,做保守筛选而不是精确表面描述。
pub struct Bounds {
pub min: Vec3,
pub max: Vec3,
}
impl Bounds {
/// 变换网格顶点并求世界 AABB,作为空间索引的保守外包围。
fn mesh(mesh: &Mesh) -> Option<Self> {
let mut points = mesh
.geometry
.vertices
.iter()
.map(|point| mesh.world.point(*point));
// 空网格没有包围盒,返回 None 后不会进入树;首顶点初始化上下界,避免把原点误算进边界。
let first = points.next()?;
Some(points.fold(
Self {
min: first,
max: first,
},
|bounds, point| Self {
min: Vec3::new(
bounds.min.x.min(point.x),
bounds.min.y.min(point.y),
bounds.min.z.min(point.z),
),
max: Vec3::new(
bounds.max.x.max(point.x),
bounds.max.y.max(point.y),
bounds.max.z.max(point.z),
),
},
))
}
/// 求两个包围盒的最小共同外包围,供 BVH 父节点使用。
fn union(self, other: Self) -> Self {
Self {
min: Vec3::new(
self.min.x.min(other.min.x),
self.min.y.min(other.min.y),
self.min.z.min(other.min.z),
),
max: Vec3::new(
self.max.x.max(other.max.x),
self.max.y.max(other.max.y),
self.max.z.max(other.max.z),
),
}
}
/// 取指定轴中心位置,供最长轴排序和分割使用。
fn center(self, axis: usize) -> f32 {
let min = [self.min.x, self.min.y, self.min.z];
let max = [self.max.x, self.max.y, self.max.z];
(min[axis] + max[axis]) / 2.
}
/// 用包围盒筛出射线候选;索引查询按进入距离排序,精确三角形求交在调用方完成。
fn ray(self, ray: Ray) -> Option<f32> {
// slab 算法逐轴交区间;near 从零开始,只保留射线前方,起点在盒内时返回零。
let mut near = 0f32;
let mut far = f32::INFINITY;
for (origin, direction, min, max) in [
(ray.origin.x, ray.direction.x, self.min.x, self.max.x),
(ray.origin.y, ray.direction.y, self.min.y, self.max.y),
(ray.origin.z, ray.direction.z, self.min.z, self.max.z),
] {
// 近乎平行该轴时不做除法;起点在 slab 外即可拒绝,在 slab 内则该轴不收窄距离。
if direction.abs() < 1e-12 {
if origin < min || origin > max {
return None;
}
continue;
}
let a = (min - origin) / direction;
let b = (max - origin) / direction;
// 方向可为负,先排序该轴进入/退出距离,再与之前各轴区间求交。
near = near.max(a.min(b));
far = far.min(a.max(b));
if far < near {
return None;
}
}
Some(near)
}
/// 检查八角是否全落在任一视锥平面外侧;返回保守可见性,不代表真实三角形可见。
fn visible(self, camera: Camera, basis: Basis, planes: &[[f32; 4]; 6]) -> bool {
// 用三位索引枚举 AABB 八角,再转到相机空间,使角点与视锥平面处于同一坐标系。
let corners: [Vec3; 8] = std::array::from_fn(|index| {
camera.view(
basis,
Vec3::new(
if index & 1 == 0 {
self.min.x
} else {
self.max.x
},
if index & 2 == 0 {
self.min.y
} else {
self.max.y
},
if index & 4 == 0 {
self.min.z
} else {
self.max.z
},
),
)
});
// 只有某个平面外侧包含全部八角时才剔除;盒与视锥可能无真实相交也会保守保留。
!planes.iter().any(|[a, b, c, d]| {
corners
.iter()
.all(|point| a * point.x + b * point.y + c * point.z + d < 0.)
})
}
}
#[derive(Debug)]
/// BVH 叶或分支;叶保存实例索引,分支保存两个子节点索引。
enum Node {
Leaf {
bounds: Bounds,
items: Vec<usize>,
},
Branch {
bounds: Bounds,
left: usize,
right: usize,
},
}
impl Node {
/// 读取叶/分支统一的包围盒,隐藏节点种类差异。
fn bounds(&self) -> Bounds {
match self {
Self::Leaf { bounds, .. } | Self::Branch { bounds, .. } => *bounds,
}
}
}
#[derive(Debug, Default)]
/// 由不可变场景建立的包围盒树,供相机视锥和射线查询复用。
pub struct Index {
bounds: Vec<Option<Bounds>>,
nodes: Vec<Node>,
root: Option<usize>,
}
impl Index {
/// 用变换后的网格顶点建立世界 AABB 与 BVH;忽略空网格,树内仅保存原生几何下标。
pub fn build(meshes: &[Mesh]) -> Self {
// 保留与原网格下标等长的包围盒数组;items 仅收集非空网格,最终候选仍指向原场景下标。
let bounds: Vec<_> = meshes.iter().map(Bounds::mesh).collect();
let mut items: Vec<_> = bounds
.iter()
.enumerate()
.filter_map(|(index, bounds)| bounds.map(|_| index))
.collect();
let mut index = Self {
bounds,
nodes: Vec::new(),
root: None,
};
if !items.is_empty() {
index.root = Some(index.split(&mut items));
}
index
}
/// 按包围盒最长轴的中心排序并递归二分,叶节点保留少量网格候选。
fn split(&mut self, items: &mut [usize]) -> usize {
let bounds = items
.iter()
.map(|&index| self.bounds[index].unwrap())
.reduce(Bounds::union)
.unwrap();
// 先占位父节点,递归子树会扩展 nodes;返回后按稳定下标替换父占位,不保留可失效的引用。
let index = self.nodes.len();
self.nodes.push(Node::Leaf {
bounds,
items: Vec::new(),
});
// 至多四项成为叶;更大集合沿包围盒最长轴排序后二等分,控制树深而非按三角形拆分。
if items.len() <= 4 {
self.nodes[index] = Node::Leaf {
bounds,
items: items.to_vec(),
};
} else {
let extent = bounds.max - bounds.min;
let axis = if extent.x >= extent.y && extent.x >= extent.z {
0
} else if extent.y >= extent.z {
1
} else {
2
};
items.sort_unstable_by(|&a, &b| {
self.bounds[a]
.unwrap()
.center(axis)
.total_cmp(&self.bounds[b].unwrap().center(axis))
});
// 两半均非空,即使所有中心重合也能递减集合规模,保证递归终止。
let mid = items.len() / 2;
let (left, right) = items.split_at_mut(mid);
let left = self.split(left);
let right = self.split(right);
self.nodes[index] = Node::Branch {
bounds,
left,
right,
};
}
index
}
/// 检查索引覆盖的网格数量是否与场景相符;缺失缓存时允许重建。
// 这里只校验数量,不验证顶点或 world 变换;调用方必须在几何改变后重建索引。
pub fn matches(&self, meshes: usize) -> bool {
self.bounds.len() == meshes
}
/// 用包围盒筛出射线候选;索引查询按进入距离排序,精确三角形求交在调用方完成。
pub fn ray(&self, ray: Ray) -> Vec<(f32, usize)> {
// 空树的栈为空;显式栈避免查询递归,先测分支包围盒,未命中时整棵子树跳过。
let mut stack: Vec<_> = self.root.into_iter().collect();
let mut output = Vec::new();
while let Some(index) = stack.pop() {
if self.nodes[index].bounds().ray(ray).is_none() {
continue;
}
match &self.nodes[index] {
Node::Leaf { items, .. } => {
for &item in items {
if let Some(distance) = self.bounds[item].unwrap().ray(ray) {
output.push((distance, item));
}
}
}
Node::Branch { left, right, .. } => {
stack.push(*left);
stack.push(*right);
}
}
}
// 距离是盒进入距离而非三角形命中距离;同距按场景下标排序,精确拾取仍由调用方完成。
output.sort_unstable_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
output
}
/// 用相机六平面保守判断包围盒可见性,返回保持原场景顺序的候选。
pub fn visible(&self, camera: Camera, aspect: f32) -> Vec<usize> {
// 相机无法建立有效正交基时没有可靠视锥,返回空候选,不继续使用无效投影。
let Some(basis) = camera.basis() else {
return Vec::new();
};
let planes = super::frustum_planes(camera, aspect);
let mut stack: Vec<_> = self.root.into_iter().collect();
let mut output = Vec::new();
while let Some(index) = stack.pop() {
if !self.nodes[index].bounds().visible(camera, basis, &planes) {
continue;
}
match &self.nodes[index] {
Node::Leaf { items, .. } => output.extend(
items
.iter()
.copied()
.filter(|item| self.bounds[*item].unwrap().visible(camera, basis, &planes)),
),
Node::Branch { left, right, .. } => {
stack.push(*left);
stack.push(*right);
}
}
}
// BVH 遍历顺序与场景顺序不同;恢复原下标顺序以保持后续绘制及同深度处理稳定。
output.sort_unstable();
output
}
}