From 5ac597c935c2830b3ecb1b9f25b65cd2fb6d498e Mon Sep 17 00:00:00 2001 From: svn Date: Thu, 31 Aug 2023 21:03:07 +0100 Subject: [PATCH 1/2] Add CMakeLists for cross platform FBXNative build --- AssetStudioCLI/AssetStudioCLI.csproj | 11 +++++++++++ AssetStudioFBXNative/CMakeLists.txt | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 AssetStudioFBXNative/CMakeLists.txt diff --git a/AssetStudioCLI/AssetStudioCLI.csproj b/AssetStudioCLI/AssetStudioCLI.csproj index 326ad44..05c1d90 100644 --- a/AssetStudioCLI/AssetStudioCLI.csproj +++ b/AssetStudioCLI/AssetStudioCLI.csproj @@ -26,12 +26,16 @@ + + + + @@ -59,17 +63,20 @@ + + + @@ -77,21 +84,25 @@ + + + + diff --git a/AssetStudioFBXNative/CMakeLists.txt b/AssetStudioFBXNative/CMakeLists.txt new file mode 100644 index 0000000..f90011d --- /dev/null +++ b/AssetStudioFBXNative/CMakeLists.txt @@ -0,0 +1,22 @@ +# Set the minimum version of CMake that can be used +cmake_minimum_required (VERSION 3.8) + +# Set the project name +project("AssetStudioFBXNative") + +# Set the C++ standard to C++ 14 +set(CMAKE_CXX_STANDARD 14) + +# Generate the shared library from the library sources +add_library(AssetStudioFBXNative SHARED + asfbx_skin_context.cpp + asfbx_morph_context.cpp + api.cpp + utils.cpp + asfbx_context.cpp + asfbx_anim_context.cpp) + +# Add the given directories to those the compiler uses to search for include files +target_include_directories(AssetStudioFBXNative PRIVATE .) + +target_link_libraries(AssetStudioFBXNative PRIVATE fbxsdk xml2) \ No newline at end of file From 7cca301f7a7a43cd2286fcbd6bf244ee2e92294c Mon Sep 17 00:00:00 2001 From: svn Date: Thu, 31 Aug 2023 21:07:02 +0100 Subject: [PATCH 2/2] Add split object fbx export to CLI --- AssetStudioCLI/Components/AssetItem.cs | 1 + AssetStudioCLI/Components/BaseNode.cs | 16 ++ AssetStudioCLI/Components/GameObjectNode.cs | 16 ++ AssetStudioCLI/Exporter.cs | 29 ++++ AssetStudioCLI/Options/CLIOptions.cs | 66 ++++++++ AssetStudioCLI/Program.cs | 3 + AssetStudioCLI/ReadMe.md | 10 ++ AssetStudioCLI/Studio.cs | 160 ++++++++++++++++++++ 8 files changed, 301 insertions(+) create mode 100644 AssetStudioCLI/Components/BaseNode.cs create mode 100644 AssetStudioCLI/Components/GameObjectNode.cs diff --git a/AssetStudioCLI/Components/AssetItem.cs b/AssetStudioCLI/Components/AssetItem.cs index 00f929a..b167c3a 100644 --- a/AssetStudioCLI/Components/AssetItem.cs +++ b/AssetStudioCLI/Components/AssetItem.cs @@ -13,6 +13,7 @@ namespace AssetStudioCLI public ClassIDType Type; public string Text; public string UniqueID; + public GameObjectNode Node; public AssetItem(Object asset) { diff --git a/AssetStudioCLI/Components/BaseNode.cs b/AssetStudioCLI/Components/BaseNode.cs new file mode 100644 index 0000000..ecc3a60 --- /dev/null +++ b/AssetStudioCLI/Components/BaseNode.cs @@ -0,0 +1,16 @@ +using AssetStudio; +using System.Collections.Generic; + +namespace AssetStudioCLI +{ + internal class BaseNode + { + public List nodes = new List(); + + public BaseNode() + { + + } + + } +} diff --git a/AssetStudioCLI/Components/GameObjectNode.cs b/AssetStudioCLI/Components/GameObjectNode.cs new file mode 100644 index 0000000..4d88ee0 --- /dev/null +++ b/AssetStudioCLI/Components/GameObjectNode.cs @@ -0,0 +1,16 @@ +using AssetStudio; +using System.Collections.Generic; + +namespace AssetStudioCLI +{ + internal class GameObjectNode : BaseNode + { + public GameObject gameObject; + + public GameObjectNode(GameObject gameObject) + { + this.gameObject = gameObject; + } + + } +} diff --git a/AssetStudioCLI/Exporter.cs b/AssetStudioCLI/Exporter.cs index 94ce07c..ca7ac23 100644 --- a/AssetStudioCLI/Exporter.cs +++ b/AssetStudioCLI/Exporter.cs @@ -1,6 +1,7 @@ using AssetStudio; using AssetStudioCLI.Options; using Newtonsoft.Json; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; @@ -264,6 +265,34 @@ namespace AssetStudioCLI return true; } + public static void ExportGameObject(GameObject gameObject, string exportPath, List animationList = null) + { + var convert = animationList != null + ? new ModelConverter(gameObject, CLIOptions.o_imageFormat.Value, animationList.Select(x => (AnimationClip)x.Asset).ToArray()) + : new ModelConverter(gameObject, CLIOptions.o_imageFormat.Value); + exportPath = exportPath + FixFileName(gameObject.m_Name) + ".fbx"; + ExportFbx(convert, exportPath); + } + + private static void ExportFbx(IImported convert, string exportPath) + { + var eulerFilter = true; + var filterPrecision = (float)0.25f; + var exportAllNodes = true; + var exportSkins = true; + var exportAnimations = true; + var exportBlendShape = true; + var castToBone = false; + var boneSize = CLIOptions.o_fbxBoneSize.Value; + var exportAllUvsAsDiffuseMaps = false; + var scaleFactor = CLIOptions.o_fbxScaleFactor.Value; + var fbxVersion = 3; + var fbxFormat = 0; + ModelExporter.ExportFbx(exportPath, convert, eulerFilter, filterPrecision, + exportAllNodes, exportSkins, exportAnimations, exportBlendShape, castToBone, boneSize, exportAllUvsAsDiffuseMaps, scaleFactor, fbxVersion, fbxFormat == 1); + } + + public static bool ExportDumpFile(AssetItem item, string exportPath) { if (!TryExportFile(exportPath, item, ".txt", out var exportFullPath)) diff --git a/AssetStudioCLI/Options/CLIOptions.cs b/AssetStudioCLI/Options/CLIOptions.cs index f8b8de9..477e064 100644 --- a/AssetStudioCLI/Options/CLIOptions.cs +++ b/AssetStudioCLI/Options/CLIOptions.cs @@ -12,6 +12,7 @@ namespace AssetStudioCLI.Options General, Convert, Logger, + FBX, Advanced, } @@ -22,6 +23,7 @@ namespace AssetStudioCLI.Options Dump, Info, ExportLive2D, + SplitObjects, } internal enum AssetGroupOption @@ -79,6 +81,9 @@ namespace AssetStudioCLI.Options public static bool convertTexture; public static Option o_imageFormat; public static Option o_audioFormat; + //fbx + public static Option o_fbxScaleFactor; + public static Option o_fbxBoneSize; //advanced public static Option o_exportAssetList; public static Option> o_filterByName; @@ -158,6 +163,7 @@ namespace AssetStudioCLI.Options "Dump - Makes asset dumps\n" + "Info - Loads file(s), shows the number of available for export assets and exits\n" + "Live2D - Exports Live2D Cubism 3 models\n" + + "SplitObjects - Export split objects\n" + "Example: \"-m info\"\n", optionHelpGroup: HelpGroups.General ); @@ -249,6 +255,27 @@ namespace AssetStudioCLI.Options ); #endregion + #region Init FBX Options + o_fbxScaleFactor = new GroupedOption + ( + optionDefaultValue: 1f, + optionName: "--fbx-scale-factor ", + optionDescription: "Specify the FBX Scale Factor\n" + + " + ( + optionDefaultValue: 10, + optionName: "--fbx-bone-size ", + optionDescription: "Specify the FBX Bone Size\n" + + " ( @@ -421,6 +448,19 @@ namespace AssetStudioCLI.Options ClassIDType.Transform, }; break; + case "splitobjects": + o_workMode.Value = WorkMode.SplitObjects; + o_exportAssetTypes.Value = new List() + { + ClassIDType.GameObject, + ClassIDType.Texture2D, + ClassIDType.Material, + ClassIDType.Transform, + ClassIDType.Mesh, + ClassIDType.MeshRenderer, + ClassIDType.MeshFilter + }; + break; default: Console.WriteLine($"{"Error".Color(brightRed)} during parsing [{option}] option. Unsupported working mode: [{value.Color(brightRed)}].\n"); Console.WriteLine(o_workMode.Description); @@ -617,6 +657,32 @@ namespace AssetStudioCLI.Options return; } break; + case "--fbx-scale-factor": + var isFloat = float.TryParse(value, out float floatValue); + if (isFloat && floatValue >= 0 && floatValue <= 100) + { + o_fbxScaleFactor.Value = floatValue; + } + else + { + Console.WriteLine($"{"Error".Color(brightRed)} during parsing [{option}] option. Unsupported scale factor value: [{value.Color(brightRed)}].\n"); + Console.WriteLine(o_fbxScaleFactor.Description); + return; + } + break; + case "--fbx-bone-size": + var isInt = int.TryParse(value, out int intValue); + if (isInt && intValue >= 0 && intValue <= 100) + { + o_fbxBoneSize.Value = intValue; + } + else + { + Console.WriteLine($"{"Error".Color(brightRed)} during parsing [{option}] option. Unsupported bone size value: [{value.Color(brightRed)}].\n"); + Console.WriteLine(o_fbxBoneSize.Description); + return; + } + break; case "--export-asset-list": switch (value.ToLower()) { diff --git a/AssetStudioCLI/Program.cs b/AssetStudioCLI/Program.cs index a7d6f01..a1a0cda 100644 --- a/AssetStudioCLI/Program.cs +++ b/AssetStudioCLI/Program.cs @@ -51,6 +51,9 @@ namespace AssetStudioCLI case WorkMode.ExportLive2D: Studio.ExportLive2D(); break; + case WorkMode.SplitObjects: + Studio.ExportSplitObjects(); + break; default: Studio.ExportAssets(); break; diff --git a/AssetStudioCLI/ReadMe.md b/AssetStudioCLI/ReadMe.md index 1f27b91..fad2ef8 100644 --- a/AssetStudioCLI/ReadMe.md +++ b/AssetStudioCLI/ReadMe.md @@ -24,6 +24,7 @@ General Options: Dump - Makes asset dumps Info - Loads file(s), shows the number of available for export assets and exits Live2D - Exports Live2D Cubism 3 models + SplitObjects - Export split objects Example: "-m info" -t, --asset-type Specify asset type(s) to export @@ -67,6 +68,15 @@ Convert Options: None - Do not convert audios and export them in their own format Example: "--audio-format wav" +FBX Options: + --fbx-scale-factor Specify the FBX Scale Factor + Specify the FBX Bone Size + Specify the format in which you want to export asset list diff --git a/AssetStudioCLI/Studio.cs b/AssetStudioCLI/Studio.cs index 6c56b15..4084d22 100644 --- a/AssetStudioCLI/Studio.cs +++ b/AssetStudioCLI/Studio.cs @@ -15,6 +15,7 @@ namespace AssetStudioCLI { public static AssetsManager assetsManager = new AssetsManager(); public static List parsedAssetsList = new List(); + public static List gameObjectTree = new List(); public static AssemblyLoader assemblyLoader = new AssemblyLoader(); private static Dictionary containers = new Dictionary(); @@ -53,6 +54,7 @@ namespace AssetStudioCLI var fileAssetsList = new List(); var objectCount = assetsManager.assetsFileList.Sum(x => x.Objects.Count); + var objectAssetItemDic = new Dictionary(objectCount); Progress.Reset(); var i = 0; @@ -61,6 +63,7 @@ namespace AssetStudioCLI foreach (var asset in assetsFile.Objects) { var assetItem = new AssetItem(asset); + objectAssetItemDic.Add(asset, assetItem); assetItem.UniqueID = "_#" + i; var isExportable = false; switch (asset) @@ -90,6 +93,9 @@ namespace AssetStudioCLI } } break; + case GameObject m_GameObject: + assetItem.Text = m_GameObject.m_Name; + break; case Texture2D m_Texture2D: if (!string.IsNullOrEmpty(m_Texture2D.m_StreamData?.path)) assetItem.FullSize = asset.byteSize + m_Texture2D.m_StreamData.size; @@ -153,6 +159,12 @@ namespace AssetStudioCLI containers.Clear(); } } + + if (CLIOptions.o_workMode.Value == WorkMode.SplitObjects) + { + BuildTreeStructure(objectAssetItemDic); + } + var log = $"Finished loading {assetsManager.assetsFileList.Count} files with {parsedAssetsList.Count} exportable assets"; var unityVer = assetsManager.assetsFileList[0].version; long m_ObjectsCount; @@ -175,6 +187,93 @@ namespace AssetStudioCLI Logger.Info(log); } + public static void BuildTreeStructure(Dictionary objectAssetItemDic) + { + foreach (var entry in containers) + { + objectAssetItemDic[entry.Key].Container = entry.Value; + } + + containers.Clear(); + + Logger.Info("Building tree structure..."); + + var treeNodeDictionary = new Dictionary(); + var assetsFileCount = assetsManager.assetsFileList.Count; + int j = 0; + Progress.Reset(); + foreach (var assetsFile in assetsManager.assetsFileList) + { + var fileNode = new BaseNode(); + + foreach (var obj in assetsFile.Objects) + { + if (obj is GameObject m_GameObject) + { + if (!treeNodeDictionary.TryGetValue(m_GameObject, out var currentNode)) + { + currentNode = new GameObjectNode(m_GameObject); + treeNodeDictionary.Add(m_GameObject, currentNode); + } + + foreach (var pptr in m_GameObject.m_Components) + { + if (pptr.TryGet(out var m_Component)) + { + objectAssetItemDic[m_Component].Node = currentNode; + if (m_Component is MeshFilter m_MeshFilter) + { + if (m_MeshFilter.m_Mesh.TryGet(out var m_Mesh)) + { + objectAssetItemDic[m_Mesh].Node = currentNode; + } + } + else if (m_Component is SkinnedMeshRenderer m_SkinnedMeshRenderer) + { + if (m_SkinnedMeshRenderer.m_Mesh.TryGet(out var m_Mesh)) + { + objectAssetItemDic[m_Mesh].Node = currentNode; + } + } + } + } + + var parentNode = fileNode; + + if (m_GameObject.m_Transform != null) + { + if (m_GameObject.m_Transform.m_Father.TryGet(out var m_Father)) + { + if (m_Father.m_GameObject.TryGet(out var parentGameObject)) + { + if (!treeNodeDictionary.TryGetValue(parentGameObject, out var parentGameObjectNode)) + { + parentGameObjectNode = new GameObjectNode(parentGameObject); + treeNodeDictionary.Add(parentGameObject, parentGameObjectNode); + } + parentNode = parentGameObjectNode; + } + } + } + + parentNode.nodes.Add(currentNode); + + } + } + + if (fileNode.nodes.Count > 0) + { + gameObjectTree.Add(fileNode); + } + + Progress.Report(++j, assetsFileCount); + } + + treeNodeDictionary.Clear(); + + objectAssetItemDic.Clear(); + } + public static void ShowExportableAssetsInfo() { var exportableAssetsCountDict = new Dictionary(); @@ -403,6 +502,67 @@ namespace AssetStudioCLI Logger.Info($"Finished exporting asset list with {parsedAssetsList.Count} items."); } + public static void ExportSplitObjects() + { + var savePath = CLIOptions.o_outputFolder.Value; + var count = parsedAssetsList.Count(); + int k = 0; + Progress.Reset(); + foreach (var node in gameObjectTree) + { + foreach(GameObjectNode j in node.nodes) + { + var gameObjects = new List(); + CollectNode(j, gameObjects); + + if (gameObjects.All(x => x.m_SkinnedMeshRenderer == null && x.m_MeshFilter == null)) + { + Progress.Report(++k, count); + continue; + } + + var filename = FixFileName(j.gameObject.m_Name); + var targetPath = $"{savePath}{filename}{Path.DirectorySeparatorChar}"; + //重名文件处理 + for (int i = 1; ; i++) + { + if (Directory.Exists(targetPath)) + { + targetPath = $"{savePath}{filename} ({i}){Path.DirectorySeparatorChar}"; + } + else + { + break; + } + } + Directory.CreateDirectory(targetPath); + //导出FBX + Logger.Info($"Exporting {filename}.fbx"); + try + { + ExportGameObject(j.gameObject, targetPath); + } + catch (Exception ex) + { + Logger.Error($"Export GameObject:{j.gameObject.m_Name} error", ex); + } + + Progress.Report(++k, count); + Logger.Info($"Finished exporting {filename}.fbx"); + } + } + Logger.Info("Finished"); + } + + private static void CollectNode(GameObjectNode node, List gameObjects) + { + gameObjects.Add(node.gameObject); + foreach (GameObjectNode i in node.nodes) + { + CollectNode(i, gameObjects); + } + } + public static void ExportLive2D() { var baseDestPath = Path.Combine(CLIOptions.o_outputFolder.Value, "Live2DOutput");