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,172 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Chameleon.Editor.WebAssets
|
||||
{
|
||||
/// <summary>
|
||||
/// Reusable catalog browser for importer windows: search field + responsive
|
||||
/// thumbnail grid with lazy, throttled thumbnail loading. The host window owns
|
||||
/// the catalog data and reacts to cell clicks via the onSelect callback.
|
||||
/// </summary>
|
||||
public sealed class AssetBrowserGrid
|
||||
{
|
||||
public sealed class Entry
|
||||
{
|
||||
public string Key; // slug / asset id
|
||||
public string Name; // display name (cell label)
|
||||
public string ThumbnailUrl;
|
||||
public string Search; // pre-lowered haystack for filtering
|
||||
}
|
||||
|
||||
private const int MaxResults = 150;
|
||||
private const int MaxThumbInFlight = 6;
|
||||
|
||||
private readonly Action _repaint;
|
||||
|
||||
private List<Entry> _catalog;
|
||||
private readonly List<Entry> _filtered = new List<Entry>();
|
||||
private string _searchQuery = "";
|
||||
private Vector2 _gridScroll;
|
||||
|
||||
private readonly Dictionary<string, Texture2D> _thumbs = new Dictionary<string, Texture2D>();
|
||||
private readonly HashSet<string> _thumbRequested = new HashSet<string>();
|
||||
private int _thumbsInFlight;
|
||||
|
||||
public AssetBrowserGrid(Action repaint)
|
||||
{
|
||||
_repaint = repaint;
|
||||
}
|
||||
|
||||
public bool HasCatalog => _catalog != null;
|
||||
|
||||
public void SetCatalog(List<Entry> catalog)
|
||||
{
|
||||
_catalog = catalog;
|
||||
ApplyFilter();
|
||||
}
|
||||
|
||||
/// <summary>Draws search field, match count and the grid. Call only when HasCatalog.</summary>
|
||||
public void Draw(Action<Entry> onSelect, Action reloadCatalog, string itemNoun, float gridHeight = 320f)
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
EditorGUI.BeginChangeCheck();
|
||||
_searchQuery = EditorGUILayout.TextField("Search", _searchQuery);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
ApplyFilter();
|
||||
if (reloadCatalog != null && GUILayout.Button("↻", GUILayout.Width(26)))
|
||||
reloadCatalog();
|
||||
}
|
||||
|
||||
int shown = Mathf.Min(_filtered.Count, MaxResults);
|
||||
string cap = _filtered.Count > MaxResults ? $", showing first {MaxResults}" : "";
|
||||
EditorGUILayout.LabelField(
|
||||
$"{_filtered.Count} match(es){cap} of {_catalog.Count} {itemNoun}.",
|
||||
EditorStyles.miniLabel);
|
||||
|
||||
DrawGrid(shown, onSelect, gridHeight);
|
||||
}
|
||||
|
||||
private void DrawGrid(int count, Action<Entry> onSelect, float gridHeight)
|
||||
{
|
||||
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(gridHeight));
|
||||
|
||||
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, onSelect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
private void DrawCell(Entry a, float size, Action<Entry> onSelect)
|
||||
{
|
||||
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))
|
||||
onSelect?.Invoke(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(Entry a)
|
||||
{
|
||||
if (_thumbs.TryGetValue(a.Key, out var tex))
|
||||
{
|
||||
if (tex)
|
||||
return tex;
|
||||
// Cached texture was destroyed (e.g. play-mode cleanup) — purge and re-request.
|
||||
_thumbs.Remove(a.Key);
|
||||
_thumbRequested.Remove(a.Key);
|
||||
}
|
||||
|
||||
// Don't add to _thumbRequested until a slot is free, so it retries next repaint.
|
||||
if (!_thumbRequested.Contains(a.Key) && _thumbsInFlight < MaxThumbInFlight)
|
||||
{
|
||||
_thumbRequested.Add(a.Key);
|
||||
_thumbsInFlight++;
|
||||
EditorWebRequest.GetTexture(a.ThumbnailUrl,
|
||||
t => { _thumbs[a.Key] = t; _thumbsInFlight--; _repaint?.Invoke(); },
|
||||
_ => { _thumbsInFlight--; _repaint?.Invoke(); }); // no retry-spam on error
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ApplyFilter()
|
||||
{
|
||||
_filtered.Clear();
|
||||
if (_catalog == null)
|
||||
return;
|
||||
|
||||
var q = _searchQuery?.Trim().ToLowerInvariant();
|
||||
if (string.IsNullOrEmpty(q))
|
||||
{
|
||||
_filtered.AddRange(_catalog);
|
||||
return;
|
||||
}
|
||||
|
||||
var terms = q.Split((char[])null, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var a in _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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 464e6d1b4d5744241ac0dcdfc3d7cf78
|
||||
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
namespace Chameleon.Editor.WebAssets
|
||||
{
|
||||
/// <summary>
|
||||
/// Non-blocking UnityWebRequest transport for editor tools. All requests poll on
|
||||
/// EditorApplication.update — never blocks the editor. Shared by the Poly Haven
|
||||
/// and ambientCG importers.
|
||||
/// </summary>
|
||||
public static class EditorWebRequest
|
||||
{
|
||||
public const string UserAgent = "Chameleon-Unity-AssetImporter/1.0";
|
||||
|
||||
/// <summary>GET a URL and return the response body as text.</summary>
|
||||
public static void GetText(string url, Action<string> onDone, Action<string> onError)
|
||||
{
|
||||
var req = UnityWebRequest.Get(url);
|
||||
req.SetRequestHeader("User-Agent", UserAgent);
|
||||
Send(req, () => onDone?.Invoke(req.downloadHandler.text), onError);
|
||||
}
|
||||
|
||||
/// <summary>Fetches an image as a Texture2D (browser thumbnails, previews).</summary>
|
||||
public static void GetTexture(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 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 downloads).</summary>
|
||||
public static void DownloadFile(string url, string destPath, Action onDone,
|
||||
Action<string> onError, Action<float> onProgress = null)
|
||||
{
|
||||
var req = new UnityWebRequest(url, UnityWebRequest.kHttpVerbGET);
|
||||
req.downloadHandler = new DownloadHandlerFile(destPath) { removeFileOnAbort = true };
|
||||
req.SetRequestHeader("User-Agent", UserAgent);
|
||||
Send(req, () => onDone?.Invoke(), onError, onProgress);
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: edbbb3505f143824590b8d77228d2407
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
|
||||
namespace Chameleon.Editor.WebAssets
|
||||
{
|
||||
/// <summary>Small shared helpers for the web-asset importer windows.</summary>
|
||||
public static class ImportUtil
|
||||
{
|
||||
public static string SanitizeFileName(string name)
|
||||
{
|
||||
foreach (var c in Path.GetInvalidFileNameChars())
|
||||
name = name.Replace(c, ' ');
|
||||
return name.Trim();
|
||||
}
|
||||
|
||||
/// <summary>"2k"/"2K" -> 2. Unknown strings sort last.</summary>
|
||||
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>Maps a "2k"-style resolution to a valid Unity max texture size.</summary>
|
||||
public static int ResolutionToMaxSize(string res)
|
||||
{
|
||||
int k = ResolutionRank(res);
|
||||
int px = k * 1024;
|
||||
int[] valid = { 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384 };
|
||||
foreach (var v in valid)
|
||||
if (px <= v) return v;
|
||||
return 16384;
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Non-destructive folder-collision handling: if the folder exists, asks the user
|
||||
/// (New folder / Cancel / Overwrite). Returns the folder to use, or null on cancel.
|
||||
/// </summary>
|
||||
public static string ResolveFolderCollision(string folder, string dialogTitle)
|
||||
{
|
||||
if (!AssetDatabase.IsValidFolder(folder))
|
||||
return folder;
|
||||
|
||||
int choice = EditorUtility.DisplayDialogComplex(dialogTitle,
|
||||
$"Folder already exists:\n{folder}", "New folder", "Cancel", "Overwrite");
|
||||
if (choice == 1) return null; // Cancel
|
||||
if (choice == 0) return AssetDatabase.GenerateUniqueAssetPath(folder); // New folder
|
||||
return folder; // Overwrite
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 22c722d093b45124fb66addc8fa83211
|
||||
@@ -0,0 +1,68 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Chameleon.Editor.WebAssets
|
||||
{
|
||||
/// <summary>
|
||||
/// The PBR map types the importers understand, provider-agnostic. "Arm" is the
|
||||
/// packed AO/Rough/Metal map some Poly Haven assets ship instead of separate maps.
|
||||
/// </summary>
|
||||
public enum PbrMapType
|
||||
{
|
||||
Diffuse,
|
||||
NormalGL,
|
||||
Rough,
|
||||
AO,
|
||||
Metal,
|
||||
Displacement,
|
||||
Arm,
|
||||
}
|
||||
|
||||
/// <summary>Static per-map metadata: file suffix, colour space, UI defaults.</summary>
|
||||
public static class PbrMapInfo
|
||||
{
|
||||
public readonly struct Meta
|
||||
{
|
||||
public readonly PbrMapType Type;
|
||||
public readonly string Suffix; // file name suffix on disk
|
||||
public readonly bool SRGB; // true => colour, false => linear data
|
||||
public readonly bool IsNormal; // import as TextureImporterType.NormalMap
|
||||
public readonly bool DefaultEnabled; // pre-checked in the UI when available
|
||||
public readonly string Label; // UI label
|
||||
|
||||
public Meta(PbrMapType type, string suffix, bool srgb, bool isNormal,
|
||||
bool defaultEnabled, string label)
|
||||
{
|
||||
Type = type;
|
||||
Suffix = suffix;
|
||||
SRGB = srgb;
|
||||
IsNormal = isNormal;
|
||||
DefaultEnabled = defaultEnabled;
|
||||
Label = label;
|
||||
}
|
||||
}
|
||||
|
||||
// Order here defines UI order.
|
||||
public static readonly Meta[] All =
|
||||
{
|
||||
new Meta(PbrMapType.Diffuse, "diffuse", true, false, true, "Diffuse (Albedo)"),
|
||||
new Meta(PbrMapType.NormalGL, "normal", false, true, true, "Normal (OpenGL)"),
|
||||
new Meta(PbrMapType.Rough, "roughness", false, false, true, "Roughness"),
|
||||
new Meta(PbrMapType.AO, "ao", false, false, true, "Ambient Occlusion"),
|
||||
new Meta(PbrMapType.Metal, "metallic", false, false, true, "Metallic"),
|
||||
new Meta(PbrMapType.Displacement, "height", false, false, false, "Displacement (Height)"),
|
||||
new Meta(PbrMapType.Arm, "arm", false, false, false, "ARM (packed AO/Rough/Metal)"),
|
||||
};
|
||||
|
||||
private static readonly Dictionary<PbrMapType, Meta> ByType = BuildLookup();
|
||||
|
||||
private static Dictionary<PbrMapType, Meta> BuildLookup()
|
||||
{
|
||||
var d = new Dictionary<PbrMapType, Meta>(All.Length);
|
||||
foreach (var m in All)
|
||||
d[m.Type] = m;
|
||||
return d;
|
||||
}
|
||||
|
||||
public static Meta Get(PbrMapType type) => ByType[type];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6e3acec0a249ed549a95141ea60978bc
|
||||
@@ -0,0 +1,256 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Chameleon.Editor.WebAssets
|
||||
{
|
||||
/// <summary>
|
||||
/// Turns downloaded PBR maps (Poly Haven, ambientCG, ...) into a ready URP/Lit
|
||||
/// material: correct import settings per map, Roughness→Smoothness repacking into
|
||||
/// a MetallicGloss texture, and wired-up keywords.
|
||||
/// </summary>
|
||||
public static class PbrMaterialBuilder
|
||||
{
|
||||
/// <summary>A map that was successfully downloaded, ready to import.</summary>
|
||||
public struct Downloaded
|
||||
{
|
||||
public PbrMapType Type;
|
||||
public string AssetPath; // "Assets/Poly Haven/.../foo_diffuse.jpg"
|
||||
}
|
||||
|
||||
public static void Build(string folder, string displayName, string slug, int maxSize,
|
||||
List<Downloaded> maps)
|
||||
{
|
||||
var byType = new Dictionary<PbrMapType, string>();
|
||||
foreach (var m in maps)
|
||||
byType[m.Type] = m.AssetPath;
|
||||
|
||||
// 1) Import settings for every raw map.
|
||||
foreach (var m in maps)
|
||||
ApplyImportSettings(m.AssetPath, m.Type, maxSize);
|
||||
|
||||
// 2) Build a MetallicSmoothness texture. Rough/Metal come from the dedicated
|
||||
// maps when present, otherwise from the packed ARM map (G=rough, B=metal).
|
||||
byType.TryGetValue(PbrMapType.Arm, out var armPath);
|
||||
byType.TryGetValue(PbrMapType.Rough, out var roughPath);
|
||||
byType.TryGetValue(PbrMapType.Metal, out var metalPath);
|
||||
|
||||
string metallicGlossPath = null;
|
||||
if (!string.IsNullOrEmpty(roughPath) || !string.IsNullOrEmpty(armPath))
|
||||
metallicGlossPath = BuildMetallicSmoothness(folder, slug, roughPath, metalPath, armPath, maxSize);
|
||||
|
||||
// The roughness source is fully consumed into the packed map and never
|
||||
// referenced by the material — drop it once the repack succeeded.
|
||||
if (!string.IsNullOrEmpty(metallicGlossPath) && !string.IsNullOrEmpty(roughPath))
|
||||
{
|
||||
AssetDatabase.DeleteAsset(roughPath);
|
||||
byType.Remove(PbrMapType.Rough);
|
||||
Debug.Log($"[PBR Import] Deleted consumed roughness source: {roughPath}");
|
||||
}
|
||||
|
||||
// AO comes from the dedicated map, or the ARM red channel as a fallback.
|
||||
string occlusionPath = null;
|
||||
if (byType.TryGetValue(PbrMapType.AO, out var aoSeparate))
|
||||
occlusionPath = aoSeparate;
|
||||
else if (!string.IsNullOrEmpty(armPath))
|
||||
occlusionPath = BuildAoFromArm(folder, slug, armPath, maxSize);
|
||||
|
||||
// 3) Create the URP/Lit material.
|
||||
var shader = Shader.Find("Universal Render Pipeline/Lit");
|
||||
if (!shader)
|
||||
{
|
||||
Debug.LogError("[PBR Import] URP/Lit shader not found — is URP installed?");
|
||||
return;
|
||||
}
|
||||
|
||||
var mat = new Material(shader) { name = displayName };
|
||||
string matPath = Path.Combine(folder, ImportUtil.SanitizeFileName(displayName) + ".mat").Replace('\\', '/');
|
||||
matPath = AssetDatabase.GenerateUniqueAssetPath(matPath);
|
||||
AssetDatabase.CreateAsset(mat, matPath);
|
||||
|
||||
if (byType.TryGetValue(PbrMapType.Diffuse, out var diffPath))
|
||||
mat.SetTexture("_BaseMap", Load(diffPath));
|
||||
|
||||
if (byType.TryGetValue(PbrMapType.NormalGL, out var norPath))
|
||||
{
|
||||
mat.SetTexture("_BumpMap", Load(norPath));
|
||||
mat.SetFloat("_BumpScale", 1f);
|
||||
mat.EnableKeyword("_NORMALMAP");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(metallicGlossPath))
|
||||
{
|
||||
mat.SetTexture("_MetallicGlossMap", Load(metallicGlossPath));
|
||||
mat.SetFloat("_Smoothness", 1f); // acts as multiplier on the map's alpha
|
||||
mat.EnableKeyword("_METALLICSPECGLOSSMAP");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(occlusionPath))
|
||||
{
|
||||
mat.SetTexture("_OcclusionMap", Load(occlusionPath));
|
||||
mat.SetFloat("_OcclusionStrength", 1f);
|
||||
mat.EnableKeyword("_OCCLUSIONMAP");
|
||||
}
|
||||
|
||||
if (byType.TryGetValue(PbrMapType.Displacement, out var heightPath))
|
||||
{
|
||||
mat.SetTexture("_ParallaxMap", Load(heightPath));
|
||||
mat.SetFloat("_Parallax", 0.03f);
|
||||
mat.EnableKeyword("_PARALLAXMAP");
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(mat);
|
||||
AssetDatabase.SaveAssets();
|
||||
EditorGUIUtility.PingObject(mat);
|
||||
Selection.activeObject = mat;
|
||||
Debug.Log($"[PBR Import] Material created: {matPath}");
|
||||
}
|
||||
|
||||
private static Texture2D Load(string path) => AssetDatabase.LoadAssetAtPath<Texture2D>(path);
|
||||
|
||||
public static void ApplyImportSettings(string assetPath, PbrMapType type, int maxSize)
|
||||
{
|
||||
var importer = AssetImporter.GetAtPath(assetPath) as TextureImporter;
|
||||
if (!importer)
|
||||
return;
|
||||
|
||||
var meta = PbrMapInfo.Get(type);
|
||||
importer.textureType = meta.IsNormal ? TextureImporterType.NormalMap : TextureImporterType.Default;
|
||||
if (!meta.IsNormal)
|
||||
importer.sRGBTexture = meta.SRGB;
|
||||
importer.maxTextureSize = maxSize;
|
||||
importer.SaveAndReimport();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Packs metallic into RGB and smoothness (1-roughness) into A, as URP/Lit expects
|
||||
/// in _MetallicGlossMap. Roughness/Metal are read from the dedicated maps when given,
|
||||
/// otherwise unpacked from the ARM map (G=rough, B=metal). Reads raw bytes off disk
|
||||
/// into linear textures to avoid sRGB sampling on data maps.
|
||||
/// </summary>
|
||||
private static string BuildMetallicSmoothness(string folder, string slug, string roughPath,
|
||||
string metalPath, string armPath, int maxSize)
|
||||
{
|
||||
Texture2D rough = string.IsNullOrEmpty(roughPath) ? null : LoadLinear(roughPath);
|
||||
Texture2D metal = string.IsNullOrEmpty(metalPath) ? null : LoadLinear(metalPath);
|
||||
Texture2D arm = string.IsNullOrEmpty(armPath) ? null : LoadLinear(armPath);
|
||||
|
||||
// Sizing grid: prefer the dedicated roughness map, else the ARM map.
|
||||
Texture2D grid = rough ? rough : arm;
|
||||
if (!grid)
|
||||
{
|
||||
if (rough) Object.DestroyImmediate(rough);
|
||||
if (metal) Object.DestroyImmediate(metal);
|
||||
if (arm) Object.DestroyImmediate(arm);
|
||||
return null;
|
||||
}
|
||||
|
||||
int w = grid.width, h = grid.height;
|
||||
var outPixels = new Color[w * h];
|
||||
|
||||
for (int y = 0; y < h; y++)
|
||||
{
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
int i = y * w + x;
|
||||
float u = (x + 0.5f) / w, v = (y + 0.5f) / h;
|
||||
|
||||
// Roughness: dedicated map .r, else ARM .g. Smoothness = 1 - roughness.
|
||||
float roughness = rough ? rough.GetPixelBilinear(u, v).r
|
||||
: arm ? arm.GetPixelBilinear(u, v).g
|
||||
: 1f;
|
||||
float smoothness = 1f - roughness;
|
||||
|
||||
// Metallic: dedicated map .r, else ARM .b.
|
||||
float m = metal ? metal.GetPixelBilinear(u, v).r
|
||||
: arm ? arm.GetPixelBilinear(u, v).b
|
||||
: 0f;
|
||||
|
||||
outPixels[i] = new Color(m, m, m, smoothness);
|
||||
}
|
||||
}
|
||||
|
||||
var packed = new Texture2D(w, h, TextureFormat.RGBA32, true, true);
|
||||
packed.SetPixels(outPixels);
|
||||
packed.Apply();
|
||||
|
||||
string outPath = Path.Combine(folder, slug + "_metallic_smoothness.png").Replace('\\', '/');
|
||||
File.WriteAllBytes(outPath, packed.EncodeToPNG());
|
||||
|
||||
Object.DestroyImmediate(packed);
|
||||
if (rough) Object.DestroyImmediate(rough);
|
||||
if (metal) Object.DestroyImmediate(metal);
|
||||
if (arm) Object.DestroyImmediate(arm);
|
||||
|
||||
ReimportLinear(outPath, maxSize, withAlpha: true);
|
||||
return outPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the AO (red) channel from a packed ARM map into a standalone grayscale
|
||||
/// occlusion texture, used when the asset ships no dedicated AO map.
|
||||
/// </summary>
|
||||
private static string BuildAoFromArm(string folder, string slug, string armPath, int maxSize)
|
||||
{
|
||||
var arm = LoadLinear(armPath);
|
||||
if (!arm)
|
||||
return null;
|
||||
|
||||
int w = arm.width, h = arm.height;
|
||||
var src = arm.GetPixels();
|
||||
var outPixels = new Color[src.Length];
|
||||
for (int i = 0; i < src.Length; i++)
|
||||
{
|
||||
float ao = src[i].r; // ARM red channel = ambient occlusion
|
||||
outPixels[i] = new Color(ao, ao, ao, 1f);
|
||||
}
|
||||
|
||||
var tex = new Texture2D(w, h, TextureFormat.RGBA32, true, true);
|
||||
tex.SetPixels(outPixels);
|
||||
tex.Apply();
|
||||
|
||||
string outPath = Path.Combine(folder, slug + "_ao.png").Replace('\\', '/');
|
||||
File.WriteAllBytes(outPath, tex.EncodeToPNG());
|
||||
|
||||
Object.DestroyImmediate(tex);
|
||||
Object.DestroyImmediate(arm);
|
||||
|
||||
ReimportLinear(outPath, maxSize, withAlpha: false);
|
||||
return outPath;
|
||||
}
|
||||
|
||||
/// <summary>Reimports a generated data texture as linear, at the target size.</summary>
|
||||
private static void ReimportLinear(string outPath, int maxSize, bool withAlpha)
|
||||
{
|
||||
AssetDatabase.ImportAsset(outPath, ImportAssetOptions.ForceSynchronousImport);
|
||||
var imp = AssetImporter.GetAtPath(outPath) as TextureImporter;
|
||||
if (!imp)
|
||||
return;
|
||||
imp.sRGBTexture = false;
|
||||
imp.maxTextureSize = maxSize;
|
||||
if (withAlpha)
|
||||
{
|
||||
imp.alphaSource = TextureImporterAlphaSource.FromInput;
|
||||
imp.alphaIsTransparency = false;
|
||||
}
|
||||
imp.SaveAndReimport();
|
||||
}
|
||||
|
||||
/// <summary>Loads an image file straight off disk into a linear Texture2D (no import needed).</summary>
|
||||
private static Texture2D LoadLinear(string assetPath)
|
||||
{
|
||||
string full = Path.GetFullPath(assetPath);
|
||||
if (!File.Exists(full))
|
||||
return null;
|
||||
var bytes = File.ReadAllBytes(full);
|
||||
var tex = new Texture2D(2, 2, TextureFormat.RGBA32, true, true);
|
||||
if (!tex.LoadImage(bytes))
|
||||
{
|
||||
Object.DestroyImmediate(tex);
|
||||
return null;
|
||||
}
|
||||
return tex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8c4d448a85521344783f6d1b0dc64c53
|
||||
Reference in new issue
Block a user