mirror of
https://github.com/aelurum/AssetStudio.git
synced 2025-05-25 05:40:21 -04:00
Add option to export Live2D Cubism models
This commit is contained in:
parent
6d41693b85
commit
aea6cbc97f
@ -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,8 +791,24 @@ 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)
|
||||
{
|
||||
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}");
|
||||
@ -787,15 +821,7 @@ namespace AssetStudioCLI.Options
|
||||
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}\"");
|
||||
break;
|
||||
}
|
||||
sb.AppendLine("======");
|
||||
Logger.Info(sb.ToString());
|
||||
|
@ -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)
|
||||
{
|
||||
case WorkMode.Info:
|
||||
studio.ShowExportableAssetsInfo();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case WorkMode.ExportLive2D:
|
||||
studio.ExportLive2D();
|
||||
break;
|
||||
default:
|
||||
studio.ExportAssets();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
20
AssetStudioGUI/AssetStudioGUIForm.Designer.cs
generated
20
AssetStudioGUI/AssetStudioGUIForm.Designer.cs
generated
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
28
AssetStudioUtility/CubismLive2DExtractor/CubismModel3Json.cs
Normal file
28
AssetStudioUtility/CubismLive2DExtractor/CubismModel3Json.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
75
AssetStudioUtility/CubismLive2DExtractor/CubismPhysicsRig.cs
Normal file
75
AssetStudioUtility/CubismLive2DExtractor/CubismPhysicsRig.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
431
AssetStudioUtility/CubismLive2DExtractor/Live2DExtractor.cs
Normal file
431
AssetStudioUtility/CubismLive2DExtractor/Live2DExtractor.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
35
AssetStudioUtility/CubismLive2DExtractor/MyJsonConverter.cs
Normal file
35
AssetStudioUtility/CubismLive2DExtractor/MyJsonConverter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
28
AssetStudioUtility/CubismLive2DExtractor/MyJsonConverter2.cs
Normal file
28
AssetStudioUtility/CubismLive2DExtractor/MyJsonConverter2.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user