游戏蛮牛学习群(纯技术交流,不闲聊):539178957
游戏蛮牛 手机端
开启辅助访问
 找回密码
 注册帐号

扫一扫,访问微社区

首页   >   博客   >   LmlNet

Unity捏人系统(一)

个人分类: 分享 | 2018-6-3 21:43
标签:分享
我在这里写几篇关于用Unity修改人物模型的博客

第一篇是自定义捏人

后面会有动态换装。

先上一张效果图



首先讲一下Mesh的基本知识

vertices 是顶点

triangles 是三角面,它的数据是顶点的数组索引

3D模型的网格基本上都由于三角形构成的

uv ,对应每一个顶点,是三角形对应到贴图上的坐标,是3维空间到2维空间的对应,一般的贴图就是那张皮肤图啦,一般模型还会有法线贴图和光照贴图,这时候两张图对应的坐

标可能不一样,所以uv可以有4套

分别是 uv,uv1,uv2,uv3。uv最多4套,想要更多要想别的方法了

normals 是法线数据,对应每一个顶点

bindposes 绑定姿势。对应每一个骨骼,是一个变换矩阵。

boneWeights 每个顶点的骨骼权重,和uv一样,一个顶点只能绑定4个骨骼。

BoneWeight 的 weight0、 weight1、 weight2、 weight3表示权重值,在0到1直接,同时他们4个相加的和也是1。(虽然你可以通过程序强行弄成超过1的值)

这些权重值对应boneIndex0、boneIndex1、boneIndex2、boneIndex3。这些个Int是指骨骼的数组索引。

我现在给出顶点动画在播放的时候,顶点和骨骼的对应关系

具体可以参阅《游戏引擎架构 》第11章节



我这边就不写数学公式,直接给出代码了,不过有一点萌新要注意:矩阵是一个变化的过程而不是某个具体的状态,就是说一个物体的坐标、旋转、缩放是v、r、s 矩阵表示的不是

v、r、s的值,而是这些值即将做的一个变化,通过矩阵M1,v、r、s 可以变成v1、r1、s1,但矩阵M1本身不代表Transform数据。

boneTransform0 是指第一个骨骼,postion是指顶点坐标,我们计算出v0是顶点在骨骼方向上和骨骼的距离

 
[code]csharpcode:v0 = Quaternion.Inverse(boneTransform0.rotation) * (boneTransform0.position - position) ;


这只是一个骨骼的数据,如果一个BoneWeight对应多个骨骼就如法炮制
[code]csharpcode: v1 = Quaternion.Inverse(boneTransform1.rotation) * (boneTransform1.position - position); v2 = Quaternion.Inverse(boneTransform2.rotation) * (boneTransform2.position - position); v3 = Quaternion.Inverse(boneTransform3.rotation) * (boneTransform3.position - position);

然后就可以计算出顶点在运动时候的坐标

 
[code]csharpcode: Vector3 position = new Vector3(); if (boneTransform0 != null) { position += (boneTransform0.position + boneTransform0.rotation * -v0) * weight0; } if (boneTransform1 != null) { position += (boneTransform1.position + boneTransform1.rotation * -v1) * weight1; } if (boneTransform2 != null) { position += (boneTransform2.position + boneTransform2.rotation * -v2) * weight2; } if (boneTransform3 != null) { position += (boneTransform3.position + boneTransform3.rotation * -v3) * weight3; } this.transform.position = position - rootBone.position;


顶点的位置是由参数weight的加权均值,所以直接相乘即可

我买了一个妹子的模型(胸部已经和谐),下面是上面代码的运动结果


左侧是模型,中间是顶点,右边是骨骼

现在我们来讲如何捏人

先创建一个小球模型用来显示我们的顶点位置

[code]csharpcode:

/// <summary>
    /// 创建随心所欲大小的小球模型
    /// </summary>
    /// <param name="size"></param>
    /// <returns></returns>
    public Mesh CreateNewMeshSphere(float size)
    {
        Mesh newMeshSphere = new Mesh();
        GameObject gGameObjectSphereBone = GameObject.CreatePrimitive(PrimitiveType.Sphere);
        Mesh systemMeshSphere = gGameObjectSphereBone.GetComponent<MeshFilter>().sharedMesh;
        newMeshSphere.vertices = systemMeshSphere.vertices;
        newMeshSphere.triangles = systemMeshSphere.triangles;
        newMeshSphere.normals = systemMeshSphere.normals;
        newMeshSphere.uv = systemMeshSphere.uv;
        newMeshSphere.uv2 = systemMeshSphere.uv2;
        newMeshSphere.colors = systemMeshSphere.colors;

        var meshSphereV = systemMeshSphere.vertices;
        for (int i = 0; i < meshSphereV.Length; i++)
        {
            meshSphereV[i] = meshSphereV[i] * size;
        }
        newMeshSphere.vertices = meshSphereV;
        DestroyImmediate(gGameObjectSphereBone);
        return newMeshSphere;
    }

