DX12 快速教程(9) —— 初识 Assimp 库
(9) AssimpAcquaintance: 初步认识并使用 Assimp 库,获取并打印《蔚蓝档案》中的 霞沢美游 gltf 模型的数据
快速导航
教程代码资源:
dgaf 的 《DirectX 12 快速教程》配套代码 (A Sample of dgaf’s DirectX 12 Quick Beginner Tutorial)
本章我们要正式进入 Assimp 时代,学习相关的模型加载与使用!



本章我们将解析 BA 的 霞沢美游 的 gltf 模型,并打印相关的模型信息
鸣谢作者大大 MOMO_RUI 提供的模型,模型源链接: https://sketchfab.com/3d-models/blue-archivekasumizawa-miyu-108d81dfd5a44dab92e4dccf0cc51a02
前言:Assimp 在 VS 中的配置与使用

Assimp :全称为 Open Asset Import Library,这是一个模型加载库,可以导入几十种不同格式的模型文件(同样也可以导出部分模型格式)。只要Assimp加载完了模型文件,我们就可以从Assimp上获取所有我们需要的模型数据。Assimp把不同的模型文件都转换为一个统一的数据结构,所有无论我们导入何种格式的模型文件,都可以用同一个方式去访问我们需要的模型数据。
CMake 构建 Assimp 项目与编译
首先,我们要去 github 上找到 Assimp 的仓库,下载压缩包并解压:


此时我们可以看到文件夹里面没有 .sln 文件,这是因为 Assimp 这个库为了满足跨平台的要求选择用 CMake 来建构,我们需要去官网下载并安装 CMake:



关于 CMake 的安装教程可以看看其他文章,本文不再一一赘述。
安装好 CMake 后,打开 CMake,填入源仓库文件夹与输出文件夹:

选择下方的 configure 配置 build 编译器,选择 VS2022:

配置完成后按 Finish 后会进行自动编译,Assimp 会查找仓库 makefile 的可用选项,标红的就是可用编译选项,我们可以直接选择 Generate 生成项目:

输出文件夹会生成 VS 项目,打开 Assimp.sln,上方选择 Debug (也可以选择 Release 版本,生成的文件会小一点),右键 ALL_BUILD -> 生成 (注意不是上方的编译并运行),耐心等待一段时间,最终就能编译完成:



Assimp 在 VS 中的配置
编译完成后,我们需要分别找到:
1. output/bin/Debug 文件夹下的 .dll 文件

2. output/lib/Debug 文件夹下的 .lib 文件

3. Assimp 源仓库文件夹 (不是 Output 文件夹) 下的 include 文件夹

4. output/include/assimp 文件夹下的 config.h (注意这里!)

在项目中新建 Assimp 文件夹:

在 Assimp 目录中新建一个 lib 文件夹,然后将 (1) (2) 的 .dll 和 .lib 拖进去:


将 (3) 中的整个 include 文件夹拖到 Assimp 中:

注意这里!我们还要将 (4) 中的 config.h 拖到 include/assimp 文件夹中,里面部分文件需要依赖 config.h

打开 DX12.sln,进行项目内部配置。新建项目 009-AssimpAcquaintance,右键项目 -> 属性 -> VC++ 目录

配置包含 Assimp 整个库的 .h 与 .cpp,点击包含目录 -> 编辑 -> “新建行” -> 右侧省略号 -> 选择 Assimp/include 文件夹 (或者直接写上 $(SolutionDir)Assimp/include) -> 确定




库目录同理,配置 Assimp/lib 文件夹:

最后,我们需要在代码上包含 Assimp 的头文件与链接库:
#include<assimp/Importer.hpp> // Assimp Importer 模型导入器,用于导入模型,读取模型数据
#include<assimp/postprocess.h> // PostProcess 后处理,提供多种标志 (aiProcess_xxx),用于改善模型的导入质量与性能
#include<assimp/scene.h> // Scene 核心组件库,用于存储与管理导入的 3D 模型的所有数据
#pragma comment(lib,"assimp-vc143-mtd.lib") // 链接 Assimp DLL
其中这个链接库的名字要和 Assimp\lib 里的相同:

注意这里!将 Assimp/lib 里面的 lib 和 dll 复制到 exe 所在的目录下 (x64/Debug 文件夹),生成的 exe 需要依赖这两个链接库:


编译并运行一下,如果没有错误就成功了:

正文:Assimp 教程
1.gltf 的文件结构

GLTF 是一种专为 3D 内容传输设计的开放标准格式,支持 Blender、Adobe Substance 等专业工具打开和使用。该格式通过 JSON描述+二进制数据结构 实现了三维场景的高效压缩与跨平台交互应用。

GLTF2.0 已作为 ISO/IEC 12113:2022 国际标准发布,Sketchfab 等一众网站也在力推 gltf 的推广和流行,任何可下载模型,都能下到 .gltf 格式的模型

