Rework : Final version FBX importer
This commit is contained in:
@@ -1,2 +1,10 @@
|
||||
f01584905fdd447489fd65c7e3c03a15 RobotHolo_5.fbx
|
||||
5aec27fd964155d41b84ae456ea4e99f RobotHolo5.fbx
|
||||
c5ffa67b650768b48a1dfee14cde0434 RobotHolo5.fbx
|
||||
949744f42ff28654bb80e5e7d4b9b404 RobotHolo_5.fbx
|
||||
45d1a2248cd0ec849ba0ae5efdaa1504 Drone.FBX
|
||||
8e1fcfbac6e337b4e9d318ef1f32e4cb Drone.FBX
|
||||
447d6845f839a5c42baf18f4e89f935a Bot.fbx
|
||||
9c7ca22fec5fc2346ba0334a486cab7e Chungus.fbx
|
||||
0aeae8387e5b14d4bac920f0def12124 Robot_Animation_013.fbx
|
||||
2e9f82ecbb3dd7540a6bcaf166140266 Robot_Animation_013.fbx
|
||||
|
||||
+16
-16
@@ -1810,8 +1810,8 @@
|
||||
"m_ObjectId": "013228b0fdf1424097798f0973a9a4fb",
|
||||
"m_Title": "Face Texture",
|
||||
"m_Position": {
|
||||
"x": -4779.0,
|
||||
"y": -2949.0
|
||||
"x": -4779.19970703125,
|
||||
"y": -2950.400390625
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4525,8 +4525,8 @@
|
||||
"m_ObjectId": "42cadae0923e4969b50bbc3f78185934",
|
||||
"m_Title": "Face + 3 Outlines + Underlay",
|
||||
"m_Position": {
|
||||
"x": -5437.0,
|
||||
"y": -3558.0
|
||||
"x": -5437.60009765625,
|
||||
"y": -3559.200439453125
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4788,8 +4788,8 @@
|
||||
"m_ObjectId": "484b51c50485473b819c4f05087b32d7",
|
||||
"m_Title": "Underlay",
|
||||
"m_Position": {
|
||||
"x": -4959.0,
|
||||
"y": -4542.0
|
||||
"x": -4959.19970703125,
|
||||
"y": -4544.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8242,8 +8242,8 @@
|
||||
"m_ObjectId": "98934a69591249d5b8b92b39045359a3",
|
||||
"m_Title": "Outline1 Texture",
|
||||
"m_Position": {
|
||||
"x": -4633.0,
|
||||
"y": -2496.0
|
||||
"x": -4633.60009765625,
|
||||
"y": -2497.600341796875
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10467,8 +10467,8 @@
|
||||
"m_ObjectId": "d0a791a544614667962a9a9a9ce0c68a",
|
||||
"m_Title": "Screen Space Ratio",
|
||||
"m_Position": {
|
||||
"x": -6486.0,
|
||||
"y": -3367.0
|
||||
"x": -6486.39990234375,
|
||||
"y": -3368.80029296875
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10567,8 +10567,8 @@
|
||||
"m_ObjectId": "d258902c6ec74942afdb9ebf8c1d07f8",
|
||||
"m_Title": "Generate Normal",
|
||||
"m_Position": {
|
||||
"x": -4511.0,
|
||||
"y": -3999.0
|
||||
"x": -4511.19970703125,
|
||||
"y": -4000.80029296875
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10849,8 +10849,8 @@
|
||||
"m_ObjectId": "daaf032a109749a88c9b8ff8e1f8b541",
|
||||
"m_Title": "Offset Scale",
|
||||
"m_Position": {
|
||||
"x": -5893.0,
|
||||
"y": -3921.0
|
||||
"x": -5893.60009765625,
|
||||
"y": -3922.400390625
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11388,8 +11388,8 @@
|
||||
"m_ObjectId": "ecf16c34d46f4502ac601f0c38c7576b",
|
||||
"m_Title": "Vertex Color",
|
||||
"m_Position": {
|
||||
"x": -3579.0,
|
||||
"y": -3525.0
|
||||
"x": -3579.2001953125,
|
||||
"y": -3526.400390625
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using System.IO;
|
||||
|
||||
namespace FBXImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Extracts animation clips embedded in a FBX into individual .anim files
|
||||
/// in the asset's Animations/ folder.
|
||||
/// </summary>
|
||||
public static class AnimationExtractor
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads all animation clips from the FBX and saves each as a .anim file
|
||||
/// in animationsDir. Skips clips that already exist (idempotent).
|
||||
/// </summary>
|
||||
public static void ExtractClips(string fbxAssetPath, string animationsDir)
|
||||
{
|
||||
if (!AssetDatabase.IsValidFolder(animationsDir))
|
||||
{
|
||||
Debug.LogWarning($"[FBXImporter] Animations folder not found: {animationsDir}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Load all assets embedded in the FBX
|
||||
var allAssets = AssetDatabase.LoadAllAssetsAtPath(fbxAssetPath);
|
||||
int count = 0;
|
||||
|
||||
foreach (var asset in allAssets)
|
||||
{
|
||||
if (!(asset is AnimationClip clip)) continue;
|
||||
|
||||
// Skip the "preview" take Unity adds internally
|
||||
if (clip.name.StartsWith("__preview__")) continue;
|
||||
|
||||
string clipName = SanitizeClipName(clip.name);
|
||||
string clipPath = $"{animationsDir}/{clipName}.anim";
|
||||
|
||||
// Skip if already extracted
|
||||
if (AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath) != null)
|
||||
{
|
||||
Debug.Log($"[FBXImporter] Clip already exists, skipped: {clipName}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Duplicate the clip as a standalone asset
|
||||
var extracted = Object.Instantiate(clip);
|
||||
extracted.name = clipName;
|
||||
AssetDatabase.CreateAsset(extracted, clipPath);
|
||||
count++;
|
||||
|
||||
Debug.Log($"[FBXImporter] Extracted clip → {clipPath}");
|
||||
}
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
AssetDatabase.SaveAssets();
|
||||
Debug.Log($"[FBXImporter] {count} clip(s) extracted to {animationsDir}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"[FBXImporter] No new animation clips found in {Path.GetFileName(fbxAssetPath)}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes a clip name for use as a filename.
|
||||
/// "Take 001" → "Take_001"
|
||||
/// </summary>
|
||||
static string SanitizeClipName(string name)
|
||||
{
|
||||
return name.Replace(" ", "_")
|
||||
.Replace("/", "_")
|
||||
.Replace("\\", "_")
|
||||
.Replace(":", "_")
|
||||
.Replace("|", "_")
|
||||
.Replace("*", "_")
|
||||
.Replace("?", "_")
|
||||
.Replace("\"", "_")
|
||||
.Replace("<", "_")
|
||||
.Replace(">", "_");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ec504778d5233d40a1b99a372f7eb3e
|
||||
@@ -0,0 +1,94 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FBXImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an AnimatorController with one state per extracted animation clip,
|
||||
/// then assigns it to the Animator component on the prefab.
|
||||
/// This allows previewing animations in the editor without manual setup.
|
||||
/// </summary>
|
||||
public static class AnimatorControllerBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an AnimatorController in animationsDir, adds one state per .anim clip found,
|
||||
/// then assigns the controller to the Animator on the prefab at prefabPath.
|
||||
/// </summary>
|
||||
public static void CreateAndAssign(string animationsDir, string prefabPath, string assetName)
|
||||
{
|
||||
if (!AssetDatabase.IsValidFolder(animationsDir))
|
||||
{
|
||||
Debug.LogWarning($"[FBXImporter] Animations folder not found: {animationsDir}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Find all extracted .anim clips in Animations/
|
||||
var clipGuids = AssetDatabase.FindAssets("t:AnimationClip", new[] { animationsDir });
|
||||
if (clipGuids.Length == 0)
|
||||
{
|
||||
Debug.Log($"[FBXImporter] No clips found in {animationsDir} — skipping controller creation");
|
||||
return;
|
||||
}
|
||||
|
||||
var clips = new List<AnimationClip>();
|
||||
foreach (var guid in clipGuids)
|
||||
{
|
||||
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(
|
||||
AssetDatabase.GUIDToAssetPath(guid));
|
||||
if (clip != null) clips.Add(clip);
|
||||
}
|
||||
|
||||
// Create the controller
|
||||
string controllerPath = $"{animationsDir}/{assetName}.controller";
|
||||
var controller = AnimatorController.CreateAnimatorControllerAtPath(controllerPath);
|
||||
|
||||
// Add one state per clip
|
||||
var rootStateMachine = controller.layers[0].stateMachine;
|
||||
foreach (var clip in clips)
|
||||
{
|
||||
var state = rootStateMachine.AddState(clip.name);
|
||||
state.motion = clip;
|
||||
Debug.Log($"[FBXImporter] Controller state added: {clip.name}");
|
||||
}
|
||||
|
||||
// Set the first clip as default state
|
||||
if (clips.Count > 0)
|
||||
rootStateMachine.defaultState = rootStateMachine.states[0].state;
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
Debug.Log($"[FBXImporter] AnimatorController created → {controllerPath}");
|
||||
|
||||
// Assign controller to the prefab
|
||||
AssignToPrefab(prefabPath, controller);
|
||||
}
|
||||
|
||||
static void AssignToPrefab(string prefabPath, AnimatorController controller)
|
||||
{
|
||||
var prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
||||
if (prefabAsset == null)
|
||||
{
|
||||
Debug.LogWarning($"[FBXImporter] Prefab not found at: {prefabPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Edit the prefab
|
||||
var prefabInstance = PrefabUtility.LoadPrefabContents(prefabPath);
|
||||
|
||||
// Get or add Animator component
|
||||
var animator = prefabInstance.GetComponent<Animator>();
|
||||
if (animator == null)
|
||||
animator = prefabInstance.AddComponent<Animator>();
|
||||
|
||||
animator.runtimeAnimatorController = controller;
|
||||
|
||||
// Save prefab changes
|
||||
PrefabUtility.SaveAsPrefabAsset(prefabInstance, prefabPath);
|
||||
PrefabUtility.UnloadPrefabContents(prefabInstance);
|
||||
|
||||
Debug.Log($"[FBXImporter] AnimatorController assigned to prefab: {Path.GetFileName(prefabPath)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5d86102882208454999c396da82b6886
|
||||
@@ -4,16 +4,27 @@ namespace FBXImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Automatically removes a FBX from the registry when it's deleted from the project.
|
||||
/// This ensures deleted assets will show up again in the batch dialog if re-imported.
|
||||
///
|
||||
/// IMPORTANT: We resolve the GUID *before* returning DidNotDelete so Unity hasn't
|
||||
/// touched the file yet — AssetPathToGUID is still valid at this point.
|
||||
/// We use UnregisterByGuid to bypass a second GUID lookup after deletion starts,
|
||||
/// which would return an empty string and silently fail.
|
||||
/// </summary>
|
||||
public class FBXDeletionWatcher : UnityEditor.AssetModificationProcessor
|
||||
{
|
||||
static AssetDeleteResult OnWillDeleteAsset(string path, RemoveAssetOptions options)
|
||||
{
|
||||
if (path.EndsWith(".fbx", System.StringComparison.OrdinalIgnoreCase)
|
||||
&& ProcessedFBXRegistry.IsRegistered(path))
|
||||
if (!path.EndsWith(".fbx", System.StringComparison.OrdinalIgnoreCase))
|
||||
return AssetDeleteResult.DidNotDelete;
|
||||
|
||||
// Resolve GUID now, while the file still exists
|
||||
string guid = AssetDatabase.AssetPathToGUID(path);
|
||||
|
||||
if (!string.IsNullOrEmpty(guid))
|
||||
{
|
||||
ProcessedFBXRegistry.Unregister(path);
|
||||
// Use UnregisterByGuid — avoids a second AssetPathToGUID call
|
||||
// inside Unregister() which may fail once deletion is in progress
|
||||
ProcessedFBXRegistry.UnregisterByGuid(guid, System.IO.Path.GetFileName(path));
|
||||
}
|
||||
|
||||
// Always let Unity handle the actual deletion
|
||||
|
||||
@@ -7,12 +7,22 @@ namespace FBXImporter
|
||||
public static class FBXProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Full pipeline for a single FBX.
|
||||
/// animationType: None=0, Legacy=1, Generic=2, Humanoid=3
|
||||
/// Full pipeline for a single FBX:
|
||||
/// 1. Detect source (Blender/Maya)
|
||||
/// 2. Create folder structure
|
||||
/// 3. Configure ModelImporter (rig type, materials)
|
||||
/// 4. Force reimport
|
||||
/// 5. Connect PBR textures → create .mat files
|
||||
/// 6. Extract animation clips → Animations/
|
||||
/// 7. Generate prefab
|
||||
/// 8. Create AnimatorController + assign to prefab
|
||||
/// 9. Delete Sources/Materials/ (Unity auto-generates it, we don't need it)
|
||||
/// 10. Delete original source folder (cleanup)
|
||||
/// </summary>
|
||||
public static void Process(string fbxAssetPath,
|
||||
ModelImporterAnimationType animationType = ModelImporterAnimationType.Generic,
|
||||
string rootNode = "")
|
||||
string rootNode = "",
|
||||
string originalSourceFolder = "")
|
||||
{
|
||||
if (!fbxAssetPath.EndsWith(".fbx", System.StringComparison.OrdinalIgnoreCase) ||
|
||||
!fbxAssetPath.StartsWith("Assets/"))
|
||||
@@ -23,13 +33,14 @@ namespace FBXImporter
|
||||
|
||||
Debug.Log($"[FBXImporter] ── Processing {Path.GetFileName(fbxAssetPath)} ──");
|
||||
|
||||
var source = FBXSourceDetector.Detect(fbxAssetPath);
|
||||
var folders = FolderSetup.Resolve(fbxAssetPath);
|
||||
var source = FBXSourceDetector.Detect(fbxAssetPath);
|
||||
var folders = FolderSetup.Resolve(fbxAssetPath);
|
||||
bool withAnims = animationType != ModelImporterAnimationType.None;
|
||||
|
||||
FolderSetup.Create(
|
||||
Path.GetDirectoryName(folders.Root),
|
||||
Path.GetFileName(folders.Root),
|
||||
animationType != ModelImporterAnimationType.None
|
||||
withAnims
|
||||
);
|
||||
folders = FolderSetup.Resolve(fbxAssetPath);
|
||||
|
||||
@@ -41,6 +52,7 @@ namespace FBXImporter
|
||||
importer.SaveAndReimport();
|
||||
}
|
||||
|
||||
// Load model
|
||||
var fbxAsset = AssetDatabase.LoadAssetAtPath<GameObject>(fbxAssetPath);
|
||||
if (fbxAsset == null)
|
||||
{
|
||||
@@ -48,13 +60,47 @@ namespace FBXImporter
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect PBR textures
|
||||
ProcessMaterials(fbxAsset, folders, source);
|
||||
string assetName = Path.GetFileNameWithoutExtension(fbxAssetPath);
|
||||
|
||||
// Extract animation clips
|
||||
if (withAnims && !string.IsNullOrEmpty(folders.Animations))
|
||||
AnimationExtractor.ExtractClips(fbxAssetPath, folders.Animations);
|
||||
|
||||
// Generate prefab
|
||||
string assetName = Path.GetFileNameWithoutExtension(fbxAssetPath);
|
||||
string prefabPath = $"{folders.Prefabs}/{assetName}.prefab";
|
||||
PrefabBuilder.CreatePrefab(fbxAsset, folders.Prefabs, assetName);
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
// Create AnimatorController with clips and assign to prefab
|
||||
if (withAnims && !string.IsNullOrEmpty(folders.Animations))
|
||||
AnimatorControllerBuilder.CreateAndAssign(folders.Animations, prefabPath, assetName);
|
||||
|
||||
// Delete Sources/Materials/ — Unity creates it automatically but we use
|
||||
// the root Materials/ folder instead, so this one is redundant
|
||||
string sourceMaterialsPath = $"{folders.Sources}/Materials";
|
||||
if (AssetDatabase.IsValidFolder(sourceMaterialsPath))
|
||||
{
|
||||
AssetDatabase.DeleteAsset(sourceMaterialsPath);
|
||||
Debug.Log($"[FBXImporter] Deleted Sources/Materials/ (using root Materials/ instead)");
|
||||
}
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
// Cleanup original source folder if provided and different from our Sources/
|
||||
if (!string.IsNullOrEmpty(originalSourceFolder) &&
|
||||
originalSourceFolder != folders.Sources &&
|
||||
originalSourceFolder != folders.Root &&
|
||||
AssetDatabase.IsValidFolder(originalSourceFolder))
|
||||
{
|
||||
AssetDatabase.DeleteAsset(originalSourceFolder);
|
||||
Debug.Log($"[FBXImporter] Deleted original folder: {originalSourceFolder}");
|
||||
}
|
||||
|
||||
Debug.Log($"[FBXImporter] ── Done: {assetName} ──");
|
||||
}
|
||||
|
||||
@@ -77,7 +123,6 @@ namespace FBXImporter
|
||||
importer.materialName = ModelImporterMaterialName.BasedOnMaterialName;
|
||||
importer.materialSearch = ModelImporterMaterialSearch.RecursiveUp;
|
||||
|
||||
// Rig / animation settings
|
||||
importer.animationType = animType;
|
||||
if (!string.IsNullOrWhiteSpace(rootNode))
|
||||
importer.motionNodeName = rootNode;
|
||||
@@ -126,7 +171,6 @@ namespace FBXImporter
|
||||
else
|
||||
{
|
||||
EditorUtility.SetDirty(target);
|
||||
Debug.Log($"[FBXImporter] Material updated → {matPath}");
|
||||
}
|
||||
|
||||
return target;
|
||||
@@ -156,8 +200,6 @@ namespace FBXImporter
|
||||
mat.EnableKeyword("_PARALLAXMAP");
|
||||
break;
|
||||
}
|
||||
|
||||
Debug.Log($"[FBXImporter] {slot} → {tex.name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace FBXImporter
|
||||
/// <summary>
|
||||
/// Persistent registry of FBX files seen by the importer.
|
||||
/// Stores GUID + filename so the file is human-readable.
|
||||
/// Format per line: "guid filename.fbx"
|
||||
/// Format per line: "guid TAB filename.fbx"
|
||||
/// </summary>
|
||||
public static class ProcessedFBXRegistry
|
||||
{
|
||||
@@ -44,14 +44,28 @@ namespace FBXImporter
|
||||
if (changed) Save();
|
||||
}
|
||||
|
||||
/// <summary>Removes a single FBX from the registry by asset path.</summary>
|
||||
/// <summary>Removes a FBX from the registry by asset path.</summary>
|
||||
public static void Unregister(string assetPath)
|
||||
{
|
||||
string guid = AssetDatabase.AssetPathToGUID(assetPath);
|
||||
if (!string.IsNullOrEmpty(guid) && Registry.Remove(guid))
|
||||
if (!string.IsNullOrEmpty(guid))
|
||||
UnregisterByGuid(guid, Path.GetFileName(assetPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a FBX from the registry directly by GUID.
|
||||
/// Used by FBXDeletionWatcher where the GUID is resolved BEFORE
|
||||
/// Unity deletes the file — avoids the race condition where
|
||||
/// AssetPathToGUID returns empty after deletion starts.
|
||||
/// </summary>
|
||||
public static void UnregisterByGuid(string guid, string displayName = "")
|
||||
{
|
||||
if (string.IsNullOrEmpty(guid)) return;
|
||||
if (Registry.Remove(guid))
|
||||
{
|
||||
Save();
|
||||
UnityEngine.Debug.Log($"[FBXImporter] Removed from registry: {Path.GetFileName(assetPath)}");
|
||||
string name = string.IsNullOrEmpty(displayName) ? guid : displayName;
|
||||
UnityEngine.Debug.Log($"[FBXImporter] Removed from registry: {name}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +81,6 @@ namespace FBXImporter
|
||||
string trimmed = line.Trim();
|
||||
if (string.IsNullOrEmpty(trimmed)) continue;
|
||||
|
||||
// Format: "guid filename.fbx"
|
||||
var parts = trimmed.Split(new[] { '\t', ' ' }, 2, System.StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length >= 1)
|
||||
_registry[parts[0]] = parts.Length >= 2 ? parts[1] : "unknown";
|
||||
@@ -81,6 +94,10 @@ namespace FBXImporter
|
||||
|
||||
var lines = Registry.Select(kvp => $"{kvp.Key}\t{kvp.Value}").ToArray();
|
||||
File.WriteAllLines(RegistryPath, lines);
|
||||
|
||||
// Nullify cache so the next access reloads from disk.
|
||||
// This prevents stale in-memory state after domain reloads.
|
||||
_registry = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,64 +10,76 @@ namespace FBXImporter
|
||||
static readonly string[] SupportedExtensions = { ".png", ".tga", ".jpg", ".jpeg", ".tiff", ".exr" };
|
||||
|
||||
/// <summary>
|
||||
/// Maps shader slot → list of keywords.
|
||||
/// Keywords are matched ANYWHERE in the filename (case-insensitive).
|
||||
/// More specific keywords first to avoid false positives.
|
||||
/// Maps shader slot -> keywords matched anywhere in filename (case-insensitive).
|
||||
/// More specific keywords listed first to avoid false positives.
|
||||
/// </summary>
|
||||
public static readonly Dictionary<string, string[]> SlotKeywords = new Dictionary<string, string[]>
|
||||
{
|
||||
{ "_BaseMap", new[] { "albedo", "diffuse", "basecolor", "base_color", "color", "_D", "_d" } },
|
||||
{ "_BaseMap", new[] { "albedo", "diffuse", "basecolor", "base_color", "_diff", "_dif", "_D", "_d" } },
|
||||
{ "_BumpMap", new[] { "normal", "nrm", "_N", "_n" } },
|
||||
{ "_MetallicGlossMap", new[] { "metallic", "metal", "_M", "_m" } },
|
||||
{ "_SpecGlossMap", new[] { "roughness", "rough", "_R", "_r" } },
|
||||
{ "_MetallicGlossMap", new[] { "metallic", "metal", "gloss", "_M", "_m" } },
|
||||
{ "_SpecGlossMap", new[] { "roughness", "rough", "spec", "_R", "_r" } },
|
||||
{ "_OcclusionMap", new[] { "occlusion", "ambientocclusion", "_AO", "_ao" } },
|
||||
{ "_EmissionMap", new[] { "emissive", "emission", "_E", "_e" } },
|
||||
{ "_ParallaxMap", new[] { "height", "parallax", "opacity", "_H", "_h" } },
|
||||
{ "_ParallaxMap", new[] { "height", "parallax", "opacity", "alpha", "_H", "_h" } },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Finds a texture in textureDir whose filename:
|
||||
/// 1. Starts with baseName (links texture to the right material)
|
||||
/// 2. Contains any known keyword for the given shader slot (case-insensitive)
|
||||
/// Example: baseName="bot", slot="_BaseMap"
|
||||
/// → matches bot_sg_albedo.jpg, bot_D.jpg, bot_color_v2.jpg
|
||||
/// Finds a texture for the given material and shader slot.
|
||||
///
|
||||
/// PASS 1 - Prefix match (strict):
|
||||
/// filename starts with baseName AND contains a slot keyword
|
||||
/// e.g. baseName="Robot", slot="_BaseMap" -> matches "Robot_D.jpg", "Robot_diffuse.png"
|
||||
///
|
||||
/// PASS 2 - Fallback (keyword only, no prefix required):
|
||||
/// If pass 1 finds nothing, scan ALL textures in the folder for the keyword.
|
||||
/// Useful when textures use a different prefix than the material name.
|
||||
/// e.g. material="Robot", textures="Drone_diff.jpg" -> connected via "diff" keyword
|
||||
/// </summary>
|
||||
public static Texture2D FindForSlot(string textureDir, string baseName, string shaderSlot)
|
||||
{
|
||||
if (!SlotKeywords.TryGetValue(shaderSlot, out string[] keywords))
|
||||
return null;
|
||||
|
||||
if (!SlotKeywords.TryGetValue(shaderSlot, out string[] keywords)) return null;
|
||||
if (!AssetDatabase.IsValidFolder(textureDir)) return null;
|
||||
|
||||
// Get all texture files in the directory
|
||||
string[] guids = AssetDatabase.FindAssets("t:Texture2D", new[] { textureDir });
|
||||
|
||||
// Pass 1: prefix + keyword
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
|
||||
string filename = Path.GetFileNameWithoutExtension(assetPath).ToLower();
|
||||
string ext = Path.GetExtension(assetPath).ToLower();
|
||||
if (!IsSupportedExtension(assetPath)) continue;
|
||||
|
||||
// Check extension is supported
|
||||
bool validExt = false;
|
||||
foreach (var e in SupportedExtensions)
|
||||
if (ext == e) { validExt = true; break; }
|
||||
if (!validExt) continue;
|
||||
|
||||
// Must start with baseName (case-insensitive)
|
||||
string filename = Path.GetFileNameWithoutExtension(assetPath).ToLower();
|
||||
if (!filename.StartsWith(baseName.ToLower())) continue;
|
||||
|
||||
// Must contain a keyword for this slot
|
||||
foreach (var keyword in keywords)
|
||||
{
|
||||
if (filename.Contains(keyword.ToLower()))
|
||||
{
|
||||
var tex = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
|
||||
if (tex == null) continue;
|
||||
if (shaderSlot == "_BumpMap") EnsureNormalMapType(assetPath);
|
||||
return tex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shaderSlot == "_BumpMap")
|
||||
EnsureNormalMapType(assetPath);
|
||||
// Pass 2: keyword only fallback (mismatched prefix)
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
|
||||
if (!IsSupportedExtension(assetPath)) continue;
|
||||
|
||||
string filename = Path.GetFileNameWithoutExtension(assetPath).ToLower();
|
||||
|
||||
foreach (var keyword in keywords)
|
||||
{
|
||||
if (filename.Contains(keyword.ToLower()))
|
||||
{
|
||||
var tex = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
|
||||
if (tex == null) continue;
|
||||
if (shaderSlot == "_BumpMap") EnsureNormalMapType(assetPath);
|
||||
Debug.Log($"[FBXImporter] Fallback match: {shaderSlot} <- {Path.GetFileName(assetPath)} (mat: {baseName})");
|
||||
return tex;
|
||||
}
|
||||
}
|
||||
@@ -78,7 +90,8 @@ namespace FBXImporter
|
||||
|
||||
/// <summary>
|
||||
/// Strips DCC-specific suffixes to recover the clean base name.
|
||||
/// Maya : "hero_blinn1" → "hero" / "standardSurface1" → "Material"
|
||||
/// Maya : "hero_blinn1" -> "hero" / "standardSurface1" -> "Material"
|
||||
/// Both : "tree_MAT" -> "tree"
|
||||
/// </summary>
|
||||
public static string CleanMaterialName(string rawName, FBXSource source)
|
||||
{
|
||||
@@ -108,6 +121,14 @@ namespace FBXImporter
|
||||
return string.IsNullOrWhiteSpace(name) ? rawName : name;
|
||||
}
|
||||
|
||||
static bool IsSupportedExtension(string assetPath)
|
||||
{
|
||||
string ext = Path.GetExtension(assetPath).ToLower();
|
||||
foreach (var e in SupportedExtensions)
|
||||
if (ext == e) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static void EnsureNormalMapType(string assetPath)
|
||||
{
|
||||
var ti = AssetImporter.GetAtPath(assetPath) as TextureImporter;
|
||||
|
||||
@@ -5,18 +5,21 @@ using System.IO;
|
||||
namespace FBXImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Per-asset config dialog — name, destination, rig type.
|
||||
/// Per-asset config dialog.
|
||||
/// Default destination: Assets/[ProjectName]/Content
|
||||
/// Deletes the original source folder after successful import.
|
||||
/// </summary>
|
||||
public class FBXImportDialog : EditorWindow
|
||||
{
|
||||
private string _fbxSourcePath;
|
||||
private string _originalFolder; // folder to delete after import
|
||||
private FBXSource _detectedSource;
|
||||
private string _assetName = "";
|
||||
private string _destinationPath = "Assets";
|
||||
private string _destinationPath = "";
|
||||
private bool _createAnims = false;
|
||||
private string _errorMessage = "";
|
||||
|
||||
// Rig settings
|
||||
// Rig
|
||||
private ModelImporterAnimationType _animType = ModelImporterAnimationType.Generic;
|
||||
private string _rootNode = "";
|
||||
|
||||
@@ -32,15 +35,42 @@ namespace FBXImporter
|
||||
public static void Show(string fbxAssetPath, FBXSource source)
|
||||
{
|
||||
var window = GetWindow<FBXImportDialog>(true, $"Configure — {Path.GetFileName(fbxAssetPath)}", true);
|
||||
window.minSize = new Vector2(460, 360);
|
||||
window.maxSize = new Vector2(460, 360);
|
||||
window._fbxSourcePath = fbxAssetPath;
|
||||
window._detectedSource = source;
|
||||
window._assetName = Path.GetFileNameWithoutExtension(fbxAssetPath);
|
||||
window._destinationPath = Path.GetDirectoryName(fbxAssetPath).Replace("\\", "/");
|
||||
window.minSize = new Vector2(460, 360);
|
||||
window.maxSize = new Vector2(460, 360);
|
||||
window._fbxSourcePath = fbxAssetPath;
|
||||
window._detectedSource = source;
|
||||
window._assetName = Path.GetFileNameWithoutExtension(fbxAssetPath);
|
||||
|
||||
// Store the original parent folder for cleanup after import
|
||||
window._originalFolder = Path.GetDirectoryName(
|
||||
Path.GetDirectoryName(fbxAssetPath)).Replace("\\", "/");
|
||||
|
||||
// Default destination: Assets/[ProjectFolder]/Content
|
||||
// Falls back to Assets/ if not found
|
||||
window._destinationPath = FindDefaultContentFolder();
|
||||
|
||||
window.ShowModal();
|
||||
}
|
||||
|
||||
// ── Default content folder detection ─────────────────────────────────────
|
||||
|
||||
static string FindDefaultContentFolder()
|
||||
{
|
||||
// Try to find a "Content" folder one level deep under Assets/
|
||||
string[] candidates = AssetDatabase.GetSubFolders("Assets");
|
||||
foreach (var folder in candidates)
|
||||
{
|
||||
string content = $"{folder}/Content";
|
||||
if (AssetDatabase.IsValidFolder(content))
|
||||
return content;
|
||||
}
|
||||
|
||||
// Fallback to Assets/
|
||||
return "Assets";
|
||||
}
|
||||
|
||||
// ── GUI ───────────────────────────────────────────────────────────────────
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
GUILayout.Space(10);
|
||||
@@ -76,12 +106,12 @@ namespace FBXImporter
|
||||
EditorGUILayout.LabelField($"→ {_destinationPath}/{_assetName}", EditorStyles.miniLabel);
|
||||
GUILayout.Space(8);
|
||||
|
||||
// Rig / animation type
|
||||
// Rig
|
||||
GUILayout.Label("Rig & Animation", EditorStyles.miniBoldLabel);
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
GUILayout.Label("Animation Type", GUILayout.Width(120));
|
||||
int current = System.Array.IndexOf(AnimTypeValues, _animType);
|
||||
int current = System.Array.IndexOf(AnimTypeValues, _animType);
|
||||
int selected = GUILayout.SelectionGrid(current, AnimTypeLabels, 4);
|
||||
_animType = AnimTypeValues[selected];
|
||||
}
|
||||
@@ -93,10 +123,10 @@ namespace FBXImporter
|
||||
GUILayout.Label("Root Node (opt.)", GUILayout.Width(120));
|
||||
_rootNode = EditorGUILayout.TextField(_rootNode);
|
||||
}
|
||||
_createAnims = true; // auto-enable Animations/ folder if rig is set
|
||||
_createAnims = true;
|
||||
}
|
||||
|
||||
GUILayout.Space(6);
|
||||
GUILayout.Space(4);
|
||||
_createAnims = EditorGUILayout.ToggleLeft("Create Animations/ folder", _createAnims);
|
||||
GUILayout.Space(8);
|
||||
|
||||
@@ -106,7 +136,7 @@ namespace FBXImporter
|
||||
GUILayout.Space(4);
|
||||
}
|
||||
|
||||
// Buttons
|
||||
// Buttons — always enabled as long as name is not empty
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
GUILayout.FlexibleSpace();
|
||||
@@ -115,7 +145,7 @@ namespace FBXImporter
|
||||
{ Close(); return; }
|
||||
|
||||
bool valid = !string.IsNullOrWhiteSpace(_assetName)
|
||||
&& (!string.IsNullOrWhiteSpace(_destinationPath));
|
||||
&& !string.IsNullOrWhiteSpace(_destinationPath);
|
||||
|
||||
using (new EditorGUI.DisabledScope(!valid))
|
||||
{
|
||||
@@ -125,10 +155,13 @@ namespace FBXImporter
|
||||
}
|
||||
}
|
||||
|
||||
// ── Confirm ───────────────────────────────────────────────────────────────
|
||||
|
||||
void Confirm()
|
||||
{
|
||||
var folders = FolderSetup.Create(_destinationPath, _assetName, _createAnims);
|
||||
|
||||
// Move FBX into Sources/
|
||||
string targetFBX = $"{folders.Sources}/{Path.GetFileName(_fbxSourcePath)}";
|
||||
if (_fbxSourcePath != targetFBX)
|
||||
{
|
||||
@@ -140,12 +173,64 @@ namespace FBXImporter
|
||||
}
|
||||
}
|
||||
|
||||
FBXProcessor.Process(targetFBX, _animType, _rootNode);
|
||||
// Move textures from original folder into Textures/
|
||||
MoveTexturesFromOriginal(folders);
|
||||
|
||||
// Run full pipeline — pass original folder for cleanup
|
||||
FBXProcessor.Process(targetFBX, _animType, _rootNode, _originalFolder);
|
||||
|
||||
var rootObj = AssetDatabase.LoadAssetAtPath<Object>(folders.Root);
|
||||
if (rootObj != null) EditorGUIUtility.PingObject(rootObj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves texture files from the original dropped folder into the asset's Textures/.
|
||||
/// Handles the case where the user dropped a folder containing both source/ and textures/.
|
||||
/// </summary>
|
||||
void MoveTexturesFromOriginal(AssetFolders folders)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_originalFolder)) return;
|
||||
|
||||
// Look for a textures subfolder in the original dropped folder
|
||||
string[] textureSources = {
|
||||
$"{_originalFolder}/textures",
|
||||
$"{_originalFolder}/Textures",
|
||||
$"{_originalFolder}/texture",
|
||||
_originalFolder // textures may be directly in the root
|
||||
};
|
||||
|
||||
string[] supportedExts = { ".png", ".tga", ".jpg", ".jpeg", ".tiff", ".exr" };
|
||||
|
||||
foreach (var texSource in textureSources)
|
||||
{
|
||||
if (!AssetDatabase.IsValidFolder(texSource)) continue;
|
||||
|
||||
string[] guids = AssetDatabase.FindAssets("t:Texture2D", new[] { texSource });
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
string srcPath = AssetDatabase.GUIDToAssetPath(guid);
|
||||
string ext = Path.GetExtension(srcPath).ToLower();
|
||||
|
||||
bool valid = false;
|
||||
foreach (var e in supportedExts) if (ext == e) { valid = true; break; }
|
||||
if (!valid) continue;
|
||||
|
||||
string dstPath = $"{folders.Textures}/{Path.GetFileName(srcPath)}";
|
||||
if (srcPath == dstPath) continue;
|
||||
|
||||
string err = AssetDatabase.MoveAsset(srcPath, dstPath);
|
||||
if (!string.IsNullOrEmpty(err))
|
||||
Debug.LogWarning($"[FBXImporter] Could not move texture {Path.GetFileName(srcPath)}: {err}");
|
||||
else
|
||||
Debug.Log($"[FBXImporter] Texture moved → {dstPath}");
|
||||
}
|
||||
|
||||
break; // Only process first valid source
|
||||
}
|
||||
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
static void DrawRow(string label, string value)
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
|
||||
@@ -4,25 +4,40 @@ using System.IO;
|
||||
|
||||
namespace FBXImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// FBX Importer Inspector Window — Window > FBX Importer > Inspector
|
||||
///
|
||||
/// Shows for the selected FBX:
|
||||
/// - Detected source (Blender/Maya)
|
||||
/// - Registry status
|
||||
/// - Folder structure status
|
||||
/// - Texture slot matching preview
|
||||
/// - Animation clips found
|
||||
/// - Animator Controller status
|
||||
/// - Actions: process, remove from registry, create folders, ping
|
||||
/// </summary>
|
||||
public class FBXImporterWindow : EditorWindow
|
||||
{
|
||||
private Object _selectedFBX;
|
||||
private Vector2 _scroll;
|
||||
private Vector2 _scrollTextures;
|
||||
private Vector2 _scrollClips;
|
||||
|
||||
[MenuItem("Window/FBX Importer/Inspector")]
|
||||
public static void Open() => GetWindow<FBXImporterWindow>("FBX Importer").Show();
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
minSize = new Vector2(450, 650);
|
||||
GUILayout.Label("FBX Importer", EditorStyles.boldLabel);
|
||||
EditorGUILayout.HelpBox(
|
||||
"Select a .fbx in the Project panel or drag it here.\n" +
|
||||
"Processing is always manual — nothing runs automatically.",
|
||||
"Select a .fbx in the Project panel.\n" +
|
||||
"Drop → batch dialog appears automatically for new files.",
|
||||
MessageType.Info);
|
||||
GUILayout.Space(6);
|
||||
|
||||
_selectedFBX = EditorGUILayout.ObjectField("FBX Asset", _selectedFBX, typeof(Object), false);
|
||||
|
||||
// Auto-fill from Project panel selection
|
||||
if (_selectedFBX == null && Selection.activeObject != null)
|
||||
{
|
||||
string sel = AssetDatabase.GetAssetPath(Selection.activeObject);
|
||||
@@ -44,7 +59,7 @@ namespace FBXImporter
|
||||
|
||||
void DrawInspector(string assetPath)
|
||||
{
|
||||
EditorGUILayout.Space(8);
|
||||
EditorGUILayout.Space(6);
|
||||
|
||||
var source = FBXSourceDetector.Detect(assetPath);
|
||||
var folders = FolderSetup.Resolve(assetPath);
|
||||
@@ -52,21 +67,28 @@ namespace FBXImporter
|
||||
string clean = TextureMatcher.CleanMaterialName(
|
||||
Path.GetFileNameWithoutExtension(assetPath), source);
|
||||
|
||||
// Info
|
||||
// ── Detection ─────────────────────────────────────────────────────────
|
||||
GUILayout.Label("Detection", EditorStyles.boldLabel);
|
||||
DrawRow("Source", source.ToString(),
|
||||
source != FBXSource.Unknown ? Color.green : Color.yellow);
|
||||
DrawRow("Registered", registered ? "Yes — won't appear in batch dialog" : "No — will appear on next drop",
|
||||
DrawRow("Registered", registered ? "Yes" : "No — will appear on next drop",
|
||||
registered ? Color.green : Color.yellow);
|
||||
DrawRow("Textures/", folders.Textures,
|
||||
AssetDatabase.IsValidFolder(folders.Textures) ? Color.green : Color.red);
|
||||
DrawRow("Materials/", folders.Materials,
|
||||
AssetDatabase.IsValidFolder(folders.Materials) ? Color.green : Color.yellow);
|
||||
|
||||
// Texture preview
|
||||
EditorGUILayout.Space(8);
|
||||
GUILayout.Label("Texture Slot Preview", EditorStyles.boldLabel);
|
||||
_scroll = EditorGUILayout.BeginScrollView(_scroll, GUILayout.Height(150));
|
||||
// ── Folder structure ──────────────────────────────────────────────────
|
||||
EditorGUILayout.Space(6);
|
||||
GUILayout.Label("Folder Structure", EditorStyles.boldLabel);
|
||||
DrawFolderRow("Sources/", folders.Sources);
|
||||
DrawFolderRow("Textures/", folders.Textures);
|
||||
DrawFolderRow("Materials/", folders.Materials);
|
||||
DrawFolderRow("Prefabs/", folders.Prefabs);
|
||||
DrawFolderRow("Animations/", folders.Animations);
|
||||
DrawFolderRow("Audio/", folders.Audio);
|
||||
DrawFolderRow("VFX/", folders.VFX);
|
||||
|
||||
// ── Texture slot preview ──────────────────────────────────────────────
|
||||
EditorGUILayout.Space(6);
|
||||
GUILayout.Label("Texture Slots", EditorStyles.boldLabel);
|
||||
_scrollTextures = EditorGUILayout.BeginScrollView(_scrollTextures, GUILayout.Height(130));
|
||||
foreach (var kvp in TextureMatcher.SlotKeywords)
|
||||
{
|
||||
var tex = TextureMatcher.FindForSlot(folders.Textures, clean, kvp.Key);
|
||||
@@ -76,38 +98,75 @@ namespace FBXImporter
|
||||
var style = new GUIStyle(EditorStyles.label)
|
||||
{ normal = { textColor = found ? new Color(0.4f, 1f, 0.4f) : new Color(0.5f, 0.5f, 0.5f) } };
|
||||
GUILayout.Label(found ? "✓" : "○", style, GUILayout.Width(16));
|
||||
GUILayout.Label(kvp.Key, GUILayout.Width(170));
|
||||
GUILayout.Label(kvp.Key, GUILayout.Width(160));
|
||||
GUILayout.Label(found ? tex.name : "–", EditorStyles.miniLabel);
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndScrollView();
|
||||
|
||||
// Actions
|
||||
// ── Animation clips ───────────────────────────────────────────────────
|
||||
EditorGUILayout.Space(6);
|
||||
GUILayout.Label("Animation Clips", EditorStyles.boldLabel);
|
||||
|
||||
var allAssets = AssetDatabase.LoadAllAssetsAtPath(assetPath);
|
||||
int clipCount = 0;
|
||||
_scrollClips = EditorGUILayout.BeginScrollView(_scrollClips, GUILayout.Height(60));
|
||||
foreach (var asset in allAssets)
|
||||
{
|
||||
if (!(asset is AnimationClip clip)) continue;
|
||||
if (clip.name.StartsWith("__preview__")) continue;
|
||||
clipCount++;
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
GUILayout.Label("♦", GUILayout.Width(16));
|
||||
GUILayout.Label(clip.name, EditorStyles.miniLabel);
|
||||
GUILayout.Label($"{clip.length:0.00}s", EditorStyles.miniLabel, GUILayout.Width(50));
|
||||
}
|
||||
}
|
||||
if (clipCount == 0)
|
||||
GUILayout.Label("No clips found in this FBX", EditorStyles.miniLabel);
|
||||
EditorGUILayout.EndScrollView();
|
||||
|
||||
// Animator controller status
|
||||
if (!string.IsNullOrEmpty(folders.Animations))
|
||||
{
|
||||
string controllerPath = $"{folders.Animations}/{Path.GetFileNameWithoutExtension(assetPath)}.controller";
|
||||
bool hasController = AssetDatabase.LoadAssetAtPath<UnityEditor.Animations.AnimatorController>(controllerPath) != null;
|
||||
DrawRow("Controller", hasController ? "Created ✓" : "Not created yet",
|
||||
hasController ? Color.green : Color.gray);
|
||||
}
|
||||
|
||||
// ── Actions ───────────────────────────────────────────────────────────
|
||||
EditorGUILayout.Space(8);
|
||||
GUILayout.Label("Actions", EditorStyles.boldLabel);
|
||||
|
||||
if (GUILayout.Button("▶ Process this FBX (full pipeline)", GUILayout.Height(28)))
|
||||
{
|
||||
var src = FBXSourceDetector.Detect(assetPath);
|
||||
FBXImportDialog.Show(assetPath, src);
|
||||
}
|
||||
FBXImportDialog.Show(assetPath, source);
|
||||
|
||||
EditorGUILayout.Space(2);
|
||||
|
||||
if (GUILayout.Button("🗑 Remove from registry (will reappear on next drop)"))
|
||||
if (GUILayout.Button("🔄 Reconnect textures only"))
|
||||
{
|
||||
var fbxAsset = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
|
||||
if (fbxAsset != null)
|
||||
{
|
||||
// Re-run texture connection without reimport
|
||||
FBXProcessor.Process(assetPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (GUILayout.Button("🗑 Remove from registry"))
|
||||
ProcessedFBXRegistry.Unregister(assetPath);
|
||||
|
||||
if (GUILayout.Button("📁 Create missing folders"))
|
||||
{
|
||||
FolderSetup.Create(
|
||||
Path.GetDirectoryName(folders.Root),
|
||||
Path.GetFileName(folders.Root), false);
|
||||
}
|
||||
FolderSetup.Create(Path.GetDirectoryName(folders.Root), Path.GetFileName(folders.Root), false);
|
||||
|
||||
if (GUILayout.Button("🔍 Ping in Project panel"))
|
||||
EditorGUIUtility.PingObject(_selectedFBX);
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
static void DrawRow(string label, string value, Color valueColor)
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
@@ -117,5 +176,19 @@ namespace FBXImporter
|
||||
EditorGUILayout.LabelField(value, style);
|
||||
}
|
||||
}
|
||||
|
||||
static void DrawFolderRow(string label, string folderPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(folderPath)) return;
|
||||
bool exists = AssetDatabase.IsValidFolder(folderPath);
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
var style = new GUIStyle(EditorStyles.label)
|
||||
{ normal = { textColor = exists ? new Color(0.4f, 1f, 0.4f) : new Color(0.6f, 0.6f, 0.6f) } };
|
||||
GUILayout.Label(exists ? "✓" : "○", style, GUILayout.Width(16));
|
||||
GUILayout.Label(label, GUILayout.Width(90));
|
||||
GUILayout.Label(folderPath, EditorStyles.miniLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# FBX Importer v6
|
||||
|
||||
## Nouveautés vs v5
|
||||
- Nomenclature intelligente : reconnait les mots-clés n'importe où dans le nom de fichier
|
||||
- Config rig dans le dialog : None / Legacy / Generic / Humanoid + Root Node
|
||||
- Suppression auto du registry quand tu supprimes un FBX
|
||||
- Bouton "Remove from registry" dans l'Inspector et clic droit
|
||||
|
||||
## Flow
|
||||
1. Drop un dossier avec FBX → batch dialog
|
||||
2. Coche ceux à traiter
|
||||
3. Pour chacun : nom + destination + type de rig
|
||||
4. Pipeline complet : dossiers / materials / prefab
|
||||
|
||||
## Nomenclature textures
|
||||
Le plugin reconnait les mots-clés n'importe où dans le nom :
|
||||
bot_sg_albedo.jpg → BaseMap (contient "albedo")
|
||||
bot_sg_roughness → Roughness (contient "roughness")
|
||||
holo_sg_emissive → Emission (contient "emissive")
|
||||
|
||||
Mots-clés reconnus :
|
||||
BaseMap : albedo, diffuse, basecolor, color, _D
|
||||
Normal : normal, nrm, _N
|
||||
Metallic : metallic, metal, _M
|
||||
Roughness : roughness, rough, _R
|
||||
AO : occlusion, ambientocclusion, _AO
|
||||
Emission : emissive, emission, _E
|
||||
Height : height, parallax, opacity, _H
|
||||
|
||||
## Registry
|
||||
processed_fbx.txt — liste GUID + nom de fichier
|
||||
Suppression FBX → retiré automatiquement
|
||||
Clic droit → "Remove from registry" → réapparaît au prochain drop
|
||||
@@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 59e10e549f96dca46b441aabe7904ec7
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user