下面是我们显示的顶点小球的代码,就是上图的中间一堆组成网格的蓝色小球的组件代码
[code]csharpcode:using System.Collections; using System.Collections.Generic; using UnityEngine; /// <summary> /// 显示的顶点小球 /// </summary> public class UnitVertice : MonoBehaviour { //权重 public float weight0 = 1f; public float weight1 = 1f; public float weight2 = 1f; public float weight3 = 1f; //骨骼Transform public Transform boneTransform0; public Transform boneTransform1; public Transform boneTransform2; public Transform boneTransform3; /// <summary> /// 初始顶点位置 /// </summary> public Vector3 vertices; public Vector3 normals; //这些是骨骼初始位置 public Vector3 v0; public Vector3 v1; public Vector3 v2; public Vector3 v3; /// <summary> /// 骨骼根节点 /// </summary> public Transform rootBone; /// <summary> /// 顶点的索引 /// </summary> public int index = 0; /// <summary> /// 初始化数据 /// </summary> public void Show ( Transform rootBone, Vector3 position, Vector3 normals, Transform boneTransform0, Transform boneTransform1, Transform boneTransform2, Transform boneTransform3, float weight0, float weight1, float weight2, float weight3, Quaternion rotation ) { this.rootBone = rootBone; this.transform.localPosition = position; vertices = position; this.normals = normals; if (boneTransform0 != null) { this.boneTransform0 = boneTransform0; this.weight0 = weight0; v0 = Quaternion.Inverse(boneTransform0.rotation) * (boneTransform0.position - position) ; } if (boneTransform1 != null) { this.boneTransform1 = boneTransform1; this.weight1 = weight1; v1 = Quaternion.Inverse(boneTransform1.rotation) * (boneTransform1.position - position); } if (boneTransform2 != null) { this.boneTransform2 = boneTransform2; this.weight2 = weight2; v2 = Quaternion.Inverse(boneTransform2.rotation) * (boneTransform2.position - position); } if (boneTransform3 != null) { this.boneTransform3 = boneTransform3; this.weight3 = weight3; v3 = Quaternion.Inverse(boneTransform3.rotation) * (boneTransform3.position - position); } } void Update () { Vector3 position = new Vector3(); if (boneTransform0 != null) { position += (boneTransform0.position + boneTransform0.rotation * -v0) * weight0; } if (boneTransform1 != null) { position += (boneTransform1.position + boneTransform1.rotation * -v1) * weight1; } if (boneTransform2 != null) { position += (boneTransform2.position + boneTransform2.rotation * -v2) * weight2; } if (boneTransform3 != null) { position += (boneTransform3.position + boneTransform3.rotation * -v3) * weight3; } this.transform.position = position - rootBone.position; } }



上图右边的绿色小球组成的是骨骼集合

我们来创建每一个顶点(蓝色)的小球和骨骼(绿色)的小球

