说在前面
- opencv版本:4.0.1
- opencv aruco版本:4.0.1
- opengl:使用glad、glfw
- 模型导入:Assimp
- ar实现:基于标记(marker)
- visual studio版本:2017
- 原理部分:【OpenCV&OpenGL&AR】原理部分
说明
主要还是OpenGL的知识点,参考Tutorial 38:Skeletal Animation With Assimp;
原理
Assimp结构
-
首先在导入模型时会返回一个aiScene对象,这个对象管理了模型的所有信息;
当使用Assimp导入一个模型的时候,它通常会将整个模型加载进一个场景(Scene)对象,它会包含导入的模型/场景中的所有数据。Assimp会将场景载入为一系列的节点(Node),每个节点包含了场景对象中所储存数据的索引,每个节点都可以有任意数量的子节点
而aiScene则管理着这些节点的根节点;
- 每个节点中有mChildren[](包含的所有子节点)以及mMeshes[](aiScene包含所有Mesh对象,而Node对象中的mMeshes是aiScene中Mesh的索引,并没有直接包含Mesh对象)
- Mesh(网格)对象本身包含了渲染所需要的所有相关数据,如顶点位置、法向量、纹理坐标、面(Face)、物体材质(Material)以及骨骼(Bone)等信息。(在骨骼对象中还有一个mOffsetMatrix属性,它描述了这块骨骼在Mesh中的位置,即偏移量;通过这个属性,我们可以直接将它从骨骼空间中转换到Mesh空间,注意不是Mesh到骨骼,见here)
- 一个网格包含了多个面。Face代表的是物体的渲染图元(Primitive)(三角形、方形、点)。一个面包含了组成图元的顶点的索引。由于顶点和索引是分开的,使用一个索引缓冲来渲染是非常简单的。
- aiScene中还有mAnimation[]对象,记录了所有的动画信息;事实上,每一个mAnimation对象可以渲染出动画中的一个帧序列。
所以,我们需要做的第一件事是将一个物体加载到Scene对象中,遍历节点,获取对应的Mesh对象(我们需要递归搜索每个节点的子节点),并处理每个Mesh对象来获取顶点数据、索引、材质属性以及骨骼信息。
(以上大部分来自LearnOpenGL教程)
骨骼(Bone)
-
我们知道在OpenGL世界中各种物体都是通过点连结而成的,例如经典的壶模型:
顶点的位置变动会改变模型的形状,骨骼动画差不多就是这样,只不过多了一个骨骼的概念。 -
一个骨骼附近有多个顶点,当骨骼运动时会影响顶点的运动;而影响顶点运动多少空间距离取决于这块骨头对顶点影响的权重;(例如某块骨头移动0.4,假设这块骨头对顶点A的权重为1,对顶点B的权重为0.5,那么可能顶点A也会移动0.4,而顶点B只移动0.2,这个栗子不是很严谨)
同时,一个顶点可能会受到多个骨骼的影响,但是所有骨骼对其的权重和为1。When a vertex is assigned to a bone a weight is defined that determines the amount of influence that bone has on the vertex when it moves. The common practice is to make the sum of all weights 1 (per vertex). For example, if a vertex is located exactly between two bones we would probably want to assign each bone a weight of 0.5 because we expect the bones to be equal in their influence on the vertex. However, if a vertex is entirely within the influence of a single bone then the weight would be 1 (which means that bone autonomously controls the movement of the vertex).
想象一下我们的手指,假设我们的皮肤是顶点构成的,那么在手指弯曲与伸直的时候,不同位置的顶点的移动范围是不同的。
-
因此,我们的顶点可以这样设计:
glm::vec3 position;//顶点位置3d glm::vec2 tex_coord;//纹理位置 glm::vec3 normal;//法线方向 int bone_ids[kMaxBonesPerVertex]; //骨骼ID,存放的是影响该顶点的所有骨骼的ID float bone_weights[kMaxBonesPerVertex]; //不同骨骼对该顶点的影响权重,与bone_ids一一对应 //kMaxBonesPerVertex为一个固定值,假设为5,那么表示一个顶点最多只能受到5块骨头的影响
骨骼动画
-
这个概念更加复杂一点,说实话,代码难看,而且那篇教程涉及的也不多,难受≧ ﹏ ≦!
- 一段连续的动画包含多个帧,而一个mAnimation对象包含了所有帧序列的信息。
- 一个mAnimation中包含多个mChannel对象,每个mChannel对象代表着一帧;但是一个mChannel只影响一个aiNode(两者是一对一的关系),那么一个aiNode是怎么产生一个很大的动作呢?(推想:骨头之间会形成一棵骨头树,影响某棵子树的根节点导致其所有子节点跟着变化)
- 一个mChannel对象又包含着该节点三个方面的变换:mPositionKeys(位置)、mRotationKeys(旋转)、mScalingKeys(缩放)
-
由于骨骼信息在Mesh中,而Mesh需要节点Node来获取;节点Node构成了一棵树;
我们需要收集一帧中所有涉及到的骨骼的mOffsetMatrix,这样就可以交给着色器程序来处理。
这里可能有点乱,咱们来理一下这个过程:- 记录所有的mAnimation、mChannel
- 访问rootNode,将rootNode记为Node
- 判断Node是否在mAnimation中;若是,则找到对应的mChannel,并计算变换信息(用于骨骼的变换); 这里用到了帧插值的方法,我们的mAnimation中相当于只记录了关键帧序列,关键帧之间需要插入一些填充帧。这个过程相当于将变换过程等分(例如两关键帧之间某个骨骼移动了1,那么我们在这两关键帧中按照移动方向插入一些帧,让两两帧间的移动距离变为0.2)
这里还要注意,由于我们是遍历节点树,那么我们可能会出现误判的情况,这是通过ticks来解决的(例如第一帧对应的节点为Node1,第二帧对应的节点为Node1的某个子节点Node2,那么在第处理第一帧的时候会访问到Node2,那么我们是不是也要计算变换信息呢?显然我们不需要,于是我们可以使用当前ticks与Node2对应的Channel中的时间戳进行对比,从而跳过计算) - 判断Node是否包含骨骼信息(Name是否一致);若是,则计算并记录骨骼的变换信息;
- 访问子节点,记为Node,返回第4步
-
然后我们就可以交给着色器程序进行处理;我们会将上面找到的骨骼变换信息传入到着色器程序。
着色器程序处理的是模型的所有顶点,我们记录的是某一帧中所有涉及到变换的骨骼的信息,那么我们怎么区分变换和不变的骨骼呢?还记得我们在定义Vertex时bone_ids属性吗?它会传入到着色器程序中。
着色器在处理一个顶点时,通过boneid找到对应的骨骼,若骨骼存在变换,对应的变换矩阵“存在”(存在这个词不太准确,大概是这个意思)
这样,我们就实现了一帧.#version 410 core const int MAX_BONES = 100; layout (location = 0) in vec3 aPosition; layout (location = 1) in vec2 aTexCoord; layout (location = 2) in vec3 aNormal; layout (location = 3) in ivec4 aBoneIDs0;//八个骨骼ID layout (location = 4) in ivec4 aBoneIDs1; layout (location = 5) in vec4 aBoneWeights0;//八个对应的权重 layout (location = 6) in vec4 aBoneWeights1; out vec3 vPosition; out vec2 vTexCoord; out vec3 vNormal; uniform mat4 uModelMatrix; uniform mat4 uViewMatrix; uniform mat4 uProjectionMatrix; uniform mat4 uBoneMatrices[MAX_BONES];//所有骨骼的变换信息 mat4 CalcBoneMatrix() { mat4 boneMatrix = mat4(0); for (int i = 0; i < 4; i++) { // 通过骨骼ID找到对应的变换矩阵,并乘以对应的权重 // 加和 boneMatrix += uBoneMatrices[aBoneIDs0[i]] * aBoneWeights0[i]; boneMatrix += uBoneMatrices[aBoneIDs1[i]] * aBoneWeights1[i]; } return boneMatrix; } void main() { mat4 boneMatrix = CalcBoneMatrix(); gl_Position = uProjectionMatrix * uViewMatrix * uModelMatrix * boneMatrix * vec4(aPosition, 1); vPosition = vec3(uModelMatrix * boneMatrix * vec4(aPosition, 1)); vTexCoord = aTexCoord; vNormal = vec3(uModelMatrix * boneMatrix * vec4(aPosition, 0)); }
代码下一节讲。。。