+ Add : Plugin fbx integration
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 51d3ce25787badd429148924ba0fdaca
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b7a761cf384e2d143b6e8b9fafd6281e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6712b96ee1ccf81428cd47e78893587a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace FBXImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds all resolved folder paths for a single imported asset.
|
||||
/// </summary>
|
||||
public class AssetFolders
|
||||
{
|
||||
public string Root { get; set; }
|
||||
public string Sources { get; set; }
|
||||
public string Textures { get; set; }
|
||||
public string Materials { get; set; }
|
||||
public string Prefabs { get; set; }
|
||||
public string Animations { get; set; }
|
||||
public string Audio { get; set; }
|
||||
public string VFX { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d94b3203f22e53f4ebcd1f0817c5987a
|
||||
@@ -0,0 +1,179 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
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)
|
||||
/// </summary>
|
||||
public static void Process(string fbxAssetPath)
|
||||
{
|
||||
if (!fbxAssetPath.EndsWith(".fbx", System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Debug.LogWarning($"[FBXImporter] Not a FBX file: {fbxAssetPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fbxAssetPath.StartsWith("Assets/"))
|
||||
{
|
||||
Debug.LogWarning($"[FBXImporter] File is outside Assets/ — skipped: {fbxAssetPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
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 folders = FolderSetup.Resolve(fbxAssetPath);
|
||||
FolderSetup.Create(
|
||||
Path.GetDirectoryName(folders.Root),
|
||||
Path.GetFileName(folders.Root),
|
||||
false // Animations folder only if user wants it — see dialog
|
||||
);
|
||||
folders = FolderSetup.Resolve(fbxAssetPath); // re-resolve after creation
|
||||
|
||||
// 3. Configure ModelImporter
|
||||
var importer = AssetImporter.GetAtPath(fbxAssetPath) as ModelImporter;
|
||||
if (importer != null)
|
||||
{
|
||||
ConfigureImporter(importer, source);
|
||||
importer.SaveAndReimport(); // force reimport with new settings
|
||||
}
|
||||
|
||||
// 4. Load the imported model
|
||||
var fbxAsset = AssetDatabase.LoadAssetAtPath<GameObject>(fbxAssetPath);
|
||||
if (fbxAsset == null)
|
||||
{
|
||||
Debug.LogError($"[FBXImporter] Could not load FBX as GameObject: {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} ──────────────────────────────────────");
|
||||
}
|
||||
|
||||
// ─── ModelImporter settings ───────────────────────────────────────────────
|
||||
|
||||
static void ConfigureImporter(ModelImporter importer, FBXSource source)
|
||||
{
|
||||
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;
|
||||
|
||||
// Both Blender and Maya: let FBX unit metadata handle scale
|
||||
importer.useFileScale = true;
|
||||
importer.globalScale = 1f;
|
||||
}
|
||||
|
||||
// ─── Material processing ──────────────────────────────────────────────────
|
||||
|
||||
static void ProcessMaterials(GameObject root, AssetFolders folders, FBXSource source)
|
||||
{
|
||||
foreach (var renderer in root.GetComponentsInChildren<Renderer>(true))
|
||||
{
|
||||
var mats = renderer.sharedMaterials;
|
||||
bool changed = false;
|
||||
|
||||
for (int i = 0; i < mats.Length; i++)
|
||||
{
|
||||
var processed = ProcessMaterial(mats[i], folders, source);
|
||||
if (processed != null && processed != mats[i])
|
||||
{
|
||||
mats[i] = processed;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) renderer.sharedMaterials = mats;
|
||||
}
|
||||
}
|
||||
|
||||
static Material ProcessMaterial(Material mat, AssetFolders folders, FBXSource source)
|
||||
{
|
||||
if (mat == null) return null;
|
||||
|
||||
string cleanName = TextureMatcher.CleanMaterialName(mat.name, source);
|
||||
string matPath = $"{folders.Materials}/{cleanName}.mat";
|
||||
|
||||
Material existing = AssetDatabase.LoadAssetAtPath<Material>(matPath);
|
||||
Material target = existing ?? new Material(ShaderDetector.GetLitShader());
|
||||
|
||||
ConnectPBRTextures(target, folders.Textures, cleanName);
|
||||
|
||||
if (existing == null)
|
||||
{
|
||||
AssetDatabase.CreateAsset(target, matPath);
|
||||
Debug.Log($"[FBXImporter] Material created → {matPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorUtility.SetDirty(target);
|
||||
Debug.Log($"[FBXImporter] Material updated → {matPath}");
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
static void ConnectPBRTextures(Material mat, string textureDir, string baseName)
|
||||
{
|
||||
foreach (var slot in TextureMatcher.SlotSuffixes.Keys)
|
||||
{
|
||||
var tex = TextureMatcher.FindForSlot(textureDir, baseName, slot);
|
||||
if (tex == null) continue;
|
||||
|
||||
string pipelineSlot = ShaderDetector.GetSlotName(slot);
|
||||
mat.SetTexture(pipelineSlot, tex);
|
||||
|
||||
switch (slot)
|
||||
{
|
||||
case "_BumpMap":
|
||||
mat.EnableKeyword("_NORMALMAP");
|
||||
break;
|
||||
case "_EmissionMap":
|
||||
mat.EnableKeyword("_EMISSION");
|
||||
mat.SetColor("_EmissionColor", Color.white);
|
||||
mat.globalIlluminationFlags = MaterialGlobalIlluminationFlags.BakedEmissive;
|
||||
break;
|
||||
case "_ParallaxMap":
|
||||
mat.EnableKeyword("_PARALLAXMAP");
|
||||
break;
|
||||
}
|
||||
|
||||
Debug.Log($"[FBXImporter] {slot} → {tex.name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eea47f2dacccac441aa4a6806412a43a
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace FBXImporter
|
||||
{
|
||||
public enum FBXSource { Unknown, Blender, Maya }
|
||||
|
||||
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;
|
||||
if (assetPath.Contains("/MayaAssets/")) return FBXSource.Maya;
|
||||
|
||||
string fullPath = Path.GetFullPath(assetPath);
|
||||
if (!File.Exists(fullPath)) return FBXSource.Unknown;
|
||||
|
||||
try
|
||||
{
|
||||
byte[] buffer = new byte[2000];
|
||||
using (var fs = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
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;
|
||||
if (header.Contains("Autodesk")) return FBXSource.Maya;
|
||||
}
|
||||
catch { }
|
||||
|
||||
return FBXSource.Unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 330be20566a6f0249bea3963ca032e4f
|
||||
@@ -0,0 +1,65 @@
|
||||
using UnityEditor;
|
||||
using System.IO;
|
||||
|
||||
namespace FBXImporter
|
||||
{
|
||||
public static class FolderSetup
|
||||
{
|
||||
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");
|
||||
|
||||
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("\\", "/");
|
||||
string root = dir.EndsWith("/Sources")
|
||||
? Path.GetDirectoryName(dir).Replace("\\", "/")
|
||||
: dir;
|
||||
|
||||
string animPath = $"{root}/Animations";
|
||||
return BuildFolders(root, AssetDatabase.IsValidFolder(animPath) ? animPath : null);
|
||||
}
|
||||
|
||||
static AssetFolders BuildFolders(string root, string animPath) => new AssetFolders
|
||||
{
|
||||
Root = root,
|
||||
Sources = $"{root}/Sources",
|
||||
Textures = $"{root}/Textures",
|
||||
Materials = $"{root}/Materials",
|
||||
Prefabs = $"{root}/Prefabs",
|
||||
Animations = animPath,
|
||||
Audio = $"{root}/Audio",
|
||||
VFX = $"{root}/VFX",
|
||||
};
|
||||
|
||||
static void EnsureFolder(string parent, string child)
|
||||
{
|
||||
string path = $"{parent}/{child}";
|
||||
if (!AssetDatabase.IsValidFolder(path))
|
||||
AssetDatabase.CreateFolder(parent, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b2256f5b9c45ebc48bbfc702f7b7177e
|
||||
@@ -0,0 +1,79 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using System.Collections.Generic;
|
||||
|
||||
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;
|
||||
|
||||
if (!AssetDatabase.IsValidFolder(prefabDir))
|
||||
{
|
||||
string parent = System.IO.Path.GetDirectoryName(prefabDir).Replace("\\", "/");
|
||||
AssetDatabase.CreateFolder(parent, System.IO.Path.GetFileName(prefabDir));
|
||||
}
|
||||
|
||||
var instance = Object.Instantiate(fbxRoot);
|
||||
instance.name = assetName;
|
||||
|
||||
if (TrySetupLODGroup(instance))
|
||||
Debug.Log($"[FBXImporter] LODGroup configured for '{assetName}'");
|
||||
|
||||
string prefabPath = $"{prefabDir}/{assetName}.prefab";
|
||||
var prefab = PrefabUtility.SaveAsPrefabAsset(instance, prefabPath);
|
||||
Object.DestroyImmediate(instance);
|
||||
|
||||
if (prefab != null) Debug.Log($"[FBXImporter] Prefab → {prefabPath}");
|
||||
else Debug.LogError($"[FBXImporter] Failed to save prefab at {prefabPath}");
|
||||
|
||||
return prefab;
|
||||
}
|
||||
|
||||
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());
|
||||
if (idx < 0) continue;
|
||||
if (!lodMap.ContainsKey(idx)) lodMap[idx] = new List<Renderer>();
|
||||
lodMap[idx].Add(r);
|
||||
}
|
||||
|
||||
if (lodMap.Count < 2) return false;
|
||||
|
||||
var existing = root.GetComponent<LODGroup>();
|
||||
if (existing != null) Object.DestroyImmediate(existing);
|
||||
|
||||
var group = root.AddComponent<LODGroup>();
|
||||
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;
|
||||
}
|
||||
|
||||
static int ExtractLODIndex(string name)
|
||||
{
|
||||
int idx = name.LastIndexOf("_LOD");
|
||||
if (idx < 0) return -1;
|
||||
return int.TryParse(name.Substring(idx + 4), out int level) ? level : -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c3b17b0a218bf80458531ba1c789cfed
|
||||
@@ -0,0 +1,76 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85c39d9a08dc3d04a974255db5d713ed
|
||||
@@ -0,0 +1,48 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace FBXImporter
|
||||
{
|
||||
public enum RenderPipeline { BuiltIn, URP, HDRP }
|
||||
|
||||
public static class ShaderDetector
|
||||
{
|
||||
static RenderPipeline? _cached;
|
||||
|
||||
public static RenderPipeline GetPipeline()
|
||||
{
|
||||
if (_cached.HasValue) return _cached.Value;
|
||||
var rp = GraphicsSettings.currentRenderPipeline;
|
||||
if (rp == null) _cached = RenderPipeline.BuiltIn;
|
||||
else if (rp.GetType().Name.Contains("Universal")) _cached = RenderPipeline.URP;
|
||||
else if (rp.GetType().Name.Contains("HD")) _cached = RenderPipeline.HDRP;
|
||||
else _cached = RenderPipeline.BuiltIn;
|
||||
return _cached.Value;
|
||||
}
|
||||
|
||||
public static Shader GetLitShader()
|
||||
{
|
||||
switch (GetPipeline())
|
||||
{
|
||||
case RenderPipeline.URP: return Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard");
|
||||
case RenderPipeline.HDRP: return Shader.Find("HDRP/Lit") ?? Shader.Find("Standard");
|
||||
default: return Shader.Find("Standard");
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetSlotName(string genericSlot)
|
||||
{
|
||||
if (GetPipeline() != RenderPipeline.HDRP) return genericSlot;
|
||||
switch (genericSlot)
|
||||
{
|
||||
case "_BaseMap": return "_BaseColorMap";
|
||||
case "_BumpMap": return "_NormalMap";
|
||||
case "_MetallicGlossMap": return "_MaskMap";
|
||||
case "_SpecGlossMap": return "_MaskMap";
|
||||
case "_OcclusionMap": return "_MaskMap";
|
||||
case "_EmissionMap": return "_EmissiveColorMap";
|
||||
default: return genericSlot;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dc40db3111674c04f94d8fdb63aed0ab
|
||||
@@ -0,0 +1,95 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 675fd374b2a03994c8ac820bee5fafa2
|
||||
@@ -0,0 +1,139 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96093adb2a120a6468377c6dc73e1607
|
||||
@@ -0,0 +1,118 @@
|
||||
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,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35883f87017729a4da4aa2723ca60654
|
||||
@@ -0,0 +1,96 @@
|
||||
using UnityEditor;
|
||||
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/")
|
||||
&& assetPath.EndsWith(".fbx", System.StringComparison.OrdinalIgnoreCase)
|
||||
&& !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;
|
||||
}
|
||||
|
||||
void OnPostprocessModel(UnityEngine.GameObject root)
|
||||
{
|
||||
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;
|
||||
EditorApplication.delayCall += ShowBatchDialog;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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
|
||||
ProcessedFBXRegistry.Register(batch);
|
||||
|
||||
// Open the batch dialog
|
||||
FBXBatchDialog.Show(batch);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b0ef00f525fdd4498665e9164696f58
|
||||
@@ -0,0 +1,130 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
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.
|
||||
/// </summary>
|
||||
public class FBXImportDialog : EditorWindow
|
||||
{
|
||||
private string _fbxSourcePath;
|
||||
private FBXSource _detectedSource;
|
||||
private string _assetName = "";
|
||||
private string _destinationPath = "Assets";
|
||||
private bool _createAnims = false;
|
||||
private string _errorMessage = "";
|
||||
|
||||
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._fbxSourcePath = fbxAssetPath;
|
||||
window._detectedSource = source;
|
||||
window._assetName = Path.GetFileNameWithoutExtension(fbxAssetPath);
|
||||
window._destinationPath = Path.GetDirectoryName(fbxAssetPath).Replace("\\", "/");
|
||||
window.ShowModal();
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
GUILayout.Space(10);
|
||||
GUILayout.Label("Configure asset", EditorStyles.boldLabel);
|
||||
GUILayout.Space(6);
|
||||
|
||||
DrawRow("File", Path.GetFileName(_fbxSourcePath));
|
||||
DrawRow("Source", _detectedSource.ToString());
|
||||
GUILayout.Space(10);
|
||||
|
||||
GUILayout.Label("Asset Name", EditorStyles.miniBoldLabel);
|
||||
_assetName = EditorGUILayout.TextField(_assetName);
|
||||
GUILayout.Space(6);
|
||||
|
||||
GUILayout.Label("Destination Folder", EditorStyles.miniBoldLabel);
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
EditorGUILayout.LabelField(_destinationPath, EditorStyles.helpBox);
|
||||
if (GUILayout.Button("Browse…", GUILayout.Width(70)))
|
||||
{
|
||||
string chosen = EditorUtility.OpenFolderPanel("Select destination", Application.dataPath, "");
|
||||
if (!string.IsNullOrEmpty(chosen))
|
||||
{
|
||||
if (chosen.StartsWith(Application.dataPath))
|
||||
_destinationPath = "Assets" + chosen.Substring(Application.dataPath.Length);
|
||||
else
|
||||
_errorMessage = "Please select a folder inside Assets/";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.LabelField($"→ {_destinationPath}/{_assetName}", EditorStyles.miniLabel);
|
||||
GUILayout.Space(6);
|
||||
|
||||
_createAnims = EditorGUILayout.ToggleLeft("Create Animations/ folder", _createAnims);
|
||||
GUILayout.Space(8);
|
||||
|
||||
if (!string.IsNullOrEmpty(_errorMessage))
|
||||
{
|
||||
EditorGUILayout.HelpBox(_errorMessage, MessageType.Error);
|
||||
GUILayout.Space(4);
|
||||
}
|
||||
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
if (GUILayout.Button("Skip", GUILayout.Width(70)))
|
||||
{
|
||||
Debug.Log($"[FBXImporter] Skipped config for: {_fbxSourcePath}");
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
bool valid = !string.IsNullOrWhiteSpace(_assetName)
|
||||
&& AssetDatabase.IsValidFolder(_destinationPath);
|
||||
|
||||
using (new EditorGUI.DisabledScope(!valid))
|
||||
{
|
||||
if (GUILayout.Button("Create & Import", GUILayout.Width(120)))
|
||||
{
|
||||
Confirm();
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Confirm()
|
||||
{
|
||||
var folders = FolderSetup.Create(_destinationPath, _assetName, _createAnims);
|
||||
|
||||
string targetFBX = $"{folders.Sources}/{Path.GetFileName(_fbxSourcePath)}";
|
||||
if (_fbxSourcePath != targetFBX)
|
||||
{
|
||||
string moveError = AssetDatabase.MoveAsset(_fbxSourcePath, targetFBX);
|
||||
if (!string.IsNullOrEmpty(moveError))
|
||||
{
|
||||
Debug.LogError($"[FBXImporter] Could not move FBX: {moveError}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
FBXProcessor.Process(targetFBX);
|
||||
|
||||
var rootObj = AssetDatabase.LoadAssetAtPath<Object>(folders.Root);
|
||||
if (rootObj != null) EditorGUIUtility.PingObject(rootObj);
|
||||
}
|
||||
|
||||
static void DrawRow(string label, string value)
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
EditorGUILayout.LabelField(label, GUILayout.Width(60));
|
||||
EditorGUILayout.LabelField(value, EditorStyles.miniLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0f982faee0b3f5142a962f73ba411924
|
||||
@@ -0,0 +1,124 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
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;
|
||||
private Vector2 _scroll;
|
||||
|
||||
[MenuItem("Window/FBX Importer/Inspector")]
|
||||
public static void Open() => GetWindow<FBXImporterWindow>("FBX Importer").Show();
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
GUILayout.Label("FBX Importer — Manual Pipeline", EditorStyles.boldLabel);
|
||||
EditorGUILayout.HelpBox(
|
||||
"Drop a FBX here or select one in the Project panel.\n" +
|
||||
"Nothing runs automatically — you trigger processing explicitly.",
|
||||
MessageType.Info);
|
||||
EditorGUILayout.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);
|
||||
if (sel.EndsWith(".fbx", System.StringComparison.OrdinalIgnoreCase))
|
||||
_selectedFBX = Selection.activeObject;
|
||||
}
|
||||
|
||||
if (_selectedFBX == null) return;
|
||||
|
||||
string path = AssetDatabase.GetAssetPath(_selectedFBX);
|
||||
if (!path.EndsWith(".fbx", System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
EditorGUILayout.HelpBox("Selected asset is not a .fbx file.", MessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
DrawInspector(path);
|
||||
}
|
||||
|
||||
void DrawInspector(string assetPath)
|
||||
{
|
||||
EditorGUILayout.Space(8);
|
||||
|
||||
var source = FBXSourceDetector.Detect(assetPath);
|
||||
var folders = FolderSetup.Resolve(assetPath);
|
||||
string clean = TextureMatcher.CleanMaterialName(
|
||||
Path.GetFileNameWithoutExtension(assetPath), source);
|
||||
|
||||
// ── Info ─────────────────────────────────────────────────────────────
|
||||
GUILayout.Label("Detection", EditorStyles.boldLabel);
|
||||
DrawRow("Source", source.ToString(),
|
||||
source != FBXSource.Unknown ? 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);
|
||||
DrawRow("Prefabs/", folders.Prefabs,
|
||||
AssetDatabase.IsValidFolder(folders.Prefabs) ? Color.green : Color.yellow);
|
||||
|
||||
// ── 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)
|
||||
{
|
||||
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) } };
|
||||
GUILayout.Label(found ? "✓" : "○", style, GUILayout.Width(16));
|
||||
GUILayout.Label(kvp.Key, GUILayout.Width(170));
|
||||
GUILayout.Label(found ? tex.name : "–", EditorStyles.miniLabel);
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndScrollView();
|
||||
|
||||
// ── Actions ───────────────────────────────────────────────────────────
|
||||
EditorGUILayout.Space(8);
|
||||
GUILayout.Label("Actions", EditorStyles.boldLabel);
|
||||
|
||||
var bigButton = GUILayout.Height(30);
|
||||
|
||||
if (GUILayout.Button("▶ Process this FBX (full pipeline)", bigButton))
|
||||
FBXProcessor.Process(assetPath);
|
||||
|
||||
EditorGUILayout.Space(4);
|
||||
|
||||
if (GUILayout.Button("📁 Create folder structure only"))
|
||||
{
|
||||
FolderSetup.Create(
|
||||
Path.GetDirectoryName(folders.Root),
|
||||
Path.GetFileName(folders.Root),
|
||||
false);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("🔍 Ping in Project panel"))
|
||||
EditorGUIUtility.PingObject(_selectedFBX);
|
||||
}
|
||||
|
||||
static void DrawRow(string label, string value, Color valueColor)
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
EditorGUILayout.LabelField(label, GUILayout.Width(90));
|
||||
var style = new GUIStyle(EditorStyles.label) { normal = { textColor = valueColor } };
|
||||
EditorGUILayout.LabelField(value, style);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 896d2ee0a3a30ef42baf3b73fc486171
|
||||
@@ -0,0 +1,32 @@
|
||||
# FBX Importer v5
|
||||
|
||||
## Flow
|
||||
|
||||
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
|
||||
|
||||
## "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.
|
||||
|
||||
## 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
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 59e10e549f96dca46b441aabe7904ec7
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user