[code]csharpcode: /// <summary> /// 创建所有的 顶点小球 /// </summary> public void CreateAllUnitVertice() { Mesh sharedMesh = skinnedMeshRenderer.sharedMesh; sharedMesh.RecalculateBounds(); var vertices = sharedMesh.vertices; var uv = sharedMesh.uv; var triangles = sharedMesh.triangles; var normals = sharedMesh.normals; var boneWeights = sharedMesh.boneWeights; var bones = skinnedMeshRenderer.bones; if (boneRoot) { DestroyImmediate(boneRoot.gameObject); } boneRoot = new GameObject("Bone Root").transform; Dictionary<int, Transform[]> allTransform = new Dictionary<int, Transform[]>(); //用这个找出所有无关紧要的骨骼点 Dictionary<int, Transform> allBones = new Dictionary<int, Transform>(); //所有用到的骨骼点 int[] allTrueBones = new int[bones.Length]; for (int i = 0; i < bones.Length; i++) { var item = bones[i]; Transform[] transforms = new Transform[2]; allTransform[item.GetHashCode()] = transforms; transforms[0] = item; transforms[1] = new GameObject(item.name).transform; transforms[1].gameObject.AddComponent<UnitBone>().lookBone = transforms[0]; allBones[i] = transforms[1]; } foreach (var item in allTransform) { Transform[] transforms = item.Value; int id = item.Key; if (transforms[0].parent) { int parentId = transforms[0].parent.GetHashCode(); if (allTransform.ContainsKey(parentId)) { transforms[1].parent = allTransform[parentId][1]; transforms[1].localPosition = transforms[0].localPosition; transforms[1].localRotation = transforms[0].localRotation; transforms[1].localScale = transforms[0].localScale; } else { transforms[1].parent = boneRoot; transforms[1].position = transforms[0].position; transforms[1].rotation = transforms[0].rotation; transforms[1].localScale = transforms[0].localScale; } } else { transforms[1].parent = boneRoot; transforms[1].position = transforms[0].position; transforms[1].rotation = transforms[0].rotation; transforms[1].localScale = transforms[0].localScale; } } foreach (var b in boneWeights) { if (b.weight0 != 0) { allTrueBones[b.boneIndex0]++; } if (b.weight1 != 0) { allTrueBones[b.boneIndex1]++; } if (b.weight2 != 0) { allTrueBones[b.boneIndex2]++; } if (b.weight3 != 0) { allTrueBones[b.boneIndex3]++; } } for (int i = 0; i < allTrueBones.Length; i++) { if (allTrueBones[i] == 0) { //GameObject.Destroy(allBones[i].gameObject);//移除不需要的骨骼 allBones.Remove(i);//移除不需要的骨骼 } } //显示骨骼 Material standard = new Material(Shader.Find("Standard")); standard.color = Color.green; foreach (Transform transformItem in allBones.Values) { transformItem.gameObject.AddComponent<MeshRenderer>().material = standard; transformItem.gameObject.AddComponent<MeshFilter>().mesh = meshSphere; } //显示顶点的小球 Material standardVertice = new Material(Shader.Find("Standard")); standardVertice.color = Color.blue; if (allUnitVerticeGameObject) { DestroyImmediate(allUnitVerticeGameObject); } //显示顶点 allUnitVerticeGameObject = new GameObject("All Unit Vertice"); allUnitVertices = new UnitVertice[vertices.Length]; for (int i = 0; i < vertices.Length; i++) { GameObject unitVerticeGameObject = new GameObject("" + i); unitVerticeGameObject.transform.parent = allUnitVerticeGameObject.transform; UnitVertice unitVertice = unitVerticeGameObject.AddComponent<UnitVertice>(); unitVertice.index = i; allUnitVertices[i] = unitVertice; BoneWeight boneWeigh = boneWeights[i]; Transform boneTransform0 = null, boneTransform1 = null, boneTransform2 = null, boneTransform3 = null; float weight0 = 0, weight1 = 0, weight2 = 0, weight3 = 0; if (boneWeigh.weight0 != 0) { weight0 = boneWeigh.weight0; boneTransform0 = allBones[boneWeigh.boneIndex0]; } if (boneWeigh.weight1 != 0) { weight1 = boneWeigh.weight1; boneTransform1 = allBones[boneWeigh.boneIndex1]; } if (boneWeigh.weight2 != 0) { weight2 = boneWeigh.weight2; boneTransform2 = allBones[boneWeigh.boneIndex2]; } if (boneWeigh.weight3 != 0) { weight3 = boneWeigh.weight3; boneTransform3 = allBones[boneWeigh.boneIndex3]; } unitVertice.Show(boneRoot, vertices[i], normals[i], boneTransform0, boneTransform1, boneTransform2, boneTransform3 , weight0, weight1, weight2, weight3 , skinnedMeshRenderer.transform.rotation); allUnitVertices[i] = unitVertice; //设置材质和网格 unitVerticeGameObject.AddComponent<MeshRenderer>().material = standardVertice; unitVerticeGameObject.AddComponent<MeshFilter>().mesh = sphereVerticesMesh; } }

结果如上图

这时候,我们要选择一些顶点作为我们变换的目标

我们先做一个ScriptableObject来保存数据

[code]csharpcode:

using UnityEngine;
using System.Collections.Generic;
public class MouldedSettingScriptableObject : ScriptableObject
{
    [UnityEngine.SerializeField]
    /// <summary>
    /// 所有的部位
    /// </summary>
    public List<MouldedPart> allPart = new List<MouldedPart>();
}

