教程代码资源:
dgaf 的 《DirectX 12 快速教程》配套代码 (A Sample of dgaf’s DirectX 12 Quick Beginner Tutorial)


在这里插入图片描述

本章我们要正式进入 Assimp 时代,学习相关的模型加载与使用!

在这里插入图片描述

sketchfab 是老牌 3D 模型展示社区,你可以从中找到各种各样令人满意的 3D 模型

在这里插入图片描述

在这里插入图片描述

本章我们将解析 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》(毁灭战士):


在这里插入图片描述
在这里插入图片描述

《DOOM》

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


在这里插入图片描述


刚性层阶式动画系统

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


在这里插入图片描述

《VR战士》

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


在这里插入图片描述

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


在这里插入图片描述

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


在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

刚性层阶式,就是角色由一堆刚性部分建模 (人物关节组织不能发生形变和缩放),这些刚性部分以层阶式组织并彼此约束 (像关节一样一层一层连接,上层带动下层运动)

刚性动画有一个严重的缺点,部位与部位之间会产生明显的裂缝,对于有皮肤的角色会在关节处产生明显裂缝 (尤其是做大幅度动作时),典型的例子就是《最终幻想7》中的人物行动:


在这里插入图片描述

在这里插入图片描述


每顶点动画

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


在这里插入图片描述

《合金装备崛起:复仇》中的"激流山姆",左下角可以看到巨大的顶点数量,难以想象如果做每顶点动画,会消耗多少昂贵的空间

蒙皮骨骼动画

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


在这里插入图片描述

原图出处:https://www.gcores.com/articles/174901

蒙皮骨骼动画有三要素:

骨骼 (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 包围盒


在这里插入图片描述

MC 里的碰撞箱其实是 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) —— 渲染模型

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