2. Assimp 导入 gltf 模型

在 Assimp 中,导入模型数据的设备叫 Assimp::Importer:
Assimp::Importer* m_ModelImporter = new Assimp::Importer; // 模型导入器
引用模型数据的叫 ModelScene (const aiScene*) 模型场景,Assimp 把一个 3D 模型叫 Scene 场景:
const aiScene* m_ModelScene = nullptr; // 模型/场景对象
我们可以用 Importer::ReadFile 来打开并解析模型文件,并且导入到 ModelScene 中:
std::string ModelFileName = "miyu/scene.gltf"; // 模型文件名
Assimp::Importer* m_ModelImporter = new Assimp::Importer; // 模型导入器
const aiScene* m_ModelScene = nullptr; // 模型/场景对象
// 导入模型使用的标志
// aiProcess_ConvertToLeftHanded: Assimp 导入的模型是以 OpenGL 的右手坐标系为基础的,将模型转换成 DirectX 的左手坐标系
// aiProcess_Triangulate:模型设计师可能使用多边形对模型进行建模的,对于用多边形建模的模型,将它们都转换成基于三角形建模
// aiProcess_FixInfacingNormals:建模软件都是双面显示的,所以设计师不会在意顶点绕序方向,部分面会被剔除无法正常显示,需要翻转过来
// aiProcess_LimitBoneWeights: 限制顶点的骨骼权重最多为 4 个,其余权重无需处理
// aiProcess_GenBoundBoxes: 对每个网格,都生成一个 AABB 体积盒
// aiProcess_JoinIdenticalVertices: 将位置相同的顶点合并为一个顶点,从而减少模型的顶点数量,优化内存使用和提升渲染效率。
UINT ModelImportFlag = aiProcess_ConvertToLeftHanded | aiProcess_Triangulate | aiProcess_FixInfacingNormals |
aiProcess_LimitBoneWeights | aiProcess_GenBoundingBoxes | aiProcess_JoinIdenticalVertices;
// 读取模型数据,数据会存储在 aiScene 对象
// 使用 ReadFile 函数直接传递文件路径打开文件,路径可以有中文文字这些 utf-8 字符 (Assimp 最近修复了这个 bug)
m_ModelScene = m_ModelImporter->ReadFile(ModelFileName, ModelImportFlag);
ModelImportFlag 模型导入标志解析:
aiProcess_ConvertToLeftHanded: 这是一个由多个标志组合成的复合标志:Assimp 导入的模型是以 OpenGL, Vulkan 的右手坐标系为基础的,将模型转换成 DirectX 的左手坐标系 (纹理坐标系也会转换)
aiProcess_Triangulate: 模型设计师可能使用多边形对模型进行建模的,对于用多边形建模的模型,将它们都转换成基于三角形建模
aiProcess_FixInfacingNormals: 建模软件都是双面显示的,所以设计师不会在意顶点绕序方向,部分面会被剔除无法正常显示,需要翻转过来图源: https://zhuanlan.zhihu.com/p/20623801
aiProcess_LimitBoneWeights: 限制顶点的骨骼权重最多为 4 个,其余权重无需处理
图上白色的菱形体部分叫 Bone 骨骼,连接骨骼的圆形节点叫 BoneNode 骨骼节点,包裹骨骼及其节点的五颜六色的部分叫 Mesh 网格,在绑定骨骼的模型中,网格可以受多个骨骼影响,一般设定成最多 4 个就行,下文以及第 11,13 章也会涉及到
aiProcess_GenBoundBoxes: 对每个网格,都生成一个 AABB 体积盒
图源: https://www.cnblogs.com/lyggqm/p/5386174.html
aiProcess_JoinIdenticalVertices: 将位置相同的顶点合并为一个顶点,从而减少模型的顶点数量,优化内存使用和提升渲染效率。
合并相同顶点的同时,也会生成顶点索引,这样画三角形可以用 DrawIndexedInstanced重用顶点
注意!如果要重用 Importer 和 ModelScene 加载多个模型,不要 delete Importer! 因为真实的模型数据是存储在 Importer 上的,ModelScene 仅仅只是存储了 Importer 的引用
3.通过 ModelScene 读入模型数据
3D 游戏动画技术发展史
精灵动画系统
最早掀起 3D 游戏发展狂潮的是 1993 年 10 月 id Software 发行的《DOOM》(毁灭战士):


此时严格意义上来说不是 3D 游戏,而是披着 3D 皮的 2.5D 游戏,方法是利用 2D 的 sprite(精灵) 来摸拟出 3D 环境。
早期的计算机由于硬件限制,无法做到真正的 3D,因此免去纯 3D 图像处理所涉及的大量计算工作,从而使模拟出来的立体图像得以更快显现。利用这种绘图方式所产生的立体效果远超过 2D。

