Files
chameleon/Assets/Scripts/Editor/AmbientCg/AmbientCgImporterWindow.cs
T
adminandClaude Fable 5 fb4098b9f8 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>
2026-07-16 15:27:02 +02:00

402 lines
15 KiB
C#

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
{
/// <summary>
/// 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).
/// </summary>
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<PbrMapType, bool> _mapEnabled = new Dictionary<PbrMapType, bool>();
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<AssetBrowserGrid.Entry> 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<AmbientCgImporterWindow>("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<PbrMapType>();
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();
});
}
/// <summary>
/// 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}".
/// </summary>
private void ExtractAndBuild(string tempZip, string folder, string displayName, List<PbrMapType> wanted)
{
var results = new List<PbrMaterialBuilder.Downloaded>();
var missing = new List<PbrMapType>(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();
}
}
}