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 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Fable 5 committed 2026-07-16 15:27:02 +02:00
1 parent 9b35ee2a97
commit fb4098b9f8
21 files changed
+1205 -417

No files matched your search

@@ -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
{
/// <summary>
/// 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.
/// </summary>
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<PolyHavenMapType, bool> _mapEnabled = new Dictionary<PolyHavenMapType, bool>();
private readonly Dictionary<PbrMapType, bool> _mapEnabled = new Dictionary<PbrMapType, bool>();
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<PolyHavenAsset> s_catalog;
private static List<AssetBrowserGrid.Entry> s_catalog;
private AssetBrowserGrid _grid;
private bool _browseFoldout = true;
private bool _autoLoadTried;
private bool _catalogLoading;
private string _searchQuery = "";
private readonly List<PolyHavenAsset> _filtered = new List<PolyHavenAsset>();
private Vector2 _gridScroll;
private readonly Dictionary<string, Texture2D> _thumbs = new Dictionary<string, Texture2D>();
private readonly HashSet<string> _thumbRequested = new HashSet<string>();
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));
}
}
/// <summary>Lazy, throttled thumbnail loader — only visible cells request, capped in-flight.</summary>
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<PolyHavenMapType>();
foreach (var meta in PolyHavenMapInfo.All)
var wanted = new List<PbrMapType>();
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<PolyHavenMaterialBuilder.Downloaded>();
var results = new List<PbrMaterialBuilder.Downloaded>();
var failures = new List<string>();
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;
}
}
}
}