Unity 人物换装实现
最近对Unity的换装方式和原理产生了一定的兴趣,所以,就去网上搜了一些文档,实现了一个简单版本的换装系统,这里简单总结一下。
在正式了解如何进行换装之前,我们需要了解几个概念。
骨骼,蒙皮和动画
目前游戏开发中常用的两种动画:顶点动画和蒙皮动画
顶点动画
通过在动画帧中直接修改mesh顶点的位置来实现,通常在mesh顶点数目较少,动画简单的情况下使用,如草的摆动,树的摆动,水的波动等蒙皮动画
通过在动画中直接修改bone的位置,让mesh的顶点随着bone的变化而变化,通常用于人形动画,如人物的跑动,跳跃等
骨骼是什么
当我们倒入带有骨骼的Model时,我们可以在其中发现一个嵌套的GameObject,这个GamoeObject以及它的所有的子GameObject都只有一个属性,就是transforms的坐标信息,这些坐标信息组成了该模型的骨骼信息。
蒙皮是什么
我们知道Mesh是由顶点和面组成的,如果不绑定蒙皮数据,称之为静态mesh,不具有动画效果的,如游戏中的房子,地面,桥,道路等;
对于绑定蒙皮的mesh,我们称之为SkinMesh,在SkinMesh中每个mesh的顶点会受到若干个骨骼的影响,并配以一定的权重比例;
就像我们真实的人一样,首先支撑并决定位置和运动的是一组骨骼,头+身体+四肢,而身上的肌肉是受到骨骼的影响而产生运动的,每一块肌肉的运动可能会受到多个骨骼的影响;
蒙皮中需要的数据
在unity中主要是通过SkinnedMeshRenderer组件来实现蒙皮动画的计算
计算蒙皮动画所需要的数据:
SkinnedMeshRenderer.bones:所有引用到bone的列表,注意顺序是确定的,后续顶点的BoneWeight中bone的索引,就是基于这个数组顺序的索引
SkinnedMeshRenderer.sharedMesh:渲染所需的mesh数据,注意相比普通的MeshRender所需的顶点和面数据,还会有一些额外的计算蒙皮相关的数据
Mesh.boneWeights:每个顶点受到哪几根bone的影响的索引和权重(每个顶点最多受到四根骨骼的影响,详见结构体BoneWeight的定义)
Mesh.bindposes:每根bone从mesh空间到自己的bone空间的变换矩阵,也就是预定义的bone的bone空间到mesh空间的变换矩阵的逆矩阵,注意顶点受到bone影响所做的变换都是基于在bone空间做的变换
根据Unity文档, Unity中BindPose的算法如下:
OneBoneBindPose = bone.worldToLocalMatrix * transform.localToWorldMatrix;
骨骼的世界转局部坐标系矩阵乘上Mesh的局部转世界矩阵
注意:美术一般在绑定蒙皮时,会将骨骼摆成一个Tpose的样式,这个时候的bone的transform转换出矩阵也就是bindpose,所有的骨骼动画都是在这个基础上相对变换的,最终会作为mesh本身的静态数据保存下来。
SkinnedMeshRenderer是一种不同于普通Mesh Renderer的渲染器,普通的渲染器是使用Mesh Filter定义的网格信息加上Material(会指定使用的Shader)对GameObject进行渲染,而SkinnedMeshRenderer不同,它还会有bones等属性,用于表明对应的骨骼信息。
这些信息都是在导入模型时自带的,在建模工具中就会定好每个点受到每个骨骼信息的影响,即当骨骼中的某个点位置发生改变时,相应的Mesh如何变化。
换装的两种方式
替换SkinnedMeshRender的方式
主要适用于衣服,裤子,发型等
替换步骤:
1:一般根据是否单独部位可以换装,将单独部位或者整套模型制作成一个Prefab
2:加载prefab,并实例化为GameObject
3:查找到新实例化的SkinnedMeshRender对应的原有的SkinnedMeshRender
4:替换bones
5:替换mesh
6:替换material
7:替换完成,销毁新实例化的Prefab
节点挂载的方式
主要适用于武器,翅膀,尾巴等
替换步骤:
1:一般将单独一套模型制作成一个Prefab
2:加载prefab,并实例化为GameObject
3:查找挂点(比如武器一般会挂载在手的骨骼节点上)
4:销毁原有的装备
5:将实例化的GameObject的父节点设置为挂点
6:设置好GameObject的偏移,缩放,旋转(一般都为0)
具体实现举例
首先是一种简单的方式,将所有的衣服的SkinnedMeshRenderer都拿出来,然后全部加到骨骼根节点上
1 | SkinnedMeshRenderer CombineSuitsToSkeleton(GameObject skeleton, SkinnedMeshRenderer[] meshes) |
通过上述方式,我们会在骨骼的根节点上添加多个Materials,这样在性能上不是最优,我们可以优化一下,将所有衣服的Material都合并起来,然后作为一个整体添加到骨骼上。
1 | SkinnedMeshRenderer CombineSuitsToSkeleton(GameObject skeleton, SkinnedMeshRenderer[] meshes) |
代码过程分析
- 首先获取所有的骨骼信息(骨骼其实就是一个transforms的数组):var transforms = new List
(skeleton.GetComponentsInChildren (true)); - 声明一个新的材质的数组:var materials = new List
(); - 声明一个CombineInstance(Struct used to describe meshes to be combined using Mesh.CombineMeshes)的数组:var combineInstances = new List
(); - 声明一个保存所有骨骼信息的数组:var bones = new List
(); - 然后是一段对meshs数组的循环,作用是将每个mesh的material保存进materials数组,然后将每个mesh的信息保存进combineInstances数组,最后根据名称从body的骨骼信息中获取与mesh的骨骼信息同名的骨骼保存进bones。
- 这段循环,是将衣服的材质信息保存进combineInstances中,骨骼信息保存进bones中。
- 获取人物body的SkinnedMeshRenderer,将它的骨骼信息绑定为所有的骨骼,它的mesh为一个新的mesh
- 创建一个新的材质,它的shader为默认的standard shader
- 保存旧有的UV信息:var oldUV = new List<Vector2[]>();
- 保存旧有的贴图信息:
1 | var Textures = new List<Texture2D>(); |
- 创建新的贴图:Texture2D newDiffuseTex = new Texture2D(1024, 1024, TextureFormat.RGBA32, true);
- 根据旧的贴图数组为新的贴图生成UV信息:Rect[] uvs = newDiffuseTex.PackTextures(Textures.ToArray(), 0);
- 将新的材质的贴图设置为新的贴图。
- 根据刚才根据生成的新的UV信息,重新计算将衣服贴图合并以后新贴图的UV信息。
参考文章:
https://zhuanlan.zhihu.com/p/87583171
https://zhuanlan.zhihu.com/p/94134459
https://gameinstitute.qq.com/community/detail/126729