Assimp

序言

Github
一个非常流行的模型导入库 Assimp(Open Asset Import Library)。它能够导入很多种不同的模型文件格式(也能导出一些),并在内存中呈现为 Assimp 的通用数据结构。一旦 Assimp 加载了模型,我们就可以通过 Assimp 提供的接口检索需要的数据。

编译

Assimp 官方比较推荐 vcpkg 的集成方式。
手动集成的话有三个坑:

  1. config.h
    假设生成目录是 assimp/build,Assimp 会在 assimp/build/include/assimp 下生成一份 config.h,这个文件最终会被 include 进源码里。
    所以在我们的项目中不但要包含 assimp/include 路径,还要包含 assimp/build/include/assimp 路径。
  2. zlib
    Assimp 依赖于 zlib 运行,所以在我们的项目中不但要链接 assimp-vc143-mt 库还要包含 zlibstatic 库,这个项目生成在 assimp/build/contrib/zlib 下,目标文件生成在 assimp/build/contrib/zlib/Debug(or Release) 下。
  3. stb
    Assimp 默认包含一份 stb_image 的定义,如果你的项目中需要手动包含 stb_image,其中一份实现会被忽略。
    Assimp 默认会 define STBI_ONLY_PNG,导致 stb_image 只能读取 .png 格式。
    这块的逻辑可以看下 Assimp.cpp 最后几行。
    解决办法是在 make Assimp 的时候使用 -DASSIMP_NO_EXPORT=ON

官方文档 assimp/Build.md 似乎没有提及这几件事,很坑。

导入

调用 Assimp::Importer 之后 Assimp 解析的模型数据会以一个只读的 aiScene * 的形式返回,并且当 Importer 实例销毁之后对应的 aiScene 也会销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>

void Import( const std::string &filePath) {
Assimp::Importer importer;

const aiScene *pScene = importer.ReadFile( filePath,
aiProcess_AAA | aiProcess_BBB);

if (nullptr == pScene) {
// Import failed.
}

ProcessScene(pScene);
}

ReadFile 的 aiProcess_XXX 是一个比较重要的参数,是 Assimp 提供的对模型的后处理,比如 aiProcess_GenNormals 代表如果模型不包含法向量的话由 Assimp 生成法向量。更多的选项看:postprocess.h File Reference

规范

默认:

  • 右手坐标系
  • CCW
  • UV 原点位于左下角

矩阵有一点奇怪,左乘并且以行主序存储,例如:

(X1Y1Z1T1X2Y2Z2T2X3Y3Z3T30001)\left( \begin{matrix} X1 & Y1 & Z1 & T1 \\ X2 & Y2 & Z2 & T2 \\ X3 & Y3 & Z3 & T3 \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right)

在内存中的排列是:[X1, Y1, Z1, T1, X2, Y2, Z2, T2, X3, Y3, Z3, T3, 0, 0, 0, 1]

以上规范大都可以通过 Post Process 进行修改。
比如 aiProcess_ConvertToLeftHanded 代表:

Supersedes the aiProcess_MakeLeftHanded and aiProcess_FlipUVs and aiProcess_FlipWindingOrder flags. The output data matches Direct3D’s conventions: left-handed geometry, upper-left origin for UV coordinates and finally clockwise face order, suitable for CCW culling.

aiScene

先看一个简化版的数据结构:
LearnOpenGL

Node

首先整个场景是以 node 的形式层级组织起来的,意义在于对某一节点进行任意形式的变换时,我们会希望对该节点的所有子节点应用相同的变换。比如一个挖掘机的大臂(动臂)挥动时连接着的小臂(斗杆)会自然地发生相同的变换。
那么 node 中存储的便是:

  • 子 node 的索引
  • 相对于父 node 的变换矩阵
  • 对应 mesh 的索引

当然 Assimp 也提供了后处理 aiProcess_PreTransformVertices 来移除层级结构并将所有变换矩阵直接应用的到顶点坐标上,不过这个选项同时会移除动画数据。

按照 node 递归读取 scene 的伪代码大致是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void ProcessNode(aiNode node, SceneObject parent, Mat4 transform) {
SceneObject nextParent;
Mat4 nextTransform;

if(node.mNumMeshes > 0) {
// If node has meshes, create a new scene object for it.
SceneObjekt newObject = new SceneObject;
parent.addChild(newObject);
ProcessMesh(node, newObject);

// The new object is the parent for all child nodes.
nextParent = newObject;
nextTransform.SetUnity();
}
else {
// If no meshes, skip the node, but keep its transformation.
nextParent = parent;
nextTransform = node.mTransform * transform;
}

for(all of node.mChildren) {
// Continue for all child nodes.
ProcessNode(node.mChildren[i], nextParent, nextTransform);
}
}

Mesh

可以发现 mesh 并不是存储于 node 中的,node 只持有一个对于 mesh 的引用,而所有 mesh 都平铺在 aiScene 中。同理 mesh 也持有一个 material 的引用,要拿着该索引回到 aiScene 才能拿到对应的材质。
这样一来 node 可以复用 mesh,mesh 可以复用 material。有一点绕,但就像 index 复用 vertex 一样合理。
不同在于一个 node 可以持有多个 mesh,而一个 mesh 只能持有一个 material。
mesh 结构体的定义看:mesh.h

Material

material 除了 GetTexture 接口可以拿到指定 aiTextureType 的贴图路径以外,还有 Get 接口可以拿到 Assimp 解析出来的材质定义。由于材质相关的定义与规范实属百花齐放,Assimp 也只提供了最简单的 MaterialProperty 的数据。
注意 Get 接口遵循十分严格的调用规范,并且需要由调用者保证正确的使用方式:Material-System。虽然约定俗成了不同 property 对应的数据类型,但好处是可以用简单的键值对获得十分复杂混乱的材质定义,也易于扩展,毕竟新增一个 MaterialProperty 也就是新增一对键值对的事。

样例

LearnOpenGL 是一个相对完整的例子。