Add split object fbx export to CLI

This commit is contained in:
svn 2023-08-31 21:07:02 +01:00
parent 5ac597c935
commit 7cca301f7a
No known key found for this signature in database
GPG Key ID: 88BD0ABBAE424364
8 changed files with 301 additions and 0 deletions

View File

@ -13,6 +13,7 @@ namespace AssetStudioCLI
public ClassIDType Type;
public string Text;
public string UniqueID;
public GameObjectNode Node;
public AssetItem(Object asset)
{

View File

@ -0,0 +1,16 @@
using AssetStudio;
using System.Collections.Generic;
namespace AssetStudioCLI
{
internal class BaseNode
{
public List<BaseNode> nodes = new List<BaseNode>();
public BaseNode()
{
}
}
}

View File

@ -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;
}
}
}

View File

@ -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<AssetItem> 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))

View File

@ -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<ImageFormat> o_imageFormat;
public static Option<AudioFormat> o_audioFormat;
//fbx
public static Option<float> o_fbxScaleFactor;
public static Option<int> o_fbxBoneSize;
//advanced
public static Option<ExportListType> o_exportAssetList;
public static Option<List<string>> 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<float>
(
optionDefaultValue: 1f,
optionName: "--fbx-scale-factor <value>",
optionDescription: "Specify the FBX Scale Factor\n" +
"<Value: float number from 0 to 100 (default=1)\n" +
"Example: \"--fbx-scale-factor 50\"\n",
optionHelpGroup: HelpGroups.FBX
);
o_fbxBoneSize = new GroupedOption<int>
(
optionDefaultValue: 10,
optionName: "--fbx-bone-size <value>",
optionDescription: "Specify the FBX Bone Size\n" +
"<Value: integer number from 0 to 100 (default=10)\n" +
"Example: \"--fbx-bone-size 10\"",
optionHelpGroup: HelpGroups.FBX
);
#endregion
#region Init Advanced Options
o_exportAssetList = new GroupedOption<ExportListType>
(
@ -421,6 +448,19 @@ namespace AssetStudioCLI.Options
ClassIDType.Transform,
};
break;
case "splitobjects":
o_workMode.Value = WorkMode.SplitObjects;
o_exportAssetTypes.Value = new List<ClassIDType>()
{
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())
{

View File

@ -51,6 +51,9 @@ namespace AssetStudioCLI
case WorkMode.ExportLive2D:
Studio.ExportLive2D();
break;
case WorkMode.SplitObjects:
Studio.ExportSplitObjects();
break;
default:
Studio.ExportAssets();
break;

View File

@ -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 <value(s)> 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 <value> Specify the FBX Scale Factor
<Value: float number from 0 to 100 (default=1)
Example: "--fbx-scale-factor 50"
--fbx-bone-size <value> Specify the FBX Bone Size
<Value: integer number from 0 to 100 (default=10)
Example: "--fbx-bone-size 10"
Advanced Options:
--export-asset-list <value> Specify the format in which you want to export asset list
<Value: none(default) | xml>

View File

@ -15,6 +15,7 @@ namespace AssetStudioCLI
{
public static AssetsManager assetsManager = new AssetsManager();
public static List<AssetItem> parsedAssetsList = new List<AssetItem>();
public static List<BaseNode> gameObjectTree = new List<BaseNode>();
public static AssemblyLoader assemblyLoader = new AssemblyLoader();
private static Dictionary<AssetStudio.Object, string> containers = new Dictionary<AssetStudio.Object, string>();
@ -53,6 +54,7 @@ namespace AssetStudioCLI
var fileAssetsList = new List<AssetItem>();
var objectCount = assetsManager.assetsFileList.Sum(x => x.Objects.Count);
var objectAssetItemDic = new Dictionary<AssetStudio.Object, AssetItem>(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<AssetStudio.Object, AssetItem> objectAssetItemDic)
{
foreach (var entry in containers)
{
objectAssetItemDic[entry.Key].Container = entry.Value;
}
containers.Clear();
Logger.Info("Building tree structure...");
var treeNodeDictionary = new Dictionary<GameObject, GameObjectNode>();
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<ClassIDType, int>();
@ -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<GameObject>();
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<GameObject> 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");