早就想把少前妹纸的live2d提取出来放桌面上做壁纸了~直到有一天终于琢磨出来了

衷心感谢up主kjkjkAIStudio
LINK:https://www.bilibili.com/video/BV1vK4y1D7wY?spm_id_from=333.788.b_636f6d6d656e74.151
经过无数次讨教与琢磨,终于将模型文件与动作文件搞出来了
全过程:(其实就是按up的方法照葫芦画瓢)
搞个安卓模拟器,下个少女前线,进到游戏里更新,更完后就是这样的

这就行了,退出,去文件管理
定位到Android/data/com.sunborn.girlsfrontline.cn/files/Android/New这个文件夹,里面全是游戏的资源文件,有spine和live2d等等

将New这个文件夹复制到模拟器与电脑的共享文件夹

还是有挺多文件的
重要:下载一个Asset Bundle Extractor和010 editor(下好了请无视,如果你有十六进制编辑器可以不用下010 editor)
需要输入密码:102030 获取资源!
[secret key="102030"] [/secret]点开 AssetBundleExtractor ,左上角file,open,选择刚刚提取出来的文件。注意文件名要带有live2d


直接点是,随便命个名(我永远都是1)
然后点info

打开出来后有一大堆东西

点一点name,排个序,这样好找一些
找了一会后,注意MonoBehaviour model_moc,这就是我们要找的文件之一:live2d的模型文件,后缀名为moc3,然后我用来举例这个文件有两个模型,有一个是normal,有一个是destroy,俗称大破(更涩气),这两个文件要分开,最好标记一下,因为后面的文件也要一一对应的

点击export raw,保存(他保存的格式为dat)

p.s.可以用一下我的方法存文件,先建文件夹,这样方便很多,模型名/normal or destroy/包含moc3文件,motions文件夹(放动态文件)和textures文件夹(放贴图)

这时又看到texture文件,照样有normal和destroy,点击plugins,选择export to png,保存

这就完事了
然后找到你提取出来的模型文件,这时你会发现,后缀名是dat,并不是moc3,也许你会尝试直接改为moc3,但是打不开,这是要用010 editor编辑一下文件

如果你与正常的moc3文件对比一下,就会发现我们刚提取出来的模型文件在moc3字符前多出一些不明字符,给他删掉,保存
记得改文件后缀名,我们就完美得到少女前线的moc3模型文件了
接着打开live2d viewer EX(此应用steam有售)创建模型配置文件,后缀名为.model3.json。因为这个文件在加密包里是没有的,我们需要手动给他创建出来。当然,自己写文件也可以,我也会介绍如何从零开始创建模型配置文件,但个人推荐还是用 live2d viewer EX 方便一些,而且能实时看到模型效果~
打开EX工作室,选择live2d编辑器并导入刚刚弄好的moc3模型

优势出来了,傻瓜式创建模型配置文件,选择对应的贴图(不要选错,区分normal和destroy的贴图!)

这不就出来了?

destroy(大破版)
欢呼
第二种创建模型配置文件的方法:自己动手丰衣足食
新建一个文本文档,然后修改后缀为 .model3.json ,粘贴以下代码
{
"Version": 3,
//以下路径需修改为自己的实际路径,moc为模型文件,textures为贴图文件
"FileReferences": {
"Moc": "ump9_normal.moc3",
"Textures": [
"textures/texture_00-CAB-b4639c0b22c12740e71d5b9d7c1a6de5-2881308979586228580.png"
],
"PhysicsV2": {}
},
"Controllers": {
"ParamHit": {},
"ParamLoop": {},
"KeyTrigger": {},
"ParamTrigger": {},
"AreaTrigger": {},
"HandTrigger": {},
"EyeBlink": {
"MinInterval": 500,
"MaxInterval": 6000,
"Enabled": true
},
"LipSync": {
"Gain": 5.0
},
"MouseTracking": {
"SmoothTime": 0.15,
"Enabled": true
},
"AutoBreath": {
"Enabled": true
},
"ExtraMotion": {
"Enabled": true
},
"Accelerometer": {
"Enabled": true
},
"Microphone": {},
"Transform": {},
"FaceTracking": {
"Enabled": true
},
"HandTracking": {},
"ParamValue": {},
"PartOpacity": {},
"ArtmeshOpacity": {},
"ArtmeshColor": {},
"ArtmeshCulling": {
"DefaultMode": 0
},
"IntimacySystem": {}
},
"Options": {}
}
注意要修改贴图和模型文件的路径,不会可以看下图

至此,模型搞好了,接下来是动作文件了!(最难)
还是需要感谢UPkjkjkAIStudio,在他的视频里有很详细的提取原理,这里不再过多赘述
还是打开 AssetBundleExtractor ,大约在moc文件附近能找到动作文件,如图


