人类在运动的时候,实际是骨骼在不断变换位置,然后骨骼带动全身皮肉在运动。在游戏中就反映为骨骼节点带动Mesh中的Vertices在运动,进而播放Mesh动画。这里以Unity工程为工具,来理解一下骨骼和蒙皮的过程。
骨骼
骨骼决定了模型整体在世界坐标系中的位置和朝向,和人类骨骼一样,你的拇指关节移动,你拇指附近的肌肉也会跟着移动。骨骼是有着层次结构的:
body上绑定了SkinnedMeshRenderer,指向的根节点为Pelvis。从这个根节点往下是一个树状的层次结构,有Spine,Thigh等子孙节点,这些就是骨骼。
现在我的SkinnedMeshRenderer是挂在body上的,意味着这就是这个mesh的原点,我的vertices都是基于这个原点的。为了简化问题,假设这个body点恰好就是坐标原点,旋转什么的也都和世界坐标原点一模一样。现在想象在坐标原点聚集着一堆顶点,这些顶点加上skin构成了一个生物模型。
如果我这时旋转body节点呢
当我旋转body节点的时候,因为body的世界旋转变了,而其内部Vertices相对于body的位置又不变,那理论上这个模型应该是会跟随body旋转才对。然后并不是。。。
模型的位置和旋转只会和骨骼有关,我的Pelvis节点并未动过,那我看到的模型也不会变化。那要实现这样的效果的话,我们只能去修改mesh的Vertices的位置,给他们做相应变换。
换句话说,当body旋转时,Vertices相对于骨骼的位置没有改变。我们可以计算出这个变换矩阵,然后反过来去求body旋转之后改变的Vertices。
骨骼计算的过程
1.mesh中的vertices原始局部坐标通过BoneOffsetMatrix转换为BoneSpace中的坐标。(以某个骨骼为原点的坐标系)
先从mesh的local空间转换到世界空间,再从世界空间转换到骨骼空间。
Verticesbone 是Vertices在BoneSpace的坐标。在美术把骨骼,body节点放好,BoneOffsetMatrix, Verticeslocal , Verticesbone 的值都已经确定了。根据这个公式看前面旋转Body的操作。旋转body改变了render.localtoworldmatrix进行改变BoneOffsetMatrix,此时引擎改变 Verticeslocal 值来保证他两乘积不变。
2.根据Vertices在BoneSpace中的坐标计算出世界坐标
只需要将BoneSpace中的局部坐标乘以一个bone.localtoworldmatrix即可。unity中支持最多四根骨骼来控制一个Vertice,每根骨骼根据Vertice在BoneSpace中的坐标算出一个Vertice的世界坐标,然后加权平均。
蒙皮
Vertices位置确定了之后,一张mesh贴上去就是蒙皮了。。。
实战
将骨骼动画渲在UI Canvas下面。
有一个需求是将3D模型渲染到UI层,选择的做法是将3D模型的skinnedmeshrenderer里的mesh信息取出来,再用CanvasRenderer来渲染,这样的一个好处是可以利用UI本身的层级顺序。
将前面的body作为待拷贝的skinnedmeshrenderer,这里旋转body不会改变原有skinnedmeshrenderer所在模型的旋转,但是却旋转了ui下的canvasrenderer渲出的模型。这是因为,旋转body时,改变了mesh中vertices的local坐标,但是canvas下的节点没有设置旋转,导致产生看到的现象。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CopyMeshToUi : MonoBehaviour
{
public SkinnedMeshRenderer SkinnedMeshRenderer;
private Material _mat;
private CanvasRenderer _canvasRenderer;
private Mesh _mesh;
void Start()
{
_mesh = new Mesh();
_mat = SkinnedMeshRenderer.material;
GameObject obj = new GameObject("Render3D");
obj.transform.parent = transform;
obj.transform.localPosition = Vector3.zero;
RectTransform rectTransform = obj.AddComponent<RectTransform>();
rectTransform.SetAsFirstSibling();
_canvasRenderer = obj.AddComponent<CanvasRenderer>();
obj.transform.rotation = SkinnedMeshRenderer.transform.rotation;
obj.transform.localScale = (SkinnedMeshRenderer.transform.lossyScale.x / transform.parent.lossyScale.x)*
Vector3.one;
}
void Update()
{
SkinnedMeshRenderer.BakeMesh(_mesh);
_mesh.RecalculateBounds();
_canvasRenderer.Clear();
_canvasRenderer.SetMaterial(_mat,null);
_canvasRenderer.SetMesh(_mesh);
}
}
人物换装
简单的换装包括更改材质,或者更改模型(如武器)
还有一种换装是基于相同骨骼,替换身体的某些部位,比如替换Head。原理也比较简单,取出新的Head的Mesh放在目标模型中,然后将这个Mesh绑定到目标模型的骨骼即可。
public Transform target; // 目标的模型,要求其骨骼已经存在
public Transform source; // 源模型,所有的可以替换的部件都在这上面
// 模型资源,对应上面的source
Dictionary<string, Dictionary<string, Transform>> data = new Dictionary<string, Dictionary<string, Transform>>();
// 目标骨架,对应上面的target
Transform[] hips;
// 目标皮肤,替换这里面的内容就行了
Dictionary<string, SkinnedMeshRenderer> targetSmr = new Dictionary<string, SkinnedMeshRenderer>();
// 初始化时的皮肤
string[,] avatarStr = new string[,] { { "coat", "003" }, { "hair", "003" }, { "pant", "003" }, { "hand", "003" }, { "foot", "003" }, { "head", "003" } };
// Use this for initialization
void Start()
{
// 获取资源的所有皮肤
SkinnedMeshRenderer[] parts = source.GetComponentsInChildren<SkinnedMeshRenderer>();
// 初始化data,将资源添加到Dictionary容器中
foreach (SkinnedMeshRenderer part in parts)
{
string[] partName = part.name.Split('-');
if (!data.ContainsKey(partName[0]))
{
data.Add(partName[0], new Dictionary<string, Transform>());
// 初始化targetSmr,添加骨架上的皮肤类,但当前的皮肤类内容是空的
GameObject partObj = new GameObject();
partObj.name = partName[0];
partObj.transform.parent = target;
partObj.transform.localPosition = part.transform.localPosition;
partObj.transform.localRotation = part.transform.localRotation;
targetSmr.Add(partName[0], partObj.AddComponent<SkinnedMeshRenderer>());
}
data[partName[0]].Add(partName[1], part.transform);
}
// 初始化hips,获取所有的骨骼,在Unity已经添加
hips = target.GetComponentsInChildren<Transform>();
// 初始化皮肤
int length = avatarStr.GetLength(0);
for (int i = 0; i < length; ++i)
{
changeMesh(avatarStr[i, 0], avatarStr[i, 1]);
}
}
// 改变部件
public void changeMesh(string part, string item)
{
SkinnedMeshRenderer smr = data[part][item].GetComponent<SkinnedMeshRenderer>(); //获取当前要替换的皮肤,这是源
// 获取target上与source对应的骨骼,这边千万不能直接把骨骼赋值进去了
List<Transform> bones = new List<Transform>();
foreach (Transform bone in smr.bones)
{
foreach (Transform hip in hips)
{
if (hip.name != bone.name)
{
continue;
}
bones.Add(hip);
break;
}
}
// 这边是目标,进行替换
targetSmr[part].sharedMesh = smr.sharedMesh; //替换皮肤
targetSmr[part].bones = bones.ToArray(); //替换骨骼
targetSmr[part].materials = smr.materials; //替换材质
}
看到自己参与的项目中换装直接就是更换Prefab,Prefab中自带mesh和骨骼,简单粗暴有点费。