刚性层阶式动画系统
1993 年 11 月,首个最早有完整人物建模可互动的 3D 游戏《VR战士》横空出世,这是公认的第一款 3D 格斗游戏,不仅使用了 3D 角色模型,还引入了足以改变业界的 刚性层阶式动画系统。

先科普一下刚体 (Rigid Body)的概念:刚体是指在运动中和受力作用后,形状和大小不变,而且内部各点的相对位置不变的物体。

人物角色模型可以根据身体部位,划分成不同的部分,例如下图:

这些部位应该如何组织呢?人形角色通常会拆分成骨盘 (pelvis)、躯干 (torso)、上臂 (upper arm)、下臂 (lower arm)、大腿 (upper leg)、小腿 (lower leg)、手部 (hand)、脚部 (feet) 及头部 (head) 等等这些部分,这些刚性部分以层阶形式连接。
(所谓的层阶形式,就是身体部分是一层一层连接的,上层部位驱动下层部位。比如说将手臂按照关节划分为上臂(upper arm),下臂(lower arm)和手(hand),而当手动的时候,上臂或者下臂未必会动,但上臂动的时候下臂和手一定会动,根据这划分为上臂>下臂>手这样的层阶)




刚性层阶式,就是角色由一堆刚性部分建模 (人物关节组织不能发生形变和缩放),这些刚性部分以层阶式组织并彼此约束 (像关节一样一层一层连接,上层带动下层运动)
刚性动画有一个严重的缺点,部位与部位之间会产生明显的裂缝,对于有皮肤的角色会在关节处产生明显裂缝 (尤其是做大幅度动作时),典型的例子就是《最终幻想7》中的人物行动:


每顶点动画
每顶点动画,就是对人物模型所有顶点做 k 个动画帧,每帧存储这些顶点的位置和法线信息。
每顶点动画可以获得非常精细的动画效果,网格的三角形可以产生更自然的形变,但是工作量非常巨大,产生的数据量也非常巨大,在实时游戏中极少使用。

蒙皮骨骼动画
为了可以得到较生动的动画效果,允许网格的三角形产生形变。同时兼顾较高的性能、较低的存储空间,蒙皮骨骼动画便出现了。蒙皮骨骼,就是蒙盖有皮肤的骨骼模型。现在市场上几乎所有的游戏都采用蒙皮动画作为动画解决方案。典型的例子就是 1996 年的《生化危机》:

蒙皮骨骼动画有三要素:
骨骼 (skeleton):使用类似于刚性层阶式动画中的刚性部件 (刚性层阶式动画是一整块皮肤部分,而蒙皮动画换成了内部的骨骼) 来控制角色的运动,骨骼并不渲染,而是作为预处理数据,输入给蒙皮计算函数:
皮肤 (skin):皮肤本质上是一堆三角形组成的网格 (网格的定义下面会讲),所以皮肤也叫 蒙皮网格 (skinned mesh),圆滑三角形网格保证了角色模型的整体性,会绑定到骨骼上,其顶点会追踪 骨骼节点 (bone node) 的移动。
蒙皮:各顶点按权重绑定至多个骨骼节点,受绑定的骨骼约束而自然拉伸,能产生逐顶点动画中的形变效果,因此不会出现刚性层阶式动画的“裂缝”问题。(第 13 章会详细解释骨骼权重混合)




(1) 骨骼树

那么我们如何在 Assimp 中访问模型的骨骼呢?首先,我们要找到骨骼的 根节点 (RootNode),它是我们访问其他骨骼节点的入口:
m_ModelScene->mRootNode;


骨骼的组织形式是骨骼树,这意味着我们需要递归函数来一层一层地访问所有节点:
// 递归展开计算模型骨骼节点,打印每个骨骼节点的名称,大部分情况下节点都是骨骼,节点名和骨骼名相同
// 骨骼在模型中的呈现形式是骨骼树,在骨骼动画中,父节点会影响子节点,子节点拿到的偏移矩阵是相对于所属父节点的,所以要递归展开计算
// Assimp 中,节点的变换矩阵会影响节点下的全部属性,包括网格、骨骼、子节点,骨骼名是唯一的
void ModelNodeTraversal(const aiNode* node, std::string NodeBaseChar, UINT tier)
{
std::cout << NodeBaseChar
<< "┗━━ 节点名: " << node->mName.C_Str() // 节点名
<< " (层级: " << tier // 层级
<< ", 子节点数: " << node->mNumChildren // 子节点数
<< ")" << std::endl;
// 当前节点打印完成,下一行打印子节点时,在前面添加空格,便于区分
NodeBaseChar += " ";
// 遍历子节点,打印子节点的名称
for (UINT i = 0; i < node->mNumChildren; i++)
{
ModelNodeTraversal(node->mChildren[i], NodeBaseChar, tier + 1);
}
}
// 基础偏移字符串,用于打印时区分父子节点
std::string NodeBaseChar = "";
// 从根节点开始递归打印
ModelNodeTraversal(m_ModelScene->mRootNode, NodeBaseChar, 1);
(2) 材质与纹理

