2. 网格数据结构:顶点、边、面的拓扑关系与存储格式(OBJ、PLY)
好,咱们进入第二章。上一章我们聊了什么是复杂几何体,以及为什么需要优化。这一章,我们来扒一扒网格的“骨架”——数据结构。
说白了,一个3D模型在计算机眼里,不是什么光滑的曲面,而是一堆点、线、面的集合。你想想看,游戏里的角色、电影里的特效,底层全是这些玩意儿。我早年刚入行时,总觉得只要模型看着漂亮就行,直到有一次做模型简化,数据量一大,程序直接崩了……嗯,从那以后,我才老老实实回来啃数据结构。
2.1 网格的三大基本元素
任何一个多边形网格,都跑不出这三个东西:顶点(Vertex)、边(Edge)、面(Face)。它们之间的拓扑关系,决定了网格的“连接方式”。
| 元素 | 定义 | 我常说的“人话” |
|---|---|---|
| 顶点 | 三维空间中的一个点,用 (x, y, z) 坐标表示。 | 就是模型的“关节”位置。 |
| 边 | 连接两个顶点的线段。 | 模型的“骨架线”。 |
| 面 | 由三条或更多条边围成的封闭多边形。最常见的是三角形和四边形。 | 模型的“皮肤”,渲染时就是靠它。 |
核心要点: 顶点只存位置,不存连接关系。连接关系由边和面来定义。这就是“拓扑”的由来——它描述的是“谁和谁连在一起”,而不是“它们长什么样”。
2.2 拓扑关系:三种常见的存储方式
不同的应用场景,对拓扑关系的存储要求不一样。我个人习惯把它们分成三类:
2.2.1 面列表(Face-Vertex Mesh)
这是最直观、最省内存的方式。每个面直接存它包含的顶点索引。比如一个三角形面,存三个顶点编号。
- 优点: 结构简单,存储紧凑,适合静态模型。
- 缺点: 想找一条边连接了哪两个面?对不起,你得遍历所有面。效率很低。
- 我在项目中遇到过: 做模型预览工具时,用面列表就够了。但一旦要做布尔运算或细分,立马卡死。
2.2.2 翼边结构(Winged-Edge Mesh)
这是经典的数据结构,每条边都存了它相邻的两个面、两个顶点,以及前后左右的边。信息非常全。
- 优点: 查询效率极高,适合做复杂的几何操作。
- 缺点: 内存开销大,实现复杂。
- 我曾经…… 在实现一个半边结构(Half-Edge)时,被指针绕晕了三天。后来发现,其实翼边结构就是半边结构的前身,理解它,半边就简单了。
2.2.3 半边结构(Half-Edge Mesh)
这是目前工业界最常用的结构。它将每条边拆成两条方向相反的“半边”,每条半边只属于一个面。这样,遍历邻接面、邻接边都非常快。
- 优点: 查询效率高,内存比翼边结构更优,支持流形网格(Manifold Mesh)。
- 缺点: 对非流形网格(比如多条边共享一个顶点)支持不好。
我的建议: 如果你只是做模型展示,用面列表就够了。如果你要做编辑、细分、简化,直接上半边结构。别在翼边上浪费时间,除非你在写教科书。
2.3 存储格式:OBJ 与 PLY
说完了内存里的结构,咱们聊聊磁盘上的格式。你拿到一个模型文件,最常见的就是 OBJ 和 PLY。它们俩,我几乎每天都在打交道。
2.3.1 OBJ 格式
OBJ 是 Wavefront 公司发明的,纯文本格式。结构非常清晰,就是一行一行的指令。
v:顶点坐标 (x y z)vt:纹理坐标 (u v)vn:法线向量 (nx ny nz)f:面,后面跟顶点/纹理/法线的索引组合
举个例子,一个简单的三角形:
v 0.0 0.0 0.0
v 1.0 0.0 0.0
v 0.0 1.0 0.0
f 1 2 3
这里 f 1 2 3 表示用第1、2、3个顶点构成一个三角形面。
注意: OBJ 的索引是从 1 开始的,不是 0!我当年第一次写解析器时,忘了减1,结果模型全乱套了。这个坑,你大概率也会踩一次。
2.3.2 PLY 格式
PLY(Polygon File Format)是斯坦福大学发明的。它比 OBJ 更灵活,支持二进制和文本两种模式。结构分两部分:头部(Header)和数据体(Data)。
头部用 element 关键字定义有多少个顶点、多少个面,以及每个元素有哪些属性。
ply
format ascii 1.0
element vertex 3
property float x
property float y
property float z
element face 1
property list uchar int vertex_indices
end_header
0.0 0.0 0.0
1.0 0.0 0.0
0.0 1.0 0.0
3 0 1 2
你看,property list uchar int vertex_indices 表示面的顶点索引是一个列表:第一个数(uchar)表示有几个顶点,后面跟着索引(int)。
2.4 OBJ vs PLY:怎么选?
| 对比维度 | OBJ | PLY |
|---|---|---|
| 可读性 | 非常好,纯文本,人眼能看懂 | 头部清晰,数据体略复杂 |
| 文件大小 | 较大(纯文本) | 二进制模式更小,文本模式与OBJ相当 |
| 支持属性 | 顶点、纹理、法线、面 | 可自定义属性(颜色、强度等) |
| 索引起始 | 从1开始 | 从0开始 |
| 我的选择 | 调试、小模型、教学 | 生产环境、大模型、需要额外属性时 |
一句话总结: 想快速看模型,用 OBJ。想高效存模型,用 PLY 二进制。想折磨自己,用 OBJ 存几百万个顶点。
2.5 知识结构图:网格数据与存储格式
下面这张图,帮你把本章的核心逻辑串起来。从几何体到顶点、边、面,再到拓扑结构和文件格式,一目了然。
2.6 实战建议:从文件到内存
最后,给你一个我自己的“套路”。当你拿到一个模型文件时,怎么把它变成内存里的数据结构?
- 读文件头: 判断是 OBJ 还是 PLY,是文本还是二进制。
- 解析顶点: 把所有
v或vertex读进来,存到一个数组里。 - 解析面: 把
f或face读进来,注意索引偏移(OBJ 要减1)。 - 构建拓扑: 如果你只需要展示,面列表就够了。如果你要做编辑,用半边结构重建邻接关系。
- 验证: 检查是否有重复顶点、退化面(面积为零的三角形)、非流形边。这一步,我建议你写个单元测试,省得后面调试到崩溃。
一个小技巧: 解析 OBJ 时,如果遇到 f v1/vt1/vn1 这种格式,说明每个顶点还带了纹理和法线索引。这时候,你需要为每个“顶点+纹理+法线”的组合创建一个新的顶点,这叫“顶点分裂”。不然渲染出来纹理是花的。