패키지 매니저에 FBX Exporter를 추가하면 사용 가능.

게임오브젝트 FBX로 내보내거나, FBX 파일에 메시가 여러 개 들어있을 때  한 개 씩 분리해야 할 일이있어서 만듬.

using UnityEngine;
using UnityEditor;
using UnityEditor.Formats.Fbx.Exporter;
using System.Collections.Generic;
using System.IO;
using System.Linq;

public class FbxExporterWindow : EditorWindow {
    private GameObject tPoseMesh;
    private GameObject exportObject;
    private Avatar referenceAvatar;
    private string exportPath;
    private string avatarPath;
    private const string exportPathKey = "FBXExporterExportPath";
    private const string avatarPathKey = "FBXExporterAvatarPath";

    [MenuItem("Window/Tools/FBX Exporter")]
    public static void ShowWindow()
    {
        var w = GetWindow<FbxExporterWindow>(false, "FBX Exporter", true);
    }

    private void OnEnable()
    {
        exportPath = Path.Combine(Application.dataPath, "Exports");
        exportPath = EditorPrefs.GetString(exportPathKey, exportPath);
        avatarPath = Path.Combine(Application.dataPath, "AvatarPaths");
        avatarPath = EditorPrefs.GetString(exportPathKey, exportPath);

    }

    private void OnGUI()
    {
        GUILayout.Label("Export GameObject to FBX", EditorStyles.boldLabel);

        exportObject = (GameObject)EditorGUILayout.ObjectField("Export Object", exportObject, typeof(GameObject), true);
        if (GUILayout.Button("Export GameObject to FBX")) {
            ExportSelectedGameObject(exportObject);
        }

        if (GUILayout.Button("Extract and Export Individual Meshes")) {
            ExtractAndExportMeshes(exportObject);
        }

        tPoseMesh = EditorGUILayout.ObjectField("T-Pose Mesh", tPoseMesh, typeof(GameObject), true) as GameObject;
        if (GUILayout.Button("Export Combined Mesh And Bones")) {
            ExportSkinnedMeshWithBones(tPoseMesh);
        }

        if (GUILayout.Button("Combine Meshes into T-Pose")) {
            CombineMeshes(tPoseMesh);
        }

        GUILayout.Label("Configure Avatar for Selected Model", EditorStyles.boldLabel);

        referenceAvatar = (Avatar)EditorGUILayout.ObjectField("Reference Avatar", referenceAvatar, typeof(Avatar), false);
        if (GUILayout.Button("Apply Avatar to Folder")) {
            ApplyAvatarToModelsInFolder(referenceAvatar);
        }

        // exportPath 값이 변경될 때마다 EditorPrefs에 저장합니다.
        if (GUI.changed) {
            EditorPrefs.SetString(exportPathKey, exportPath);
            EditorPrefs.SetString(avatarPathKey, avatarPath);
        }
    }

    private void ExportSelectedGameObject(GameObject obj)
    {
        if (obj == null) {
            EditorUtility.DisplayDialog("Error", "Please assign a GameObject to export.", "OK");
            return;
        }

        string filePath = EditorUtility.SaveFilePanel("Export .fbx file", exportPath, obj.name, "fbx");
        if (string.IsNullOrEmpty(filePath)) {
            return;
        }

        // 파일 저장 경로에서 폴더 경로만 추출
        string folderPath = Path.GetDirectoryName(filePath);

        // 해당 폴더가 존재하지 않으면 생성
        if (!AssetDatabase.IsValidFolder(folderPath)) {
            Directory.CreateDirectory(folderPath);
            AssetDatabase.Refresh();
        }

        ModelExporter.ExportObject(filePath, obj);
    }

