开启辅助访问
 找回密码
 注册帐号

扫一扫,访问微社区

开发者专栏

关注:2212

当前位置:游戏蛮牛 技术专区 开发者专栏

__________________________________________________________________________________
开发者干货区版块规则:

  1、文章必须是图文形式。(至少2幅图)
      2、文章字数必须保持在1500字节以上。(编辑器右下角有字数检查)
      3、本版块只支持在游戏蛮牛原创首发,不支持转载。
      4、本版块回复不得无意义,如:顶、呵呵、不错......【真的会扣分的哦】
      5、......
__________________________________________________________________________________
查看: 3900|回复: 59

[士郎] 深度干货——捏脸系统如何实现?

[复制链接]  [移动端链接]
排名
3
昨日变化

6378

主题

6892

帖子

2万

积分

Rank: 16

UID
1231
好友
185
蛮牛币
2775
威望
30
注册时间
2013-7-29
在线时间
3267 小时
最后登录
2018-5-25

社区QQ达人活力之星原创精华达人突出贡献奖财富之证游戏蛮牛QQ群会员蛮牛妹VIP

发表于 2018-2-11 17:27:01 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?注册帐号

x
捏脸有两种做法,一种是利用骨骼变换,一种是利用Blend Shape。

1.jpg


骨骼法是在脸的内部正常建立骨骼并蒙皮,并利用骨骼的缩放系数,位移来改变脸的外形。但由于是数学计算的结果,设计时难以直接对最终效果进行调整。
BlendShape则是正常做多个种类的脸型并离线出差异数据,设计的时候比较简单,但叠加的时候也不太好控制,而且数据量较大。


由于现在游戏都需要利用BlendShape来实现人物的表情,重叠的BlendShape不易处理。而骨骼法只是制作上麻烦点,但可控性较高,而且数据量也小,所以大部分游戏都是用骨骼来实现捏脸的。


骨骼的设计是唯一的难点
具体怎么布置骨骼可参考上图,其实并不需要太多的控制项,更细节的可动部分(比如口型和眼睛)是交给BlendShape完成的,骨骼仅仅用来布置捏脸需要变化的部分。


通常会在正常的骨骼上加上一个Position(0,0,0),Scale(1,1,1)的节点,然后让它代替父节点进行蒙皮,并把它作为捏脸时的可变数据。这样数值是单位化的,捏脸时容易处理。不希望缩放参数影响到子节点的时候也可以这样做。


2.jpg

这样直接改Bone的Transform数值就能看到变化了。

相比脸型,眼睛需要的节点数更多,因为需要针对眼球等多个物体处理,眼角长度和上挑都要可控,是捏脸骨骼最复杂的部分。

让捏脸变得人性化

虽然提供全部可变骨骼就能做出任何一种脸型,但那样只会让玩家轻易捏出怪物来。

3.jpg


通常的做法是给出一组可调整值的数值,对应骨骼的一个或者多个单位数值,让玩家在0-1之间选择。


4.jpg
至于这些调整数值背后的逻辑——


我选择让一个调整项同时影响多个骨骼数据,每个骨骼数据只影响某条骨骼的某个值,并且可以指定动画曲线,以应对弧面上的位移。

5.jpg



通过组合就可以应对各种情况(眼睛这些地方必然需要影响多个骨骼)。

将Value值调整到某个极值,然后修改Min,Max查看效果,修改Curve曲线处理中间状况,生成捏脸数据的过程还是比较直观的。设计好之后,将序列化的数据存储起来,以后要用的时候载入并只允许修改Value的值,就可以应用在游戏的捏脸部分了。


而且这个编辑器的实现也是非常简单的,利用Unity自己的数据序列化面板就可以了。

[AppleScript] 纯文本查看 复制代码
 public enum ModifyType
    {
        ScaleX,
        ScaleY,
        ScaleZ,
        X,
        Y,
        Z
    }
    [System.Serializable]
    public class ModifyDataGroup
    {
        public string name;
        [Range(0,1)]
        public float value = 0.5f;
        public ModifyData[] modifys;
    }

    [System.Serializable]
    public class ModifyData
    {
        public string name;
        public ModifyType type;
        public float min;
        public float max;
        public AnimationCurve curve;
    }
    public ModifyDataGroup[] modifyDataGroup;


