diff --git a/Assets/Scripts/Editor/PolyHaven/PolyHavenApi.cs b/Assets/Scripts/Editor/PolyHaven/PolyHavenApi.cs index 697b389..317efc6 100644 --- a/Assets/Scripts/Editor/PolyHaven/PolyHavenApi.cs +++ b/Assets/Scripts/Editor/PolyHaven/PolyHavenApi.cs @@ -10,7 +10,7 @@ namespace Chameleon.Editor.PolyHaven public readonly struct PolyHavenFile { public readonly string Url; - public readonly string Format; // "jpg" / "png" / "exr" + public readonly string Format; // "jpg" / "png" / "exr" / "fbx" public readonly long Size; public PolyHavenFile(string url, string format, long size) @@ -33,29 +33,74 @@ namespace Chameleon.Editor.PolyHaven public string[] Categories = Array.Empty(); } - /// Parsed /files/{slug}: MapType -> resolution("2k") -> best file. - public sealed class PolyHavenFiles + /// + /// One texture set of an asset. Textures have a single unnamed set; models may ship + /// several (e.g. "cart"/"props"/"mugs"), keyed by the prefix of the API map keys. + /// + public sealed class PolyHavenMapSet { + public string Name = ""; // "" for the default/only set + public readonly Dictionary> Maps = new Dictionary>(); - // Every resolution string seen across all maps (e.g. "1k","2k","4k"), sorted low->high. - public List Resolutions = new List(); - public bool Has(PbrMapType type) => Maps.ContainsKey(type); - public bool TryGet(PbrMapType type, string res, out PolyHavenFile file) - { - file = default; - return Maps.TryGetValue(type, out var byRes) && byRes.TryGetValue(res, out file); - } - /// Best available file at res, else next-lower resolution of the same map. public bool TryGetWithFallback(PbrMapType type, string res, out PolyHavenFile file, out string usedRes) { file = default; usedRes = null; - if (!Maps.TryGetValue(type, out var byRes)) + return Maps.TryGetValue(type, out var byRes) + && PolyHavenFiles.PickWithFallback(byRes, res, out file, out usedRes); + } + } + + /// Parsed /files/{slug}: texture sets plus (for models) the FBX per resolution. + public sealed class PolyHavenFiles + { + public readonly List Sets = new List(); + + /// Model geometry: resolution("2k") -> FBX file. Empty for texture assets. + public readonly Dictionary FbxByRes = + new Dictionary(); + + // Every resolution string seen across all maps (e.g. "1k","2k","4k"), sorted low->high. + public List Resolutions = new List(); + + public bool HasFbx => FbxByRes.Count > 0; + + public int MapTypeCount + { + get + { + var types = new HashSet(); + foreach (var set in Sets) + foreach (var t in set.Maps.Keys) + types.Add(t); + return types.Count; + } + } + + /// True if any set carries this map type. + public bool Has(PbrMapType type) + { + foreach (var set in Sets) + if (set.Has(type)) + return true; + return false; + } + + public bool TryGetFbxWithFallback(string res, out PolyHavenFile file, out string usedRes) => + PickWithFallback(FbxByRes, res, out file, out usedRes); + + /// Exact res, else the highest one at-or-below it, else the lowest available. + internal static bool PickWithFallback(Dictionary byRes, string res, + out PolyHavenFile file, out string usedRes) + { + file = default; + usedRes = null; + if (byRes == null || byRes.Count == 0) return false; if (byRes.TryGetValue(res, out file)) { usedRes = res; return true; } @@ -96,16 +141,23 @@ namespace Chameleon.Editor.PolyHaven { private const string ApiBase = "https://api.polyhaven.com"; - // Poly Haven's dynamic file-tree keys per map type. - private static readonly (PbrMapType type, string apiKey)[] ApiKeys = + // Map-key suffixes in the dynamic file tree. Textures use the bare names + // ("Diffuse", "nor_gl"); multi-set models prefix them ("cart_diff", + // "props_roughness"). Longer suffixes first so "roughness" wins over "rough". + private static readonly (string suffix, PbrMapType type)[] SuffixMap = { - (PbrMapType.Diffuse, "Diffuse"), - (PbrMapType.NormalGL, "nor_gl"), - (PbrMapType.Rough, "Rough"), - (PbrMapType.AO, "AO"), - (PbrMapType.Metal, "Metal"), - (PbrMapType.Displacement, "Displacement"), - (PbrMapType.Arm, "arm"), + ("displacement", PbrMapType.Displacement), + ("roughness", PbrMapType.Rough), + ("metallic", PbrMapType.Metal), + ("diffuse", PbrMapType.Diffuse), + ("height", PbrMapType.Displacement), + ("nor_gl", PbrMapType.NormalGL), + ("metal", PbrMapType.Metal), + ("rough", PbrMapType.Rough), + ("diff", PbrMapType.Diffuse), + ("disp", PbrMapType.Displacement), + ("arm", PbrMapType.Arm), + ("ao", PbrMapType.AO), }; // Best format preference per map: lossless where it matters, jpg otherwise. @@ -163,7 +215,7 @@ namespace Chameleon.Editor.PolyHaven }, onError); } - /// Loads the full texture catalog as browser-grid entries. + /// Loads the asset catalog ("textures"/"models") as browser-grid entries. public static void FetchAssets(string type, Action> onDone, Action onError) { EditorWebRequest.GetText($"{ApiBase}/assets?t={type}", text => @@ -217,15 +269,60 @@ namespace Chameleon.Editor.PolyHaven return string.Join(" ", list); } + private static bool MatchMapKey(string key, out string setName, out PbrMapType type) + { + setName = null; + type = default; + string k = key.ToLowerInvariant(); + foreach (var (suffix, t) in SuffixMap) + { + if (k == suffix) + { + setName = ""; + type = t; + return true; + } + if (k.EndsWith("_" + suffix, StringComparison.Ordinal)) + { + setName = key.Substring(0, key.Length - suffix.Length - 1); + type = t; + return true; + } + } + return false; + } + private static PolyHavenFiles ParseFiles(string json) { var root = JObject.Parse(json); var result = new PolyHavenFiles(); var resSet = new HashSet(); + var setsByName = new Dictionary(); - foreach (var (type, apiKey) in ApiKeys) + foreach (var prop in root) { - if (root[apiKey] is not JObject byRes) + // Model geometry: fbx -> res -> "fbx" -> {url,size}. + if (string.Equals(prop.Key, "fbx", StringComparison.OrdinalIgnoreCase)) + { + if (prop.Value is not JObject fbxByRes) continue; + foreach (var resProp in fbxByRes) + { + if (resProp.Value is JObject inner && inner["fbx"] is JObject fo && fo["url"] != null) + { + result.FbxByRes[resProp.Key] = new PolyHavenFile( + (string)fo["url"], "fbx", + fo["size"] != null ? (long)fo["size"] : 0); + resSet.Add(resProp.Key); + } + } + continue; + } + + // Everything else: either a map key (possibly set-prefixed) or an + // unsupported container (blend/gltf/usd/nor_dx/...) we skip. + if (!MatchMapKey(prop.Key, out var setName, out var type)) + continue; + if (prop.Value is not JObject byRes) continue; var meta = PbrMapInfo.Get(type); @@ -258,10 +355,26 @@ namespace Chameleon.Editor.PolyHaven } } - if (perRes.Count > 0) - result.Maps[type] = perRes; + if (perRes.Count == 0) + continue; + + if (!setsByName.TryGetValue(setName, out var set)) + { + set = new PolyHavenMapSet { Name = setName }; + setsByName[setName] = set; + result.Sets.Add(set); + } + set.Maps[type] = perRes; } + // Deterministic set order: default set first, then alphabetical. + result.Sets.Sort((a, b) => + { + if (a.Name.Length == 0 != (b.Name.Length == 0)) + return a.Name.Length == 0 ? -1 : 1; + return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase); + }); + var resList = new List(resSet); resList.Sort((x, y) => ImportUtil.ResolutionRank(x).CompareTo(ImportUtil.ResolutionRank(y))); result.Resolutions = resList; diff --git a/Assets/Scripts/Editor/PolyHaven/PolyHavenImporterWindow.cs b/Assets/Scripts/Editor/PolyHaven/PolyHavenImporterWindow.cs index fd0e773..2cda95d 100644 --- a/Assets/Scripts/Editor/PolyHaven/PolyHavenImporterWindow.cs +++ b/Assets/Scripts/Editor/PolyHaven/PolyHavenImporterWindow.cs @@ -8,14 +8,19 @@ using UnityEngine; namespace Chameleon.Editor.PolyHaven { /// - /// Editor window: browse/paste a Poly Haven asset, fetch its maps, pick resolution + - /// maps, then download and auto-build a URP/Lit material under Assets/Poly Haven/{Name}/. + /// Editor window: browse/paste a Poly Haven asset (texture or model), fetch its files, + /// pick resolution + maps, then download into Assets/Poly Haven/{Name}/. Textures get an + /// auto-built URP/Lit material; models additionally download the FBX and remap its + /// material slots to the built material(s) — one per texture set. /// public sealed class PolyHavenImporterWindow : EditorWindow { private const string RootFolder = "Assets/Poly Haven"; private const string DefaultRes = "2k"; + private const int TypeTexture = 1; + private const int TypeModel = 2; + private string _urlInput = "https://polyhaven.com/a/"; private PolyHavenInfo _info; private PolyHavenFiles _files; @@ -29,14 +34,22 @@ namespace Chameleon.Editor.PolyHaven // ---- Browser ---------------------------------------------------------- - // Catalog cached for the whole session (survives window close, cleared on domain reload). - private static List s_catalog; + private static readonly string[] TabLabels = { "Textures", "Models" }; + private static readonly string[] TabTypes = { "textures", "models" }; + + // Catalogs cached per type for the whole session (survive window close, + // cleared on domain reload). + private static readonly Dictionary> s_catalogs = + new Dictionary>(); private AssetBrowserGrid _grid; private bool _browseFoldout = true; - private bool _autoLoadTried; + private int _catalogTab; + private readonly HashSet _autoLoadTried = new HashSet(); private bool _catalogLoading; + private string CatalogType => TabTypes[_catalogTab]; + [MenuItem("Tools/Poly Haven Importer")] public static void Open() { @@ -49,10 +62,10 @@ namespace Chameleon.Editor.PolyHaven _grid = new AssetBrowserGrid(Repaint); // Catalog may already be cached from a previous window instance this session. - if (s_catalog != null) + if (s_catalogs.TryGetValue(CatalogType, out var cached)) { - _autoLoadTried = true; - _grid.SetCatalog(s_catalog); + _autoLoadTried.Add(CatalogType); + _grid.SetCatalog(cached); } } @@ -62,8 +75,9 @@ namespace Chameleon.Editor.PolyHaven EditorGUILayout.LabelField("Poly Haven Importer", EditorStyles.boldLabel); EditorGUILayout.HelpBox( - "Pick a texture on polyhaven.com, paste its URL here (e.g. .../a/broken_brick_wall), " + - "Fetch, choose resolution + maps, then Download. A URP/Lit material is built automatically.", + "Pick a texture or model on polyhaven.com, paste its URL here (e.g. .../a/broken_brick_wall), " + + "Fetch, choose resolution + maps, then Download. A URP/Lit material is built automatically; " + + "models also import the FBX with the material(s) wired up.", MessageType.Info); EditorGUILayout.Space(); @@ -93,6 +107,9 @@ namespace Chameleon.Editor.PolyHaven EditorGUILayout.EndScrollView(); } + private bool IsSupportedType => _info != null && (_info.Type == TypeTexture || _info.Type == TypeModel); + private bool IsModel => _info != null && _info.Type == TypeModel; + private void DrawAssetSection() { EditorGUILayout.Space(); @@ -106,11 +123,15 @@ namespace Chameleon.Editor.PolyHaven using (new EditorGUILayout.VerticalScope()) { EditorGUILayout.LabelField(_info.Name, EditorStyles.boldLabel); - EditorGUILayout.LabelField($"Slug: {_info.Slug}", EditorStyles.miniLabel); + EditorGUILayout.LabelField( + $"Slug: {_info.Slug} ({(IsModel ? "Model" : _info.Type == TypeTexture ? "Texture" : "Unsupported")})", + EditorStyles.miniLabel); if (_info.Categories != null && _info.Categories.Length > 0) EditorGUILayout.LabelField($"Categories: {string.Join(", ", _info.Categories)}", EditorStyles.miniLabel); - if (_info.Type != 1) - EditorGUILayout.HelpBox("This asset is not a texture — only textures are supported.", MessageType.Warning); + if (!IsSupportedType) + EditorGUILayout.HelpBox("Only textures and models are supported (no HDRIs).", MessageType.Warning); + if (IsModel && _files != null && !_files.HasFbx) + EditorGUILayout.HelpBox("This model has no FBX download — cannot import.", MessageType.Warning); } } @@ -126,7 +147,10 @@ namespace Chameleon.Editor.PolyHaven _selectedRes = resArray[resIdx]; EditorGUILayout.Space(); - EditorGUILayout.LabelField("Maps", EditorStyles.miniBoldLabel); + string mapsLabel = "Maps"; + if (_files.Sets.Count > 1) + mapsLabel += $" ({_files.Sets.Count} texture sets: {string.Join(", ", _files.Sets.Select(s => s.Name))})"; + EditorGUILayout.LabelField(mapsLabel, EditorStyles.miniBoldLabel); foreach (var meta in PbrMapInfo.All) { if (!_files.Has(meta.Type)) @@ -136,10 +160,11 @@ namespace Chameleon.Editor.PolyHaven } EditorGUILayout.Space(); - bool canDownload = !_busy && _info.Type == 1 && AnyMapSelected(); + bool canDownload = !_busy && IsSupportedType && AnyMapSelected() + && (!IsModel || _files.HasFbx); using (new EditorGUI.DisabledScope(!canDownload)) { - if (GUILayout.Button("Download / Import", GUILayout.Height(30))) + if (GUILayout.Button(IsModel ? "Download / Import Model" : "Download / Import", GUILayout.Height(30))) Download(); } } @@ -166,18 +191,28 @@ namespace Chameleon.Editor.PolyHaven using (new EditorGUI.DisabledScope(_busy)) { + using (new EditorGUI.DisabledScope(_catalogLoading)) + { + int newTab = GUILayout.Toolbar(_catalogTab, TabLabels); + if (newTab != _catalogTab) + { + _catalogTab = newTab; + _grid.SetCatalog(s_catalogs.TryGetValue(CatalogType, out var cached) ? cached : null); + } + } + if (!_grid.HasCatalog) { - // Auto-load once when the browser is first opened; still offer a manual retry. - if (!_catalogLoading && !_autoLoadTried) + // Auto-load once per type when first shown; still offer a manual retry. + if (!_catalogLoading && !_autoLoadTried.Contains(CatalogType)) { - _autoLoadTried = true; + _autoLoadTried.Add(CatalogType); LoadCatalog(); } using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.LabelField(_catalogLoading - ? "Loading texture catalog…" + ? $"Loading {CatalogType} catalog…" : "Catalog not loaded."); using (new EditorGUI.DisabledScope(_catalogLoading)) if (GUILayout.Button("Load catalog", GUILayout.Width(110))) @@ -186,7 +221,7 @@ namespace Chameleon.Editor.PolyHaven return; } - _grid.Draw(SelectAsset, LoadCatalog, "textures"); + _grid.Draw(SelectAsset, LoadCatalog, CatalogType); } } @@ -199,17 +234,19 @@ namespace Chameleon.Editor.PolyHaven private void LoadCatalog() { + string type = CatalogType; _catalogLoading = true; - _status = "Loading Poly Haven texture catalog…"; + _status = $"Loading Poly Haven {type} catalog…"; Repaint(); - PolyHavenApi.FetchAssets("textures", + PolyHavenApi.FetchAssets(type, list => { - s_catalog = list; + s_catalogs[type] = list; _catalogLoading = false; - _grid.SetCatalog(list); - _status = $"Catalog loaded: {list.Count} textures."; + if (type == CatalogType) + _grid.SetCatalog(list); + _status = $"Catalog loaded: {list.Count} {type}."; Repaint(); }, err => @@ -255,9 +292,11 @@ namespace Chameleon.Editor.PolyHaven _selectedRes = files.Resolutions[Mathf.Min(1, files.Resolutions.Count - 1)]; _busy = false; - _status = info.Type == 1 - ? $"Ready: {files.Maps.Count} map type(s), {files.Resolutions.Count} resolution(s)." - : "Fetched, but this asset is not a texture."; + _status = IsSupportedType + ? $"Ready: {files.MapTypeCount} map type(s), {files.Resolutions.Count} resolution(s)" + + (files.Sets.Count > 1 ? $", {files.Sets.Count} texture sets" : "") + + (IsModel ? (files.HasFbx ? ", FBX available." : ", but no FBX!") : ".") + : "Fetched, but this asset type is not supported (textures/models only)."; Repaint(); }, err => Fail(err)); }, err => Fail(err)); @@ -265,29 +304,27 @@ namespace Chameleon.Editor.PolyHaven // ---- Download ---------------------------------------------------------- + /// One pending file download: which set/map it belongs to (Type null for the FBX). + private struct Pending + { + public PolyHavenMapSet Set; + public PbrMapType Type; + public bool IsFbx; + public PolyHavenFile File; + public string DestPath; + } + private void Download() { - // Resolve which maps to fetch. - var wanted = new List(); + // Resolve which map types to fetch (per files-level availability). + var wantedTypes = new List(); foreach (var meta in PbrMapInfo.All) { if (!_files.Has(meta.Type)) continue; bool on = _mapEnabled.TryGetValue(meta.Type, out var v) ? v : meta.DefaultEnabled; - if (on) wanted.Add(meta.Type); + if (on) wantedTypes.Add(meta.Type); } - - // ARM fallback: if the asset packs AO/Rough/Metal only into ARM (no dedicated - // map for a channel), pull ARM so the builder can unpack the missing channels. - if (_files.Has(PbrMapType.Arm) && !wanted.Contains(PbrMapType.Arm)) - { - bool channelMissing = !_files.Has(PbrMapType.Metal) - || !_files.Has(PbrMapType.Rough) - || !_files.Has(PbrMapType.AO); - if (channelMissing) - wanted.Add(PbrMapType.Arm); - } - - if (wanted.Count == 0) + if (wantedTypes.Count == 0) return; string displayName = _info.Name; @@ -298,14 +335,74 @@ namespace Chameleon.Editor.PolyHaven return; ImportUtil.EnsureFolder(folder); + // Build the per-set download list. ARM fallback per set: if a set packs + // AO/Rough/Metal only into ARM, pull ARM so the builder can unpack it. + var pending = new List(); + foreach (var set in _files.Sets) + { + var setTypes = new List(); + foreach (var t in wantedTypes) + if (set.Has(t)) + setTypes.Add(t); + + if (set.Has(PbrMapType.Arm) && !setTypes.Contains(PbrMapType.Arm)) + { + bool channelMissing = !set.Has(PbrMapType.Metal) + || !set.Has(PbrMapType.Rough) + || !set.Has(PbrMapType.AO); + if (channelMissing) + setTypes.Add(PbrMapType.Arm); + } + + string setPart = string.IsNullOrEmpty(set.Name) + ? "" + : "_" + ImportUtil.SanitizeFileName(set.Name); + + foreach (var type in setTypes) + { + if (!set.TryGetWithFallback(type, _selectedRes, out var file, out var usedRes)) + continue; + if (usedRes != _selectedRes) + Debug.LogWarning($"[PolyHaven] {set.Name}/{type}: {_selectedRes} unavailable, using {usedRes}."); + + var meta = PbrMapInfo.Get(type); + pending.Add(new Pending + { + Set = set, + Type = type, + File = file, + DestPath = $"{folder}/{_info.Slug}{setPart}_{meta.Suffix}.{file.Format}", + }); + } + } + + // Model: the FBX geometry itself. + string fbxPath = null; + if (IsModel) + { + if (!_files.TryGetFbxWithFallback(_selectedRes, out var fbx, out var fbxRes)) + { + _status = "No FBX file available for this model."; + return; + } + if (fbxRes != _selectedRes) + Debug.LogWarning($"[PolyHaven] FBX: {_selectedRes} unavailable, using {fbxRes}."); + fbxPath = $"{folder}/{_info.Slug}_{fbxRes}.fbx"; + pending.Add(new Pending { IsFbx = true, File = fbx, DestPath = fbxPath }); + } + + if (pending.Count == 0) + return; + int maxSize = ImportUtil.ResolutionToMaxSize(_selectedRes); _busy = true; - _status = $"Downloading {wanted.Count} map(s) at {_selectedRes.ToUpperInvariant()}…"; + _status = $"Downloading {pending.Count} file(s) at {_selectedRes.ToUpperInvariant()}…"; Repaint(); - var results = new List(); + var resultsBySet = new Dictionary>(); var failures = new List(); - int remaining = wanted.Count; + bool fbxOk = false; + int remaining = pending.Count; AssetDatabase.StartAssetEditing(); @@ -318,7 +415,8 @@ namespace Chameleon.Editor.PolyHaven AssetDatabase.StopAssetEditing(); AssetDatabase.Refresh(); - if (results.Count == 0) + int mapCount = resultsBySet.Sum(kv => kv.Value.Count); + if (mapCount == 0) { _busy = false; _status = "Download failed — no maps saved."; @@ -328,38 +426,53 @@ namespace Chameleon.Editor.PolyHaven return; } - PbrMaterialBuilder.Build(folder, displayName, _info.Slug, maxSize, results); + // One material per texture set (single-set assets keep the plain name). + var setMaterials = new List<(string setName, Material mat)>(); + foreach (var set in _files.Sets) + { + if (!resultsBySet.TryGetValue(set, out var maps) || maps.Count == 0) + continue; + bool named = !string.IsNullOrEmpty(set.Name) && _files.Sets.Count > 1; + string matName = named ? $"{displayName} {set.Name}" : displayName; + string setSlug = named ? $"{_info.Slug}_{ImportUtil.SanitizeFileName(set.Name)}" : _info.Slug; + var mat = PbrMaterialBuilder.Build(folder, matName, setSlug, maxSize, maps); + if (mat) + setMaterials.Add((set.Name, mat)); + } + + // Model: wire the built materials into the imported FBX. + if (IsModel && fbxOk && setMaterials.Count > 0) + ModelImportUtil.RemapMaterials(fbxPath, setMaterials); + _busy = false; - _status = $"Done: {results.Count} map(s) imported, material built in {folder}."; + _status = $"Done: {mapCount} map(s) imported, {setMaterials.Count} material(s) built in {folder}" + + (IsModel ? (fbxOk ? ", FBX imported." : ", but the FBX download FAILED.") : "."); if (failures.Count > 0) - _status += $" ({failures.Count} map(s) failed — see console.)"; + _status += $" ({failures.Count} file(s) failed — see console.)"; Repaint(); } - foreach (var type in wanted) + foreach (var p in pending) { - if (!_files.TryGetWithFallback(type, _selectedRes, out var file, out var usedRes)) - { - failures.Add($"{type}: no file"); - FinishOne(); - continue; - } - if (usedRes != _selectedRes) - Debug.LogWarning($"[PolyHaven] {type}: {_selectedRes} unavailable, using {usedRes}."); - - var meta = PbrMapInfo.Get(type); - string fileName = $"{_info.Slug}_{meta.Suffix}.{file.Format}"; - string destPath = $"{folder}/{fileName}"; - - EditorWebRequest.DownloadFile(file.Url, Path.GetFullPath(destPath), + var item = p; // capture per iteration + EditorWebRequest.DownloadFile(item.File.Url, Path.GetFullPath(item.DestPath), onDone: () => { - results.Add(new PbrMaterialBuilder.Downloaded { Type = type, AssetPath = destPath }); + if (item.IsFbx) + { + fbxOk = true; + } + else + { + if (!resultsBySet.TryGetValue(item.Set, out var list)) + resultsBySet[item.Set] = list = new List(); + list.Add(new PbrMaterialBuilder.Downloaded { Type = item.Type, AssetPath = item.DestPath }); + } FinishOne(); }, onError: err => { - failures.Add($"{type}: {err}"); + failures.Add($"{(item.IsFbx ? "FBX" : item.Type.ToString())}: {err}"); FinishOne(); }); } diff --git a/Assets/Scripts/Editor/WebAssets/ModelImportUtil.cs b/Assets/Scripts/Editor/WebAssets/ModelImportUtil.cs new file mode 100644 index 0000000..4bfe5b6 --- /dev/null +++ b/Assets/Scripts/Editor/WebAssets/ModelImportUtil.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace Chameleon.Editor.WebAssets +{ + /// Shared helpers for imported 3D models (FBX etc.). + public static class ModelImportUtil + { + /// + /// Remaps every source material of an imported model to the given built materials. + /// With a single set, all slots get that material; with multiple sets, the source + /// material name is matched against the set names (substring, case-insensitive). + /// + public static void RemapMaterials(string modelAssetPath, List<(string setName, Material mat)> sets) + { + if (sets == null || sets.Count == 0) + return; + var importer = AssetImporter.GetAtPath(modelAssetPath) as ModelImporter; + if (!importer) + { + Debug.LogWarning($"[PBR Import] No model importer at {modelAssetPath} — skipping material remap."); + return; + } + + // Source material names = the embedded materials Unity generated on import. + var srcNames = new List(); + foreach (var obj in AssetDatabase.LoadAllAssetsAtPath(modelAssetPath)) + if (obj is Material m) + srcNames.Add(m.name); + + if (srcNames.Count == 0) + { + Debug.LogWarning($"[PBR Import] {modelAssetPath} exposes no materials to remap."); + return; + } + + foreach (var name in srcNames) + { + var target = Pick(name, sets); + if (!target) + continue; + importer.AddRemap(new AssetImporter.SourceAssetIdentifier(typeof(Material), name), target); + } + importer.SaveAndReimport(); + Debug.Log($"[PBR Import] Remapped {srcNames.Count} material slot(s) on {modelAssetPath}."); + } + + private static Material Pick(string srcName, List<(string setName, Material mat)> sets) + { + if (sets.Count == 1) + return sets[0].mat; + + string s = srcName.ToLowerInvariant(); + foreach (var (setName, mat) in sets) + if (!string.IsNullOrEmpty(setName) && s.Contains(setName.ToLowerInvariant())) + return mat; + + Debug.LogWarning($"[PBR Import] Material '{srcName}' matched no texture set — using '{sets[0].setName}'."); + return sets[0].mat; + } + } +} diff --git a/Assets/Scripts/Editor/WebAssets/ModelImportUtil.cs.meta b/Assets/Scripts/Editor/WebAssets/ModelImportUtil.cs.meta new file mode 100644 index 0000000..2084ada --- /dev/null +++ b/Assets/Scripts/Editor/WebAssets/ModelImportUtil.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a54d5f7a2461bb54ca28df0d87665f8e \ No newline at end of file diff --git a/Assets/Scripts/Editor/WebAssets/PbrMaterialBuilder.cs b/Assets/Scripts/Editor/WebAssets/PbrMaterialBuilder.cs index 087f29c..39d64e3 100644 --- a/Assets/Scripts/Editor/WebAssets/PbrMaterialBuilder.cs +++ b/Assets/Scripts/Editor/WebAssets/PbrMaterialBuilder.cs @@ -19,7 +19,8 @@ namespace Chameleon.Editor.WebAssets public string AssetPath; // "Assets/Poly Haven/.../foo_diffuse.jpg" } - public static void Build(string folder, string displayName, string slug, int maxSize, + /// Builds the material and returns it (null if it could not be created). + public static Material Build(string folder, string displayName, string slug, int maxSize, List maps) { var byType = new Dictionary(); @@ -61,7 +62,7 @@ namespace Chameleon.Editor.WebAssets if (!shader) { Debug.LogError("[PBR Import] URP/Lit shader not found — is URP installed?"); - return; + return null; } var mat = new Material(shader) { name = displayName }; @@ -105,6 +106,7 @@ namespace Chameleon.Editor.WebAssets EditorGUIUtility.PingObject(mat); Selection.activeObject = mat; Debug.Log($"[PBR Import] Material created: {matPath}"); + return mat; } private static Texture2D Load(string path) => AssetDatabase.LoadAssetAtPath(path);