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