材质 (Material) 在计算机图形学中是非常重要的概念,它决定了物体表面的视觉特性和光照反应。不同的材质在不同的光线作用下,表现出不同的外观。外观是 材质 + 光线 共同作用结果。
那和纹理 (Texture) 有什么区别呢?简单来说,材质是物体表面光学属性集合,决定了光如何与表面交互;纹理是图像或数据贴图,用于为材质提供具体的细节图案或参数分布。一副材质对应多张不同类型的纹理贴图,纹理贴图服务于材质。
第 9-14 章,我们只会涉及这三种类型的纹理贴图:
(1) 漫反射贴图 (Diffuse Map),最常见的贴图,漫反射是指光线照射到物体表面后,向各个方向散射的现象。漫反射颜色是物体在均匀光照下的基本颜色,基本上可以看作物体的原图:

(2) 自发光贴图 (Emissive Map),控制表面发射光的颜色和亮度。当场景中使用了自发光材质时,它看起来像一个可见光。物体将呈现发光效果。(如本模型头上的生命光环用了自发光贴图)

(3) 法线贴图 (Normal Map),控制表面法线的方向,通过 RGB 颜色通道存储三维法线向量数据,能够高精度模拟低多边形模型表面凹凸细节:


在 Assimp 中,一副网格对应一副材质,如何获取该材质下拥有的所有类型贴图?我们可以用 aiMaterial::GetTexture 函数来逐一判断类型,并获取需要的纹理贴图名:
// 在 3D 建模的世界里,材质 (Material) 就像是给模型穿上的一件“外衣”
// 它不仅决定了模型的颜色和光泽,还能展现出材料的透明度、反射特性以及那些细微的表面纹理
// 材质组
std::vector<std::string> MaterialGroup;
// 遍历模型中的所有材质
for (UINT i = 0; i < m_ModelScene->mNumMaterials; i++)
{
// Assimp 解析出来的模型材质
aiMaterial* material = m_ModelScene->mMaterials[i];
// 添加材质名到 MaterialGroup 中
std::cout << "材质名: " << material->GetName().C_Str() << std::endl;
MaterialGroup.push_back(material->GetName().C_Str());
// 纹理是材质的子集,一个材质可能有很多组不同类型的纹理
// 获取材质中的纹理数,目前我们只会用到 EMISSIVE, DIFFUSE, NORMAL 这三种纹理,后面我们会逐一介绍这些纹理的功能与区别
// 在 Assimp 中,有一些类型 (例如 DIFFUSE 和 BASE_COLOR) 其实指的是同一个纹理,不过会有一些功能上的区别
std::cout
<< "EMISSIVE 自发光纹理数: " << material->GetTextureCount(aiTextureType_EMISSIVE) << "\n"
<< "DIFFUSE 漫反射纹理数: " << material->GetTextureCount(aiTextureType_DIFFUSE) << "\n"
<< "NORMAL 法线纹理数: " << material->GetTextureCount(aiTextureType_NORMALS) << "\n";
// 材质对应的纹理文件名
aiString materialPath;
// 获取材质对应的纹理,有时候一个材质甚至会有多个名字相同,但是类型不同的纹理贴图
// Assimp 为了区分这些纹理,特意设置了一个叫 Channel 通道的东西,如果名字相同,不同类型的纹理贴图会占据不同通道
// GetTexture 的第二个参数就是通道索引,大部分材质同类型下最多只有一个纹理,所以第二个参数直接指定 0 就行
// 注意 GetTexture 的返回值表示状态,aiReturn_SUCCESS 才算获取成功
if (material->GetTexture(aiTextureType_EMISSIVE, 0, &materialPath) == aiReturn_SUCCESS)
{
std::cout << "EMISSIVE 自发光纹理文件名: " << materialPath.C_Str() << "\n";
}
if (material->GetTexture(aiTextureType_DIFFUSE, 0, &materialPath) == aiReturn_SUCCESS)
{
std::cout << "DIFFUSE 漫反射纹理文件名: " << materialPath.C_Str() << "\n";
}
if (material->GetTexture(aiTextureType_NORMALS, 0, &materialPath) == aiReturn_SUCCESS)
{
std::cout << "NORMAL 法线纹理文件名: " << materialPath.C_Str() << "\n";
}
std::cout << std::endl;
}
注意!ModelScene 的 mTexture 指的是模型文件 (例如.glb) 内嵌的纹理贴图 (如果有的话,没有就是 nullptr),和 mMaterial 意思不同!
(3) 网格

