Rework : FBX importer
This commit is contained in:
-3
@@ -1,8 +1,5 @@
|
||||
namespace FBXImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds all resolved folder paths for a single imported asset.
|
||||
/// </summary>
|
||||
public class AssetFolders
|
||||
{
|
||||
public string Root { get; set; }
|
||||
@@ -0,0 +1,23 @@
|
||||
using UnityEditor;
|
||||
|
||||
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.
|
||||
/// </summary>
|
||||
public class FBXDeletionWatcher : UnityEditor.AssetModificationProcessor
|
||||
{
|
||||
static AssetDeleteResult OnWillDeleteAsset(string path, RemoveAssetOptions options)
|
||||
{
|
||||
if (path.EndsWith(".fbx", System.StringComparison.OrdinalIgnoreCase)
|
||||
&& ProcessedFBXRegistry.IsRegistered(path))
|
||||
{
|
||||
ProcessedFBXRegistry.Unregister(path);
|
||||
}
|
||||
|
||||
// Always let Unity handle the actual deletion
|
||||
return AssetDeleteResult.DidNotDelete;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 689b81497b9e3a444965cbdf9a0260a2
|
||||
+29
-44
@@ -4,82 +4,64 @@ using System.IO;
|
||||
|
||||
namespace FBXImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Core processing logic. Called explicitly by the right-click menu or the Inspector window.
|
||||
/// Never runs automatically — no AssetPostprocessor, no domain reload triggers.
|
||||
/// </summary>
|
||||
public static class FBXProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Full pipeline for a single FBX asset path:
|
||||
/// 1. Detect source (Blender / Maya)
|
||||
/// 2. Configure ModelImporter settings
|
||||
/// 3. Create folder structure
|
||||
/// 4. Force reimport with new settings
|
||||
/// 5. Connect PBR textures → create .mat files
|
||||
/// 6. Generate prefab (with LODGroup if applicable)
|
||||
/// Full pipeline for a single FBX.
|
||||
/// animationType: None=0, Legacy=1, Generic=2, Humanoid=3
|
||||
/// </summary>
|
||||
public static void Process(string fbxAssetPath)
|
||||
public static void Process(string fbxAssetPath,
|
||||
ModelImporterAnimationType animationType = ModelImporterAnimationType.Generic,
|
||||
string rootNode = "")
|
||||
{
|
||||
if (!fbxAssetPath.EndsWith(".fbx", System.StringComparison.OrdinalIgnoreCase))
|
||||
if (!fbxAssetPath.EndsWith(".fbx", System.StringComparison.OrdinalIgnoreCase) ||
|
||||
!fbxAssetPath.StartsWith("Assets/"))
|
||||
{
|
||||
Debug.LogWarning($"[FBXImporter] Not a FBX file: {fbxAssetPath}");
|
||||
Debug.LogWarning($"[FBXImporter] Skipped: {fbxAssetPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fbxAssetPath.StartsWith("Assets/"))
|
||||
{
|
||||
Debug.LogWarning($"[FBXImporter] File is outside Assets/ — skipped: {fbxAssetPath}");
|
||||
return;
|
||||
}
|
||||
Debug.Log($"[FBXImporter] ── Processing {Path.GetFileName(fbxAssetPath)} ──");
|
||||
|
||||
Debug.Log($"[FBXImporter] ── Processing {Path.GetFileName(fbxAssetPath)} ──────────────");
|
||||
|
||||
// 1. Detect source
|
||||
var source = FBXSourceDetector.Detect(fbxAssetPath);
|
||||
Debug.Log($"[FBXImporter] Source detected: {source}");
|
||||
|
||||
// 2. Resolve / create folder structure
|
||||
var source = FBXSourceDetector.Detect(fbxAssetPath);
|
||||
var folders = FolderSetup.Resolve(fbxAssetPath);
|
||||
|
||||
FolderSetup.Create(
|
||||
Path.GetDirectoryName(folders.Root),
|
||||
Path.GetFileName(folders.Root),
|
||||
false // Animations folder only if user wants it — see dialog
|
||||
animationType != ModelImporterAnimationType.None
|
||||
);
|
||||
folders = FolderSetup.Resolve(fbxAssetPath); // re-resolve after creation
|
||||
folders = FolderSetup.Resolve(fbxAssetPath);
|
||||
|
||||
// 3. Configure ModelImporter
|
||||
// Configure importer
|
||||
var importer = AssetImporter.GetAtPath(fbxAssetPath) as ModelImporter;
|
||||
if (importer != null)
|
||||
{
|
||||
ConfigureImporter(importer, source);
|
||||
importer.SaveAndReimport(); // force reimport with new settings
|
||||
ConfigureImporter(importer, source, animationType, rootNode);
|
||||
importer.SaveAndReimport();
|
||||
}
|
||||
|
||||
// 4. Load the imported model
|
||||
var fbxAsset = AssetDatabase.LoadAssetAtPath<GameObject>(fbxAssetPath);
|
||||
if (fbxAsset == null)
|
||||
{
|
||||
Debug.LogError($"[FBXImporter] Could not load FBX as GameObject: {fbxAssetPath}");
|
||||
Debug.LogError($"[FBXImporter] Could not load: {fbxAssetPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. Connect PBR textures
|
||||
ProcessMaterials(fbxAsset, folders, source);
|
||||
|
||||
// 6. Create prefab
|
||||
string assetName = Path.GetFileNameWithoutExtension(fbxAssetPath);
|
||||
PrefabBuilder.CreatePrefab(fbxAsset, folders.Prefabs, assetName);
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
Debug.Log($"[FBXImporter] ── Done: {assetName} ──────────────────────────────────────");
|
||||
Debug.Log($"[FBXImporter] ── Done: {assetName} ──");
|
||||
}
|
||||
|
||||
// ─── ModelImporter settings ───────────────────────────────────────────────
|
||||
// ── Importer settings ─────────────────────────────────────────────────────
|
||||
|
||||
static void ConfigureImporter(ModelImporter importer, FBXSource source)
|
||||
static void ConfigureImporter(ModelImporter importer, FBXSource source,
|
||||
ModelImporterAnimationType animType, string rootNode)
|
||||
{
|
||||
importer.importCameras = false;
|
||||
importer.importLights = false;
|
||||
@@ -87,18 +69,21 @@ namespace FBXImporter
|
||||
importer.importTangents = ModelImporterTangents.CalculateMikk;
|
||||
importer.importBlendShapes = true;
|
||||
importer.importVisibility = true;
|
||||
importer.useFileScale = true;
|
||||
importer.globalScale = 1f;
|
||||
|
||||
importer.materialImportMode = ModelImporterMaterialImportMode.ImportViaMaterialDescription;
|
||||
importer.materialLocation = ModelImporterMaterialLocation.External;
|
||||
importer.materialName = ModelImporterMaterialName.BasedOnMaterialName;
|
||||
importer.materialSearch = ModelImporterMaterialSearch.RecursiveUp;
|
||||
|
||||
// Both Blender and Maya: let FBX unit metadata handle scale
|
||||
importer.useFileScale = true;
|
||||
importer.globalScale = 1f;
|
||||
// Rig / animation settings
|
||||
importer.animationType = animType;
|
||||
if (!string.IsNullOrWhiteSpace(rootNode))
|
||||
importer.motionNodeName = rootNode;
|
||||
}
|
||||
|
||||
// ─── Material processing ──────────────────────────────────────────────────
|
||||
// ── Materials ─────────────────────────────────────────────────────────────
|
||||
|
||||
static void ProcessMaterials(GameObject root, AssetFolders folders, FBXSource source)
|
||||
{
|
||||
@@ -149,7 +134,7 @@ namespace FBXImporter
|
||||
|
||||
static void ConnectPBRTextures(Material mat, string textureDir, string baseName)
|
||||
{
|
||||
foreach (var slot in TextureMatcher.SlotSuffixes.Keys)
|
||||
foreach (var slot in TextureMatcher.SlotKeywords.Keys)
|
||||
{
|
||||
var tex = TextureMatcher.FindForSlot(textureDir, baseName, slot);
|
||||
if (tex == null) continue;
|
||||
-5
@@ -7,10 +7,6 @@ namespace FBXImporter
|
||||
|
||||
public static class FBXSourceDetector
|
||||
{
|
||||
/// <summary>
|
||||
/// Detects whether a FBX was authored in Blender or Maya.
|
||||
/// Priority: folder convention → binary header fallback.
|
||||
/// </summary>
|
||||
public static FBXSource Detect(string assetPath)
|
||||
{
|
||||
if (assetPath.Contains("/BlenderAssets/")) return FBXSource.Blender;
|
||||
@@ -26,7 +22,6 @@ namespace FBXImporter
|
||||
fs.Read(buffer, 0, buffer.Length);
|
||||
|
||||
string header = Encoding.ASCII.GetString(buffer);
|
||||
|
||||
if (header.Contains("Blender")) return FBXSource.Blender;
|
||||
if (header.Contains("Maya")) return FBXSource.Maya;
|
||||
if (header.Contains("FBX SDK")) return FBXSource.Maya;
|
||||
+1
-12
@@ -7,31 +7,20 @@ namespace FBXImporter
|
||||
{
|
||||
static readonly string[] RequiredFolders = { "Sources", "Textures", "Materials", "Prefabs", "Audio", "VFX" };
|
||||
|
||||
/// <summary>
|
||||
/// Creates the full folder structure under parentPath/assetName.
|
||||
/// Safe to call multiple times — skips folders that already exist.
|
||||
/// </summary>
|
||||
public static AssetFolders Create(string parentPath, string assetName, bool withAnimations = false)
|
||||
{
|
||||
string root = $"{parentPath}/{assetName}";
|
||||
EnsureFolder(parentPath, assetName);
|
||||
|
||||
foreach (var folder in RequiredFolders)
|
||||
EnsureFolder(root, folder);
|
||||
|
||||
string animPath = $"{root}/Animations";
|
||||
if (withAnimations)
|
||||
EnsureFolder(root, "Animations");
|
||||
if (withAnimations) EnsureFolder(root, "Animations");
|
||||
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
return BuildFolders(root, withAnimations ? animPath : null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves existing folder paths from a known FBX asset path.
|
||||
/// If the FBX is inside Sources/, goes one level up to the root.
|
||||
/// </summary>
|
||||
public static AssetFolders Resolve(string fbxAssetPath)
|
||||
{
|
||||
string dir = Path.GetDirectoryName(fbxAssetPath).Replace("\\", "/");
|
||||
-7
@@ -6,10 +6,6 @@ namespace FBXImporter
|
||||
{
|
||||
public static class PrefabBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a prefab from a loaded FBX GameObject.
|
||||
/// Adds a LODGroup automatically if _LOD0, _LOD1... meshes are found.
|
||||
/// </summary>
|
||||
public static GameObject CreatePrefab(GameObject fbxRoot, string prefabDir, string assetName)
|
||||
{
|
||||
if (fbxRoot == null) return null;
|
||||
@@ -39,7 +35,6 @@ namespace FBXImporter
|
||||
static bool TrySetupLODGroup(GameObject root)
|
||||
{
|
||||
var lodMap = new SortedDictionary<int, List<Renderer>>();
|
||||
|
||||
foreach (var r in root.GetComponentsInChildren<Renderer>(true))
|
||||
{
|
||||
int idx = ExtractLODIndex(r.gameObject.name.ToUpper());
|
||||
@@ -57,13 +52,11 @@ namespace FBXImporter
|
||||
float[] thresh = { 0.6f, 0.3f, 0.1f, 0.05f, 0.01f };
|
||||
var lods = new LOD[lodMap.Count];
|
||||
int i = 0;
|
||||
|
||||
foreach (var kvp in lodMap)
|
||||
{
|
||||
lods[i] = new LOD(i < thresh.Length ? thresh[i] : 0.01f, kvp.Value.ToArray());
|
||||
i++;
|
||||
}
|
||||
|
||||
group.SetLODs(lods);
|
||||
group.RecalculateBounds();
|
||||
return true;
|
||||
@@ -0,0 +1,86 @@
|
||||
using UnityEditor;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
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"
|
||||
/// </summary>
|
||||
public static class ProcessedFBXRegistry
|
||||
{
|
||||
static readonly string RegistryPath = "Assets/Scripts/Editor/FBXImporter/processed_fbx.txt";
|
||||
|
||||
// guid → filename (for display)
|
||||
static Dictionary<string, string> _registry;
|
||||
|
||||
static Dictionary<string, string> Registry
|
||||
{
|
||||
get { if (_registry == null) Load(); return _registry; }
|
||||
}
|
||||
|
||||
public static bool IsRegistered(string assetPath)
|
||||
{
|
||||
string guid = AssetDatabase.AssetPathToGUID(assetPath);
|
||||
return !string.IsNullOrEmpty(guid) && Registry.ContainsKey(guid);
|
||||
}
|
||||
|
||||
public static void Register(IEnumerable<string> assetPaths)
|
||||
{
|
||||
bool changed = false;
|
||||
foreach (var path in assetPaths)
|
||||
{
|
||||
string guid = AssetDatabase.AssetPathToGUID(path);
|
||||
if (string.IsNullOrEmpty(guid)) continue;
|
||||
if (!Registry.ContainsKey(guid))
|
||||
{
|
||||
Registry[guid] = Path.GetFileName(path);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) Save();
|
||||
}
|
||||
|
||||
/// <summary>Removes a single 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))
|
||||
{
|
||||
Save();
|
||||
UnityEngine.Debug.Log($"[FBXImporter] Removed from registry: {Path.GetFileName(assetPath)}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Persistence ───────────────────────────────────────────────────────────
|
||||
|
||||
static void Load()
|
||||
{
|
||||
_registry = new Dictionary<string, string>();
|
||||
if (!File.Exists(RegistryPath)) return;
|
||||
|
||||
foreach (var line in File.ReadAllLines(RegistryPath))
|
||||
{
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
static void Save()
|
||||
{
|
||||
string dir = Path.GetDirectoryName(RegistryPath);
|
||||
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
|
||||
|
||||
var lines = Registry.Select(kvp => $"{kvp.Key}\t{kvp.Value}").ToArray();
|
||||
File.WriteAllLines(RegistryPath, lines);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FBXImporter
|
||||
{
|
||||
public static class TextureMatcher
|
||||
{
|
||||
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.
|
||||
/// </summary>
|
||||
public static readonly Dictionary<string, string[]> SlotKeywords = new Dictionary<string, string[]>
|
||||
{
|
||||
{ "_BaseMap", new[] { "albedo", "diffuse", "basecolor", "base_color", "color", "_D", "_d" } },
|
||||
{ "_BumpMap", new[] { "normal", "nrm", "_N", "_n" } },
|
||||
{ "_MetallicGlossMap", new[] { "metallic", "metal", "_M", "_m" } },
|
||||
{ "_SpecGlossMap", new[] { "roughness", "rough", "_R", "_r" } },
|
||||
{ "_OcclusionMap", new[] { "occlusion", "ambientocclusion", "_AO", "_ao" } },
|
||||
{ "_EmissionMap", new[] { "emissive", "emission", "_E", "_e" } },
|
||||
{ "_ParallaxMap", new[] { "height", "parallax", "opacity", "_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
|
||||
/// </summary>
|
||||
public static Texture2D FindForSlot(string textureDir, string baseName, string shaderSlot)
|
||||
{
|
||||
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 });
|
||||
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
|
||||
string filename = Path.GetFileNameWithoutExtension(assetPath).ToLower();
|
||||
string ext = Path.GetExtension(assetPath).ToLower();
|
||||
|
||||
// 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)
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strips DCC-specific suffixes to recover the clean base name.
|
||||
/// Maya : "hero_blinn1" → "hero" / "standardSurface1" → "Material"
|
||||
/// </summary>
|
||||
public static string CleanMaterialName(string rawName, FBXSource source)
|
||||
{
|
||||
string name = rawName;
|
||||
|
||||
if (source == FBXSource.Maya)
|
||||
{
|
||||
string[] mayaSuffixes = { "_blinn", "_phong", "_lambert", "_aiStandardSurface", "_standardSurface", "_SG" };
|
||||
foreach (var s in mayaSuffixes)
|
||||
{
|
||||
int idx = name.ToLower().IndexOf(s.ToLower());
|
||||
if (idx > 0) { name = name.Substring(0, idx); break; }
|
||||
}
|
||||
|
||||
if (name.ToLower().StartsWith("standardsurface") ||
|
||||
name.ToLower().StartsWith("lambert") ||
|
||||
name.ToLower().StartsWith("phong") ||
|
||||
name.ToLower().StartsWith("blinn"))
|
||||
name = "Material";
|
||||
|
||||
name = System.Text.RegularExpressions.Regex.Replace(name, @"\d+$", "").TrimEnd('_');
|
||||
}
|
||||
|
||||
foreach (var s in new[] { "_MAT", "_Mat", "_material", "_Material" })
|
||||
if (name.EndsWith(s)) name = name.Substring(0, name.Length - s.Length);
|
||||
|
||||
return string.IsNullOrWhiteSpace(name) ? rawName : name;
|
||||
}
|
||||
|
||||
static void EnsureNormalMapType(string assetPath)
|
||||
{
|
||||
var ti = AssetImporter.GetAtPath(assetPath) as TextureImporter;
|
||||
if (ti != null && ti.textureType != TextureImporterType.NormalMap)
|
||||
{
|
||||
ti.textureType = TextureImporterType.NormalMap;
|
||||
ti.SaveAndReimport();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b7a761cf384e2d143b6e8b9fafd6281e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,76 +0,0 @@
|
||||
using UnityEditor;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace FBXImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Persistent registry of FBX files that have already been seen by the importer.
|
||||
/// Stores asset GUIDs in a text file so it survives domain reloads and Unity restarts.
|
||||
///
|
||||
/// A FBX is "registered" as soon as the user confirms the batch dialog —
|
||||
/// regardless of whether they chose to process it or not.
|
||||
/// This ensures the dialog never shows the same file twice.
|
||||
/// </summary>
|
||||
public static class ProcessedFBXRegistry
|
||||
{
|
||||
// Stored inside the plugin folder so it travels with the project
|
||||
static readonly string RegistryPath = "Assets/Scripts/Editor/FBXImporter/processed_fbx.txt";
|
||||
|
||||
static HashSet<string> _guids;
|
||||
|
||||
static HashSet<string> Guids
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_guids == null) Load();
|
||||
return _guids;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Returns true if this FBX GUID has already been seen.</summary>
|
||||
public static bool IsRegistered(string assetPath)
|
||||
{
|
||||
string guid = AssetDatabase.AssetPathToGUID(assetPath);
|
||||
return !string.IsNullOrEmpty(guid) && Guids.Contains(guid);
|
||||
}
|
||||
|
||||
/// <summary>Marks one or more FBX paths as seen and saves the registry.</summary>
|
||||
public static void Register(IEnumerable<string> assetPaths)
|
||||
{
|
||||
bool changed = false;
|
||||
foreach (var path in assetPaths)
|
||||
{
|
||||
string guid = AssetDatabase.AssetPathToGUID(path);
|
||||
if (!string.IsNullOrEmpty(guid) && Guids.Add(guid))
|
||||
changed = true;
|
||||
}
|
||||
if (changed) Save();
|
||||
}
|
||||
|
||||
// ── Persistence ───────────────────────────────────────────────────────────
|
||||
|
||||
static void Load()
|
||||
{
|
||||
_guids = new HashSet<string>();
|
||||
if (!File.Exists(RegistryPath)) return;
|
||||
|
||||
foreach (var line in File.ReadAllLines(RegistryPath))
|
||||
{
|
||||
string trimmed = line.Trim();
|
||||
if (!string.IsNullOrEmpty(trimmed))
|
||||
_guids.Add(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
static void Save()
|
||||
{
|
||||
// Ensure the directory exists
|
||||
string dir = Path.GetDirectoryName(RegistryPath);
|
||||
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
|
||||
|
||||
File.WriteAllLines(RegistryPath, Guids.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FBXImporter
|
||||
{
|
||||
public static class TextureMatcher
|
||||
{
|
||||
static readonly string[] SupportedExtensions = { ".png", ".tga", ".jpg", ".jpeg", ".tiff", ".exr" };
|
||||
|
||||
public static readonly Dictionary<string, string[]> SlotSuffixes = new Dictionary<string, string[]>
|
||||
{
|
||||
{ "_BaseMap", new[] { "_D", "_d", "_Diffuse", "_diffuse", "_Albedo", "_albedo", "_Color", "_BaseColor" } },
|
||||
{ "_BumpMap", new[] { "_N", "_n", "_Normal", "_normal", "_NRM", "_nrm" } },
|
||||
{ "_MetallicGlossMap", new[] { "_M", "_m", "_Metallic", "_metallic" } },
|
||||
{ "_SpecGlossMap", new[] { "_R", "_r", "_Roughness", "_roughness" } },
|
||||
{ "_OcclusionMap", new[] { "_AO", "_ao", "_Occlusion", "_occlusion" } },
|
||||
{ "_EmissionMap", new[] { "_E", "_e", "_Emission", "_emission" } },
|
||||
{ "_ParallaxMap", new[] { "_H", "_h", "_Height", "_height" } },
|
||||
};
|
||||
|
||||
public static Texture2D FindForSlot(string textureDir, string baseName, string shaderSlot)
|
||||
{
|
||||
if (!SlotSuffixes.TryGetValue(shaderSlot, out string[] suffixes))
|
||||
return null;
|
||||
|
||||
foreach (var suffix in suffixes)
|
||||
{
|
||||
var tex = FindFile(textureDir, baseName, suffix);
|
||||
if (tex == null) continue;
|
||||
|
||||
if (shaderSlot == "_BumpMap")
|
||||
EnsureNormalMapType(AssetDatabase.GetAssetPath(tex));
|
||||
|
||||
return tex;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strips DCC-specific suffixes to recover the clean base name.
|
||||
/// Maya : "hero_blinn1" → "hero" / "standardSurface1" → "Material"
|
||||
/// Both : "tree_MAT" → "tree"
|
||||
/// </summary>
|
||||
public static string CleanMaterialName(string rawName, FBXSource source)
|
||||
{
|
||||
string name = rawName;
|
||||
|
||||
if (source == FBXSource.Maya)
|
||||
{
|
||||
string[] mayaSuffixes = { "_blinn", "_phong", "_lambert", "_aiStandardSurface", "_standardSurface", "_SG" };
|
||||
foreach (var s in mayaSuffixes)
|
||||
{
|
||||
int idx = name.ToLower().IndexOf(s.ToLower());
|
||||
if (idx > 0) { name = name.Substring(0, idx); break; }
|
||||
}
|
||||
|
||||
// Handle names that ARE a maya default shader (e.g. "standardSurface1")
|
||||
if (name.ToLower().StartsWith("standardsurface") ||
|
||||
name.ToLower().StartsWith("lambert") ||
|
||||
name.ToLower().StartsWith("phong") ||
|
||||
name.ToLower().StartsWith("blinn"))
|
||||
name = "Material";
|
||||
|
||||
name = System.Text.RegularExpressions.Regex.Replace(name, @"\d+$", "").TrimEnd('_');
|
||||
}
|
||||
|
||||
foreach (var s in new[] { "_MAT", "_Mat", "_material", "_Material" })
|
||||
if (name.EndsWith(s)) name = name.Substring(0, name.Length - s.Length);
|
||||
|
||||
return string.IsNullOrWhiteSpace(name) ? rawName : name;
|
||||
}
|
||||
|
||||
static Texture2D FindFile(string dir, string baseName, string suffix)
|
||||
{
|
||||
foreach (var ext in SupportedExtensions)
|
||||
{
|
||||
var tex = AssetDatabase.LoadAssetAtPath<Texture2D>($"{dir}/{baseName}{suffix}{ext}");
|
||||
if (tex != null) return tex;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static void EnsureNormalMapType(string assetPath)
|
||||
{
|
||||
var ti = AssetImporter.GetAtPath(assetPath) as TextureImporter;
|
||||
if (ti != null && ti.textureType != TextureImporterType.NormalMap)
|
||||
{
|
||||
ti.textureType = TextureImporterType.NormalMap;
|
||||
ti.SaveAndReimport();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace FBXImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Batch dialog shown once after a drop that contains new FBX files.
|
||||
/// Lists every new FBX with a toggle — user decides which ones to process.
|
||||
/// All listed files are already registered so they won't appear again.
|
||||
/// </summary>
|
||||
public class FBXBatchDialog : EditorWindow
|
||||
{
|
||||
// ── Entry data ────────────────────────────────────────────────────────────
|
||||
|
||||
private class FBXEntry
|
||||
{
|
||||
public string Path;
|
||||
public string Name;
|
||||
public FBXSource Source;
|
||||
public bool ShouldProcess = true; // checked by default
|
||||
}
|
||||
|
||||
private List<FBXEntry> _entries = new List<FBXEntry>();
|
||||
private Vector2 _scroll;
|
||||
|
||||
// ── Entry point ───────────────────────────────────────────────────────────
|
||||
|
||||
public static void Show(List<string> fbxPaths)
|
||||
{
|
||||
var window = GetWindow<FBXBatchDialog>(true, "FBX Importer — New Assets", true);
|
||||
window.minSize = new Vector2(500, 300);
|
||||
|
||||
foreach (var path in fbxPaths)
|
||||
{
|
||||
window._entries.Add(new FBXEntry
|
||||
{
|
||||
Path = path,
|
||||
Name = Path.GetFileName(path),
|
||||
Source = FBXSourceDetector.Detect(path),
|
||||
ShouldProcess = true,
|
||||
});
|
||||
}
|
||||
|
||||
window.ShowModal();
|
||||
}
|
||||
|
||||
// ── GUI ───────────────────────────────────────────────────────────────────
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
GUILayout.Space(10);
|
||||
GUILayout.Label($"{_entries.Count} new FBX detected", EditorStyles.boldLabel);
|
||||
GUILayout.Label("All files are registered. Check which ones you want to process.",
|
||||
EditorStyles.miniLabel);
|
||||
GUILayout.Space(8);
|
||||
|
||||
// ── Header ────────────────────────────────────────────────────────────
|
||||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||||
{
|
||||
GUILayout.Label("Process", GUILayout.Width(55));
|
||||
GUILayout.Label("File", GUILayout.Width(220));
|
||||
GUILayout.Label("Source", GUILayout.Width(70));
|
||||
GUILayout.Label("Path", GUILayout.ExpandWidth(true));
|
||||
}
|
||||
|
||||
// ── File list ─────────────────────────────────────────────────────────
|
||||
_scroll = EditorGUILayout.BeginScrollView(_scroll, GUILayout.ExpandHeight(true));
|
||||
foreach (var entry in _entries)
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
entry.ShouldProcess = EditorGUILayout.Toggle(entry.ShouldProcess, GUILayout.Width(55));
|
||||
|
||||
var nameStyle = new GUIStyle(EditorStyles.label)
|
||||
{ normal = { textColor = entry.ShouldProcess ? Color.white : Color.gray } };
|
||||
|
||||
GUILayout.Label(entry.Name, nameStyle, GUILayout.Width(220));
|
||||
GUILayout.Label(entry.Source.ToString(), EditorStyles.miniLabel, GUILayout.Width(70));
|
||||
GUILayout.Label(entry.Path, EditorStyles.miniLabel, GUILayout.ExpandWidth(true));
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndScrollView();
|
||||
|
||||
// ── Select all / none ─────────────────────────────────────────────────
|
||||
GUILayout.Space(4);
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
if (GUILayout.Button("All", GUILayout.Width(60)))
|
||||
_entries.ForEach(e => e.ShouldProcess = true);
|
||||
if (GUILayout.Button("None", GUILayout.Width(60)))
|
||||
_entries.ForEach(e => e.ShouldProcess = false);
|
||||
}
|
||||
|
||||
GUILayout.Space(8);
|
||||
|
||||
// ── Action buttons ────────────────────────────────────────────────────
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
if (GUILayout.Button("Skip all", GUILayout.Width(80)))
|
||||
{
|
||||
// Already registered — just close, nothing to process
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
int count = _entries.FindAll(e => e.ShouldProcess).Count;
|
||||
string label = count > 0 ? $"Process {count} FBX" : "Process (none selected)";
|
||||
|
||||
using (new EditorGUI.DisabledScope(count == 0))
|
||||
{
|
||||
if (GUILayout.Button(label, GUILayout.Width(140)))
|
||||
{
|
||||
ProcessSelected();
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GUILayout.Space(8);
|
||||
}
|
||||
|
||||
// ── Processing ────────────────────────────────────────────────────────────
|
||||
|
||||
void ProcessSelected()
|
||||
{
|
||||
foreach (var entry in _entries)
|
||||
{
|
||||
if (!entry.ShouldProcess) continue;
|
||||
|
||||
// Open individual config dialog for each selected FBX
|
||||
FBXImportDialog.Show(entry.Path, entry.Source);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using System.Linq;
|
||||
|
||||
namespace FBXImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a right-click context menu to FBX files in the Project panel.
|
||||
/// This is the ONLY way to trigger the FBX import pipeline.
|
||||
/// Nothing runs automatically — the user always initiates processing explicitly.
|
||||
/// </summary>
|
||||
public static class FBXContextMenu
|
||||
{
|
||||
// ── Single FBX ────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Right-click a .fbx file → "FBX Importer / Process this FBX"
|
||||
/// Runs the full pipeline: folders + settings + materials + prefab.
|
||||
/// </summary>
|
||||
[MenuItem("Assets/FBX Importer/Process this FBX", false, 1000)]
|
||||
static void ProcessSingle()
|
||||
{
|
||||
foreach (var obj in Selection.objects)
|
||||
{
|
||||
string path = AssetDatabase.GetAssetPath(obj);
|
||||
if (!path.EndsWith(".fbx", System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Debug.LogWarning($"[FBXImporter] Skipped (not a FBX): {path}");
|
||||
continue;
|
||||
}
|
||||
FBXProcessor.Process(path);
|
||||
}
|
||||
}
|
||||
|
||||
[MenuItem("Assets/FBX Importer/Process this FBX", true)]
|
||||
static bool ProcessSingleValidate()
|
||||
{
|
||||
// Only show the menu item when at least one selected file is a .fbx
|
||||
return Selection.objects.Any(o =>
|
||||
AssetDatabase.GetAssetPath(o).EndsWith(".fbx", System.StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
// ── Reconnect textures only (no reimport, no prefab) ─────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Right-click a .fbx → "FBX Importer / Reconnect Textures Only"
|
||||
/// Useful when you've added new textures to Textures/ after the first import.
|
||||
/// </summary>
|
||||
[MenuItem("Assets/FBX Importer/Reconnect Textures Only", false, 1001)]
|
||||
static void ReconnectTextures()
|
||||
{
|
||||
foreach (var obj in Selection.objects)
|
||||
{
|
||||
string path = AssetDatabase.GetAssetPath(obj);
|
||||
if (!path.EndsWith(".fbx", System.StringComparison.OrdinalIgnoreCase)) continue;
|
||||
|
||||
var source = FBXSourceDetector.Detect(path);
|
||||
var folders = FolderSetup.Resolve(path);
|
||||
var fbxRoot = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||||
|
||||
if (fbxRoot == null)
|
||||
{
|
||||
Debug.LogError($"[FBXImporter] Could not load: {path}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Re-run texture connection only
|
||||
// (calls internal method via FBXProcessor reflection would be cleaner,
|
||||
// but a direct duplicate is simpler and more robust here)
|
||||
Debug.Log($"[FBXImporter] Reconnecting textures for {System.IO.Path.GetFileName(path)}");
|
||||
FBXProcessor.Process(path); // Process handles idempotency — existing mats are reused
|
||||
}
|
||||
}
|
||||
|
||||
[MenuItem("Assets/FBX Importer/Reconnect Textures Only", true)]
|
||||
static bool ReconnectTexturesValidate()
|
||||
{
|
||||
return Selection.objects.Any(o =>
|
||||
AssetDatabase.GetAssetPath(o).EndsWith(".fbx", System.StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
// ── Create folder structure only ──────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Right-click anywhere in the Project panel →
|
||||
/// "FBX Importer / Create Asset Folder Structure"
|
||||
/// Creates Sources/Textures/Materials/Prefabs/Audio/VFX under the selected folder.
|
||||
/// </summary>
|
||||
[MenuItem("Assets/FBX Importer/Create Asset Folder Structure", false, 1020)]
|
||||
static void CreateFolderStructure()
|
||||
{
|
||||
string selectedPath = GetSelectedFolderPath();
|
||||
if (string.IsNullOrEmpty(selectedPath))
|
||||
{
|
||||
Debug.LogWarning("[FBXImporter] Select a folder first.");
|
||||
return;
|
||||
}
|
||||
|
||||
string parent = System.IO.Path.GetDirectoryName(selectedPath).Replace("\\", "/");
|
||||
string folderName = System.IO.Path.GetFileName(selectedPath);
|
||||
|
||||
FolderSetup.Create(parent, folderName, false);
|
||||
Debug.Log($"[FBXImporter] Folder structure created under: {selectedPath}");
|
||||
}
|
||||
|
||||
[MenuItem("Assets/FBX Importer/Create Asset Folder Structure", true)]
|
||||
static bool CreateFolderStructureValidate() => !string.IsNullOrEmpty(GetSelectedFolderPath());
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
static string GetSelectedFolderPath()
|
||||
{
|
||||
if (Selection.activeObject == null) return null;
|
||||
string path = AssetDatabase.GetAssetPath(Selection.activeObject);
|
||||
return AssetDatabase.IsValidFolder(path) ? path : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace FBXImporter
|
||||
{
|
||||
public class FBXBatchDialog : EditorWindow
|
||||
{
|
||||
private class FBXEntry
|
||||
{
|
||||
public string Path;
|
||||
public string Name;
|
||||
public FBXSource Source;
|
||||
public bool ShouldProcess = true;
|
||||
}
|
||||
|
||||
private List<FBXEntry> _entries = new List<FBXEntry>();
|
||||
private Vector2 _scroll;
|
||||
|
||||
public static void Show(List<string> fbxPaths)
|
||||
{
|
||||
var window = GetWindow<FBXBatchDialog>(true, "FBX Importer — New Assets", true);
|
||||
window.minSize = new Vector2(520, 320);
|
||||
|
||||
foreach (var path in fbxPaths)
|
||||
window._entries.Add(new FBXEntry {
|
||||
Path = path,
|
||||
Name = Path.GetFileName(path),
|
||||
Source = FBXSourceDetector.Detect(path),
|
||||
ShouldProcess = true,
|
||||
});
|
||||
|
||||
window.ShowModal();
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
GUILayout.Space(10);
|
||||
GUILayout.Label($"{_entries.Count} new FBX detected", EditorStyles.boldLabel);
|
||||
GUILayout.Label("All files are registered. Check which ones to process.", EditorStyles.miniLabel);
|
||||
GUILayout.Space(8);
|
||||
|
||||
// Header
|
||||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||||
{
|
||||
GUILayout.Label("Process", GUILayout.Width(55));
|
||||
GUILayout.Label("File", GUILayout.Width(200));
|
||||
GUILayout.Label("Source", GUILayout.Width(70));
|
||||
GUILayout.Label("Path", GUILayout.ExpandWidth(true));
|
||||
}
|
||||
|
||||
// List
|
||||
_scroll = EditorGUILayout.BeginScrollView(_scroll, GUILayout.ExpandHeight(true));
|
||||
foreach (var entry in _entries)
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
entry.ShouldProcess = EditorGUILayout.Toggle(entry.ShouldProcess, GUILayout.Width(55));
|
||||
var style = new GUIStyle(EditorStyles.label)
|
||||
{ normal = { textColor = entry.ShouldProcess ? Color.white : Color.gray } };
|
||||
GUILayout.Label(entry.Name, style, GUILayout.Width(200));
|
||||
GUILayout.Label(entry.Source.ToString(), EditorStyles.miniLabel, GUILayout.Width(70));
|
||||
GUILayout.Label(entry.Path, EditorStyles.miniLabel, GUILayout.ExpandWidth(true));
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndScrollView();
|
||||
|
||||
// Select all / none
|
||||
GUILayout.Space(4);
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
if (GUILayout.Button("All", GUILayout.Width(60))) _entries.ForEach(e => e.ShouldProcess = true);
|
||||
if (GUILayout.Button("None", GUILayout.Width(60))) _entries.ForEach(e => e.ShouldProcess = false);
|
||||
}
|
||||
|
||||
GUILayout.Space(8);
|
||||
|
||||
// Actions
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
if (GUILayout.Button("Skip all", GUILayout.Width(80)))
|
||||
{ Close(); return; }
|
||||
|
||||
int count = _entries.FindAll(e => e.ShouldProcess).Count;
|
||||
using (new EditorGUI.DisabledScope(count == 0))
|
||||
{
|
||||
if (GUILayout.Button($"Process {count} FBX", GUILayout.Width(130)))
|
||||
{ ProcessSelected(); Close(); }
|
||||
}
|
||||
}
|
||||
GUILayout.Space(8);
|
||||
}
|
||||
|
||||
void ProcessSelected()
|
||||
{
|
||||
foreach (var entry in _entries)
|
||||
{
|
||||
if (!entry.ShouldProcess) continue;
|
||||
FBXImportDialog.Show(entry.Path, entry.Source);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using UnityEditor;
|
||||
using System.Linq;
|
||||
|
||||
namespace FBXImporter
|
||||
{
|
||||
public static class FBXContextMenu
|
||||
{
|
||||
[MenuItem("Assets/FBX Importer/Process this FBX", false, 1000)]
|
||||
static void ProcessSingle()
|
||||
{
|
||||
foreach (var obj in Selection.objects)
|
||||
{
|
||||
string path = AssetDatabase.GetAssetPath(obj);
|
||||
if (!path.EndsWith(".fbx", System.StringComparison.OrdinalIgnoreCase)) continue;
|
||||
var source = FBXSourceDetector.Detect(path);
|
||||
FBXImportDialog.Show(path, source);
|
||||
}
|
||||
}
|
||||
|
||||
[MenuItem("Assets/FBX Importer/Process this FBX", true)]
|
||||
static bool ProcessSingleValidate() =>
|
||||
Selection.objects.Any(o => AssetDatabase.GetAssetPath(o)
|
||||
.EndsWith(".fbx", System.StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
[MenuItem("Assets/FBX Importer/Remove from registry", false, 1001)]
|
||||
static void RemoveFromRegistry()
|
||||
{
|
||||
foreach (var obj in Selection.objects)
|
||||
{
|
||||
string path = AssetDatabase.GetAssetPath(obj);
|
||||
if (path.EndsWith(".fbx", System.StringComparison.OrdinalIgnoreCase))
|
||||
ProcessedFBXRegistry.Unregister(path);
|
||||
}
|
||||
}
|
||||
|
||||
[MenuItem("Assets/FBX Importer/Remove from registry", true)]
|
||||
static bool RemoveFromRegistryValidate() =>
|
||||
Selection.objects.Any(o => AssetDatabase.GetAssetPath(o)
|
||||
.EndsWith(".fbx", System.StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
[MenuItem("Assets/FBX Importer/Create Asset Folder Structure", false, 1020)]
|
||||
static void CreateFolderStructure()
|
||||
{
|
||||
string selectedPath = GetSelectedFolderPath();
|
||||
if (string.IsNullOrEmpty(selectedPath)) return;
|
||||
|
||||
string parent = System.IO.Path.GetDirectoryName(selectedPath).Replace("\\", "/");
|
||||
FolderSetup.Create(parent, System.IO.Path.GetFileName(selectedPath), false);
|
||||
}
|
||||
|
||||
[MenuItem("Assets/FBX Importer/Create Asset Folder Structure", true)]
|
||||
static bool CreateFolderStructureValidate() => !string.IsNullOrEmpty(GetSelectedFolderPath());
|
||||
|
||||
static string GetSelectedFolderPath()
|
||||
{
|
||||
if (Selection.activeObject == null) return null;
|
||||
string path = AssetDatabase.GetAssetPath(Selection.activeObject);
|
||||
return AssetDatabase.IsValidFolder(path) ? path : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-32
@@ -3,29 +3,11 @@ using System.Collections.Generic;
|
||||
|
||||
namespace FBXImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Watches for FBX files imported into Assets/.
|
||||
/// Collects all NEW files across the entire import batch, then shows
|
||||
/// a single dialog listing them all — user decides which ones to process.
|
||||
///
|
||||
/// "New" means: not in ProcessedFBXRegistry (GUID-based, persistent).
|
||||
/// All files shown in the dialog are registered immediately so they
|
||||
/// never appear again, regardless of whether the user chose to process them.
|
||||
///
|
||||
/// SAFETY:
|
||||
/// - Hard blocked on anything outside Assets/
|
||||
/// - No asset DB writes during import hooks
|
||||
/// - Dialog + processing deferred via EditorApplication.delayCall
|
||||
/// </summary>
|
||||
public class FBXDropWatcher : AssetPostprocessor
|
||||
{
|
||||
// Accumulates new FBX paths across the current import batch.
|
||||
// Static so all postprocessor instances share the same list.
|
||||
static readonly List<string> _pendingNewFBX = new List<string>();
|
||||
static bool _dialogScheduled = false;
|
||||
|
||||
// ── Guard ─────────────────────────────────────────────────────────────────
|
||||
|
||||
bool ShouldWatch()
|
||||
{
|
||||
return assetPath.StartsWith("Assets/")
|
||||
@@ -33,27 +15,21 @@ namespace FBXImporter
|
||||
&& !ProcessedFBXRegistry.IsRegistered(assetPath);
|
||||
}
|
||||
|
||||
// ── Import hooks ──────────────────────────────────────────────────────────
|
||||
|
||||
void OnPreprocessModel()
|
||||
{
|
||||
if (!ShouldWatch()) return;
|
||||
|
||||
// Configure ModelImporter settings only — no asset DB writes here
|
||||
var importer = assetImporter as ModelImporter;
|
||||
|
||||
importer.importCameras = false;
|
||||
importer.importLights = false;
|
||||
importer.importNormals = ModelImporterNormals.Import;
|
||||
importer.importTangents = ModelImporterTangents.CalculateMikk;
|
||||
importer.importBlendShapes = true;
|
||||
importer.importVisibility = true;
|
||||
|
||||
importer.materialImportMode = ModelImporterMaterialImportMode.ImportViaMaterialDescription;
|
||||
importer.materialLocation = ModelImporterMaterialLocation.External;
|
||||
importer.materialName = ModelImporterMaterialName.BasedOnMaterialName;
|
||||
importer.materialSearch = ModelImporterMaterialSearch.RecursiveUp;
|
||||
|
||||
importer.useFileScale = true;
|
||||
importer.globalScale = 1f;
|
||||
}
|
||||
@@ -62,11 +38,9 @@ namespace FBXImporter
|
||||
{
|
||||
if (!ShouldWatch()) return;
|
||||
|
||||
// Add to the pending batch
|
||||
if (!_pendingNewFBX.Contains(assetPath))
|
||||
_pendingNewFBX.Add(assetPath);
|
||||
|
||||
// Schedule ONE dialog for the whole batch (deduped with the flag)
|
||||
if (!_dialogScheduled)
|
||||
{
|
||||
_dialogScheduled = true;
|
||||
@@ -74,22 +48,17 @@ namespace FBXImporter
|
||||
}
|
||||
}
|
||||
|
||||
// ── Batch dialog ──────────────────────────────────────────────────────────
|
||||
|
||||
static void ShowBatchDialog()
|
||||
{
|
||||
_dialogScheduled = false;
|
||||
|
||||
if (_pendingNewFBX.Count == 0) return;
|
||||
|
||||
// Snapshot and clear before opening the dialog
|
||||
var batch = new List<string>(_pendingNewFBX);
|
||||
_pendingNewFBX.Clear();
|
||||
|
||||
// Register ALL files immediately — they won't appear again
|
||||
// Register all immediately — won't appear again regardless of user choice
|
||||
ProcessedFBXRegistry.Register(batch);
|
||||
|
||||
// Open the batch dialog
|
||||
FBXBatchDialog.Show(batch);
|
||||
}
|
||||
}
|
||||
+45
-17
@@ -5,8 +5,7 @@ using System.IO;
|
||||
namespace FBXImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Per-asset config dialog opened after the batch dialog for each selected FBX.
|
||||
/// User sets the asset name and destination folder, then confirms.
|
||||
/// Per-asset config dialog — name, destination, rig type.
|
||||
/// </summary>
|
||||
public class FBXImportDialog : EditorWindow
|
||||
{
|
||||
@@ -17,11 +16,24 @@ namespace FBXImporter
|
||||
private bool _createAnims = false;
|
||||
private string _errorMessage = "";
|
||||
|
||||
// Rig settings
|
||||
private ModelImporterAnimationType _animType = ModelImporterAnimationType.Generic;
|
||||
private string _rootNode = "";
|
||||
|
||||
static readonly string[] AnimTypeLabels = { "None", "Legacy", "Generic", "Humanoid" };
|
||||
static readonly ModelImporterAnimationType[] AnimTypeValues =
|
||||
{
|
||||
ModelImporterAnimationType.None,
|
||||
ModelImporterAnimationType.Legacy,
|
||||
ModelImporterAnimationType.Generic,
|
||||
ModelImporterAnimationType.Human,
|
||||
};
|
||||
|
||||
public static void Show(string fbxAssetPath, FBXSource source)
|
||||
{
|
||||
var window = GetWindow<FBXImportDialog>(true, $"Configure — {Path.GetFileName(fbxAssetPath)}", true);
|
||||
window.minSize = new Vector2(440, 260);
|
||||
window.maxSize = new Vector2(440, 260);
|
||||
window.minSize = new Vector2(460, 360);
|
||||
window.maxSize = new Vector2(460, 360);
|
||||
window._fbxSourcePath = fbxAssetPath;
|
||||
window._detectedSource = source;
|
||||
window._assetName = Path.GetFileNameWithoutExtension(fbxAssetPath);
|
||||
@@ -39,10 +51,12 @@ namespace FBXImporter
|
||||
DrawRow("Source", _detectedSource.ToString());
|
||||
GUILayout.Space(10);
|
||||
|
||||
// Asset name
|
||||
GUILayout.Label("Asset Name", EditorStyles.miniBoldLabel);
|
||||
_assetName = EditorGUILayout.TextField(_assetName);
|
||||
GUILayout.Space(6);
|
||||
|
||||
// Destination
|
||||
GUILayout.Label("Destination Folder", EditorStyles.miniBoldLabel);
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
@@ -59,10 +73,30 @@ namespace FBXImporter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.LabelField($"→ {_destinationPath}/{_assetName}", EditorStyles.miniLabel);
|
||||
GUILayout.Space(6);
|
||||
GUILayout.Space(8);
|
||||
|
||||
// Rig / animation type
|
||||
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 selected = GUILayout.SelectionGrid(current, AnimTypeLabels, 4);
|
||||
_animType = AnimTypeValues[selected];
|
||||
}
|
||||
|
||||
if (_animType != ModelImporterAnimationType.None)
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
GUILayout.Label("Root Node (opt.)", GUILayout.Width(120));
|
||||
_rootNode = EditorGUILayout.TextField(_rootNode);
|
||||
}
|
||||
_createAnims = true; // auto-enable Animations/ folder if rig is set
|
||||
}
|
||||
|
||||
GUILayout.Space(6);
|
||||
_createAnims = EditorGUILayout.ToggleLeft("Create Animations/ folder", _createAnims);
|
||||
GUILayout.Space(8);
|
||||
|
||||
@@ -72,27 +106,21 @@ namespace FBXImporter
|
||||
GUILayout.Space(4);
|
||||
}
|
||||
|
||||
// Buttons
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
if (GUILayout.Button("Skip", GUILayout.Width(70)))
|
||||
{
|
||||
Debug.Log($"[FBXImporter] Skipped config for: {_fbxSourcePath}");
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
{ Close(); return; }
|
||||
|
||||
bool valid = !string.IsNullOrWhiteSpace(_assetName)
|
||||
&& AssetDatabase.IsValidFolder(_destinationPath);
|
||||
&& (!string.IsNullOrWhiteSpace(_destinationPath));
|
||||
|
||||
using (new EditorGUI.DisabledScope(!valid))
|
||||
{
|
||||
if (GUILayout.Button("Create & Import", GUILayout.Width(120)))
|
||||
{
|
||||
Confirm();
|
||||
Close();
|
||||
}
|
||||
{ Confirm(); Close(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,7 +140,7 @@ namespace FBXImporter
|
||||
}
|
||||
}
|
||||
|
||||
FBXProcessor.Process(targetFBX);
|
||||
FBXProcessor.Process(targetFBX, _animType, _rootNode);
|
||||
|
||||
var rootObj = AssetDatabase.LoadAssetAtPath<Object>(folders.Root);
|
||||
if (rootObj != null) EditorGUIUtility.PingObject(rootObj);
|
||||
+30
-33
@@ -4,10 +4,6 @@ using System.IO;
|
||||
|
||||
namespace FBXImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Editor utility window — Window > FBX Importer > Inspector
|
||||
/// Inspect any FBX and trigger processing manually.
|
||||
/// </summary>
|
||||
public class FBXImporterWindow : EditorWindow
|
||||
{
|
||||
private Object _selectedFBX;
|
||||
@@ -18,16 +14,15 @@ namespace FBXImporter
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
GUILayout.Label("FBX Importer — Manual Pipeline", EditorStyles.boldLabel);
|
||||
GUILayout.Label("FBX Importer", EditorStyles.boldLabel);
|
||||
EditorGUILayout.HelpBox(
|
||||
"Drop a FBX here or select one in the Project panel.\n" +
|
||||
"Nothing runs automatically — you trigger processing explicitly.",
|
||||
"Select a .fbx in the Project panel or drag it here.\n" +
|
||||
"Processing is always manual — nothing runs automatically.",
|
||||
MessageType.Info);
|
||||
EditorGUILayout.Space(6);
|
||||
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);
|
||||
@@ -40,7 +35,7 @@ namespace FBXImporter
|
||||
string path = AssetDatabase.GetAssetPath(_selectedFBX);
|
||||
if (!path.EndsWith(".fbx", System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
EditorGUILayout.HelpBox("Selected asset is not a .fbx file.", MessageType.Warning);
|
||||
EditorGUILayout.HelpBox("Not a .fbx file.", MessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -51,36 +46,35 @@ namespace FBXImporter
|
||||
{
|
||||
EditorGUILayout.Space(8);
|
||||
|
||||
var source = FBXSourceDetector.Detect(assetPath);
|
||||
var folders = FolderSetup.Resolve(assetPath);
|
||||
string clean = TextureMatcher.CleanMaterialName(
|
||||
Path.GetFileNameWithoutExtension(assetPath), source);
|
||||
var source = FBXSourceDetector.Detect(assetPath);
|
||||
var folders = FolderSetup.Resolve(assetPath);
|
||||
bool registered = ProcessedFBXRegistry.IsRegistered(assetPath);
|
||||
string clean = TextureMatcher.CleanMaterialName(
|
||||
Path.GetFileNameWithoutExtension(assetPath), source);
|
||||
|
||||
// ── Info ─────────────────────────────────────────────────────────────
|
||||
// Info
|
||||
GUILayout.Label("Detection", EditorStyles.boldLabel);
|
||||
DrawRow("Source", source.ToString(),
|
||||
DrawRow("Source", source.ToString(),
|
||||
source != FBXSource.Unknown ? Color.green : Color.yellow);
|
||||
DrawRow("Textures/", folders.Textures,
|
||||
DrawRow("Registered", registered ? "Yes — won't appear in batch dialog" : "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,
|
||||
DrawRow("Materials/", folders.Materials,
|
||||
AssetDatabase.IsValidFolder(folders.Materials) ? Color.green : Color.yellow);
|
||||
DrawRow("Prefabs/", folders.Prefabs,
|
||||
AssetDatabase.IsValidFolder(folders.Prefabs) ? Color.green : Color.yellow);
|
||||
|
||||
// ── Texture preview ───────────────────────────────────────────────────
|
||||
// Texture preview
|
||||
EditorGUILayout.Space(8);
|
||||
GUILayout.Label("Texture Slot Preview", EditorStyles.boldLabel);
|
||||
|
||||
_scroll = EditorGUILayout.BeginScrollView(_scroll, GUILayout.Height(150));
|
||||
foreach (var kvp in TextureMatcher.SlotSuffixes)
|
||||
foreach (var kvp in TextureMatcher.SlotKeywords)
|
||||
{
|
||||
var tex = TextureMatcher.FindForSlot(folders.Textures, clean, kvp.Key);
|
||||
bool found = tex != null;
|
||||
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
var style = new GUIStyle(EditorStyles.label)
|
||||
{ normal = { textColor = found ? new Color(0.4f, 1f, 0.4f) : new Color(0.6f, 0.6f, 0.6f) } };
|
||||
{ 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(found ? tex.name : "–", EditorStyles.miniLabel);
|
||||
@@ -88,23 +82,26 @@ namespace FBXImporter
|
||||
}
|
||||
EditorGUILayout.EndScrollView();
|
||||
|
||||
// ── Actions ───────────────────────────────────────────────────────────
|
||||
// Actions
|
||||
EditorGUILayout.Space(8);
|
||||
GUILayout.Label("Actions", EditorStyles.boldLabel);
|
||||
|
||||
var bigButton = GUILayout.Height(30);
|
||||
if (GUILayout.Button("▶ Process this FBX (full pipeline)", GUILayout.Height(28)))
|
||||
{
|
||||
var src = FBXSourceDetector.Detect(assetPath);
|
||||
FBXImportDialog.Show(assetPath, src);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("▶ Process this FBX (full pipeline)", bigButton))
|
||||
FBXProcessor.Process(assetPath);
|
||||
EditorGUILayout.Space(2);
|
||||
|
||||
EditorGUILayout.Space(4);
|
||||
if (GUILayout.Button("🗑 Remove from registry (will reappear on next drop)"))
|
||||
ProcessedFBXRegistry.Unregister(assetPath);
|
||||
|
||||
if (GUILayout.Button("📁 Create folder structure only"))
|
||||
if (GUILayout.Button("📁 Create missing folders"))
|
||||
{
|
||||
FolderSetup.Create(
|
||||
Path.GetDirectoryName(folders.Root),
|
||||
Path.GetFileName(folders.Root),
|
||||
false);
|
||||
Path.GetFileName(folders.Root), false);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("🔍 Ping in Project panel"))
|
||||
@@ -1,32 +1,33 @@
|
||||
# FBX Importer v5
|
||||
# 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
|
||||
|
||||
1. Tu glisses un dossier avec FBX dans Unity
|
||||
2. Une seule pop up liste TOUS les nouveaux FBX
|
||||
3. Tu coches ceux que tu veux traiter
|
||||
4. Pour chacun → dialog de config (nom + destination)
|
||||
5. 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")
|
||||
|
||||
## "Nouveau" = jamais vu par le plugin
|
||||
Les GUIDs sont stockés dans processed_fbx.txt.
|
||||
Un FBX vu une fois (même si skip) ne repasse plus jamais.
|
||||
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
|
||||
|
||||
## Reset un FBX
|
||||
Clic droit → FBX Importer → Reset (remove from registry)
|
||||
Ou supprime son GUID dans processed_fbx.txt
|
||||
|
||||
## Process manuel
|
||||
Clic droit sur un .fbx → FBX Importer → Process this FBX
|
||||
|
||||
## Structure attendue
|
||||
AssetName/
|
||||
├── Sources/ ← FBX
|
||||
├── Textures/ ← PNG/TGA
|
||||
├── Materials/ ← auto
|
||||
├── Prefabs/ ← auto
|
||||
├── Audio/ ← auto vide
|
||||
└── VFX/ ← auto vide
|
||||
|
||||
## Texture naming
|
||||
_D / _N / _R / _M / _AO / _E / _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
|
||||
|
||||
Reference in New Issue
Block a user