From fb4098b9f85a5115ed3c8dc0dcc6c76da8a8bc58 Mon Sep 17 00:00:00 2001 From: MelekTaus Moribundus Date: Thu, 16 Jul 2026 15:27:02 +0200 Subject: [PATCH] Add ambientCG importer and extract shared web-asset import framework New Tools/ambientCG Importer window (catalog browser, ZIP download with selective map extraction, URP/Lit material build). Generic pieces moved from the PolyHaven tool into Editor/WebAssets (EditorWebRequest, PbrMapType, PbrMaterialBuilder, ImportUtil, AssetBrowserGrid); PolyHaven API/window now delegate to the shared code, behavior unchanged. Co-Authored-By: Claude Fable 5 --- Assets/Scripts/Editor/AmbientCg.meta | 8 + .../Scripts/Editor/AmbientCg/AmbientCgApi.cs | 279 ++++++++++++ .../Editor/AmbientCg/AmbientCgApi.cs.meta | 2 + .../AmbientCg/AmbientCgImporterWindow.cs | 401 ++++++++++++++++++ .../AmbientCg/AmbientCgImporterWindow.cs.meta | 2 + .../Scripts/Editor/PolyHaven/PolyHavenApi.cs | 172 +++----- .../PolyHaven/PolyHavenImporterWindow.cs | 226 ++-------- .../Editor/PolyHaven/PolyHavenMapType.cs | 71 ---- .../Editor/PolyHaven/PolyHavenMapType.cs.meta | 2 - .../PolyHavenMaterialBuilder.cs.meta | 2 - Assets/Scripts/Editor/WebAssets.meta | 8 + .../Editor/WebAssets/AssetBrowserGrid.cs | 172 ++++++++ .../Editor/WebAssets/AssetBrowserGrid.cs.meta | 2 + .../Editor/WebAssets/EditorWebRequest.cs | 84 ++++ .../Editor/WebAssets/EditorWebRequest.cs.meta | 2 + Assets/Scripts/Editor/WebAssets/ImportUtil.cs | 66 +++ .../Editor/WebAssets/ImportUtil.cs.meta | 2 + Assets/Scripts/Editor/WebAssets/PbrMapType.cs | 68 +++ .../Editor/WebAssets/PbrMapType.cs.meta | 2 + .../PbrMaterialBuilder.cs} | 49 +-- .../WebAssets/PbrMaterialBuilder.cs.meta | 2 + 21 files changed, 1205 insertions(+), 417 deletions(-) create mode 100644 Assets/Scripts/Editor/AmbientCg.meta create mode 100644 Assets/Scripts/Editor/AmbientCg/AmbientCgApi.cs create mode 100644 Assets/Scripts/Editor/AmbientCg/AmbientCgApi.cs.meta create mode 100644 Assets/Scripts/Editor/AmbientCg/AmbientCgImporterWindow.cs create mode 100644 Assets/Scripts/Editor/AmbientCg/AmbientCgImporterWindow.cs.meta delete mode 100644 Assets/Scripts/Editor/PolyHaven/PolyHavenMapType.cs delete mode 100644 Assets/Scripts/Editor/PolyHaven/PolyHavenMapType.cs.meta delete mode 100644 Assets/Scripts/Editor/PolyHaven/PolyHavenMaterialBuilder.cs.meta create mode 100644 Assets/Scripts/Editor/WebAssets.meta create mode 100644 Assets/Scripts/Editor/WebAssets/AssetBrowserGrid.cs create mode 100644 Assets/Scripts/Editor/WebAssets/AssetBrowserGrid.cs.meta create mode 100644 Assets/Scripts/Editor/WebAssets/EditorWebRequest.cs create mode 100644 Assets/Scripts/Editor/WebAssets/EditorWebRequest.cs.meta create mode 100644 Assets/Scripts/Editor/WebAssets/ImportUtil.cs create mode 100644 Assets/Scripts/Editor/WebAssets/ImportUtil.cs.meta create mode 100644 Assets/Scripts/Editor/WebAssets/PbrMapType.cs create mode 100644 Assets/Scripts/Editor/WebAssets/PbrMapType.cs.meta rename Assets/Scripts/Editor/{PolyHaven/PolyHavenMaterialBuilder.cs => WebAssets/PbrMaterialBuilder.cs} (84%) create mode 100644 Assets/Scripts/Editor/WebAssets/PbrMaterialBuilder.cs.meta diff --git a/Assets/Scripts/Editor/AmbientCg.meta b/Assets/Scripts/Editor/AmbientCg.meta new file mode 100644 index 0000000..440ff4e --- /dev/null +++ b/Assets/Scripts/Editor/AmbientCg.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fa6fff02eb207e748b98bea4ebc83bf0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/AmbientCg/AmbientCgApi.cs b/Assets/Scripts/Editor/AmbientCg/AmbientCgApi.cs new file mode 100644 index 0000000..f27efbb --- /dev/null +++ b/Assets/Scripts/Editor/AmbientCg/AmbientCgApi.cs @@ -0,0 +1,279 @@ +using System; +using System.Collections.Generic; +using Chameleon.Editor.WebAssets; +using Newtonsoft.Json.Linq; + +namespace Chameleon.Editor.AmbientCg +{ + /// One downloadable ZIP variant of an asset (e.g. attribute "2K-JPG"). + public readonly struct AmbientCgZip + { + public readonly string Resolution; // "2K" + public readonly string Format; // "JPG" / "PNG" + public readonly string Url; + public readonly string FileName; + public readonly long Size; + + public AmbientCgZip(string resolution, string format, string url, string fileName, long size) + { + Resolution = resolution; + Format = format; + Url = url; + FileName = fileName; + Size = size; + } + + public bool IsValid => !string.IsNullOrEmpty(Url); + } + + /// Details for one asset: display data, available maps and ZIP downloads. + public sealed class AmbientCgAsset + { + public string Id; // "Tiles141" + public string Name; // "Tiles 141" + public string Category; + public string ThumbnailUrl; + public string DataType; // "Material" expected + + // PBR channels this asset ships, already mapped to the shared map types. + public readonly List Maps = new List(); + + public readonly List Zips = new List(); + + public List Resolutions() + { + var set = new HashSet(); + foreach (var z in Zips) set.Add(z.Resolution); + var list = new List(set); + list.Sort((x, y) => ImportUtil.ResolutionRank(x).CompareTo(ImportUtil.ResolutionRank(y))); + return list; + } + + public List Formats(string resolution) + { + var list = new List(); + foreach (var z in Zips) + if (z.Resolution == resolution && !list.Contains(z.Format)) + list.Add(z.Format); + list.Sort(); // "JPG" before "PNG" + return list; + } + + public bool TryGetZip(string resolution, string format, out AmbientCgZip zip) + { + foreach (var z in Zips) + { + if (z.Resolution == resolution && z.Format == format) + { + zip = z; + return true; + } + } + zip = default; + return false; + } + } + + /// + /// Async client for the public ambientCG API (v2 full_json). Unlike Poly Haven, + /// ambientCG ships one ZIP per resolution+format containing all maps with + /// standardized file suffixes (Color, NormalGL, Roughness, AmbientOcclusion, ...). + /// + public static class AmbientCgApi + { + private const string ApiBase = "https://ambientcg.com/api/v2/full_json"; + private const int PageSize = 250; // API maximum + + // ambientCG "maps" names -> shared map type + file suffix inside the ZIP. + public static readonly (string apiName, PbrMapType type, string zipSuffix)[] MapNames = + { + ("color", PbrMapType.Diffuse, "Color"), + ("normal", PbrMapType.NormalGL, "NormalGL"), + ("roughness", PbrMapType.Rough, "Roughness"), + ("ambient-occlusion", PbrMapType.AO, "AmbientOcclusion"), + ("metalness", PbrMapType.Metal, "Metalness"), + ("displacement", PbrMapType.Displacement, "Displacement"), + // "opacity" is intentionally unsupported — URP/Lit opaque materials don't use it. + }; + + public static string ZipSuffix(PbrMapType type) + { + foreach (var (_, t, suffix) in MapNames) + if (t == type) + return suffix; + return null; + } + + public static string ExtractId(string urlOrId) + { + if (string.IsNullOrWhiteSpace(urlOrId)) return null; + var s = urlOrId.Trim(); + + // https://ambientcg.com/a/Tiles141 + int a = s.IndexOf("/a/", StringComparison.OrdinalIgnoreCase); + if (a >= 0) + s = s.Substring(a + 3); + + // https://ambientcg.com/view?id=Tiles141 + int q = s.IndexOf("id=", StringComparison.OrdinalIgnoreCase); + if (q >= 0) + s = s.Substring(q + 3); + + foreach (var sep in new[] { '?', '#', '/', '&' }) + { + int i = s.IndexOf(sep); + if (i >= 0) s = s.Substring(0, i); + } + s = s.Trim(); + return s.Length == 0 ? null : s; + } + + public static string ThumbnailUrl(string id) => + $"https://acg-media.struffelproductions.com/file/ambientCG-Web/media/thumbnail/256-PNG/{id}.png"; + + // ---- Catalog (paginated) ------------------------------------------------ + + /// + /// Loads the full Material catalog as browser-grid entries. The API caps pages + /// at 250 assets, so this walks all pages sequentially (~2000 assets => ~8 calls). + /// + public static void FetchCatalog(Action> onDone, Action onError, + Action onProgress = null) + { + var all = new List(); + FetchCatalogPage(all, 0, onDone, onError, onProgress); + } + + private static void FetchCatalogPage(List all, int offset, + Action> onDone, Action onError, Action onProgress) + { + string url = $"{ApiBase}?type=Material&sort=Alphabet&limit={PageSize}&offset={offset}&include=imageData"; + EditorWebRequest.GetText(url, text => + { + int total; + try + { + var root = JObject.Parse(text); + total = root["numberOfResults"] != null ? (int)root["numberOfResults"] : 0; + + if (root["foundAssets"] is JArray assets) + { + foreach (var t in assets) + { + if (t is not JObject o) continue; + string id = (string)o["assetId"]; + if (string.IsNullOrEmpty(id)) continue; + + string name = (string)o["displayName"] ?? id; + var e = new AssetBrowserGrid.Entry + { + Key = id, + Name = name, + ThumbnailUrl = (string)(o["previewImage"]?["256-PNG"]) ?? ThumbnailUrl(id), + }; + e.Search = (name + " " + id + " " + + ((string)o["displayCategory"] ?? "") + " " + + JoinStrings(o["tags"])).ToLowerInvariant(); + all.Add(e); + } + } + } + catch (Exception e) + { + onError?.Invoke($"Catalog parse failed: {e.Message}"); + return; + } + + onProgress?.Invoke(all.Count, total); + if (all.Count < total && offset + PageSize < total) + FetchCatalogPage(all, offset + PageSize, onDone, onError, onProgress); + else + onDone?.Invoke(all); + }, onError); + } + + // ---- Asset details ------------------------------------------------------- + + public static void FetchAsset(string id, Action onDone, Action onError) + { + string url = $"{ApiBase}?id={Uri.EscapeDataString(id)}&include=downloadData,imageData,mapData"; + EditorWebRequest.GetText(url, text => + { + try + { + var root = JObject.Parse(text); + if (root["foundAssets"] is not JArray assets || assets.Count == 0) + { + onError?.Invoke($"Asset '{id}' not found."); + return; + } + onDone?.Invoke(ParseAsset((JObject)assets[0])); + } + catch (Exception e) { onError?.Invoke($"Asset parse failed: {e.Message}"); } + }, onError); + } + + private static AmbientCgAsset ParseAsset(JObject o) + { + string id = (string)o["assetId"]; + var asset = new AmbientCgAsset + { + Id = id, + Name = (string)o["displayName"] ?? id, + Category = (string)o["displayCategory"] ?? "", + DataType = (string)o["dataType"] ?? "", + ThumbnailUrl = (string)(o["previewImage"]?["256-PNG"]) ?? ThumbnailUrl(id), + }; + + // Which PBR channels this asset ships. + if (o["maps"] is JArray maps) + { + foreach (var m in maps) + { + var name = (string)m; + foreach (var (apiName, type, _) in MapNames) + if (string.Equals(name, apiName, StringComparison.OrdinalIgnoreCase)) + asset.Maps.Add(type); + } + } + + // ZIP downloads: downloadFolders.default.downloadFiletypeCategories.zip.downloads[], + // each with attribute like "2K-JPG". + if (o.SelectToken("downloadFolders.default.downloadFiletypeCategories.zip.downloads") is JArray downloads) + { + foreach (var t in downloads) + { + if (t is not JObject d) continue; + string attr = (string)d["attribute"]; + string link = (string)d["downloadLink"] ?? (string)d["fullDownloadPath"]; + if (string.IsNullOrEmpty(attr) || string.IsNullOrEmpty(link)) + continue; + + int dash = attr.LastIndexOf('-'); + if (dash <= 0) + continue; // not a "{RES}-{FMT}" texture zip + string res = attr.Substring(0, dash); + string fmt = attr.Substring(dash + 1); + + asset.Zips.Add(new AmbientCgZip(res, fmt, link, + (string)d["fileName"] ?? $"{id}_{attr}.zip", + d["size"] != null ? (long)d["size"] : 0)); + } + } + + return asset; + } + + private static string JoinStrings(JToken token) + { + if (token is not JArray arr) return ""; + var list = new List(arr.Count); + foreach (var t in arr) + { + var s = (string)t; + if (!string.IsNullOrEmpty(s)) list.Add(s); + } + return string.Join(" ", list); + } + } +} diff --git a/Assets/Scripts/Editor/AmbientCg/AmbientCgApi.cs.meta b/Assets/Scripts/Editor/AmbientCg/AmbientCgApi.cs.meta new file mode 100644 index 0000000..6676126 --- /dev/null +++ b/Assets/Scripts/Editor/AmbientCg/AmbientCgApi.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4cfc661c9314b9d4f9c5c491ae7df74f \ No newline at end of file diff --git a/Assets/Scripts/Editor/AmbientCg/AmbientCgImporterWindow.cs b/Assets/Scripts/Editor/AmbientCg/AmbientCgImporterWindow.cs new file mode 100644 index 0000000..80dec07 --- /dev/null +++ b/Assets/Scripts/Editor/AmbientCg/AmbientCgImporterWindow.cs @@ -0,0 +1,401 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using Chameleon.Editor.WebAssets; +using UnityEditor; +using UnityEngine; + +namespace Chameleon.Editor.AmbientCg +{ + /// + /// Editor window: browse/paste an ambientCG material, pick resolution + format + + /// maps, then download the ZIP, extract the selected maps and auto-build a URP/Lit + /// material under Assets/ambientCG/{Name}/. Reuses the shared WebAssets pipeline + /// (transport, browser grid, PBR material builder). + /// + public sealed class AmbientCgImporterWindow : EditorWindow + { + private const string RootFolder = "Assets/ambientCG"; + private const string DefaultRes = "2K"; + private const string DefaultFmt = "JPG"; + + private string _urlInput = "https://ambientcg.com/a/"; + private AmbientCgAsset _asset; + private Texture2D _thumbnail; + private string _selectedRes = DefaultRes; + private string _selectedFmt = DefaultFmt; + private readonly Dictionary _mapEnabled = new Dictionary(); + + private bool _busy; + private string _status = ""; + private Vector2 _scroll; + + // ---- Browser ---------------------------------------------------------- + + // Catalog cached for the whole session (survives window close, cleared on domain reload). + private static List s_catalog; + + private AssetBrowserGrid _grid; + private bool _browseFoldout = true; + private bool _autoLoadTried; + private bool _catalogLoading; + + [MenuItem("Tools/ambientCG Importer")] + public static void Open() + { + var w = GetWindow("ambientCG Importer"); + w.minSize = new Vector2(440, 480); + } + + private void OnEnable() + { + _grid = new AssetBrowserGrid(Repaint); + if (s_catalog != null) + { + _autoLoadTried = true; + _grid.SetCatalog(s_catalog); + } + } + + private void OnGUI() + { + _scroll = EditorGUILayout.BeginScrollView(_scroll); + + EditorGUILayout.LabelField("ambientCG Importer", EditorStyles.boldLabel); + EditorGUILayout.HelpBox( + "Pick a material on ambientcg.com, paste its URL here (e.g. .../a/Tiles141), " + + "Fetch, choose resolution + format + maps, then Download. The maps ship as one ZIP; " + + "the selected maps are extracted and a URP/Lit material is built automatically.", + MessageType.Info); + + EditorGUILayout.Space(); + + DrawBrowser(); + + EditorGUILayout.Space(); + + using (new EditorGUI.DisabledScope(_busy)) + { + EditorGUILayout.LabelField("Asset URL or ID", EditorStyles.miniBoldLabel); + using (new EditorGUILayout.HorizontalScope()) + { + _urlInput = EditorGUILayout.TextField(_urlInput); + if (GUILayout.Button("Fetch", GUILayout.Width(70))) + Fetch(); + } + } + + if (_asset != null) + DrawAssetSection(); + + EditorGUILayout.Space(); + if (!string.IsNullOrEmpty(_status)) + EditorGUILayout.HelpBox(_status, _busy ? MessageType.None : MessageType.Info); + + EditorGUILayout.EndScrollView(); + } + + private void DrawAssetSection() + { + EditorGUILayout.Space(); + using (new EditorGUILayout.HorizontalScope()) + { + if (_thumbnail) + { + var r = GUILayoutUtility.GetRect(96, 96, GUILayout.Width(96), GUILayout.Height(96)); + GUI.DrawTexture(r, _thumbnail, ScaleMode.ScaleToFit); + } + using (new EditorGUILayout.VerticalScope()) + { + EditorGUILayout.LabelField(_asset.Name, EditorStyles.boldLabel); + EditorGUILayout.LabelField($"ID: {_asset.Id}", EditorStyles.miniLabel); + if (!string.IsNullOrEmpty(_asset.Category)) + EditorGUILayout.LabelField($"Category: {_asset.Category}", EditorStyles.miniLabel); + if (_asset.DataType != "Material") + EditorGUILayout.HelpBox("This asset is not a material — only materials are supported.", MessageType.Warning); + } + } + + var resolutions = _asset.Resolutions(); + if (resolutions.Count == 0) + { + EditorGUILayout.HelpBox("No ZIP downloads found for this asset.", MessageType.Warning); + return; + } + + EditorGUILayout.Space(); + + // Resolution popup. + var resArray = resolutions.ToArray(); + int resIdx = Mathf.Max(0, Array.IndexOf(resArray, _selectedRes)); + resIdx = EditorGUILayout.Popup("Resolution", resIdx, resArray); + _selectedRes = resArray[resIdx]; + + // Format popup (per resolution; JPG is smaller, PNG lossless). + var formats = _asset.Formats(_selectedRes); + var fmtArray = formats.ToArray(); + int fmtIdx = Mathf.Max(0, Array.IndexOf(fmtArray, _selectedFmt)); + fmtIdx = EditorGUILayout.Popup("Format", fmtIdx, fmtArray); + _selectedFmt = fmtArray[fmtIdx]; + + if (_asset.TryGetZip(_selectedRes, _selectedFmt, out var zipInfo) && zipInfo.Size > 0) + EditorGUILayout.LabelField($"ZIP size: {zipInfo.Size / (1024f * 1024f):0.#} MB", EditorStyles.miniLabel); + + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Maps", EditorStyles.miniBoldLabel); + foreach (var meta in PbrMapInfo.All) + { + if (!_asset.Maps.Contains(meta.Type)) + continue; + bool cur = _mapEnabled.TryGetValue(meta.Type, out var v) ? v : meta.DefaultEnabled; + _mapEnabled[meta.Type] = EditorGUILayout.ToggleLeft(meta.Label, cur); + } + + EditorGUILayout.Space(); + bool canDownload = !_busy && _asset.DataType == "Material" && AnyMapSelected(); + using (new EditorGUI.DisabledScope(!canDownload)) + { + if (GUILayout.Button("Download / Import", GUILayout.Height(30))) + Download(); + } + } + + private bool AnyMapSelected() + { + foreach (var meta in PbrMapInfo.All) + { + if (!_asset.Maps.Contains(meta.Type)) + continue; + if (_mapEnabled.TryGetValue(meta.Type, out var v) ? v : meta.DefaultEnabled) + return true; + } + return false; + } + + // ---- Browser ----------------------------------------------------------- + + private void DrawBrowser() + { + _browseFoldout = EditorGUILayout.Foldout(_browseFoldout, "Browse catalog", true, EditorStyles.foldoutHeader); + if (!_browseFoldout) + return; + + using (new EditorGUI.DisabledScope(_busy)) + { + if (!_grid.HasCatalog) + { + if (!_catalogLoading && !_autoLoadTried) + { + _autoLoadTried = true; + LoadCatalog(); + } + using (new EditorGUILayout.HorizontalScope()) + { + EditorGUILayout.LabelField(_catalogLoading + ? "Loading material catalog…" + : "Catalog not loaded."); + using (new EditorGUI.DisabledScope(_catalogLoading)) + if (GUILayout.Button("Load catalog", GUILayout.Width(110))) + LoadCatalog(); + } + return; + } + + _grid.Draw(SelectAsset, LoadCatalog, "materials"); + } + } + + private void SelectAsset(AssetBrowserGrid.Entry a) + { + _urlInput = "https://ambientcg.com/a/" + a.Key; + GUI.FocusControl(null); + Fetch(); + } + + private void LoadCatalog() + { + _catalogLoading = true; + _status = "Loading ambientCG material catalog…"; + Repaint(); + + AmbientCgApi.FetchCatalog( + list => + { + s_catalog = list; + _catalogLoading = false; + _grid.SetCatalog(list); + _status = $"Catalog loaded: {list.Count} materials."; + Repaint(); + }, + err => + { + _catalogLoading = false; + _status = "Catalog error: " + err; + Debug.LogError("[ambientCG] " + err); + Repaint(); + }, + (loaded, total) => + { + _status = $"Loading catalog… {loaded}/{total}"; + Repaint(); + }); + } + + // ---- Fetch ------------------------------------------------------------- + + private void Fetch() + { + string id = AmbientCgApi.ExtractId(_urlInput); + if (string.IsNullOrEmpty(id)) + { + EditorUtility.DisplayDialog("ambientCG", "Could not read an asset ID from the URL.", "OK"); + return; + } + + _busy = true; + _status = $"Fetching '{id}'…"; + _asset = null; + _thumbnail = null; + _mapEnabled.Clear(); + Repaint(); + + AmbientCgApi.FetchAsset(id, asset => + { + _asset = asset; + EditorWebRequest.GetTexture(asset.ThumbnailUrl, tex => { _thumbnail = tex; Repaint(); }, _ => { }); + + var resolutions = asset.Resolutions(); + if (!resolutions.Contains(_selectedRes)) + _selectedRes = resolutions.Contains(DefaultRes) ? DefaultRes + : resolutions.Count > 0 ? resolutions[0] : DefaultRes; + var formats = asset.Formats(_selectedRes); + if (!formats.Contains(_selectedFmt)) + _selectedFmt = formats.Contains(DefaultFmt) ? DefaultFmt + : formats.Count > 0 ? formats[0] : DefaultFmt; + + _busy = false; + _status = asset.DataType == "Material" + ? $"Ready: {asset.Maps.Count} map type(s), {resolutions.Count} resolution(s)." + : "Fetched, but this asset is not a material."; + Repaint(); + }, err => Fail(err)); + } + + // ---- Download ---------------------------------------------------------- + + private void Download() + { + if (!_asset.TryGetZip(_selectedRes, _selectedFmt, out var zip)) + { + Fail($"No ZIP for {_selectedRes}-{_selectedFmt}."); + return; + } + + var wanted = new List(); + foreach (var meta in PbrMapInfo.All) + { + if (!_asset.Maps.Contains(meta.Type)) + continue; + if (_mapEnabled.TryGetValue(meta.Type, out var v) ? v : meta.DefaultEnabled) + wanted.Add(meta.Type); + } + if (wanted.Count == 0) + return; + + string displayName = _asset.Name; + string folder = $"{RootFolder}/{ImportUtil.SanitizeFileName(displayName)}"; + + folder = ImportUtil.ResolveFolderCollision(folder, "ambientCG"); + if (folder == null) + return; + ImportUtil.EnsureFolder(folder); + + string tempZip = Path.Combine(Path.GetTempPath(), zip.FileName); + + _busy = true; + _status = $"Downloading {zip.FileName}…"; + Repaint(); + + EditorWebRequest.DownloadFile(zip.Url, tempZip, + onDone: () => ExtractAndBuild(tempZip, folder, displayName, wanted), + onError: err => + { + Fail(err); + }, + onProgress: p => + { + _status = $"Downloading {zip.FileName}… {p * 100f:0}%"; + Repaint(); + }); + } + + /// + /// Extracts the selected maps out of the downloaded ZIP into the target folder, + /// then hands them to the shared PBR material builder. ZIP contents follow the + /// ambientCG convention "{Id}_{Res}-{Fmt}_{MapSuffix}.{ext}". + /// + private void ExtractAndBuild(string tempZip, string folder, string displayName, List wanted) + { + var results = new List(); + var missing = new List(wanted); + + try + { + using var archive = ZipFile.OpenRead(tempZip); + foreach (var entry in archive.Entries) + { + string nameNoExt = Path.GetFileNameWithoutExtension(entry.Name); + foreach (var type in wanted) + { + string suffix = "_" + AmbientCgApi.ZipSuffix(type); + if (!nameNoExt.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) + continue; + + string destPath = $"{folder}/{entry.Name}"; + entry.ExtractToFile(Path.GetFullPath(destPath), overwrite: true); + results.Add(new PbrMaterialBuilder.Downloaded { Type = type, AssetPath = destPath }); + missing.Remove(type); + break; + } + } + } + catch (Exception e) + { + Fail($"ZIP extraction failed: {e.Message}"); + return; + } + finally + { + try { File.Delete(tempZip); } catch { /* temp file, best effort */ } + } + + if (results.Count == 0) + { + Fail("No selected maps found inside the ZIP."); + return; + } + foreach (var m in missing) + Debug.LogWarning($"[ambientCG] Map '{m}' not found inside the ZIP — skipped."); + + AssetDatabase.Refresh(); + + int maxSize = ImportUtil.ResolutionToMaxSize(_selectedRes); + PbrMaterialBuilder.Build(folder, displayName, _asset.Id, maxSize, results); + + _busy = false; + _status = $"Done: {results.Count} map(s) imported, material built in {folder}."; + Repaint(); + } + + // ---- Helpers ----------------------------------------------------------- + + private void Fail(string message) + { + _busy = false; + _status = "Error: " + message; + Debug.LogError("[ambientCG] " + message); + Repaint(); + } + } +} diff --git a/Assets/Scripts/Editor/AmbientCg/AmbientCgImporterWindow.cs.meta b/Assets/Scripts/Editor/AmbientCg/AmbientCgImporterWindow.cs.meta new file mode 100644 index 0000000..2f48bc0 --- /dev/null +++ b/Assets/Scripts/Editor/AmbientCg/AmbientCgImporterWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b732bd5ab747dd54cb08525c089a1d8f \ No newline at end of file diff --git a/Assets/Scripts/Editor/PolyHaven/PolyHavenApi.cs b/Assets/Scripts/Editor/PolyHaven/PolyHavenApi.cs index a17a840..697b389 100644 --- a/Assets/Scripts/Editor/PolyHaven/PolyHavenApi.cs +++ b/Assets/Scripts/Editor/PolyHaven/PolyHavenApi.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; +using Chameleon.Editor.WebAssets; using Newtonsoft.Json.Linq; -using UnityEditor; using UnityEngine; -using UnityEngine.Networking; namespace Chameleon.Editor.PolyHaven { @@ -34,36 +33,25 @@ namespace Chameleon.Editor.PolyHaven public string[] Categories = Array.Empty(); } - /// One catalog entry from /assets (lightweight, for the browser grid). - public sealed class PolyHavenAsset - { - public string Slug; - public string Name; - public string ThumbnailUrl; - public string[] Categories = Array.Empty(); - public string[] Tags = Array.Empty(); - public string Search; // pre-lowered "name slug categories tags" haystack - } - /// Parsed /files/{slug}: MapType -> resolution("2k") -> best file. public sealed class PolyHavenFiles { - public readonly Dictionary> Maps = - new Dictionary>(); + 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(PolyHavenMapType type) => Maps.ContainsKey(type); + public bool Has(PbrMapType type) => Maps.ContainsKey(type); - public bool TryGet(PolyHavenMapType type, string res, out PolyHavenFile file) + 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(PolyHavenMapType type, string res, out PolyHavenFile file, out string usedRes) + public bool TryGetWithFallback(PbrMapType type, string res, out PolyHavenFile file, out string usedRes) { file = default; usedRes = null; @@ -71,11 +59,11 @@ namespace Chameleon.Editor.PolyHaven return false; if (byRes.TryGetValue(res, out file)) { usedRes = res; return true; } - int target = ResolutionRank(res); + int target = ImportUtil.ResolutionRank(res); int bestRank = -1; foreach (var kv in byRes) { - int rank = ResolutionRank(kv.Key); + int rank = ImportUtil.ResolutionRank(kv.Key); if (rank <= target && rank > bestRank) { bestRank = rank; @@ -87,7 +75,7 @@ namespace Chameleon.Editor.PolyHaven { foreach (var kv in byRes) { - int rank = ResolutionRank(kv.Key); + int rank = ImportUtil.ResolutionRank(kv.Key); if (bestRank == -1 || rank < bestRank) { bestRank = rank; @@ -98,23 +86,27 @@ namespace Chameleon.Editor.PolyHaven } return usedRes != null; } - - public static int ResolutionRank(string res) - { - if (string.IsNullOrEmpty(res)) return int.MaxValue; - var digits = res.TrimEnd('k', 'K'); - return int.TryParse(digits, out var n) ? n : int.MaxValue; - } } /// - /// Async client for the public Poly Haven API (same endpoints the Blender/UE5 plugins use). - /// All requests poll on EditorApplication.update — never blocks the editor. + /// Async client for the public Poly Haven API (same endpoints the Blender/UE5 + /// plugins use). Transport is the shared non-blocking EditorWebRequest. /// public static class PolyHavenApi { private const string ApiBase = "https://api.polyhaven.com"; - private const string UserAgent = "Chameleon-Unity-PolyHavenImporter/1.0"; + + // Poly Haven's dynamic file-tree keys per map type. + private static readonly (PbrMapType type, string apiKey)[] ApiKeys = + { + (PbrMapType.Diffuse, "Diffuse"), + (PbrMapType.NormalGL, "nor_gl"), + (PbrMapType.Rough, "Rough"), + (PbrMapType.AO, "AO"), + (PbrMapType.Metal, "Metal"), + (PbrMapType.Displacement, "Displacement"), + (PbrMapType.Arm, "arm"), + }; // Best format preference per map: lossless where it matters, jpg otherwise. private static readonly string[] LosslessPref = { "png", "exr", "jpg" }; @@ -147,13 +139,11 @@ namespace Chameleon.Editor.PolyHaven public static void FetchInfo(string slug, Action onDone, Action onError) { - var req = UnityWebRequest.Get($"{ApiBase}/info/{slug}"); - req.SetRequestHeader("User-Agent", UserAgent); - Send(req, () => + EditorWebRequest.GetText($"{ApiBase}/info/{slug}", text => { try { - var o = JObject.Parse(req.downloadHandler.text); + var o = JObject.Parse(text); var info = new PolyHavenInfo { Slug = slug, @@ -173,32 +163,29 @@ namespace Chameleon.Editor.PolyHaven }, onError); } - /// Loads the full texture catalog (keyed by slug) for the browser grid. - public static void FetchAssets(string type, Action> onDone, Action onError) + /// Loads the full texture catalog as browser-grid entries. + public static void FetchAssets(string type, Action> onDone, Action onError) { - var req = UnityWebRequest.Get($"{ApiBase}/assets?t={type}"); - req.SetRequestHeader("User-Agent", UserAgent); - Send(req, () => + EditorWebRequest.GetText($"{ApiBase}/assets?t={type}", text => { try { - var root = JObject.Parse(req.downloadHandler.text); - var list = new List(root.Count); + var root = JObject.Parse(text); + var list = new List(root.Count); foreach (var prop in root) { if (prop.Value is not JObject o) continue; - var a = new PolyHavenAsset + string name = (string)o["name"] ?? prop.Key; + var e = new AssetBrowserGrid.Entry { - Slug = prop.Key, - Name = (string)o["name"] ?? prop.Key, + Key = prop.Key, + Name = name, ThumbnailUrl = (string)o["thumbnail_url"] ?? ThumbnailUrl(prop.Key), - Categories = ToStringArray(o["categories"]), - Tags = ToStringArray(o["tags"]), }; - a.Search = (a.Name + " " + a.Slug + " " + - string.Join(" ", a.Categories) + " " + - string.Join(" ", a.Tags)).ToLowerInvariant(); - list.Add(a); + e.Search = (name + " " + prop.Key + " " + + JoinStrings(o["categories"]) + " " + + JoinStrings(o["tags"])).ToLowerInvariant(); + list.Add(e); } list.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase)); onDone?.Invoke(list); @@ -209,52 +196,25 @@ namespace Chameleon.Editor.PolyHaven public static void FetchFiles(string slug, Action onDone, Action onError) { - var req = UnityWebRequest.Get($"{ApiBase}/files/{slug}"); - req.SetRequestHeader("User-Agent", UserAgent); - Send(req, () => + EditorWebRequest.GetText($"{ApiBase}/files/{slug}", text => { - try { onDone?.Invoke(ParseFiles(req.downloadHandler.text)); } + try { onDone?.Invoke(ParseFiles(text)); } catch (Exception e) { onError?.Invoke($"Files parse failed: {e.Message}"); } }, onError); } - public static void FetchThumbnail(string url, Action onDone, Action onError) - { - var req = UnityWebRequestTexture.GetTexture(url); - req.SetRequestHeader("User-Agent", UserAgent); - Send(req, () => - { - var tex = DownloadHandlerTexture.GetContent(req); - // Survive play-mode enter/exit cleanup — otherwise Unity destroys these - // loose runtime textures and the browser previews go blank. - if (tex) - tex.hideFlags = HideFlags.HideAndDontSave; - onDone?.Invoke(tex); - }, onError); - } - - /// Streams a file straight to disk (no RAM peak on big EXR/8K files). - public static void DownloadFile(PolyHavenFile file, string destPath, Action onDone, - Action onError, Action onProgress = null) - { - var req = new UnityWebRequest(file.Url, UnityWebRequest.kHttpVerbGET); - req.downloadHandler = new DownloadHandlerFile(destPath) { removeFileOnAbort = true }; - req.SetRequestHeader("User-Agent", UserAgent); - Send(req, () => onDone?.Invoke(), onError, onProgress); - } - // ---- Parsing ----------------------------------------------------------- - private static string[] ToStringArray(JToken token) + private static string JoinStrings(JToken token) { - if (token is not JArray arr) return Array.Empty(); + if (token is not JArray arr) return ""; var list = new List(arr.Count); foreach (var t in arr) { var s = (string)t; if (!string.IsNullOrEmpty(s)) list.Add(s); } - return list.ToArray(); + return string.Join(" ", list); } private static PolyHavenFiles ParseFiles(string json) @@ -263,12 +223,13 @@ namespace Chameleon.Editor.PolyHaven var result = new PolyHavenFiles(); var resSet = new HashSet(); - foreach (var meta in PolyHavenMapInfo.All) + foreach (var (type, apiKey) in ApiKeys) { - if (root[meta.ApiKey] is not JObject byRes) + if (root[apiKey] is not JObject byRes) continue; - var pref = (meta.IsNormal || meta.Type == PolyHavenMapType.Displacement) + var meta = PbrMapInfo.Get(type); + var pref = (meta.IsNormal || type == PbrMapType.Displacement) ? LosslessPref : LossyPref; @@ -298,48 +259,13 @@ namespace Chameleon.Editor.PolyHaven } if (perRes.Count > 0) - result.Maps[meta.Type] = perRes; + result.Maps[type] = perRes; } var resList = new List(resSet); - resList.Sort((x, y) => PolyHavenFiles.ResolutionRank(x).CompareTo(PolyHavenFiles.ResolutionRank(y))); + resList.Sort((x, y) => ImportUtil.ResolutionRank(x).CompareTo(ImportUtil.ResolutionRank(y))); result.Resolutions = resList; return result; } - - // ---- Non-blocking transport ------------------------------------------- - - private static void Send(UnityWebRequest req, Action onSuccess, Action onError, - Action onProgress = null) - { - var op = req.SendWebRequest(); - void Tick() - { - if (onProgress != null && !op.isDone) - onProgress(req.downloadProgress); - - if (!op.isDone) - return; - - EditorApplication.update -= Tick; - try - { - if (req.result != UnityWebRequest.Result.Success) - { - onError?.Invoke($"{req.error} ({req.url})"); - } - else - { - onProgress?.Invoke(1f); - onSuccess?.Invoke(); - } - } - finally - { - req.Dispose(); - } - } - EditorApplication.update += Tick; - } } } diff --git a/Assets/Scripts/Editor/PolyHaven/PolyHavenImporterWindow.cs b/Assets/Scripts/Editor/PolyHaven/PolyHavenImporterWindow.cs index 28b11d0..fd0e773 100644 --- a/Assets/Scripts/Editor/PolyHaven/PolyHavenImporterWindow.cs +++ b/Assets/Scripts/Editor/PolyHaven/PolyHavenImporterWindow.cs @@ -1,16 +1,15 @@ -using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Chameleon.Editor.WebAssets; using UnityEditor; using UnityEngine; namespace Chameleon.Editor.PolyHaven { /// - /// Editor window: paste a Poly Haven asset URL, fetch its maps, pick resolution + + /// 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}/. - /// No browser — asset selection stays on polyhaven.com. /// public sealed class PolyHavenImporterWindow : EditorWindow { @@ -22,29 +21,21 @@ namespace Chameleon.Editor.PolyHaven private PolyHavenFiles _files; private Texture2D _thumbnail; private string _selectedRes = DefaultRes; - private readonly Dictionary _mapEnabled = new Dictionary(); + private readonly Dictionary _mapEnabled = new Dictionary(); private bool _busy; private string _status = ""; private Vector2 _scroll; // ---- Browser ---------------------------------------------------------- - private const int MaxResults = 150; - private const int MaxThumbInFlight = 6; // Catalog cached for the whole session (survives window close, cleared on domain reload). - private static List s_catalog; + private static List s_catalog; + private AssetBrowserGrid _grid; private bool _browseFoldout = true; private bool _autoLoadTried; private bool _catalogLoading; - private string _searchQuery = ""; - private readonly List _filtered = new List(); - private Vector2 _gridScroll; - - private readonly Dictionary _thumbs = new Dictionary(); - private readonly HashSet _thumbRequested = new HashSet(); - private int _thumbsInFlight; [MenuItem("Tools/Poly Haven Importer")] public static void Open() @@ -55,11 +46,13 @@ namespace Chameleon.Editor.PolyHaven private void OnEnable() { + _grid = new AssetBrowserGrid(Repaint); + // Catalog may already be cached from a previous window instance this session. if (s_catalog != null) { _autoLoadTried = true; - ApplyFilter(); + _grid.SetCatalog(s_catalog); } } @@ -134,7 +127,7 @@ namespace Chameleon.Editor.PolyHaven EditorGUILayout.Space(); EditorGUILayout.LabelField("Maps", EditorStyles.miniBoldLabel); - foreach (var meta in PolyHavenMapInfo.All) + foreach (var meta in PbrMapInfo.All) { if (!_files.Has(meta.Type)) continue; @@ -153,11 +146,11 @@ namespace Chameleon.Editor.PolyHaven private bool AnyMapSelected() { - foreach (var meta in PolyHavenMapInfo.All) + foreach (var meta in PbrMapInfo.All) if (_files.Has(meta.Type) && _mapEnabled.TryGetValue(meta.Type, out var v) && v) return true; // Fall back to defaults if the user hasn't toggled anything yet. - foreach (var meta in PolyHavenMapInfo.All) + foreach (var meta in PbrMapInfo.All) if (_files.Has(meta.Type) && meta.DefaultEnabled && !_mapEnabled.ContainsKey(meta.Type)) return true; return false; @@ -173,7 +166,7 @@ namespace Chameleon.Editor.PolyHaven using (new EditorGUI.DisabledScope(_busy)) { - if (s_catalog == null) + if (!_grid.HasCatalog) { // Auto-load once when the browser is first opened; still offer a manual retry. if (!_catalogLoading && !_autoLoadTried) @@ -193,105 +186,13 @@ namespace Chameleon.Editor.PolyHaven return; } - using (new EditorGUILayout.HorizontalScope()) - { - EditorGUI.BeginChangeCheck(); - _searchQuery = EditorGUILayout.TextField("Search", _searchQuery); - if (EditorGUI.EndChangeCheck()) - ApplyFilter(); - if (GUILayout.Button("↻", GUILayout.Width(26))) - LoadCatalog(); - } - - int shown = Mathf.Min(_filtered.Count, MaxResults); - string cap = _filtered.Count > MaxResults ? $", showing first {MaxResults}" : ""; - EditorGUILayout.LabelField( - $"{_filtered.Count} match(es){cap} of {s_catalog.Count} textures.", - EditorStyles.miniLabel); - - DrawGrid(shown); + _grid.Draw(SelectAsset, LoadCatalog, "textures"); } } - private void DrawGrid(int count) + private void SelectAsset(AssetBrowserGrid.Entry a) { - const float cell = 104f; - const float pad = 6f; - float viewW = EditorGUIUtility.currentViewWidth - 24f; - int cols = Mathf.Max(1, Mathf.FloorToInt((viewW + pad) / (cell + pad))); - - _gridScroll = EditorGUILayout.BeginScrollView(_gridScroll, GUILayout.Height(320)); - - int rows = Mathf.CeilToInt(count / (float)cols); - for (int row = 0; row < rows; row++) - { - using (new EditorGUILayout.HorizontalScope()) - { - for (int c = 0; c < cols; c++) - { - int i = row * cols + c; - if (i >= count) - { - GUILayout.FlexibleSpace(); - break; - } - DrawCell(_filtered[i], cell); - } - } - } - - EditorGUILayout.EndScrollView(); - } - - private void DrawCell(PolyHavenAsset a, float size) - { - using (new EditorGUILayout.VerticalScope(GUILayout.Width(size))) - { - var thumbRect = GUILayoutUtility.GetRect(size, size - 22f, - GUILayout.Width(size), GUILayout.Height(size - 22f)); - - var tex = GetThumb(a); - if (tex) - GUI.DrawTexture(thumbRect, tex, ScaleMode.ScaleToFit); - else - EditorGUI.DrawRect(thumbRect, new Color(0f, 0f, 0f, 0.15f)); - - // Transparent hit target over the thumbnail. - if (GUI.Button(thumbRect, new GUIContent("", a.Name), GUIStyle.none)) - SelectAsset(a); - - EditorGUILayout.LabelField(new GUIContent(a.Name, a.Name), - EditorStyles.miniLabel, GUILayout.Width(size)); - } - } - - /// Lazy, throttled thumbnail loader — only visible cells request, capped in-flight. - private Texture2D GetThumb(PolyHavenAsset a) - { - if (_thumbs.TryGetValue(a.Slug, out var tex)) - { - if (tex) - return tex; - // Cached texture was destroyed (e.g. play-mode cleanup) — purge and re-request. - _thumbs.Remove(a.Slug); - _thumbRequested.Remove(a.Slug); - } - - // Don't add to _thumbRequested until a slot is free, so it retries next repaint. - if (!_thumbRequested.Contains(a.Slug) && _thumbsInFlight < MaxThumbInFlight) - { - _thumbRequested.Add(a.Slug); - _thumbsInFlight++; - PolyHavenApi.FetchThumbnail(a.ThumbnailUrl, - t => { _thumbs[a.Slug] = t; _thumbsInFlight--; Repaint(); }, - _ => { _thumbsInFlight--; Repaint(); }); // keep as requested: no retry-spam on error - } - return null; - } - - private void SelectAsset(PolyHavenAsset a) - { - _urlInput = "https://polyhaven.com/a/" + a.Slug; + _urlInput = "https://polyhaven.com/a/" + a.Key; GUI.FocusControl(null); Fetch(); // url filled -> fetch, then everything as before } @@ -307,7 +208,7 @@ namespace Chameleon.Editor.PolyHaven { s_catalog = list; _catalogLoading = false; - ApplyFilter(); + _grid.SetCatalog(list); _status = $"Catalog loaded: {list.Count} textures."; Repaint(); }, @@ -320,30 +221,6 @@ namespace Chameleon.Editor.PolyHaven }); } - private void ApplyFilter() - { - _filtered.Clear(); - if (s_catalog == null) - return; - - var q = _searchQuery?.Trim().ToLowerInvariant(); - if (string.IsNullOrEmpty(q)) - { - _filtered.AddRange(s_catalog); - return; - } - - var terms = q.Split((char[])null, StringSplitOptions.RemoveEmptyEntries); - foreach (var a in s_catalog) - { - bool all = true; - foreach (var t in terms) - if (a.Search.IndexOf(t, StringComparison.Ordinal) < 0) { all = false; break; } - if (all) - _filtered.Add(a); - } - } - // ---- Fetch ------------------------------------------------------------- private void Fetch() @@ -367,7 +244,7 @@ namespace Chameleon.Editor.PolyHaven { _info = info; Repaint(); - PolyHavenApi.FetchThumbnail(info.ThumbnailUrl, tex => { _thumbnail = tex; Repaint(); }, _ => { }); + EditorWebRequest.GetTexture(info.ThumbnailUrl, tex => { _thumbnail = tex; Repaint(); }, _ => { }); PolyHavenApi.FetchFiles(slug, files => { @@ -391,8 +268,8 @@ namespace Chameleon.Editor.PolyHaven private void Download() { // Resolve which maps to fetch. - var wanted = new List(); - foreach (var meta in PolyHavenMapInfo.All) + var wanted = 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; @@ -401,39 +278,32 @@ namespace Chameleon.Editor.PolyHaven // 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(PolyHavenMapType.Arm) && !wanted.Contains(PolyHavenMapType.Arm)) + if (_files.Has(PbrMapType.Arm) && !wanted.Contains(PbrMapType.Arm)) { - bool channelMissing = !_files.Has(PolyHavenMapType.Metal) - || !_files.Has(PolyHavenMapType.Rough) - || !_files.Has(PolyHavenMapType.AO); + bool channelMissing = !_files.Has(PbrMapType.Metal) + || !_files.Has(PbrMapType.Rough) + || !_files.Has(PbrMapType.AO); if (channelMissing) - wanted.Add(PolyHavenMapType.Arm); + wanted.Add(PbrMapType.Arm); } if (wanted.Count == 0) return; string displayName = _info.Name; - string folder = $"{RootFolder}/{PolyHavenMaterialBuilder.SanitizeFileName(displayName)}"; + string folder = $"{RootFolder}/{ImportUtil.SanitizeFileName(displayName)}"; - // Folder-collision handling (non-destructive default). - if (AssetDatabase.IsValidFolder(folder)) - { - int choice = EditorUtility.DisplayDialogComplex("Poly Haven", - $"Folder already exists:\n{folder}", "New folder", "Cancel", "Overwrite"); - if (choice == 1) return; // Cancel - if (choice == 0) // New folder - folder = AssetDatabase.GenerateUniqueAssetPath(folder); - // choice == 2 => Overwrite: reuse folder as-is. - } - EnsureFolder(folder); + folder = ImportUtil.ResolveFolderCollision(folder, "Poly Haven"); + if (folder == null) + return; + ImportUtil.EnsureFolder(folder); - int maxSize = ResolutionToMaxSize(_selectedRes); + int maxSize = ImportUtil.ResolutionToMaxSize(_selectedRes); _busy = true; _status = $"Downloading {wanted.Count} map(s) at {_selectedRes.ToUpperInvariant()}…"; Repaint(); - var results = new List(); + var results = new List(); var failures = new List(); int remaining = wanted.Count; @@ -458,7 +328,7 @@ namespace Chameleon.Editor.PolyHaven return; } - PolyHavenMaterialBuilder.Build(folder, displayName, _info.Slug, maxSize, results); + PbrMaterialBuilder.Build(folder, displayName, _info.Slug, maxSize, results); _busy = false; _status = $"Done: {results.Count} map(s) imported, material built in {folder}."; if (failures.Count > 0) @@ -477,14 +347,14 @@ namespace Chameleon.Editor.PolyHaven if (usedRes != _selectedRes) Debug.LogWarning($"[PolyHaven] {type}: {_selectedRes} unavailable, using {usedRes}."); - var meta = PolyHavenMapInfo.Get(type); + var meta = PbrMapInfo.Get(type); string fileName = $"{_info.Slug}_{meta.Suffix}.{file.Format}"; string destPath = $"{folder}/{fileName}"; - PolyHavenApi.DownloadFile(file, Path.GetFullPath(destPath), + EditorWebRequest.DownloadFile(file.Url, Path.GetFullPath(destPath), onDone: () => { - results.Add(new PolyHavenMaterialBuilder.Downloaded { Type = type, AssetPath = destPath }); + results.Add(new PbrMaterialBuilder.Downloaded { Type = type, AssetPath = destPath }); FinishOne(); }, onError: err => @@ -504,31 +374,5 @@ namespace Chameleon.Editor.PolyHaven Debug.LogError("[PolyHaven] " + message); Repaint(); } - - private static int ResolutionToMaxSize(string res) - { - int k = PolyHavenFiles.ResolutionRank(res); - int px = k * 1024; - // Clamp to a valid Unity max texture size (power of two up to 16384). - int[] valid = { 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384 }; - foreach (var v in valid) - if (px <= v) return v; - return 16384; - } - - private static void EnsureFolder(string folder) - { - if (AssetDatabase.IsValidFolder(folder)) - return; - var parts = folder.Split('/'); - string cur = parts[0]; // "Assets" - for (int i = 1; i < parts.Length; i++) - { - string next = $"{cur}/{parts[i]}"; - if (!AssetDatabase.IsValidFolder(next)) - AssetDatabase.CreateFolder(cur, parts[i]); - cur = next; - } - } } } diff --git a/Assets/Scripts/Editor/PolyHaven/PolyHavenMapType.cs b/Assets/Scripts/Editor/PolyHaven/PolyHavenMapType.cs deleted file mode 100644 index b6eebb4..0000000 --- a/Assets/Scripts/Editor/PolyHaven/PolyHavenMapType.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Collections.Generic; - -namespace Chameleon.Editor.PolyHaven -{ - /// - /// The PBR map types the importer understands. Values map onto Poly Haven's - /// dynamic file-tree keys (e.g. "nor_gl", "Rough"). "Arm" is the packed - /// AO/Rough/Metal fallback some assets ship instead of separate maps. - /// - public enum PolyHavenMapType - { - Diffuse, - NormalGL, - Rough, - AO, - Metal, - Displacement, - Arm, - } - - /// Static per-map metadata: API key, file suffix, colour space, defaults. - public static class PolyHavenMapInfo - { - public readonly struct Meta - { - public readonly PolyHavenMapType Type; - public readonly string ApiKey; // exact key in the /files JSON - public readonly string Suffix; // file name suffix on disk - public readonly bool SRGB; // true => colour, false => linear data - public readonly bool IsNormal; // import as TextureImporterType.NormalMap - public readonly bool DefaultEnabled; // pre-checked in the UI when available - public readonly string Label; // UI label - - public Meta(PolyHavenMapType type, string apiKey, string suffix, bool srgb, - bool isNormal, bool defaultEnabled, string label) - { - Type = type; - ApiKey = apiKey; - Suffix = suffix; - SRGB = srgb; - IsNormal = isNormal; - DefaultEnabled = defaultEnabled; - Label = label; - } - } - - // Order here defines UI order. - public static readonly Meta[] All = - { - new Meta(PolyHavenMapType.Diffuse, "Diffuse", "diffuse", true, false, true, "Diffuse (Albedo)"), - new Meta(PolyHavenMapType.NormalGL, "nor_gl", "normal", false, true, true, "Normal (OpenGL)"), - new Meta(PolyHavenMapType.Rough, "Rough", "roughness", false, false, true, "Roughness"), - new Meta(PolyHavenMapType.AO, "AO", "ao", false, false, true, "Ambient Occlusion"), - new Meta(PolyHavenMapType.Metal, "Metal", "metallic", false, false, true, "Metallic"), - new Meta(PolyHavenMapType.Displacement, "Displacement", "height", false, false, false, "Displacement (Height)"), - new Meta(PolyHavenMapType.Arm, "arm", "arm", false, false, false, "ARM (packed AO/Rough/Metal)"), - }; - - private static readonly Dictionary ByType = BuildLookup(); - - private static Dictionary BuildLookup() - { - var d = new Dictionary(All.Length); - foreach (var m in All) - d[m.Type] = m; - return d; - } - - public static Meta Get(PolyHavenMapType type) => ByType[type]; - } -} diff --git a/Assets/Scripts/Editor/PolyHaven/PolyHavenMapType.cs.meta b/Assets/Scripts/Editor/PolyHaven/PolyHavenMapType.cs.meta deleted file mode 100644 index 1ead6ef..0000000 --- a/Assets/Scripts/Editor/PolyHaven/PolyHavenMapType.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: d47448fcb2873654d8d7b4369c6c5da4 \ No newline at end of file diff --git a/Assets/Scripts/Editor/PolyHaven/PolyHavenMaterialBuilder.cs.meta b/Assets/Scripts/Editor/PolyHaven/PolyHavenMaterialBuilder.cs.meta deleted file mode 100644 index ff5caf2..0000000 --- a/Assets/Scripts/Editor/PolyHaven/PolyHavenMaterialBuilder.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 3c242963f05cc5545b0034908b5807f1 \ No newline at end of file diff --git a/Assets/Scripts/Editor/WebAssets.meta b/Assets/Scripts/Editor/WebAssets.meta new file mode 100644 index 0000000..699a29c --- /dev/null +++ b/Assets/Scripts/Editor/WebAssets.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: be7bdf4fbe283ec4cae8f0ec0c6a9de2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/WebAssets/AssetBrowserGrid.cs b/Assets/Scripts/Editor/WebAssets/AssetBrowserGrid.cs new file mode 100644 index 0000000..4cce5f5 --- /dev/null +++ b/Assets/Scripts/Editor/WebAssets/AssetBrowserGrid.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace Chameleon.Editor.WebAssets +{ + /// + /// Reusable catalog browser for importer windows: search field + responsive + /// thumbnail grid with lazy, throttled thumbnail loading. The host window owns + /// the catalog data and reacts to cell clicks via the onSelect callback. + /// + public sealed class AssetBrowserGrid + { + public sealed class Entry + { + public string Key; // slug / asset id + public string Name; // display name (cell label) + public string ThumbnailUrl; + public string Search; // pre-lowered haystack for filtering + } + + private const int MaxResults = 150; + private const int MaxThumbInFlight = 6; + + private readonly Action _repaint; + + private List _catalog; + private readonly List _filtered = new List(); + private string _searchQuery = ""; + private Vector2 _gridScroll; + + private readonly Dictionary _thumbs = new Dictionary(); + private readonly HashSet _thumbRequested = new HashSet(); + private int _thumbsInFlight; + + public AssetBrowserGrid(Action repaint) + { + _repaint = repaint; + } + + public bool HasCatalog => _catalog != null; + + public void SetCatalog(List catalog) + { + _catalog = catalog; + ApplyFilter(); + } + + /// Draws search field, match count and the grid. Call only when HasCatalog. + public void Draw(Action onSelect, Action reloadCatalog, string itemNoun, float gridHeight = 320f) + { + using (new EditorGUILayout.HorizontalScope()) + { + EditorGUI.BeginChangeCheck(); + _searchQuery = EditorGUILayout.TextField("Search", _searchQuery); + if (EditorGUI.EndChangeCheck()) + ApplyFilter(); + if (reloadCatalog != null && GUILayout.Button("↻", GUILayout.Width(26))) + reloadCatalog(); + } + + int shown = Mathf.Min(_filtered.Count, MaxResults); + string cap = _filtered.Count > MaxResults ? $", showing first {MaxResults}" : ""; + EditorGUILayout.LabelField( + $"{_filtered.Count} match(es){cap} of {_catalog.Count} {itemNoun}.", + EditorStyles.miniLabel); + + DrawGrid(shown, onSelect, gridHeight); + } + + private void DrawGrid(int count, Action onSelect, float gridHeight) + { + const float cell = 104f; + const float pad = 6f; + float viewW = EditorGUIUtility.currentViewWidth - 24f; + int cols = Mathf.Max(1, Mathf.FloorToInt((viewW + pad) / (cell + pad))); + + _gridScroll = EditorGUILayout.BeginScrollView(_gridScroll, GUILayout.Height(gridHeight)); + + int rows = Mathf.CeilToInt(count / (float)cols); + for (int row = 0; row < rows; row++) + { + using (new EditorGUILayout.HorizontalScope()) + { + for (int c = 0; c < cols; c++) + { + int i = row * cols + c; + if (i >= count) + { + GUILayout.FlexibleSpace(); + break; + } + DrawCell(_filtered[i], cell, onSelect); + } + } + } + + EditorGUILayout.EndScrollView(); + } + + private void DrawCell(Entry a, float size, Action onSelect) + { + using (new EditorGUILayout.VerticalScope(GUILayout.Width(size))) + { + var thumbRect = GUILayoutUtility.GetRect(size, size - 22f, + GUILayout.Width(size), GUILayout.Height(size - 22f)); + + var tex = GetThumb(a); + if (tex) + GUI.DrawTexture(thumbRect, tex, ScaleMode.ScaleToFit); + else + EditorGUI.DrawRect(thumbRect, new Color(0f, 0f, 0f, 0.15f)); + + // Transparent hit target over the thumbnail. + if (GUI.Button(thumbRect, new GUIContent("", a.Name), GUIStyle.none)) + onSelect?.Invoke(a); + + EditorGUILayout.LabelField(new GUIContent(a.Name, a.Name), + EditorStyles.miniLabel, GUILayout.Width(size)); + } + } + + /// Lazy, throttled thumbnail loader — only visible cells request, capped in-flight. + private Texture2D GetThumb(Entry a) + { + if (_thumbs.TryGetValue(a.Key, out var tex)) + { + if (tex) + return tex; + // Cached texture was destroyed (e.g. play-mode cleanup) — purge and re-request. + _thumbs.Remove(a.Key); + _thumbRequested.Remove(a.Key); + } + + // Don't add to _thumbRequested until a slot is free, so it retries next repaint. + if (!_thumbRequested.Contains(a.Key) && _thumbsInFlight < MaxThumbInFlight) + { + _thumbRequested.Add(a.Key); + _thumbsInFlight++; + EditorWebRequest.GetTexture(a.ThumbnailUrl, + t => { _thumbs[a.Key] = t; _thumbsInFlight--; _repaint?.Invoke(); }, + _ => { _thumbsInFlight--; _repaint?.Invoke(); }); // no retry-spam on error + } + return null; + } + + private void ApplyFilter() + { + _filtered.Clear(); + if (_catalog == null) + return; + + var q = _searchQuery?.Trim().ToLowerInvariant(); + if (string.IsNullOrEmpty(q)) + { + _filtered.AddRange(_catalog); + return; + } + + var terms = q.Split((char[])null, StringSplitOptions.RemoveEmptyEntries); + foreach (var a in _catalog) + { + bool all = true; + foreach (var t in terms) + if (a.Search.IndexOf(t, StringComparison.Ordinal) < 0) { all = false; break; } + if (all) + _filtered.Add(a); + } + } + } +} diff --git a/Assets/Scripts/Editor/WebAssets/AssetBrowserGrid.cs.meta b/Assets/Scripts/Editor/WebAssets/AssetBrowserGrid.cs.meta new file mode 100644 index 0000000..ce46ca6 --- /dev/null +++ b/Assets/Scripts/Editor/WebAssets/AssetBrowserGrid.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 464e6d1b4d5744241ac0dcdfc3d7cf78 \ No newline at end of file diff --git a/Assets/Scripts/Editor/WebAssets/EditorWebRequest.cs b/Assets/Scripts/Editor/WebAssets/EditorWebRequest.cs new file mode 100644 index 0000000..c6b990d --- /dev/null +++ b/Assets/Scripts/Editor/WebAssets/EditorWebRequest.cs @@ -0,0 +1,84 @@ +using System; +using UnityEditor; +using UnityEngine; +using UnityEngine.Networking; + +namespace Chameleon.Editor.WebAssets +{ + /// + /// Non-blocking UnityWebRequest transport for editor tools. All requests poll on + /// EditorApplication.update — never blocks the editor. Shared by the Poly Haven + /// and ambientCG importers. + /// + public static class EditorWebRequest + { + public const string UserAgent = "Chameleon-Unity-AssetImporter/1.0"; + + /// GET a URL and return the response body as text. + public static void GetText(string url, Action onDone, Action onError) + { + var req = UnityWebRequest.Get(url); + req.SetRequestHeader("User-Agent", UserAgent); + Send(req, () => onDone?.Invoke(req.downloadHandler.text), onError); + } + + /// Fetches an image as a Texture2D (browser thumbnails, previews). + public static void GetTexture(string url, Action onDone, Action onError) + { + var req = UnityWebRequestTexture.GetTexture(url); + req.SetRequestHeader("User-Agent", UserAgent); + Send(req, () => + { + var tex = DownloadHandlerTexture.GetContent(req); + // Survive play-mode enter/exit cleanup — otherwise Unity destroys these + // loose runtime textures and browser previews go blank. + if (tex) + tex.hideFlags = HideFlags.HideAndDontSave; + onDone?.Invoke(tex); + }, onError); + } + + /// Streams a file straight to disk (no RAM peak on big downloads). + public static void DownloadFile(string url, string destPath, Action onDone, + Action onError, Action onProgress = null) + { + var req = new UnityWebRequest(url, UnityWebRequest.kHttpVerbGET); + req.downloadHandler = new DownloadHandlerFile(destPath) { removeFileOnAbort = true }; + req.SetRequestHeader("User-Agent", UserAgent); + Send(req, () => onDone?.Invoke(), onError, onProgress); + } + + public static void Send(UnityWebRequest req, Action onSuccess, Action onError, + Action onProgress = null) + { + var op = req.SendWebRequest(); + void Tick() + { + if (onProgress != null && !op.isDone) + onProgress(req.downloadProgress); + + if (!op.isDone) + return; + + EditorApplication.update -= Tick; + try + { + if (req.result != UnityWebRequest.Result.Success) + { + onError?.Invoke($"{req.error} ({req.url})"); + } + else + { + onProgress?.Invoke(1f); + onSuccess?.Invoke(); + } + } + finally + { + req.Dispose(); + } + } + EditorApplication.update += Tick; + } + } +} diff --git a/Assets/Scripts/Editor/WebAssets/EditorWebRequest.cs.meta b/Assets/Scripts/Editor/WebAssets/EditorWebRequest.cs.meta new file mode 100644 index 0000000..124a578 --- /dev/null +++ b/Assets/Scripts/Editor/WebAssets/EditorWebRequest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: edbbb3505f143824590b8d77228d2407 \ No newline at end of file diff --git a/Assets/Scripts/Editor/WebAssets/ImportUtil.cs b/Assets/Scripts/Editor/WebAssets/ImportUtil.cs new file mode 100644 index 0000000..1202011 --- /dev/null +++ b/Assets/Scripts/Editor/WebAssets/ImportUtil.cs @@ -0,0 +1,66 @@ +using System.IO; +using UnityEditor; + +namespace Chameleon.Editor.WebAssets +{ + /// Small shared helpers for the web-asset importer windows. + public static class ImportUtil + { + public static string SanitizeFileName(string name) + { + foreach (var c in Path.GetInvalidFileNameChars()) + name = name.Replace(c, ' '); + return name.Trim(); + } + + /// "2k"/"2K" -> 2. Unknown strings sort last. + public static int ResolutionRank(string res) + { + if (string.IsNullOrEmpty(res)) return int.MaxValue; + var digits = res.TrimEnd('k', 'K'); + return int.TryParse(digits, out var n) ? n : int.MaxValue; + } + + /// Maps a "2k"-style resolution to a valid Unity max texture size. + public static int ResolutionToMaxSize(string res) + { + int k = ResolutionRank(res); + int px = k * 1024; + int[] valid = { 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384 }; + foreach (var v in valid) + if (px <= v) return v; + return 16384; + } + + public static void EnsureFolder(string folder) + { + if (AssetDatabase.IsValidFolder(folder)) + return; + var parts = folder.Split('/'); + string cur = parts[0]; // "Assets" + for (int i = 1; i < parts.Length; i++) + { + string next = $"{cur}/{parts[i]}"; + if (!AssetDatabase.IsValidFolder(next)) + AssetDatabase.CreateFolder(cur, parts[i]); + cur = next; + } + } + + /// + /// Non-destructive folder-collision handling: if the folder exists, asks the user + /// (New folder / Cancel / Overwrite). Returns the folder to use, or null on cancel. + /// + public static string ResolveFolderCollision(string folder, string dialogTitle) + { + if (!AssetDatabase.IsValidFolder(folder)) + return folder; + + int choice = EditorUtility.DisplayDialogComplex(dialogTitle, + $"Folder already exists:\n{folder}", "New folder", "Cancel", "Overwrite"); + if (choice == 1) return null; // Cancel + if (choice == 0) return AssetDatabase.GenerateUniqueAssetPath(folder); // New folder + return folder; // Overwrite + } + } +} diff --git a/Assets/Scripts/Editor/WebAssets/ImportUtil.cs.meta b/Assets/Scripts/Editor/WebAssets/ImportUtil.cs.meta new file mode 100644 index 0000000..c7b195b --- /dev/null +++ b/Assets/Scripts/Editor/WebAssets/ImportUtil.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 22c722d093b45124fb66addc8fa83211 \ No newline at end of file diff --git a/Assets/Scripts/Editor/WebAssets/PbrMapType.cs b/Assets/Scripts/Editor/WebAssets/PbrMapType.cs new file mode 100644 index 0000000..4406467 --- /dev/null +++ b/Assets/Scripts/Editor/WebAssets/PbrMapType.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; + +namespace Chameleon.Editor.WebAssets +{ + /// + /// The PBR map types the importers understand, provider-agnostic. "Arm" is the + /// packed AO/Rough/Metal map some Poly Haven assets ship instead of separate maps. + /// + public enum PbrMapType + { + Diffuse, + NormalGL, + Rough, + AO, + Metal, + Displacement, + Arm, + } + + /// Static per-map metadata: file suffix, colour space, UI defaults. + public static class PbrMapInfo + { + public readonly struct Meta + { + public readonly PbrMapType Type; + public readonly string Suffix; // file name suffix on disk + public readonly bool SRGB; // true => colour, false => linear data + public readonly bool IsNormal; // import as TextureImporterType.NormalMap + public readonly bool DefaultEnabled; // pre-checked in the UI when available + public readonly string Label; // UI label + + public Meta(PbrMapType type, string suffix, bool srgb, bool isNormal, + bool defaultEnabled, string label) + { + Type = type; + Suffix = suffix; + SRGB = srgb; + IsNormal = isNormal; + DefaultEnabled = defaultEnabled; + Label = label; + } + } + + // Order here defines UI order. + public static readonly Meta[] All = + { + new Meta(PbrMapType.Diffuse, "diffuse", true, false, true, "Diffuse (Albedo)"), + new Meta(PbrMapType.NormalGL, "normal", false, true, true, "Normal (OpenGL)"), + new Meta(PbrMapType.Rough, "roughness", false, false, true, "Roughness"), + new Meta(PbrMapType.AO, "ao", false, false, true, "Ambient Occlusion"), + new Meta(PbrMapType.Metal, "metallic", false, false, true, "Metallic"), + new Meta(PbrMapType.Displacement, "height", false, false, false, "Displacement (Height)"), + new Meta(PbrMapType.Arm, "arm", false, false, false, "ARM (packed AO/Rough/Metal)"), + }; + + private static readonly Dictionary ByType = BuildLookup(); + + private static Dictionary BuildLookup() + { + var d = new Dictionary(All.Length); + foreach (var m in All) + d[m.Type] = m; + return d; + } + + public static Meta Get(PbrMapType type) => ByType[type]; + } +} diff --git a/Assets/Scripts/Editor/WebAssets/PbrMapType.cs.meta b/Assets/Scripts/Editor/WebAssets/PbrMapType.cs.meta new file mode 100644 index 0000000..5946e52 --- /dev/null +++ b/Assets/Scripts/Editor/WebAssets/PbrMapType.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6e3acec0a249ed549a95141ea60978bc \ No newline at end of file diff --git a/Assets/Scripts/Editor/PolyHaven/PolyHavenMaterialBuilder.cs b/Assets/Scripts/Editor/WebAssets/PbrMaterialBuilder.cs similarity index 84% rename from Assets/Scripts/Editor/PolyHaven/PolyHavenMaterialBuilder.cs rename to Assets/Scripts/Editor/WebAssets/PbrMaterialBuilder.cs index 158f6e7..087f29c 100644 --- a/Assets/Scripts/Editor/PolyHaven/PolyHavenMaterialBuilder.cs +++ b/Assets/Scripts/Editor/WebAssets/PbrMaterialBuilder.cs @@ -3,26 +3,26 @@ using System.IO; using UnityEditor; using UnityEngine; -namespace Chameleon.Editor.PolyHaven +namespace Chameleon.Editor.WebAssets { /// - /// Turns downloaded Poly Haven maps into a ready URP/Lit material: - /// correct import settings per map, Roughness→Smoothness repacking into a - /// MetallicGloss texture, and wired-up keywords. + /// Turns downloaded PBR maps (Poly Haven, ambientCG, ...) into a ready URP/Lit + /// material: correct import settings per map, Roughness→Smoothness repacking into + /// a MetallicGloss texture, and wired-up keywords. /// - public static class PolyHavenMaterialBuilder + public static class PbrMaterialBuilder { /// A map that was successfully downloaded, ready to import. public struct Downloaded { - public PolyHavenMapType Type; + public PbrMapType Type; public string AssetPath; // "Assets/Poly Haven/.../foo_diffuse.jpg" } public static void Build(string folder, string displayName, string slug, int maxSize, List maps) { - var byType = new Dictionary(); + var byType = new Dictionary(); foreach (var m in maps) byType[m.Type] = m.AssetPath; @@ -32,9 +32,9 @@ namespace Chameleon.Editor.PolyHaven // 2) Build a MetallicSmoothness texture. Rough/Metal come from the dedicated // maps when present, otherwise from the packed ARM map (G=rough, B=metal). - byType.TryGetValue(PolyHavenMapType.Arm, out var armPath); - byType.TryGetValue(PolyHavenMapType.Rough, out var roughPath); - byType.TryGetValue(PolyHavenMapType.Metal, out var metalPath); + byType.TryGetValue(PbrMapType.Arm, out var armPath); + byType.TryGetValue(PbrMapType.Rough, out var roughPath); + byType.TryGetValue(PbrMapType.Metal, out var metalPath); string metallicGlossPath = null; if (!string.IsNullOrEmpty(roughPath) || !string.IsNullOrEmpty(armPath)) @@ -45,13 +45,13 @@ namespace Chameleon.Editor.PolyHaven if (!string.IsNullOrEmpty(metallicGlossPath) && !string.IsNullOrEmpty(roughPath)) { AssetDatabase.DeleteAsset(roughPath); - byType.Remove(PolyHavenMapType.Rough); - Debug.Log($"[PolyHaven] Deleted consumed roughness source: {roughPath}"); + byType.Remove(PbrMapType.Rough); + Debug.Log($"[PBR Import] Deleted consumed roughness source: {roughPath}"); } // AO comes from the dedicated map, or the ARM red channel as a fallback. string occlusionPath = null; - if (byType.TryGetValue(PolyHavenMapType.AO, out var aoSeparate)) + if (byType.TryGetValue(PbrMapType.AO, out var aoSeparate)) occlusionPath = aoSeparate; else if (!string.IsNullOrEmpty(armPath)) occlusionPath = BuildAoFromArm(folder, slug, armPath, maxSize); @@ -60,19 +60,19 @@ namespace Chameleon.Editor.PolyHaven var shader = Shader.Find("Universal Render Pipeline/Lit"); if (!shader) { - Debug.LogError("[PolyHaven] URP/Lit shader not found — is URP installed?"); + Debug.LogError("[PBR Import] URP/Lit shader not found — is URP installed?"); return; } var mat = new Material(shader) { name = displayName }; - string matPath = Path.Combine(folder, SanitizeFileName(displayName) + ".mat").Replace('\\', '/'); + string matPath = Path.Combine(folder, ImportUtil.SanitizeFileName(displayName) + ".mat").Replace('\\', '/'); matPath = AssetDatabase.GenerateUniqueAssetPath(matPath); AssetDatabase.CreateAsset(mat, matPath); - if (byType.TryGetValue(PolyHavenMapType.Diffuse, out var diffPath)) + if (byType.TryGetValue(PbrMapType.Diffuse, out var diffPath)) mat.SetTexture("_BaseMap", Load(diffPath)); - if (byType.TryGetValue(PolyHavenMapType.NormalGL, out var norPath)) + if (byType.TryGetValue(PbrMapType.NormalGL, out var norPath)) { mat.SetTexture("_BumpMap", Load(norPath)); mat.SetFloat("_BumpScale", 1f); @@ -93,7 +93,7 @@ namespace Chameleon.Editor.PolyHaven mat.EnableKeyword("_OCCLUSIONMAP"); } - if (byType.TryGetValue(PolyHavenMapType.Displacement, out var heightPath)) + if (byType.TryGetValue(PbrMapType.Displacement, out var heightPath)) { mat.SetTexture("_ParallaxMap", Load(heightPath)); mat.SetFloat("_Parallax", 0.03f); @@ -104,18 +104,18 @@ namespace Chameleon.Editor.PolyHaven AssetDatabase.SaveAssets(); EditorGUIUtility.PingObject(mat); Selection.activeObject = mat; - Debug.Log($"[PolyHaven] Material created: {matPath}"); + Debug.Log($"[PBR Import] Material created: {matPath}"); } private static Texture2D Load(string path) => AssetDatabase.LoadAssetAtPath(path); - public static void ApplyImportSettings(string assetPath, PolyHavenMapType type, int maxSize) + public static void ApplyImportSettings(string assetPath, PbrMapType type, int maxSize) { var importer = AssetImporter.GetAtPath(assetPath) as TextureImporter; if (!importer) return; - var meta = PolyHavenMapInfo.Get(type); + var meta = PbrMapInfo.Get(type); importer.textureType = meta.IsNormal ? TextureImporterType.NormalMap : TextureImporterType.Default; if (!meta.IsNormal) importer.sRGBTexture = meta.SRGB; @@ -252,12 +252,5 @@ namespace Chameleon.Editor.PolyHaven } return tex; } - - public static string SanitizeFileName(string name) - { - foreach (var c in Path.GetInvalidFileNameChars()) - name = name.Replace(c, ' '); - return name.Trim(); - } } } diff --git a/Assets/Scripts/Editor/WebAssets/PbrMaterialBuilder.cs.meta b/Assets/Scripts/Editor/WebAssets/PbrMaterialBuilder.cs.meta new file mode 100644 index 0000000..288a06a --- /dev/null +++ b/Assets/Scripts/Editor/WebAssets/PbrMaterialBuilder.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8c4d448a85521344783f6d1b0dc64c53 \ No newline at end of file