上图中牛头模型被不同数量的三角形如网状般覆盖着,顶点组成的图元围成的一整块网状结构就叫 网格 (Mesh),三角形数量越多,网格密集程度越大,模型越精细。
Assimp 以 Mesh 网格为单位拆分模型的不同部位,这些网格一般都有:
网格名 (mName)
顶点数量 (mVertices)
索引数量 (mFaces,在 Assimp 中叫 mFaces 是因为网格图元可以是多边形,一个图元索引不止 3 个)
材质索引 (mMaterialIndex)
关联的骨骼 (mBones)
AABB 包围盒 (mAABB)
其他等等
// Mesh 网格相当于模型的皮肤,它存储了模型要渲染的顶点信息。在骨骼模型中,Mesh 需要依赖骨骼节点才能正确渲染
// 遍历模型的所有 Mesh 网格
for (UINT i = 0; i < m_ModelScene->mNumMeshes; i++)
{
// 当前网格
const aiMesh* mesh = m_ModelScene->mMeshes[i];
std::cout
<< "网格名: " << mesh->mName.C_Str() << "\n"
<< "顶点数: " << mesh->mNumVertices << "\n"
<< "索引数: " << mesh->mNumFaces * 3 << "\n"
<< "所用材质索引: " << mesh->mMaterialIndex
<< " (对应材质: " << MaterialGroup[mesh->mMaterialIndex] << ") \n";
// 如果 Mesh 有被骨骼影响到,就输出相关骨骼。对骨骼模型而言非常重要
// 一个网格可以被多个骨骼影响到,这是因为网格依赖骨骼,附着在骨骼上,通常是整块整块附着
// 关节是骨骼之间的连接点,这些网格很多都会覆盖关节 (或占据关节部分位置),网格上的顶点受不同骨骼的影响程度各不相同
if (mesh->HasBones())
{
std::cout << "受影响的骨骼: \n";
// 遍历骨骼
for (UINT i = 0; i < mesh->mNumBones; i++)
{
std::cout << mesh->mBones[i]->mName.C_Str() << std::endl;
}
}
else // 没有绑定骨骼,网格上的顶点坐标就表示相对于整个模型的绝对位置,即使是骨骼模型,也会有网格没被骨骼影响到
{
std::cout << "没有骨骼影响!\n";
}
std::cout << "\n";
}
(4) AABB 包围盒