[System.Serializable]
public class MouldedPart
{
    /// <summary>
    /// 变换威力
    /// </summary>
    [Range(-0.2f, 0.2f)]
    public float changePower;

    /// <summary>
    /// 名字
    /// </summary>
    public string name = "";

    /// <summary>
    /// 变换曲线
    /// </summary>
    public Curve curves;

    /// <summary>
    /// 顶点集合
    /// </summary>
    public List<int> vertices;

    /// <summary>
    /// 变换类型
    /// </summary>
    public ChangeType changeType;

    public float maxY;
    public float minY;

    public float maxX;
    public float minX;

    /// <summary>
    /// 变换类型
    /// </summary>
    public enum ChangeType
    {
        /// <summary>
        /// Y轴向变换
        /// </summary>
        DirectionY,
        /// <summary>
        /// X轴向变换
        /// </summary>
        DirectionX,
        /// <summary>
        /// 直接法线变换
        /// </summary>
        Normals
    }
}
我们把选择的顶点保存

[code]csharpcode:

  List<int> indexs = new List<int>();
                foreach (var item in Selection.gameObjects)
                {
                    UnitVertice vertice = item.GetComponent<UnitVertice>();
                    if (vertice)
                    {
                        indexs.Add(vertice.index);
                    }
                }
                mouldedSetting.allPart[mouldedSettingManage.nowSlelectIndex].vertices = indexs;
                //保存顶点的最大最小值
                mouldedSettingManage.SaveAllVerticeMaxMin();
unity里面的蒙皮组件叫做 SkinnedMeshRenderer

我们需要给定一个新的Mesh

[code]csharpcode:

  newMesh = new Mesh();
        newMesh.vertices = sharedMesh.vertices;
        newMesh.triangles = sharedMesh.triangles;
        newMesh.uv = sharedMesh.uv;
        newMesh.normals = sharedMesh.normals;
        newMesh.bindposes = sharedMesh.bindposes;
        newMesh.boneWeights = sharedMesh.boneWeights;
        newMesh.colors = sharedMesh.colors;
        //这里要新建一个拷贝的网格,如果直接修改原来的sharedMesh就会直接影响资源
        skinnedMeshRenderer.sharedMesh = newMesh;

        normals = sharedMesh.normals;
        vertices = sharedMesh.vertices;
        nowVertices = new Vector3[vertices.Length];
        vertices.CopyTo(nowVertices, 0);

        foreach (var item in mouldedSetting.allPart)
        {
            item.changePower = 0;
        }
然后我们在模型的法线方向上延展顶点坐标,但是我发现直接的法线方向有些地方不柔和,所以就将某个轴向上的值忽略,并且通过一个曲线去调整整个效果,使模型更加柔和。

[code]csharpcode:

  /// <summary>
    /// 对Y轴上变换
    /// </summary>
    /// <param name="nowMouldedPart"></param>
    void UpdateDirectionY(MouldedPart nowMouldedPart)
    {
        foreach (var i in nowMouldedPart.vertices)
        {
            nowVertices[i] = vertices[i] + (new Vector3(normals[i].x, 0, normals[i].z)
           * nowMouldedPart.changePower
           )
           *
               nowMouldedPart.curves.Evaluate((vertices[i].y - nowMouldedPart.minY) / (nowMouldedPart.maxY - nowMouldedPart.minY));
        }
    }
对应Y轴向上的变换的的部件可以是腰部或者腿部

[code]csharpcode:

 /// <summary>
    /// 直接变换
    /// </summary>
    /// <param name="nowMouldedPart"></param>
    void UpdateDirectionNormals(MouldedPart nowMouldedPart)
    {
        foreach (var i in nowMouldedPart.vertices)
        {
            nowVertices[i] = vertices[i] + (normals[i]
           * nowMouldedPart.changePower
           )
           *
               nowMouldedPart.curves.Evaluate((vertices[i].y - nowMouldedPart.minY) / (nowMouldedPart.maxY - nowMouldedPart.minY));
        }
    }
直接的法线变换可以用在胸部上

X轴向上的变换的部件可以用到手臂上。

废话不多说具体工程放在

https://share.weiyun.com/5oCWUOp

直接去下载吧,各位老铁!

后面会写如何将变形了的模型 匹配衣服,并且将未绑定骨骼的衣服模型 自动匹配计算骨骼权重。

0 0

评论 (0 个评论)

facelist doodle 涂鸦板

您需要登录后才可以评论 登录 | 注册帐号
关闭

站长推荐 上一条 /1 下一条

返回顶部