去掉捏脸用的无用数据
很显然,我们为了实现捏脸加了许多没用的骨骼数据,这些都会影响到游戏运行时的效能。虽然量并不是很大,但要移除也并不困难。
如果不考虑BlendShape,其实只需要调用Unity的BakeMesh方法,替换Mesh并删掉骨骼就可以了。


但如果有BlendShape,Bake后的Mesh数据应用BlendShape后,会和以前有微量的偏移。这是因为BlendShape是在骨骼变化前先应用的,而Bake后,相当于是在骨骼变化后应用。

6.jpg

两者的差异和骨骼的变化强度有关,也和BlendShape的改变幅度有关,但确实在通常情况下差异不大(第三幅图是前两幅图的叠加,可以看到在脸部表情影响到的部分产生了微量的偏移)

但是即使少也是存在的。

解决方法是先将每一个BlendShape分别应用到Mesh上,再进行骨骼变换,蒙皮生成网格数据,然后再减掉Bake后的Mesh,重新生成一个新的BlendShape。
这其实相当于重做了一次BlendShape的生成过程(两个模型间求差值),数据就是完全准确的了。

下面的代码除了修正BlendShape外,还有蒙皮网格烘焙的代码实现。

[AppleScript] 纯文本查看 复制代码
 public static Mesh BakeMesh(SkinnedMeshRenderer source)
    {
        Mesh target = Object.Instantiate(source.sharedMesh);
        int vertexCount = source.sharedMesh.vertexCount;
        
        Bounds bounds = source.sharedMesh.bounds;
        BoneWeight[] boneWeights = source.sharedMesh.boneWeights;
        Vector3[] vertices = source.sharedMesh.vertices;
        Vector3[] normals = source.sharedMesh.normals;
        Vector4[] tangents = source.sharedMesh.tangents;
        Vector3[] newVertices = new Vector3[vertexCount];
        Vector3[] newNormals = new Vector3[vertexCount];
        Vector4[] newTangents = new Vector4[vertexCount];
        Matrix4x4[] bindposes = source.sharedMesh.bindposes;
        Transform[] bones = source.bones;
        
        //Bake SkinMesh
        int count = bones.Length;
        Matrix4x4[] boneMatrixs = new Matrix4x4[count];
        for (int i = 0; i < count; i++)
        {
            boneMatrixs[i] = source.rootBone.worldToLocalMatrix * bones[i].localToWorldMatrix * bindposes[i];
        }
        for (int i = 0; i < vertexCount; i++)
        {
            ApplyBoneMatrix(boneWeights[i], boneMatrixs, vertices[i], normals[i], tangents[i], out newVertices[i], out newNormals[i], out newTangents[i]);
        }
        target.vertices = newVertices;
        target.normals = newNormals;
        target.tangents = newTangents;
        target.boneWeights = null;
        target.bounds = bounds;

        //修正BlendShape
        target.ClearBlendShapes();
        count = source.sharedMesh.blendShapeCount;
        for (int i = 0; i < count; i++)
        {
            string name = source.sharedMesh.GetBlendShapeName(i);
            int frameCount = source.sharedMesh.GetBlendShapeFrameCount(i);
            Vector3[] deltaVertices = new Vector3[vertexCount];
            Vector3[] deltaNormals = new Vector3[vertexCount];
            Vector3[] deltaTangents = new Vector3[vertexCount];
            for (int j = 0; j < frameCount; j++)
            {
                source.sharedMesh.GetBlendShapeFrameVertices(i, j, deltaVertices, deltaNormals, deltaTangents);
                for (int r = 0; r < vertexCount;r++)
                {
                    Vector3 shapeVector;
                    Vector3 shapeNormal;
                    Vector4 shapeTangent;
                    ApplyBoneMatrix(boneWeights[i], boneMatrixs, vertices[i] + deltaVertices[i], normals[i] + deltaNormals[i], tangents[i] + (Vector4)deltaTangents[i], out shapeVector, out shapeNormal, out shapeTangent);
                    deltaVertices[i] = shapeVector - newVertices[i];
                    deltaNormals[i] = shapeNormal - newNormals[i];
                    deltaTangents[i] = shapeTangent - newTangents[i];
                }
                float weight = source.sharedMesh.GetBlendShapeFrameWeight(i, j);
                target.AddBlendShapeFrame(name, weight, deltaVertices, deltaNormals, deltaTangents);
            }
        }

        return target;
    }

    //对每个顶点蒙皮
    private static void ApplyBoneMatrix(BoneWeight bw, Matrix4x4[] boneMatrixs, Vector3 vector, Vector3 normal, Vector4 tangent, out Vector3 newVector, out Vector3 newNormal, out Vector4 newTangent)
    {
        Vector3 resultVector = new Vector3();
        Vector3 resultNormal = new Vector3();
        Vector3 resultTangent = new Vector3();
        if (bw.weight0 > 0)
        {
            resultVector += boneMatrixs[bw.boneIndex0].MultiplyPoint3x4(vector) * bw.weight0;
            resultNormal += boneMatrixs[bw.boneIndex0].MultiplyVector(normal) * bw.weight0;
            resultTangent += boneMatrixs[bw.boneIndex0].MultiplyVector(tangent) * bw.weight0;
        }
        if (bw.weight1 > 0)
        {
            resultVector += boneMatrixs[bw.boneIndex1].MultiplyPoint3x4(vector) * bw.weight1;
            resultNormal += boneMatrixs[bw.boneIndex1].MultiplyVector(normal) * bw.weight1;
            resultTangent += boneMatrixs[bw.boneIndex1].MultiplyVector(tangent) * bw.weight1;
        }
        if (bw.weight2 > 0)
        {
            resultVector += boneMatrixs[bw.boneIndex2].MultiplyPoint3x4(vector) * bw.weight2;
            resultNormal += boneMatrixs[bw.boneIndex2].MultiplyVector(normal) * bw.weight2;
            resultTangent += boneMatrixs[bw.boneIndex2].MultiplyVector(tangent) * bw.weight2;
        }
        if (bw.weight3 > 0)
        {
            resultVector += boneMatrixs[bw.boneIndex3].MultiplyPoint3x4(vector) * bw.weight3;
            resultNormal += boneMatrixs[bw.boneIndex3].MultiplyVector(normal) * bw.weight3;
            resultTangent += boneMatrixs[bw.boneIndex3].MultiplyVector(tangent) * bw.weight3;
        }
        newVector = resultVector;
        newNormal = resultNormal;
        newTangent = new Vector4(resultTangent.x, resultTangent.y, resultTangent.z, tangent.w);
    }