AABB 包围盒,全称为 axis-aligned bounding box 轴对齐包围盒,它是一个长方体,且它的边是和轴线(比如 xyz 轴)对齐的。AABB 包围盒对碰撞检测和体积计算便捷高效,我们可以通过局部 Mesh 的 AABB 包围盒,更新整个模型的 AABB 包围盒,方便我们后续设置相机:
struct AABB // AABB 包围盒,下一章有大用
{
float minBoundsX; // 最小坐标点 X 值
float minBoundsY; // 最小坐标点 Y 值
float minBoundsZ; // 最小坐标点 Z 值
float maxBoundsX; // 最大坐标点 X 值
float maxBoundsY; // 最大坐标点 Y 值
float maxBoundsZ; // 最大坐标点 Z 值
};
AABB ModelBoundingBox; // 模型 AABB 包围盒,用于调整摄像机视野,防止模型在摄像机视野外飞出去
// 设置初始值
ModelBoundingBox =
{
m_ModelScene->mMeshes[0]->mAABB.mMin.x,
m_ModelScene->mMeshes[0]->mAABB.mMin.y,
m_ModelScene->mMeshes[0]->mAABB.mMin.z,
m_ModelScene->mMeshes[0]->mAABB.mMax.x,
m_ModelScene->mMeshes[0]->mAABB.mMax.y,
m_ModelScene->mMeshes[0]->mAABB.mMax.z
};
// 逐网格遍历,计算整个模型的 AABB 包围盒,请注意导入模型时要指定 aiProcess_GenBoundingBoxes,否则 mAABB 成员会没有数据
for (UINT i = 1; i < m_ModelScene->mNumMeshes; i++)
{
// 当前网格
const aiMesh* mesh = m_ModelScene->mMeshes[i];
// 更新总包围盒
ModelBoundingBox.minBoundsX = std::min(mesh->mAABB.mMin.x, ModelBoundingBox.minBoundsX);
ModelBoundingBox.minBoundsY = std::min(mesh->mAABB.mMin.y, ModelBoundingBox.minBoundsY);
ModelBoundingBox.minBoundsZ = std::min(mesh->mAABB.mMin.z, ModelBoundingBox.minBoundsZ);
ModelBoundingBox.maxBoundsX = std::max(mesh->mAABB.mMax.x, ModelBoundingBox.maxBoundsX);
ModelBoundingBox.maxBoundsY = std::max(mesh->mAABB.mMax.y, ModelBoundingBox.maxBoundsY);
ModelBoundingBox.maxBoundsZ = std::max(mesh->mAABB.mMax.z, ModelBoundingBox.maxBoundsZ);
}
std::cout << "模型包围盒: "
<< "min(" << ModelBoundingBox.minBoundsX << ","
<< ModelBoundingBox.minBoundsY << ","
<< ModelBoundingBox.minBoundsZ << ") "
<< "max(" << ModelBoundingBox.maxBoundsX << ","
<< ModelBoundingBox.maxBoundsY << ","
<< ModelBoundingBox.maxBoundsZ << ")" << std::endl;
第九章全代码
本章教程是一个简单的控制台程序,目的是为了让大家在下一章动手渲染做好心理准备:
// (9) AssimpAcquaintance: 初步认识并使用 Assimp 库,获取并打印《蔚蓝档案》中的 霞沢美游 gltf 模型的数据
// 鸣谢原作者大大: Onerui(momo) (https://sketchfab.com/hswangrui)
// 模型项目地址: https://sketchfab.com/3d-models/blue-archivekasumizawa-miyu-108d81dfd5a44dab92e4dccf0cc51a02
#define NOMINMAX // windows.h 与标准库里的 min/max 函数重名导致冲突了,禁用 windows.h 里面的 min/max 函数
#include<Windows.h> // Windows 窗口编程核心头文件
#include<iostream> // C++ 标准输入输出库
#include<vector> // C++ STL vector 容器库
#include<assimp/Importer.hpp> // Assimp Importer 模型导入器,用于导入模型,读取模型数据
#include<assimp/postprocess.h> // PostProcess 后处理,提供多种标志 (aiProcess_xxx),用于改善模型的导入质量与性能
#include<assimp/scene.h> // Scene 核心组件库,用于存储与管理导入的 3D 模型的所有数据
#pragma comment(lib,"assimp-vc143-mtd.lib") // 链接 Assimp DLL
// 1.项目 -> 属性 -> VC++ 目录 -> 包含目录 -> 添加 Assimp/include (或者写 $(SolutionDir)Assimp/include )
// 2.项目 -> 属性 -> VC++ 目录 -> 库目录 -> 添加 Assimp/lib (或者写 $(SolutionDir)Assimp/lib )
// 3.将 Assimp/lib 里面的 assimp-vc143-mtd.lib 与 assimp-vc143-mtd.dll 复制到
// exe 所在的目录下 (x64/Debug 文件夹),生成的 exe 需要依赖这两个链接库
// GLTF 是一种 3D 模型文件格式,用于引擎和应用程序高效传输和加载 3D 场景和模型
// 它的文件结构如下:
// textures 模型纹理文件夹
// license.txt 模型使用的许可证 (常用许可证是 CC-BY: 发布必须署名原作者,可商用)
// scene.bin 储存模型图元,顶点,骨骼动画等二进制数据
// scene.gltf JSON 文件,定义 scene 的结构与元素,并储存 bin 和 textures 的链接
// ---------------------------------------------------------------------------------------------------------------
UINT TotalNodeNum = 0; // 总节点数
// 递归展开计算模型骨骼节点,打印每个骨骼节点的名称,大部分情况下节点都是骨骼,节点名和骨骼名相同
// 骨骼在模型中的呈现形式是骨骼树,在骨骼动画中,父节点会影响子节点,子节点拿到的偏移矩阵是相对于所属父节点的,所以要递归展开计算
// Assimp 中,节点的变换矩阵会影响节点下的全部属性,包括网格、骨骼、子节点,骨骼名是唯一的
void ModelNodeTraversal(const aiNode* node, std::string NodeBaseChar, UINT tier)
{
std::cout << NodeBaseChar
<< "┗━━ 节点名: " << node->mName.C_Str() // 节点名
<< " (层级: " << tier // 层级
<< ", 子节点数: " << node->mNumChildren // 子节点数
<< ")" << std::endl;
// 当前节点打印完成,下一行打印子节点时,在前面添加空格,便于区分
NodeBaseChar += " ";
// 遍历子节点,打印子节点的名称
for (UINT i = 0; i < node->mNumChildren; i++)
{
ModelNodeTraversal(node->mChildren[i], NodeBaseChar, tier + 1);
}
TotalNodeNum++; // 总节点数 +1
}
int main()
{
std::string ModelFileName = "miyu/scene.gltf"; // 模型文件名
Assimp::Importer* m_ModelImporter = new Assimp::Importer; // 模型导入器
const aiScene* m_ModelScene = nullptr; // 模型/场景对象
// 导入模型使用的标志
// aiProcess_ConvertToLeftHanded: Assimp 导入的模型是以 OpenGL 的右手坐标系为基础的,将模型转换成 DirectX 的左手坐标系
// aiProcess_Triangulate:模型设计师可能使用多边形对模型进行建模的,对于用多边形建模的模型,将它们都转换成基于三角形建模
// aiProcess_FixInfacingNormals:建模软件都是双面显示的,所以设计师不会在意顶点绕序方向,部分面会被剔除无法正常显示,需要翻转过来
// aiProcess_LimitBoneWeights: 限制顶点的骨骼权重最多为 4 个,其余权重无需处理
// aiProcess_GenBoundBoxes: 对每个网格,都生成一个 AABB 体积盒
// aiProcess_JoinIdenticalVertices: 将位置相同的顶点合并为一个顶点,从而减少模型的顶点数量,优化内存使用和提升渲染效率。
UINT ModelImportFlag = aiProcess_ConvertToLeftHanded | aiProcess_Triangulate | aiProcess_FixInfacingNormals |
aiProcess_LimitBoneWeights | aiProcess_GenBoundingBoxes | aiProcess_JoinIdenticalVertices;
// 读取模型数据,数据会存储在 aiScene 对象
// 使用 ReadFile 函数直接传递文件路径打开文件,路径可以有中文文字这些 utf-8 字符 (Assimp 最近修复了这个 bug)
m_ModelScene = m_ModelImporter->ReadFile(ModelFileName, ModelImportFlag);
// 如果模型没有成功载入 (无法载入,载入未完成,载入后无根节点)
if (!m_ModelScene || m_ModelScene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !m_ModelScene->mRootNode)
{
// Assimp 载入模型的错误信息
std::string Assimp_error_msg = m_ModelImporter->GetErrorString();
std::string errorMsg = "载入文件 ";
errorMsg += ModelFileName;
errorMsg += " 失败!错误原因:";
errorMsg += Assimp_error_msg;
MessageBoxA(NULL, errorMsg.c_str(), "错误", MB_ICONERROR | MB_OK);
return 1;
}
std::cout << "成功加载 " << ModelFileName << " ! \n" << std::endl;
// ---------------------------------------------------------------------------------------------------------------
std::cout << "开始遍历节点! \n\n";
// 基础偏移字符串,用于打印时区分父子节点
std::string NodeBaseChar = "";
// 从根节点开始递归打印
ModelNodeTraversal(m_ModelScene->mRootNode, NodeBaseChar, 1);
std::cout << "\n" << "总节点数: " << TotalNodeNum << "\n\n";
std::cout << "------------------------------------------------------------------------\n\n";
// ---------------------------------------------------------------------------------------------------------------
// 在 3D 建模的世界里,材质 (Material) 就像是给模型穿上的一件“外衣”
// 它不仅决定了模型的颜色和光泽,还能展现出材料的透明度、反射特性以及那些细微的表面纹理
// 材质组
std::vector<std::string> MaterialGroup;
// 遍历模型中的所有材质
for (UINT i = 0; i < m_ModelScene->mNumMaterials; i++)
{
// Assimp 解析出来的模型材质
aiMaterial* material = m_ModelScene->mMaterials[i];
// 添加材质名到 MaterialGroup 中
std::cout << "材质名: " << material->GetName().C_Str() << std::endl;
MaterialGroup.push_back(material->GetName().C_Str());
// 纹理是材质的子集,一个材质可能有很多组不同类型的纹理
// 获取材质中的纹理数,目前我们只会用到 EMISSIVE, DIFFUSE, NORMAL 这三种纹理,后面我们会逐一介绍这些纹理的功能与区别
// 在 Assimp 中,有一些类型 (例如 DIFFUSE 和 BASE_COLOR) 其实指的是同一个纹理,不过会有一些功能上的区别
std::cout
<< "EMISSIVE 自发光纹理数: " << material->GetTextureCount(aiTextureType_EMISSIVE) << "\n"
<< "DIFFUSE 漫反射纹理数: " << material->GetTextureCount(aiTextureType_DIFFUSE) << "\n"
<< "NORMAL 法线纹理数: " << material->GetTextureCount(aiTextureType_NORMALS) << "\n";
// 材质对应的纹理文件名
aiString materialPath;
// 获取材质对应的纹理,有时候一个材质甚至会有多个名字相同,但是类型不同的纹理贴图
// Assimp 为了区分这些纹理,特意设置了一个叫 Channel 通道的东西,如果名字相同,不同类型的纹理贴图会占据不同通道
// GetTexture 的第二个参数就是通道索引,大部分材质同类型下最多只有一个纹理,所以第二个参数直接指定 0 就行
// 注意 GetTexture 的返回值表示状态,aiReturn_SUCCESS 才算获取成功
if (material->GetTexture(aiTextureType_EMISSIVE, 0, &materialPath) == aiReturn_SUCCESS)
{
std::cout << "EMISSIVE 自发光纹理文件名: " << materialPath.C_Str() << "\n";
}
if (material->GetTexture(aiTextureType_DIFFUSE, 0, &materialPath) == aiReturn_SUCCESS)
{
std::cout << "DIFFUSE 漫反射纹理文件名: " << materialPath.C_Str() << "\n";
}
if (material->GetTexture(aiTextureType_NORMALS, 0, &materialPath) == aiReturn_SUCCESS)
{
std::cout << "NORMAL 法线纹理文件名: " << materialPath.C_Str() << "\n";
}
std::cout << std::endl;
}
std::cout << "\n" << "总材质数: " << m_ModelScene->mNumMaterials << "\n\n";
std::cout << "------------------------------------------------------------------------\n\n";
// ---------------------------------------------------------------------------------------------------------------
std::cout << "开始遍历网格! \n\n";
// Mesh 网格相当于模型的皮肤,它存储了模型要渲染的顶点信息。在骨骼模型中,Mesh 需要依赖骨骼节点才能正确渲染
// 遍历模型的所有 Mesh 网格
for (UINT i = 0; i < m_ModelScene->mNumMeshes; i++)
{
// 当前网格
const aiMesh* mesh = m_ModelScene->mMeshes[i];
std::cout
<< "网格名: " << mesh->mName.C_Str() << "\n"
<< "顶点数: " << mesh->mNumVertices << "\n"
<< "索引数: " << mesh->mNumFaces * 3 << "\n"
<< "所用材质索引: " << mesh->mMaterialIndex
<< " (对应材质: " << MaterialGroup[mesh->mMaterialIndex] << ") \n";
// 如果 Mesh 有被骨骼影响到,就输出相关骨骼。对骨骼模型而言非常重要
// 一个网格可以被多个骨骼影响到,这是因为网格依赖骨骼,附着在骨骼上,通常是整块整块附着
// 关节是骨骼之间的连接点,这些网格很多都会覆盖关节 (或占据关节部分位置),网格上的顶点受不同骨骼的影响程度各不相同
if (mesh->HasBones())
{
std::cout << "受影响的骨骼: \n";
// 遍历骨骼
for (UINT i = 0; i < mesh->mNumBones; i++)
{
std::cout << mesh->mBones[i]->mName.C_Str() << std::endl;
}
}
else // 没有绑定骨骼,网格上的顶点坐标就表示相对于整个模型的绝对位置,即使是骨骼模型,也会有网格没被骨骼影响到
{
std::cout << "没有骨骼影响!\n";
}
std::cout << "\n";
}
std::cout << "总网格数: " << m_ModelScene->mNumMeshes << "\n\n";
// ---------------------------------------------------------------------------------------------------------------
std::cout << "开始计算包围盒! \n\n";
struct AABB // AABB 包围盒,下一章有大用
{
float minBoundsX; // 最小坐标点 X 值
float minBoundsY; // 最小坐标点 Y 值
float minBoundsZ; // 最小坐标点 Z 值
float maxBoundsX; // 最大坐标点 X 值
float maxBoundsY; // 最大坐标点 Y 值
float maxBoundsZ; // 最大坐标点 Z 值
};
AABB ModelBoundingBox; // 模型 AABB 包围盒,用于调整摄像机视野,防止模型在摄像机视野外飞出去
// 设置初始值
ModelBoundingBox =
{
m_ModelScene->mMeshes[0]->mAABB.mMin.x,
m_ModelScene->mMeshes[0]->mAABB.mMin.y,
m_ModelScene->mMeshes[0]->mAABB.mMin.z,
m_ModelScene->mMeshes[0]->mAABB.mMax.x,
m_ModelScene->mMeshes[0]->mAABB.mMax.y,
m_ModelScene->mMeshes[0]->mAABB.mMax.z
};
// 逐网格遍历,计算整个模型的 AABB 包围盒,请注意导入模型时要指定 aiProcess_GenBoundingBoxes,否则 mAABB 成员会没有数据
for (UINT i = 1; i < m_ModelScene->mNumMeshes; i++)
{
// 当前网格
const aiMesh* mesh = m_ModelScene->mMeshes[i];
// 更新总包围盒
ModelBoundingBox.minBoundsX = std::min(mesh->mAABB.mMin.x, ModelBoundingBox.minBoundsX);
ModelBoundingBox.minBoundsY = std::min(mesh->mAABB.mMin.y, ModelBoundingBox.minBoundsY);
ModelBoundingBox.minBoundsZ = std::min(mesh->mAABB.mMin.z, ModelBoundingBox.minBoundsZ);
ModelBoundingBox.maxBoundsX = std::max(mesh->mAABB.mMax.x, ModelBoundingBox.maxBoundsX);
ModelBoundingBox.maxBoundsY = std::max(mesh->mAABB.mMax.y, ModelBoundingBox.maxBoundsY);
ModelBoundingBox.maxBoundsZ = std::max(mesh->mAABB.mMax.z, ModelBoundingBox.maxBoundsZ);
}
std::cout << "模型包围盒: "
<< "min(" << ModelBoundingBox.minBoundsX << ","
<< ModelBoundingBox.minBoundsY << ","
<< ModelBoundingBox.minBoundsZ << ") "
<< "max(" << ModelBoundingBox.maxBoundsX << ","
<< ModelBoundingBox.maxBoundsY << ","
<< ModelBoundingBox.maxBoundsZ << ")" << std::endl;
return 0;
}




下一章,我们将正式开始 Assimp 征程,学习模型在 DX12 上的导入与加载。
下一章教程: DX12 快速教程(10) —— 渲染模型
更多推荐







所有评论(0)