diff --git a/Assets/Scripts/Editor/FBXImporter/processed_fbx.txt b/Assets/Scripts/Editor/FBXImporter/processed_fbx.txt index 010ead72..461928cc 100644 --- a/Assets/Scripts/Editor/FBXImporter/processed_fbx.txt +++ b/Assets/Scripts/Editor/FBXImporter/processed_fbx.txt @@ -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 diff --git a/Assets/Unheimlich_POC/Content/Gameplay/WIP dev/Dialogue/Materials/TMP_SDF_PortalClip.shadergraph b/Assets/Unheimlich_POC/Content/Gameplay/WIP dev/Dialogue/Materials/TMP_SDF_PortalClip.shadergraph index 27fceeae..3322479d 100644 --- a/Assets/Unheimlich_POC/Content/Gameplay/WIP dev/Dialogue/Materials/TMP_SDF_PortalClip.shadergraph +++ b/Assets/Unheimlich_POC/Content/Gameplay/WIP dev/Dialogue/Materials/TMP_SDF_PortalClip.shadergraph @@ -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 } } diff --git a/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/AnimationExtractor.cs b/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/AnimationExtractor.cs new file mode 100644 index 00000000..d0f14260 --- /dev/null +++ b/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/AnimationExtractor.cs @@ -0,0 +1,84 @@ +using UnityEngine; +using UnityEditor; +using System.IO; + +namespace FBXImporter +{ + /// + /// Extracts animation clips embedded in a FBX into individual .anim files + /// in the asset's Animations/ folder. + /// + public static class AnimationExtractor + { + /// + /// Reads all animation clips from the FBX and saves each as a .anim file + /// in animationsDir. Skips clips that already exist (idempotent). + /// + 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(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)}"); + } + } + + /// + /// Sanitizes a clip name for use as a filename. + /// "Take 001" → "Take_001" + /// + static string SanitizeClipName(string name) + { + return name.Replace(" ", "_") + .Replace("/", "_") + .Replace("\\", "_") + .Replace(":", "_") + .Replace("|", "_") + .Replace("*", "_") + .Replace("?", "_") + .Replace("\"", "_") + .Replace("<", "_") + .Replace(">", "_"); + } + } +} diff --git a/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/AnimationExtractor.cs.meta b/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/AnimationExtractor.cs.meta new file mode 100644 index 00000000..e1e6d4f7 --- /dev/null +++ b/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/AnimationExtractor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0ec504778d5233d40a1b99a372f7eb3e \ No newline at end of file diff --git a/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/AnimatorControllerBuilder.cs b/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/AnimatorControllerBuilder.cs new file mode 100644 index 00000000..30f5e036 --- /dev/null +++ b/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/AnimatorControllerBuilder.cs @@ -0,0 +1,94 @@ +using UnityEngine; +using UnityEditor; +using UnityEditor.Animations; +using System.IO; +using System.Collections.Generic; + +namespace FBXImporter +{ + /// + /// 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. + /// + public static class AnimatorControllerBuilder + { + /// + /// Creates an AnimatorController in animationsDir, adds one state per .anim clip found, + /// then assigns the controller to the Animator on the prefab at prefabPath. + /// + 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(); + foreach (var guid in clipGuids) + { + var clip = AssetDatabase.LoadAssetAtPath( + 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(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(); + if (animator == null) + animator = prefabInstance.AddComponent(); + + animator.runtimeAnimatorController = controller; + + // Save prefab changes + PrefabUtility.SaveAsPrefabAsset(prefabInstance, prefabPath); + PrefabUtility.UnloadPrefabContents(prefabInstance); + + Debug.Log($"[FBXImporter] AnimatorController assigned to prefab: {Path.GetFileName(prefabPath)}"); + } + } +} diff --git a/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/AnimatorControllerBuilder.cs.meta b/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/AnimatorControllerBuilder.cs.meta new file mode 100644 index 00000000..dba163a5 --- /dev/null +++ b/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/AnimatorControllerBuilder.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5d86102882208454999c396da82b6886 \ No newline at end of file diff --git a/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/FBXDeletionWatcher.cs b/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/FBXDeletionWatcher.cs index 0dcdf24a..15c66969 100644 --- a/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/FBXDeletionWatcher.cs +++ b/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/FBXDeletionWatcher.cs @@ -4,16 +4,27 @@ namespace FBXImporter { /// /// 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. /// 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 diff --git a/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/FBXProcessor.cs b/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/FBXProcessor.cs index 7782a1a5..046edfa1 100644 --- a/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/FBXProcessor.cs +++ b/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/FBXProcessor.cs @@ -7,12 +7,22 @@ namespace FBXImporter public static class FBXProcessor { /// - /// 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) /// 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(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}"); } } } diff --git a/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/ProcessedFBXRegistry.cs b/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/ProcessedFBXRegistry.cs index 54a5d571..428c15f0 100644 --- a/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/ProcessedFBXRegistry.cs +++ b/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/ProcessedFBXRegistry.cs @@ -8,7 +8,7 @@ namespace FBXImporter /// /// 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" /// public static class ProcessedFBXRegistry { @@ -44,14 +44,28 @@ namespace FBXImporter if (changed) Save(); } - /// Removes a single FBX from the registry by asset path. + /// Removes a FBX from the registry by asset path. 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)); + } + + /// + /// 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. + /// + 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; } } } diff --git a/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/TextureMatcher.cs b/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/TextureMatcher.cs index c856d683..a89900a1 100644 --- a/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/TextureMatcher.cs +++ b/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/Core/TextureMatcher.cs @@ -10,64 +10,76 @@ namespace FBXImporter static readonly string[] SupportedExtensions = { ".png", ".tga", ".jpg", ".jpeg", ".tiff", ".exr" }; /// - /// 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. /// public static readonly Dictionary SlotKeywords = new Dictionary { - { "_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" } }, }; /// - /// 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 /// 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(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(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 /// /// 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" /// 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; diff --git a/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/FBXImportDialog.cs b/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/FBXImportDialog.cs index 77626dd7..0ee452da 100644 --- a/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/FBXImportDialog.cs +++ b/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/FBXImportDialog.cs @@ -5,18 +5,21 @@ using System.IO; namespace FBXImporter { /// - /// 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. /// 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(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(folders.Root); if (rootObj != null) EditorGUIUtility.PingObject(rootObj); } + /// + /// 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/. + /// + 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()) diff --git a/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/FBXImporterWindow.cs b/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/FBXImporterWindow.cs index 22fe34e9..8072747c 100644 --- a/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/FBXImporterWindow.cs +++ b/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/FBXImporterWindow.cs @@ -4,25 +4,40 @@ using System.IO; namespace FBXImporter { + /// + /// 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 + /// 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("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(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(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); + } + } } } diff --git a/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/README.md b/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/README.md deleted file mode 100644 index 2949669d..00000000 --- a/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/README.md +++ /dev/null @@ -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 diff --git a/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/README.md.meta b/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/README.md.meta deleted file mode 100644 index 035b645d..00000000 --- a/Assets/Unheimlich_POC/Scripts/Editor/FBXImporter/README.md.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 59e10e549f96dca46b441aabe7904ec7 -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/ProjectSettings/Packages/com.unity.probuilder/Settings.json b/ProjectSettings/Packages/com.unity.probuilder/Settings.json index 978217b1..6d9fb264 100644 --- a/ProjectSettings/Packages/com.unity.probuilder/Settings.json +++ b/ProjectSettings/Packages/com.unity.probuilder/Settings.json @@ -46,10 +46,30 @@ "key": "ShapeComponent.ResetSettings", "value": "{\"m_Value\":false}" }, + { + "type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", + "key": "ShapeComponent.SettingsEnabled", + "value": "{\"m_Value\":false}" + }, { "type": "UnityEngine.ProBuilder.SelectMode, Unity.ProBuilder, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", "key": "s_SelectMode", "value": "{\"m_Value\":4}" + }, + { + "type": "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", + "key": "ShapeBuilder.ActiveShapeIndex", + "value": "{\"m_Value\":5}" + }, + { + "type": "UnityEngine.Vector3, UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", + "key": "ShapeBuilder.LastSize.Prism", + "value": "{\"m_Value\":{\"x\":1.0,\"y\":1.0,\"z\":1.0}}" + }, + { + "type": "UnityEngine.Quaternion, UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", + "key": "ShapeBuilder.LastRotation.Prism", + "value": "{\"m_Value\":{\"x\":0.0,\"y\":0.0,\"z\":0.0,\"w\":1.0}}" } ] }