2023/7/12:此脚本已过时,表情导出逻辑现已集成至赛马娘查看器,推荐使用:
https://github.com/croakfang/UmaViewer (可以直接导出整个模型,附带表情,骨骼,所有UV)
2022/9/19更新: 在@LC_ilmlp的提醒下注意到了Maya与unity直接的旋转问题,trsarray里记录的旋转值为maya的旋转,其旋转顺序与unity不同,直接给unity赋值就会出现问题。脚本已更新
2022/8/23更新:突然发现每个类型的表情的首个表情是默认表情,在应用表情之前首先要应用默认表情,否则某些马娘的表情会出现错误(比如有虎牙的角色虎牙会错位)。
之前写过一个工具来提取赛马娘的表情(上一篇文章地址),但无论如何调整参数,某些骨骼总是有些许错位,总是不能达成想要的效果,究其原因是MMD和PMD没有骨骼缩放,以及它们的长度单位、坐标轴与Unity有所不同。
所以想要完美地导出赛马娘的表情,还是需要在其他3D软件上进行处理才行。这里我就选择用Unity来进行提取和导出了(毕竟赛马娘是Unity游戏,我对Unity也比较熟悉)。
1.数据结构分析
上一篇文章说过,赛马娘是使用脚本控制骨骼实现的表情,每个头的模型都有一个对应的MonoBehaviour脚本,命名为“ast_chr角色编号_服装编号_facial_target”,通过AssetStudio可以将脚本以JSON格式导出。
查看导出的JSON文件我们可以知道赛马娘人物表情信息的数据结构。
根目录下的_eyeTarget、eyebrowTarget以及mouthTarget就是我们的目标,展开其中一个Target,可以看到里面包含了若干个_faceGroupInfo数组,展开 faceGroupInfo ,能看到里面包含两个_trsArray数组( mouthTarget 里只有一个),最后展开 _trsArray数组 ,就能看到若干个骨骼名、旋转位移缩放等信息。
通过分析得知,表情分为三类,分别为眼睛、眉毛和嘴巴表情、每一类就是一个Target,而每一类里面有若干个表情,每一个表情就是一个 faceGroupInfo ,每个表情内又分左表情和右边表情,每一边就是一个 trsArray (嘴巴不分左右,所以只有一个 trsArray ),每一边表情都由多个骨骼控制,所以 trsArray 里包含了若干个骨骼的信息。
2.脚本的编写
分析完数据结构,我们就对表情的控制有了大致的思路——对于一个表情,遍历它TrsArray里的所有的骨骼,对于每一个骨骼,我们在人物模型上找到对应名称的骨骼,并根据骨骼信息进行旋转位移缩放即可。
首先是定义数据结构
[Serializable]
public class Morph
{
public int index;
public string name;
public enum morphType
{
EyeBrow,
Eye,
Mouth
}
public morphType type;
public List<Bone> bones = new List<Bone>();
}
[Serializable]
public class Bone
{
public string Bonename;
public bool isValidScale;
public bool isOverride;
public Vector3 pos, rot, sca;
}
然后是解析JSON文件的核心代码
JObject jObject = (JObject)JsonConvert.DeserializeObject(FacialTarget.text);
if (jObject == null) return;
for (int f = 0; f < typeList.Length; f++)
{
JArray jArray = (JArray)jObject[typeList[f]];
for (int i = 0; i < jArray.Count; i++)
{
JArray Group = (JArray)jArray[i]["_faceGroupInfo"];
for (int j = 0; j < Group.Count; j++)
{
Morph morph = new Morph();
morph.type = (Morph.morphType)f;
JArray trsArray = (JArray)Group[j]["_trsArray"];
for (int n = 0; n < trsArray.Count; n++)
{
Bone bone = new Bone();
bone.Bonename = trsArray[n]["_path"].ToString();
bone.pos = new Vector3(
Convert.ToSingle(trsArray[n]["_position"]["x"]),
Convert.ToSingle(trsArray[n]["_position"]["y"]),
Convert.ToSingle(trsArray[n]["_position"]["z"]));
bone.rot = new Vector3(
Convert.ToSingle(trsArray[n]["_rotation"]["x"]),
Convert.ToSingle(trsArray[n]["_rotation"]["y"]),
Convert.ToSingle(trsArray[n]["_rotation"]["z"]));
bone.sca = new Vector3(
Convert.ToSingle(trsArray[n]["_scale"]["x"]),
Convert.ToSingle(trsArray[n]["_scale"]["y"]),
Convert.ToSingle(trsArray[n]["_scale"]["z"]));
bone.isValidScale = (int)trsArray[n]["_isValidScaleTransform"] == 1;
bone.isOverride = (int)trsArray[n]["IsOverrideTarget"] == 1;
if (i == 0)
{
bone.isOverride = true;
morph.name = ((Morph.morphType)f).ToString() + "_Base" +
(Group.Count < 2 ? "" : (j == 0 ? "_R" : "_L"));
}
else
morph.name = ((Morph.morphType)f).ToString() + "_" + i +
(Group.Count < 2 ? "" : (j == 0 ? "_R" : "_L"));
morph.bones.Add(bone);
}
morphs.Add(morph);
}
}
}
然后是控制表情的方法
public void ChangeMorph(Morph morph)
{
FacialReset();
foreach (Bone bone in morph.bones)
{
var tran = objs.Find(ani => ani.name.Equals(bone.Bonename));
if (tran)
{
if (bone.isOverride)
{
tran.localRotation = Quaternion.Euler(bone.rot);
tran.localPosition = bone.pos;
tran.localScale = bone.sca;
}
else
{
var tmp = tran.localRotation.eulerAngles;
tmp += bone.rot;
tran.localRotation = Quaternion.Euler(tmp);
tran.localPosition += bone.pos;
tran.localScale += bone.sca;
}
}
}
}
至此,我们已经成功将表情信息提取出来,并在Unity中成功预览。
3.表情的导出
通过前面我们知道,模型和表情是分离的,而如果想要方便地在其他软件上使用表情,比较好的方法就是使用BlendShape,所以我们要将表情信息导入进模型的BlendShape里。
Unity里有对FPX操作的API,只需要我们在导入模型中开启Read/Write enable,并使用mesh里的AddBlendShapeFrame方法即可添加一个BlendShape表情到模型里。使用 AddBlendShapeFrame 需要传入变形的顶点、法线等信息,我们需要先调用上面预览表情的方法,再调用 AddBlendShapeFrame 方法。代码如下:
private void RecordBlendshape()
{
eyebrowMesh.sharedMesh.ClearBlendShapes();
faceMesh.sharedMesh.ClearBlendShapes();
foreach (Morph m in morphs)
{
ChangeMorph(m);
switch (m.type)
{
case Morph.morphType.EyeBrow:
case Morph.morphType.Eye:
case Morph.morphType.Mouth:
Mesh facemesh = new Mesh();
faceMesh.BakeMesh(facemesh);
faceMesh.sharedMesh.AddBlendShapeFrame(m.name, 1,
CalDelta(faceMesh.sharedMesh.vertices, facemesh.vertices),
CalDelta(faceMesh.sharedMesh.normals, facemesh.normals),
CalDelta(Vec4ToVec3(faceMesh.sharedMesh.tangents), Vec4ToVec3(facemesh.tangents)));
break;
}
FacialReset();
}
}
导出表情后,我们打开模型的SkinMeshRenderer,里面应该已经有相应的BlendShape了,这时表情信息已经存进模型里了。
4.模型的导出
最后我们只需要将模型导出就可以了,这里我选择的是使用官方的FPX Exporter插件,直接去Package Manager里安装即可,安装后在场景目录下的模型上右键选择导出FBX即可。
导出选项除了导出目录外不做任何修改,点击导出
导出后可以用其他的软件打开模型(比如Blender),并查看表情
至此,赛马娘提取表情并导出的步骤就全部完成了,这次应该是100%还原表情了QVQ。
5.后记
这次在Unity上实现表情导出还是耗费了我不少的时间的,尤其是在骨骼的位移旋转缩放相对还是绝对的处理,如何导出到BlendShape上走了不少弯路,幸好最后的成果十分不错,也算是了却了我的一个心愿吧。
最后附上全部代码(后缀改成.cs即可作为Unity脚本使用):