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