    private void ExtractAndExportMeshes(GameObject sourceFbx)
    {
        if (sourceFbx == null) {
            EditorUtility.DisplayDialog("Error", "Please assign a GameObject to export.", "OK");
            return;
        }

        var folderPath = EditorUtility.OpenFolderPanel("Choose Export Folder", exportPath, "");
        if (string.IsNullOrEmpty(folderPath)) {
            return;
        }

        Transform rootOriginal = sourceFbx.transform.FindDeepChild("Root");
        if (rootOriginal == null) {
            Debug.LogError("Root bone not found.");
            return;
        }


        foreach (var smr in sourceFbx.GetComponentsInChildren<SkinnedMeshRenderer>(true)) {
            var meshName = smr.sharedMesh.name;
            var meshGameObject = new GameObject(meshName);
            var root = Instantiate(rootOriginal);
            root.name = "Root";
            root.transform.SetParent(meshGameObject.transform, false);

            SkinnedMeshRenderer newSmr = meshGameObject.AddComponent<SkinnedMeshRenderer>();
            newSmr.sharedMesh = smr.sharedMesh;
            //newSmr.sharedMaterials = smr.sharedMaterials;

            List<Transform> bones = new List<Transform>();
            foreach (var bone in smr.bones) {
                bones.Add(root.FindDeepChild(bone.name));
            }
            newSmr.bones = bones.ToArray();
            newSmr.rootBone = root.FindDeepChild(smr.rootBone.name);

            string finalPath = Path.Combine(folderPath, meshName + ".fbx");
            ModelExporter.ExportObject(finalPath, meshGameObject);
            DestroyImmediate(meshGameObject);
        }

        AssetDatabase.Refresh();
    }

    private Mesh[] FindMeshesInSelectedFbx(GameObject sourceFbx)
    {
        List<Mesh> meshes = new List<Mesh>();

        if (sourceFbx != null) {
            // MeshFilter 컴포넌트가 있는 경우
            MeshFilter[] meshFilters = sourceFbx.GetComponentsInChildren<MeshFilter>();
            foreach (MeshFilter mf in meshFilters) {
                if (mf.sharedMesh != null) {
                    meshes.Add(mf.sharedMesh);
                }
            }

            // SkinnedMeshRenderer 컴포넌트가 있는 경우
            SkinnedMeshRenderer[] skinnedMeshRenderers = sourceFbx.GetComponentsInChildren<SkinnedMeshRenderer>();
            foreach (SkinnedMeshRenderer smr in skinnedMeshRenderers) {
                if (smr.sharedMesh != null) {
                    meshes.Add(smr.sharedMesh);
                }
            }
        }

        return meshes.ToArray();
    }

    void ExportSkinnedMeshWithBones(GameObject skinnedMeshObj)
    {
        // 파일 저장 대화 상자를 통해 내보낼 FBX 파일의 경로를 지정합니다.
        string filePath = EditorUtility.SaveFilePanel("Export Skinned Mesh with Bones", exportPath, skinnedMeshObj.name, "fbx");
        if (string.IsNullOrEmpty(filePath)) {
            Debug.LogError("Export path is empty. Cancelling export.");
            return;
        }

        // 스킨드 메시를 복제합니다.
        GameObject clonedSkinnedMeshObj = Instantiate(skinnedMeshObj);
        clonedSkinnedMeshObj.name = skinnedMeshObj.name + "_Cloned";

        // 본 구조를 복제합니다 (이는 복제할 스킨드 메시에 본이 포함된 경우에만 필요합니다).
        Transform[] bones = clonedSkinnedMeshObj.GetComponentInChildren<SkinnedMeshRenderer>().bones;
        Transform clonedRootBone = Instantiate(bones[0].root);
        clonedRootBone.name = bones[0].root.name + "_Cloned";

        // 복제된 스킨드 메시의 본을 복제된 본 구조로 업데이트합니다.
        SkinnedMeshRenderer skinnedMeshRenderer = clonedSkinnedMeshObj.GetComponentInChildren<SkinnedMeshRenderer>();
        skinnedMeshRenderer.rootBone = clonedRootBone;
        skinnedMeshRenderer.bones = clonedRootBone.GetComponentsInChildren<Transform>();

        // FBX로 내보냅니다.
        var isSuccess = ModelExporter.ExportObject(filePath, clonedSkinnedMeshObj);
        if (string.IsNullOrEmpty(isSuccess) == false) {
            Debug.Log("Exported skinned mesh with bones to " + filePath);
        } else {
            Debug.LogError("Failed to export skinned mesh with bones.");
        }

        // 임시 객체를 삭제합니다.
        DestroyImmediate(clonedSkinnedMeshObj);
        DestroyImmediate(clonedRootBone.gameObject);
    }