为什么我不把红圈上面的destroy.fade和normal.fade圈起来呢?因为经过无数实验,那两个不是动作文件,所以忽略
assets bundle里找到mono behaviour xxxx.fade文件就是
type为motion fade.CubismFadeMotionData
点击export dump,选择导出文件类型为UABE json dump


要注意动作文件也有normal和destroy之分
导出来了,难道这就是真正的动作文件了吗?想多了,后缀名都不一样,正常的文件后缀名是.motion3.json的,此时就要用up主的思路来写一个解密小软件
Visual Studio安排上,下图文件结构

程序读取转换实现(Program.cs)
using System;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace CubismFadeMotionDataToJson
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("CubismFadeMontionDataToJson 版本0.1b1");
Console.WriteLine("功能:用UABE以json形式从assess bundle导出cubismfademotiondata类的数据,可以用此程序转换为一般的live2d动作文件");
Console.WriteLine("仅支持cubism live2d sdk3 motion json的导出");
Console.WriteLine("警告:处于实验性阶段,出现问题很正常");
Console.WriteLine("按任意键开始");
Console.ReadKey();
if (Directory.Exists("dst"))
{
string[] existFilename = Directory.GetFiles("dst");
foreach (string i in existFilename)
File.Delete(i);
}
else
{
Directory.CreateDirectory("dst");
}
string[] filenames = Directory.GetFiles("src");
MotionDataConverter converter = new MotionDataConverter();
foreach (string i in filenames)
{
Console.WriteLine(string.Format("转换:[0]......", i));
try
{
var json = File.ReadAllText(i);
var infinity = JsonConvert.PositiveInfinity;
var newContent = json.Replace("1.#INF", infinity.ToString());
var obj = JObject.Parse(newContent);
var content = converter.Convert(obj).ToString();
File.WriteAllText("dst/" + Path.GetFileName(i),content);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
Console.WriteLine("转换失败");
}
}
Console.WriteLine("全部转换成功,现在开始改名");
RenameMacro();
Console.WriteLine("successful");
Console.ReadKey();
}
static void RenameMacro()
{
string[] filenames = Directory.GetFiles("dst");
for (int i = 0; i < filenames.Length; ++i)
{
string name = Path.GetFileName(filenames[i]);
int chrPos = name.LastIndexOf('-'); // find last '-'
if (chrPos == -1)
continue;
name = name.Remove(chrPos,name.Length - chrPos);
name += ".motion3.json";
File.Move(filenames[i], "dst/" + name);
}
}
}
}
关键逻辑(解密代码,即MotionDataConverter.cs)
using Newtonsoft.Json.Linq;
using System;
namespace CubismFadeMotionDataToJson
{
public class MotionDataConverter
{
public MotionDataConverter()
{
}
public JObject Convert(JObject cubismFadeMotionData)
{
JObject result = new JObject();
WriteHead(result);
WriteCurves(result, (JObject)cubismFadeMotionData.GetValue("0 MonoBehaviour Base"));
return result;
}
class Keyframe
{
public float time, value, inSlope, outSlope;
}
void WriteHead(JObject dstData)
{
dstData.Add("Version", 3);
JObject meta = new JObject();
meta.Add("Duration", 0.0f);
meta.Add("Pps", 0.0f);
meta.Add("Loop",true);
meta.Add("AreBeziersRestricted",true);
meta.Add("CurveCount", 0.0f);
meta.Add("TotalSegmentCount", 0.0f);
meta.Add("TotalPointCount", 0.0f);
meta.Add("UserDataCount", 0.0f);
meta.Add("TotalUserDataSize", 0.0f);
dstData.Add("Meta", meta);
}
void WriteCurves(JObject dstData, JObject srcData)
{
JArray curves = new JArray();
string[] parameterIds = GetParameterIds(srcData);
float[] parameterFadeInTimes = GetParameterFadeInTimes(srcData);
float[] parameterFadeOutTimes = GetParameterFadeOutTimes(srcData);
JArray animationCurves = (JArray)((JObject)srcData.GetValue("0 vector ParameterCurves")).GetValue("1 Array Array");
for (int i = 0; i < parameterIds.Length; ++i)
{
if (string.IsNullOrEmpty(parameterIds[i]))
continue;
JObject curve = new JObject();
curve.Add("Target", "Parameter");
curve.Add("Id", parameterIds[i]);
if (parameterFadeInTimes[i] >= 0.0f)
curve.Add("FadeInTimes", parameterFadeInTimes[i]);
if (parameterFadeOutTimes[i] >= 0.0f)
curve.Add("FadeOutTimes", parameterFadeOutTimes[i]);
curve.Add("Segments", ConvertKeyFramesToCurvesSegments((JObject)((JObject)animationCurves[i]).GetValue("0 AnimationCurve data")) );
curves.Add(curve);
}
dstData.Add("Curves", curves);
}
string[] GetParameterIds(JObject srcData)
{
JArray array = (JArray)((JObject)srcData.GetValue("0 vector ParameterIds")).GetValue("1 Array Array");
string[] result = new string[array.Count];
for (int i = 0; i < result.Length; ++i)
result[i] = (string)((JObject)array[i]).GetValue("1 string data");
return result;
}
float[] GetParameterFadeInTimes(JObject srcData)
{
JArray array = (JArray)((JObject)srcData.GetValue("0 vector ParameterFadeInTimes")).GetValue("1 Array Array");
float[] result = new float[array.Count];
for (int i = 0; i < result.Length; ++i)
result[i] = (float)((JObject)array[i]).GetValue("0 float data");
return result;
}
float[] GetParameterFadeOutTimes(JObject srcData)
{
JArray array = (JArray)((JObject)srcData.GetValue("0 vector ParameterFadeOutTimes")).GetValue("1 Array Array");
float[] result = new float[array.Count];
for (int i = 0; i < result.Length; ++i)
result[i] = (float)((JObject)array[i]).GetValue("0 float data");
return result;
}
JArray ConvertKeyFramesToCurvesSegments(JObject animationCurves)
{
JArray result = new JArray();
JArray curveArray = (JArray)((JObject)animationCurves.GetValue("0 vector m_Curve")).GetValue("1 Array Array");
if (curveArray.Count == 0)
return result;
Keyframe[] keyframe = ConvertJsonToArray(curveArray);
//first 2 keyframe must be segment
result.Add(keyframe[0].time);
result.Add(keyframe[0].value);
for (int j = 1; j < keyframe.Length; ++j)
{
//judge keyfraame type
if (j + 1 < keyframe.Length && keyframe[j].inSlope != 0.0f && keyframe[j].outSlope == 0.0f && keyframe[j +1].inSlope == 0.0f &&keyframe[j+1].inSlope == 0.0f)
{
result.Add(3.0f); //type:inverseStepped
result.Add(keyframe[j + 1].time);
result.Add(keyframe[j + 1].value);
++j; //inversestepped create 2 keyframe
}
else if (float.IsPositiveInfinity(keyframe[j].inSlope))
{
result.Add(2.0f);
result.Add(keyframe[j].time);
result.Add(keyframe[j].value);
}
else if (keyframe[j - 1].outSlope == keyframe[j].inSlope)
{
result.Add(0.0f);
result.Add(keyframe[j].time);
result.Add(keyframe[j].value);
}
else
{
result.Add(1.0f);
float tangentLength = Math.Abs(keyframe[j - 1].time - keyframe[j].time) * 0.333333f;
result.Add(0.0f);
result.Add(keyframe[j - 1].outSlope * tangentLength + keyframe[j - 1].value);
result.Add(0.0f);
result.Add(keyframe[j].value - keyframe[j].inSlope * tangentLength);
result.Add(keyframe[j].time);
result.Add(keyframe[j].value);
}
}
return result;
}
Keyframe[] ConvertJsonToArray(JArray array)
{
Keyframe[] result = new Keyframe[array.Count];
for (int i = 0; i < array.Count; ++i)
{
JObject obj = (JObject)((JObject)array[i]).GetValue("0 Keyframe data");
result[i] = new Keyframe();
result[i].time = (float)obj.GetValue("0 float time");
result[i].value = (float)obj.GetValue("0 float value");
var test = obj.GetValue("0 float inSlope");
float max;
if(float.TryParse(test.ToString(), out max))
{
result[i].inSlope = (float)obj.GetValue("0 float inSlope");
}
else
{
result[i].inSlope = float.MaxValue;
}
result[i].outSlope = (float)obj.GetValue("0 float outSlope");
}
return result;
}
}
}
实现:将提取出来的动作文件放入src文件夹,程序将其转换成真正的motion3.json位于dst文件夹内

需要输入密码:102030 获取资源!
[secret key="102030"] [/secret]转换程序,将文件后缀改为txt
解压密码146586742786875288767857857869986788775246452
当初为了抄up这玩意,真是琢磨了好久。。。
我们可以看看转换后的文件与转换前的文件对比,下图是转换前

下图是转换后

用live2d viewer EX 打开后一切正常~
当然,还要修改一下模型配置文件,将动作文件导进去,这里也不说了
至此,少前的liv2d提出来了,这下桌面又生动起来了~~~
原创文章,作者:Rosmontics,如若转载,请注明出处:https://rosmontis.com/archives/74