diff --git a/AssetStudioCLI/Options/CLIOptions.cs b/AssetStudioCLI/Options/CLIOptions.cs index bf46e6c..0480f65 100644 --- a/AssetStudioCLI/Options/CLIOptions.cs +++ b/AssetStudioCLI/Options/CLIOptions.cs @@ -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 ", optionDescription: "Specify working mode\n" + - "\n" + + "\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.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(); 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()); diff --git a/AssetStudioCLI/Program.cs b/AssetStudioCLI/Program.cs index 6342fc6..5e0f094 100644 --- a/AssetStudioCLI/Program.cs +++ b/AssetStudioCLI/Program.cs @@ -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) diff --git a/AssetStudioCLI/Studio.cs b/AssetStudioCLI/Studio.cs index c4414ab..f63ee22 100644 --- a/AssetStudioCLI/Studio.cs +++ b/AssetStudioCLI/Studio.cs @@ -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 parsedAssetsList = new List(); + private static Dictionary containers = new Dictionary(); private readonly CLIOptions options; public Studio(CLIOptions cliOptions) @@ -51,7 +53,6 @@ namespace AssetStudioCLI Logger.Info("Parse assets..."); var fileAssetsList = new List(); - var containers = new Dictionary(); 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); + } } } diff --git a/AssetStudioGUI/AssetStudioGUIForm.Designer.cs b/AssetStudioGUI/AssetStudioGUIForm.Designer.cs index 518ab0d..3d91ffd 100644 --- a/AssetStudioGUI/AssetStudioGUIForm.Designer.cs +++ b/AssetStudioGUI/AssetStudioGUIForm.Designer.cs @@ -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; } } diff --git a/AssetStudioGUI/AssetStudioGUIForm.cs b/AssetStudioGUI/AssetStudioGUIForm.cs index b158d1f..5f31b85 100644 --- a/AssetStudioGUI/AssetStudioGUIForm.cs +++ b/AssetStudioGUI/AssetStudioGUIForm.cs @@ -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() { diff --git a/AssetStudioGUI/Studio.cs b/AssetStudioGUI/Studio.cs index a1dc2b5..33e05aa 100644 --- a/AssetStudioGUI/Studio.cs +++ b/AssetStudioGUI/Studio.cs @@ -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 exportableAssets = new List(); public static List visibleAssets = new List(); + private static Dictionary allContainers = new Dictionary(); internal static Action 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); + } + }); + } } } diff --git a/AssetStudioUtility/AssetStudioUtility.csproj b/AssetStudioUtility/AssetStudioUtility.csproj index 425d6d5..9970658 100644 --- a/AssetStudioUtility/AssetStudioUtility.csproj +++ b/AssetStudioUtility/AssetStudioUtility.csproj @@ -9,6 +9,7 @@ + diff --git a/AssetStudioUtility/CubismLive2DExtractor/CubismExpression3Json.cs b/AssetStudioUtility/CubismLive2DExtractor/CubismExpression3Json.cs new file mode 100644 index 0000000..60c4101 --- /dev/null +++ b/AssetStudioUtility/CubismLive2DExtractor/CubismExpression3Json.cs @@ -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; + } + } +} diff --git a/AssetStudioUtility/CubismLive2DExtractor/CubismModel3Json.cs b/AssetStudioUtility/CubismLive2DExtractor/CubismModel3Json.cs new file mode 100644 index 0000000..c3823f4 --- /dev/null +++ b/AssetStudioUtility/CubismLive2DExtractor/CubismModel3Json.cs @@ -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; + } + } +} diff --git a/AssetStudioUtility/CubismLive2DExtractor/CubismMotion3Converter.cs b/AssetStudioUtility/CubismLive2DExtractor/CubismMotion3Converter.cs new file mode 100644 index 0000000..34590d7 --- /dev/null +++ b/AssetStudioUtility/CubismLive2DExtractor/CubismMotion3Converter.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using AssetStudio; + +namespace CubismLive2DExtractor +{ + class CubismMotion3Converter + { + private Dictionary bonePathHash = new Dictionary(); + public List AnimationList { get; protected set; } = new List(); + + 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(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(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; + } + } +} diff --git a/AssetStudioUtility/CubismLive2DExtractor/CubismMotion3Json.cs b/AssetStudioUtility/CubismLive2DExtractor/CubismMotion3Json.cs new file mode 100644 index 0000000..c5cd623 --- /dev/null +++ b/AssetStudioUtility/CubismLive2DExtractor/CubismMotion3Json.cs @@ -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 Segments; + }; + + public class SerializableUserData + { + public float Time; + public string Value; + } + } +} diff --git a/AssetStudioUtility/CubismLive2DExtractor/CubismPhysics3Json.cs b/AssetStudioUtility/CubismLive2DExtractor/CubismPhysics3Json.cs new file mode 100644 index 0000000..3205b14 --- /dev/null +++ b/AssetStudioUtility/CubismLive2DExtractor/CubismPhysics3Json.cs @@ -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; + } + } +} diff --git a/AssetStudioUtility/CubismLive2DExtractor/CubismPhysicsRig.cs b/AssetStudioUtility/CubismLive2DExtractor/CubismPhysicsRig.cs new file mode 100644 index 0000000..d9c7219 --- /dev/null +++ b/AssetStudioUtility/CubismLive2DExtractor/CubismPhysicsRig.cs @@ -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; + } +} diff --git a/AssetStudioUtility/CubismLive2DExtractor/ImportedKeyframedAnimation.cs b/AssetStudioUtility/CubismLive2DExtractor/ImportedKeyframedAnimation.cs new file mode 100644 index 0000000..624df52 --- /dev/null +++ b/AssetStudioUtility/CubismLive2DExtractor/ImportedKeyframedAnimation.cs @@ -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 TrackList { get; set; } = new List(); + public List Events = new List(); + + 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 + { + 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> Curve = new List>(); + } + + public class ImportedEvent + { + public float time { get; set; } + public string value { get; set; } + } +} diff --git a/AssetStudioUtility/CubismLive2DExtractor/Live2DExtractor.cs b/AssetStudioUtility/CubismLive2DExtractor/Live2DExtractor.cs new file mode 100644 index 0000000..a20aefd --- /dev/null +++ b/AssetStudioUtility/CubismLive2DExtractor/Live2DExtractor.cs @@ -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 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(); + var texture2Ds = new List(); + var gameObjects = new List(); + var animationClips = new List(); + + 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(); + 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 { 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(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(); + + 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(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 m_GameObject, m_Enabled, PPtr + reader.ReadAlignedString(); //m_Name + return reader.ReadBytes(reader.ReadInt32()); + } + } +} diff --git a/AssetStudioUtility/CubismLive2DExtractor/MyJsonConverter.cs b/AssetStudioUtility/CubismLive2DExtractor/MyJsonConverter.cs new file mode 100644 index 0000000..c8056e9 --- /dev/null +++ b/AssetStudioUtility/CubismLive2DExtractor/MyJsonConverter.cs @@ -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); + } + + 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)value); + writer.WriteEndArray(); + } + + private void Convert(JsonWriter writer, List array) + { + foreach (var n in array) + { + var v = n.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture); + writer.WriteRawValue(v); + } + } + } +} \ No newline at end of file diff --git a/AssetStudioUtility/CubismLive2DExtractor/MyJsonConverter2.cs b/AssetStudioUtility/CubismLive2DExtractor/MyJsonConverter2.cs new file mode 100644 index 0000000..89e4256 --- /dev/null +++ b/AssetStudioUtility/CubismLive2DExtractor/MyJsonConverter2.cs @@ -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)); + } + } +}