SkinnedMesh原理及一些应用

人类在运动的时候,实际是骨骼在不断变换位置,然后骨骼带动全身皮肉在运动。在游戏中就反映为骨骼节点带动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;    // 源模型,所有的可以替换的部件都在这上面


// 模型资源,对应上面的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和骨骼,简单粗暴有点费。

  • 10
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SkinnedMesh的实现原理是通过将Mesh中的顶点与骨骼进行绑定来实现动画效果。SkinnedMesh由两个部分组成:骨骼(Bone)和蒙皮(Skinned Mesh)\[3\]。 在SkinnedMesh中,Mesh是一个整体,只有一个Mesh,而不像关节动画中使用多个分散的Mesh\[3\]。为了让骨骼决定顶点的世界坐标,需要将顶点和骨骼联系起来,这就是通过添加蒙皮信息(Skin info)来实现的\[2\]。 蒙皮是指将Mesh中的顶点附着(绑定)在骨骼之上,每个顶点可以被多个骨骼所控制\[3\]。顶点的蒙皮数据包括顶点受哪些骨骼影响以及这些骨骼对该顶点的权重\[3\]。此外,每块骨骼还需要骨骼偏移矩阵(BoneOffsetMatrix)用来将顶点从Mesh空间变换到骨骼空间\[3\]。 在动画中,每个关键帧包含时间和骨骼的运动信息,可以用矩阵或四元数来表示骨骼的变换\[3\]。这些运动信息可以是预先编辑好的动画帧数据,也可以是通过物理计算实时控制骨骼的运动\[3\]。 通过将顶点与骨骼绑定并根据骨骼的运动信息进行变换,SkinnedMesh实现了模型的动画效果,并消除了在关节处产生裂缝的问题\[1\]。 综上所述,SkinnedMesh的实现原理是通过将Mesh中的顶点与骨骼进行绑定,并根据骨骼的运动信息对顶点进行变换,从而实现模型的动画效果。 #### 引用[.reference_title] - *1* *2* *3* [Skinned Mesh原理解析和一个最简单的实现示例](https://blog.csdn.net/n5/article/details/3105872)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值