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 知识结构图:网格数据与存储格式

下面这张图,帮你把本章的核心逻辑串起来。从几何体到顶点、边、面,再到拓扑结构和文件格式,一目了然。

网格数据结构与存储格式知识结构图 网格数据结构与存储格式 复杂几何体 顶点 (Vertex) 边 (Edge) 面 (Face) 面列表 (Face-Vertex) 翼边结构 (Winged-Edge) 半边结构 (Half-Edge) OBJ 格式 PLY 格式 💡 核心逻辑 几何体 → 三大元素 → 拓扑关系(决定查询效率) → 文件格式(决定存储与交换)

2.6 实战建议:从文件到内存

最后,给你一个我自己的“套路”。当你拿到一个模型文件时,怎么把它变成内存里的数据结构?

  1. 读文件头: 判断是 OBJ 还是 PLY,是文本还是二进制。
  2. 解析顶点: 把所有 v 或 vertex 读进来,存到一个数组里。
  3. 解析面: 把 f 或 face 读进来,注意索引偏移(OBJ 要减1)。
  4. 构建拓扑: 如果你只需要展示,面列表就够了。如果你要做编辑,用半边结构重建邻接关系。
  5. 验证: 检查是否有重复顶点、退化面(面积为零的三角形)、非流形边。这一步,我建议你写个单元测试,省得后面调试到崩溃。

一个小技巧: 解析 OBJ 时,如果遇到 f v1/vt1/vn1 这种格式,说明每个顶点还带了纹理和法线索引。这时候,你需要为每个“顶点+纹理+法线”的组合创建一个新的顶点,这叫“顶点分裂”。不然渲染出来纹理是花的。