3D模型的变与换3
简单叙述下上篇的内容。
上篇我们了解到,一个简化模型的算法,以及普通网格和蒙皮网格的区别。其中蒙皮网格是专门用来制作动画的,它除了普通网格所需要的顶点,三角形,uv数据外,还需要骨骼数据,和每个顶点的骨骼权重数据。
其中骨骼动画是由骨骼点组成的,每个骨骼点之间的关系,要么是父子关系,要么就是平行的兄弟关系,要么就是没有关系,这些骨骼点在Unity3D中对应到模型上的GameObject点。在这种骨骼结构下,当父节的骨骼点位移,旋转时,子节点的骨骼也同时相对于父节点位移和旋转。所以骨骼点之间的结构就是父子关系或平行结构关系。
===
那么骨骼点是怎么影响到顶点的呢,又是如何判断影响哪些顶点的呢?
骨骼动画中,需要顶点的骨骼权重数据,每个顶点都受到骨骼的影响,一个顶点最多被4个骨骼影响,所以在Unity3D中每个顶点都有一个BoneWeight实例,实例中描述了当前这个顶点对分别4个骨骼有多少权重,因为最多4个,每个顶点一个,所以顶点数组有多长,BoneWeight数组就有多长,当骨骼点移动时,将使用这些顶点权重值来计算顶点的旋转和偏移量。
简单来说就是,用顶点上的骨骼权重数据,来确定该点会被哪些骨骼点所影响。
蒙皮动画分两步:第一步是用3DMax,Maya等3D模型软件在几何模型上建立一系列的骨骼点(bones),并计算好几何模型的每个顶点受这些骨骼点的影响权重(BoneWeight);
第二步则是在Unity3D中用户或关键帧动画持续改变一系列骨骼点位置,几何模型随着骨骼点发生相应变形。
通常我们使用的都是关键帧动画,就是Unity3D里的Animation动画文件,在某个时间点上对需要改变的骨骼做一个关键帧,而并不是在每帧上都做关键帧的操作。
使用关键帧作为骨骼的旋转位移点的好处是不需要每帧去设置骨骼点的位置变化,在关键帧与关键帧之间的骨骼位置,是由Animation动画组件做了平滑的插值计算得到,这样可以减少大量的人力劳动,相当于关键帧之间做了‘补间动画’,这些‘补间动画’的目的就是对需要改变的骨骼做平滑的位移或者旋转插值计算,得到相应的位置和旋转角度。
’补间动画‘在每帧都只对骨骼动画做了位置和旋转的改变,然后蒙皮网格组件(SkinnedMeshRender)在每帧必须重新计算骨骼与网格的关系,所以最终就有了‘补间动画’每帧改变一些列骨骼点,骨骼点被(SkinnedMeshRender)重新计算得到模型网格变化,于是每帧就呈现出不同的网格变化,于是就有了3D模型网格动画。
整个骨骼动画的呈现流程清晰了,我们来用Unity3D的API来整理下,让我们理解的更加透彻。
举例代码和解释如下,重点在注释的解释:
//新建个动画组件和蒙皮组件
gameObject.AddComponent<Animation>();
gameObject.AddComponent<SkinnedMeshRenderer>();
SkinnedMeshRenderer rend = GetComponent<SkinnedMeshRenderer>();
Animation anim = GetComponent<Animation>();
//新建个网格组件,并编入4个顶点形成一个矩形形状的网格
Mesh mesh = new Mesh();
mesh.vertices = new Vector3[] {new Vector3(-1, 0, 0), new Vector3(1, 0, 0), new Vector3(-1, 5, 0), new Vector3(1, 5, 0)};
mesh.uv = new Vector2[] {new Vector2(0, 0), new Vector2(1, 0), new Vector2(0, 1), new Vector2(1, 1)};
mesh.triangles = new int[] {0, 1, 2, 1, 3, 2};
mesh.RecalculateNormals();
//新建个漫反射的材质球
rend.material = new Material(Shader.Find("Diffuse"));
//为每个顶点定制相应的骨骼权重
BoneWeight[] weights = new BoneWeight[4];
weights[0].boneIndex0 = 0;
weights[0].weight0 = 1;
weights[1].boneIndex0 = 0;
weights[1].weight0 = 1;
weights[2].boneIndex0 = 1;
weights[2].weight0 = 1;
weights[3].boneIndex0 = 1;
weights[3].weight0 = 1;
//把骨骼权重赋值给网格组件
mesh.boneWeights = weights;
//创建新的骨骼点,设置骨骼点的位置,父节点,和位移旋转矩阵
Transform[] bones = new Transform[2];
Matrix4x4[] bindPoses = new Matrix4x4[2];
bones[0] = new GameObject("Lower").transform;
bones[0].parent = transform;
bones[0].localRotation = Quaternion.identity;
bones[0].localPosition = Vector3.zero;
bindPoses[0] = bones[0].worldToLocalMatrix * transform.localToWorldMatrix;
bones[1] = new GameObject("Upper").transform;
bones[1].parent = transform;
bones[1].localRotation = Quaternion.identity;
bones[1].localPosition = new Vector3(0, 5, 0);
bindPoses[1] = bones[1].worldToLocalMatrix * transform.localToWorldMatrix;
mesh.bindposes = bindPoses;
//把骨骼点和网格赋值给蒙皮组件
rend.bones = bones;
rend.sharedMesh = mesh;
//定制几个关键帧
AnimationCurve curve = new AnimationCurve();
curve.keys = new Keyframe[] {new Keyframe(0, 0, 0, 0), new Keyframe(1, 3, 0, 0), new Keyframe(2, 0.0F, 0, 0)};
//创建帧动画
AnimationClip clip = new AnimationClip();
clip.SetCurve("Lower", typeof(Transform), "m_LocalPosition.z", curve);
//把帧动画赋值给动画组件,并播放动画
anim.AddClip(clip, "test");
anim.Play("test");
以上的Unity3D代码就呈现了,几何模型数据,蒙皮动画数据,从无到有的过程。
我们再返回去看在蒙皮动画第一步中权重的计算决定了蒙皮算法的效果,如果想要几何模型发生自然、高质量的形变,必须得有一种高效准确的权重计算方法。
这里简单讲一下蒙皮的计算方式,以了解下计算原理。
线性混合蒙皮(Linear Blending Skinning,LBS)是最最常用的蒙皮计算方式,由于它的计算速度优势使得其成为商业应用中最主要的方法之一。
什么是线性混合蒙皮计算方式呢?简单说就是,当前顶点位置变化 = 初始位置 + 骨骼点变化1 * 骨骼权重1 + 骨骼点变化2 * 骨骼权重2 ....
那么直接使用这种线性混合计算蒙皮的方式效果有点粗糙,为了更好的效果。[Jacobson et al. 2011]提出了一种有界双调和权重(Bounded Biharmonic Weights,BBW)的计算方法,该权重能使得几何模型发生平滑变形,这个算法后来就成为了我们现在最常使用的骨骼蒙皮动画的计算方式。
他大概的意思就是说,既然网格数据的变化计算量大,线性混合计算的速度又是最快的,我们可以在线性计算的基础上加以改进。在线性蒙皮混合计算公式中,初始位位置无法改变,骨骼点的变化也无法改变,所以权重计算骨骼点的变化的量决定了最终效果是否好的关键。
于是他就提出了,有界双调和权重的计算方法,其数学表达式如下:
由于蒙皮动画是每帧都通过骨骼点来计算网格的变化的,如果骨骼点很多,网格很复杂(顶点或者面数很多)那么消耗的CPU就很多,因为网格里的顶点都需要通过蒙皮算法来算出顶点的变化,这些都是靠CPU来计算的。因此在制作模型动画的时候,特别要注意,同屏里有多少蒙皮动画在播放,以及每个蒙皮动画中,骨骼的数量有多少,网格的面数有多复杂,如果太多太复杂就会巨量的消耗CPU。
5.人物3D模型动画换皮换装
有了上面的这些3D模型和骨骼动画的知识,我们在3D模型动画换装这种常见的游戏功能的编码设计上,就显得简单的多了。
首先,为了达到模型动画的动态拼接,我们必须一个人物只使用一套骨骼。
其次,把骨骼和模型部件拆分开来,骨骼文件只有骨骼数据,每个部件的模型文件只包含了它自己的模型数据,同时它也必须包含了顶点上的骨骼权重数据。
用Unity3D的术语来说就是,把一个人物模型拆分成有很多个Fbx,其中一个Fbx只有骨骼数据,其他Fbx是每个部件的模型数据,它们都带有已经计算好的骨骼权重数据。
然后,把骨骼数据和模型动画都动态拼接起来。
具体步骤是,用骨骼Fbx模型数据实例化成一个SkinnedMeshRenderer,这样基础的骨骼数据就包含在这个实例里了。再把需要显示的各个部件Fbx模型数据实例化出来,拥有自己的SkinnedMeshRenderer,然后把骨骼信息从前面骨骼SkinnedMeshRenderer里取出来赋值给自己。
这样每个部件都进行了SkinnedMeshRenderer实例化,SkinnedMeshRenderer可以渲染出自己的模型效果,并且自己的SkinnedMeshRenderer有了自己的骨骼数据,每个部件模型上也都有骨骼的权重数据,使每个模型部件针对骨骼动画是有效的。
再然后,在骨骼的SkinnedMeshRenderer上挂上Animator来播放动画文件,动画改变的是骨骼点,当动画播放时骨骼点会针对动画关键帧进行位移和渲染,由于部件模型的骨骼数据都是从骨骼的SkinnedMeshRenderer上映射过来的,所以当骨骼点动起来时就能带动众多的模型部件一起动起来。
当骨骼动画的SkinnedMeshRenderer上的动画文件开始播放时,每个部件也会随着骨骼点的变动而不断的计算出网格模型的变动情况,进而在渲染上体现出部件模型的动画效果。
最后,当我们需要更换人物上的某个部件模型时,只需要把原有的部件模型实例删除,再实例化出那个我们需要的部件模型,并把骨骼数据赋值给它就完成了操作,更换的操作看上去很简单,在表现上就是更换了人物的某个部件,脸,或腿,或手,或腰,或脚。
这种方式虽然是最简单的,但并不是最好的方式,比如人物拆分成了5个部件,头,手,身体,腿,脚,就需要6个SkinnedMeshRenderer来支撑,其中1个为骨骼动画的SkinnedMeshRenderer,其他5个为部件模型的SkinnedMeshRenderer,看上去很不友好,也就是说一个人物要至少5个Drawcall来支撑。
骨骼动画已经很消耗性能了,还需要5个材质球去消耗5个drawcall,加重了性能消耗的力度。
我们有更好的办法,我们希望一个人物动画只使用一个drawcall,那么我们就需要把这5个部件合并成一个模型,他们都使用同一个材质球,模型合并好办,使用Unity3D的Mesh.CombineMeshes就可以实现。
那么贴图怎么办?也同样合并。在每次初始化拼接一个人物模型时,或者更换人物的部件模型时,将5张贴图动态的合并成一张,并在合并贴图的同时需要改变每个模型部件的uv,将他们的uv偏移到这张合并整图的某个范围内。
这样一来,每个人物模型只需要消耗1个drawcall,减轻了gpu的负担。从CPU消耗来看,拼接的操作只存在于人物初始化,和更换部件模型时才会有消耗,所以合并贴图和模型的消耗并不频繁。
伪代码为:
function change_role_part( list of part_fbx )
{
root = new SkinnedMesh('role_bone.fbx');
combine_mesh = new Mesh();
combine_tex = new Texture();
for( item in part_fbx )
{
item.mesh.uv = item.uv + offset;
combine_mesh.combine(item.mesh);
combine_tex.combine(item.texture);
}
root.mesh = combine_mesh;
root.texture = combine_tex;
return root;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)