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(); } } }