首页 每日剑灵 新手晋级 副本百科 剑灵攻略
当前位置 首页 > 剑灵攻略

捏脸的设计是唯一的难点具体怎么布置骨骼?

作者:admin   来源:找剑灵   发表时间:2022-07-21 09:06:34  

捏脸的方法有两种,一种是使用骨骼变换,另一种是使用Blend Shape。

剑灵捏人

骨架法是在面部内部正常构建骨骼和皮肤,并使用骨骼的缩放因子和位移来改变面部的形状。但由于是数学计算的结果,设计时很难直接调整最终效果。

BlendShape 通常会制作多种类型的人脸形状并离线生成差异数据。设计起来比较简单,但是叠加的时候不容易控制,数据量大。

由于游戏现在需要使用 BlendShape 来实现角色的表情,重叠的 BlendShape 不好处理。骨架法只是在制作上比较麻烦,但是可控性高,数据量少,所以大部分游戏都是用骨头来实现捏脸的。

骨骼的设计是唯一的难点

有关如何排列骨骼的详细信息,请参阅上图。事实上,您不需要太多的控制项。更详细的可移动部分(例如嘴形和眼睛)交给 BlendShape。骨骼只用于排列捏脸时需要改变的部分。.

通常在普通骨骼上添加一个Position(0,0,0),Scale(1,1,1)的节点,然后代替父节点进行蒙皮,使用as 捏脸时的变量数据,这样捏脸的时候值是统一的,容易处理,当你不希望缩放参数影响子节点时也可以这样做。

剑灵捏人

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

与脸型相比,眼睛需要的节点数量更多,因为需要处理眼球等多个物体,而且眼角的长度和提升必须是可控的,这是最捏脸骨骼的复杂部分。

使捏脸人

虽然提供所有可变骨骼可以做出任何一种脸型,但这只会让玩家轻松挤出怪物。

剑灵捏人

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

剑灵捏人

至于这些调整值背后的逻辑——

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

剑灵捏人

通过组合,您可以处理各种情况(眼睛必须影响多个骨骼)。

将Value值调整到某个极值,然后修改Min和Max查看效果剑灵捏人,修改Curve曲线处理中间情况,生成捏脸数据的过程比较直观。设计完成后,将序列化的数据进行存储、加载,只允许后期使用时修改Value的值,可以应用于游戏的捏脸部分。

而且这个编辑器的实现也很简单,使用Unity自带的数据序列化面板即可。

   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,则在应用 BlendShape 后,Bake 后的 Mesh 数据会与之前的略有偏移。这是因为 BlendShape 在骨骼变化之前应用,而在 Bake 之后,它在骨骼变化之后应用。

剑灵捏人

两者的差异与骨骼的变化强度有关,也与BlendShape的变化幅度有关,但一般情况下差别不大(第三张图是前两张图的叠加,可以看到面部表情影响部分产生轻微偏移)

但更少。

解决方法是先将每个BlendShape应用到Mesh上,然后进行骨骼变换、蒙皮生成mesh数据,然后减去Bake Mesh重新生成一个新的BlendShape。

这其实相当于重做了BlendShape的生成过程(两个模型的区别),数据完全准确。

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

   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设置为对应绑定姿势的逆,它们就不会影响需要烘焙的骨骼烤。

Matrix4x4 m = rootBone.worldToLocalMatrix * bindposes[i].inverse;
SetTransformMatrix(bone, m);

Bindposes本身就是Mesh空间相对于骨架节点空间的变换矩阵的逆……将Mesh空间的顶点乘以bindposes相当于将自身转换到骨架节点所在的空间,所以“Bone -> 世界空间变换”再次。可以完成换肤,这也是上述换肤代码实现的原理。

在这里又找到了bindposes的逆,得到了骨架节点相对于Mesh空间的矩阵,其实就是骨骼最开始开始的位置。将骨骼移回这个位置意味着骨骼不参与蒙皮,不会影响其他需要蒙皮的骨骼。

设置Transform,而不是直接跳过蒙皮阶段,是为了让需要烘焙的节点的父节点在正确的位置。相比重新计算需要烘焙的节点的Matrix4x4,这种方法会更简单(但性能确实差一些)

然后只需按照正常的 BakeMesh 流程进行操作即可。

完成后,还需要去掉已经烘焙过的实际上无效的骨骼的boneWeight。所有涉及这些骨骼的boneWeight部分都需要重置为骨骼的根节点RootBone的序号,即0。

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其实有一个捏人脸参数的配置文件。有兴趣的可以自己去拿剑灵捏人,省去配置参数的时间(这大概是工作量的大头)

/p/28471808

当然,如果你想自律,也可以参考自己。


感谢欣赏,以上是 www.zhaojl.com 找剑灵,带来的捏脸的设计是唯一的难点具体怎么布置骨骼?信息,如果您喜欢我们,喜欢当前文章 捏脸的设计是唯一的难点具体怎么布置骨骼? 请复制传播更多人知道!
我喜欢:
更多
最新资讯
每日剑灵热点
神武一个比较蛋疼活动玲珑塔的玲珑塔攻略 今天给大家带来神武一个比较蛋疼活动玲珑塔的攻略,本人现在玩的比…
找剑灵(zhaojl.com)抵制盗版,相关关键词:剑灵私服,剑灵私服发布网,新开剑灵网站,超变剑灵私服,新开剑灵私服,
剑灵开服发布网,找剑灵发布网,剑灵sf,网址:www.zhaojl.com - 找剑灵 蜀ICP备2022016416号-1