只烘培部分骨骼

上面的是将整个头部烘培成Mesh,但有时候我们也会想保留部分骨骼(例如头发)供捏脸之外的部分使用。而如果是体型部分的自定义,就更需要仅对部分骨骼烘培的功能。

虽然看上去骨骼的多层级和多权重难以下手,实际上也没有多困难。
——只要把不需要烘培的骨骼重置回未经蒙皮变换时的状态,也就是将那些骨骼的Transform设置为对应bindposes的逆,它们就不会对需要烘培的骨骼造成影响。

[AppleScript] 纯文本查看 复制代码
Matrix4x4 m = rootBone.worldToLocalMatrix * bindposes.inverse;
SetTransformMatrix(bone, m);


bindposes本身就是Mesh空间相对于骨骼结点空间的变换矩阵……的逆,Mesh空间的顶点乘bindposes相当于将自己转换到骨骼结点所在空间,所以再做一次“骨骼 -> 世界空间变换”就能完成蒙皮,这也是上面的蒙皮代码实现的原理,实在不懂可自行查阅蒙皮相关资料。


我这里再求一次bindposes的逆,就得到了骨骼结点相对于Mesh空间的矩阵,实际上就是骨骼最开始所在的位置。将骨骼移动回这个位置,就相当于这个骨骼没有参与蒙皮,也就不会影响到其他需要蒙皮的骨骼。



设置Transform,而非直接跳过蒙皮阶段,是为了让需要烘培的结点的父结点位置正确。比起重新计算需要烘培的结点的Matrix4x4,这种方式会简单一点(但是性能确实差一些)
之后走正常的BakeMesh流程就可以了。

