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:
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user