SkinnedMesh原理及一些应用

来源:互联网 发布:马上6是什么软件 编辑:程序博客网 时间:2024/05/22 00:20

人类在运动的时候,实际是骨骼在不断变换位置,然后骨骼带动全身皮肉在运动。在游戏中就反映为骨骼节点带动Mesh中的Vertices在运动,进而播放Mesh动画。这里以Unity工程为工具,来理解一下骨骼和蒙皮的过程。

骨骼

骨骼决定了模型整体在世界坐标系中的位置和朝向,和人类骨骼一样,你的拇指关节移动,你拇指附近的肌肉也会跟着移动。骨骼是有着层次结构的:
LeftRight
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中的坐标。(以某个骨骼为原点的坐标系)

BoneOffsetMatrix=bone.worldtolocalmatrixrender.localtoworldmatrix

先从mesh的local空间转换到世界空间,再从世界空间转换到骨骼空间。
BoneOffsetMatrixVerticeslocal=Verticesbone

Verticesbone是Vertices在BoneSpace的坐标。在美术把骨骼,body节点放好,BoneOffsetMatrix,Verticeslocal,Verticesbone的值都已经确定了。根据这个公式看前面旋转Body的操作。旋转body改变了render.localtoworldmatrix进行改变BoneOffsetMatrix,此时引擎改变Verticeslocal值来保证他两乘积不变。
2.根据Vertices在BoneSpace中的坐标计算出世界坐标
只需要将BoneSpace中的局部坐标乘以一个bone.localtoworldmatrix即可。unity中支持最多四根骨骼来控制一个Vertice,每根骨骼根据Vertice在BoneSpace中的坐标算出一个Vertice的世界坐标,然后加权平均。
4i=1bone[i].localtoworldmatrixVerticesboneboneweight[i]4

蒙皮

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;    // 源模型,所有的可以替换的部件都在这上面// 模型资源,对应上面的sourceDictionary<string, Dictionary<string, Transform>> data = new Dictionary<string, Dictionary<string, Transform>>();// 目标骨架,对应上面的targetTransform[] 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 initializationvoid 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和骨骼,简单粗暴有点费。

原创粉丝点击