Somewhat fixed ui
This commit is contained in:
@@ -4,7 +4,7 @@ using SplashEdit.RuntimeCode;
|
||||
|
||||
namespace SplashEdit.EditorCode
|
||||
{
|
||||
// ─── Scene Preview Gizmos ───
|
||||
// --- Scene Preview Gizmos ---
|
||||
// These draw filled rectangles in the Scene view for WYSIWYG UI preview.
|
||||
|
||||
public static class PSXUIGizmos
|
||||
@@ -76,9 +76,8 @@ namespace SplashEdit.EditorCode
|
||||
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.
|
||||
// Pixel-perfect preview: render each glyph from the actual font bitmap
|
||||
// at PS1 scale, using advance widths for proportional positioning.
|
||||
string label = string.IsNullOrEmpty(text.DefaultText) ? "[empty]" : text.DefaultText;
|
||||
|
||||
PSXFontAsset font = text.GetEffectiveFont();
|
||||
@@ -86,39 +85,85 @@ namespace SplashEdit.EditorCode
|
||||
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 guiX = Mathf.Min(topLeft.x, botRight.x);
|
||||
float guiY = Mathf.Min(topLeft.y, botRight.y);
|
||||
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;
|
||||
Color tintColor = text.TextColor;
|
||||
tintColor.a = selected ? 1f : 0.7f;
|
||||
|
||||
// If we have a font bitmap, render pixel-perfect from the texture
|
||||
if (font != null && font.FontTexture != null && font.SourceFont != null)
|
||||
{
|
||||
Texture2D fontTex = font.FontTexture;
|
||||
int glyphsPerRow = font.GlyphsPerRow;
|
||||
int texH = font.TextureHeight;
|
||||
float cellScreenW = glyphW * psxPixelScale;
|
||||
float cellScreenH = glyphH * psxPixelScale;
|
||||
|
||||
// Get advance widths for proportional positioning
|
||||
float cursorX = guiX;
|
||||
GUI.color = tintColor;
|
||||
foreach (char ch in label)
|
||||
{
|
||||
if (ch < 32 || ch > 126) continue;
|
||||
int charIdx = ch - 32;
|
||||
int col = charIdx % glyphsPerRow;
|
||||
int row = charIdx / glyphsPerRow;
|
||||
|
||||
float advance = glyphW; // fallback
|
||||
if (font.AdvanceWidths != null && charIdx < font.AdvanceWidths.Length)
|
||||
{
|
||||
advance = font.AdvanceWidths[charIdx];
|
||||
}
|
||||
|
||||
if (ch != ' ')
|
||||
{
|
||||
// UV rect for this glyph in the font bitmap
|
||||
float uvX = (float)(col * glyphW) / fontTex.width;
|
||||
float uvY = 1f - (float)((row + 1) * glyphH) / fontTex.height;
|
||||
float uvW = (float)glyphW / fontTex.width;
|
||||
float uvH = (float)glyphH / fontTex.height;
|
||||
|
||||
// Draw at advance width, not cell width - matches PS1 proportional rendering
|
||||
float spriteScreenW = advance * psxPixelScale;
|
||||
Rect screenRect = new Rect(cursorX, guiY, spriteScreenW, cellScreenH);
|
||||
// UV: only show the portion of the cell that the advance covers
|
||||
float uvWScaled = uvW * (advance / glyphW);
|
||||
Rect uvRect = new Rect(uvX, uvY, uvWScaled, uvH);
|
||||
|
||||
if (screenRect.xMax > guiX && screenRect.x < guiX + guiW)
|
||||
GUI.DrawTextureWithTexCoords(screenRect, fontTex, uvRect);
|
||||
}
|
||||
|
||||
cursorX += advance * psxPixelScale;
|
||||
}
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: use Unity's text rendering for system font
|
||||
int fSize = Mathf.Clamp(Mathf.RoundToInt(glyphH * psxPixelScale * 0.75f), 6, 72);
|
||||
GUIStyle style = new GUIStyle(EditorStyles.label);
|
||||
style.normal.textColor = tintColor;
|
||||
style.alignment = TextAnchor.UpperLeft;
|
||||
style.fontSize = fSize;
|
||||
style.wordWrap = false;
|
||||
style.clipping = TextClipping.Clip;
|
||||
|
||||
Rect guiRect = new Rect(guiX, guiY, guiW, guiH);
|
||||
GUI.color = tintColor;
|
||||
GUI.Label(guiRect, label, style);
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
Handles.EndGUI();
|
||||
}
|
||||
|
||||
@@ -259,7 +304,7 @@ namespace SplashEdit.EditorCode
|
||||
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);
|
||||
EditorGUILayout.HelpBox("Texture exceeds 256x256. It will be resized during export.", MessageType.Warning);
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
@@ -377,31 +422,40 @@ namespace SplashEdit.EditorCode
|
||||
{
|
||||
serializedObject.Update();
|
||||
|
||||
PSXFontAsset font = (PSXFontAsset)target;
|
||||
|
||||
EditorGUILayout.LabelField("PSX Font Asset", EditorStyles.boldLabel);
|
||||
EditorGUILayout.Space(4);
|
||||
|
||||
// Source font (TTF/OTF)
|
||||
// ── 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."));
|
||||
"Assign a Unity Font (TrueType/OpenType). Click 'Generate Bitmap' to rasterize it.\n" +
|
||||
"Glyph cell dimensions are auto-derived from the font metrics."));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("fontSize"), new GUIContent("Font Size",
|
||||
"Pixel height for rasterization. Determines glyph cell height."));
|
||||
"Pixel height for rasterization. Determines glyph cell height.\n" +
|
||||
"Glyph cell width is auto-derived from the widest character.\n" +
|
||||
"Changing this and re-generating will update both the bitmap AND the glyph dimensions."));
|
||||
|
||||
PSXFontAsset font = (PSXFontAsset)target;
|
||||
if (font.SourceFont != null)
|
||||
{
|
||||
if (GUILayout.Button("Generate Bitmap from Font"))
|
||||
EditorGUILayout.Space(2);
|
||||
if (GUILayout.Button("Generate Bitmap from Font", GUILayout.Height(28)))
|
||||
{
|
||||
Undo.RecordObject(font, "Generate PSX Font Bitmap");
|
||||
font.GenerateBitmapFromFont();
|
||||
}
|
||||
|
||||
if (font.FontTexture == null)
|
||||
EditorGUILayout.HelpBox("Click 'Generate Bitmap' to create the font texture.", MessageType.Info);
|
||||
EditorGUILayout.HelpBox(
|
||||
"Click 'Generate Bitmap' to create the font texture.\n" +
|
||||
"If generation fails, check that the font's import settings have " +
|
||||
"'Character' set to 'ASCII Default Set'.", MessageType.Info);
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(8);
|
||||
|
||||
// Manual bitmap
|
||||
// ── 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). " +
|
||||
@@ -409,15 +463,32 @@ namespace SplashEdit.EditorCode
|
||||
|
||||
EditorGUILayout.Space(4);
|
||||
|
||||
// Glyph metrics
|
||||
// ── Glyph metrics ──
|
||||
// If using auto-convert, these are set by GenerateBitmapFromFont.
|
||||
// For manual bitmaps, the user sets them directly.
|
||||
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."));
|
||||
if (font.SourceFont != null && font.FontTexture != null)
|
||||
{
|
||||
// Show as read-only when auto-generated
|
||||
EditorGUI.BeginDisabledGroup(true);
|
||||
EditorGUILayout.IntField(new GUIContent("Glyph Width", "Auto-derived from font. Re-generate to change."), font.GlyphWidth);
|
||||
EditorGUILayout.IntField(new GUIContent("Glyph Height", "Auto-derived from font. Re-generate to change."), font.GlyphHeight);
|
||||
EditorGUI.EndDisabledGroup();
|
||||
EditorGUILayout.HelpBox("Glyph dimensions are auto-derived when generating from a font.\n" +
|
||||
"Change the Font Size slider and re-generate to adjust.", MessageType.Info);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Editable for manual bitmap workflow
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("glyphWidth"), new GUIContent("Glyph Width",
|
||||
"Width of each glyph cell in pixels. Must divide 256 evenly (4, 8, 16, or 32)."));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("glyphHeight"), new GUIContent("Glyph Height",
|
||||
"Height of each glyph cell in pixels."));
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(4);
|
||||
|
||||
// ── Layout info ──
|
||||
int glyphsPerRow = font.GlyphsPerRow;
|
||||
int rowCount = font.RowCount;
|
||||
int totalH = font.TextureHeight;
|
||||
@@ -427,15 +498,42 @@ namespace SplashEdit.EditorCode
|
||||
$"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}",
|
||||
$"Glyph cell: {font.GlyphWidth} x {font.GlyphHeight}",
|
||||
MessageType.Info);
|
||||
|
||||
// Show advance width status
|
||||
if (font.AdvanceWidths != null && font.AdvanceWidths.Length >= 95)
|
||||
{
|
||||
int minAdv = 255, maxAdv = 0;
|
||||
for (int i = 1; i < 95; i++) // skip space
|
||||
{
|
||||
if (font.AdvanceWidths[i] < minAdv) minAdv = font.AdvanceWidths[i];
|
||||
if (font.AdvanceWidths[i] > maxAdv) maxAdv = font.AdvanceWidths[i];
|
||||
}
|
||||
EditorGUILayout.HelpBox(
|
||||
$"Advance widths: {minAdv}-{maxAdv}px (proportional spacing stored)",
|
||||
MessageType.Info);
|
||||
}
|
||||
else if (font.FontTexture != null)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"No advance widths stored. Click 'Generate Bitmap' to compute them.",
|
||||
MessageType.Warning);
|
||||
}
|
||||
|
||||
// ── Validation ──
|
||||
if (font.FontTexture != null)
|
||||
{
|
||||
if (font.FontTexture.width != 256)
|
||||
EditorGUILayout.HelpBox($"Font texture must be 256 pixels wide (currently {font.FontTexture.width}).", MessageType.Error);
|
||||
|
||||
if (256 % font.GlyphWidth != 0)
|
||||
EditorGUILayout.HelpBox($"Glyph width ({font.GlyphWidth}) must divide 256 evenly. " +
|
||||
"Valid values: 4, 8, 16, 32.", MessageType.Error);
|
||||
|
||||
// Show preview
|
||||
EditorGUILayout.Space(4);
|
||||
EditorGUILayout.LabelField("Preview", EditorStyles.miniBoldLabel);
|
||||
Rect previewRect = EditorGUILayout.GetControlRect(false, 64);
|
||||
GUI.DrawTexture(previewRect, font.FontTexture, ScaleMode.ScaleToFit);
|
||||
}
|
||||
|
||||
@@ -81,5 +81,8 @@ namespace SplashEdit.RuntimeCode
|
||||
|
||||
/// <summary>Packed 4bpp pixel data ready for VRAM upload.</summary>
|
||||
public byte[] PixelData;
|
||||
|
||||
/// <summary>Per-character advance widths (96 entries, ASCII 0x20-0x7F) for proportional rendering.</summary>
|
||||
public byte[] AdvanceWidths;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,76 +7,54 @@ using UnityEditor;
|
||||
|
||||
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.")]
|
||||
[Header("Source - Option A: TrueType/OTF Font")]
|
||||
[Tooltip("Assign a Unity Font asset (TTF/OTF). Click 'Generate Bitmap' to rasterize.")]
|
||||
[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.")]
|
||||
[Tooltip("Font size in pixels. Larger = more detail but uses more VRAM.\n" +
|
||||
"The actual glyph cell size is auto-computed to fit within PS1 texture page limits.")]
|
||||
[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.")]
|
||||
[Header("Source - Option B: Manual Bitmap")]
|
||||
[Tooltip("Font bitmap texture. Must be 256 pixels wide.\n" +
|
||||
"Glyphs in ASCII order from 0x20, transparent = bg, opaque = fg.")]
|
||||
[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)]
|
||||
[Tooltip("Width of each glyph cell (auto-set from font, editable for manual bitmap).\n" +
|
||||
"Must divide 256 evenly: 4, 8, 16, or 32.")]
|
||||
[SerializeField] private int glyphWidth = 8;
|
||||
|
||||
[Tooltip("Height of each glyph cell in pixels (e.g. 16 for an 8x16 font).")]
|
||||
[Range(4, 32)]
|
||||
[Tooltip("Height of each glyph cell (auto-set from font, editable for manual bitmap).")]
|
||||
[SerializeField] private int glyphHeight = 16;
|
||||
|
||||
/// <summary>The Unity Font source (TTF/OTF). Null if using manual bitmap.</summary>
|
||||
[HideInInspector]
|
||||
[SerializeField] private byte[] storedAdvanceWidths;
|
||||
|
||||
// Valid glyph widths: must divide 256 evenly for PSYQo texture UV wrapping.
|
||||
private static readonly int[] ValidGlyphWidths = { 4, 8, 16, 32 };
|
||||
|
||||
// PS1 texture page is 256 pixels tall. Font texture MUST fit in one page.
|
||||
private const int MAX_TEXTURE_PAGE_HEIGHT = 256;
|
||||
|
||||
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>
|
||||
/// <summary>Per-character advance widths (96 entries, ASCII 0x20-0x7F). Computed during generation.</summary>
|
||||
public byte[] AdvanceWidths => storedAdvanceWidths;
|
||||
|
||||
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)
|
||||
@@ -85,153 +63,338 @@ namespace SplashEdit.RuntimeCode
|
||||
return;
|
||||
}
|
||||
|
||||
// Request all printable ASCII characters from the font at the target size
|
||||
CharacterInfo ci;
|
||||
// ── Step 1: Populate the font atlas ──
|
||||
string ascii = "";
|
||||
for (int c = 0x20; c <= 0x7E; c++) ascii += (char)c;
|
||||
sourceFont.RequestCharactersInTexture(ascii, fontSize, FontStyle.Normal);
|
||||
|
||||
// ── Step 2: Get readable copy of atlas texture ──
|
||||
// For non-dynamic fonts, the atlas may only be populated at the native size.
|
||||
// Try the requested size first, then fall back to size=0.
|
||||
Texture fontTex = sourceFont.material != null ? sourceFont.material.mainTexture : null;
|
||||
if (fontTex == null || fontTex.width == 0 || fontTex.height == 0)
|
||||
{
|
||||
// Retry with size=0 (native size) for non-dynamic fonts
|
||||
sourceFont.RequestCharactersInTexture(ascii, 0, FontStyle.Normal);
|
||||
fontTex = sourceFont.material != null ? sourceFont.material.mainTexture : null;
|
||||
}
|
||||
if (fontTex == null)
|
||||
{
|
||||
Debug.LogError("PSXFontAsset: Font atlas is null. Set Character to 'ASCII Default Set' in font import settings.");
|
||||
return;
|
||||
}
|
||||
|
||||
int fontTexW = fontTex.width;
|
||||
int fontTexH = fontTex.height;
|
||||
if (fontTexW == 0 || fontTexH == 0)
|
||||
{
|
||||
Debug.LogError("PSXFontAsset: Font atlas has zero dimensions. Try re-importing the font with 'ASCII Default Set'.");
|
||||
return;
|
||||
}
|
||||
|
||||
Color[] fontPixels;
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
// Verify atlas isn't blank
|
||||
bool hasAnyPixel = false;
|
||||
for (int i = 0; i < fontPixels.Length && !hasAnyPixel; i++)
|
||||
{
|
||||
if (fontPixels[i].a > 0.1f) hasAnyPixel = true;
|
||||
}
|
||||
if (!hasAnyPixel)
|
||||
{
|
||||
Debug.LogError("PSXFontAsset: Font atlas is blank. Set Character to 'ASCII Default Set' in font import settings.");
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Step 3: Get character info ──
|
||||
// Non-dynamic fonts only respond to size=0 or their native size.
|
||||
// Dynamic fonts respond to any size.
|
||||
CharacterInfo[] charInfos = new CharacterInfo[95];
|
||||
bool[] charValid = new bool[95];
|
||||
int validCount = 0;
|
||||
int workingSize = fontSize;
|
||||
|
||||
// Try requested fontSize first
|
||||
for (int c = 0x20; c <= 0x7E; c++)
|
||||
{
|
||||
int idx = c - 0x20;
|
||||
if (sourceFont.GetCharacterInfo((char)c, out charInfos[idx], fontSize, FontStyle.Normal))
|
||||
{
|
||||
charValid[idx] = true;
|
||||
validCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// If that failed, try size=0 (non-dynamic fonts need this)
|
||||
if (validCount == 0)
|
||||
{
|
||||
sourceFont.RequestCharactersInTexture(ascii, 0, FontStyle.Normal);
|
||||
for (int c = 0x20; c <= 0x7E; c++)
|
||||
{
|
||||
int idx = c - 0x20;
|
||||
if (sourceFont.GetCharacterInfo((char)c, out charInfos[idx], 0, FontStyle.Normal))
|
||||
{
|
||||
charValid[idx] = true;
|
||||
validCount++;
|
||||
}
|
||||
}
|
||||
if (validCount > 0)
|
||||
{
|
||||
workingSize = 0;
|
||||
Debug.Log("PSXFontAsset: Using font's native size for character info.");
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort: read characterInfo array directly
|
||||
if (validCount == 0 && sourceFont.characterInfo != null)
|
||||
{
|
||||
foreach (CharacterInfo fci in sourceFont.characterInfo)
|
||||
{
|
||||
int c = fci.index;
|
||||
if (c >= 0x20 && c <= 0x7E)
|
||||
{
|
||||
charInfos[c - 0x20] = fci;
|
||||
charValid[c - 0x20] = true;
|
||||
validCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (validCount == 0)
|
||||
{
|
||||
Debug.LogError("PSXFontAsset: Could not get character info from font.");
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Step 4: Choose glyph cell dimensions ──
|
||||
// Constraints:
|
||||
// - glyphWidth must divide 256 (valid: 4, 8, 16, 32)
|
||||
// - ceil(95 / (256/glyphWidth)) * glyphHeight <= 256 (must fit in one texture page)
|
||||
// - glyphHeight in [4, 32]
|
||||
// Strategy: pick the smallest valid width where everything fits.
|
||||
// Glyphs that exceed the cell are scaled to fit.
|
||||
|
||||
int measuredMaxW = 0, measuredMaxH = 0;
|
||||
for (int idx = 1; idx < 95; idx++) // skip space
|
||||
{
|
||||
if (!charValid[idx]) continue;
|
||||
CharacterInfo ci = charInfos[idx];
|
||||
int pw = Mathf.Abs(ci.maxX - ci.minX);
|
||||
int ph = Mathf.Abs(ci.maxY - ci.minY);
|
||||
if (pw > measuredMaxW) measuredMaxW = pw;
|
||||
if (ph > measuredMaxH) measuredMaxH = ph;
|
||||
}
|
||||
|
||||
// Target height based on measured glyphs + margin
|
||||
int targetH = Mathf.Clamp(measuredMaxH + 2, 4, 32);
|
||||
|
||||
// Find the best valid width: start from the IDEAL (closest to measured width)
|
||||
// and go smaller only if the texture wouldn't fit in 256px vertically.
|
||||
// This maximizes glyph quality by using the widest cells that fit.
|
||||
int bestW = -1, bestH = -1;
|
||||
|
||||
// Find ideal: smallest valid width >= measured glyph width
|
||||
int idealIdx = ValidGlyphWidths.Length - 1; // default to largest (32)
|
||||
for (int i = 0; i < ValidGlyphWidths.Length; i++)
|
||||
{
|
||||
if (ValidGlyphWidths[i] >= measuredMaxW)
|
||||
{
|
||||
idealIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Try from ideal downward until we find one that fits
|
||||
for (int i = idealIdx; i >= 0; i--)
|
||||
{
|
||||
int vw = ValidGlyphWidths[i];
|
||||
int perRow = 256 / vw;
|
||||
int rows = Mathf.CeilToInt(95f / perRow);
|
||||
int totalH = rows * targetH;
|
||||
if (totalH <= MAX_TEXTURE_PAGE_HEIGHT)
|
||||
{
|
||||
bestW = vw;
|
||||
bestH = targetH;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing fits even at width=4, clamp height
|
||||
if (bestW < 0)
|
||||
{
|
||||
bestW = 4;
|
||||
int rows4 = Mathf.CeilToInt(95f / 64); // 64 per row at width 4
|
||||
bestH = Mathf.Clamp(MAX_TEXTURE_PAGE_HEIGHT / rows4, 4, 32);
|
||||
Debug.LogWarning($"PSXFontAsset: Font too large for PS1 texture page. " +
|
||||
$"Clamping to {bestW}x{bestH} cells.");
|
||||
}
|
||||
|
||||
glyphWidth = bestW;
|
||||
glyphHeight = bestH;
|
||||
|
||||
int texW = 256;
|
||||
int glyphsPerRow = texW / glyphWidth;
|
||||
int rowCount = Mathf.CeilToInt(95f / glyphsPerRow);
|
||||
int texH = rowCount * glyphHeight;
|
||||
|
||||
// Create output texture
|
||||
// Compute baseline metrics for proper vertical positioning.
|
||||
// Characters sit on a common baseline. Ascenders go up, descenders go down.
|
||||
int maxAscender = 0; // highest point above baseline (positive)
|
||||
int maxDescender = 0; // lowest point below baseline (negative)
|
||||
for (int idx = 1; idx < 95; idx++)
|
||||
{
|
||||
if (!charValid[idx]) continue;
|
||||
CharacterInfo ci = charInfos[idx];
|
||||
if (ci.maxY > maxAscender) maxAscender = ci.maxY;
|
||||
if (ci.minY < maxDescender) maxDescender = ci.minY;
|
||||
}
|
||||
int totalFontH = maxAscender - maxDescender;
|
||||
|
||||
// Vertical scale only if font exceeds cell height
|
||||
float vScale = 1f;
|
||||
int usableH = glyphHeight - 2;
|
||||
if (totalFontH > usableH)
|
||||
vScale = (float)usableH / totalFontH;
|
||||
|
||||
// NO horizontal scaling. Glyphs rendered at native width, left-aligned.
|
||||
// This makes the native advance widths match the bitmap exactly for
|
||||
// proportional rendering. Characters wider than cell get clipped (rare).
|
||||
|
||||
Debug.Log($"PSXFontAsset: Cell {glyphWidth}x{glyphHeight}, {glyphsPerRow}/row, " +
|
||||
$"{rowCount} rows, texture {texW}x{texH} " +
|
||||
$"(measured: {measuredMaxW}x{measuredMaxH}, " +
|
||||
$"ascender={maxAscender}, descender={maxDescender}, vScale={vScale:F2})");
|
||||
|
||||
// ── Step 5: Render glyphs into grid ──
|
||||
// Each glyph is LEFT-ALIGNED at native width for proportional rendering.
|
||||
// The advance widths from CharacterInfo match native glyph proportions.
|
||||
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;
|
||||
int renderedCount = 0;
|
||||
|
||||
for (int idx = 0; idx < 95; idx++)
|
||||
{
|
||||
char ch = (char)(0x20 + idx);
|
||||
if (!charValid[idx]) continue;
|
||||
CharacterInfo ci = charInfos[idx];
|
||||
|
||||
int col = idx % glyphsPerRow;
|
||||
int row = idx / glyphsPerRow;
|
||||
int cellX = col * glyphWidth;
|
||||
int cellY = row * glyphHeight; // top-down in output
|
||||
int cellY = row * glyphHeight;
|
||||
|
||||
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;
|
||||
// Use all four UV corners to handle atlas rotation.
|
||||
// Unity's atlas packer can rotate glyphs 90 degrees to pack efficiently.
|
||||
// Wide characters like 'm' and 'M' are commonly rotated.
|
||||
// Bilinear interpolation across the UV quad handles any orientation.
|
||||
Vector2 uvBL = ci.uvBottomLeft;
|
||||
Vector2 uvBR = ci.uvBottomRight;
|
||||
Vector2 uvTL = ci.uvTopLeft;
|
||||
Vector2 uvTR = ci.uvTopRight;
|
||||
|
||||
// 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++)
|
||||
// Native width (clipped to cell), scaled height
|
||||
int renderW = Mathf.Min(gw, glyphWidth);
|
||||
int renderH = Mathf.Max(1, Mathf.RoundToInt(gh * vScale));
|
||||
|
||||
// Y offset: baseline positioning
|
||||
int baselineFromTop = 1 + Mathf.RoundToInt(maxAscender * vScale);
|
||||
int glyphTopFromBaseline = Mathf.RoundToInt(ci.maxY * vScale);
|
||||
int offsetY = baselineFromTop - glyphTopFromBaseline;
|
||||
if (offsetY < 0) offsetY = 0;
|
||||
|
||||
// Include left bearing so glyph sits at correct position within
|
||||
// the advance space. Negative bearing (left overhang) clamped to 0.
|
||||
int offsetX = Mathf.Max(0, ci.minX);
|
||||
|
||||
bool anyPixel = false;
|
||||
|
||||
for (int py = 0; py < renderH && (offsetY + py) < glyphHeight; py++)
|
||||
{
|
||||
for (int px = 0; px < drawW; px++)
|
||||
for (int px = 0; px < renderW && (offsetX + px) < glyphWidth; px++)
|
||||
{
|
||||
// Map output pixel to source glyph UV
|
||||
float srcU = (px + 0.5f) / drawW;
|
||||
float srcV = (py + 0.5f) / drawH;
|
||||
// Scale to fit if glyph wider than cell, 1:1 otherwise
|
||||
float srcU = (px + 0.5f) / renderW;
|
||||
float srcV = (py + 0.5f) / renderH;
|
||||
|
||||
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);
|
||||
// Bilinear interpolation across the UV quad (handles rotation)
|
||||
// Bottom edge: lerp BL->BR by srcU
|
||||
// Top edge: lerp TL->TR by srcU
|
||||
// Then lerp bottom->top by (1-srcV) for top-down rendering
|
||||
float t = 1f - srcV; // 0=bottom, 1=top -> invert for top-down
|
||||
float u = Mathf.Lerp(
|
||||
Mathf.Lerp(uvBL.x, uvBR.x, srcU),
|
||||
Mathf.Lerp(uvTL.x, uvTR.x, srcU), t);
|
||||
float v = Mathf.Lerp(
|
||||
Mathf.Lerp(uvBL.y, uvBR.y, srcU),
|
||||
Mathf.Lerp(uvTL.y, uvTR.y, srcU), t);
|
||||
|
||||
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;
|
||||
if (sc.a <= 0.3f) 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);
|
||||
int outX = cellX + offsetX + px;
|
||||
int outY = texH - 1 - (cellY + offsetY + py);
|
||||
if (outX >= 0 && outX < texW && outY >= 0 && outY < texH)
|
||||
{
|
||||
bmp.SetPixel(outX, outY, Color.white);
|
||||
anyPixel = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (anyPixel) renderedCount++;
|
||||
}
|
||||
|
||||
bmp.Apply();
|
||||
|
||||
// Save as asset
|
||||
if (renderedCount == 0)
|
||||
{
|
||||
Debug.LogError("PSXFontAsset: Generated bitmap is empty.");
|
||||
DestroyImmediate(bmp);
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"PSXFontAsset: Rendered {renderedCount}/95 glyphs.");
|
||||
|
||||
// Store advance widths from the same CharacterInfo used for rendering.
|
||||
// This guarantees advances match the bitmap glyphs exactly.
|
||||
storedAdvanceWidths = new byte[96];
|
||||
for (int idx = 0; idx < 96; idx++)
|
||||
{
|
||||
if (idx < 95 && charValid[idx])
|
||||
{
|
||||
CharacterInfo ci = charInfos[idx];
|
||||
storedAdvanceWidths[idx] = (byte)Mathf.Clamp(Mathf.CeilToInt(ci.advance), 1, 255);
|
||||
}
|
||||
else
|
||||
{
|
||||
storedAdvanceWidths[idx] = (byte)glyphWidth; // fallback
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 6: Save ──
|
||||
string path = AssetDatabase.GetAssetPath(this);
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
@@ -241,12 +404,10 @@ namespace SplashEdit.RuntimeCode
|
||||
|
||||
string dir = System.IO.Path.GetDirectoryName(path);
|
||||
string texPath = dir + "/" + name + "_bitmap.png";
|
||||
byte[] pngData = bmp.EncodeToPNG();
|
||||
System.IO.File.WriteAllBytes(texPath, pngData);
|
||||
System.IO.File.WriteAllBytes(texPath, bmp.EncodeToPNG());
|
||||
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)
|
||||
{
|
||||
@@ -263,23 +424,16 @@ namespace SplashEdit.RuntimeCode
|
||||
fontTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(texPath);
|
||||
EditorUtility.SetDirty(this);
|
||||
AssetDatabase.SaveAssets();
|
||||
Debug.Log($"PSXFontAsset: Generated bitmap {texW}x{texH} at {texPath}");
|
||||
Debug.Log($"PSXFontAsset: Saved {texW}x{texH} bitmap 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 texW = 256;
|
||||
int texH = TextureHeight;
|
||||
// 4bpp: 2 pixels per byte → 128 bytes per row
|
||||
int bytesPerRow = texW / 2;
|
||||
byte[] result = new byte[bytesPerRow * texH];
|
||||
|
||||
@@ -291,9 +445,8 @@ namespace SplashEdit.RuntimeCode
|
||||
{
|
||||
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)
|
||||
byte lo = SamplePixel(pixels, srcW, srcH, x, y);
|
||||
byte hi = SamplePixel(pixels, srcW, srcH, x + 1, y);
|
||||
result[y * bytesPerRow + x / 2] = (byte)(lo | (hi << 4));
|
||||
}
|
||||
}
|
||||
@@ -301,23 +454,13 @@ namespace SplashEdit.RuntimeCode
|
||||
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)
|
||||
private byte SamplePixel(Color[] pixels, int srcW, int srcH, int x, int y)
|
||||
{
|
||||
if (x >= srcW || y >= srcH) return 0;
|
||||
// Source texture is bottom-up (Unity convention), output is top-down (PS1)
|
||||
int srcY = srcH - 1 - y;
|
||||
int srcY = srcH - 1 - y; // top-down (PS1) to bottom-up (Unity)
|
||||
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;
|
||||
return c.a > 0.5f ? (byte)1 : (byte)0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -634,7 +634,7 @@ namespace SplashEdit.RuntimeCode
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// UI canvas + font data (version 13)
|
||||
// Font descriptors: 16 bytes each (before canvas data)
|
||||
// Font descriptors: 112 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
|
||||
@@ -645,7 +645,7 @@ namespace SplashEdit.RuntimeCode
|
||||
AlignToFourBytes(writer);
|
||||
long uiTableStart = writer.BaseStream.Position;
|
||||
|
||||
// ── Font descriptors (16 bytes each) ──
|
||||
// ── Font descriptors (112 bytes each) ──
|
||||
// Layout: glyphW(1) glyphH(1) vramX(2) vramY(2) textureH(2)
|
||||
// dataOffset(4) dataSize(4)
|
||||
List<long> fontDataOffsetPositions = new List<long>();
|
||||
@@ -661,6 +661,11 @@ namespace SplashEdit.RuntimeCode
|
||||
fontDataOffsetPositions.Add(writer.BaseStream.Position);
|
||||
writer.Write((uint)0); // [8-11] dataOffset placeholder
|
||||
writer.Write((uint)(font.PixelData?.Length ?? 0)); // [12-15] dataSize
|
||||
// [16-111] per-character advance widths for proportional rendering
|
||||
if (font.AdvanceWidths != null && font.AdvanceWidths.Length >= 96)
|
||||
writer.Write(font.AdvanceWidths, 0, 96);
|
||||
else
|
||||
writer.Write(new byte[96]); // zero-fill if missing
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace SplashEdit.RuntimeCode
|
||||
/// </summary>
|
||||
public static class PSXUIExporter
|
||||
{
|
||||
/// <summary>
|
||||
/// <summary>
|
||||
/// Collect all PSXCanvas components and their child UI elements,
|
||||
/// converting RectTransform coordinates to PS1 pixel space.
|
||||
/// Also collects and deduplicates custom fonts.
|
||||
@@ -49,28 +49,60 @@ namespace SplashEdit.RuntimeCode
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// Build font data with VRAM positions.
|
||||
// Each font gets its own texture page to avoid V-coordinate overflow.
|
||||
// Font textures go at x=960:
|
||||
// Font 1: y=0 (page 15,0) - 256px available
|
||||
// Font 2: y=256 (page 15,1) - 208px available (system font at y=464)
|
||||
// Font 3: not supported (would need different VRAM column)
|
||||
// System font: (960, 464) in page (15,1), occupies y=464-511.
|
||||
List<PSXFontData> fontDataList = new List<PSXFontData>();
|
||||
ushort fontVramY = 0; // start from top of VRAM at x=960
|
||||
ushort[] fontPageStarts = { 0, 256 }; // one per texture page
|
||||
int fontPageIndex = 0;
|
||||
|
||||
foreach (PSXFontAsset fa in uniqueFonts)
|
||||
{
|
||||
byte[] pixelData = fa.ConvertTo4BPP();
|
||||
if (pixelData == null) continue;
|
||||
|
||||
// Read advance widths directly from the font asset.
|
||||
// These were computed during bitmap generation from the exact same
|
||||
// CharacterInfo used to render the glyphs - guaranteed to match.
|
||||
byte[] advances = fa.AdvanceWidths;
|
||||
if (advances == null || advances.Length < 96)
|
||||
{
|
||||
Debug.LogWarning($"PSXUIExporter: Font '{fa.name}' has no stored advance widths. Using cell width as fallback.");
|
||||
advances = new byte[96];
|
||||
for (int i = 0; i < 96; i++) advances[i] = (byte)fa.GlyphWidth;
|
||||
}
|
||||
|
||||
ushort texH = (ushort)fa.TextureHeight;
|
||||
|
||||
if (fontPageIndex >= fontPageStarts.Length)
|
||||
{
|
||||
Debug.LogError($"PSXUIExporter: Max 2 custom fonts supported (need separate texture pages). Skipping '{fa.name}'.");
|
||||
continue;
|
||||
}
|
||||
|
||||
ushort vramY = fontPageStarts[fontPageIndex];
|
||||
int maxHeight = (fontPageIndex == 1) ? 208 : 256; // page 1 shares with system font
|
||||
if (texH > maxHeight)
|
||||
{
|
||||
Debug.LogWarning($"PSXUIExporter: Font '{fa.name}' texture ({texH}px) exceeds page limit ({maxHeight}px). May be clipped.");
|
||||
}
|
||||
|
||||
fontDataList.Add(new PSXFontData
|
||||
{
|
||||
Source = fa,
|
||||
GlyphWidth = (byte)fa.GlyphWidth,
|
||||
GlyphHeight = (byte)fa.GlyphHeight,
|
||||
VramX = 960, // same column as system font (64 VRAM hwords for 256px 4bpp)
|
||||
VramY = fontVramY,
|
||||
VramX = 960,
|
||||
VramY = vramY,
|
||||
TextureHeight = texH,
|
||||
PixelData = pixelData
|
||||
PixelData = pixelData,
|
||||
AdvanceWidths = advances
|
||||
});
|
||||
fontVramY += texH;
|
||||
fontPageIndex++;
|
||||
}
|
||||
fonts = fontDataList.ToArray();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user