【计算机图形学】3D 模型的内部世界:从 OBJ 文件到验证框架

Wednesday, May 13, 2026 | 2 minute read | Updated at Wednesday, May 13, 2026

@

打开一个 .obj 文件,里面到底有什么?顶点坐标、面片索引、法线方向、UV 坐标——这些数据结构是理解一切 3D 验证的基础。今天我们从"文件里存了什么"出发,建立起 3D 验证的四维度框架。

为什么需要这个?

你用 Meshy 生成过潮玩,用虚幻引擎做过小游戏,但你可能从没想过:一个 3D 文件内部到底长什么样?为什么有些模型能 3D 打印,有些不行?为什么渲染出来有奇怪的黑面?

这些问题的答案都藏在 mesh 的底层数据结构里。不理解这些,后面的几何验证、渲染验证都是空中楼阁。

💡 我的起点:知道穿模和贴图概念,但不清楚 3D 文件内部存储了什么。

核心概念

OBJ 文件:最直观的 3D 格式

OBJ 是最简单的 3D 文件格式,纯文本,可以直接用记事本打开。一个典型的 .obj 文件长这样:

text
# 顶点坐标
v 0.0 0.0 0.0
v 1.0 0.0 0.0
v 0.0 1.0 0.0
# 法线
vn 0.0 0.0 1.0
# 面片(顶点索引//法线索引)
f 1//1 2//1 3//1

v 是顶点(vertex),vn 是法线(normal),f 是面片(face)。面片中的数字是索引,指向前面定义的顶点和法线。

🧭 引导过程:老师问"如果让你存一个三角形,最笨的办法是什么?“我说存三个点的坐标。老师追问"那两个三角形共享一条边呢?存两次?"——我推导出"只存一次所有顶点,然后通过索引引用”。

这种"顶点数组 + 索引数组"的结构是 mesh 的核心:

python
import trimesh

mesh = trimesh.load('samples/cube.obj')
print(f"顶点: {mesh.vertices.shape}")  # vertices: [N, 3]
print(f"面片: {mesh.faces.shape}")      # faces: [M, 3]
print(f"边: {mesh.edges.shape}")        # edges: [E, 2]

顶点是 [N, 3] 的数组,每行是 (x, y, z) 坐标。面片是 [M, 3] 的数组,每行是三个顶点的索引。这种结构在所有 3D 格式中通用——.obj、.gltf、.ply 只是序列化方式不同,底层都是顶点 + 索引。

法线:定义"哪面朝外"

法线(Normal)是一个单位向量,垂直于面片表面,指向"外侧"。它决定了光照如何计算——法线朝向光源的面是亮的,背向的是暗的。

面法线可以通过三角形两条边的叉积计算:

$$ \vec{n} = \frac{(\vec{v_1} - \vec{v_0}) \times (\vec{v_2} - \vec{v_0})}{|(\vec{v_1} - \vec{v_0}) \times (\vec{v_2} - \vec{v_0})|} $$

💡 我当时不确定法向量如何定义内外,以为依赖光线。后来理解到法线方向是数学约定——通过叉积的右手定则确定,不依赖任何外部参照。

顶点法线 vs 面法线:一个顶点通常被多个面片共享。顶点法线是周围面法线的均值,用于平滑渲染(Phong shading)。

🧭 引导过程:老师问"一个顶点属于几个面?“我说三个。“那这个顶点有几条法线?"——我推导出"一个顶点属于几个面,就有几条法线”,这就是顶点拆分(Split Vertex):为了给不同面片分配不同的法线,GPU 实际上会把共享顶点拆成多个。

python
# 面法线
print(f"面法线: {mesh.face_normals.shape}")  # face_normals: [M, 3]

# 顶点法线(周围面法线的均值)
print(f"顶点法线: {mesh.vertex_normals.shape}")  # vertex_normals: [N, 3]

水密性与流形性:拓扑的两道关卡

水密性(Watertightness) 是几何概念:mesh 没有洞,每条边恰好被两个面片共享。水密的 mesh 可以计算体积、可以 3D 打印、可以做物理仿真。

$$ \forall e \in E, \quad |\{f \in F : e \in f\}| = 2 $$

