Add option to export Live2D Cubism models

This commit is contained in:
VaDiM 2023-07-02 03:10:11 +03:00
parent 6d41693b85
commit aea6cbc97f
17 changed files with 1256 additions and 31 deletions

View File

@ -21,6 +21,7 @@ namespace AssetStudioCLI.Options
ExportRaw,
Dump,
Info,
ExportLive2D,
}
internal enum AssetGroupOption
@ -132,11 +133,12 @@ namespace AssetStudioCLI.Options
optionDefaultValue: WorkMode.Export,
optionName: "-m, --mode <value>",
optionDescription: "Specify working mode\n" +
"<Value: export(default) | exportRaw | dump | info>\n" +
"<Value: export(default) | exportRaw | dump | info | live2d>\n" +
"Export - Exports converted assets\n" +
"ExportRaw - Exports raw data\n" +
"Dump - Makes asset dumps\n" +
"Info - Loads file(s), shows the number of supported for export assets and exits\n" +
"Live2D - Exports Live2D Cubism 3 models\n" +
"Example: \"-m info\"\n",
optionHelpGroup: HelpGroups.General
);
@ -414,6 +416,17 @@ namespace AssetStudioCLI.Options
case "info":
o_workMode.Value = WorkMode.Info;
break;
case "live2d":
o_workMode.Value = WorkMode.ExportLive2D;
o_exportAssetTypes.Value = new List<ClassIDType>()
{
ClassIDType.AnimationClip,
ClassIDType.GameObject,
ClassIDType.MonoBehaviour,
ClassIDType.Texture2D,
ClassIDType.Transform,
};
break;
default:
Console.WriteLine($"{"Error".Color(brightRed)} during parsing [{option}] option. Unsupported working mode: [{value.Color(brightRed)}].\n");
Console.WriteLine(o_workMode.Description);
@ -422,6 +435,11 @@ namespace AssetStudioCLI.Options
break;
case "-t":
case "--asset-type":
if (o_workMode.Value == WorkMode.ExportLive2D)
{
i++;
continue;
}
var splittedTypes = ValueSplitter(value);
o_exportAssetTypes.Value = new List<ClassIDType>();
foreach (var type in splittedTypes)
@ -773,29 +791,37 @@ namespace AssetStudioCLI.Options
sb.AppendLine("[Current Options]");
sb.AppendLine($"# Working Mode: {o_workMode}");
sb.AppendLine($"# Input Path: \"{inputPath}\"");
if (o_workMode.Value != WorkMode.Info)
switch (o_workMode.Value)
{
sb.AppendLine($"# Output Path: \"{o_outputFolder}\"");
sb.AppendLine($"# Export Asset Type(s): {string.Join(", ", o_exportAssetTypes.Value)}");
sb.AppendLine($"# Asset Group Option: {o_groupAssetsBy}");
sb.AppendLine($"# Export Image Format: {o_imageFormat}");
sb.AppendLine($"# Export Audio Format: {o_audioFormat}");
sb.AppendLine($"# Log Level: {o_logLevel}");
sb.AppendLine($"# Log Output: {o_logOutput}");
sb.AppendLine($"# Export Asset List: {o_exportAssetList}");
sb.AppendLine(ShowCurrentFilter());
sb.AppendLine($"# Assebmly Path: \"{o_assemblyPath}\"");
sb.AppendLine($"# Unity Version: \"{o_unityVersion}\"");
sb.AppendLine($"# Restore TextAsset extension: {!f_notRestoreExtensionName.Value}");
}
else
{
sb.AppendLine($"# Export Asset Type(s): {string.Join(", ", o_exportAssetTypes.Value)}");
sb.AppendLine($"# Log Level: {o_logLevel}");
sb.AppendLine($"# Log Output: {o_logOutput}");
sb.AppendLine($"# Export Asset List: {o_exportAssetList}");
sb.AppendLine(ShowCurrentFilter());
sb.AppendLine($"# Unity Version: \"{o_unityVersion}\"");
case WorkMode.Info:
sb.AppendLine($"# Export Asset Type(s): {string.Join(", ", o_exportAssetTypes.Value)}");
sb.AppendLine($"# Log Level: {o_logLevel}");
sb.AppendLine($"# Log Output: {o_logOutput}");
sb.AppendLine($"# Export Asset List: {o_exportAssetList}");
sb.AppendLine(ShowCurrentFilter());
sb.AppendLine($"# Unity Version: \"{o_unityVersion}\"");
break;
case WorkMode.ExportLive2D:
sb.AppendLine($"# Output Path: \"{o_outputFolder}\"");
sb.AppendLine($"# Log Level: {o_logLevel}");
sb.AppendLine($"# Log Output: {o_logOutput}");
sb.AppendLine($"# Export Asset List: {o_exportAssetList}");
sb.AppendLine($"# Unity Version: \"{o_unityVersion}\"");
break;
default:
sb.AppendLine($"# Output Path: \"{o_outputFolder}\"");
sb.AppendLine($"# Export Asset Type(s): {string.Join(", ", o_exportAssetTypes.Value)}");
sb.AppendLine($"# Asset Group Option: {o_groupAssetsBy}");
sb.AppendLine($"# Export Image Format: {o_imageFormat}");
sb.AppendLine($"# Export Audio Format: {o_audioFormat}");
sb.AppendLine($"# Log Level: {o_logLevel}");
sb.AppendLine($"# Log Output: {o_logOutput}");
sb.AppendLine($"# Export Asset List: {o_exportAssetList}");
sb.AppendLine(ShowCurrentFilter());
sb.AppendLine($"# Assebmly Path: \"{o_assemblyPath}\"");
sb.AppendLine($"# Unity Version: \"{o_unityVersion}\"");
sb.AppendLine($"# Restore TextAsset extension: {!f_notRestoreExtensionName.Value}");
break;
}
sb.AppendLine("======");
Logger.Info(sb.ToString());

View File

