📜 序章:承前启后,继往开来
盖图形之学,以模型为骨,以纹理为肤;节点为纲,网格为目。前篇既述节点层级 之遍历,今当深究网格数据之解析。
🌟 观其大略:一节点之下,可藏数枚网格;一网格之内,可含万千顶点。由 ID 而索数据,由数据而生成形,此乃模型加载之核心要义也。
🏛️ 第一章:新增玄功・processMesh 函数出世
1.1 引经据典,头文件先行
欲炼 Mesh 之玄功,必先引其典籍。于 loader 之中,新增静态函数一尊,名曰processMesh,专司网格解析之职。
cpp
// 📚 引入网格之典籍
#include "framework/mesh.h"
1.2 函数初立,雏形初具
函数之形,以aiMesh为饵,以自研Mesh*为果。初立之时,但存其形,未填其实,先返空指针以占位:
cpp
/**
* @brief 解析Assimp原始网格,转化为自研Mesh对象
* @param aiMesh Assimp原始网格指针(含顶点/法线/UV/索引)
* @return Mesh* 自研网格对象指针
*
* 🎯 核心职责:
* ① 提取顶点坐标 positions
* ② 提取法线向量 normals
* ③ 提取纹理坐标 uvs
* ④ 提取面片索引 indices
* ⑤ 组装Geometry + Material → Mesh
*/
static Mesh* processMesh(const aiMesh* aiMesh)
{
// ⏳ 先返空,后续填充玄功
return nullptr;
}
💡 小注 :此函数置于
processNode之后,与节点解析遥相呼应,形成 "节点为纲、网格为目" 之架构。
🌳 第二章:节点遍历之增补・寻 Mesh 之踪
2.1 补全参数,场景为引
前篇processNode之函数,仅传节点而无场景,犹如有舟无楫、有弓无矢。欲寻 Mesh 之真身,必借aiScene以为舟楫。
修改前:孤舟独桨,难济沧海
cpp
Object* processNode(aiNode* aiNode) // ❌ 缺少场景参数
修改后:场景为引,可探深渊
cpp
Object* processNode(aiNode* aiNode, const aiScene* aiScene) // ✅ 增补场景
⚠️ 重要提醒 :
aiScene于顶层获取时即为const类型,故传参亦须标注const,否则类型不匹配,编译必败。此乃 C++ 类型安全之铁律,不可不慎!
2.2 递归调用,层层传递
参数既补,则凡调用processNode之处,皆须传递aiScene:
-
顶层入口调用 → 传全局
aiScene -
递归子节点调用 → 继续向下传递
如此层层传递,场景数据方能通达于每一个节点之中。
2.3 循环遍历,ID 为钥
aiNode之下,有二物焉:
-
🔢
mNumMeshes:当前节点绑定之网格数量 -
🗝️
mMeshes[]:网格 ID 数组(注意:仅存 ID,非真实数据)
cpp
// 🔄 遍历当前节点所有网格ID
for (int i = 0; i < aiNode->mNumMeshes; i++)
{
// 🗝️ 取出第i个网格的ID号(只是编号,非真实网格)
int meshId = aiNode->mMeshes[i];
// 🎯 以ID为钥,于场景中寻得真实aiMesh对象
aiMesh* meshData = aiScene->mMeshes[meshId];
// ⚙️ 调用解析函数,将原始网格转为自研Mesh
Mesh* mesh = processMesh(meshData);
// 📌 挂载到当前节点对应的Object物体下
currentObject->addChild(mesh);
}
2.4 节点挂载之图谱
一个 Object 节点之下,可挂两类子物体,如树之分枝:
Plain
Object(当前节点)
├── 📦 Mesh子物体 × N ← 由当前节点的mMeshes生成
└── 🌿 Object子节点 × M ← 由当前节点的mChildren递归生成
📖 图解:若节点有网格,则挂网格;若有子节点,则生节点。二者并行不悖,共同构成场景层级之全貌。若于此尚有疑惑,可回顾 Assimp 数据结构之章节,温故而知新。
💎 第三章:Mesh 解析之核心・四象数据提取
3.1 四象容器,预先备之
欲炼网格之真身,必先备四象之容器,以储四类数据:
cpp
// 🌊 位置容器:存储所有顶点的x/y/z坐标,紧密排列
std::vector<float> positions;
// 🌟 法线容器:存储所有顶点的法线方向
std::vector<float> normals;
// 🎨 UV容器:存储所有顶点的纹理坐标
std::vector<float> uvs;
// 🔗 索引容器:存储三角面片的顶点索引
std::vector<unsigned int> indices;
3.2 第一象・顶点坐标(Positions)
aiMesh->mVertices:顶点数组,类型为aiVector3D*,长度等于顶点总数。
cpp
// 🔄 遍历每一个顶点
for (unsigned int i = 0; i < aiMesh->mNumVertices; i++)
{
// 📍 取出第i个顶点的位置
aiVector3D pos = aiMesh->mVertices[i];
// 📥 依次推入x、y、z(紧密排列格式)
positions.push_back(pos.x);
positions.push_back(pos.y);
positions.push_back(pos.z);
📊 性能小注:紧密排列(x,y,z,x,y,z...)之格式,与 OpenGL 顶点缓冲之布局天然契合,后续传至 GPU 时无需额外转换,效率极高。
3.3 第二象・法线向量(Normals)
aiMesh->mNormals:法线数组,结构与顶点完全一致。
cpp
// 🌈 取出第i个顶点的法线
aiVector3D nor = aiMesh->mNormals[i];
// 📥 推入法线的x、y、z分量
normals.push_back(nor.x);
normals.push_back(nor.y);
normals.push_back(nor.z);
💡 原理:法线决定光照计算之明暗,无法线则模型扁平无立体感。其数据结构与顶点一一对应,每顶点皆有其法线。
3.4 第三象・纹理坐标(UVs)------ 难点解析
⚠️ 特殊之处:多 UV 集的奥秘
UV 数据与顶点、法线不同,其结构多了一层维度:
cpp
// 顶点 / 法线:一维数组
aiVector3D* mVertices; // 一个数组
aiVector3D* mNormals; // 一个数组
// UV坐标:二维数组(支持多套UV)
aiVector3D* mTextureCoords[AI_MAX_NUMBER_OF_TEXTURECOORDS];
🎯 多 UV 的用途
Plain
一套UV → 漫反射贴图(颜色贴图)
二套UV → 光照贴图(Lightmap)
三套UV → 细节贴图(Detail Map)
...
🎨 美术知识:第一套 UV(索引 0)通常为基础贴图 UV,亦是我们最常用之数据。第二套 UV 常用于烘焙光照信息,于虚幻引擎等高级渲染中常见。
🛡️ 判空保护与数据提取
cpp
// 🎨 处理UV纹理坐标(只取第0套,即基础贴图UV)
if (aiMesh->mTextureCoords[0]) // 🛡️ 判空:防止无UV的模型崩溃
{
// 📍 取出第0套UV中第i个顶点的纹理坐标
aiVector3D uv = aiMesh->mTextureCoords[0][i];
// 📥 推入u、v(注意:UV只需x和y,z分量弃之不用)
uvs.push_back(uv.x); // u坐标
uvs.push_back(uv.y); // v坐标
}
else
{
// 🛡️ 若无UV数据,填充默认值0,避免着色器报错
uvs.push_back(0.0f);
uvs.push_back(0.0f);
}
} // 🔚 顶点循环结束
⚠️ 常见坑点 :部分模型未展开 UV(如纯几何测试模型),此时
mTextureCoords[0]为空指针。若不判空直接访问,必致程序崩溃。此乃新手常犯之误,不可不察!
3.5 第四象・面片索引(Indices)
📐 原理简述
aiMesh->mFaces:面片数组,每个aiFace代表一个多边形(通常为三角形)。 每个aiFace内含mIndices[],记录该面片使用哪些顶点。
cpp
// 🔄 遍历每一个面片(三角形)
for (unsigned int i = 0; i < aiMesh->mNumFaces; i++)
{
// 📐 取出第i个面片
aiFace face = aiMesh->mFaces[i];
// 🔄 遍历该面片的每一个顶点索引
for (unsigned int j = 0; j < face.mNumIndices; j++)
{
// 🔗 将索引推入索引数组
indices.push_back(face.mIndices[j]);
}
}
📊 性能说明:索引渲染(Indexed Drawing)可大幅减少重复顶点数据。例如立方体仅 8 个顶点,却有 36 个索引(12 个三角形 ×3 顶点)。若无索引,则需 36 个重复顶点,显存占用翻数倍。
🏗️ 第四章:组装归位・Geometry + Material = Mesh
4.1 构造几何体 Geometry
四象数据既备,则可铸几何体之真身:
cpp
// 🏗️ 用顶点/法线/UV/索引数据,构造自研几何体
Geometry* geometry = new Geometry(positions, normals, uvs, indices);
🔧 构造函数回顾:此前已为 Geometry 新增数据初始化之构造函数,可直接接收四类 vector 完成构建。
4.2 临时材质占位
材质解析乃下篇之内容,本篇先用默认材质占位,以验证几何数据之正确性:
cpp
// 🎨 引入材质头文件
#include "framework/material.h"
// 🧪 创建临时材质(下节课将解析模型自带材质)
FormMaterial* material = new FormMaterial();
// 🖼️ 临时绑定一张默认贴图到0号纹理单元
material->diffuse = new Texture("textures/box.png");
material->diffuse->bind(0);
4.3 合成 Mesh,功成返回
几何体与材质既备,则可合而为 Mesh,返回于调用者:
cpp
// ✨ 几何体 + 材质 = 网格对象
Mesh* resultMesh = new Mesh(geometry, material);
// 🎉 返回解析完成的自研Mesh
return resultMesh;
🧪 第五章:实战测试・模型加载验证
5.1 主函数加载模型
于main.cpp之prepare函数中,加载测试模型:
cpp
// 📂 加载外部模型文件,返回根节点Object
Object* testModel = Loader::load("models/your_model.fbx");
// 🔍 缩放修正:模型原始尺寸过大,缩小至10%
// (很多建模软件导出的模型单位较大,需根据实际调整)
testModel->setScale(0.1f);
// 🌳 添加到场景根节点,参与渲染循环
scene->addChild(testModel);
5.2 测试现象与问题排查
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 🖤 全黑无物 | 模型尺寸过大,摄像机在模型内部 | 调整setScale缩小模型 |
| 🔲 几何体正确但贴图错误 | 临时使用默认贴图,非模型自带纹理 | 正常现象,下节课解析材质 |
| 💥 程序崩溃 | 模型无 UV 数据,未判空访问 | 检查mTextureCoords[0]判空逻辑 |
🎯 验证标准:若能看到模型之几何轮廓(哪怕贴图不对),即说明顶点、法线、索引数据解析正确。材质纹理乃皮肤,几何结构乃骨架,骨架既成,皮肤可后续添加。
📋 终章:本篇总结与下篇预告
✅ 本篇完成之功
-
函数新增 :
processMesh静态函数,专司网格解析 -
参数补全 :
processNode增补aiScene参数,可查网格数据 -
四象提取:顶点位置、法线向量、UV 坐标、面片索引,四类核心数据全部解析
-
对象组装:Geometry + Material → Mesh,完成自研对象封装
-
层级挂载:网格挂载至对应 Object 节点,场景层级完整
📚 下篇预告
本篇几何骨架已成,然材质纹理尚未解析。模型自带之贴图、材质参数、纹理路径等,皆藏于aiMaterial之中。
🔮 下篇预告:材质解析篇・纹理贴图之解码玄功
届时将深入
aiMaterial之秘境,解锁漫反射贴图、法线贴图、高光贴图等诸般纹理,使模型真正 "有血有肉",焕发真实光彩。

