09-模型与网格:OBJ/GLTF(简化版)加载、法线、切线¶
目录页:见 OpenGL 教程目录
目标¶
- 从“手写顶点数组”升级到“加载模型文件并渲染”
- 建立 Mesh 的最小工程抽象:顶点格式、索引、材质引用、GPU 资源生命周期
- 搞清楚法线/切线存在的意义:它们决定光照与法线贴图怎么工作
先决条件¶
- 已完成 06-纹理:采样、wrap/filter、mipmap、sRGB
- 已理解 08-深度与裁剪:depth test、face culling、clip space 的 3D 基础状态
1. 为什么要抽 Mesh:你不想在 main.cpp 里堆 5000 行¶
当你开始画立方体、画多个物体、加载模型时,代码会迅速变成:
- “一堆顶点数组 + 一堆 glBind + 一堆 draw”
这会让你无法扩展到:
- 多个模型
- 多种顶点格式(有的有 uv,有的有法线)
- 多材质、多贴图
所以本章最重要的交付,不是“加载某个格式”,而是:
- 你有一套稳定的 Mesh 数据结构与上传/绘制流程
2. 顶点格式:你至少要包含什么¶
最小 3D 光照常用的顶点格式:
- position(vec3)
- normal(vec3)
- uv(vec2)
可选但很快会需要:
- tangent(vec4 或 vec3 + sign):法线贴图需要它
你可以先定义一个 CPU 侧结构(概念示意):
对应 GPU attribute layout 的 stride/offset 也就确定了。
3. OBJ vs GLTF:怎么选(主线建议)¶
3.1 OBJ¶
- 优点:格式简单、教程多、适合学习几何与法线
- 缺点:材质/贴图描述能力弱、扩展性差
3.2 GLTF¶
- 优点:现代资产交换格式,材质/贴图/动画支持更完整
- 缺点:解析更复杂(但工具链成熟)
主线建议:
- 先用 OBJ 把“加载 → 顶点/索引 → 上传 → 渲染”的链路跑通
- 再用 GLTF 把“材质与贴图组织方式”变成工程化能力
4. 最小 Mesh 抽象(你要的不是框架,是边界清晰)¶
一个最小 Mesh 至少要表达:
- CPU 数据:
vertices、indices - GPU 资源:
vao/vbo/ebo - 绘制参数:索引数量、拓扑类型
行为上至少有两件事:
upload():CPU → GPUdraw():绑定资源并发起 draw call
当你后面进入多模型/材质时,Mesh 再扩展“材质引用/纹理句柄”即可。
5. 法线:光照计算为什么离不开它¶
没有法线,你只能做“平面着色”或假光照。
你只需要先掌握两句直觉:
- 法线是“表面朝向”,决定光从哪个角度打上来
- 法线必须和位置在同一空间里参与计算(通常在世界空间或视图空间)
后面做光照时,会用到:
- 法线变换(尤其是有非均匀缩放时)
- 在 fragment shader 里使用插值后的法线
6. 切线:法线贴图为什么需要它¶
法线贴图(normal map)存的是“切线空间”的法线,而不是世界空间的法线。
切线空间由三根互相正交的基向量组成:
- tangent(切线,沿着 UV 的 U 方向)
- bitangent(副切线,沿着 UV 的 V 方向)
- normal(法线)
你现在不用马上实现完整 TBN,但要先知道:
- 没有 tangent,你无法把 normal map 的信息正确转换到世界/视图空间
7. 常见坑¶
- 坐标系/单位不一致:模型导入后特别大/特别小/转向奇怪,优先检查导入时的缩放与坐标系约定
- 法线缺失或错误:光照会看起来像“塑料块”或完全黑
- 索引绕序不一致:开了 culling 后会缺面
- 顶点去重策略:一个位置在不同 UV/法线下必须拆成不同顶点,否则会接缝错乱
小练习¶
练习 1:把立方体抽成一个 Mesh 类¶
- 题目:把你之前的立方体顶点/索引从 main.cpp 抽出来,封装 upload/draw
- 目标:让后续章节可持续扩展
练习 2:加载一个 OBJ 并验证法线¶
- 题目:加载一个带法线的 OBJ,并用“法线可视化”方式验证(例如把
normal*0.5+0.5当颜色输出) - 目标:在进入真实光照前先校验数据链路
小结¶
- 本章核心是 Mesh 的工程边界:CPU 数据、GPU 资源、upload/draw 行为
- OBJ 适合先跑通链路,GLTF 适合进入现代资产与材质体系
- 法线决定基础光照,切线决定法线贴图能否成立