@ -36,7 +36,7 @@ namespace AssetStudioCLI
if (studio.LoadAssets())
{
studio.ParseAssets();
if (options.filterBy != FilterBy.None)
if (options.filterBy != FilterBy.None && options.o_workMode.Value != WorkMode.ExportLive2D)
{
studio.FilterAssets();
}
@ -44,12 +44,18 @@ namespace AssetStudioCLI
{
studio.ExportAssetList();
}
if (options.o_workMode.Value == WorkMode.Info)
switch (options.o_workMode.Value)
{
studio.ShowExportableAssetsInfo();
return;
case WorkMode.Info:
studio.ShowExportableAssetsInfo();
break;
case WorkMode.ExportLive2D:
studio.ExportLive2D();
break;
default:
studio.ExportAssets();
break;
}
studio.ExportAssets();
}
}
catch (Exception ex)

View File

@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using System.Xml.Linq;
using static AssetStudioCLI.Exporter;
using static CubismLive2DExtractor.Live2DExtractor;
using Ansi = AssetStudioCLI.CLIAnsiColors;
namespace AssetStudioCLI
@ -14,6 +15,7 @@ namespace AssetStudioCLI
{
public AssetsManager assetsManager = new AssetsManager();
public List<AssetItem> parsedAssetsList = new List<AssetItem>();
private static Dictionary<AssetStudio.Object, string> containers = new Dictionary<AssetStudio.Object, string>();
private readonly CLIOptions options;
public Studio(CLIOptions cliOptions)
@ -51,7 +53,6 @@ namespace AssetStudioCLI
Logger.Info("Parse assets...");
var fileAssetsList = new List<AssetItem>();
var containers = new Dictionary<AssetStudio.Object, string>();
var objectCount = assetsManager.assetsFileList.Sum(x => x.Objects.Count);
Progress.Reset();
@ -147,7 +148,6 @@ namespace AssetStudioCLI
}
}
parsedAssetsList.AddRange(fileAssetsList);
containers.Clear();
fileAssetsList.Clear();
}
}
@ -379,5 +379,78 @@ namespace AssetStudioCLI
}
Logger.Info($"Finished exporting asset list with {parsedAssetsList.Count} items.");
}
public void ExportLive2D()
{
var baseDestPath = Path.Combine(options.o_outputFolder.Value, "Live2DOutput");
var useFullContainerPath = false;
Progress.Reset();
Logger.Info($"Searching for Live2D files...");
var cubismMocs = parsedAssetsList.Where(x =>
{
if (x.Type == ClassIDType.MonoBehaviour)
{
((MonoBehaviour)x.Asset).m_Script.TryGet(out var m_Script);
return m_Script?.m_ClassName == "CubismMoc";
}
return false;
}).Select(x => x.Asset).ToArray();
if (cubismMocs.Length == 0)
{
Logger.Default.Log(LoggerEvent.Info, "Live2D Cubism models were not found.", ignoreLevel: true);
return;
}
if (cubismMocs.Length > 1)
{
var basePathSet = cubismMocs.Select(x => containers[x].Substring(0, containers[x].LastIndexOf("/"))).ToHashSet();
if (basePathSet.Count != cubismMocs.Length)
{
useFullContainerPath = true;
Logger.Debug($"useFullContainerPath: {useFullContainerPath}");
}
}
var basePathList = useFullContainerPath ?
cubismMocs.Select(x => containers[x]).ToList() :
cubismMocs.Select(x => containers[x].Substring(0, containers[x].LastIndexOf("/"))).ToList();
var lookup = containers.ToLookup(
x => basePathList.Find(b => x.Value.Contains(b) && x.Value.Split('/').Any(y => y == b.Substring(b.LastIndexOf("/") + 1))),
x => x.Key
);
var totalModelCount = lookup.LongCount(x => x.Key != null);
Logger.Info($"Found {totalModelCount} model(s).");
var name = "";
var modelCounter = 0;
foreach (var assets in lookup)
{
var container = assets.Key;
if (container == null)
continue;
name = container;
Logger.Info($"[{modelCounter + 1}/{totalModelCount}] Exporting Live2D: \"{container.Color(Ansi.BrightCyan)}\"");
try
{
var modelName = useFullContainerPath ? Path.GetFileNameWithoutExtension(container) : container.Substring(container.LastIndexOf('/') + 1);
container = Path.HasExtension(container) ? container.Replace(Path.GetExtension(container), "") : container;
var destPath = Path.Combine(baseDestPath, container) + Path.DirectorySeparatorChar;
ExtractLive2D(assets, destPath, modelName);
modelCounter++;
}
catch (Exception ex)
{
Logger.Error($"Live2D model export error: \"{name}\"", ex);
}
Progress.Report(modelCounter, (int)totalModelCount);
}
var status = modelCounter > 0 ?
$"Finished exporting [{modelCounter}/{totalModelCount}] Live2D model(s) to \"{options.o_outputFolder.Value.Color(Ansi.BrightCyan)}\"" :
"Nothing exported.";
Logger.Default.Log(LoggerEvent.Info, status, ignoreLevel: true);
}
}
}

View File

