打开一个 .obj 文件,里面到底有什么?顶点坐标、面片索引、法线方向、UV 坐标——这些数据结构是理解一切 3D 验证的基础。今天我们从"文件里存了什么"出发,建立起 3D 验证的四维度框架。
为什么需要这个?
你用 Meshy 生成过潮玩,用虚幻引擎做过小游戏,但你可能从没想过:一个 3D 文件内部到底长什么样?为什么有些模型能 3D 打印,有些不行?为什么渲染出来有奇怪的黑面?
这些问题的答案都藏在 mesh 的底层数据结构里。不理解这些,后面的几何验证、渲染验证都是空中楼阁。
💡 我的起点:知道穿模和贴图概念,但不清楚 3D 文件内部存储了什么。
核心概念
OBJ 文件:最直观的 3D 格式
OBJ 是最简单的 3D 文件格式,纯文本,可以直接用记事本打开。一个典型的 .obj 文件长这样:
# 顶点坐标
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//1v 是顶点(vertex),vn 是法线(normal),f 是面片(face)。面片中的数字是索引,指向前面定义的顶点和法线。
🧭 引导过程:老师问"如果让你存一个三角形,最笨的办法是什么?“我说存三个点的坐标。老师追问"那两个三角形共享一条边呢?存两次?"——我推导出"只存一次所有顶点,然后通过索引引用”。
这种"顶点数组 + 索引数组"的结构是 mesh 的核心:
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 实际上会把共享顶点拆成多个。
# 面法线
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 在这里有"洞"。
# 水密性检查
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 导致纹理变形。
# UV 坐标(如果有的话)
if mesh.visual.uv is not None:
print(f"UV: {mesh.visual.uv.shape}") # uv: [N, 2]3D 生成的三条技术路线
当前 3D 生成有三条主流路线:
- 多视角重建(如 NeRF → mesh 提取):从多个 2D 图像推断 3D 结构。优点是灵活,缺点是接缝处容易有裂缝
- 单图/文本直接生成(如 TripoSR、Stable Fast 3D):端到端从一张图或文字描述生成 mesh。速度快,但几何质量不稳定
- 程序化生成(如 Blender 脚本、Houdini):用规则和参数生成。质量可控,但需要人工设计
💡 我用过 Meshy 做潮玩,它属于文本/图片直接生成路线。之前只关注"好不好看”,现在理解了底层可能的几何质量问题。
3D 验证的四维度框架
综合今天学的内容,3D 验证可以归纳为四个维度:
| 维度 | 检查内容 | 典型问题 |
|---|---|---|
| 几何正确性 | 水密性、自相交、穿插 | 模型有洞、两个部件互相穿透 |
| 拓扑正确性 | 流形性、法线一致性 | 非流形连接、法线朝内 |
| 视觉正确性 | 渲染结果、纹理映射 | 黑面、纹理拉伸、光照异常 |
| 语义正确性 | 结构合理性、比例 | 手指数量错误、头身比失调 |
今天学的水密性、流形性、法线属于前两个维度。后面的课程会覆盖视觉和语义维度。
常见误区
- “文件格式不同就是数据结构不同”:.obj、.gltf、.ply 底层都是顶点+索引,只是序列化方式不同
- “法线是文件里存的所以可以信任”:AI 生成的 mesh 法线经常不一致,需要后处理修复
- “水密就是流形”:水密是几何概念(有没有洞),流形是拓扑概念(连接关系是否正常),两者独立
- “UV 坐标不重要”:UV 错误会导致纹理变形,在视觉验证中是重要检查项
与更广知识体系的联系
今天建立的是基础层:理解 mesh 的数据结构是一切后续验证的前提。
- 向上:明天学的 BVH 树、穿插检测、法线一致性检查,都是在今天的基础上操作这些数据结构
- 向下:UV 坐标连接到渲染验证(视觉维度),水密性连接到 3D 打印和物理仿真
- 框架:四维度框架是整个课程的骨架,后面每个主题都会映射到某个维度
延伸阅读
- OBJ 文件格式规范 — Wikipedia — OBJ 格式的完整字段说明
- trimesh 快速入门 — 从加载到操作 mesh 的完整教程
- Half-edge 数据结构 — Wikipedia — 更高效的 mesh 拓扑表示,工业级 3D 软件常用
- Real-Time Rendering, Chapter 12 — Geometry Representation — mesh 表示的权威参考
- Neural 3D Generation Survey (arXiv 2024) — 当前 3D 生成技术的综述
基于 2026-05-13 学习 session 生成 · 时长 60 分钟(含诊断)