DirectX12(D3D12)基础教程(十七)——让小姐姐翩翩起舞(3D骨骼动画渲染【3】)
目录
- 6、骨骼绑定
- 6.1、Shader中的骨骼绑定
- 6.2、aiMesh中的骨骼绑定信息
- 6.3、骨骼绑定信息的解算
- 7、骨骼树(骨架)
6、骨骼绑定
搞清楚了基本的骨骼动画的基本原理,那么就直接来进入“骨骼绑定”的编程学习,至于过于复杂的理论,就不多介绍了,我觉得直接上源码可能更容易让大家快速掌握骨骼动画,过多的理论反而让大家会觉得无从下手。
6.1、Shader中的骨骼绑定
首先从数据的角度讲,骨骼绑定在Shader中就是指在顶点的数据结构中扩展属性,用以指明顶点受其影响的骨骼的索引号,以及受影响的权重。就像刚才的例子中的那样:
struct VSInput
{float4 position : POSITION0; //顶点位置float4 normal : NORMAL0; //法线float2 texuv : TEXCOORD0; //纹理坐标uint4 bonesID : BLENDINDICES0; //骨骼索引float4 fWeights : BLENDWEIGHT0; //骨骼权重
};
其中最后两个4分量向量就是骨骼绑定的数据。其中bonesID的每一个分量就关联一个骨骼bone的id,在Shader中这其实就是后面要讲到的骨骼动画调色板(其实就是一组变换矩阵组成的数组)中的一个的数组元素索引,所以通常是UINT值。fWeights,就是对应ID的Bone使该顶点受到影响的程度权重值,是个0-1.0之间的值,一般情况下,所有fWeights的所有分量之和 ⩽1.0\leqslant 1.0⩽1.0 。
这里可能大家会有个疑问,为啥一个顶点最多只能受到4个骨骼的影响呢?其实“4”这个值,更大意义上是一个经验值,因为一般的动画中,比如人体动作中,外表任何地方的活动(任何一副“皮囊”),也就最多用关联的4个关节的运动即可表达。而过多的关联骨骼,其实并不会带来更多的动画细节的改善,反倒是如果改进骨骼自身的变换矩阵参数反而会使动画效果提升更明显。所以,就没必要让一个顶点关联过多的骨头。这样做也大大的缩小了Shader中顶点自身的数据结构,并减轻了计算量。对于其它生物甚至非生命活动物其实其动作基本也不需要过多的骨头关联即可较逼真的模拟。当然也有一些动画中,为了更精细的表示,部分顶点会关联多于4根骨头。
其次从形象的角度理解,可以看下图骨骼绑定的示意图:
图中白色的部分即可视化的虚拟骨骼,而灰色部分即为人体网格外形。按照示意图,实质上骨骼绑定就是说外表网格上的顶点会受到哪些(BonesID)黄色圈出的关节点的影响,并且影响是多大(Weights)。直观的也可以看出,一般肯定是离关节近的顶点会受其较大的影响,而离得远的顶点肯定受其影响小,或者直接就不受影响了。比如脚部的关节,肯定不会影响到头部的外表顶点。同时仔细观察还会发现,其实人体骨骼系统中大的关节是不多的,而且作为一般的动作模拟,只做到大的关节模拟,基本也就足够了,但是对于复杂的追求逼真动画效果的模拟来说其实挑战就会在人体的手部关节模拟和表情动画模拟上。美术界就有所谓“画树难画柳,画人难画手”说法,既是指人类的手部动作实在是太丰富了要画的非常准确就很难,其实3D动画模拟起来也是异常复杂的。不过好消息就是这些复杂的模拟建模的考虑基本都是美工的事情,而我们作为程序员要做的就是知道怎么解析、理解和操作这些数据!
最后,基于前述的结构体,在Shader中基本上一两行代码,就完成了骨骼绑定动画的计算:
// 根据骨头索引以及权重计算骨头变换的复合矩阵
float4x4 mxBonesTrans = vin.fWeights[0] * mxBones[vin.bonesID[0]]+ vin.fWeights[1] * mxBones[vin.bonesID[1]]+ vin.fWeights[2] * mxBones[vin.bonesID[2]]+ vin.fWeights[3] * mxBones[vin.bonesID[3]];
// 变换骨头对顶点的影响
vout.position = mul(vin.position, mxBonesTrans);
//最终变换到视-投影空间
vout.position = mul(vout.position, mxMVP);
这段代码,就是经典的Vertex Shader顶点动画计算过程。
6.2、aiMesh中的骨骼绑定信息
前一篇文章中,已经介绍过了,在Assimp库中,导入的骨骼数据其实是反绑定的。即它不是在每个网格顶点数据中绑定对应的骨骼,而是在骨骼的数据中用数组标明受其影响的顶点索引数组。这其实是为了方便数据的压缩表示,或者说数据结构更符合“范式”要求。
在Assimp中,骨骼绑定的数据主要在aiMesh对象中,并且以数组的形式存储如下:
struct aiMesh
{//......./** The number of bones this mesh contains.* Can be 0, in which case the mBones array is NULL.*/unsigned int mNumBones;/** The bones of this mesh.* A bone consists of a name by which it can be found in the* frame hierarchy and a set of vertex weights.*/C_STRUCT aiBone** mBones;
//.......
};
而aiBone类中存储的主要就是骨骼影响哪些顶点的数组:
struct aiBone {//! The name of the bone.C_STRUCT aiString mName;//! The number of vertices affected by this bone.//! The maximum value for this member is #AI_MAX_BONE_WEIGHTS.unsigned int mNumWeights;//! The influence weights of this bone, by vertex index.C_STRUCT aiVertexWeight* mWeights;/** Matrix that transforms from bone space to mesh space in bind pose.** This matrix describes the position of the mesh* in the local space of this bone when the skeleton was bound.* Thus it can be used directly to determine a desired vertex position,* given the world-space transform of the bone when animated,* and the position of the vertex in mesh space.** It is sometimes called an inverse-bind matrix,* or inverse bind pose matrix.*/C_STRUCT aiMatrix4x4 mOffsetMatrix;//.......
}
上面的类定义中,一定要注意的是,之前一篇中已经介绍过,Assimp中最终骨骼的全部信息被分散在了三个地方,aiBone中存储的就是骨骼绑定信息。
首先、aiBone::mName成员就是骨骼的名称,也是分散在三处的骨骼信息最终解算和汇聚成动画帧的关键线索,即这三处骨骼信息以相同名称相互关联。
其次、aiBone::mNumWeights和aiBone::mWeights即该骨骼影响到的顶点列表极其对应的影响程度权重数组。而mWeights的类如下定义:
struct aiVertexWeight {//! Index of the vertex which is influenced by the bone.unsigned int mVertexId;//! The strength of the influence in the range (0...1).//! The influence from all bones at one vertex amounts to 1.float mWeight;
// ......
};
这个类定义就很清晰了,就不做过多的说明了。
最后、aiBone::mOffsetMatrix是一个4x4矩阵,这个矩阵很重要,它表示的就是该骨骼(或者说关节更准确)在模型坐标系中的“方位”,即包含了位置信息、也包含旋转和缩放的信息。当不做动画渲染时,直接用这个矩阵值就可以显示出模型静态时的样子。之前也说过这个矩阵通常被叫做“逆位姿绑定矩阵”,说白了就是从骨关节的局部空间将骨骼变换到模型空间的一个矩阵。这说明了,骨关节在建模时其坐标系就是相对于自身的一个局部坐标系,通常就是骨关节最灵活变动的那个点作为这个局部坐标系的原点,因为这样就可以不用考虑比如,人的小腿运动时,往往还会关联大腿运动以及胯骨的运动,这样如果都使用模型空间的某一个统一坐标系,那么变换的生成和表达都将过于复杂。而放在刚才说的关节点处,那么同样的小腿运动,就可以以大腿与小腿间的关节点作为局部坐标系的原点,这样小腿的运动就简单的抽象表达为一个围绕该点的一定角度的旋转运动!
6.3、骨骼绑定信息的解算
在本章示例中,对上述数据做了必要的解算。首先是将mWeights数据填充到每个Vertex中去,代码如下所示:
UINT VertexID = 0;
FLOAT Weight = 0.0f;
UINT nBoneIndex = 0;
CStringA strBoneName;
aiMatrix4x4 mxBoneOffset;
aiBone* pBone = nullptr;
// 加载骨骼数据
for ( UINT i = 0; i < nMeshCnt; i++ )
{paiSubMesh = stMeshData.m_paiModel->mMeshes[i];for (UINT j = 0; j < paiSubMesh->mNumBones; j++){nBoneIndex = 0;pBone = paiSubMesh->mBones[j];strBoneName = pBone->mName.data;if ( nullptr == stMeshData.m_mapName2Bone.Lookup(strBoneName) ){// 新骨头索引nBoneIndex = nNumBones ++;stMeshData.m_arBoneDatas.SetCount(nNumBones);stMeshData.m_arBoneDatas[nBoneIndex].m_mxBoneOffset = XMMatrixTranspose(MXEqual(stMeshData.m_arBoneDatas[nBoneIndex].m_mxBoneOffset, pBone->mOffsetMatrix));stMeshData.m_mapName2Bone.SetAt(strBoneName, nBoneIndex);}else{nBoneIndex = stMeshData.m_mapName2Bone[strBoneName];}for (UINT k = 0; k < pBone->mNumWeights; k++){VertexID = stMeshData.m_arSubMeshInfo[i].m_nBaseVertex + pBone->mWeights[k].mVertexId;Weight = pBone->mWeights[k].mWeight;stMeshData.m_arBoneIndices[VertexID].AddBoneData(nBoneIndex, Weight);}}
}
上面代码中,稍微有点复杂,最终甚至嵌套了3重循环:
1、第一重循环是遍历模型中所有的网格,之前就已经说过,一个模型文件中可能会有多个网格,所以在Assimp中将导入的模型干脆叫做aiScene,即“场景”。其实这里多个网格有两重意思,第一个意思确实是说该模型中包含了多个物体的网格,所以称之为“场景”;第二个意思则是说可能该物体不止一个网格构成,是一个有多张“画皮”组成的复杂物体。而模型文件中往往第二种情况出现的较多,此时不能再理解为是“场景”,而是一个复杂的物体而已。
2、第二重循环就是针对每个aiMesh对象中的aiBone数组进行遍历。这里多了一个if-else判断,主要是为每个骨骼生成一个唯一索引,所以借助了一个Map对象,用骨骼名称与对应的索引建立映射。这个设计同时也保证了骨骼索引的唯一性,即如果模型数据中有同名的骨骼,那么最后它的索引是唯一的。这也暗示说,骨骼名称相同的都将被认为是同一根骨骼。然后代码中把每个骨骼的“逆位姿绑定矩阵”按照唯一索引存储到一个数组Array对象中,这个数组对象非常重要,后面的所有动画解算几乎都是围绕填充这个数组展开的,最后这个数组就被传入Vertex Shader的 Bone Const Buffer成为“骨骼动画调色板”。这里需要注意的就是我们对这个矩阵做了一个转置操作,因为默认情况下这个矩阵通常被存储为OpenGL的“右手坐标系”,而之前我们说过,我们将使用的是D3D祖传的“左手坐标系”,需要矩阵“右乘”行向量,所以做个转置即可。
3、最后第三重循环就是遍历每个aiBone对象中的权重Weights数组,将对顶点影响大小的权重数据再还原存储到对应的顶点上去。这里使用了一个顶点结构体的辅助函数AddBoneData:
#define GRS_BONE_DATACNT 4
struct ST_GRS_VERTEX_BONE
{public:UINT32 m_nBonesIDs[GRS_BONE_DATACNT];FLOAT m_fWeights[GRS_BONE_DATACNT];
public:VOID AddBoneData(UINT nBoneID, FLOAT fWeight){for (UINT32 i = 0; i < GRS_BONE_DATACNT; i++){if ( m_fWeights[i] == 0.0 ){m_nBonesIDs[i] = nBoneID;m_fWeights[i] = fWeight;break;}}}
};
利用这个方法将受影响的权重,反存回每个顶点中,并且这里每个顶点最多就存储4个权重值,即前面说的每个顶点最多受到4根骨头的影响。注意其中的if ( m_fWeights[i] == 0.0 )判断,其实这个判断大家不用担心,假如一个骨骼对一个顶点的影响是0,实际上这就是表示没有影响,那么其实不用存储这个骨骼的索引到顶点中的。
这里还要提醒大家注意的是,即使一个模型中有骨骼绑定信息,但并不表示模型中一定有动画,这种情况可能只是为了能够进一步引用或产生动画而准备的骨骼绑定的原始信息。本章结束后,大家可能都会冲动的去某宝网站买各种小姐姐的模型回来加载运行试一下,这里就是提醒大家一定注意看清楚,很多模型是标明了带骨骼绑定,但并没有说具有完整动作动画,这类模型只是为了方便动画美工师进一步加上复杂动画用的,如果程序直接加载可能只有静态的模型显示。所以在冲动消费前,请一定看仔细说明。(正如你想的那样,我其实已经上过当了,损失了巨额的350分人民币!)
7、骨骼树(骨架)
之前已经描述过,骨骼动画的根本原理就是模拟动物骨骼尤其是人体骨骼的动作,最终变换网格的顶点,形成动画的效果。这样骨骼数据本身,其实也是有结构的,这主要是几方面的原因,第一,人体或动物的骨骼天然具有关联关系,往往一个动作的完成不是简单的靠一两个骨骼就能完成的,而是需要一种具有“传导”关系的相互配合才能完成一个动作,比如跑动时都是靠“大腿带动小腿”这样的关系。第二、不论什么动物、人类、甚至机械等的动作,其实都是围绕“重心”展开的,比如人体天然的“重心”就是腰椎骨靠下部分的髋关节,而“重心”对于所有骨骼坐标系的模拟建模具有天然优势,完全可以将这类“重心”作为物体自身动作的原点,然后反算出其它各个“关节”部位的坐标系。第三、最终由于这些骨骼的天然层次关联关系,以及动作的“传导”展开,所以完全可以将所有的骨骼信息建模为“树形”的数据结构。即每个子关节的动作仅考虑自身坐标系的变动,而逐级向上关联父关节的动作即可形成完整的动作。
在前一篇文章中,其实已经在命令行状态下输出了Assimp导入模型后解析出的骨骼树形结构了:
在本章示例中,就不是简单的这样列印这颗树就完了,而是需要配合针对这棵树的递归算法,按照动画帧的变换数据来生成一帧帧的动画了。需要也就是说,上一篇文章中的递归算法“骨架”任然可以用于这一章真实动画渲染中。如果没搞明白,最好去复习一下先,前方高能,防止被虐。
需要提醒大家的就是,一定要注意骨架中骨骼的名字,其中有些骨骼名字是“匿名”,这种节点就是没有绑定到顶点的骨骼,只是起到中间过渡变换的目的,而有名字的节点就可以通过名字来搜索骨骼绑定信息和动画信息了。
DirectX12(D3D12)基础教程(十七)——让小姐姐翩翩起舞(3D骨骼动画渲染【3】)相关推荐
- DirectX12(D3D12)基础教程(十七)——让小姐姐翩翩起舞(3D骨骼动画渲染【1】)
目录 1.前言 2.本章代码简要说明 1.前言 经过了一系列比较枯燥的命令行式的"外篇"系列教程后,这一篇起回归主干,继续我们的D3D12之旅,本章就利用已经学习的assimp ...
- DirectX12(D3D12)基础教程(十七)——让小姐姐翩翩起舞(3D骨骼动画渲染【6】)
目录 12.多Slot上传顶点数据 12.1.多Slot上传数据基本原理 12.2.Assimp中间数据的简单转换 12.2.Layout的定义 12.3.缓冲区准备 12.4.多Slot渲染 13. ...
- DirectX12(D3D12)基础教程(十七)——让小姐姐翩翩起舞(3D骨骼动画渲染【2】)
目录 3.Assimp的导入标志 4.网格(Mesh) 5.骨骼动画基础 3.Assimp的导入标志 一般的模型文件中,大多数情况下在建模时默认都保存成了OpenGL的右手坐标系,即z轴坐标垂直屏 ...
- DirectX12(D3D12)基础教程(十七)——让小姐姐翩翩起舞(3D骨骼动画渲染【5】)
目录 10.动画关键帧解算 10.1.时间轴 10.2.遍历动作CalcAnimation 10.2.递归遍历骨骼树ReadNodeHeirarchy 10.3.关键帧数据解算和插值 10.4.生成关 ...
- DirectX12(D3D12)基础教程(十七)——让小姐姐翩翩起舞(3D骨骼动画渲染【4】)
目录 8.动画基本原理 9.四元数和SQT组合变换 9.1.四元数 9.2.SQT变换综合 9.3.存储方面的考虑 8.动画基本原理 对于一般的2D动画,甚至视频来说,相信各位已经很了解其原理了, ...
- python基础教程书籍推荐-小猿圈推荐Python入门书籍,不知道这些你就太low了。
原标题:小猿圈推荐Python入门书籍,不知道这些你就太low了. PYPL发布6月编程语言排行榜,盘踞前五名的分别是Python.Java.Java.C# 和 PHP.近五年,Python采用率高居 ...
- 【Python爬虫系列教程 21-100】小姐姐是时候带大家爬取表情包,再也不担心你没有表情包发了!
是这样的 有一次想要斗图 配图 就在网上搜索表情包 然后发现了一个表情巨多的网站 不小心动起了邪念 产生了兴趣 那就 把它们存下来吧 用 requests 请求了一下 发现这个网站没有做反爬 发现这里 ...
- 【Python爬虫系列教程 28-100】小姐姐带你入门爬虫框架Scrapy、 使用Scrapy框架爬取糗事百科段子
文章目录 Scrapy快速入门 安装和文档: 快速入门: 创建项目: 目录结构介绍: Scrapy框架架构 Scrapy框架介绍: Scrapy框架模块功能: Scrapy Shell 打开Scrap ...
- 学习笔记(02):XCX微信小程序基础教程-XCX微信小程序基础教程-1:小程序简介和注册1 ... ......
立即学习:https://edu.csdn.net/course/play/10043/213774?utm_source=blogtoedu weixing
最新文章
- Hibernate各种主键生成策略与配置详解 - 真的很详细啊!!
- etcd+calico集群的部署
- checked js 获取值_js获取所有checkbox的值的简单实例
- mesh和wifi中继的区别_什么是MESH WIFI?通俗易懂告诉你为什么需要它
- 一步一步学Silverlight 2系列(23):Silverlight与HTML混合之无窗口模式
- 前端学习(632):转义字符
- PHP内存管理机制与垃圾回收机制
- Python | threading02 - 互斥锁解决多个线程之间随机调度,造成“线程不安全”的问题。
- coco showanns不显示_coco奶茶加盟好不好?【5月官网最新公布】加盟费用+加盟流程...
- 数据结构与算法基础-数组
- 045、JVM实战总结:动手实验:自己动手模拟出对象进入老年代的场景体验一下(上)
- 计算机的组成 —— 存储(内存/硬盘)
- 基于MUI框架的影视播放APP的设计与实现毕业设计论文参考【原查重5.1%】
- 桌面云之深信服VMP平台搭建
- gradle下载与配置
- 谷歌安装ntko跨浏览器插件_Chrome浏览器不能安装WebEx扩展插件的解决方案
- CES这个会下腰的中国机器人火了,大型仿人机器人市场迎来“头号玩家”
- 【移动应用趋势】2022 年值得关注的 15 大移动应用开发趋势
- java 加背景颜色_Java 给PPT幻灯片添加背景颜色和背景图片
- Java泛型进阶篇: 无界通配符、上界通配符以及下界通配符