@ -66,6 +66,8 @@
this.toolStripMenuItem7 = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripMenuItem8 = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripMenuItem9 = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator6 = new System.Windows.Forms.ToolStripSeparator();
this.allLive2DModelsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
this.toolStripMenuItem10 = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripMenuItem11 = new System.Windows.Forms.ToolStripMenuItem();
@ -354,6 +356,8 @@
this.toolStripSeparator4,
this.toolStripMenuItem2,
this.toolStripMenuItem3,
this.toolStripSeparator6,
this.allLive2DModelsToolStripMenuItem,
this.toolStripSeparator2,
this.toolStripMenuItem10});
this.exportToolStripMenuItem.Name = "exportToolStripMenuItem";
@ -460,6 +464,18 @@
this.toolStripMenuItem9.Text = "Filtered assets";
this.toolStripMenuItem9.Click += new System.EventHandler(this.toolStripMenuItem9_Click);
//
// toolStripSeparator6
//
this.toolStripSeparator6.Name = "toolStripSeparator6";
this.toolStripSeparator6.Size = new System.Drawing.Size(263, 6);
//
// allLive2DModelsToolStripMenuItem
//
this.allLive2DModelsToolStripMenuItem.Name = "allLive2DModelsToolStripMenuItem";
this.allLive2DModelsToolStripMenuItem.Size = new System.Drawing.Size(266, 22);
this.allLive2DModelsToolStripMenuItem.Text = "Live2D Cubism models";
this.allLive2DModelsToolStripMenuItem.Click += new System.EventHandler(this.allLive2DModelsToolStripMenuItem_Click);
//
// toolStripSeparator2
//
this.toolStripSeparator2.Name = "toolStripSeparator2";
@ -510,7 +526,7 @@
this.allToolStripMenuItem.CheckOnClick = true;
this.allToolStripMenuItem.CheckState = System.Windows.Forms.CheckState.Checked;
this.allToolStripMenuItem.Name = "allToolStripMenuItem";
this.allToolStripMenuItem.Size = new System.Drawing.Size(88, 22);
this.allToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.allToolStripMenuItem.Text = "All";
this.allToolStripMenuItem.Click += new System.EventHandler(this.typeToolStripMenuItem_Click);
//
@ -1368,6 +1384,8 @@
private System.Windows.Forms.ComboBox listSearchFilterMode;
private System.Windows.Forms.ComboBox listSearchHistory;
private System.Windows.Forms.RichTextBox listSearch;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator6;
private System.Windows.Forms.ToolStripMenuItem allLive2DModelsToolStripMenuItem;
}
}

View File

@ -1880,6 +1880,40 @@ namespace AssetStudioGUI
listSearch.SelectionStart = listSearch.Text.Length;
}
private void allLive2DModelsToolStripMenuItem_Click(object sender, EventArgs e)
{
if (exportableAssets.Count > 0)
{
var cubismMocs = exportableAssets.Where(x =>
{
if (x.Type == ClassIDType.MonoBehaviour)
{
((MonoBehaviour)x.Asset).m_Script.TryGet(out var m_Script);
return m_Script?.m_ClassName == "CubismMoc";
}
return false;
}).Select(x => x.Asset).ToArray();
if (cubismMocs.Length == 0)
{
Logger.Info("Live2D Cubism models were not found.");
return;
}
var saveFolderDialog = new OpenFolderDialog();
saveFolderDialog.InitialFolder = saveDirectoryBackup;
if (saveFolderDialog.ShowDialog(this) == DialogResult.OK)
{
timer.Stop();
saveDirectoryBackup = saveFolderDialog.Folder;
Studio.ExportLive2D(cubismMocs, saveFolderDialog.Folder);
}
}
else
{
Logger.Info("No exportable assets loaded");
}
}
#region FMOD
private void FMODinit()
{

View File

@ -9,6 +9,7 @@ using System.Threading;
using System.Windows.Forms;
using System.Xml.Linq;
using static AssetStudioGUI.Exporter;
using static CubismLive2DExtractor.Live2DExtractor;
using Object = AssetStudio.Object;
namespace AssetStudioGUI
@ -54,6 +55,7 @@ namespace AssetStudioGUI
public static AssemblyLoader assemblyLoader = new AssemblyLoader();
public static List<AssetItem> exportableAssets = new List<AssetItem>();
public static List<AssetItem> visibleAssets = new List<AssetItem>();
private static Dictionary<Object, string> allContainers = new Dictionary<Object, string>();
internal static Action<string> StatusStripUpdate = x => { };
public static int ExtractFolder(string path, string savePath)
@ -262,6 +264,7 @@ namespace AssetStudioGUI
if (pptr.TryGet(out var obj))
{
objectAssetItemDic[obj].Container = container;
allContainers[obj] = container;
}
}
foreach (var tmp in exportableAssets)
@ -738,5 +741,66 @@ namespace AssetStudioGUI
info.UseShellExecute = true;
Process.Start(info);
}
public static void ExportLive2D(Object[] cubismMocs, string exportPath)
{
var baseDestPath = Path.Combine(exportPath, "Live2DOutput");
ThreadPool.QueueUserWorkItem(state =>
{
Progress.Reset();
Logger.Info($"Searching for Live2D files...");
var useFullContainerPath = false;
if (cubismMocs.Length > 1)
{
var basePathSet = cubismMocs.Select(x => allContainers[x].Substring(0, allContainers[x].LastIndexOf("/"))).ToHashSet();
if (basePathSet.Count != cubismMocs.Length)
{
useFullContainerPath = true;
}
}
var basePathList = useFullContainerPath ?
cubismMocs.Select(x => allContainers[x]).ToList() :
cubismMocs.Select(x => allContainers[x].Substring(0, allContainers[x].LastIndexOf("/"))).ToList();
var lookup = allContainers.ToLookup(
x => basePathList.Find(b => x.Value.Contains(b) && x.Value.Split('/').Any(y => y == b.Substring(b.LastIndexOf("/") + 1))),
x => x.Key
);
var totalModelCount = lookup.LongCount(x => x.Key != null);
var name = "";
var modelCounter = 0;
foreach (var assets in lookup)
{
var container = assets.Key;
if (container == null)
continue;
name = container;
Logger.Info($"[{modelCounter + 1}/{totalModelCount}] Exporting Live2D: \"{container}\"...");
try
{
var modelName = useFullContainerPath ? Path.GetFileNameWithoutExtension(container) : container.Substring(container.LastIndexOf('/') + 1);
container = Path.HasExtension(container) ? container.Replace(Path.GetExtension(container), "") : container;
var destPath = Path.Combine(baseDestPath, container) + Path.DirectorySeparatorChar;
ExtractLive2D(assets, destPath, modelName);
modelCounter++;
}
catch (Exception ex)
{
Logger.Error($"Live2D model export error: \"{name}\"", ex);
}
Progress.Report(modelCounter, (int)totalModelCount);
}
Logger.Info($"Finished exporting [{modelCounter}/{totalModelCount}] Live2D model(s).");
if (Properties.Settings.Default.openAfterExport && modelCounter > 0)
{
OpenFolderInExplorer(exportPath);
}
});
}
}
}

