Broken UI system
This commit is contained in:
446
Editor/Inspectors/PSXUIEditors.cs
Normal file
446
Editor/Inspectors/PSXUIEditors.cs
Normal file
@@ -0,0 +1,446 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using UnityEditor;
|
||||||
|
using SplashEdit.RuntimeCode;
|
||||||
|
|
||||||
|
namespace SplashEdit.EditorCode
|
||||||
|
{
|
||||||
|
// ─── Scene Preview Gizmos ───
|
||||||
|
// These draw filled rectangles in the Scene view for WYSIWYG UI preview.
|
||||||
|
|
||||||
|
public static class PSXUIGizmos
|
||||||
|
{
|
||||||
|
[DrawGizmo(GizmoType.NonSelected | GizmoType.Selected)]
|
||||||
|
static void DrawBoxGizmo(PSXUIBox box, GizmoType gizmoType)
|
||||||
|
{
|
||||||
|
RectTransform rt = box.GetComponent<RectTransform>();
|
||||||
|
if (rt == null) return;
|
||||||
|
Vector3[] corners = new Vector3[4];
|
||||||
|
rt.GetWorldCorners(corners);
|
||||||
|
Color fill = box.BoxColor;
|
||||||
|
fill.a = (gizmoType & GizmoType.Selected) != 0 ? 0.8f : 0.5f;
|
||||||
|
Color border = Color.white;
|
||||||
|
border.a = (gizmoType & GizmoType.Selected) != 0 ? 1f : 0.4f;
|
||||||
|
Handles.DrawSolidRectangleWithOutline(corners, fill, border);
|
||||||
|
}
|
||||||
|
|
||||||
|
[DrawGizmo(GizmoType.NonSelected | GizmoType.Selected)]
|
||||||
|
static void DrawImageGizmo(PSXUIImage image, GizmoType gizmoType)
|
||||||
|
{
|
||||||
|
RectTransform rt = image.GetComponent<RectTransform>();
|
||||||
|
if (rt == null) return;
|
||||||
|
Vector3[] corners = new Vector3[4];
|
||||||
|
rt.GetWorldCorners(corners);
|
||||||
|
|
||||||
|
bool selected = (gizmoType & GizmoType.Selected) != 0;
|
||||||
|
|
||||||
|
// Draw texture preview if available
|
||||||
|
if (image.SourceTexture != null)
|
||||||
|
{
|
||||||
|
Color tint = image.TintColor;
|
||||||
|
tint.a = selected ? 0.9f : 0.6f;
|
||||||
|
Handles.DrawSolidRectangleWithOutline(corners, tint * 0.3f, tint);
|
||||||
|
|
||||||
|
// Draw texture in GUI overlay
|
||||||
|
Handles.BeginGUI();
|
||||||
|
Vector2 min = HandleUtility.WorldToGUIPoint(corners[0]);
|
||||||
|
Vector2 max = HandleUtility.WorldToGUIPoint(corners[2]);
|
||||||
|
Rect screenRect = new Rect(
|
||||||
|
Mathf.Min(min.x, max.x), Mathf.Min(min.y, max.y),
|
||||||
|
Mathf.Abs(max.x - min.x), Mathf.Abs(max.y - min.y));
|
||||||
|
if (screenRect.width > 2 && screenRect.height > 2)
|
||||||
|
{
|
||||||
|
GUI.color = new Color(tint.r, tint.g, tint.b, selected ? 0.9f : 0.5f);
|
||||||
|
GUI.DrawTexture(screenRect, image.SourceTexture, ScaleMode.StretchToFill);
|
||||||
|
GUI.color = Color.white;
|
||||||
|
}
|
||||||
|
Handles.EndGUI();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Color fill = new Color(0.4f, 0.4f, 0.8f, selected ? 0.4f : 0.2f);
|
||||||
|
Handles.DrawSolidRectangleWithOutline(corners, fill, Color.cyan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DrawGizmo(GizmoType.NonSelected | GizmoType.Selected)]
|
||||||
|
static void DrawTextGizmo(PSXUIText text, GizmoType gizmoType)
|
||||||
|
{
|
||||||
|
RectTransform rt = text.GetComponent<RectTransform>();
|
||||||
|
if (rt == null) return;
|
||||||
|
Vector3[] corners = new Vector3[4];
|
||||||
|
rt.GetWorldCorners(corners);
|
||||||
|
|
||||||
|
bool selected = (gizmoType & GizmoType.Selected) != 0;
|
||||||
|
Color border = text.TextColor;
|
||||||
|
border.a = selected ? 1f : 0.5f;
|
||||||
|
Color fill = new Color(0, 0, 0, selected ? 0.3f : 0.1f);
|
||||||
|
Handles.DrawSolidRectangleWithOutline(corners, fill, border);
|
||||||
|
|
||||||
|
// Draw text preview at actual PSX glyph size.
|
||||||
|
// On the PS1, chainprintf renders each glyph top-left aligned at the
|
||||||
|
// element's position — no centering. Mimic that here.
|
||||||
|
string label = string.IsNullOrEmpty(text.DefaultText) ? "[empty]" : text.DefaultText;
|
||||||
|
|
||||||
|
PSXFontAsset font = text.GetEffectiveFont();
|
||||||
|
int glyphW = font != null ? font.GlyphWidth : 8;
|
||||||
|
int glyphH = font != null ? font.GlyphHeight : 16;
|
||||||
|
|
||||||
|
Handles.BeginGUI();
|
||||||
|
// Convert top-left corner (corners[1] in Unity is top-left after GetWorldCorners)
|
||||||
|
Vector2 topLeft = HandleUtility.WorldToGUIPoint(corners[1]);
|
||||||
|
Vector2 botRight = HandleUtility.WorldToGUIPoint(corners[3]);
|
||||||
|
|
||||||
|
// Calculate pixel scale: how many screen pixels represent one PSX pixel
|
||||||
|
float rectWorldW = Vector3.Distance(corners[0], corners[3]);
|
||||||
|
float rectScreenW = Mathf.Abs(botRight.x - topLeft.x);
|
||||||
|
float rectW = rt.rect.width;
|
||||||
|
float psxPixelScale = (rectW > 0.01f) ? rectScreenW / rectW : 1f;
|
||||||
|
|
||||||
|
float charScreenW = glyphW * psxPixelScale;
|
||||||
|
float charScreenH = glyphH * psxPixelScale;
|
||||||
|
int fontSize = Mathf.Clamp(Mathf.RoundToInt(charScreenH * 0.75f), 6, 72);
|
||||||
|
|
||||||
|
GUIStyle style = new GUIStyle(EditorStyles.label);
|
||||||
|
style.normal.textColor = text.TextColor;
|
||||||
|
style.alignment = TextAnchor.UpperLeft;
|
||||||
|
style.fontSize = fontSize;
|
||||||
|
style.wordWrap = false;
|
||||||
|
style.clipping = TextClipping.Clip;
|
||||||
|
style.padding = new RectOffset(0, 0, 0, 0);
|
||||||
|
|
||||||
|
// Render position = top-left of the rect (matching PSX chainprintf behavior)
|
||||||
|
float guiW = Mathf.Abs(botRight.x - topLeft.x);
|
||||||
|
float guiH = Mathf.Abs(botRight.y - topLeft.y);
|
||||||
|
Rect guiRect = new Rect(
|
||||||
|
Mathf.Min(topLeft.x, botRight.x),
|
||||||
|
Mathf.Min(topLeft.y, botRight.y),
|
||||||
|
guiW, guiH);
|
||||||
|
|
||||||
|
GUI.color = new Color(text.TextColor.r, text.TextColor.g, text.TextColor.b, selected ? 1f : 0.7f);
|
||||||
|
GUI.Label(guiRect, label, style);
|
||||||
|
GUI.color = Color.white;
|
||||||
|
Handles.EndGUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
[DrawGizmo(GizmoType.NonSelected | GizmoType.Selected)]
|
||||||
|
static void DrawProgressBarGizmo(PSXUIProgressBar bar, GizmoType gizmoType)
|
||||||
|
{
|
||||||
|
RectTransform rt = bar.GetComponent<RectTransform>();
|
||||||
|
if (rt == null) return;
|
||||||
|
Vector3[] corners = new Vector3[4];
|
||||||
|
rt.GetWorldCorners(corners);
|
||||||
|
|
||||||
|
bool selected = (gizmoType & GizmoType.Selected) != 0;
|
||||||
|
|
||||||
|
// Background
|
||||||
|
Color bgColor = bar.BackgroundColor;
|
||||||
|
bgColor.a = selected ? 0.8f : 0.5f;
|
||||||
|
Handles.DrawSolidRectangleWithOutline(corners, bgColor, Color.white * (selected ? 1f : 0.4f));
|
||||||
|
|
||||||
|
// Fill portion
|
||||||
|
float t = bar.InitialValue / 100f;
|
||||||
|
if (t > 0.001f)
|
||||||
|
{
|
||||||
|
Vector3[] fillCorners = new Vector3[4];
|
||||||
|
fillCorners[0] = corners[0]; // bottom-left
|
||||||
|
fillCorners[1] = corners[1]; // top-left
|
||||||
|
fillCorners[2] = Vector3.Lerp(corners[1], corners[2], t); // top-right (partial)
|
||||||
|
fillCorners[3] = Vector3.Lerp(corners[0], corners[3], t); // bottom-right (partial)
|
||||||
|
Color fillColor = bar.FillColor;
|
||||||
|
fillColor.a = selected ? 0.9f : 0.6f;
|
||||||
|
Handles.DrawSolidRectangleWithOutline(fillCorners, fillColor, Color.clear);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DrawGizmo(GizmoType.NonSelected | GizmoType.Selected)]
|
||||||
|
static void DrawCanvasGizmo(PSXCanvas canvas, GizmoType gizmoType)
|
||||||
|
{
|
||||||
|
RectTransform rt = canvas.GetComponent<RectTransform>();
|
||||||
|
if (rt == null) return;
|
||||||
|
Vector3[] corners = new Vector3[4];
|
||||||
|
rt.GetWorldCorners(corners);
|
||||||
|
bool selected = (gizmoType & GizmoType.Selected) != 0;
|
||||||
|
Color border = selected ? Color.yellow : new Color(1, 1, 0, 0.3f);
|
||||||
|
Handles.DrawSolidRectangleWithOutline(corners, Color.clear, border);
|
||||||
|
|
||||||
|
// Label
|
||||||
|
if (selected)
|
||||||
|
{
|
||||||
|
Vector2 res = PSXCanvas.PSXResolution;
|
||||||
|
Vector3 topMid = (corners[1] + corners[2]) * 0.5f;
|
||||||
|
string label = $"PSX Canvas: {canvas.CanvasName} ({res.x}x{res.y})";
|
||||||
|
GUIStyle style = new GUIStyle(EditorStyles.boldLabel);
|
||||||
|
style.normal.textColor = Color.yellow;
|
||||||
|
Handles.Label(topMid, label, style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Custom inspector for PSXCanvas component.
|
||||||
|
/// Shows canvas name, visibility, sort order, font, and a summary of child elements.
|
||||||
|
/// </summary>
|
||||||
|
[CustomEditor(typeof(PSXCanvas))]
|
||||||
|
public class PSXCanvasEditor : Editor
|
||||||
|
{
|
||||||
|
public override void OnInspectorGUI()
|
||||||
|
{
|
||||||
|
serializedObject.Update();
|
||||||
|
|
||||||
|
Vector2 res = PSXCanvas.PSXResolution;
|
||||||
|
EditorGUILayout.LabelField($"PSX Canvas ({res.x}x{res.y})", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.Space(4);
|
||||||
|
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("canvasName"), new GUIContent("Canvas Name",
|
||||||
|
"Name used from Lua: UI.FindCanvas(\"name\"). Max 24 chars."));
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("startVisible"), new GUIContent("Start Visible",
|
||||||
|
"Whether the canvas is visible when the scene loads."));
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("sortOrder"), new GUIContent("Sort Order",
|
||||||
|
"Render priority (0 = back, 255 = front)."));
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("defaultFont"), new GUIContent("Default Font",
|
||||||
|
"Default custom font for text elements. If empty, uses built-in system font (8x16)."));
|
||||||
|
|
||||||
|
EditorGUILayout.Space(4);
|
||||||
|
|
||||||
|
// Force Canvas configuration button
|
||||||
|
if (GUILayout.Button($"Reset Canvas to {res.x}x{res.y}"))
|
||||||
|
{
|
||||||
|
PSXCanvas.InvalidateResolutionCache();
|
||||||
|
((PSXCanvas)target).ConfigureCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.Space(4);
|
||||||
|
|
||||||
|
// Element summary
|
||||||
|
PSXCanvas canvas = (PSXCanvas)target;
|
||||||
|
int imageCount = canvas.GetComponentsInChildren<PSXUIImage>(true).Length;
|
||||||
|
int boxCount = canvas.GetComponentsInChildren<PSXUIBox>(true).Length;
|
||||||
|
int textCount = canvas.GetComponentsInChildren<PSXUIText>(true).Length;
|
||||||
|
int progressCount = canvas.GetComponentsInChildren<PSXUIProgressBar>(true).Length;
|
||||||
|
int total = imageCount + boxCount + textCount + progressCount;
|
||||||
|
|
||||||
|
EditorGUILayout.HelpBox(
|
||||||
|
$"Elements: {total} total\n" +
|
||||||
|
$" Images: {imageCount} | Boxes: {boxCount}\n" +
|
||||||
|
$" Texts: {textCount} | Progress Bars: {progressCount}",
|
||||||
|
total > 128 ? MessageType.Warning : MessageType.Info);
|
||||||
|
|
||||||
|
if (total > 128)
|
||||||
|
EditorGUILayout.HelpBox("PS1 UI system supports max 128 elements total across all canvases.", MessageType.Error);
|
||||||
|
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Custom inspector for PSXUIImage component.
|
||||||
|
/// </summary>
|
||||||
|
[CustomEditor(typeof(PSXUIImage))]
|
||||||
|
public class PSXUIImageEditor : Editor
|
||||||
|
{
|
||||||
|
public override void OnInspectorGUI()
|
||||||
|
{
|
||||||
|
serializedObject.Update();
|
||||||
|
|
||||||
|
EditorGUILayout.LabelField("PSX UI Image", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.Space(4);
|
||||||
|
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("elementName"), new GUIContent("Element Name",
|
||||||
|
"Name used from Lua: UI.FindElement(canvas, \"name\"). Max 24 chars."));
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("sourceTexture"), new GUIContent("Source Texture",
|
||||||
|
"Texture to quantize and pack into VRAM."));
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("bitDepth"), new GUIContent("Bit Depth",
|
||||||
|
"VRAM storage depth. 4-bit = 16 colors, 8-bit = 256 colors, 16-bit = direct color."));
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("tintColor"), new GUIContent("Tint Color",
|
||||||
|
"Color multiply applied to the image (white = no tint)."));
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("startVisible"));
|
||||||
|
|
||||||
|
// Texture size warning
|
||||||
|
PSXUIImage img = (PSXUIImage)target;
|
||||||
|
if (img.SourceTexture != null)
|
||||||
|
{
|
||||||
|
if (img.SourceTexture.width > 256 || img.SourceTexture.height > 256)
|
||||||
|
EditorGUILayout.HelpBox("Texture exceeds 256×256. It will be resized during export.", MessageType.Warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Custom inspector for PSXUIBox component.
|
||||||
|
/// </summary>
|
||||||
|
[CustomEditor(typeof(PSXUIBox))]
|
||||||
|
public class PSXUIBoxEditor : Editor
|
||||||
|
{
|
||||||
|
public override void OnInspectorGUI()
|
||||||
|
{
|
||||||
|
serializedObject.Update();
|
||||||
|
|
||||||
|
EditorGUILayout.LabelField("PSX UI Box", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.Space(4);
|
||||||
|
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("elementName"));
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("boxColor"), new GUIContent("Box Color",
|
||||||
|
"Solid fill color rendered as a FastFill primitive."));
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("startVisible"));
|
||||||
|
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Custom inspector for PSXUIText component.
|
||||||
|
/// </summary>
|
||||||
|
[CustomEditor(typeof(PSXUIText))]
|
||||||
|
public class PSXUITextEditor : Editor
|
||||||
|
{
|
||||||
|
public override void OnInspectorGUI()
|
||||||
|
{
|
||||||
|
serializedObject.Update();
|
||||||
|
|
||||||
|
EditorGUILayout.LabelField("PSX UI Text", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.Space(4);
|
||||||
|
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("elementName"));
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("defaultText"), new GUIContent("Default Text",
|
||||||
|
"Initial text content. Max 63 chars. Change at runtime via UI.SetText()."));
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("textColor"), new GUIContent("Text Color",
|
||||||
|
"Text render color."));
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("fontOverride"), new GUIContent("Font Override",
|
||||||
|
"Custom font for this text element. If empty, uses the canvas default font or built-in system font (8x16)."));
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("startVisible"));
|
||||||
|
|
||||||
|
// Character count
|
||||||
|
PSXUIText txt = (PSXUIText)target;
|
||||||
|
if (!string.IsNullOrEmpty(txt.DefaultText) && txt.DefaultText.Length > 63)
|
||||||
|
EditorGUILayout.HelpBox("Text exceeds 63 characters and will be truncated.", MessageType.Warning);
|
||||||
|
|
||||||
|
// Font info
|
||||||
|
PSXFontAsset font = txt.GetEffectiveFont();
|
||||||
|
if (font != null)
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox(
|
||||||
|
$"Font: {font.name} ({font.GlyphWidth}x{font.GlyphHeight} glyphs)",
|
||||||
|
MessageType.Info);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox("Using built-in system font (8x16 glyphs).", MessageType.Info);
|
||||||
|
}
|
||||||
|
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Custom inspector for PSXUIProgressBar component.
|
||||||
|
/// </summary>
|
||||||
|
[CustomEditor(typeof(PSXUIProgressBar))]
|
||||||
|
public class PSXUIProgressBarEditor : Editor
|
||||||
|
{
|
||||||
|
public override void OnInspectorGUI()
|
||||||
|
{
|
||||||
|
serializedObject.Update();
|
||||||
|
|
||||||
|
EditorGUILayout.LabelField("PSX UI Progress Bar", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.Space(4);
|
||||||
|
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("elementName"));
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("backgroundColor"), new GUIContent("Background Color",
|
||||||
|
"Color shown behind the fill bar."));
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("fillColor"), new GUIContent("Fill Color",
|
||||||
|
"Color of the progress fill."));
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("initialValue"), new GUIContent("Initial Value",
|
||||||
|
"Starting progress (0-100). Change via UI.SetProgress()."));
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("startVisible"));
|
||||||
|
|
||||||
|
// Preview bar
|
||||||
|
Rect r = EditorGUILayout.GetControlRect(false, 16);
|
||||||
|
PSXUIProgressBar bar = (PSXUIProgressBar)target;
|
||||||
|
EditorGUI.DrawRect(r, bar.BackgroundColor);
|
||||||
|
Rect fill = r;
|
||||||
|
fill.width *= bar.InitialValue / 100f;
|
||||||
|
EditorGUI.DrawRect(fill, bar.FillColor);
|
||||||
|
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Custom inspector for PSXFontAsset ScriptableObject.
|
||||||
|
/// Shows font metrics, auto-conversion from TTF/OTF, and a preview of the glyph layout.
|
||||||
|
/// </summary>
|
||||||
|
[CustomEditor(typeof(PSXFontAsset))]
|
||||||
|
public class PSXFontAssetEditor : Editor
|
||||||
|
{
|
||||||
|
public override void OnInspectorGUI()
|
||||||
|
{
|
||||||
|
serializedObject.Update();
|
||||||
|
|
||||||
|
EditorGUILayout.LabelField("PSX Font Asset", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.Space(4);
|
||||||
|
|
||||||
|
// Source font (TTF/OTF)
|
||||||
|
EditorGUILayout.LabelField("Auto-Convert from Font", EditorStyles.miniBoldLabel);
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("sourceFont"), new GUIContent("Source Font (TTF/OTF)",
|
||||||
|
"Assign a Unity Font (TrueType/OpenType). Click 'Generate Bitmap' to rasterize it."));
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("fontSize"), new GUIContent("Font Size",
|
||||||
|
"Pixel height for rasterization. Determines glyph cell height."));
|
||||||
|
|
||||||
|
PSXFontAsset font = (PSXFontAsset)target;
|
||||||
|
if (font.SourceFont != null)
|
||||||
|
{
|
||||||
|
if (GUILayout.Button("Generate Bitmap from Font"))
|
||||||
|
{
|
||||||
|
font.GenerateBitmapFromFont();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (font.FontTexture == null)
|
||||||
|
EditorGUILayout.HelpBox("Click 'Generate Bitmap' to create the font texture.", MessageType.Info);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.Space(8);
|
||||||
|
|
||||||
|
// Manual bitmap
|
||||||
|
EditorGUILayout.LabelField("Manual Bitmap Source", EditorStyles.miniBoldLabel);
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("fontTexture"), new GUIContent("Font Texture",
|
||||||
|
"256px wide bitmap. Glyphs in ASCII order from 0x20 (space). " +
|
||||||
|
"Transparent = background, opaque = foreground."));
|
||||||
|
|
||||||
|
EditorGUILayout.Space(4);
|
||||||
|
|
||||||
|
// Glyph metrics
|
||||||
|
EditorGUILayout.LabelField("Glyph Metrics", EditorStyles.miniBoldLabel);
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("glyphWidth"), new GUIContent("Glyph Width",
|
||||||
|
"Width of each glyph cell in pixels."));
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("glyphHeight"), new GUIContent("Glyph Height",
|
||||||
|
"Height of each glyph cell in pixels."));
|
||||||
|
|
||||||
|
EditorGUILayout.Space(4);
|
||||||
|
|
||||||
|
int glyphsPerRow = font.GlyphsPerRow;
|
||||||
|
int rowCount = font.RowCount;
|
||||||
|
int totalH = font.TextureHeight;
|
||||||
|
int vramBytes = totalH * 128; // 128 bytes per row at 4bpp 256px
|
||||||
|
|
||||||
|
EditorGUILayout.HelpBox(
|
||||||
|
$"Layout: {glyphsPerRow} glyphs/row, {rowCount} rows\n" +
|
||||||
|
$"Texture: 256 x {totalH} pixels (4bpp)\n" +
|
||||||
|
$"VRAM: {vramBytes} bytes ({vramBytes / 1024f:F1} KB)\n" +
|
||||||
|
$"Glyph size: {font.GlyphWidth} x {font.GlyphHeight}",
|
||||||
|
MessageType.Info);
|
||||||
|
|
||||||
|
if (font.FontTexture != null)
|
||||||
|
{
|
||||||
|
if (font.FontTexture.width != 256)
|
||||||
|
EditorGUILayout.HelpBox($"Font texture must be 256 pixels wide (currently {font.FontTexture.width}).", MessageType.Error);
|
||||||
|
|
||||||
|
// Show preview
|
||||||
|
Rect previewRect = EditorGUILayout.GetControlRect(false, 64);
|
||||||
|
GUI.DrawTexture(previewRect, font.FontTexture, ScaleMode.ScaleToFit);
|
||||||
|
}
|
||||||
|
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Editor/Inspectors/PSXUIEditors.cs.meta
Normal file
2
Editor/Inspectors/PSXUIEditors.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 385b3916e29dc0e48b2866851d1fc1a9
|
||||||
@@ -87,6 +87,8 @@ namespace SplashEdit.RuntimeCode
|
|||||||
// Collect scene references for validation
|
// Collect scene references for validation
|
||||||
var exporterNames = new HashSet<string>();
|
var exporterNames = new HashSet<string>();
|
||||||
var audioNames = new HashSet<string>();
|
var audioNames = new HashSet<string>();
|
||||||
|
var canvasNames = new HashSet<string>();
|
||||||
|
var elementNames = new Dictionary<string, HashSet<string>>(); // canvas → element names
|
||||||
var exporters = Object.FindObjectsByType<PSXObjectExporter>(FindObjectsSortMode.None);
|
var exporters = Object.FindObjectsByType<PSXObjectExporter>(FindObjectsSortMode.None);
|
||||||
foreach (var e in exporters)
|
foreach (var e in exporters)
|
||||||
exporterNames.Add(e.gameObject.name);
|
exporterNames.Add(e.gameObject.name);
|
||||||
@@ -94,6 +96,26 @@ namespace SplashEdit.RuntimeCode
|
|||||||
foreach (var a in audioSources)
|
foreach (var a in audioSources)
|
||||||
if (!string.IsNullOrEmpty(a.ClipName))
|
if (!string.IsNullOrEmpty(a.ClipName))
|
||||||
audioNames.Add(a.ClipName);
|
audioNames.Add(a.ClipName);
|
||||||
|
var canvases = Object.FindObjectsByType<PSXCanvas>(FindObjectsSortMode.None);
|
||||||
|
foreach (var c in canvases)
|
||||||
|
{
|
||||||
|
string cName = c.CanvasName ?? "";
|
||||||
|
if (!string.IsNullOrEmpty(cName))
|
||||||
|
{
|
||||||
|
canvasNames.Add(cName);
|
||||||
|
if (!elementNames.ContainsKey(cName))
|
||||||
|
elementNames[cName] = new HashSet<string>();
|
||||||
|
// Gather all UI element names under this canvas
|
||||||
|
foreach (var box in c.GetComponentsInChildren<PSXUIBox>())
|
||||||
|
if (!string.IsNullOrEmpty(box.ElementName)) elementNames[cName].Add(box.ElementName);
|
||||||
|
foreach (var txt in c.GetComponentsInChildren<PSXUIText>())
|
||||||
|
if (!string.IsNullOrEmpty(txt.ElementName)) elementNames[cName].Add(txt.ElementName);
|
||||||
|
foreach (var bar in c.GetComponentsInChildren<PSXUIProgressBar>())
|
||||||
|
if (!string.IsNullOrEmpty(bar.ElementName)) elementNames[cName].Add(bar.ElementName);
|
||||||
|
foreach (var img in c.GetComponentsInChildren<PSXUIImage>())
|
||||||
|
if (!string.IsNullOrEmpty(img.ElementName)) elementNames[cName].Add(img.ElementName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Tracks ──
|
// ── Tracks ──
|
||||||
EditorGUILayout.Space(8);
|
EditorGUILayout.Space(8);
|
||||||
@@ -114,13 +136,39 @@ namespace SplashEdit.RuntimeCode
|
|||||||
EditorGUILayout.EndHorizontal();
|
EditorGUILayout.EndHorizontal();
|
||||||
|
|
||||||
bool isCameraTrack = track.TrackType == PSXTrackType.CameraPosition || track.TrackType == PSXTrackType.CameraRotation;
|
bool isCameraTrack = track.TrackType == PSXTrackType.CameraPosition || track.TrackType == PSXTrackType.CameraRotation;
|
||||||
EditorGUI.BeginDisabledGroup(isCameraTrack);
|
bool isUITrack = track.IsUITrack;
|
||||||
track.ObjectName = EditorGUILayout.TextField("Object Name", isCameraTrack ? "(camera)" : track.ObjectName);
|
bool isUIElementTrack = track.IsUIElementTrack;
|
||||||
EditorGUI.EndDisabledGroup();
|
|
||||||
|
|
||||||
// Validation
|
if (isCameraTrack)
|
||||||
if (!isCameraTrack && !string.IsNullOrEmpty(track.ObjectName) && !exporterNames.Contains(track.ObjectName))
|
{
|
||||||
EditorGUILayout.HelpBox($"No PSXObjectExporter found for '{track.ObjectName}' in scene.", MessageType.Error);
|
EditorGUI.BeginDisabledGroup(true);
|
||||||
|
EditorGUILayout.TextField("Target", "(camera)");
|
||||||
|
EditorGUI.EndDisabledGroup();
|
||||||
|
}
|
||||||
|
else if (isUITrack)
|
||||||
|
{
|
||||||
|
track.UICanvasName = EditorGUILayout.TextField("Canvas Name", track.UICanvasName);
|
||||||
|
if (!string.IsNullOrEmpty(track.UICanvasName) && !canvasNames.Contains(track.UICanvasName))
|
||||||
|
EditorGUILayout.HelpBox($"No PSXCanvas with name '{track.UICanvasName}' in scene.", MessageType.Error);
|
||||||
|
|
||||||
|
if (isUIElementTrack)
|
||||||
|
{
|
||||||
|
track.UIElementName = EditorGUILayout.TextField("Element Name", track.UIElementName);
|
||||||
|
if (!string.IsNullOrEmpty(track.UICanvasName) && !string.IsNullOrEmpty(track.UIElementName))
|
||||||
|
{
|
||||||
|
if (elementNames.TryGetValue(track.UICanvasName, out var elNames) && !elNames.Contains(track.UIElementName))
|
||||||
|
EditorGUILayout.HelpBox($"No UI element '{track.UIElementName}' found under canvas '{track.UICanvasName}'.", MessageType.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
track.ObjectName = EditorGUILayout.TextField("Object Name", track.ObjectName);
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (!string.IsNullOrEmpty(track.ObjectName) && !exporterNames.Contains(track.ObjectName))
|
||||||
|
EditorGUILayout.HelpBox($"No PSXObjectExporter found for '{track.ObjectName}' in scene.", MessageType.Error);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Keyframes ──
|
// ── Keyframes ──
|
||||||
if (track.Keyframes == null) track.Keyframes = new List<PSXKeyframe>();
|
if (track.Keyframes == null) track.Keyframes = new List<PSXKeyframe>();
|
||||||
@@ -152,7 +200,7 @@ namespace SplashEdit.RuntimeCode
|
|||||||
else Debug.LogWarning("No active Scene View.");
|
else Debug.LogWarning("No active Scene View.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (track.TrackType == PSXTrackType.ObjectPosition || track.TrackType == PSXTrackType.ObjectRotationY)
|
else if (!isUITrack && (track.TrackType == PSXTrackType.ObjectPosition || track.TrackType == PSXTrackType.ObjectRotationY))
|
||||||
{
|
{
|
||||||
if (GUILayout.Button("From Sel.", GUILayout.Width(70)))
|
if (GUILayout.Button("From Sel.", GUILayout.Width(70)))
|
||||||
{
|
{
|
||||||
@@ -173,13 +221,46 @@ namespace SplashEdit.RuntimeCode
|
|||||||
switch (track.TrackType)
|
switch (track.TrackType)
|
||||||
{
|
{
|
||||||
case PSXTrackType.ObjectActive:
|
case PSXTrackType.ObjectActive:
|
||||||
bool active = EditorGUILayout.Toggle("Active", kf.Value.x > 0.5f);
|
case PSXTrackType.UICanvasVisible:
|
||||||
|
case PSXTrackType.UIElementVisible:
|
||||||
|
{
|
||||||
|
string label = track.TrackType == PSXTrackType.ObjectActive ? "Active" : "Visible";
|
||||||
|
bool active = EditorGUILayout.Toggle(label, kf.Value.x > 0.5f);
|
||||||
kf.Value = new Vector3(active ? 1f : 0f, 0, 0);
|
kf.Value = new Vector3(active ? 1f : 0f, 0, 0);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case PSXTrackType.ObjectRotationY:
|
case PSXTrackType.ObjectRotationY:
|
||||||
|
{
|
||||||
float yRot = EditorGUILayout.FloatField("Y\u00b0", kf.Value.y);
|
float yRot = EditorGUILayout.FloatField("Y\u00b0", kf.Value.y);
|
||||||
kf.Value = new Vector3(0, yRot, 0);
|
kf.Value = new Vector3(0, yRot, 0);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
case PSXTrackType.UIProgress:
|
||||||
|
{
|
||||||
|
float progress = EditorGUILayout.Slider("Progress %", kf.Value.x, 0f, 100f);
|
||||||
|
kf.Value = new Vector3(progress, 0, 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PSXTrackType.UIPosition:
|
||||||
|
{
|
||||||
|
Vector2 pos = EditorGUILayout.Vector2Field("Position (px)", new Vector2(kf.Value.x, kf.Value.y));
|
||||||
|
kf.Value = new Vector3(pos.x, pos.y, 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PSXTrackType.UIColor:
|
||||||
|
{
|
||||||
|
// Show as RGB 0-255 integers
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
EditorGUILayout.LabelField("R", GUILayout.Width(14));
|
||||||
|
float r = EditorGUILayout.IntField(Mathf.Clamp(Mathf.RoundToInt(kf.Value.x), 0, 255), GUILayout.Width(40));
|
||||||
|
EditorGUILayout.LabelField("G", GUILayout.Width(14));
|
||||||
|
float g = EditorGUILayout.IntField(Mathf.Clamp(Mathf.RoundToInt(kf.Value.y), 0, 255), GUILayout.Width(40));
|
||||||
|
EditorGUILayout.LabelField("B", GUILayout.Width(14));
|
||||||
|
float b = EditorGUILayout.IntField(Mathf.Clamp(Mathf.RoundToInt(kf.Value.z), 0, 255), GUILayout.Width(40));
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
kf.Value = new Vector3(r, g, b);
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
kf.Value = EditorGUILayout.Vector3Field("Value", kf.Value);
|
kf.Value = EditorGUILayout.Vector3Field("Value", kf.Value);
|
||||||
break;
|
break;
|
||||||
@@ -216,7 +297,7 @@ namespace SplashEdit.RuntimeCode
|
|||||||
track.Keyframes.Add(new PSXKeyframe { Frame = frame, Value = val });
|
track.Keyframes.Add(new PSXKeyframe { Frame = frame, Value = val });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (track.TrackType == PSXTrackType.ObjectPosition || track.TrackType == PSXTrackType.ObjectRotationY)
|
else if (!isUITrack && (track.TrackType == PSXTrackType.ObjectPosition || track.TrackType == PSXTrackType.ObjectRotationY))
|
||||||
{
|
{
|
||||||
if (GUILayout.Button("+ from Selected", GUILayout.Width(120)))
|
if (GUILayout.Button("+ from Selected", GUILayout.Width(120)))
|
||||||
{
|
{
|
||||||
@@ -511,6 +592,15 @@ namespace SplashEdit.RuntimeCode
|
|||||||
if (_savedObjectActive.ContainsKey(track.ObjectName ?? ""))
|
if (_savedObjectActive.ContainsKey(track.ObjectName ?? ""))
|
||||||
initialVal = new Vector3(_savedObjectActive[track.ObjectName] ? 1f : 0f, 0, 0);
|
initialVal = new Vector3(_savedObjectActive[track.ObjectName] ? 1f : 0f, 0, 0);
|
||||||
break;
|
break;
|
||||||
|
// UI tracks: initial values stay zero (no scene preview state to capture)
|
||||||
|
case PSXTrackType.UICanvasVisible:
|
||||||
|
case PSXTrackType.UIElementVisible:
|
||||||
|
initialVal = new Vector3(1f, 0, 0); // assume visible by default
|
||||||
|
break;
|
||||||
|
case PSXTrackType.UIProgress:
|
||||||
|
case PSXTrackType.UIPosition:
|
||||||
|
case PSXTrackType.UIColor:
|
||||||
|
break; // zero is fine
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector3 val = EvaluateTrack(track, frame, initialVal);
|
Vector3 val = EvaluateTrack(track, frame, initialVal);
|
||||||
@@ -541,6 +631,13 @@ namespace SplashEdit.RuntimeCode
|
|||||||
if (go != null) go.SetActive(val.x > 0.5f);
|
if (go != null) go.SetActive(val.x > 0.5f);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// UI tracks: no scene preview, values are applied on PS1 only
|
||||||
|
case PSXTrackType.UICanvasVisible:
|
||||||
|
case PSXTrackType.UIElementVisible:
|
||||||
|
case PSXTrackType.UIProgress:
|
||||||
|
case PSXTrackType.UIPosition:
|
||||||
|
case PSXTrackType.UIColor:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -582,10 +679,11 @@ namespace SplashEdit.RuntimeCode
|
|||||||
if (track.Keyframes == null || track.Keyframes.Count == 0)
|
if (track.Keyframes == null || track.Keyframes.Count == 0)
|
||||||
return Vector3.zero;
|
return Vector3.zero;
|
||||||
|
|
||||||
// ObjectActive always uses step interpolation regardless of InterpMode
|
// Step interpolation tracks: ObjectActive, UICanvasVisible, UIElementVisible
|
||||||
if (track.TrackType == PSXTrackType.ObjectActive)
|
if (track.TrackType == PSXTrackType.ObjectActive ||
|
||||||
|
track.TrackType == PSXTrackType.UICanvasVisible ||
|
||||||
|
track.TrackType == PSXTrackType.UIElementVisible)
|
||||||
{
|
{
|
||||||
// Use initial state if before first keyframe
|
|
||||||
if (track.Keyframes.Count > 0 && track.Keyframes[0].Frame > 0 && frame < track.Keyframes[0].Frame)
|
if (track.Keyframes.Count > 0 && track.Keyframes[0].Frame > 0 && frame < track.Keyframes[0].Frame)
|
||||||
return initialValue;
|
return initialValue;
|
||||||
return EvaluateStep(track.Keyframes, frame);
|
return EvaluateStep(track.Keyframes, frame);
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ namespace SplashEdit.EditorCode
|
|||||||
private Color bufferColor2 = new Color(0, 1, 0, 0.5f);
|
private Color bufferColor2 = new Color(0, 1, 0, 0.5f);
|
||||||
private Color prohibitedColor = new Color(1, 0, 0, 0.3f);
|
private Color prohibitedColor = new Color(1, 0, 0, 0.3f);
|
||||||
private PSXData _psxData;
|
private PSXData _psxData;
|
||||||
|
private PSXFontData[] _cachedFonts;
|
||||||
|
|
||||||
private static readonly Vector2[] resolutions =
|
private static readonly Vector2[] resolutions =
|
||||||
{
|
{
|
||||||
@@ -144,6 +145,62 @@ namespace SplashEdit.EditorCode
|
|||||||
vramImage.SetPixel(x, VramHeight - y - 1, packed.vramPixels[x, y].GetUnityColor());
|
vramImage.SetPixel(x, VramHeight - y - 1, packed.vramPixels[x, y].GetUnityColor());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Overlay custom font textures into the VRAM preview.
|
||||||
|
// Fonts live at x=960 (4bpp = 64 VRAM hwords wide), stacking from y=0.
|
||||||
|
PSXFontData[] fonts;
|
||||||
|
PSXUIExporter.CollectCanvases(selectedResolution, out fonts);
|
||||||
|
_cachedFonts = fonts;
|
||||||
|
if (fonts != null && fonts.Length > 0)
|
||||||
|
{
|
||||||
|
foreach (var font in fonts)
|
||||||
|
{
|
||||||
|
if (font.PixelData == null || font.PixelData.Length == 0) continue;
|
||||||
|
|
||||||
|
int vramX = font.VramX;
|
||||||
|
int vramY = font.VramY;
|
||||||
|
int texH = font.TextureHeight;
|
||||||
|
int bytesPerRow = 256 / 2; // 4bpp: 2 pixels per byte, 256 pixels wide = 128 bytes/row
|
||||||
|
|
||||||
|
// Each byte holds two 4bpp pixels. In VRAM, 4 4bpp pixels = 1 16-bit hword.
|
||||||
|
// So 256 4bpp pixels = 64 VRAM hwords.
|
||||||
|
for (int y = 0; y < texH && (vramY + y) < VramHeight; y++)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < 64 && (vramX + x) < VramWidth; x++)
|
||||||
|
{
|
||||||
|
// Read 4 4bpp pixels from this VRAM hword position
|
||||||
|
int byteIdx = y * bytesPerRow + x * 2;
|
||||||
|
if (byteIdx + 1 >= font.PixelData.Length) continue;
|
||||||
|
byte b0 = font.PixelData[byteIdx];
|
||||||
|
byte b1 = font.PixelData[byteIdx + 1];
|
||||||
|
// Each byte: low nibble = first pixel, high nibble = second
|
||||||
|
// 4 pixels per hword: b0 low, b0 high, b1 low, b1 high
|
||||||
|
bool anyOpaque = ((b0 & 0x0F) | (b0 >> 4) | (b1 & 0x0F) | (b1 >> 4)) != 0;
|
||||||
|
|
||||||
|
if (anyOpaque)
|
||||||
|
{
|
||||||
|
int px = vramX + x;
|
||||||
|
int py = VramHeight - 1 - (vramY + y);
|
||||||
|
if (px < VramWidth && py >= 0)
|
||||||
|
vramImage.SetPixel(px, py, new Color(0.8f, 0.8f, 1f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also show system font area (960, 464)-(1023, 511) = 64x48
|
||||||
|
for (int y = 464; y < 512 && y < VramHeight; y++)
|
||||||
|
{
|
||||||
|
for (int x = 960; x < 1024 && x < VramWidth; x++)
|
||||||
|
{
|
||||||
|
int py = VramHeight - 1 - y;
|
||||||
|
Color existing = vramImage.GetPixel(x, py);
|
||||||
|
if (existing.r < 0.01f && existing.g < 0.01f && existing.b < 0.01f)
|
||||||
|
vramImage.SetPixel(x, py, new Color(0.3f, 0.3f, 0.5f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
vramImage.Apply();
|
vramImage.Apply();
|
||||||
|
|
||||||
// Prompt the user to select a file location and save the VRAM data.
|
// Prompt the user to select a file location and save the VRAM data.
|
||||||
@@ -297,6 +354,24 @@ namespace SplashEdit.EditorCode
|
|||||||
EditorGUI.DrawRect(areaRect, prohibitedColor);
|
EditorGUI.DrawRect(areaRect, prohibitedColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw font region overlays.
|
||||||
|
if (_cachedFonts != null)
|
||||||
|
{
|
||||||
|
Color fontColor = new Color(0.2f, 0.4f, 0.9f, 0.25f);
|
||||||
|
foreach (var font in _cachedFonts)
|
||||||
|
{
|
||||||
|
if (font.PixelData == null || font.PixelData.Length == 0) continue;
|
||||||
|
Rect fontRect = new Rect(vramRect.x + font.VramX, vramRect.y + font.VramY, 64, font.TextureHeight);
|
||||||
|
EditorGUI.DrawRect(fontRect, fontColor);
|
||||||
|
GUI.Label(new Rect(fontRect.x + 2, fontRect.y + 2, 60, 16), "Font", EditorStyles.miniLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// System font overlay
|
||||||
|
Rect sysFontRect = new Rect(vramRect.x + 960, vramRect.y + 464, 64, 48);
|
||||||
|
EditorGUI.DrawRect(sysFontRect, new Color(0.4f, 0.2f, 0.9f, 0.25f));
|
||||||
|
GUI.Label(new Rect(sysFontRect.x + 2, sysFontRect.y + 2, 60, 16), "SysFont", EditorStyles.miniLabel);
|
||||||
|
}
|
||||||
|
|
||||||
GUILayout.EndHorizontal();
|
GUILayout.EndHorizontal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
121
Runtime/PSXCanvas.cs
Normal file
121
Runtime/PSXCanvas.cs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
using UnityEditor;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace SplashEdit.RuntimeCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Marks a Unity Canvas as a PSX UI canvas for splashpack export.
|
||||||
|
/// Attach to a GameObject that also has a Unity Canvas component.
|
||||||
|
/// Children with PSXUIImage / PSXUIBox / PSXUIText / PSXUIProgressBar
|
||||||
|
/// components will be exported as UI elements in this canvas.
|
||||||
|
/// Auto-configures the Canvas to the PSX resolution from PSXData settings.
|
||||||
|
/// </summary>
|
||||||
|
[RequireComponent(typeof(Canvas))]
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
[ExecuteAlways]
|
||||||
|
[AddComponentMenu("PSX/UI/PSX Canvas")]
|
||||||
|
public class PSXCanvas : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Tooltip("Name used to reference this canvas from Lua (max 24 chars). Must be unique per scene.")]
|
||||||
|
[SerializeField] private string canvasName = "canvas";
|
||||||
|
|
||||||
|
[Tooltip("Whether this canvas is visible when the scene first loads.")]
|
||||||
|
[SerializeField] private bool startVisible = true;
|
||||||
|
|
||||||
|
[Tooltip("Render order (0 = back, higher = front). Canvases render back-to-front.")]
|
||||||
|
[Range(0, 255)]
|
||||||
|
[SerializeField] private int sortOrder = 0;
|
||||||
|
|
||||||
|
[Tooltip("Optional custom font for text elements in this canvas. If null, uses the built-in system font (8x16).")]
|
||||||
|
[SerializeField] private PSXFontAsset defaultFont;
|
||||||
|
|
||||||
|
/// <summary>Canvas name for Lua access. Truncated to 24 chars on export.</summary>
|
||||||
|
public string CanvasName => canvasName;
|
||||||
|
|
||||||
|
/// <summary>Initial visibility flag written into the splashpack.</summary>
|
||||||
|
public bool StartVisible => startVisible;
|
||||||
|
|
||||||
|
/// <summary>Sort order in 0-255 range.</summary>
|
||||||
|
public byte SortOrder => (byte)Mathf.Clamp(sortOrder, 0, 255);
|
||||||
|
|
||||||
|
/// <summary>Default font for text elements. Null = system font.</summary>
|
||||||
|
public PSXFontAsset DefaultFont => defaultFont;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PSX target resolution read from the PSXData asset. Falls back to 320x240.
|
||||||
|
/// Cached per domain reload for efficiency.
|
||||||
|
/// </summary>
|
||||||
|
public static Vector2 PSXResolution
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!s_resolutionCached)
|
||||||
|
{
|
||||||
|
s_cachedResolution = LoadResolutionFromProject();
|
||||||
|
s_resolutionCached = true;
|
||||||
|
}
|
||||||
|
return s_cachedResolution;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector2 s_cachedResolution = new Vector2(320, 240);
|
||||||
|
private static bool s_resolutionCached = false;
|
||||||
|
|
||||||
|
/// <summary>Invalidate the cached resolution (call when PSXData changes).</summary>
|
||||||
|
public static void InvalidateResolutionCache()
|
||||||
|
{
|
||||||
|
s_resolutionCached = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector2 LoadResolutionFromProject()
|
||||||
|
{
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
var data = AssetDatabase.LoadAssetAtPath<PSXData>("Assets/PSXData.asset");
|
||||||
|
if (data != null)
|
||||||
|
return data.OutputResolution;
|
||||||
|
#endif
|
||||||
|
return new Vector2(320, 240);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Reset()
|
||||||
|
{
|
||||||
|
InvalidateResolutionCache();
|
||||||
|
ConfigureCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
ConfigureCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
private void OnValidate()
|
||||||
|
{
|
||||||
|
// Delay to avoid modifying in OnValidate directly
|
||||||
|
UnityEditor.EditorApplication.delayCall += ConfigureCanvas;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Force the Canvas + CanvasScaler to match the PSX resolution from project settings.
|
||||||
|
/// </summary>
|
||||||
|
public void ConfigureCanvas()
|
||||||
|
{
|
||||||
|
if (this == null) return;
|
||||||
|
|
||||||
|
Vector2 res = PSXResolution;
|
||||||
|
|
||||||
|
Canvas canvas = GetComponent<Canvas>();
|
||||||
|
if (canvas != null)
|
||||||
|
{
|
||||||
|
canvas.renderMode = RenderMode.WorldSpace;
|
||||||
|
}
|
||||||
|
|
||||||
|
RectTransform rt = GetComponent<RectTransform>();
|
||||||
|
rt.sizeDelta = new Vector2(res.x, res.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Runtime/PSXCanvas.cs.meta
Normal file
2
Runtime/PSXCanvas.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: dc481397cd94e03409e462478df09d58
|
||||||
85
Runtime/PSXCanvasData.cs
Normal file
85
Runtime/PSXCanvasData.cs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
namespace SplashEdit.RuntimeCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Pre-computed data for one UI canvas and its elements,
|
||||||
|
/// ready for binary serialization by <see cref="PSXSceneWriter"/>.
|
||||||
|
/// Populated by <see cref="PSXUIExporter"/> during the export pipeline.
|
||||||
|
/// </summary>
|
||||||
|
public struct PSXCanvasData
|
||||||
|
{
|
||||||
|
/// <summary>Canvas name (max 24 chars, truncated on export).</summary>
|
||||||
|
public string Name;
|
||||||
|
|
||||||
|
/// <summary>Initial visibility flag.</summary>
|
||||||
|
public bool StartVisible;
|
||||||
|
|
||||||
|
/// <summary>Sort order (0 = back, 255 = front).</summary>
|
||||||
|
public byte SortOrder;
|
||||||
|
|
||||||
|
/// <summary>Exported elements belonging to this canvas.</summary>
|
||||||
|
public PSXUIElementData[] Elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pre-computed data for one UI element, ready for binary serialization.
|
||||||
|
/// Matches the 48-byte on-disk element record parsed by uisystem.cpp.
|
||||||
|
/// </summary>
|
||||||
|
public struct PSXUIElementData
|
||||||
|
{
|
||||||
|
// Identity
|
||||||
|
public PSXUIElementType Type;
|
||||||
|
public bool StartVisible;
|
||||||
|
public string Name; // max 24 chars
|
||||||
|
|
||||||
|
// Layout (PS1 pixel coords, already Y-inverted)
|
||||||
|
public short X, Y, W, H;
|
||||||
|
|
||||||
|
// Anchors (8.8 fixed-point: 0=0.0, 128=0.5, 255≈1.0)
|
||||||
|
public byte AnchorMinX, AnchorMinY;
|
||||||
|
public byte AnchorMaxX, AnchorMaxY;
|
||||||
|
|
||||||
|
// Primary color (RGB)
|
||||||
|
public byte ColorR, ColorG, ColorB;
|
||||||
|
|
||||||
|
// Type-specific: Image
|
||||||
|
public byte TexpageX, TexpageY;
|
||||||
|
public ushort ClutX, ClutY;
|
||||||
|
public byte U0, V0, U1, V1;
|
||||||
|
public byte BitDepthIndex; // 0=4bit, 1=8bit, 2=16bit
|
||||||
|
|
||||||
|
// Type-specific: Progress
|
||||||
|
public byte BgR, BgG, BgB;
|
||||||
|
public byte ProgressValue;
|
||||||
|
|
||||||
|
// Type-specific: Text
|
||||||
|
public string DefaultText; // max 63 chars
|
||||||
|
public byte FontIndex; // 0 = system font, 1+ = custom font
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Export data for a custom font to be embedded in the splashpack.
|
||||||
|
/// </summary>
|
||||||
|
public struct PSXFontData
|
||||||
|
{
|
||||||
|
/// <summary>Source font asset (for identification/dedup).</summary>
|
||||||
|
public PSXFontAsset Source;
|
||||||
|
|
||||||
|
/// <summary>Glyph cell width in pixels.</summary>
|
||||||
|
public byte GlyphWidth;
|
||||||
|
|
||||||
|
/// <summary>Glyph cell height in pixels.</summary>
|
||||||
|
public byte GlyphHeight;
|
||||||
|
|
||||||
|
/// <summary>VRAM X position for upload (16-bit pixel units).</summary>
|
||||||
|
public ushort VramX;
|
||||||
|
|
||||||
|
/// <summary>VRAM Y position for upload (16-bit pixel units).</summary>
|
||||||
|
public ushort VramY;
|
||||||
|
|
||||||
|
/// <summary>Texture height in pixels (width is always 256 in 4bpp = 64 VRAM hwords).</summary>
|
||||||
|
public ushort TextureHeight;
|
||||||
|
|
||||||
|
/// <summary>Packed 4bpp pixel data ready for VRAM upload.</summary>
|
||||||
|
public byte[] PixelData;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Runtime/PSXCanvasData.cs.meta
Normal file
2
Runtime/PSXCanvasData.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f4142a9858eccd04eac0b826af8f3621
|
||||||
@@ -151,9 +151,7 @@ namespace SplashEdit.RuntimeCode
|
|||||||
for (int ti = 0; ti < trackCount; ti++)
|
for (int ti = 0; ti < trackCount; ti++)
|
||||||
{
|
{
|
||||||
PSXCutsceneTrack track = clip.Tracks[ti];
|
PSXCutsceneTrack track = clip.Tracks[ti];
|
||||||
bool isCameraTrack = track.TrackType == PSXTrackType.CameraPosition || track.TrackType == PSXTrackType.CameraRotation;
|
string objName = GetTrackTargetName(track);
|
||||||
string objName = isCameraTrack ? "" : (track.ObjectName ?? "");
|
|
||||||
if (objName.Length > MAX_NAME_LEN) objName = objName.Substring(0, MAX_NAME_LEN);
|
|
||||||
|
|
||||||
int kfCount = Mathf.Min(track.Keyframes?.Count ?? 0, MAX_KEYFRAMES);
|
int kfCount = Mathf.Min(track.Keyframes?.Count ?? 0, MAX_KEYFRAMES);
|
||||||
|
|
||||||
@@ -228,6 +226,39 @@ namespace SplashEdit.RuntimeCode
|
|||||||
writer.Write((short)0);
|
writer.Write((short)0);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case PSXTrackType.UICanvasVisible:
|
||||||
|
case PSXTrackType.UIElementVisible:
|
||||||
|
{
|
||||||
|
// Step: values[0] = 0 or 1
|
||||||
|
writer.Write((short)(kf.Value.x > 0.5f ? 1 : 0));
|
||||||
|
writer.Write((short)0);
|
||||||
|
writer.Write((short)0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PSXTrackType.UIProgress:
|
||||||
|
{
|
||||||
|
// values[0] = progress 0-100 as int16
|
||||||
|
writer.Write((short)Mathf.Clamp(Mathf.RoundToInt(kf.Value.x), 0, 100));
|
||||||
|
writer.Write((short)0);
|
||||||
|
writer.Write((short)0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PSXTrackType.UIPosition:
|
||||||
|
{
|
||||||
|
// values[0] = x, values[1] = y (PSX screen coordinates, raw int16)
|
||||||
|
writer.Write((short)Mathf.RoundToInt(kf.Value.x));
|
||||||
|
writer.Write((short)Mathf.RoundToInt(kf.Value.y));
|
||||||
|
writer.Write((short)0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PSXTrackType.UIColor:
|
||||||
|
{
|
||||||
|
// values[0] = r, values[1] = g, values[2] = b (0-255)
|
||||||
|
writer.Write((short)Mathf.Clamp(Mathf.RoundToInt(kf.Value.x), 0, 255));
|
||||||
|
writer.Write((short)Mathf.Clamp(Mathf.RoundToInt(kf.Value.y), 0, 255));
|
||||||
|
writer.Write((short)Mathf.Clamp(Mathf.RoundToInt(kf.Value.z), 0, 255));
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,13 +271,11 @@ namespace SplashEdit.RuntimeCode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Object name strings (per track) ──
|
// ── Object / UI target name strings (per track) ──
|
||||||
for (int ti = 0; ti < trackCount; ti++)
|
for (int ti = 0; ti < trackCount; ti++)
|
||||||
{
|
{
|
||||||
PSXCutsceneTrack track = clip.Tracks[ti];
|
PSXCutsceneTrack track = clip.Tracks[ti];
|
||||||
bool isCameraTrack = track.TrackType == PSXTrackType.CameraPosition || track.TrackType == PSXTrackType.CameraRotation;
|
string objName = GetTrackTargetName(track);
|
||||||
string objName = isCameraTrack ? "" : (track.ObjectName ?? "");
|
|
||||||
if (objName.Length > MAX_NAME_LEN) objName = objName.Substring(0, MAX_NAME_LEN);
|
|
||||||
|
|
||||||
if (objName.Length > 0)
|
if (objName.Length > 0)
|
||||||
{
|
{
|
||||||
@@ -329,5 +358,29 @@ namespace SplashEdit.RuntimeCode
|
|||||||
if (padding > 0)
|
if (padding > 0)
|
||||||
writer.Write(new byte[padding]);
|
writer.Write(new byte[padding]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the target name string for a track.
|
||||||
|
/// Camera tracks: empty. Object tracks: ObjectName.
|
||||||
|
/// UICanvasVisible: UICanvasName.
|
||||||
|
/// UI element tracks: "UICanvasName/UIElementName".
|
||||||
|
/// </summary>
|
||||||
|
private static string GetTrackTargetName(PSXCutsceneTrack track)
|
||||||
|
{
|
||||||
|
bool isCameraTrack = track.TrackType == PSXTrackType.CameraPosition || track.TrackType == PSXTrackType.CameraRotation;
|
||||||
|
if (isCameraTrack) return "";
|
||||||
|
|
||||||
|
string name;
|
||||||
|
if (track.IsUIElementTrack)
|
||||||
|
name = (track.UICanvasName ?? "") + "/" + (track.UIElementName ?? "");
|
||||||
|
else if (track.IsUITrack)
|
||||||
|
name = track.UICanvasName ?? "";
|
||||||
|
else
|
||||||
|
name = track.ObjectName ?? "";
|
||||||
|
|
||||||
|
if (name.Length > MAX_NAME_LEN)
|
||||||
|
name = name.Substring(0, MAX_NAME_LEN);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,22 @@ namespace SplashEdit.RuntimeCode
|
|||||||
[Tooltip("What property this track drives.")]
|
[Tooltip("What property this track drives.")]
|
||||||
public PSXTrackType TrackType;
|
public PSXTrackType TrackType;
|
||||||
|
|
||||||
[Tooltip("Target GameObject name (must match a PSXObjectExporter). Leave empty for camera tracks.")]
|
[Tooltip("Target GameObject name (must match a PSXObjectExporter). Leave empty for camera/UI tracks.")]
|
||||||
public string ObjectName = "";
|
public string ObjectName = "";
|
||||||
|
|
||||||
|
[Tooltip("For UI tracks: canvas name (e.g. 'hud'). Used by UICanvasVisible and to resolve elements.")]
|
||||||
|
public string UICanvasName = "";
|
||||||
|
|
||||||
|
[Tooltip("For UI element tracks: element name within the canvas. Used by UIElementVisible, UIProgress, UIPosition, UIColor.")]
|
||||||
|
public string UIElementName = "";
|
||||||
|
|
||||||
[Tooltip("Keyframes for this track. Sort by frame number.")]
|
[Tooltip("Keyframes for this track. Sort by frame number.")]
|
||||||
public List<PSXKeyframe> Keyframes = new List<PSXKeyframe>();
|
public List<PSXKeyframe> Keyframes = new List<PSXKeyframe>();
|
||||||
|
|
||||||
|
/// <summary>Returns true if this track type targets a UI canvas or element.</summary>
|
||||||
|
public bool IsUITrack => TrackType >= PSXTrackType.UICanvasVisible && TrackType <= PSXTrackType.UIColor;
|
||||||
|
|
||||||
|
/// <summary>Returns true if this track type targets a UI element (not just a canvas).</summary>
|
||||||
|
public bool IsUIElementTrack => TrackType >= PSXTrackType.UIElementVisible && TrackType <= PSXTrackType.UIColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
323
Runtime/PSXFontAsset.cs
Normal file
323
Runtime/PSXFontAsset.cs
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
using UnityEditor;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace SplashEdit.RuntimeCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defines a custom bitmap font for PSX UI text rendering.
|
||||||
|
///
|
||||||
|
/// Two workflows are supported:
|
||||||
|
/// 1. Manual bitmap: assign a 256px-wide texture with glyphs in ASCII order.
|
||||||
|
/// 2. Auto-convert: assign a Unity Font (TTF/OTF) and set glyph dimensions.
|
||||||
|
/// The bitmap is generated automatically from the font at the chosen size.
|
||||||
|
///
|
||||||
|
/// On export the texture is converted to 4bpp indexed format:
|
||||||
|
/// pixel value 0 = transparent, value 1 = opaque.
|
||||||
|
/// The color is applied at runtime through the CLUT.
|
||||||
|
/// </summary>
|
||||||
|
[CreateAssetMenu(fileName = "New PSXFont", menuName = "PSX/Font Asset")]
|
||||||
|
public class PSXFontAsset : ScriptableObject
|
||||||
|
{
|
||||||
|
[Header("Source — Option A: TrueType/OTF Font")]
|
||||||
|
[Tooltip("Assign a Unity Font asset (TTF/OTF). A bitmap will be auto-generated.\n" +
|
||||||
|
"Leave this empty to use a manual bitmap texture instead.")]
|
||||||
|
[SerializeField] private Font sourceFont;
|
||||||
|
|
||||||
|
[Tooltip("Font size in pixels used to rasterize the TrueType font. " +
|
||||||
|
"This determines glyph height; width is derived auto-fit per glyph.")]
|
||||||
|
[Range(6, 32)]
|
||||||
|
[SerializeField] private int fontSize = 16;
|
||||||
|
|
||||||
|
[Header("Source — Option B: Manual Bitmap")]
|
||||||
|
[Tooltip("Font bitmap texture. Must be 256 pixels wide. " +
|
||||||
|
"Glyphs arranged left-to-right, top-to-bottom in ASCII order starting from space (0x20). " +
|
||||||
|
"Transparent pixels = background, any opaque pixel = foreground.")]
|
||||||
|
[SerializeField] private Texture2D fontTexture;
|
||||||
|
|
||||||
|
[Header("Glyph Metrics")]
|
||||||
|
[Tooltip("Width of each glyph cell in pixels (e.g. 8 for an 8x16 font).")]
|
||||||
|
[Range(4, 32)]
|
||||||
|
[SerializeField] private int glyphWidth = 8;
|
||||||
|
|
||||||
|
[Tooltip("Height of each glyph cell in pixels (e.g. 16 for an 8x16 font).")]
|
||||||
|
[Range(4, 32)]
|
||||||
|
[SerializeField] private int glyphHeight = 16;
|
||||||
|
|
||||||
|
/// <summary>The Unity Font source (TTF/OTF). Null if using manual bitmap.</summary>
|
||||||
|
public Font SourceFont => sourceFont;
|
||||||
|
|
||||||
|
/// <summary>Font size for TrueType rasterization.</summary>
|
||||||
|
public int FontSize => fontSize;
|
||||||
|
|
||||||
|
/// <summary>Source font texture (256px wide, monochrome). May be auto-generated.</summary>
|
||||||
|
public Texture2D FontTexture => fontTexture;
|
||||||
|
|
||||||
|
/// <summary>Width of each glyph cell in pixels.</summary>
|
||||||
|
public int GlyphWidth => glyphWidth;
|
||||||
|
|
||||||
|
/// <summary>Height of each glyph cell in pixels.</summary>
|
||||||
|
public int GlyphHeight => glyphHeight;
|
||||||
|
|
||||||
|
/// <summary>Number of glyphs per row (always 256 / glyphWidth).</summary>
|
||||||
|
public int GlyphsPerRow => 256 / glyphWidth;
|
||||||
|
|
||||||
|
/// <summary>Number of rows needed for all 95 printable ASCII characters.</summary>
|
||||||
|
public int RowCount => Mathf.CeilToInt(95f / GlyphsPerRow);
|
||||||
|
|
||||||
|
/// <summary>Total height of the font texture in pixels.</summary>
|
||||||
|
public int TextureHeight => RowCount * glyphHeight;
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a 256px-wide monochrome bitmap from the assigned TrueType font.
|
||||||
|
/// Each printable ASCII character (0x20–0x7E) is rasterized into
|
||||||
|
/// a fixed-size glyph cell. The result is stored in fontTexture.
|
||||||
|
/// </summary>
|
||||||
|
public void GenerateBitmapFromFont()
|
||||||
|
{
|
||||||
|
if (sourceFont == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("PSXFontAsset: No source font assigned.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request all printable ASCII characters from the font at the target size
|
||||||
|
CharacterInfo ci;
|
||||||
|
string ascii = "";
|
||||||
|
for (int c = 0x20; c <= 0x7E; c++) ascii += (char)c;
|
||||||
|
sourceFont.RequestCharactersInTexture(ascii, fontSize, FontStyle.Normal);
|
||||||
|
|
||||||
|
int texW = 256;
|
||||||
|
int glyphsPerRow = texW / glyphWidth;
|
||||||
|
int rowCount = Mathf.CeilToInt(95f / glyphsPerRow);
|
||||||
|
int texH = rowCount * glyphHeight;
|
||||||
|
|
||||||
|
// Create output texture
|
||||||
|
Texture2D bmp = new Texture2D(texW, texH, TextureFormat.RGBA32, false);
|
||||||
|
bmp.filterMode = FilterMode.Point;
|
||||||
|
bmp.wrapMode = TextureWrapMode.Clamp;
|
||||||
|
|
||||||
|
// Fill transparent
|
||||||
|
Color[] clearPixels = new Color[texW * texH];
|
||||||
|
for (int i = 0; i < clearPixels.Length; i++)
|
||||||
|
clearPixels[i] = new Color(0, 0, 0, 0);
|
||||||
|
bmp.SetPixels(clearPixels);
|
||||||
|
|
||||||
|
// Get the font's internal texture for reading glyph pixels
|
||||||
|
Texture2D fontTex = (Texture2D)sourceFont.material.mainTexture;
|
||||||
|
Color[] fontPixels = null;
|
||||||
|
int fontTexW = 0, fontTexH = 0;
|
||||||
|
|
||||||
|
if (fontTex != null)
|
||||||
|
{
|
||||||
|
// Make sure it's readable
|
||||||
|
fontTexW = fontTex.width;
|
||||||
|
fontTexH = fontTex.height;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (fontTex != null)
|
||||||
|
{
|
||||||
|
fontTexW = fontTex.width;
|
||||||
|
fontTexH = fontTex.height;
|
||||||
|
|
||||||
|
// Make a readable copy
|
||||||
|
RenderTexture rt = RenderTexture.GetTemporary(fontTexW, fontTexH, 0);
|
||||||
|
Graphics.Blit(fontTex, rt);
|
||||||
|
RenderTexture.active = rt;
|
||||||
|
|
||||||
|
Texture2D readableCopy = new Texture2D(fontTexW, fontTexH);
|
||||||
|
readableCopy.ReadPixels(new Rect(0, 0, fontTexW, fontTexH), 0, 0);
|
||||||
|
readableCopy.Apply();
|
||||||
|
|
||||||
|
RenderTexture.active = null;
|
||||||
|
RenderTexture.ReleaseTemporary(rt);
|
||||||
|
|
||||||
|
// Get pixels from the copy
|
||||||
|
fontPixels = readableCopy.GetPixels();
|
||||||
|
|
||||||
|
// Done with it — dispose immediately
|
||||||
|
DestroyImmediate(readableCopy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
Debug.LogException(e);
|
||||||
|
// Font texture not readable — fall back to RenderTexture copy
|
||||||
|
RenderTexture rt = RenderTexture.GetTemporary(fontTexW, fontTexH, 0, RenderTextureFormat.ARGB32);
|
||||||
|
Graphics.Blit(fontTex, rt);
|
||||||
|
RenderTexture prev = RenderTexture.active;
|
||||||
|
RenderTexture.active = rt;
|
||||||
|
Texture2D readable = new Texture2D(fontTexW, fontTexH, TextureFormat.RGBA32, false);
|
||||||
|
readable.ReadPixels(new Rect(0, 0, fontTexW, fontTexH), 0, 0);
|
||||||
|
readable.Apply();
|
||||||
|
RenderTexture.active = prev;
|
||||||
|
RenderTexture.ReleaseTemporary(rt);
|
||||||
|
fontPixels = readable.GetPixels();
|
||||||
|
DestroyImmediate(readable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rasterize each glyph into fixed-size cells.
|
||||||
|
// Glyphs are scaled to fill their bounding box using bilinear sampling.
|
||||||
|
// A 1-pixel margin is kept on each side to prevent bleeding between glyphs.
|
||||||
|
int margin = 1;
|
||||||
|
int drawW = glyphWidth - margin * 2;
|
||||||
|
int drawH = glyphHeight - margin * 2;
|
||||||
|
if (drawW < 2) drawW = glyphWidth; // fallback if cell too tiny
|
||||||
|
if (drawH < 2) drawH = glyphHeight;
|
||||||
|
|
||||||
|
for (int idx = 0; idx < 95; idx++)
|
||||||
|
{
|
||||||
|
char ch = (char)(0x20 + idx);
|
||||||
|
int col = idx % glyphsPerRow;
|
||||||
|
int row = idx / glyphsPerRow;
|
||||||
|
int cellX = col * glyphWidth;
|
||||||
|
int cellY = row * glyphHeight; // top-down in output
|
||||||
|
|
||||||
|
if (!sourceFont.GetCharacterInfo(ch, out ci, fontSize, FontStyle.Normal))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (fontPixels == null) continue;
|
||||||
|
|
||||||
|
// ci gives UV coords in font texture
|
||||||
|
int gw = Mathf.Abs(ci.maxX - ci.minX);
|
||||||
|
int gh = Mathf.Abs(ci.maxY - ci.minY);
|
||||||
|
if (gw <= 0 || gh <= 0) continue;
|
||||||
|
|
||||||
|
// UV rect of the source glyph in the font's internal texture
|
||||||
|
float uvLeft = Mathf.Min(ci.uvBottomLeft.x, ci.uvTopRight.x);
|
||||||
|
float uvRight = Mathf.Max(ci.uvBottomLeft.x, ci.uvTopRight.x);
|
||||||
|
float uvBottom = Mathf.Min(ci.uvBottomLeft.y, ci.uvTopRight.y);
|
||||||
|
float uvTop = Mathf.Max(ci.uvBottomLeft.y, ci.uvTopRight.y);
|
||||||
|
bool flipped = ci.uvBottomLeft.y > ci.uvTopRight.y;
|
||||||
|
|
||||||
|
// Scale the glyph to fit the cell exactly (minus margin).
|
||||||
|
// Each output pixel samples the source glyph proportionally.
|
||||||
|
for (int py = 0; py < drawH; py++)
|
||||||
|
{
|
||||||
|
for (int px = 0; px < drawW; px++)
|
||||||
|
{
|
||||||
|
// Map output pixel to source glyph UV
|
||||||
|
float srcU = (px + 0.5f) / drawW;
|
||||||
|
float srcV = (py + 0.5f) / drawH;
|
||||||
|
|
||||||
|
float u = Mathf.Lerp(uvLeft, uvRight, srcU);
|
||||||
|
float v;
|
||||||
|
if (flipped)
|
||||||
|
v = Mathf.Lerp(uvTop, uvBottom, srcV);
|
||||||
|
else
|
||||||
|
v = Mathf.Lerp(uvBottom, uvTop, 1f - srcV);
|
||||||
|
|
||||||
|
int sx = Mathf.Clamp(Mathf.FloorToInt(u * fontTexW), 0, fontTexW - 1);
|
||||||
|
int sy = Mathf.Clamp(Mathf.FloorToInt(v * fontTexH), 0, fontTexH - 1);
|
||||||
|
Color sc = fontPixels[sy * fontTexW + sx];
|
||||||
|
|
||||||
|
// Threshold: if alpha or luminance > 0.3, mark as opaque
|
||||||
|
bool opaque = sc.a > 0.3f || (sc.r + sc.g + sc.b) / 3f > 0.3f;
|
||||||
|
if (!opaque) continue;
|
||||||
|
|
||||||
|
// Write to output: output Y is top-down, Unity texture Y is bottom-up
|
||||||
|
int outX = cellX + margin + px;
|
||||||
|
int outY = texH - 1 - (cellY + margin + py);
|
||||||
|
if (outX >= 0 && outX < texW && outY >= 0 && outY < texH)
|
||||||
|
bmp.SetPixel(outX, outY, Color.white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bmp.Apply();
|
||||||
|
|
||||||
|
// Save as asset
|
||||||
|
string path = AssetDatabase.GetAssetPath(this);
|
||||||
|
if (string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
|
fontTexture = bmp;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string dir = System.IO.Path.GetDirectoryName(path);
|
||||||
|
string texPath = dir + "/" + name + "_bitmap.png";
|
||||||
|
byte[] pngData = bmp.EncodeToPNG();
|
||||||
|
System.IO.File.WriteAllBytes(texPath, pngData);
|
||||||
|
DestroyImmediate(bmp);
|
||||||
|
|
||||||
|
AssetDatabase.ImportAsset(texPath, ImportAssetOptions.ForceUpdate);
|
||||||
|
// Set import settings for point filter, no compression
|
||||||
|
TextureImporter importer = AssetImporter.GetAtPath(texPath) as TextureImporter;
|
||||||
|
if (importer != null)
|
||||||
|
{
|
||||||
|
importer.textureType = TextureImporterType.Default;
|
||||||
|
importer.filterMode = FilterMode.Point;
|
||||||
|
importer.textureCompression = TextureImporterCompression.Uncompressed;
|
||||||
|
importer.isReadable = true;
|
||||||
|
importer.npotScale = TextureImporterNPOTScale.None;
|
||||||
|
importer.mipmapEnabled = false;
|
||||||
|
importer.alphaIsTransparency = true;
|
||||||
|
importer.SaveAndReimport();
|
||||||
|
}
|
||||||
|
|
||||||
|
fontTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(texPath);
|
||||||
|
EditorUtility.SetDirty(this);
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
|
Debug.Log($"PSXFontAsset: Generated bitmap {texW}x{texH} at {texPath}");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert the font texture to packed 4bpp data suitable for PS1 VRAM upload.
|
||||||
|
/// Returns byte array where each byte contains two 4-bit pixels.
|
||||||
|
/// Pixel value 0 = transparent, value 1 = opaque.
|
||||||
|
/// Output is 256 pixels wide × TextureHeight pixels tall.
|
||||||
|
/// </summary>
|
||||||
|
public byte[] ConvertTo4BPP()
|
||||||
|
{
|
||||||
|
if (fontTexture == null) return null;
|
||||||
|
|
||||||
|
int texW = 256; // always 256 pixels wide in 4bpp
|
||||||
|
int texH = TextureHeight;
|
||||||
|
// 4bpp: 2 pixels per byte → 128 bytes per row
|
||||||
|
int bytesPerRow = texW / 2;
|
||||||
|
byte[] result = new byte[bytesPerRow * texH];
|
||||||
|
|
||||||
|
Color[] pixels = fontTexture.GetPixels(0, 0, fontTexture.width, fontTexture.height);
|
||||||
|
int srcW = fontTexture.width;
|
||||||
|
int srcH = fontTexture.height;
|
||||||
|
|
||||||
|
for (int y = 0; y < texH; y++)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < texW; x += 2)
|
||||||
|
{
|
||||||
|
byte lo = SamplePixel(pixels, srcW, srcH, x, y, texH);
|
||||||
|
byte hi = SamplePixel(pixels, srcW, srcH, x + 1, y, texH);
|
||||||
|
// Pack two 4-bit values: low nibble first (PS1 byte order)
|
||||||
|
result[y * bytesPerRow + x / 2] = (byte)(lo | (hi << 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sample the font texture at (x, y) in output space.
|
||||||
|
/// Y=0 is top of texture (PS1 convention: top-down).
|
||||||
|
/// Returns 0 (transparent) or 1 (opaque).
|
||||||
|
/// </summary>
|
||||||
|
private byte SamplePixel(Color[] pixels, int srcW, int srcH, int x, int y, int outH)
|
||||||
|
{
|
||||||
|
if (x >= srcW || y >= srcH) return 0;
|
||||||
|
// Source texture is bottom-up (Unity convention), output is top-down (PS1)
|
||||||
|
int srcY = srcH - 1 - y;
|
||||||
|
if (srcY < 0 || srcY >= srcH) return 0;
|
||||||
|
Color c = pixels[srcY * srcW + x];
|
||||||
|
// Opaque if alpha > 0.5 or luminance > 0.5 (handles both alpha-based and grayscale fonts)
|
||||||
|
if (c.a > 0.5f || (c.r + c.g + c.b) / 3f > 0.5f)
|
||||||
|
return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
2
Runtime/PSXFontAsset.cs.meta
Normal file
2
Runtime/PSXFontAsset.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b5bc0d8f6252841439e81a8406c37859
|
||||||
@@ -55,6 +55,10 @@ namespace SplashEdit.RuntimeCode
|
|||||||
// Phase 5: Room/portal system (interior scenes)
|
// Phase 5: Room/portal system (interior scenes)
|
||||||
private PSXRoomBuilder _roomBuilder;
|
private PSXRoomBuilder _roomBuilder;
|
||||||
|
|
||||||
|
// Phase 6: UI canvases
|
||||||
|
private PSXCanvasData[] _canvases;
|
||||||
|
private PSXFontData[] _fonts;
|
||||||
|
|
||||||
private PSXData _psxData;
|
private PSXData _psxData;
|
||||||
|
|
||||||
private Vector2 selectedResolution;
|
private Vector2 selectedResolution;
|
||||||
@@ -106,9 +110,27 @@ namespace SplashEdit.RuntimeCode
|
|||||||
_interactables = FindObjectsByType<PSXInteractable>(FindObjectsSortMode.None);
|
_interactables = FindObjectsByType<PSXInteractable>(FindObjectsSortMode.None);
|
||||||
_audioSources = FindObjectsByType<PSXAudioSource>(FindObjectsSortMode.None);
|
_audioSources = FindObjectsByType<PSXAudioSource>(FindObjectsSortMode.None);
|
||||||
|
|
||||||
|
// Collect UI image textures for VRAM packing alongside 3D textures
|
||||||
|
PSXUIImage[] uiImages = FindObjectsByType<PSXUIImage>(FindObjectsSortMode.None);
|
||||||
|
List<PSXTexture2D> uiTextures = new List<PSXTexture2D>();
|
||||||
|
foreach (PSXUIImage img in uiImages)
|
||||||
|
{
|
||||||
|
if (img.SourceTexture != null)
|
||||||
|
{
|
||||||
|
Utils.SetTextureImporterFormat(img.SourceTexture, true);
|
||||||
|
PSXTexture2D tex = PSXTexture2D.CreateFromTexture2D(img.SourceTexture, img.BitDepth);
|
||||||
|
tex.OriginalTexture = img.SourceTexture;
|
||||||
|
img.PackedTexture = tex;
|
||||||
|
uiTextures.Add(tex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
EditorUtility.ClearProgressBar();
|
EditorUtility.ClearProgressBar();
|
||||||
|
|
||||||
PackTextures();
|
PackTextures(uiTextures);
|
||||||
|
|
||||||
|
// Collect UI canvases after VRAM packing (so PSXUIImage.PackedTexture has valid VRAM coords)
|
||||||
|
_canvases = PSXUIExporter.CollectCanvases(selectedResolution, out _fonts);
|
||||||
|
|
||||||
PSXPlayer player = FindObjectsByType<PSXPlayer>(FindObjectsSortMode.None).FirstOrDefault();
|
PSXPlayer player = FindObjectsByType<PSXPlayer>(FindObjectsSortMode.None).FirstOrDefault();
|
||||||
if (player != null)
|
if (player != null)
|
||||||
@@ -185,7 +207,7 @@ namespace SplashEdit.RuntimeCode
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void PackTextures()
|
void PackTextures(List<PSXTexture2D> additionalTextures = null)
|
||||||
{
|
{
|
||||||
(Rect buffer1, Rect buffer2) = Utils.BufferForResolution(selectedResolution, verticalLayout);
|
(Rect buffer1, Rect buffer2) = Utils.BufferForResolution(selectedResolution, verticalLayout);
|
||||||
|
|
||||||
@@ -196,7 +218,7 @@ namespace SplashEdit.RuntimeCode
|
|||||||
}
|
}
|
||||||
|
|
||||||
VRAMPacker tp = new VRAMPacker(framebuffers, prohibitedAreas);
|
VRAMPacker tp = new VRAMPacker(framebuffers, prohibitedAreas);
|
||||||
var packed = tp.PackTexturesIntoVRAM(_exporters);
|
var packed = tp.PackTexturesIntoVRAM(_exporters, additionalTextures);
|
||||||
_exporters = packed.processedObjects;
|
_exporters = packed.processedObjects;
|
||||||
_atlases = packed.atlases;
|
_atlases = packed.atlases;
|
||||||
|
|
||||||
@@ -260,6 +282,8 @@ namespace SplashEdit.RuntimeCode
|
|||||||
fogDensity = FogDensity,
|
fogDensity = FogDensity,
|
||||||
cutscenes = Cutscenes,
|
cutscenes = Cutscenes,
|
||||||
audioSources = _audioSources,
|
audioSources = _audioSources,
|
||||||
|
canvases = _canvases,
|
||||||
|
fonts = _fonts,
|
||||||
};
|
};
|
||||||
|
|
||||||
PSXSceneWriter.Write(path, in scene, (msg, type) =>
|
PSXSceneWriter.Write(path, in scene, (msg, type) =>
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ namespace SplashEdit.RuntimeCode
|
|||||||
public PSXCutsceneClip[] cutscenes;
|
public PSXCutsceneClip[] cutscenes;
|
||||||
public PSXAudioSource[] audioSources;
|
public PSXAudioSource[] audioSources;
|
||||||
|
|
||||||
|
// UI canvases (v13)
|
||||||
|
public PSXCanvasData[] canvases;
|
||||||
|
|
||||||
|
// Custom fonts (v13, embedded in UI block)
|
||||||
|
public PSXFontData[] fonts;
|
||||||
|
|
||||||
// Player
|
// Player
|
||||||
public Vector3 playerPos;
|
public Vector3 playerPos;
|
||||||
public Quaternion playerRot;
|
public Quaternion playerRot;
|
||||||
@@ -119,7 +125,7 @@ namespace SplashEdit.RuntimeCode
|
|||||||
// ──────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────
|
||||||
writer.Write('S');
|
writer.Write('S');
|
||||||
writer.Write('P');
|
writer.Write('P');
|
||||||
writer.Write((ushort)12); // version
|
writer.Write((ushort)13); // version
|
||||||
writer.Write((ushort)luaFiles.Count);
|
writer.Write((ushort)luaFiles.Count);
|
||||||
writer.Write((ushort)scene.exporters.Length);
|
writer.Write((ushort)scene.exporters.Length);
|
||||||
writer.Write((ushort)0); // navmeshCount (legacy)
|
writer.Write((ushort)0); // navmeshCount (legacy)
|
||||||
@@ -219,6 +225,15 @@ namespace SplashEdit.RuntimeCode
|
|||||||
long cutsceneTableOffsetPos = writer.BaseStream.Position;
|
long cutsceneTableOffsetPos = writer.BaseStream.Position;
|
||||||
writer.Write((uint)0); // cutsceneTableOffset placeholder
|
writer.Write((uint)0); // cutsceneTableOffset placeholder
|
||||||
|
|
||||||
|
// UI canvas header (version 13, 8 bytes)
|
||||||
|
int uiCanvasCount = scene.canvases?.Length ?? 0;
|
||||||
|
int uiFontCount = scene.fonts?.Length ?? 0;
|
||||||
|
writer.Write((ushort)uiCanvasCount);
|
||||||
|
writer.Write((byte)uiFontCount); // was uiReserved low byte
|
||||||
|
writer.Write((byte)0); // was uiReserved high byte
|
||||||
|
long uiTableOffsetPos = writer.BaseStream.Position;
|
||||||
|
writer.Write((uint)0); // uiTableOffset placeholder
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────
|
||||||
// Lua file metadata
|
// Lua file metadata
|
||||||
// ──────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────
|
||||||
@@ -617,6 +632,242 @@ namespace SplashEdit.RuntimeCode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
// UI canvas + font data (version 13)
|
||||||
|
// Font descriptors: 16 bytes each (before canvas data)
|
||||||
|
// Font pixel data: raw 4bpp (after font descriptors)
|
||||||
|
// Canvas descriptor table: 12 bytes per canvas
|
||||||
|
// Element records: 48 bytes each
|
||||||
|
// Name and text strings follow with offset backfill
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
if ((uiCanvasCount > 0 && scene.canvases != null) || uiFontCount > 0)
|
||||||
|
{
|
||||||
|
AlignToFourBytes(writer);
|
||||||
|
long uiTableStart = writer.BaseStream.Position;
|
||||||
|
|
||||||
|
// ── Font descriptors (16 bytes each) ──
|
||||||
|
// Layout: glyphW(1) glyphH(1) vramX(2) vramY(2) textureH(2)
|
||||||
|
// dataOffset(4) dataSize(4)
|
||||||
|
List<long> fontDataOffsetPositions = new List<long>();
|
||||||
|
if (scene.fonts != null)
|
||||||
|
{
|
||||||
|
foreach (var font in scene.fonts)
|
||||||
|
{
|
||||||
|
writer.Write(font.GlyphWidth); // [0]
|
||||||
|
writer.Write(font.GlyphHeight); // [1]
|
||||||
|
writer.Write(font.VramX); // [2-3]
|
||||||
|
writer.Write(font.VramY); // [4-5]
|
||||||
|
writer.Write(font.TextureHeight); // [6-7]
|
||||||
|
fontDataOffsetPositions.Add(writer.BaseStream.Position);
|
||||||
|
writer.Write((uint)0); // [8-11] dataOffset placeholder
|
||||||
|
writer.Write((uint)(font.PixelData?.Length ?? 0)); // [12-15] dataSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Font pixel data (raw 4bpp) ──
|
||||||
|
if (scene.fonts != null)
|
||||||
|
{
|
||||||
|
for (int fi = 0; fi < scene.fonts.Length; fi++)
|
||||||
|
{
|
||||||
|
var font = scene.fonts[fi];
|
||||||
|
if (font.PixelData == null || font.PixelData.Length == 0) continue;
|
||||||
|
|
||||||
|
AlignToFourBytes(writer);
|
||||||
|
long dataPos = writer.BaseStream.Position;
|
||||||
|
writer.Write(font.PixelData);
|
||||||
|
|
||||||
|
// Backfill data offset
|
||||||
|
long curPos = writer.BaseStream.Position;
|
||||||
|
writer.Seek((int)fontDataOffsetPositions[fi], SeekOrigin.Begin);
|
||||||
|
writer.Write((uint)dataPos);
|
||||||
|
writer.Seek((int)curPos, SeekOrigin.Begin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scene.fonts.Length > 0)
|
||||||
|
{
|
||||||
|
int totalFontBytes = 0;
|
||||||
|
foreach (var f in scene.fonts) totalFontBytes += f.PixelData?.Length ?? 0;
|
||||||
|
log?.Invoke($"{scene.fonts.Length} custom font(s) written ({totalFontBytes} bytes 4bpp data).", LogType.Log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Canvas descriptor table (12 bytes each) ──
|
||||||
|
// Layout per descriptor:
|
||||||
|
// uint32 dataOffset — offset to this canvas's element array
|
||||||
|
// uint8 nameLen
|
||||||
|
// uint8 sortOrder
|
||||||
|
// uint8 elementCount
|
||||||
|
// uint8 flags — bit 0 = startVisible
|
||||||
|
// uint32 nameOffset — offset to null-terminated name string
|
||||||
|
List<long> canvasDataOffsetPos = new List<long>();
|
||||||
|
List<long> canvasNameOffsetPos = new List<long>();
|
||||||
|
for (int ci = 0; ci < uiCanvasCount; ci++)
|
||||||
|
{
|
||||||
|
var cv = scene.canvases[ci];
|
||||||
|
string cvName = cv.Name ?? "";
|
||||||
|
if (cvName.Length > 24) cvName = cvName.Substring(0, 24);
|
||||||
|
|
||||||
|
canvasDataOffsetPos.Add(writer.BaseStream.Position);
|
||||||
|
writer.Write((uint)0); // dataOffset placeholder
|
||||||
|
writer.Write((byte)cvName.Length); // nameLen
|
||||||
|
writer.Write((byte)cv.SortOrder); // sortOrder
|
||||||
|
writer.Write((byte)(cv.Elements?.Length ?? 0)); // elementCount
|
||||||
|
writer.Write((byte)(cv.StartVisible ? 0x01 : 0x00)); // flags
|
||||||
|
canvasNameOffsetPos.Add(writer.BaseStream.Position);
|
||||||
|
writer.Write((uint)0); // nameOffset placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: Write element records (56 bytes each) per canvas
|
||||||
|
for (int ci = 0; ci < uiCanvasCount; ci++)
|
||||||
|
{
|
||||||
|
var cv = scene.canvases[ci];
|
||||||
|
if (cv.Elements == null || cv.Elements.Length == 0) continue;
|
||||||
|
|
||||||
|
AlignToFourBytes(writer);
|
||||||
|
long elemStart = writer.BaseStream.Position;
|
||||||
|
|
||||||
|
// Track text offset positions for backfill
|
||||||
|
List<long> textOffsetPositions = new List<long>();
|
||||||
|
List<string> textContents = new List<string>();
|
||||||
|
|
||||||
|
foreach (var el in cv.Elements)
|
||||||
|
{
|
||||||
|
// Identity (8 bytes)
|
||||||
|
writer.Write((byte)el.Type); // type
|
||||||
|
byte eFlags = (byte)(el.StartVisible ? 0x01 : 0x00);
|
||||||
|
writer.Write(eFlags); // flags
|
||||||
|
string eName = el.Name ?? "";
|
||||||
|
if (eName.Length > 24) eName = eName.Substring(0, 24);
|
||||||
|
writer.Write((byte)eName.Length); // nameLen
|
||||||
|
writer.Write((byte)0); // pad0
|
||||||
|
// nameOffset placeholder (backfilled later)
|
||||||
|
long elemNameOffPos = writer.BaseStream.Position;
|
||||||
|
writer.Write((uint)0); // nameOffset
|
||||||
|
|
||||||
|
// Layout (8 bytes)
|
||||||
|
writer.Write(el.X);
|
||||||
|
writer.Write(el.Y);
|
||||||
|
writer.Write(el.W);
|
||||||
|
writer.Write(el.H);
|
||||||
|
|
||||||
|
// Anchors (4 bytes)
|
||||||
|
writer.Write(el.AnchorMinX);
|
||||||
|
writer.Write(el.AnchorMinY);
|
||||||
|
writer.Write(el.AnchorMaxX);
|
||||||
|
writer.Write(el.AnchorMaxY);
|
||||||
|
|
||||||
|
// Primary color (4 bytes)
|
||||||
|
writer.Write(el.ColorR);
|
||||||
|
writer.Write(el.ColorG);
|
||||||
|
writer.Write(el.ColorB);
|
||||||
|
writer.Write((byte)0); // pad1
|
||||||
|
|
||||||
|
// Type-specific data (16 bytes)
|
||||||
|
switch (el.Type)
|
||||||
|
{
|
||||||
|
case PSXUIElementType.Image:
|
||||||
|
writer.Write(el.TexpageX); // [0]
|
||||||
|
writer.Write(el.TexpageY); // [1]
|
||||||
|
writer.Write(el.ClutX); // [2-3]
|
||||||
|
writer.Write(el.ClutY); // [4-5]
|
||||||
|
writer.Write(el.U0); // [6]
|
||||||
|
writer.Write(el.V0); // [7]
|
||||||
|
writer.Write(el.U1); // [8]
|
||||||
|
writer.Write(el.V1); // [9]
|
||||||
|
writer.Write(el.BitDepthIndex); // [10]
|
||||||
|
writer.Write(new byte[5]); // [11-15] padding
|
||||||
|
break;
|
||||||
|
case PSXUIElementType.Progress:
|
||||||
|
writer.Write(el.BgR); // [0]
|
||||||
|
writer.Write(el.BgG); // [1]
|
||||||
|
writer.Write(el.BgB); // [2]
|
||||||
|
writer.Write(el.ProgressValue); // [3]
|
||||||
|
writer.Write(new byte[12]); // [4-15] padding
|
||||||
|
break;
|
||||||
|
case PSXUIElementType.Text:
|
||||||
|
writer.Write(el.FontIndex); // [0] font index (0=system, 1+=custom)
|
||||||
|
writer.Write(new byte[15]); // [1-15] padding
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
writer.Write(new byte[16]); // zeroed
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text content offset (8 bytes)
|
||||||
|
long textOff = writer.BaseStream.Position;
|
||||||
|
writer.Write((uint)0); // textOffset placeholder
|
||||||
|
writer.Write((uint)0); // pad2
|
||||||
|
|
||||||
|
// Remember for backfill
|
||||||
|
textOffsetPositions.Add(textOff);
|
||||||
|
textContents.Add(el.Type == PSXUIElementType.Text ? (el.DefaultText ?? "") : null);
|
||||||
|
|
||||||
|
// Also remember element name for backfill
|
||||||
|
// We need to write it after all elements
|
||||||
|
textOffsetPositions.Add(elemNameOffPos);
|
||||||
|
textContents.Add("__NAME__" + eName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backfill canvas data offset
|
||||||
|
{
|
||||||
|
long curPos = writer.BaseStream.Position;
|
||||||
|
writer.Seek((int)canvasDataOffsetPos[ci], SeekOrigin.Begin);
|
||||||
|
writer.Write((uint)elemStart);
|
||||||
|
writer.Seek((int)curPos, SeekOrigin.Begin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write strings and backfill offsets
|
||||||
|
for (int si = 0; si < textOffsetPositions.Count; si++)
|
||||||
|
{
|
||||||
|
string s = textContents[si];
|
||||||
|
if (s == null) continue;
|
||||||
|
|
||||||
|
bool isName = s.StartsWith("__NAME__");
|
||||||
|
string content = isName ? s.Substring(8) : s;
|
||||||
|
if (string.IsNullOrEmpty(content) && !isName) continue;
|
||||||
|
|
||||||
|
long strPos = writer.BaseStream.Position;
|
||||||
|
byte[] strBytes = Encoding.UTF8.GetBytes(content);
|
||||||
|
writer.Write(strBytes);
|
||||||
|
writer.Write((byte)0); // null terminator
|
||||||
|
|
||||||
|
long curPos = writer.BaseStream.Position;
|
||||||
|
writer.Seek((int)textOffsetPositions[si], SeekOrigin.Begin);
|
||||||
|
writer.Write((uint)strPos);
|
||||||
|
writer.Seek((int)curPos, SeekOrigin.Begin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write canvas name strings and backfill name offsets
|
||||||
|
for (int ci = 0; ci < uiCanvasCount; ci++)
|
||||||
|
{
|
||||||
|
string cvName = scene.canvases[ci].Name ?? "";
|
||||||
|
if (cvName.Length > 24) cvName = cvName.Substring(0, 24);
|
||||||
|
|
||||||
|
long namePos = writer.BaseStream.Position;
|
||||||
|
byte[] nameBytes = Encoding.UTF8.GetBytes(cvName);
|
||||||
|
writer.Write(nameBytes);
|
||||||
|
writer.Write((byte)0); // null terminator
|
||||||
|
|
||||||
|
long curPos = writer.BaseStream.Position;
|
||||||
|
writer.Seek((int)canvasNameOffsetPos[ci], SeekOrigin.Begin);
|
||||||
|
writer.Write((uint)namePos);
|
||||||
|
writer.Seek((int)curPos, SeekOrigin.Begin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backfill UI table offset in header
|
||||||
|
{
|
||||||
|
long curPos = writer.BaseStream.Position;
|
||||||
|
writer.Seek((int)uiTableOffsetPos, SeekOrigin.Begin);
|
||||||
|
writer.Write((uint)uiTableStart);
|
||||||
|
writer.Seek((int)curPos, SeekOrigin.Begin);
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalElements = 0;
|
||||||
|
foreach (var cv in scene.canvases) totalElements += cv.Elements?.Length ?? 0;
|
||||||
|
log?.Invoke($"{uiCanvasCount} UI canvases ({totalElements} elements) written.", LogType.Log);
|
||||||
|
}
|
||||||
|
|
||||||
// Backfill offsets
|
// Backfill offsets
|
||||||
BackfillOffsets(writer, luaOffset, "lua", log);
|
BackfillOffsets(writer, luaOffset, "lua", log);
|
||||||
BackfillOffsets(writer, meshOffset, "mesh", log);
|
BackfillOffsets(writer, meshOffset, "mesh", log);
|
||||||
|
|||||||
@@ -10,5 +10,11 @@ namespace SplashEdit.RuntimeCode
|
|||||||
ObjectPosition = 2,
|
ObjectPosition = 2,
|
||||||
ObjectRotationY = 3,
|
ObjectRotationY = 3,
|
||||||
ObjectActive = 4,
|
ObjectActive = 4,
|
||||||
|
// UI track types (v13+)
|
||||||
|
UICanvasVisible = 5,
|
||||||
|
UIElementVisible = 6,
|
||||||
|
UIProgress = 7,
|
||||||
|
UIPosition = 8,
|
||||||
|
UIColor = 9,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
33
Runtime/PSXUIBox.cs
Normal file
33
Runtime/PSXUIBox.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace SplashEdit.RuntimeCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A solid-color rectangle UI element for PSX export.
|
||||||
|
/// Rendered as a FastFill primitive on PS1 hardware.
|
||||||
|
/// Attach to a child of a PSXCanvas GameObject.
|
||||||
|
/// </summary>
|
||||||
|
[RequireComponent(typeof(RectTransform))]
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
[AddComponentMenu("PSX/UI/PSX UI Box")]
|
||||||
|
public class PSXUIBox : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Tooltip("Name used to reference this element from Lua (max 24 chars).")]
|
||||||
|
[SerializeField] private string elementName = "box";
|
||||||
|
|
||||||
|
[Tooltip("Fill color for the box.")]
|
||||||
|
[SerializeField] private Color boxColor = Color.black;
|
||||||
|
|
||||||
|
[Tooltip("Whether this element is visible when the scene first loads.")]
|
||||||
|
[SerializeField] private bool startVisible = true;
|
||||||
|
|
||||||
|
/// <summary>Element name for Lua access.</summary>
|
||||||
|
public string ElementName => elementName;
|
||||||
|
|
||||||
|
/// <summary>Box fill color (RGB, alpha ignored).</summary>
|
||||||
|
public Color BoxColor => boxColor;
|
||||||
|
|
||||||
|
/// <summary>Initial visibility flag.</summary>
|
||||||
|
public bool StartVisible => startVisible;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Runtime/PSXUIBox.cs.meta
Normal file
2
Runtime/PSXUIBox.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6d0070761745bb147bb74f85ca7960cd
|
||||||
14
Runtime/PSXUIElementType.cs
Normal file
14
Runtime/PSXUIElementType.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace SplashEdit.RuntimeCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// UI element types matching the C++ UIElementType enum.
|
||||||
|
/// Values must stay in sync with uisystem.hh.
|
||||||
|
/// </summary>
|
||||||
|
public enum PSXUIElementType : byte
|
||||||
|
{
|
||||||
|
Image = 0,
|
||||||
|
Box = 1,
|
||||||
|
Text = 2,
|
||||||
|
Progress = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Runtime/PSXUIElementType.cs.meta
Normal file
2
Runtime/PSXUIElementType.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1f27e7fda137b1d43b493af1fa727370
|
||||||
346
Runtime/PSXUIExporter.cs
Normal file
346
Runtime/PSXUIExporter.cs
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// Font textures go at x=960 (same column as system font), stacking upward from y=464
|
||||||
|
// System font: (960, 464)-(1023, 511) → 64 wide, 48 tall
|
||||||
|
List<PSXFontData> fontDataList = new List<PSXFontData>();
|
||||||
|
ushort fontVramY = 0; // start from top of VRAM at x=960
|
||||||
|
foreach (PSXFontAsset fa in uniqueFonts)
|
||||||
|
{
|
||||||
|
byte[] pixelData = fa.ConvertTo4BPP();
|
||||||
|
if (pixelData == null) continue;
|
||||||
|
|
||||||
|
ushort texH = (ushort)fa.TextureHeight;
|
||||||
|
fontDataList.Add(new PSXFontData
|
||||||
|
{
|
||||||
|
Source = fa,
|
||||||
|
GlyphWidth = (byte)fa.GlyphWidth,
|
||||||
|
GlyphHeight = (byte)fa.GlyphHeight,
|
||||||
|
VramX = 960, // same column as system font (64 VRAM hwords for 256px 4bpp)
|
||||||
|
VramY = fontVramY,
|
||||||
|
TextureHeight = texH,
|
||||||
|
PixelData = pixelData
|
||||||
|
});
|
||||||
|
fontVramY += texH;
|
||||||
|
}
|
||||||
|
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>();
|
||||||
|
|
||||||
|
CollectImages(canvas.transform, canvasRect, scaleX, scaleY, resolution, elements);
|
||||||
|
CollectBoxes(canvas.transform, canvasRect, scaleX, scaleY, resolution, elements);
|
||||||
|
CollectTexts(canvas.transform, canvasRect, scaleX, scaleY, resolution, elements, uniqueFonts);
|
||||||
|
CollectProgressBars(canvas.transform, canvasRect, scaleX, scaleY, resolution, elements);
|
||||||
|
|
||||||
|
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 ───
|
||||||
|
|
||||||
|
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;
|
||||||
|
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 * expander / ((int)tex.BitDepth / (int)PSXBPP.TEX_4BIT));
|
||||||
|
data.V1 = (byte)(tex.PackingY + tex.Height);
|
||||||
|
data.BitDepthIndex = tex.BitDepth switch
|
||||||
|
{
|
||||||
|
PSXBPP.TEX_4BIT => 0,
|
||||||
|
PSXBPP.TEX_8BIT => 1,
|
||||||
|
PSXBPP.TEX_16BIT => 2,
|
||||||
|
_ => 2
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Runtime/PSXUIExporter.cs.meta
Normal file
2
Runtime/PSXUIExporter.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 394bbca774462bf44b775ea916961394
|
||||||
51
Runtime/PSXUIImage.cs
Normal file
51
Runtime/PSXUIImage.cs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace SplashEdit.RuntimeCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A textured UI image element for PSX export.
|
||||||
|
/// Attach to a child of a PSXCanvas GameObject.
|
||||||
|
/// The RectTransform determines position and size in PS1 screen space.
|
||||||
|
/// </summary>
|
||||||
|
[RequireComponent(typeof(RectTransform))]
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
[AddComponentMenu("PSX/UI/PSX UI Image")]
|
||||||
|
public class PSXUIImage : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Tooltip("Name used to reference this element from Lua (max 24 chars).")]
|
||||||
|
[SerializeField] private string elementName = "image";
|
||||||
|
|
||||||
|
[Tooltip("Source texture for this UI image. Will be quantized and packed into VRAM.")]
|
||||||
|
[SerializeField] private Texture2D sourceTexture;
|
||||||
|
|
||||||
|
[Tooltip("Bit depth for VRAM storage.")]
|
||||||
|
[SerializeField] private PSXBPP bitDepth = PSXBPP.TEX_8BIT;
|
||||||
|
|
||||||
|
[Tooltip("Tint color applied to the image (white = no tint).")]
|
||||||
|
[SerializeField] private Color tintColor = Color.white;
|
||||||
|
|
||||||
|
[Tooltip("Whether this element is visible when the scene first loads.")]
|
||||||
|
[SerializeField] private bool startVisible = true;
|
||||||
|
|
||||||
|
/// <summary>Element name for Lua access.</summary>
|
||||||
|
public string ElementName => elementName;
|
||||||
|
|
||||||
|
/// <summary>Source texture for quantization and VRAM packing.</summary>
|
||||||
|
public Texture2D SourceTexture => sourceTexture;
|
||||||
|
|
||||||
|
/// <summary>Bit depth for the packed texture.</summary>
|
||||||
|
public PSXBPP BitDepth => bitDepth;
|
||||||
|
|
||||||
|
/// <summary>Tint color (RGB, alpha ignored).</summary>
|
||||||
|
public Color TintColor => tintColor;
|
||||||
|
|
||||||
|
/// <summary>Initial visibility flag.</summary>
|
||||||
|
public bool StartVisible => startVisible;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// After VRAM packing, the exporter fills in these fields so the
|
||||||
|
/// binary writer can emit the correct tpage/clut/UV data.
|
||||||
|
/// </summary>
|
||||||
|
[System.NonSerialized] public PSXTexture2D PackedTexture;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Runtime/PSXUIImage.cs.meta
Normal file
2
Runtime/PSXUIImage.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 00606032435f7154cb260ebf56f477a9
|
||||||
46
Runtime/PSXUIProgressBar.cs
Normal file
46
Runtime/PSXUIProgressBar.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace SplashEdit.RuntimeCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A progress bar UI element for PSX export.
|
||||||
|
/// Rendered as two FastFill primitives (background + fill) on PS1 hardware.
|
||||||
|
/// Attach to a child of a PSXCanvas GameObject.
|
||||||
|
/// </summary>
|
||||||
|
[RequireComponent(typeof(RectTransform))]
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
[AddComponentMenu("PSX/UI/PSX UI Progress Bar")]
|
||||||
|
public class PSXUIProgressBar : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Tooltip("Name used to reference this element from Lua (max 24 chars).")]
|
||||||
|
[SerializeField] private string elementName = "progress";
|
||||||
|
|
||||||
|
[Tooltip("Background color (shown behind the fill).")]
|
||||||
|
[SerializeField] private Color backgroundColor = new Color(0.2f, 0.2f, 0.2f);
|
||||||
|
|
||||||
|
[Tooltip("Fill color (the progressing portion).")]
|
||||||
|
[SerializeField] private Color fillColor = Color.green;
|
||||||
|
|
||||||
|
[Tooltip("Initial progress value (0-100).")]
|
||||||
|
[Range(0, 100)]
|
||||||
|
[SerializeField] private int initialValue = 0;
|
||||||
|
|
||||||
|
[Tooltip("Whether this element is visible when the scene first loads.")]
|
||||||
|
[SerializeField] private bool startVisible = true;
|
||||||
|
|
||||||
|
/// <summary>Element name for Lua access.</summary>
|
||||||
|
public string ElementName => elementName;
|
||||||
|
|
||||||
|
/// <summary>Background color (RGB).</summary>
|
||||||
|
public Color BackgroundColor => backgroundColor;
|
||||||
|
|
||||||
|
/// <summary>Fill color (RGB).</summary>
|
||||||
|
public Color FillColor => fillColor;
|
||||||
|
|
||||||
|
/// <summary>Initial progress value 0-100.</summary>
|
||||||
|
public int InitialValue => Mathf.Clamp(initialValue, 0, 100);
|
||||||
|
|
||||||
|
/// <summary>Initial visibility flag.</summary>
|
||||||
|
public bool StartVisible => startVisible;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Runtime/PSXUIProgressBar.cs.meta
Normal file
2
Runtime/PSXUIProgressBar.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a3c1e93bdff6a714ca5b47989b5b2401
|
||||||
60
Runtime/PSXUIText.cs
Normal file
60
Runtime/PSXUIText.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace SplashEdit.RuntimeCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A text UI element for PSX export.
|
||||||
|
/// Rendered via psyqo::Font::chainprintf on PS1 hardware.
|
||||||
|
/// Attach to a child of a PSXCanvas GameObject.
|
||||||
|
/// </summary>
|
||||||
|
[RequireComponent(typeof(RectTransform))]
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
[AddComponentMenu("PSX/UI/PSX UI Text")]
|
||||||
|
public class PSXUIText : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Tooltip("Name used to reference this element from Lua (max 24 chars).")]
|
||||||
|
[SerializeField] private string elementName = "text";
|
||||||
|
|
||||||
|
[Tooltip("Default text content (max 63 chars). Can be changed at runtime via Lua UI.SetText().")]
|
||||||
|
[SerializeField] private string defaultText = "";
|
||||||
|
|
||||||
|
[Tooltip("Text color.")]
|
||||||
|
[SerializeField] private Color textColor = Color.white;
|
||||||
|
|
||||||
|
[Tooltip("Whether this element is visible when the scene first loads.")]
|
||||||
|
[SerializeField] private bool startVisible = true;
|
||||||
|
|
||||||
|
[Tooltip("Custom font override. If null, uses the canvas default font (or built-in system font).")]
|
||||||
|
[SerializeField] private PSXFontAsset fontOverride;
|
||||||
|
|
||||||
|
/// <summary>Element name for Lua access.</summary>
|
||||||
|
public string ElementName => elementName;
|
||||||
|
|
||||||
|
/// <summary>Default text content (truncated to 63 chars on export).</summary>
|
||||||
|
public string DefaultText => defaultText;
|
||||||
|
|
||||||
|
/// <summary>Text color (RGB, alpha ignored).</summary>
|
||||||
|
public Color TextColor => textColor;
|
||||||
|
|
||||||
|
/// <summary>Initial visibility flag.</summary>
|
||||||
|
public bool StartVisible => startVisible;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Custom font override. If null, inherits from parent PSXCanvas.DefaultFont.
|
||||||
|
/// If that is also null, uses the built-in system font.
|
||||||
|
/// </summary>
|
||||||
|
public PSXFontAsset FontOverride => fontOverride;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolve the effective font for this text element.
|
||||||
|
/// Checks: fontOverride → parent PSXCanvas.DefaultFont → null (system font).
|
||||||
|
/// </summary>
|
||||||
|
public PSXFontAsset GetEffectiveFont()
|
||||||
|
{
|
||||||
|
if (fontOverride != null) return fontOverride;
|
||||||
|
PSXCanvas canvas = GetComponentInParent<PSXCanvas>();
|
||||||
|
if (canvas != null && canvas.DefaultFont != null) return canvas.DefaultFont;
|
||||||
|
return null; // system font
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Runtime/PSXUIText.cs.meta
Normal file
2
Runtime/PSXUIText.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ca4c021b4490c8f4fb9de4e14055b9e3
|
||||||
@@ -64,8 +64,9 @@ namespace SplashEdit.RuntimeCode
|
|||||||
/// Returns the processed objects and the final VRAM pixel array.
|
/// Returns the processed objects and the final VRAM pixel array.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="objects">Array of PSXObjectExporter objects to process.</param>
|
/// <param name="objects">Array of PSXObjectExporter objects to process.</param>
|
||||||
|
/// <param name="additionalTextures">Optional standalone textures (e.g. UI images) to include in VRAM packing.</param>
|
||||||
/// <returns>Tuple containing processed objects, texture atlases, and the VRAM pixel array.</returns>
|
/// <returns>Tuple containing processed objects, texture atlases, and the VRAM pixel array.</returns>
|
||||||
public (PSXObjectExporter[] processedObjects, TextureAtlas[] atlases, VRAMPixel[,] vramPixels) PackTexturesIntoVRAM(PSXObjectExporter[] objects)
|
public (PSXObjectExporter[] processedObjects, TextureAtlas[] atlases, VRAMPixel[,] vramPixels) PackTexturesIntoVRAM(PSXObjectExporter[] objects, List<PSXTexture2D> additionalTextures = null)
|
||||||
{
|
{
|
||||||
// Gather all textures from all exporters.
|
// Gather all textures from all exporters.
|
||||||
List<PSXTexture2D> allTextures = new List<PSXTexture2D>();
|
List<PSXTexture2D> allTextures = new List<PSXTexture2D>();
|
||||||
@@ -74,6 +75,10 @@ namespace SplashEdit.RuntimeCode
|
|||||||
allTextures.AddRange(obj.Textures);
|
allTextures.AddRange(obj.Textures);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Include additional standalone textures (e.g. UI images)
|
||||||
|
if (additionalTextures != null)
|
||||||
|
allTextures.AddRange(additionalTextures);
|
||||||
|
|
||||||
// List to track unique textures and their indices
|
// List to track unique textures and their indices
|
||||||
List<PSXTexture2D> uniqueTextures = new List<PSXTexture2D>();
|
List<PSXTexture2D> uniqueTextures = new List<PSXTexture2D>();
|
||||||
Dictionary<(int, PSXBPP), int> textureToIndexMap = new Dictionary<(int, PSXBPP), int>();
|
Dictionary<(int, PSXBPP), int> textureToIndexMap = new Dictionary<(int, PSXBPP), int>();
|
||||||
|
|||||||
Reference in New Issue
Block a user