📌 附录:核心数据结构速查表
| Assimp 原始结构 | 成员变量 | 含义 | 类型 |
|---|---|---|---|
aiNode |
mNumMeshes |
当前节点网格数量 | unsigned int |
aiNode |
mMeshes[] |
网格 ID 数组(仅 ID) | unsigned int* |
aiScene |
mMeshes[] |
全部网格数据数组 | aiMesh** |
aiMesh |
mNumVertices |
顶点总数 | unsigned int |
aiMesh |
mVertices[] |
顶点坐标数组 | aiVector3D* |
aiMesh |
mNormals[] |
法线数组 | aiVector3D* |
aiMesh |
mTextureCoords[][] |
多套 UV 数组 | aiVector3D* \[\] |
aiMesh |
mNumFaces |
面片总数 | unsigned int |
aiMesh |
mFaces[] |
面片数组 | aiFace* |
aiFace |
mNumIndices |
单面片索引数 | unsigned int |
aiFace |
mIndices[] |
顶点索引数组 | unsigned int* |
🌙 结语:
网格解析,乃模型加载之中流砥柱;四象数据,为渲染管线之根基要旨。 由 ID 而索数据,由数据而成形,由形而挂载,由挂载而成层级。 骨架既立,何患无皮?下篇材质,再续华章。
------ 图形学修炼者 识