View File

@ -9,6 +9,7 @@
<ItemGroup>
<PackageReference Include="Mono.Cecil" Version="0.11.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta15" />
</ItemGroup>

View File

@ -0,0 +1,17 @@
namespace CubismLive2DExtractor
{
public class CubismExpression3Json
{
public string Type;
public float FadeInTime;
public float FadeOutTime;
public SerializableExpressionParameter[] Parameters;
public class SerializableExpressionParameter
{
public string Id;
public float Value;
public int Blend;
}
}
}

View File

@ -0,0 +1,28 @@
using Newtonsoft.Json.Linq;
namespace CubismLive2DExtractor
{
public class CubismModel3Json
{
public int Version;
public string Name;
public SerializableFileReferences FileReferences;
public SerializableGroup[] Groups;
public class SerializableFileReferences
{
public string Moc;
public string[] Textures;
public string Physics;
public JObject Motions;
public JArray Expressions;
}
public class SerializableGroup
{
public string Target;
public string Name;
public string[] Ids;
}
}
}

View File

@ -0,0 +1,198 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using AssetStudio;
namespace CubismLive2DExtractor
{
class CubismMotion3Converter
{
private Dictionary<uint, string> bonePathHash = new Dictionary<uint, string>();
public List<ImportedKeyframedAnimation> AnimationList { get; protected set; } = new List<ImportedKeyframedAnimation>();
public CubismMotion3Converter(GameObject rootGameObject, AnimationClip[] animationClips)
{
var rootTransform = GetTransform(rootGameObject);
CreateBonePathHash(rootTransform);
ConvertAnimations(animationClips);
}
private void ConvertAnimations(AnimationClip[] animationClips)
{
foreach (var animationClip in animationClips)
{
var iAnim = new ImportedKeyframedAnimation();
AnimationList.Add(iAnim);
iAnim.Name = animationClip.m_Name;
iAnim.SampleRate = animationClip.m_SampleRate;
iAnim.Duration = animationClip.m_MuscleClip.m_StopTime;
var m_Clip = animationClip.m_MuscleClip.m_Clip;
var streamedFrames = m_Clip.m_StreamedClip.ReadData();
var m_ClipBindingConstant = animationClip.m_ClipBindingConstant;
for (int frameIndex = 1; frameIndex < streamedFrames.Count - 1; frameIndex++)
{
var frame = streamedFrames[frameIndex];
for (int curveIndex = 0; curveIndex < frame.keyList.Length; curveIndex++)
{
ReadStreamedData(iAnim, m_ClipBindingConstant, frame.time, frame.keyList[curveIndex]);
}
}
var m_DenseClip = m_Clip.m_DenseClip;
var streamCount = m_Clip.m_StreamedClip.curveCount;
for (int frameIndex = 0; frameIndex < m_DenseClip.m_FrameCount; frameIndex++)
{
var time = m_DenseClip.m_BeginTime + frameIndex / m_DenseClip.m_SampleRate;
var frameOffset = frameIndex * m_DenseClip.m_CurveCount;
for (int curveIndex = 0; curveIndex < m_DenseClip.m_CurveCount; curveIndex++)
{
var index = streamCount + curveIndex;
ReadCurveData(iAnim, m_ClipBindingConstant, (int)index, time, m_DenseClip.m_SampleArray, (int)frameOffset, curveIndex);
}
}
var m_ConstantClip = m_Clip.m_ConstantClip;
var denseCount = m_Clip.m_DenseClip.m_CurveCount;
var time2 = 0.0f;
for (int i = 0; i < 2; i++)
{
for (int curveIndex = 0; curveIndex < m_ConstantClip.data.Length; curveIndex++)
{
var index = streamCount + denseCount + curveIndex;
ReadCurveData(iAnim, m_ClipBindingConstant, (int)index, time2, m_ConstantClip.data, 0, curveIndex);
}
time2 = animationClip.m_MuscleClip.m_StopTime;
}
foreach (var m_Event in animationClip.m_Events)
{
iAnim.Events.Add(new ImportedEvent
{
time = m_Event.time,
value = m_Event.data
});
}
if (iAnim.TrackList.Count == 0 || iAnim.Events.Count == 0)
{
Logger.Warning($"[Motion Converter] {iAnim.Name} has {iAnim.TrackList.Count} tracks and {iAnim.Events.Count} event!.");
}
}
}
private void ReadStreamedData(ImportedKeyframedAnimation iAnim, AnimationClipBindingConstant m_ClipBindingConstant, float time, StreamedClip.StreamedCurveKey curveKey)
{
var binding = m_ClipBindingConstant.FindBinding(curveKey.index);
GetLive2dPath(binding, out var target, out var boneName);
if (string.IsNullOrEmpty(boneName))
{
Logger.Warning($"[Motion Converter] {iAnim.Name} read fail on binding {Array.IndexOf(m_ClipBindingConstant.genericBindings, binding)}");
return;
}
var track = iAnim.FindTrack(boneName);
track.Target = target;
track.Curve.Add(new ImportedKeyframe<float>(time, curveKey.value, curveKey.inSlope, curveKey.outSlope, curveKey.coeff));
}
private void ReadCurveData(ImportedKeyframedAnimation iAnim, AnimationClipBindingConstant m_ClipBindingConstant, int index, float time, float[] data, int offset, int curveIndex)
{
var binding = m_ClipBindingConstant.FindBinding(index);
GetLive2dPath(binding, out var target, out var boneName);
if (string.IsNullOrEmpty(boneName))
{
Logger.Warning($"[Motion Converter] {iAnim.Name} read fail on binding {Array.IndexOf(m_ClipBindingConstant.genericBindings, binding)}");
return;
}
var track = iAnim.FindTrack(boneName);
track.Target = target;
var value = data[curveIndex];
track.Curve.Add(new ImportedKeyframe<float>(time, value, 0, 0, null));
}
private void GetLive2dPath(GenericBinding binding, out string target, out string id)
{
var path = binding.path;
id = null;
target = null;
if (path != 0 && bonePathHash.TryGetValue(path, out var boneName))
{
var index = boneName.LastIndexOf('/');
id = boneName.Substring(index + 1);
target = boneName.Substring(0, index);
if (target == "Parameters")
{
target = "Parameter";
}
else if (target == "Parts")
{
target = "PartOpacity";
}
}
else if (binding.script.TryGet(out MonoScript script))
{
switch (script.m_ClassName)
{
case "CubismRenderController":
target = "Model";
id = "Opacity";
break;
case "CubismEyeBlinkController":
target = "Model";
id = "EyeBlink";
break;
case "CubismMouthController":
target = "Model";
id = "LipSync";
break;
}
}
}
private Transform GetTransform(GameObject gameObject)
{
foreach (var m_Component in gameObject.m_Components)
{
if (m_Component.TryGet(out Transform m_Transform))
{
return m_Transform;
}
}
return null;
}
private void CreateBonePathHash(Transform m_Transform)
{
var name = GetTransformPath(m_Transform);
var crc = new SevenZip.CRC();
var bytes = Encoding.UTF8.GetBytes(name);
crc.Update(bytes, 0, (uint)bytes.Length);
bonePathHash[crc.GetDigest()] = name;
int index;
while ((index = name.IndexOf("/", StringComparison.Ordinal)) >= 0)
{
name = name.Substring(index + 1);
crc = new SevenZip.CRC();
bytes = Encoding.UTF8.GetBytes(name);
crc.Update(bytes, 0, (uint)bytes.Length);
bonePathHash[crc.GetDigest()] = name;
}
foreach (var pptr in m_Transform.m_Children)
{
if (pptr.TryGet(out var child))
CreateBonePathHash(child);
}
}
private string GetTransformPath(Transform transform)
{
transform.m_GameObject.TryGet(out var m_GameObject);
if (transform.m_Father.TryGet(out var father))
{
return GetTransformPath(father) + "/" + m_GameObject.m_Name;
}
return m_GameObject.m_Name;
}
}
}