完成后还需要单独移除已经进行烘培,实际上已经无效的骨骼的boneWeight。所有boneWeight里涉及到那些骨骼的地方,都需要重置为骨骼的根节点RootBone的序号,也就是0。

[AppleScript] 纯文本查看 复制代码
for (int i = 0; i < vertexCount; i++)
{
       BoneWeight bw = boneWeights[i];
       bw.boneIndex0 = boneIndexFilter.Contains(bw.boneIndex0) ? 0 : bw.boneIndex0;
       bw.boneIndex1 = boneIndexFilter.Contains(bw.boneIndex1) ? 0 : bw.boneIndex1;
       bw.boneIndex2 = boneIndexFilter.Contains(bw.boneIndex2) ? 0 : bw.boneIndex2;
       bw.boneIndex3 = boneIndexFilter.Contains(bw.boneIndex3) ? 0 : bw.boneIndex3;
       boneWeights[i] = bw;
}


对于SkinMesh来讲,并不存在删除骨骼结点一说,不管怎么样都会至少做一次相对于根结点的骨骼变换。把所有数据都重置为0是不行的,至少要有留下一个{boneIndex0 : 0, weight0 : 1f}。
烘培完后还需要将骨骼的状态再次恢复。

不过如果是游戏运行时,其实并不需要专门重置骨骼,因为骨骼刚加载出来的时候本来就是重置好的。只要先加载捏脸的骨骼数据,烘培,修改BoneWeight,再加载其他和骨骼相关的部分,烘培和非烘培就不会有冲突了。

然而模型从哪里弄?

捏脸的技术难度其实就这些(烘培那段估计大部分人也不用),主要的难点还是骨骼和捏脸数据的设计。

我这个模型是从HoneySelect里扒的,它的模型数据全在abdata这个文件夹下以AssetBoundle形式存在,所以很简单就能扒出来。

但UnityStudio这些破解工具导出的文件迄今为止依然会丢失BlendShape,所以需要用UnityAPI直接读取ab文件并另存为。

HoneySelect其实也有捏脸参数的配置文件,有兴趣可以自己扒过来以节约配置参数的时间(这个恐怕才是工作量的大头)



知乎@flashyiyi



跟我念“站长妹纸萌萌哒!”我说站长,你说YO!爱你们么么哒~
回复

使用道具 举报

5熟悉之中
740/1000
排名
3516
昨日变化
2

0

主题

184

帖子

740

积分

Rank: 5Rank: 5

UID
135463
好友
0
蛮牛币
147
威望
0
注册时间
2016-1-23
在线时间
188 小时
最后登录
2018-5-26
发表于 2018-2-11 20:21:29 | 显示全部楼层
uiouiouioiuoiuoiuoiu

回复 支持 反对

使用道具 举报

5熟悉之中
672/1000
排名
3165
昨日变化
19

0

主题

52

帖子

672

积分

Rank: 5Rank: 5

UID
226710
好友
0
蛮牛币
1298
威望
0
注册时间
2017-6-14
在线时间
214 小时
最后登录
2018-5-25
发表于 2018-2-11 20:24:08 | 显示全部楼层
一直想找这方面资料学习,但老忘。谢谢站长

回复 支持 反对

使用道具 举报

排名
24550
昨日变化
9

0

主题

21

帖子

53

积分

Rank: 2Rank: 2

UID
229952
好友
0
蛮牛币
112
威望
0
注册时间
2017-7-3
在线时间
19 小时
最后登录
2018-5-19
发表于 2018-2-11 21:35:59 | 显示全部楼层
今天玩游戏呢,突然想这个捏脸怎么回事,没想到晚上就看见这个帖子!原来缘分这个东西真的很扯淡。哈哈哈

回复 支持 反对

使用道具 举报

7日久生情
2533/5000
排名
3334
昨日变化
2

2

主题

1780

帖子

2533

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
241666
好友
0
蛮牛币
10193
威望
0
注册时间
2017-9-6
在线时间
360 小时
最后登录
2018-5-23
发表于 2018-2-12 07:31:32 来自Mobile--- | 显示全部楼层
感谢分享

回复

使用道具 举报

6蛮牛粉丝
1426/1500
排名
6228
昨日变化
33

0

主题

