【OpenCV&OpenGL&Marker-based 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,这样就可以交给着色器程序来处理。
    这里可能有点乱,咱们来理一下这个过程:

    1. 记录所有的mAnimation、mChannel
    2. 访问rootNode,将rootNode记为Node
    3. 判断Node是否在mAnimation中;若是,则找到对应的mChannel,并计算变换信息(用于骨骼的变换); 这里用到了帧插值的方法,我们的mAnimation中相当于只记录了关键帧序列,关键帧之间需要插入一些填充帧。这个过程相当于将变换过程等分(例如两关键帧之间某个骨骼移动了1,那么我们在这两关键帧中按照移动方向插入一些帧,让两两帧间的移动距离变为0.2)
      这里还要注意,由于我们是遍历节点树,那么我们可能会出现误判的情况,这是通过ticks来解决的(例如第一帧对应的节点为Node1,第二帧对应的节点为Node1的某个子节点Node2,那么在第处理第一帧的时候会访问到Node2,那么我们是不是也要计算变换信息呢?显然我们不需要,于是我们可以使用当前ticks与Node2对应的Channel中的时间戳进行对比,从而跳过计算)
    4. 判断Node是否包含骨骼信息(Name是否一致);若是,则计算并记录骨骼的变换信息;
    5. 访问子节点,记为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));
    }
    

代码下一节讲。。。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值