    private void CombineMeshes(GameObject parentObject)
    {
        // 메시 필터와 스킨드 메시 렌더러에서 모든 메시를 가져옵니다.
        MeshFilter[] meshFilters = parentObject.GetComponentsInChildren<MeshFilter>(true);
        SkinnedMeshRenderer[] skinnedMeshRenderers = parentObject.GetComponentsInChildren<SkinnedMeshRenderer>(true);

        // 결합 인스턴스의 배열을 초기화합니다.
        List<CombineInstance> combine = new List<CombineInstance>();

        // MeshFilter 컴포넌트에서 메시를 추가합니다.
        foreach (var meshFilter in meshFilters) {
            if (meshFilter.sharedMesh != null) {
                CombineInstance ci = new CombineInstance();
                ci.mesh = meshFilter.sharedMesh;
                ci.transform = meshFilter.transform.localToWorldMatrix;
                combine.Add(ci);
            }
        }

        // SkinnedMeshRenderer 컴포넌트에서 메시를 추가합니다.
        foreach (var skinnedMeshRenderer in skinnedMeshRenderers) {
            if (skinnedMeshRenderer.sharedMesh != null) {
                CombineInstance ci = new CombineInstance();
                ci.mesh = skinnedMeshRenderer.sharedMesh;
                ci.transform = skinnedMeshRenderer.transform.localToWorldMatrix;
                combine.Add(ci);
            }
        }

        // 새로운 GameObject를 생성하여 결합된 메시를 저장합니다.
        GameObject combinedObject = new GameObject("Combined Mesh");

        // 메시 필터 및 렌더러를 추가하고 메시를 결합합니다.
        MeshFilter combinedMeshFilter = combinedObject.AddComponent<MeshFilter>();
        combinedMeshFilter.mesh = new Mesh();
        combinedMeshFilter.mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
        combinedMeshFilter.mesh.CombineMeshes(combine.ToArray(), true, false);

        // 메시 렌더러에 재질을 할당합니다.
        MeshRenderer combinedMeshRenderer = combinedObject.AddComponent<MeshRenderer>();
        // 예시: 첫 번째 MeshFilter 또는 SkinnedMeshRenderer의 재질을 사용합니다.
        if (meshFilters.Length > 0 && meshFilters[0].GetComponent<MeshRenderer>()) {
            combinedMeshRenderer.sharedMaterials = meshFilters[0].GetComponent<MeshRenderer>().sharedMaterials;
        } else if (skinnedMeshRenderers.Length > 0) {
            combinedMeshRenderer.sharedMaterials = skinnedMeshRenderers[0].sharedMaterials;
        }

        // 씬에 결합된 메시를 활성화합니다.
        combinedObject.SetActive(true);

        // 디버깅 메시지
        Debug.Log($"Combined mesh has {combinedMeshFilter.mesh.vertexCount} vertices.");
        AssetDatabase.Refresh();
    }

    private void ApplyAvatarToModelsInFolder(Avatar avatar)
    {
        if (avatar == null) {
            EditorUtility.DisplayDialog("Error", "Please assign an avatar.", "OK");
            return;
        }

        string folderPath = EditorUtility.OpenFolderPanel("Choose Model Folder", avatarPath, "");
        var fullPath = Path.GetFullPath(folderPath);
        if (!Directory.Exists(fullPath)) {
            EditorUtility.DisplayDialog("Error", "The specified folder path does not exist.", "OK");
            return;
        }

        var modelPaths = Directory.GetFiles(fullPath, "*.fbx", SearchOption.AllDirectories)
            .Union(Directory.GetFiles(fullPath, "*.obj", SearchOption.AllDirectories));

        foreach (var absoluteModelPath in modelPaths) {
            var relativeModelPath = "Assets" + absoluteModelPath.Substring(Application.dataPath.Length).Replace('\\', '/');

            // Debugging to see what the final path is
            Debug.Log($"Trying to load model importer at path: {relativeModelPath}");

            var modelImporter = AssetImporter.GetAtPath(relativeModelPath) as ModelImporter;
            if (modelImporter) {
                modelImporter.animationType = ModelImporterAnimationType.Human;
                modelImporter.avatarSetup = ModelImporterAvatarSetup.CopyFromOther;
                modelImporter.sourceAvatar = avatar;

                modelImporter.SaveAndReimport();

                Debug.Log($"Avatar applied to model: {Path.GetFileName(absoluteModelPath)}");
            } else {
                Debug.LogError($"Model importer could not be loaded at path: {relativeModelPath}");
            }
        }

        AssetDatabase.Refresh();
        EditorUtility.DisplayDialog("Avatar Setup", "Avatar has been applied to all models in the specified folder.", "OK");
    }
}

 

반응형