流形性(Manifoldness) 是拓扑概念:mesh 的局部拓扑等价于一个圆盘(边界上的点)或半球(内部的点)。非流形的 mesh 有"奇怪"的连接关系,比如一条边被三个面片共享,或者多个面片只在一点相连。

🤔 我一开始以为水密和流形是同一个东西,后来理解到水密是"有没有洞”(几何),流形是"连接关系是否正常"(拓扑)。一个 mesh 可以水密但非流形(如两个立方体只在一点相连),也可以流形但非水密(如一个有边界的平面)。

拓扑洞是水密性的反面:如果一条边只被一个面片拥有,它就是边界边,mesh 在这里有"洞"。

python
# 水密性检查
print(f"水密: {mesh.is_watertight}")
print(f"边界边数: {len(mesh.edges_boundary)}")

# Euler 特征数:V - E + F = 2(封闭曲面)
euler = len(mesh.vertices) - len(mesh.edges) + len(mesh.faces)
print(f"Euler 特征数: {euler}")

UV 坐标:给表面贴图

UV 坐标是 mesh 表面点到 2D 纹理图的映射。U 和 V 是 2D 纹理空间的两个轴(对应 S 和 T),范围通常是 $[0, 1]$。

🧭 引导过程:老师问"怎么把一张照片贴到 3D 模型上?“我说对应位置。“怎么对应?"——我理解到 UV 就是 mesh 表面点寻找其对应位置贴图的坐标索引。

UV 展开(UV unwrapping)是把 3D 表面"摊平"到 2D 的过程。好的 UV 展开减少拉伸和接缝,差的 UV 导致纹理变形。

python
# UV 坐标(如果有的话)
if mesh.visual.uv is not None:
    print(f"UV: {mesh.visual.uv.shape}")  # uv: [N, 2]

3D 生成的三条技术路线

当前 3D 生成有三条主流路线:

  1. 多视角重建(如 NeRF → mesh 提取):从多个 2D 图像推断 3D 结构。优点是灵活,缺点是接缝处容易有裂缝
  2. 单图/文本直接生成(如 TripoSR、Stable Fast 3D):端到端从一张图或文字描述生成 mesh。速度快,但几何质量不稳定
  3. 程序化生成(如 Blender 脚本、Houdini):用规则和参数生成。质量可控,但需要人工设计

💡 我用过 Meshy 做潮玩,它属于文本/图片直接生成路线。之前只关注"好不好看”,现在理解了底层可能的几何质量问题。

3D 验证的四维度框架

综合今天学的内容,3D 验证可以归纳为四个维度:

维度检查内容典型问题
几何正确性水密性、自相交、穿插模型有洞、两个部件互相穿透
拓扑正确性流形性、法线一致性非流形连接、法线朝内
视觉正确性渲染结果、纹理映射黑面、纹理拉伸、光照异常
语义正确性结构合理性、比例手指数量错误、头身比失调

今天学的水密性、流形性、法线属于前两个维度。后面的课程会覆盖视觉和语义维度。

常见误区

  • “文件格式不同就是数据结构不同”:.obj、.gltf、.ply 底层都是顶点+索引,只是序列化方式不同
  • “法线是文件里存的所以可以信任”:AI 生成的 mesh 法线经常不一致,需要后处理修复
  • “水密就是流形”:水密是几何概念(有没有洞),流形是拓扑概念(连接关系是否正常),两者独立
  • “UV 坐标不重要”:UV 错误会导致纹理变形,在视觉验证中是重要检查项

与更广知识体系的联系

今天建立的是基础层:理解 mesh 的数据结构是一切后续验证的前提。

  • 向上:明天学的 BVH 树、穿插检测、法线一致性检查,都是在今天的基础上操作这些数据结构
  • 向下:UV 坐标连接到渲染验证(视觉维度),水密性连接到 3D 打印和物理仿真
  • 框架:四维度框架是整个课程的骨架,后面每个主题都会映射到某个维度

延伸阅读


基于 2026-05-13 学习 session 生成 · 时长 60 分钟(含诊断)

© 2021 - 2026 古月月仔的博客

🌱 Powered by Hugo with theme Dream.