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,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<string>();
|
||||
}
|
||||
|
||||
/// <summary>One catalog entry from /assets (lightweight, for the browser grid).</summary>
|
||||
public sealed class PolyHavenAsset
|
||||
{
|
||||
public string Slug;
|
||||
public string Name;
|
||||
public string ThumbnailUrl;
|
||||
public string[] Categories = Array.Empty<string>();
|
||||
public string[] Tags = Array.Empty<string>();
|
||||
public string Search; // pre-lowered "name slug categories tags" haystack
|
||||
}
|
||||
|
||||
/// <summary>Parsed /files/{slug}: MapType -> resolution("2k") -> best file.</summary>
|
||||
public sealed class PolyHavenFiles
|
||||
{
|
||||
public readonly Dictionary<PolyHavenMapType, Dictionary<string, PolyHavenFile>> Maps =
|
||||
new Dictionary<PolyHavenMapType, Dictionary<string, PolyHavenFile>>();
|
||||
public readonly Dictionary<PbrMapType, Dictionary<string, PolyHavenFile>> Maps =
|
||||
new Dictionary<PbrMapType, Dictionary<string, PolyHavenFile>>();
|
||||
|
||||
// Every resolution string seen across all maps (e.g. "1k","2k","4k"), sorted low->high.
|
||||
public List<string> Resolutions = new List<string>();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Best available file at res, else next-lower resolution of the same map.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<PolyHavenInfo> onDone, Action<string> 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);
|
||||
}
|
||||
|
||||
/// <summary>Loads the full texture catalog (keyed by slug) for the browser grid.</summary>
|
||||
public static void FetchAssets(string type, Action<List<PolyHavenAsset>> onDone, Action<string> onError)
|
||||
/// <summary>Loads the full texture catalog as browser-grid entries.</summary>
|
||||
public static void FetchAssets(string type, Action<List<AssetBrowserGrid.Entry>> onDone, Action<string> 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<PolyHavenAsset>(root.Count);
|
||||
var root = JObject.Parse(text);
|
||||
var list = new List<AssetBrowserGrid.Entry>(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<PolyHavenFiles> onDone, Action<string> 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<Texture2D> onDone, Action<string> 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);
|
||||
}
|
||||
|
||||
/// <summary>Streams a file straight to disk (no RAM peak on big EXR/8K files).</summary>
|
||||
public static void DownloadFile(PolyHavenFile file, string destPath, Action onDone,
|
||||
Action<string> onError, Action<float> 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<string>();
|
||||
if (token is not JArray arr) return "";
|
||||
var list = new List<string>(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<string>();
|
||||
|
||||
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<string>(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<string> onError,
|
||||
Action<float> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user