View File

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace CubismLive2DExtractor
{
public class CubismMotion3Json
{
public int Version;
public SerializableMeta Meta;
public SerializableCurve[] Curves;
public SerializableUserData[] UserData;
public class SerializableMeta
{
public float Duration;
public float Fps;
public bool Loop;
public bool AreBeziersRestricted;
public int CurveCount;
public int TotalSegmentCount;
public int TotalPointCount;
public int UserDataCount;
public int TotalUserDataSize;
};
public class SerializableCurve
{
public string Target;
public string Id;
public List<float> Segments;
};
public class SerializableUserData
{
public float Time;
public string Value;
}
}
}

View File

@ -0,0 +1,88 @@
using AssetStudio;
namespace CubismLive2DExtractor
{
public class CubismPhysics3Json
{
public int Version;
public SerializableMeta Meta;
public SerializablePhysicsSettings[] PhysicsSettings;
public class SerializableNormalizationValue
{
public float Minimum;
public float Default;
public float Maximum;
}
public class SerializableParameter
{
public string Target;
public string Id;
}
public class SerializableInput
{
public SerializableParameter Source;
public float Weight;
public string Type;
public bool Reflect;
}
public class SerializableOutput
{
public SerializableParameter Destination;
public int VertexIndex;
public float Scale;
public float Weight;
public string Type;
public bool Reflect;
}
public class SerializableVertex
{
public Vector2 Position;
public float Mobility;
public float Delay;
public float Acceleration;
public float Radius;
}
public class SerializableNormalization
{
public SerializableNormalizationValue Position;
public SerializableNormalizationValue Angle;
}
public class SerializablePhysicsSettings
{
public string Id;
public SerializableInput[] Input;
public SerializableOutput[] Output;
public SerializableVertex[] Vertices;
public SerializableNormalization Normalization;
}
public class SerializableMeta
{
public int PhysicsSettingCount;
public int TotalInputCount;
public int TotalOutputCount;
public int VertexCount;
public SerializableEffectiveForces EffectiveForces;
public SerializablePhysicsDictionary[] PhysicsDictionary;
}
public class SerializableEffectiveForces
{
public Vector2 Gravity;
public Vector2 Wind;
}
public class SerializablePhysicsDictionary
{
public string Id;
public string Name;
}
}
}

View File

@ -0,0 +1,75 @@
using AssetStudio;
namespace CubismLive2DExtractor
{
public class CubismPhysicsNormalizationTuplet
{
public float Maximum;
public float Minimum;
public float Default;
}
public class CubismPhysicsNormalization
{
public CubismPhysicsNormalizationTuplet Position;
public CubismPhysicsNormalizationTuplet Angle;
}
public class CubismPhysicsParticle
{
public Vector2 InitialPosition;
public float Mobility;
public float Delay;
public float Acceleration;
public float Radius;
}
public class CubismPhysicsOutput
{
public string DestinationId;
public int ParticleIndex;
public Vector2 TranslationScale;
public float AngleScale;
public float Weight;
public CubismPhysicsSourceComponent SourceComponent;
public bool IsInverted;
}
public enum CubismPhysicsSourceComponent
{
X,
Y,
Angle,
}
public class CubismPhysicsInput
{
public string SourceId;
public Vector2 ScaleOfTranslation;
public float AngleScale;
public float Weight;
public CubismPhysicsSourceComponent SourceComponent;
public bool IsInverted;
}
public class CubismPhysicsSubRig
{
public CubismPhysicsInput[] Input;
public CubismPhysicsOutput[] Output;
public CubismPhysicsParticle[] Particles;
public CubismPhysicsNormalization Normalization;
}
public class CubismPhysicsRig
{
public CubismPhysicsSubRig[] SubRigs;
public Vector2 Gravity = new Vector2(0, -1);
public Vector2 Wind;
}
public class CubismPhysics
{
public string m_Name;
public CubismPhysicsRig _rig;
}
}