1059

帖子

1426

积分

Rank: 6Rank: 6Rank: 6

UID
267381
好友
3
蛮牛币
2324
威望
0
注册时间
2018-2-1
在线时间
177 小时
最后登录
2018-5-26

迈向小康

发表于 2018-2-12 08:55:21 | 显示全部楼层

一直想找这方面资料学习,

回复 支持 反对

使用道具 举报

4四处流浪
371/500
排名
5972
昨日变化
5

2

主题

87

帖子

371

积分

Rank: 4

UID
250757
好友
1
蛮牛币
1107
威望
0
注册时间
2017-10-25
在线时间
84 小时
最后登录
2018-5-16
发表于 2018-2-12 09:00:08 | 显示全部楼层

感谢分享

回复

使用道具 举报

7日久生情
1922/5000
排名
3429
昨日变化
15

0

主题

1331

帖子

1922

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
185339
好友
0
蛮牛币
2730
威望
0
注册时间
2016-11-20
在线时间
213 小时
最后登录
2018-5-25
发表于 2018-2-12 09:12:57 | 显示全部楼层
谢谢分享

回复

使用道具 举报

7日久生情
1922/5000
排名
3429
昨日变化
15

0

主题

1331

帖子

1922

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
185339
好友
0
蛮牛币
2730
威望
0
注册时间
2016-11-20
在线时间
213 小时
最后登录
2018-5-25
发表于 2018-2-12 09:16:11 | 显示全部楼层
谢谢分享
[发帖际遇]: 一个袋子砸在了 步行骑士 头上,步行骑士 赚了 1 蛮牛币. 幸运榜 / 衰神榜

回复

使用道具 举报

7日久生情
2175/5000
排名
1409
昨日变化
1

4

主题

570

帖子

2175

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
112674
好友
4
蛮牛币
1290
威望
0
注册时间
2015-7-8
在线时间
813 小时
最后登录
2018-5-25
QQ
发表于 2018-2-12 09:22:17 | 显示全部楼层
深度干货——捏脸系统如何实现?

回复 支持 反对

使用道具 举报

5熟悉之中
753/1000
排名
10208
昨日变化
129

1

主题

247

帖子

753

积分

Rank: 5Rank: 5

UID
159858
好友
1
蛮牛币
352
威望
0
注册时间
2016-8-1
在线时间
415 小时
最后登录
2018-5-26
发表于 2018-2-12 09:29:17 | 显示全部楼层
这个工具好啊。。好文章

回复 支持 反对

使用道具 举报

7日久生情
2040/5000
排名
540
昨日变化
1

1

主题

120

帖子

2040

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
15956
好友
1
蛮牛币
3096
威望
0
注册时间
2014-2-28
在线时间
593 小时
最后登录
2018-5-25
发表于 2018-2-12 09:32:22 | 显示全部楼层
楼主真的是干货!!!

回复

使用道具 举报

3偶尔光临
191/300
排名
8423
昨日变化
6

0

主题

10

帖子

191

积分

Rank: 3Rank: 3Rank: 3

UID
268196
好友
0
蛮牛币
419
威望
0
注册时间
2018-2-8
在线时间
57 小时
最后登录
2018-5-26
发表于 2018-2-12 09:33:43 | 显示全部楼层
好资源   感谢楼主分享了

回复 支持 反对

使用道具 举报

5熟悉之中
702/1000
排名
4117
昨日变化
20

0

主题

224

帖子

702

积分

Rank: 5Rank: 5

UID
229748
好友
0
蛮牛币
2233
威望
0
注册时间
2017-7-1
在线时间
168 小时
最后登录
2018-5-25
发表于 2018-2-12 09:45:15 | 显示全部楼层
谢谢分享

回复

使用道具 举报

3偶尔光临
291/300
排名
7144
昨日变化
8

0

主题

35

帖子

291

积分

Rank: 3Rank: 3Rank: 3

UID
130351
好友
0
蛮牛币
586
威望
0
注册时间
2015-11-29
在线时间
101 小时
最后登录
2018-5-24
发表于 2018-2-12 09:52:51 | 显示全部楼层
这干货厉害,谢谢分享

回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则

关闭

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

快速回复 返回顶部 返回列表