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
@@ -0,0 +1,279 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Chameleon.Editor.WebAssets;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Chameleon.Editor.AmbientCg
|
||||
{
|
||||
/// <summary>One downloadable ZIP variant of an asset (e.g. attribute "2K-JPG").</summary>
|
||||
public readonly struct AmbientCgZip
|
||||
{
|
||||
public readonly string Resolution; // "2K"
|
||||
public readonly string Format; // "JPG" / "PNG"
|
||||
public readonly string Url;
|
||||
public readonly string FileName;
|
||||
public readonly long Size;
|
||||
|
||||
public AmbientCgZip(string resolution, string format, string url, string fileName, long size)
|
||||
{
|
||||
Resolution = resolution;
|
||||
Format = format;
|
||||
Url = url;
|
||||
FileName = fileName;
|
||||
Size = size;
|
||||
}
|
||||
|
||||
public bool IsValid => !string.IsNullOrEmpty(Url);
|
||||
}
|
||||
|
||||
/// <summary>Details for one asset: display data, available maps and ZIP downloads.</summary>
|
||||
public sealed class AmbientCgAsset
|
||||
{
|
||||
public string Id; // "Tiles141"
|
||||
public string Name; // "Tiles 141"
|
||||
public string Category;
|
||||
public string ThumbnailUrl;
|
||||
public string DataType; // "Material" expected
|
||||
|
||||
// PBR channels this asset ships, already mapped to the shared map types.
|
||||
public readonly List<PbrMapType> Maps = new List<PbrMapType>();
|
||||
|
||||
public readonly List<AmbientCgZip> Zips = new List<AmbientCgZip>();
|
||||
|
||||
public List<string> Resolutions()
|
||||
{
|
||||
var set = new HashSet<string>();
|
||||
foreach (var z in Zips) set.Add(z.Resolution);
|
||||
var list = new List<string>(set);
|
||||
list.Sort((x, y) => ImportUtil.ResolutionRank(x).CompareTo(ImportUtil.ResolutionRank(y)));
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<string> Formats(string resolution)
|
||||
{
|
||||
var list = new List<string>();
|
||||
foreach (var z in Zips)
|
||||
if (z.Resolution == resolution && !list.Contains(z.Format))
|
||||
list.Add(z.Format);
|
||||
list.Sort(); // "JPG" before "PNG"
|
||||
return list;
|
||||
}
|
||||
|
||||
public bool TryGetZip(string resolution, string format, out AmbientCgZip zip)
|
||||
{
|
||||
foreach (var z in Zips)
|
||||
{
|
||||
if (z.Resolution == resolution && z.Format == format)
|
||||
{
|
||||
zip = z;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
zip = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Async client for the public ambientCG API (v2 full_json). Unlike Poly Haven,
|
||||
/// ambientCG ships one ZIP per resolution+format containing all maps with
|
||||
/// standardized file suffixes (Color, NormalGL, Roughness, AmbientOcclusion, ...).
|
||||
/// </summary>
|
||||
public static class AmbientCgApi
|
||||
{
|
||||
private const string ApiBase = "https://ambientcg.com/api/v2/full_json";
|
||||
private const int PageSize = 250; // API maximum
|
||||
|
||||
// ambientCG "maps" names -> shared map type + file suffix inside the ZIP.
|
||||
public static readonly (string apiName, PbrMapType type, string zipSuffix)[] MapNames =
|
||||
{
|
||||
("color", PbrMapType.Diffuse, "Color"),
|
||||
("normal", PbrMapType.NormalGL, "NormalGL"),
|
||||
("roughness", PbrMapType.Rough, "Roughness"),
|
||||
("ambient-occlusion", PbrMapType.AO, "AmbientOcclusion"),
|
||||
("metalness", PbrMapType.Metal, "Metalness"),
|
||||
("displacement", PbrMapType.Displacement, "Displacement"),
|
||||
// "opacity" is intentionally unsupported — URP/Lit opaque materials don't use it.
|
||||
};
|
||||
|
||||
public static string ZipSuffix(PbrMapType type)
|
||||
{
|
||||
foreach (var (_, t, suffix) in MapNames)
|
||||
if (t == type)
|
||||
return suffix;
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string ExtractId(string urlOrId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(urlOrId)) return null;
|
||||
var s = urlOrId.Trim();
|
||||
|
||||
// https://ambientcg.com/a/Tiles141
|
||||
int a = s.IndexOf("/a/", StringComparison.OrdinalIgnoreCase);
|
||||
if (a >= 0)
|
||||
s = s.Substring(a + 3);
|
||||
|
||||
// https://ambientcg.com/view?id=Tiles141
|
||||
int q = s.IndexOf("id=", StringComparison.OrdinalIgnoreCase);
|
||||
if (q >= 0)
|
||||
s = s.Substring(q + 3);
|
||||
|
||||
foreach (var sep in new[] { '?', '#', '/', '&' })
|
||||
{
|
||||
int i = s.IndexOf(sep);
|
||||
if (i >= 0) s = s.Substring(0, i);
|
||||
}
|
||||
s = s.Trim();
|
||||
return s.Length == 0 ? null : s;
|
||||
}
|
||||
|
||||
public static string ThumbnailUrl(string id) =>
|
||||
$"https://acg-media.struffelproductions.com/file/ambientCG-Web/media/thumbnail/256-PNG/{id}.png";
|
||||
|
||||
// ---- Catalog (paginated) ------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Loads the full Material catalog as browser-grid entries. The API caps pages
|
||||
/// at 250 assets, so this walks all pages sequentially (~2000 assets => ~8 calls).
|
||||
/// </summary>
|
||||
public static void FetchCatalog(Action<List<AssetBrowserGrid.Entry>> onDone, Action<string> onError,
|
||||
Action<int, int> onProgress = null)
|
||||
{
|
||||
var all = new List<AssetBrowserGrid.Entry>();
|
||||
FetchCatalogPage(all, 0, onDone, onError, onProgress);
|
||||
}
|
||||
|
||||
private static void FetchCatalogPage(List<AssetBrowserGrid.Entry> all, int offset,
|
||||
Action<List<AssetBrowserGrid.Entry>> onDone, Action<string> onError, Action<int, int> onProgress)
|
||||
{
|
||||
string url = $"{ApiBase}?type=Material&sort=Alphabet&limit={PageSize}&offset={offset}&include=imageData";
|
||||
EditorWebRequest.GetText(url, text =>
|
||||
{
|
||||
int total;
|
||||
try
|
||||
{
|
||||
var root = JObject.Parse(text);
|
||||
total = root["numberOfResults"] != null ? (int)root["numberOfResults"] : 0;
|
||||
|
||||
if (root["foundAssets"] is JArray assets)
|
||||
{
|
||||
foreach (var t in assets)
|
||||
{
|
||||
if (t is not JObject o) continue;
|
||||
string id = (string)o["assetId"];
|
||||
if (string.IsNullOrEmpty(id)) continue;
|
||||
|
||||
string name = (string)o["displayName"] ?? id;
|
||||
var e = new AssetBrowserGrid.Entry
|
||||
{
|
||||
Key = id,
|
||||
Name = name,
|
||||
ThumbnailUrl = (string)(o["previewImage"]?["256-PNG"]) ?? ThumbnailUrl(id),
|
||||
};
|
||||
e.Search = (name + " " + id + " " +
|
||||
((string)o["displayCategory"] ?? "") + " " +
|
||||
JoinStrings(o["tags"])).ToLowerInvariant();
|
||||
all.Add(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
onError?.Invoke($"Catalog parse failed: {e.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
onProgress?.Invoke(all.Count, total);
|
||||
if (all.Count < total && offset + PageSize < total)
|
||||
FetchCatalogPage(all, offset + PageSize, onDone, onError, onProgress);
|
||||
else
|
||||
onDone?.Invoke(all);
|
||||
}, onError);
|
||||
}
|
||||
|
||||
// ---- Asset details -------------------------------------------------------
|
||||
|
||||
public static void FetchAsset(string id, Action<AmbientCgAsset> onDone, Action<string> onError)
|
||||
{
|
||||
string url = $"{ApiBase}?id={Uri.EscapeDataString(id)}&include=downloadData,imageData,mapData";
|
||||
EditorWebRequest.GetText(url, text =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var root = JObject.Parse(text);
|
||||
if (root["foundAssets"] is not JArray assets || assets.Count == 0)
|
||||
{
|
||||
onError?.Invoke($"Asset '{id}' not found.");
|
||||
return;
|
||||
}
|
||||
onDone?.Invoke(ParseAsset((JObject)assets[0]));
|
||||
}
|
||||
catch (Exception e) { onError?.Invoke($"Asset parse failed: {e.Message}"); }
|
||||
}, onError);
|
||||
}
|
||||
|
||||
private static AmbientCgAsset ParseAsset(JObject o)
|
||||
{
|
||||
string id = (string)o["assetId"];
|
||||
var asset = new AmbientCgAsset
|
||||
{
|
||||
Id = id,
|
||||
Name = (string)o["displayName"] ?? id,
|
||||
Category = (string)o["displayCategory"] ?? "",
|
||||
DataType = (string)o["dataType"] ?? "",
|
||||
ThumbnailUrl = (string)(o["previewImage"]?["256-PNG"]) ?? ThumbnailUrl(id),
|
||||
};
|
||||
|
||||
// Which PBR channels this asset ships.
|
||||
if (o["maps"] is JArray maps)
|
||||
{
|
||||
foreach (var m in maps)
|
||||
{
|
||||
var name = (string)m;
|
||||
foreach (var (apiName, type, _) in MapNames)
|
||||
if (string.Equals(name, apiName, StringComparison.OrdinalIgnoreCase))
|
||||
asset.Maps.Add(type);
|
||||
}
|
||||
}
|
||||
|
||||
// ZIP downloads: downloadFolders.default.downloadFiletypeCategories.zip.downloads[],
|
||||
// each with attribute like "2K-JPG".
|
||||
if (o.SelectToken("downloadFolders.default.downloadFiletypeCategories.zip.downloads") is JArray downloads)
|
||||
{
|
||||
foreach (var t in downloads)
|
||||
{
|
||||
if (t is not JObject d) continue;
|
||||
string attr = (string)d["attribute"];
|
||||
string link = (string)d["downloadLink"] ?? (string)d["fullDownloadPath"];
|
||||
if (string.IsNullOrEmpty(attr) || string.IsNullOrEmpty(link))
|
||||
continue;
|
||||
|
||||
int dash = attr.LastIndexOf('-');
|
||||
if (dash <= 0)
|
||||
continue; // not a "{RES}-{FMT}" texture zip
|
||||
string res = attr.Substring(0, dash);
|
||||
string fmt = attr.Substring(dash + 1);
|
||||
|
||||
asset.Zips.Add(new AmbientCgZip(res, fmt, link,
|
||||
(string)d["fileName"] ?? $"{id}_{attr}.zip",
|
||||
d["size"] != null ? (long)d["size"] : 0));
|
||||
}
|
||||
}
|
||||
|
||||
return asset;
|
||||
}
|
||||
|
||||
private static string JoinStrings(JToken token)
|
||||
{
|
||||
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 string.Join(" ", list);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4cfc661c9314b9d4f9c5c491ae7df74f
|
||||
@@ -0,0 +1,401 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b732bd5ab747dd54cb08525c089a1d8f
|
||||
Reference in new issue
Block a user