View File

@ -0,0 +1,62 @@
using System.Collections.Generic;
namespace CubismLive2DExtractor
{
public class ImportedKeyframedAnimation
{
public string Name { get; set; }
public float SampleRate { get; set; }
public float Duration { get; set; }
public List<ImportedAnimationKeyframedTrack> TrackList { get; set; } = new List<ImportedAnimationKeyframedTrack>();
public List<ImportedEvent> Events = new List<ImportedEvent>();
public ImportedAnimationKeyframedTrack FindTrack(string name)
{
var track = TrackList.Find(x => x.Name == name);
if (track == null)
{
track = new ImportedAnimationKeyframedTrack { Name = name };
TrackList.Add(track);
}
return track;
}
}
public class ImportedKeyframe<T>
{
public float time { get; set; }
public T value { get; set; }
public T inSlope { get; set; }
public T outSlope { get; set; }
public float[] coeff { get; set; }
public ImportedKeyframe(float time, T value, T inSlope, T outSlope, float[] coeff)
{
this.time = time;
this.value = value;
this.inSlope = inSlope;
this.outSlope = outSlope;
this.coeff = coeff;
}
public float Evaluate(float sampleTime)
{
float t = sampleTime - time;
return (t * (t * (t * coeff[0] + coeff[1]) + coeff[2])) + coeff[3];
}
}
public class ImportedAnimationKeyframedTrack
{
public string Name { get; set; }
public string Target { get; set; }
public List<ImportedKeyframe<float>> Curve = new List<ImportedKeyframe<float>>();
}
public class ImportedEvent
{
public float time { get; set; }
public string value { get; set; }
}
}

View File

