643 lines
30 KiB
C#
643 lines
30 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
#if UNITY_EDITOR
|
||
using UnityEditor;
|
||
#endif
|
||
|
||
namespace SplashEdit.RuntimeCode
|
||
{
|
||
/// <summary>
|
||
/// Collects all PSXCanvas hierarchies in the scene, bakes RectTransform
|
||
/// coordinates into PS1 pixel space, and produces <see cref="PSXCanvasData"/>
|
||
/// arrays ready for binary serialization.
|
||
/// </summary>
|
||
public static class PSXUIExporter
|
||
{
|
||
/// <summary>
|
||
/// Collect all PSXCanvas components and their child UI elements,
|
||
/// converting RectTransform coordinates to PS1 pixel space.
|
||
/// Also collects and deduplicates custom fonts.
|
||
/// </summary>
|
||
/// <param name="resolution">Target PS1 resolution (e.g. 320×240).</param>
|
||
/// <param name="fonts">Output: collected custom font data (max 3).</param>
|
||
/// <returns>Array of canvas data ready for binary writing.</returns>
|
||
public static PSXCanvasData[] CollectCanvases(Vector2 resolution, out PSXFontData[] fonts)
|
||
{
|
||
// Collect and deduplicate all custom fonts used by text elements
|
||
List<PSXFontAsset> uniqueFonts = new List<PSXFontAsset>();
|
||
|
||
#if UNITY_EDITOR
|
||
PSXCanvas[] canvases = Object.FindObjectsByType<PSXCanvas>(FindObjectsSortMode.None);
|
||
#else
|
||
PSXCanvas[] canvases = Object.FindObjectsOfType<PSXCanvas>();
|
||
#endif
|
||
if (canvases == null || canvases.Length == 0)
|
||
{
|
||
fonts = new PSXFontData[0];
|
||
return new PSXCanvasData[0];
|
||
}
|
||
|
||
return CollectCanvasesInternal(canvases, resolution, out fonts);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Collect a single canvas from a prefab instance for loading screen export.
|
||
/// The prefab must have a PSXCanvas on its root.
|
||
/// Note: Image elements that reference VRAM textures will NOT work in loading screens
|
||
/// since the VRAM hasn't been populated yet. Use Box, Text, and ProgressBar only.
|
||
/// </summary>
|
||
public static PSXCanvasData[] CollectCanvasFromPrefab(GameObject prefab, Vector2 resolution, out PSXFontData[] fonts)
|
||
{
|
||
if (prefab == null)
|
||
{
|
||
fonts = new PSXFontData[0];
|
||
return new PSXCanvasData[0];
|
||
}
|
||
|
||
PSXCanvas canvas = prefab.GetComponentInChildren<PSXCanvas>(true);
|
||
if (canvas == null)
|
||
{
|
||
Debug.LogWarning($"PSXUIExporter: Prefab '{prefab.name}' has no PSXCanvas component.");
|
||
fonts = new PSXFontData[0];
|
||
return new PSXCanvasData[0];
|
||
}
|
||
|
||
return CollectCanvasesInternal(new[] { canvas }, resolution, out fonts);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Internal shared implementation for canvas collection.
|
||
/// Works on an explicit array of PSXCanvas components.
|
||
/// </summary>
|
||
private static PSXCanvasData[] CollectCanvasesInternal(PSXCanvas[] canvases, Vector2 resolution, out PSXFontData[] fonts)
|
||
{
|
||
List<PSXFontAsset> uniqueFonts = new List<PSXFontAsset>();
|
||
|
||
// First pass: collect unique fonts
|
||
foreach (PSXCanvas canvas in canvases)
|
||
{
|
||
PSXUIText[] texts = canvas.GetComponentsInChildren<PSXUIText>(true);
|
||
foreach (PSXUIText txt in texts)
|
||
{
|
||
PSXFontAsset font = txt.GetEffectiveFont();
|
||
if (font != null && !uniqueFonts.Contains(font) && uniqueFonts.Count < 3)
|
||
uniqueFonts.Add(font);
|
||
}
|
||
}
|
||
|
||
// Build font data with VRAM positions.
|
||
// Each font gets its own texture page to avoid V-coordinate overflow.
|
||
// Font textures go at x=960:
|
||
// Font 1: y=0 (page 15,0) - 256px available
|
||
// Font 2: y=256 (page 15,1) - 208px available (system font at y=464)
|
||
// Font 3: not supported (would need different VRAM column)
|
||
// System font: (960, 464) in page (15,1), occupies y=464-511.
|
||
List<PSXFontData> fontDataList = new List<PSXFontData>();
|
||
ushort[] fontPageStarts = { 0, 256 }; // one per texture page
|
||
int fontPageIndex = 0;
|
||
|
||
foreach (PSXFontAsset fa in uniqueFonts)
|
||
{
|
||
byte[] pixelData = fa.ConvertTo4BPP();
|
||
if (pixelData == null) continue;
|
||
|
||
// Read advance widths directly from the font asset.
|
||
// These were computed during bitmap generation from the exact same
|
||
// CharacterInfo used to render the glyphs - guaranteed to match.
|
||
byte[] advances = fa.AdvanceWidths;
|
||
if (advances == null || advances.Length < 96)
|
||
{
|
||
Debug.LogWarning($"PSXUIExporter: Font '{fa.name}' has no stored advance widths. Using cell width as fallback.");
|
||
advances = new byte[96];
|
||
for (int i = 0; i < 96; i++) advances[i] = (byte)fa.GlyphWidth;
|
||
}
|
||
|
||
ushort texH = (ushort)fa.TextureHeight;
|
||
|
||
if (fontPageIndex >= fontPageStarts.Length)
|
||
{
|
||
Debug.LogError($"PSXUIExporter: Max 2 custom fonts supported (need separate texture pages). Skipping '{fa.name}'.");
|
||
continue;
|
||
}
|
||
|
||
ushort vramY = fontPageStarts[fontPageIndex];
|
||
int maxHeight = (fontPageIndex == 1) ? 208 : 256; // page 1 shares with system font
|
||
if (texH > maxHeight)
|
||
{
|
||
Debug.LogWarning($"PSXUIExporter: Font '{fa.name}' texture ({texH}px) exceeds page limit ({maxHeight}px). May be clipped.");
|
||
}
|
||
|
||
fontDataList.Add(new PSXFontData
|
||
{
|
||
Source = fa,
|
||
GlyphWidth = (byte)fa.GlyphWidth,
|
||
GlyphHeight = (byte)fa.GlyphHeight,
|
||
VramX = 960,
|
||
VramY = vramY,
|
||
TextureHeight = texH,
|
||
PixelData = pixelData,
|
||
AdvanceWidths = advances
|
||
});
|
||
fontPageIndex++;
|
||
}
|
||
fonts = fontDataList.ToArray();
|
||
|
||
// Second pass: collect canvases with font index assignment
|
||
List<PSXCanvasData> result = new List<PSXCanvasData>();
|
||
|
||
foreach (PSXCanvas canvas in canvases)
|
||
{
|
||
Canvas unityCanvas = canvas.GetComponent<Canvas>();
|
||
if (unityCanvas == null) continue;
|
||
|
||
RectTransform canvasRect = canvas.GetComponent<RectTransform>();
|
||
float canvasW = canvasRect.rect.width;
|
||
float canvasH = canvasRect.rect.height;
|
||
if (canvasW <= 0) canvasW = resolution.x;
|
||
if (canvasH <= 0) canvasH = resolution.y;
|
||
|
||
float scaleX = resolution.x / canvasW;
|
||
float scaleY = resolution.y / canvasH;
|
||
|
||
List<PSXUIElementData> elements = new List<PSXUIElementData>();
|
||
|
||
Debug.Log($"[UIExporter] Canvas '{canvas.CanvasName}' on '{canvas.gameObject.name}' " +
|
||
$"canvasW={canvasW} canvasH={canvasH} childCount={canvas.transform.childCount}");
|
||
|
||
// Collect all UI elements in hierarchy order (depth-first, sibling index).
|
||
// GetComponentsInChildren returns top-of-hierarchy first, but the C++
|
||
// renderer draws index 0 first (back). Reverse so that top-of-hierarchy
|
||
// elements are rendered last (on top), matching Unity's visual stacking.
|
||
CollectAllElementsInHierarchyOrder(canvas.transform, canvasRect, scaleX, scaleY, resolution, elements, uniqueFonts);
|
||
elements.Reverse();
|
||
Debug.Log($"[UIExporter] TOTAL elements: {elements.Count}");
|
||
|
||
string name = canvas.CanvasName ?? "canvas";
|
||
if (name.Length > 24) name = name.Substring(0, 24);
|
||
|
||
result.Add(new PSXCanvasData
|
||
{
|
||
Name = name,
|
||
StartVisible = canvas.StartVisible,
|
||
SortOrder = canvas.SortOrder,
|
||
Elements = elements.ToArray()
|
||
});
|
||
}
|
||
|
||
return result.ToArray();
|
||
}
|
||
|
||
// ─── Coordinate baking helpers ───
|
||
|
||
/// <summary>
|
||
/// Convert a RectTransform into PS1 pixel-space layout values.
|
||
/// Handles anchor-based positioning and Y inversion.
|
||
/// </summary>
|
||
private static void BakeLayout(
|
||
RectTransform rt, RectTransform canvasRect,
|
||
float scaleX, float scaleY, Vector2 resolution,
|
||
out short x, out short y, out short w, out short h,
|
||
out byte anchorMinX, out byte anchorMinY,
|
||
out byte anchorMaxX, out byte anchorMaxY)
|
||
{
|
||
// Anchor values in 8.8 fixed point (0-255 maps to 0.0-~1.0)
|
||
anchorMinX = (byte)Mathf.Clamp(Mathf.RoundToInt(rt.anchorMin.x * 255f), 0, 255);
|
||
anchorMinY = (byte)Mathf.Clamp(Mathf.RoundToInt((1f - rt.anchorMax.y) * 255f), 0, 255); // Y invert
|
||
anchorMaxX = (byte)Mathf.Clamp(Mathf.RoundToInt(rt.anchorMax.x * 255f), 0, 255);
|
||
anchorMaxY = (byte)Mathf.Clamp(Mathf.RoundToInt((1f - rt.anchorMin.y) * 255f), 0, 255); // Y invert
|
||
|
||
if (Mathf.Approximately(rt.anchorMin.x, rt.anchorMax.x) &&
|
||
Mathf.Approximately(rt.anchorMin.y, rt.anchorMax.y))
|
||
{
|
||
// Fixed-size element with single anchor point
|
||
// anchoredPosition is the offset from the anchor in canvas pixels
|
||
float px = rt.anchoredPosition.x * scaleX;
|
||
float py = -rt.anchoredPosition.y * scaleY; // Y invert
|
||
float pw = rt.rect.width * scaleX;
|
||
float ph = rt.rect.height * scaleY;
|
||
|
||
// Adjust for pivot (anchoredPosition is at the pivot point)
|
||
px -= rt.pivot.x * pw;
|
||
py -= (1f - rt.pivot.y) * ph; // pivot Y inverted
|
||
|
||
x = (short)Mathf.RoundToInt(px);
|
||
y = (short)Mathf.RoundToInt(py);
|
||
w = (short)Mathf.Max(1, Mathf.RoundToInt(pw));
|
||
h = (short)Mathf.Max(1, Mathf.RoundToInt(ph));
|
||
}
|
||
else
|
||
{
|
||
// Stretched element: offsets from anchored edges
|
||
// offsetMin = distance from anchorMin corner, offsetMax = distance from anchorMax corner
|
||
float leftOff = rt.offsetMin.x * scaleX;
|
||
float rightOff = rt.offsetMax.x * scaleX;
|
||
float topOff = -rt.offsetMax.y * scaleY; // Y invert
|
||
float bottomOff = -rt.offsetMin.y * scaleY; // Y invert
|
||
|
||
// For stretched elements, x/y store the offset from the anchor start,
|
||
// and w/h store the combined inset (negative = shrink)
|
||
x = (short)Mathf.RoundToInt(leftOff);
|
||
y = (short)Mathf.RoundToInt(topOff);
|
||
w = (short)Mathf.RoundToInt(rightOff - leftOff);
|
||
h = (short)Mathf.RoundToInt(bottomOff - topOff);
|
||
}
|
||
}
|
||
|
||
private static string TruncateName(string name, int maxLen = 24)
|
||
{
|
||
if (string.IsNullOrEmpty(name)) return "";
|
||
return name.Length > maxLen ? name.Substring(0, maxLen) : name;
|
||
}
|
||
|
||
// ─── Collectors ───
|
||
|
||
/// <summary>
|
||
/// Walk the hierarchy depth-first in sibling order, collecting every
|
||
/// PSX UI component into <paramref name="elements"/> so that draw order
|
||
/// matches the Unity scene tree (top-to-bottom = back-to-front).
|
||
/// </summary>
|
||
private static void CollectAllElementsInHierarchyOrder(
|
||
Transform root, RectTransform canvasRect,
|
||
float scaleX, float scaleY, Vector2 resolution,
|
||
List<PSXUIElementData> elements,
|
||
List<PSXFontAsset> uniqueFonts)
|
||
{
|
||
// GetComponentsInChildren iterates depth-first in sibling order —
|
||
// exactly the hierarchy ordering we want.
|
||
Transform[] allTransforms = root.GetComponentsInChildren<Transform>(true);
|
||
foreach (Transform t in allTransforms)
|
||
{
|
||
if (t == root) continue; // skip the canvas root itself
|
||
|
||
// Check each supported component type on this transform.
|
||
// A single GameObject should only have one PSX UI component,
|
||
// but we check all to be safe.
|
||
PSXUIImage img = t.GetComponent<PSXUIImage>();
|
||
if (img != null)
|
||
{
|
||
CollectSingleImage(img, canvasRect, scaleX, scaleY, resolution, elements);
|
||
continue;
|
||
}
|
||
|
||
PSXUIBox box = t.GetComponent<PSXUIBox>();
|
||
if (box != null)
|
||
{
|
||
CollectSingleBox(box, canvasRect, scaleX, scaleY, resolution, elements);
|
||
continue;
|
||
}
|
||
|
||
PSXUIText txt = t.GetComponent<PSXUIText>();
|
||
if (txt != null)
|
||
{
|
||
CollectSingleText(txt, canvasRect, scaleX, scaleY, resolution, elements, uniqueFonts);
|
||
continue;
|
||
}
|
||
|
||
PSXUIProgressBar bar = t.GetComponent<PSXUIProgressBar>();
|
||
if (bar != null)
|
||
{
|
||
CollectSingleProgressBar(bar, canvasRect, scaleX, scaleY, resolution, elements);
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
|
||
private static void CollectSingleImage(
|
||
PSXUIImage img, RectTransform canvasRect,
|
||
float scaleX, float scaleY, Vector2 resolution,
|
||
List<PSXUIElementData> elements)
|
||
{
|
||
RectTransform rt = img.GetComponent<RectTransform>();
|
||
if (rt == null) return;
|
||
|
||
BakeLayout(rt, canvasRect, scaleX, scaleY, resolution,
|
||
out short x, out short y, out short w, out short h,
|
||
out byte amin_x, out byte amin_y, out byte amax_x, out byte amax_y);
|
||
|
||
var data = new PSXUIElementData
|
||
{
|
||
Type = PSXUIElementType.Image,
|
||
StartVisible = img.StartVisible,
|
||
Name = TruncateName(img.ElementName),
|
||
X = x, Y = y, W = w, H = h,
|
||
AnchorMinX = amin_x, AnchorMinY = amin_y,
|
||
AnchorMaxX = amax_x, AnchorMaxY = amax_y,
|
||
ColorR = (byte)Mathf.Clamp(Mathf.RoundToInt(img.TintColor.r * 255f), 0, 255),
|
||
ColorG = (byte)Mathf.Clamp(Mathf.RoundToInt(img.TintColor.g * 255f), 0, 255),
|
||
ColorB = (byte)Mathf.Clamp(Mathf.RoundToInt(img.TintColor.b * 255f), 0, 255),
|
||
};
|
||
|
||
if (img.PackedTexture != null)
|
||
{
|
||
PSXTexture2D tex = img.PackedTexture;
|
||
int expander = 16 / (int)tex.BitDepth;
|
||
data.TexpageX = tex.TexpageX;
|
||
data.TexpageY = tex.TexpageY;
|
||
data.ClutX = (ushort)tex.ClutPackingX;
|
||
data.ClutY = (ushort)tex.ClutPackingY;
|
||
data.U0 = (byte)(tex.PackingX * expander);
|
||
data.V0 = (byte)tex.PackingY;
|
||
data.U1 = (byte)(tex.PackingX * expander + tex.Width - 1);
|
||
data.V1 = (byte)(tex.PackingY + tex.Height - 1);
|
||
data.BitDepthIndex = tex.BitDepth switch
|
||
{
|
||
PSXBPP.TEX_4BIT => 0,
|
||
PSXBPP.TEX_8BIT => 1,
|
||
PSXBPP.TEX_16BIT => 2,
|
||
_ => 2
|
||
};
|
||
|
||
Debug.Log($"[UIImage] '{img.ElementName}' src='{(tex.OriginalTexture ? tex.OriginalTexture.name : "null")}' " +
|
||
$"bpp={(int)tex.BitDepth} W={tex.Width} H={tex.Height} QW={tex.QuantizedWidth} " +
|
||
$"packXY=({tex.PackingX},{tex.PackingY}) tpage=({tex.TexpageX},{tex.TexpageY}) " +
|
||
$"clutXY=({tex.ClutPackingX},{tex.ClutPackingY}) " +
|
||
$"UV=({data.U0},{data.V0})->({data.U1},{data.V1}) expander={expander} bitIdx={data.BitDepthIndex}");
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning($"[UIImage] '{img.ElementName}' has NULL PackedTexture!");
|
||
}
|
||
|
||
elements.Add(data);
|
||
}
|
||
|
||
private static void CollectSingleBox(
|
||
PSXUIBox box, RectTransform canvasRect,
|
||
float scaleX, float scaleY, Vector2 resolution,
|
||
List<PSXUIElementData> elements)
|
||
{
|
||
RectTransform rt = box.GetComponent<RectTransform>();
|
||
if (rt == null) return;
|
||
|
||
BakeLayout(rt, canvasRect, scaleX, scaleY, resolution,
|
||
out short x, out short y, out short w, out short h,
|
||
out byte amin_x, out byte amin_y, out byte amax_x, out byte amax_y);
|
||
|
||
elements.Add(new PSXUIElementData
|
||
{
|
||
Type = PSXUIElementType.Box,
|
||
StartVisible = box.StartVisible,
|
||
Name = TruncateName(box.ElementName),
|
||
X = x, Y = y, W = w, H = h,
|
||
AnchorMinX = amin_x, AnchorMinY = amin_y,
|
||
AnchorMaxX = amax_x, AnchorMaxY = amax_y,
|
||
ColorR = (byte)Mathf.Clamp(Mathf.RoundToInt(box.BoxColor.r * 255f), 0, 255),
|
||
ColorG = (byte)Mathf.Clamp(Mathf.RoundToInt(box.BoxColor.g * 255f), 0, 255),
|
||
ColorB = (byte)Mathf.Clamp(Mathf.RoundToInt(box.BoxColor.b * 255f), 0, 255),
|
||
});
|
||
}
|
||
|
||
private static void CollectSingleText(
|
||
PSXUIText txt, RectTransform canvasRect,
|
||
float scaleX, float scaleY, Vector2 resolution,
|
||
List<PSXUIElementData> elements,
|
||
List<PSXFontAsset> uniqueFonts)
|
||
{
|
||
RectTransform rt = txt.GetComponent<RectTransform>();
|
||
if (rt == null) return;
|
||
|
||
BakeLayout(rt, canvasRect, scaleX, scaleY, resolution,
|
||
out short x, out short y, out short w, out short h,
|
||
out byte amin_x, out byte amin_y, out byte amax_x, out byte amax_y);
|
||
|
||
string defaultText = txt.DefaultText ?? "";
|
||
if (defaultText.Length > 63) defaultText = defaultText.Substring(0, 63);
|
||
|
||
byte fontIndex = 0;
|
||
PSXFontAsset effectiveFont = txt.GetEffectiveFont();
|
||
if (effectiveFont != null && uniqueFonts != null)
|
||
{
|
||
int idx = uniqueFonts.IndexOf(effectiveFont);
|
||
if (idx >= 0) fontIndex = (byte)(idx + 1);
|
||
}
|
||
|
||
elements.Add(new PSXUIElementData
|
||
{
|
||
Type = PSXUIElementType.Text,
|
||
StartVisible = txt.StartVisible,
|
||
Name = TruncateName(txt.ElementName),
|
||
X = x, Y = y, W = w, H = h,
|
||
AnchorMinX = amin_x, AnchorMinY = amin_y,
|
||
AnchorMaxX = amax_x, AnchorMaxY = amax_y,
|
||
ColorR = (byte)Mathf.Clamp(Mathf.RoundToInt(txt.TextColor.r * 255f), 0, 255),
|
||
ColorG = (byte)Mathf.Clamp(Mathf.RoundToInt(txt.TextColor.g * 255f), 0, 255),
|
||
ColorB = (byte)Mathf.Clamp(Mathf.RoundToInt(txt.TextColor.b * 255f), 0, 255),
|
||
DefaultText = defaultText,
|
||
FontIndex = fontIndex,
|
||
});
|
||
}
|
||
|
||
private static void CollectSingleProgressBar(
|
||
PSXUIProgressBar bar, RectTransform canvasRect,
|
||
float scaleX, float scaleY, Vector2 resolution,
|
||
List<PSXUIElementData> elements)
|
||
{
|
||
RectTransform rt = bar.GetComponent<RectTransform>();
|
||
if (rt == null) return;
|
||
|
||
BakeLayout(rt, canvasRect, scaleX, scaleY, resolution,
|
||
out short x, out short y, out short w, out short h,
|
||
out byte amin_x, out byte amin_y, out byte amax_x, out byte amax_y);
|
||
|
||
elements.Add(new PSXUIElementData
|
||
{
|
||
Type = PSXUIElementType.Progress,
|
||
StartVisible = bar.StartVisible,
|
||
Name = TruncateName(bar.ElementName),
|
||
X = x, Y = y, W = w, H = h,
|
||
AnchorMinX = amin_x, AnchorMinY = amin_y,
|
||
AnchorMaxX = amax_x, AnchorMaxY = amax_y,
|
||
ColorR = (byte)Mathf.Clamp(Mathf.RoundToInt(bar.FillColor.r * 255f), 0, 255),
|
||
ColorG = (byte)Mathf.Clamp(Mathf.RoundToInt(bar.FillColor.g * 255f), 0, 255),
|
||
ColorB = (byte)Mathf.Clamp(Mathf.RoundToInt(bar.FillColor.b * 255f), 0, 255),
|
||
BgR = (byte)Mathf.Clamp(Mathf.RoundToInt(bar.BackgroundColor.r * 255f), 0, 255),
|
||
BgG = (byte)Mathf.Clamp(Mathf.RoundToInt(bar.BackgroundColor.g * 255f), 0, 255),
|
||
BgB = (byte)Mathf.Clamp(Mathf.RoundToInt(bar.BackgroundColor.b * 255f), 0, 255),
|
||
ProgressValue = (byte)bar.InitialValue,
|
||
});
|
||
}
|
||
|
||
// ─── Legacy per-type collectors (kept for reference, no longer called) ───
|
||
|
||
private static void CollectImages(
|
||
Transform root, RectTransform canvasRect,
|
||
float scaleX, float scaleY, Vector2 resolution,
|
||
List<PSXUIElementData> elements)
|
||
{
|
||
PSXUIImage[] images = root.GetComponentsInChildren<PSXUIImage>(true);
|
||
foreach (PSXUIImage img in images)
|
||
{
|
||
RectTransform rt = img.GetComponent<RectTransform>();
|
||
if (rt == null) continue;
|
||
|
||
BakeLayout(rt, canvasRect, scaleX, scaleY, resolution,
|
||
out short x, out short y, out short w, out short h,
|
||
out byte amin_x, out byte amin_y, out byte amax_x, out byte amax_y);
|
||
|
||
var data = new PSXUIElementData
|
||
{
|
||
Type = PSXUIElementType.Image,
|
||
StartVisible = img.StartVisible,
|
||
Name = TruncateName(img.ElementName),
|
||
X = x, Y = y, W = w, H = h,
|
||
AnchorMinX = amin_x, AnchorMinY = amin_y,
|
||
AnchorMaxX = amax_x, AnchorMaxY = amax_y,
|
||
ColorR = (byte)Mathf.Clamp(Mathf.RoundToInt(img.TintColor.r * 255f), 0, 255),
|
||
ColorG = (byte)Mathf.Clamp(Mathf.RoundToInt(img.TintColor.g * 255f), 0, 255),
|
||
ColorB = (byte)Mathf.Clamp(Mathf.RoundToInt(img.TintColor.b * 255f), 0, 255),
|
||
};
|
||
|
||
// Image texture data is filled in after VRAM packing by
|
||
// FillImageTextureData() — see PSXSceneExporter integration
|
||
if (img.PackedTexture != null)
|
||
{
|
||
PSXTexture2D tex = img.PackedTexture;
|
||
// Convert PackingX from VRAM halfwords to texture-pixel U coords.
|
||
// 4bpp: 4 pixels per halfword, 8bpp: 2, 16bpp: 1
|
||
int expander = 16 / (int)tex.BitDepth;
|
||
data.TexpageX = tex.TexpageX;
|
||
data.TexpageY = tex.TexpageY;
|
||
data.ClutX = (ushort)tex.ClutPackingX;
|
||
data.ClutY = (ushort)tex.ClutPackingY;
|
||
data.U0 = (byte)(tex.PackingX * expander);
|
||
data.V0 = (byte)tex.PackingY;
|
||
// U1/V1 are the LAST texel (inclusive), not one-past-end.
|
||
// Without -1, values >= 256 overflow byte to 0.
|
||
data.U1 = (byte)(tex.PackingX * expander + tex.Width - 1);
|
||
data.V1 = (byte)(tex.PackingY + tex.Height - 1);
|
||
data.BitDepthIndex = tex.BitDepth switch
|
||
{
|
||
PSXBPP.TEX_4BIT => 0,
|
||
PSXBPP.TEX_8BIT => 1,
|
||
PSXBPP.TEX_16BIT => 2,
|
||
_ => 2
|
||
};
|
||
|
||
Debug.Log($"[UIImage] '{img.ElementName}' src='{(tex.OriginalTexture ? tex.OriginalTexture.name : "null")}' " +
|
||
$"bpp={(int)tex.BitDepth} W={tex.Width} H={tex.Height} QW={tex.QuantizedWidth} " +
|
||
$"packXY=({tex.PackingX},{tex.PackingY}) tpage=({tex.TexpageX},{tex.TexpageY}) " +
|
||
$"clutXY=({tex.ClutPackingX},{tex.ClutPackingY}) " +
|
||
$"UV=({data.U0},{data.V0})->({data.U1},{data.V1}) expander={expander} bitIdx={data.BitDepthIndex}");
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning($"[UIImage] '{img.ElementName}' has NULL PackedTexture!");
|
||
}
|
||
|
||
elements.Add(data);
|
||
}
|
||
}
|
||
|
||
private static void CollectBoxes(
|
||
Transform root, RectTransform canvasRect,
|
||
float scaleX, float scaleY, Vector2 resolution,
|
||
List<PSXUIElementData> elements)
|
||
{
|
||
PSXUIBox[] boxes = root.GetComponentsInChildren<PSXUIBox>(true);
|
||
foreach (PSXUIBox box in boxes)
|
||
{
|
||
RectTransform rt = box.GetComponent<RectTransform>();
|
||
if (rt == null) continue;
|
||
|
||
BakeLayout(rt, canvasRect, scaleX, scaleY, resolution,
|
||
out short x, out short y, out short w, out short h,
|
||
out byte amin_x, out byte amin_y, out byte amax_x, out byte amax_y);
|
||
|
||
elements.Add(new PSXUIElementData
|
||
{
|
||
Type = PSXUIElementType.Box,
|
||
StartVisible = box.StartVisible,
|
||
Name = TruncateName(box.ElementName),
|
||
X = x, Y = y, W = w, H = h,
|
||
AnchorMinX = amin_x, AnchorMinY = amin_y,
|
||
AnchorMaxX = amax_x, AnchorMaxY = amax_y,
|
||
ColorR = (byte)Mathf.Clamp(Mathf.RoundToInt(box.BoxColor.r * 255f), 0, 255),
|
||
ColorG = (byte)Mathf.Clamp(Mathf.RoundToInt(box.BoxColor.g * 255f), 0, 255),
|
||
ColorB = (byte)Mathf.Clamp(Mathf.RoundToInt(box.BoxColor.b * 255f), 0, 255),
|
||
});
|
||
}
|
||
}
|
||
|
||
private static void CollectTexts(
|
||
Transform root, RectTransform canvasRect,
|
||
float scaleX, float scaleY, Vector2 resolution,
|
||
List<PSXUIElementData> elements,
|
||
List<PSXFontAsset> uniqueFonts = null)
|
||
{
|
||
PSXUIText[] texts = root.GetComponentsInChildren<PSXUIText>(true);
|
||
foreach (PSXUIText txt in texts)
|
||
{
|
||
RectTransform rt = txt.GetComponent<RectTransform>();
|
||
if (rt == null) continue;
|
||
|
||
BakeLayout(rt, canvasRect, scaleX, scaleY, resolution,
|
||
out short x, out short y, out short w, out short h,
|
||
out byte amin_x, out byte amin_y, out byte amax_x, out byte amax_y);
|
||
|
||
string defaultText = txt.DefaultText ?? "";
|
||
if (defaultText.Length > 63) defaultText = defaultText.Substring(0, 63);
|
||
|
||
// Resolve font index: 0 = system font, 1+ = custom font
|
||
byte fontIndex = 0;
|
||
PSXFontAsset effectiveFont = txt.GetEffectiveFont();
|
||
if (effectiveFont != null && uniqueFonts != null)
|
||
{
|
||
int idx = uniqueFonts.IndexOf(effectiveFont);
|
||
if (idx >= 0) fontIndex = (byte)(idx + 1); // 1-based for custom fonts
|
||
}
|
||
|
||
elements.Add(new PSXUIElementData
|
||
{
|
||
Type = PSXUIElementType.Text,
|
||
StartVisible = txt.StartVisible,
|
||
Name = TruncateName(txt.ElementName),
|
||
X = x, Y = y, W = w, H = h,
|
||
AnchorMinX = amin_x, AnchorMinY = amin_y,
|
||
AnchorMaxX = amax_x, AnchorMaxY = amax_y,
|
||
ColorR = (byte)Mathf.Clamp(Mathf.RoundToInt(txt.TextColor.r * 255f), 0, 255),
|
||
ColorG = (byte)Mathf.Clamp(Mathf.RoundToInt(txt.TextColor.g * 255f), 0, 255),
|
||
ColorB = (byte)Mathf.Clamp(Mathf.RoundToInt(txt.TextColor.b * 255f), 0, 255),
|
||
DefaultText = defaultText,
|
||
FontIndex = fontIndex,
|
||
});
|
||
}
|
||
}
|
||
|
||
private static void CollectProgressBars(
|
||
Transform root, RectTransform canvasRect,
|
||
float scaleX, float scaleY, Vector2 resolution,
|
||
List<PSXUIElementData> elements)
|
||
{
|
||
PSXUIProgressBar[] bars = root.GetComponentsInChildren<PSXUIProgressBar>(true);
|
||
foreach (PSXUIProgressBar bar in bars)
|
||
{
|
||
RectTransform rt = bar.GetComponent<RectTransform>();
|
||
if (rt == null) continue;
|
||
|
||
BakeLayout(rt, canvasRect, scaleX, scaleY, resolution,
|
||
out short x, out short y, out short w, out short h,
|
||
out byte amin_x, out byte amin_y, out byte amax_x, out byte amax_y);
|
||
|
||
elements.Add(new PSXUIElementData
|
||
{
|
||
Type = PSXUIElementType.Progress,
|
||
StartVisible = bar.StartVisible,
|
||
Name = TruncateName(bar.ElementName),
|
||
X = x, Y = y, W = w, H = h,
|
||
AnchorMinX = amin_x, AnchorMinY = amin_y,
|
||
AnchorMaxX = amax_x, AnchorMaxY = amax_y,
|
||
// Fill color goes into primary color (used for the fill bar)
|
||
ColorR = (byte)Mathf.Clamp(Mathf.RoundToInt(bar.FillColor.r * 255f), 0, 255),
|
||
ColorG = (byte)Mathf.Clamp(Mathf.RoundToInt(bar.FillColor.g * 255f), 0, 255),
|
||
ColorB = (byte)Mathf.Clamp(Mathf.RoundToInt(bar.FillColor.b * 255f), 0, 255),
|
||
// Background color goes into progress-specific fields
|
||
BgR = (byte)Mathf.Clamp(Mathf.RoundToInt(bar.BackgroundColor.r * 255f), 0, 255),
|
||
BgG = (byte)Mathf.Clamp(Mathf.RoundToInt(bar.BackgroundColor.g * 255f), 0, 255),
|
||
BgB = (byte)Mathf.Clamp(Mathf.RoundToInt(bar.BackgroundColor.b * 255f), 0, 255),
|
||
ProgressValue = (byte)bar.InitialValue,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|