几何数据基础——Triangle / Point / Ray / Intersection / AABB
🎯 第二课:几何数据基础——Triangle / Point / Ray / Intersection / AABB
为什么排第二?
第一课的 ObjectPool 是”货架”,这一课的几何数据就是”货架上的货物”。3D 打印分析的一切算法——光线追踪、曲率计算、聚类分类——都在操作这些东西。你如果连操作对象是什么都不清楚,上层算法就是空中楼阁。
用你自己的话说:”理解业务域的核心实体,这是’定义问题’的能力”(理念 1.2)。这四个结构就是 Huhb3D 业务的原子。
费曼学习法 Step 1:用大白话解释它们是什么
想象你是一个质检员,面前有一块 3D 打印的零件:
Point:零件表面上的一个点——有 x, y, z 三个坐标
Triangle:零件表面是由无数小三角面拼成的,每个三角形就是一小块”皮”
Ray:你拿一束激光照过去,这束激光就是射线——有起点、有方向
Intersection:激光打在零件表面上的那个点——记录了”打在哪”、”打在哪个三角形上”、”距离多远”
AABB:把零件装进一个方盒子,盒子的六个面分别平行于坐标轴——这就是轴对齐包围盒
一句话关系链:
零件表面 = N 个 Triangle
Triangle = 3 个 Point + 1 个法线
Ray 打上去 → 命中 Intersection(在哪、打谁、多远)
N 个 Triangle 塞进 AABB → 给 BVH 做快速排除
费曼学习法 Step 2:逐个拆解核心知识点
📦 A. Point — 3D 世界的一个位置
struct Point {
float x, y, z;
};
看似简单,但有几个关键认知:
A1. 为什么用 float 而不是 double?
float (32位) double (64位)
精度 约7位有效数字 约15位有效数字
内存 4字节 8字节
运算速度 SIMD可同时算4个 SIMD同时算2个
3D打印零件通常尺寸在毫米级,精度要求0.01mm,float 的 7 位有效数字(如 123.4567)完全够用。但选择 float 的真正原因是:一个 Triangle 3个顶点 = 9个float = 36字节,用 double 就是 72 字节——数据量翻倍,BVH遍历的缓存命中率直接砍半。
AI坑点: AI默认写 double,因为”更精确不会错”。但在百万三角形级别的场景下,double 会把 BVH 遍历性能拖慢 30-50%。
A2. 浮点精度陷阱
float a = 0.1f + 0.2f;
// a ≠ 0.3f ! 实际 a ≈ 0.30000001192…
这就是后面”空间哈希编码”为什么要做量化(把连续的 float 离散化到网格)的根本原因——浮点数不能直接比相等,必须量化到整数再比较。
📦 B. Triangle — 3D 世界的最小面片
struct alignas(64) Triangle {
float normal[3]; // 法线方向
float vertex1[3]; // 顶点1
float vertex2[3]; // 顶点2
float vertex3[3]; // 顶点3
float attribute_count; // 属性计数
// padding 填充到 64 字节
};
B1. 为什么 alignas(64)?
第一课学过——缓存行对齐。一个 Triangle 正好 64 字节(13 个 float = 52 字节 + 12 字节 padding = 64 字节),独占一个缓存行。
当光线追踪遍历时,CPU 一次读取 64 字节 = 恰好一个完整的 Triangle,不会有半个三角形跨两行缓存的浪费。这是”数据结构设计为硬件服务”的典型思维。
B2. 法线为什么要单独存?
法线可以通过叉积算出来:normal = normalize(e1 × e2),为什么还要额外存?
场景 重新算 直接读
STL 文件本身就带法线 需要额外运算 零开销
曲率计算需要频繁访问法线 每次叉积+归一化 一次内存读取
法线可能和几何法线不同 — 存的是”真实法线”
STL 格式本身带法线,直接读进来存着,后面用到的时候不需要重复计算。这是**”空间换时间”**的经典决策。
B3. 为什么用 float[3] 而不是 Point vertex1?
因为 Triangle 和 Point 定义在不同头文件,而 Triangle 的布局需要严格可控——用裸数组保证内存布局是连续的 3 个 float,不会有编译器插入的 padding。在序列化(STL 文件读写)和 SIMD 运算中,内存布局的可预测性至关重要。
AI坑点: AI 经常把 Triangle 写成 Point v1, v2, v3,看起来更”面向对象”,但破坏了内存布局的可控性,也无法直接 memcpy 从 STL 二进制数据中读取。
📦 C. Ray — 一束有方向的激光
struct Ray {
Point origin; // 起点
Point direction; // 方向(单位向量)
};
C1. 方向向量为什么要归一化?
如果方向向量长度不是1,那么 t * direction 算出来的距离 t 就不是真实距离。光线追踪中,交点距离 t 直接用于判断”谁最近”,方向不归一化会导致遮挡判断错误。
C2. 射线的数学表达
射线 = 一个参数方程:
P(t) = origin + t × direction (t ≥ 0)
t < 0:交点在射线反方向,无意义
t = 0:交点就是起点
t > 0:沿射线方向的交点
这个 t 就是 Intersection 里的 distance。 理解这一点,后面学 Möller-Trumbore 光线追踪算法就只需要解一个方程。
📦 D. Intersection — 命中结果
struct Intersection {
Point point; // 命中位置
const Triangle* tri; // 命中的三角形指针
float distance; // 射线起点到命中点的距离
};
D1. 为什么存 Triangle* 而不是 int index?
存指针 → 后续需要法线、顶点时直接 tri->normal 一步到位
存索引 → 需要 triangles[index] 额外一次间接寻址
在光线追踪中,命中后立刻需要三角形的法线和顶点来做曲率计算,所以直接存指针省掉一次间接寻址。
但这也引入了一个风险: 指针在 ObjectPool 回收后会悬空。所以 ObjectPool 的设计中,三角形一旦分配就不会被回收——这是架构层面的契约,不是代码层面能强制保证的。
AI坑点: AI 不会意识到这个架构契约。它可能建议”用 shared_ptr 管理三角形生命周期”,这在百万级对象中会导致巨大的引用计数开销。
D2. distance 的双重用途
判断哪个交点最近(t 最小的那个)
在曲率计算中,distance 就是”射线走过多少距离碰到对面”,这个距离 = 局部壁厚
📦 E. AABB (Bounds) — 万能的粗筛盒子
struct Bounds {
float min[3]; // 盒子的最小角点 (xmin, ymin, zmin)
float max[3]; // 盒子的最大角点 (xmax, ymax, zmax)
};
E1. 为什么 AABB 是 BVH 的基石?
BVH 树的每个节点都存一个 AABB。遍历时:
射线 vs AABB:用 Slab Method,O(1) 判断”有没有可能命中”
→ 不可能:直接跳过整棵子树(剪枝)
→ 可能:进入子节点继续判断
AABB 就是”粗筛”——用极低的成本排除大量不可能命中的区域。没有 AABB,光线追踪就要跟每个三角形都算一次,O(N) 变 O(log N)。
E2. AABB 的关键操作
操作 作用 复杂度
expand(point) 把点纳入盒子,更新 min/max O(1)
surface_area() 计算表面积——SAH 构建要用 O(1)
center() 盒子中心——BVH 按质心排序 O(1)
intersect(ray) Slab Method 射线求交 O(1)
全都是 O(1)!这就是 AABB 的威力——所有操作都是常数时间。
E3. surface_area() 的公式与直觉
表面积 = 2 × (宽×高 + 宽×深 + 高×深)
= 2 × ((max.x-min.x)(max.y-min.y) + (max.x-min.x)(max.z-min.z) + (max.y-min.y)(max.z-min.z))
为什么要算表面积?下一课 BVH 会讲——SAH(表面积启发式)用表面积估算”射线命中这个盒子的概率”。表面积越大,越容易被射线碰到,越值得进一步细分。
费曼学习法 Step 3:五个结构的关系全景图
┌─────────────────────────────────────────────────────┐
│ STL 文件 │
│ Triangle × N (normal + v1 + v2 + v3, alignas(64)) │
│ │ │
│ ┌────────────┼────────────┐ │
│ ▼ ▼ ▼ │
│ Point Point Point │
│ (v1) (v2) (v3) │
│ │ │ │ │
│ └────────────┼────────────┘ │
│ ▼ │
│ Bounds (AABB) │
│ min[3] ← expand(v1,v2,v3) → max[3] │
│ │ │
│ ▼ │
│ BVHNode │
│ bounds + left + right │
│ │ │
│ ┌──────────┴──────────┐ │
│ ▼ ▼ │
│ Ray ────────→ Intersection │
│ origin + dir point + tri* + dist │
└─────────────────────────────────────────────────────┘
数据流: STL → Triangle → AABB → BVHNode → Ray+BVH → Intersection → 曲率/壁厚
费曼学习法 Step 4:识别你的知识缺口
# 自检问题 如果你答不出
1 一个 Triangle 为什么是 64 字节?13个float只有52字节,剩下12字节去哪了? 重新理解 alignas + padding
2 float a = 0.1f; float b = 0.10000001f; 它们”相等”吗?怎么判断? 重新理解浮点精度 → 引出第3课空间哈希
3 射线 P(t) = O + tD,如果方向 D 不是单位向量,t=2 代表什么? 重新理解归一化的意义
4 为什么 AABB 的所有操作都是 O(1)?一个包围盒要判断”射线是否命中”,最少需要几次比较? 去 Slab Method(第5课预告)
5 Intersection 存 Triangle* 而非 int index,有什么隐患?什么场景下会崩? 重新理解指针 vs 索引的架构取舍
6 为什么法线要单独存而不是每次重新算?这体现了什么设计原则? “空间换时间” + 业务域知识
费曼学习法 Step 5:用你的话复述
试着这样解释:
“Huhb3D 分析的是 3D 打印零件,零件表面由几十万个三角形(Triangle)拼成。每个三角形占 64 字节——刚好一个缓存行,CPU 一次就能读完。射线(Ray)从表面射出去找对面的壁,命中后记录交点(Intersection)——位置、打在哪个三角形上、距离多远。距离就是壁厚。几十万个三角形不可能逐个判断,所以先把它们装进轴对齐包围盒(AABB),用 O(1) 的粗筛排除掉不可能命中的区域——这就是 BVH 的基础。所有这些数据结构的设计,都不是’面向对象好看’,而是’为硬件服务和为算法服务’。”
📌 本课与你的 AI 时代理念的映射
你的理念 本课体现
1.1 当出题人 Triangle/Ray/Intersection 不是教科书概念,而是”3D打印质量检测”这个业务问题的数据抽象——定义问题的人决定数据结构
1.2 技术是切入点 float[3] vs Point、Triangle* vs int index——技术决策背后是架构思维和业务理解
1.5 AI代码有坑 AI 默认用 double、用 Point 封装顶点、用 shared_ptr——每个都在真实场景下有性能或正确性问题
1.4 架构能力 Intersection 存指针而不是索引,这是”数据流向决定数据结构”的架构决策,而不是单纯的代码选择