@ -0,0 +1,431 @@
////
// Based on UnityLive2DExtractorMod by aelurum
// https://github.com/aelurum/UnityLive2DExtractor
//
// Original version - by Perfare
// https://github.com/Perfare/UnityLive2DExtractor
////
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AssetStudio;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace CubismLive2DExtractor
{
public static class Live2DExtractor
{
public static void ExtractLive2D(IGrouping<string, AssetStudio.Object> assets, string destPath, string modelName)
{
var destTexturePath = Path.Combine(destPath, "textures") + Path.DirectorySeparatorChar;
var destMotionPath = Path.Combine(destPath, "motions") + Path.DirectorySeparatorChar;
var destExpressionPath = Path.Combine(destPath, "expressions") + Path.DirectorySeparatorChar;
Directory.CreateDirectory(destPath);
Directory.CreateDirectory(destTexturePath);
var monoBehaviours = new List<MonoBehaviour>();
var texture2Ds = new List<Texture2D>();
var gameObjects = new List<GameObject>();
var animationClips = new List<AnimationClip>();
foreach (var asset in assets)
{
switch (asset)
{
case MonoBehaviour m_MonoBehaviour:
monoBehaviours.Add(m_MonoBehaviour);
break;
case Texture2D m_Texture2D:
texture2Ds.Add(m_Texture2D);
break;
case GameObject m_GameObject:
gameObjects.Add(m_GameObject);
break;
case AnimationClip m_AnimationClip:
animationClips.Add(m_AnimationClip);
break;
}
}
//physics
var physics = monoBehaviours.FirstOrDefault(x =>
{
if (x.m_Script.TryGet(out var m_Script))
{
return m_Script.m_ClassName == "CubismPhysicsController";
}
return false;
});
if (physics != null)
{
try
{
var buff = ParsePhysics(physics);
File.WriteAllText($"{destPath}{modelName}.physics3.json", buff);
}
catch (Exception e)
{
Logger.Warning($"Error in parsing physics data: {e.Message}");
physics = null;
}
}
//moc
var moc = monoBehaviours.First(x =>
{
if (x.m_Script.TryGet(out var m_Script))
{
return m_Script.m_ClassName == "CubismMoc";
}
return false;
});
File.WriteAllBytes($"{destPath}{modelName}.moc3", ParseMoc(moc));
//texture
var textures = new SortedSet<string>();
foreach (var texture2D in texture2Ds)
{
using (var image = texture2D.ConvertToImage(flip: true))
{
textures.Add($"textures/{texture2D.m_Name}.png");
using (var file = File.OpenWrite($"{destTexturePath}{texture2D.m_Name}.png"))
{
image.WriteToStream(file, ImageFormat.Png);
}
}
}
//motion
var motions = new JArray();
if (gameObjects.Count > 0)
{
var rootTransform = gameObjects[0].m_Transform;
while (rootTransform.m_Father.TryGet(out var m_Father))
{
rootTransform = m_Father;
}
rootTransform.m_GameObject.TryGet(out var rootGameObject);
var converter = new CubismMotion3Converter(rootGameObject, animationClips.ToArray());
if (converter.AnimationList.Count > 0)
{
Directory.CreateDirectory(destMotionPath);
}
foreach (ImportedKeyframedAnimation animation in converter.AnimationList)
{
var json = new CubismMotion3Json
{
Version = 3,
Meta = new CubismMotion3Json.SerializableMeta
{
Duration = animation.Duration,
Fps = animation.SampleRate,
Loop = true,
AreBeziersRestricted = true,
CurveCount = animation.TrackList.Count,
UserDataCount = animation.Events.Count
},
Curves = new CubismMotion3Json.SerializableCurve[animation.TrackList.Count]
};
int totalSegmentCount = 1;
int totalPointCount = 1;
for (int i = 0; i < animation.TrackList.Count; i++)
{
var track = animation.TrackList[i];
json.Curves[i] = new CubismMotion3Json.SerializableCurve
{
Target = track.Target,
Id = track.Name,
Segments = new List<float> { 0f, track.Curve[0].value }
};
for (var j = 1; j < track.Curve.Count; j++)
{
var curve = track.Curve[j];
var preCurve = track.Curve[j - 1];
if (Math.Abs(curve.time - preCurve.time - 0.01f) < 0.0001f) //InverseSteppedSegment
{
var nextCurve = track.Curve[j + 1];
if (nextCurve.value == curve.value)
{
json.Curves[i].Segments.Add(3f);
json.Curves[i].Segments.Add(nextCurve.time);
json.Curves[i].Segments.Add(nextCurve.value);
j += 1;
totalPointCount += 1;
totalSegmentCount++;
continue;
}
}
if (float.IsPositiveInfinity(curve.inSlope)) //SteppedSegment
{
json.Curves[i].Segments.Add(2f);
json.Curves[i].Segments.Add(curve.time);
json.Curves[i].Segments.Add(curve.value);
totalPointCount += 1;
}
else if (preCurve.outSlope == 0f && Math.Abs(curve.inSlope) < 0.0001f) //LinearSegment
{
json.Curves[i].Segments.Add(0f);
json.Curves[i].Segments.Add(curve.time);
json.Curves[i].Segments.Add(curve.value);
totalPointCount += 1;
}
else //BezierSegment
{
var tangentLength = (curve.time - preCurve.time) / 3f;
json.Curves[i].Segments.Add(1f);
json.Curves[i].Segments.Add(preCurve.time + tangentLength);
json.Curves[i].Segments.Add(preCurve.outSlope * tangentLength + preCurve.value);
json.Curves[i].Segments.Add(curve.time - tangentLength);
json.Curves[i].Segments.Add(curve.value - curve.inSlope * tangentLength);
json.Curves[i].Segments.Add(curve.time);
json.Curves[i].Segments.Add(curve.value);
totalPointCount += 3;
}
totalSegmentCount++;
}
}
json.Meta.TotalSegmentCount = totalSegmentCount;
json.Meta.TotalPointCount = totalPointCount;
json.UserData = new CubismMotion3Json.SerializableUserData[animation.Events.Count];
var totalUserDataSize = 0;
for (var i = 0; i < animation.Events.Count; i++)
{
var @event = animation.Events[i];
json.UserData[i] = new CubismMotion3Json.SerializableUserData
{
Time = @event.time,
Value = @event.value
};
totalUserDataSize += @event.value.Length;
}
json.Meta.TotalUserDataSize = totalUserDataSize;
motions.Add(new JObject
{
{ "Name", animation.Name },
{ "File", $"motions/{animation.Name}.motion3.json" }
});
File.WriteAllText($"{destMotionPath}{animation.Name}.motion3.json", JsonConvert.SerializeObject(json, Formatting.Indented, new MyJsonConverter()));
}
}
//expression
var expressions = new JArray();
var monoBehaviourArray = monoBehaviours.Where(x => x.m_Name.EndsWith(".exp3")).ToArray();
if (monoBehaviourArray.Length > 0)
{
Directory.CreateDirectory(destExpressionPath);
}
foreach (var monoBehaviour in monoBehaviourArray)
{
var fullName = monoBehaviour.m_Name;
var expressionName = fullName.Replace(".exp3", "");
var expressionObj = monoBehaviour.ToType();
if (expressionObj == null)
continue;
var expression = JsonConvert.DeserializeObject<CubismExpression3Json>(JsonConvert.SerializeObject(expressionObj));
expressions.Add(new JObject
{
{ "Name", expressionName },
{ "File", $"expressions/{fullName}.json" }
});
File.WriteAllText($"{destExpressionPath}{fullName}.json", JsonConvert.SerializeObject(expression, Formatting.Indented));
}
//model
var groups = new List<CubismModel3Json.SerializableGroup>();
var eyeBlinkParameters = monoBehaviours.Where(x =>
{
x.m_Script.TryGet(out var m_Script);
return m_Script?.m_ClassName == "CubismEyeBlinkParameter";
}).Select(x =>
{
x.m_GameObject.TryGet(out var m_GameObject);
return m_GameObject?.m_Name;
}).ToHashSet();
if (eyeBlinkParameters.Count == 0)
{
eyeBlinkParameters = gameObjects.Where(x =>
{
return x.m_Name.ToLower().Contains("eye")
&& x.m_Name.ToLower().Contains("open")
&& (x.m_Name.ToLower().Contains('l') || x.m_Name.ToLower().Contains('r'));
}).Select(x => x.m_Name).ToHashSet();
}
groups.Add(new CubismModel3Json.SerializableGroup
{
Target = "Parameter",
Name = "EyeBlink",
Ids = eyeBlinkParameters.ToArray()
});
var lipSyncParameters = monoBehaviours.Where(x =>
{
x.m_Script.TryGet(out var m_Script);
return m_Script?.m_ClassName == "CubismMouthParameter";
}).Select(x =>
{
x.m_GameObject.TryGet(out var m_GameObject);
return m_GameObject?.m_Name;
}).ToHashSet();
if (lipSyncParameters.Count == 0)
{
lipSyncParameters = gameObjects.Where(x =>
{
return x.m_Name.ToLower().Contains("mouth")
&& x.m_Name.ToLower().Contains("open")
&& x.m_Name.ToLower().Contains('y');
}).Select(x => x.m_Name).ToHashSet();
}
groups.Add(new CubismModel3Json.SerializableGroup
{
Target = "Parameter",
Name = "LipSync",
Ids = lipSyncParameters.ToArray()
});
var model3 = new CubismModel3Json
{
Version = 3,
Name = modelName,
FileReferences = new CubismModel3Json.SerializableFileReferences
{
Moc = $"{modelName}.moc3",
Textures = textures.ToArray(),
Motions = new JObject { { "", motions } },
Expressions = expressions,
},
Groups = groups.ToArray()
};
if (physics != null)
{
model3.FileReferences.Physics = $"{modelName}.physics3.json";
}
File.WriteAllText($"{destPath}{modelName}.model3.json", JsonConvert.SerializeObject(model3, Formatting.Indented));
}
private static string ParsePhysics(MonoBehaviour physics)
{
var physicsObj = physics.ToType();
if (physicsObj == null)
throw new Exception("MonoBehaviour is not readable.");
var cubismPhysicsRig = JsonConvert.DeserializeObject<CubismPhysics>(JsonConvert.SerializeObject(physicsObj))._rig;
var physicsSettings = new CubismPhysics3Json.SerializablePhysicsSettings[cubismPhysicsRig.SubRigs.Length];
for (int i = 0; i < physicsSettings.Length; i++)
{
var subRigs = cubismPhysicsRig.SubRigs[i];
physicsSettings[i] = new CubismPhysics3Json.SerializablePhysicsSettings
{
Id = $"PhysicsSetting{i + 1}",
Input = new CubismPhysics3Json.SerializableInput[subRigs.Input.Length],
Output = new CubismPhysics3Json.SerializableOutput[subRigs.Output.Length],
Vertices = new CubismPhysics3Json.SerializableVertex[subRigs.Particles.Length],
Normalization = new CubismPhysics3Json.SerializableNormalization
{
Position = new CubismPhysics3Json.SerializableNormalizationValue
{
Minimum = subRigs.Normalization.Position.Minimum,
Default = subRigs.Normalization.Position.Default,
Maximum = subRigs.Normalization.Position.Maximum
},
Angle = new CubismPhysics3Json.SerializableNormalizationValue
{
Minimum = subRigs.Normalization.Angle.Minimum,
Default = subRigs.Normalization.Angle.Default,
Maximum = subRigs.Normalization.Angle.Maximum
}
}
};
for (int j = 0; j < subRigs.Input.Length; j++)
{
var input = subRigs.Input[j];
physicsSettings[i].Input[j] = new CubismPhysics3Json.SerializableInput
{
Source = new CubismPhysics3Json.SerializableParameter
{
Target = "Parameter", //同名GameObject父节点的名称
Id = input.SourceId
},
Weight = input.Weight,
Type = Enum.GetName(typeof(CubismPhysicsSourceComponent), input.SourceComponent),
Reflect = input.IsInverted
};
}
for (int j = 0; j < subRigs.Output.Length; j++)
{
var output = subRigs.Output[j];
physicsSettings[i].Output[j] = new CubismPhysics3Json.SerializableOutput
{
Destination = new CubismPhysics3Json.SerializableParameter
{
Target = "Parameter", //同名GameObject父节点的名称
Id = output.DestinationId
},
VertexIndex = output.ParticleIndex,
Scale = output.AngleScale,
Weight = output.Weight,
Type = Enum.GetName(typeof(CubismPhysicsSourceComponent), output.SourceComponent),
Reflect = output.IsInverted
};
}
for (int j = 0; j < subRigs.Particles.Length; j++)
{
var particles = subRigs.Particles[j];
physicsSettings[i].Vertices[j] = new CubismPhysics3Json.SerializableVertex
{
Position = particles.InitialPosition,
Mobility = particles.Mobility,
Delay = particles.Delay,
Acceleration = particles.Acceleration,
Radius = particles.Radius
};
}
}
var physicsDictionary = new CubismPhysics3Json.SerializablePhysicsDictionary[physicsSettings.Length];
for (int i = 0; i < physicsSettings.Length; i++)
{
physicsDictionary[i] = new CubismPhysics3Json.SerializablePhysicsDictionary
{
Id = $"PhysicsSetting{i + 1}",
Name = $"Dummy{i + 1}"
};
}
var physicsJson = new CubismPhysics3Json
{
Version = 3,
Meta = new CubismPhysics3Json.SerializableMeta
{
PhysicsSettingCount = cubismPhysicsRig.SubRigs.Length,
TotalInputCount = cubismPhysicsRig.SubRigs.Sum(x => x.Input.Length),
TotalOutputCount = cubismPhysicsRig.SubRigs.Sum(x => x.Output.Length),
VertexCount = cubismPhysicsRig.SubRigs.Sum(x => x.Particles.Length),
EffectiveForces = new CubismPhysics3Json.SerializableEffectiveForces
{
Gravity = cubismPhysicsRig.Gravity,
Wind = cubismPhysicsRig.Wind
},
PhysicsDictionary = physicsDictionary
},
PhysicsSettings = physicsSettings
};
return JsonConvert.SerializeObject(physicsJson, Formatting.Indented, new MyJsonConverter2());
}
private static byte[] ParseMoc(MonoBehaviour moc)
{
var reader = moc.reader;
reader.Reset();
reader.Position += 28; //PPtr<GameObject> m_GameObject, m_Enabled, PPtr<MonoScript>
reader.ReadAlignedString(); //m_Name
return reader.ReadBytes(reader.ReadInt32());
}
}
}

View File

@ -0,0 +1,35 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
namespace CubismLive2DExtractor
{
public class MyJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(List<float>);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteStartArray();
Convert(writer, (List<float>)value);
writer.WriteEndArray();
}
private void Convert(JsonWriter writer, List<float> array)
{
foreach (var n in array)
{
var v = n.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture);
writer.WriteRawValue(v);
}
}
}
}

View File

@ -0,0 +1,28 @@
using System;
using Newtonsoft.Json;
namespace CubismLive2DExtractor
{
public class MyJsonConverter2 : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(float);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
Convert(writer, (float)value);
}
private void Convert(JsonWriter writer, float value)
{
writer.WriteRawValue(value.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture));
}
}
}