This commit is contained in:
Jan Racek
2026-03-27 21:29:24 +01:00
parent 45a552be5a
commit 13ed569eaf
85 changed files with 1953 additions and 2470 deletions

View File

@@ -1,5 +1,8 @@
namespace SplashEdit.RuntimeCode
{
// I tried to make this and now I'm scared to delete this.
/// <summary>
/// Implemented by MonoBehaviours that participate in the PSX scene export pipeline.
/// Each exportable object converts its Unity representation into PSX-ready data.

View File

@@ -137,53 +137,55 @@ namespace SplashEdit.RuntimeCode
private class Node
{
public Vector3 Point;
public int Index;
public Node Left, Right;
}
private Node root;
private List<Vector3> points;
public KDTree(List<Vector3> points)
{
this.points = points;
root = Build(points, 0);
var indexed = new List<(Vector3 point, int index)>();
for (int i = 0; i < points.Count; i++)
indexed.Add((points[i], i));
root = Build(indexed, 0);
}
private Node Build(List<Vector3> points, int depth)
private Node Build(List<(Vector3 point, int index)> items, int depth)
{
if (points.Count == 0) return null;
if (items.Count == 0) return null;
int axis = depth % 3;
points.Sort((a, b) => a[axis].CompareTo(b[axis]));
int median = points.Count / 2;
items.Sort((a, b) => a.point[axis].CompareTo(b.point[axis]));
int median = items.Count / 2;
return new Node
{
Point = points[median],
Left = Build(points.Take(median).ToList(), depth + 1),
Right = Build(points.Skip(median + 1).ToList(), depth + 1)
Point = items[median].point,
Index = items[median].index,
Left = Build(items.Take(median).ToList(), depth + 1),
Right = Build(items.Skip(median + 1).ToList(), depth + 1)
};
}
public int FindNearestIndex(Vector3 target)
{
Vector3 nearest = FindNearest(root, target, 0, root.Point);
return points.IndexOf(nearest);
return FindNearest(root, target, 0, root).Index;
}
private Vector3 FindNearest(Node node, Vector3 target, int depth, Vector3 best)
private Node FindNearest(Node node, Vector3 target, int depth, Node best)
{
if (node == null) return best;
if (Vector3.SqrMagnitude(target - node.Point) < Vector3.SqrMagnitude(target - best))
best = node.Point;
if (Vector3.SqrMagnitude(target - node.Point) < Vector3.SqrMagnitude(target - best.Point))
best = node;
int axis = depth % 3;
Node first = target[axis] < node.Point[axis] ? node.Left : node.Right;
Node second = first == node.Left ? node.Right : node.Left;
best = FindNearest(first, target, depth + 1, best);
if (Mathf.Pow(target[axis] - node.Point[axis], 2) < Vector3.SqrMagnitude(target - best))
if (Mathf.Pow(target[axis] - node.Point[axis], 2) < Vector3.SqrMagnitude(target - best.Point))
best = FindNearest(second, target, depth + 1, best);
return best;

View File

@@ -2,6 +2,7 @@ using UnityEngine;
namespace SplashEdit.RuntimeCode
{
[Icon("Packages/net.psxsplash.splashedit/Icons/LuaFile.png")]
public class LuaFile : ScriptableObject
{
[SerializeField] private string luaScript;

View File

@@ -4,8 +4,6 @@ namespace SplashEdit.RuntimeCode
{
/// <summary>
/// Pre-converted audio clip data ready for splashpack serialization.
/// Populated by the Editor (PSXSceneExporter) so Runtime code never
/// touches PSXAudioConverter.
/// </summary>
public struct AudioClipExport
{
@@ -18,9 +16,10 @@ namespace SplashEdit.RuntimeCode
/// <summary>
/// Attach to a GameObject to include an audio clip in the PS1 build.
/// At export time, the AudioClip is converted to SPU ADPCM and packed
/// into the splashpack binary. Use Audio.Play(clipIndex) from Lua.
/// into the splashpack for runtime loading.
/// </summary>
[AddComponentMenu("PSX/Audio Clip")]
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXAudioClip.png")]
public class PSXAudioClip : MonoBehaviour
{
[Tooltip("Name used to identify this clip in Lua (Audio.Play(\"name\"))." )]

View File

@@ -17,6 +17,7 @@ namespace SplashEdit.RuntimeCode
[DisallowMultipleComponent]
[ExecuteAlways]
[AddComponentMenu("PSX/UI/PSX Canvas")]
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXCanvas.png")]
public class PSXCanvas : MonoBehaviour
{
[Tooltip("Name used to reference this canvas from Lua (max 24 chars). Must be unique per scene.")]

View File

@@ -10,6 +10,7 @@ namespace SplashEdit.RuntimeCode
/// all PSXCutsceneClip assets via Resources.FindObjectsOfTypeAll.
/// </summary>
[CreateAssetMenu(fileName = "NewCutscene", menuName = "PSX/Cutscene Clip", order = 100)]
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXCutsceneClip.png")]
public class PSXCutsceneClip : ScriptableObject
{
[Tooltip("Name used to reference this cutscene from Lua (max 24 chars). Must be unique per scene.")]

View File

@@ -5,6 +5,7 @@ namespace SplashEdit.RuntimeCode
{
[CreateAssetMenu(fileName = "PSXData", menuName = "PSXSplash/PS1 Project Data")]
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXData.png")]
public class PSXData : ScriptableObject
{

View File

@@ -8,6 +8,7 @@ using UnityEditor;
namespace SplashEdit.RuntimeCode
{
[CreateAssetMenu(fileName = "New PSXFont", menuName = "PSX/Font Asset")]
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXFontAsset.png")]
public class PSXFontAsset : ScriptableObject
{
[Header("Source - Option A: TrueType/OTF Font")]
@@ -154,7 +155,6 @@ namespace SplashEdit.RuntimeCode
if (validCount > 0)
{
workingSize = 0;
Debug.Log("PSXFontAsset: Using font's native size for character info.");
}
}
@@ -273,11 +273,6 @@ namespace SplashEdit.RuntimeCode
// 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.
@@ -376,8 +371,6 @@ namespace SplashEdit.RuntimeCode
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];
@@ -424,7 +417,6 @@ namespace SplashEdit.RuntimeCode
fontTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(texPath);
EditorUtility.SetDirty(this);
AssetDatabase.SaveAssets();
Debug.Log($"PSXFontAsset: Saved {texW}x{texH} bitmap at {texPath}");
}
#endif

View File

@@ -8,6 +8,7 @@ namespace SplashEdit.RuntimeCode
/// the onInteract Lua event fires.
/// </summary>
[RequireComponent(typeof(PSXObjectExporter))]
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXInteractable.png")]
public class PSXInteractable : MonoBehaviour
{
[Header("Interaction Settings")]

View File

@@ -76,6 +76,8 @@ namespace SplashEdit.RuntimeCode
finalColor += lightContribution;
}
// Clamp to 0.8 to leave headroom for PS1 2x color blending mode,
// which doubles vertex colors. Without this cap, bright areas would clip.
finalColor.r = Mathf.Clamp(finalColor.r, 0.0f, 0.8f);
finalColor.g = Mathf.Clamp(finalColor.g, 0.0f, 0.8f);
finalColor.b = Mathf.Clamp(finalColor.b, 0.0f, 0.8f);

View File

@@ -83,9 +83,6 @@ namespace SplashEdit.RuntimeCode
}
/// <summary>
/// Creates a PSXMesh from a Unity Mesh by converting its vertices, normals, UVs, and applying shading.
/// </summary>
/// <summary>
/// Creates a PSXMesh from a Unity Renderer by extracting its mesh and materials.
/// </summary>
@@ -203,12 +200,12 @@ namespace SplashEdit.RuntimeCode
/// Converts a Unity vertex into a PSXVertex by applying fixed-point conversion, shading, and UV mapping.
/// </summary>
/// <param name="vertex">The position of the vertex.</param>
/// <param name="GTEScaling">World-to-GTE scaling factor.</param>
/// <param name="normal">The normal vector at the vertex.</param>
/// <param name="uv">Texture coordinates for the vertex.</param>
/// <param name="lightDir">The light direction used for shading calculations.</param>
/// <param name="lightColor">The color of the light affecting the vertex.</param>
/// <param name="textureWidth">Width of the texture for UV scaling.</param>
/// <param name="textureHeight">Height of the texture for UV scaling.</param>
/// <param name="color">Pre-computed vertex color from lighting.</param>
/// <returns>A PSXVertex with converted coordinates, normals, UVs, and color.</returns>
private static PSXVertex ConvertToPSXVertex(Vector3 vertex, float GTEScaling, Vector3 normal, Vector2 uv, int? textureWidth, int? textureHeight, Color color)
{

View File

@@ -14,6 +14,7 @@ namespace SplashEdit.RuntimeCode
[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXObjectExporter.png")]
public class PSXObjectExporter : MonoBehaviour, IPSXExportable
{
public LuaFile LuaFile => luaFile;

View File

@@ -4,6 +4,7 @@ using UnityEngine.Serialization;
namespace SplashEdit.RuntimeCode
{
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXPlayer.png")]
public class PSXPlayer : MonoBehaviour
{
[Header("Player Dimensions")]

View File

@@ -11,6 +11,7 @@ namespace SplashEdit.RuntimeCode
/// This is independent of the navigation portal system (PSXNavRegion).
/// </summary>
[ExecuteInEditMode]
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXPortalLink.png")]
public class PSXPortalLink : MonoBehaviour
{
[Tooltip("First room connected by this portal.")]

View File

@@ -13,6 +13,7 @@ namespace SplashEdit.RuntimeCode
/// This is independent of the navregion/portal system used for navigation.
/// </summary>
[ExecuteInEditMode]
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXRoom.png")]
public class PSXRoom : MonoBehaviour
{
[Tooltip("Optional display name for this room (used in editor gizmos).")]

View File

@@ -16,6 +16,7 @@ namespace SplashEdit.RuntimeCode
}
[ExecuteInEditMode]
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXSceneExporter.png")]
public class PSXSceneExporter : MonoBehaviour
{
/// <summary>

View File

@@ -7,7 +7,7 @@ using UnityEngine;
namespace SplashEdit.RuntimeCode
{
/// <summary>
/// Pure binary serializer for the splashpack v8 format.
/// Pure binary serializer for the splashpack v16 format.
/// All I/O extracted from PSXSceneExporter so the MonoBehaviour stays thin.
/// </summary>
public static class PSXSceneWriter
@@ -72,7 +72,7 @@ namespace SplashEdit.RuntimeCode
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Serialize the scene to a splashpack v8 binary file.
/// Serialize the scene to a splashpack v16 binary file.
/// </summary>
/// <param name="path">Absolute file path to write.</param>
/// <param name="scene">Pre-built scene data.</param>
@@ -148,13 +148,13 @@ namespace SplashEdit.RuntimeCode
writer.Write((ushort)clutCount);
writer.Write((ushort)colliderCount);
writer.Write((ushort)scene.interactables.Length);
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(scene.playerPos.x, gte));
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(-scene.playerPos.y, gte));
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(scene.playerPos.z, gte));
writer.Write(PSXTrig.ConvertCoordinateToPSX(scene.playerPos.x, gte));
writer.Write(PSXTrig.ConvertCoordinateToPSX(-scene.playerPos.y, gte));
writer.Write(PSXTrig.ConvertCoordinateToPSX(scene.playerPos.z, gte));
writer.Write((ushort)PSXTrig.ConvertToFixed12(scene.playerRot.eulerAngles.x * Mathf.Deg2Rad));
writer.Write((ushort)PSXTrig.ConvertToFixed12(scene.playerRot.eulerAngles.y * Mathf.Deg2Rad));
writer.Write((ushort)PSXTrig.ConvertToFixed12(scene.playerRot.eulerAngles.z * Mathf.Deg2Rad));
writer.Write(PSXTrig.ConvertToFixed12(scene.playerRot.eulerAngles.x * Mathf.Deg2Rad));
writer.Write(PSXTrig.ConvertToFixed12(scene.playerRot.eulerAngles.y * Mathf.Deg2Rad));
writer.Write(PSXTrig.ConvertToFixed12(scene.playerRot.eulerAngles.z * Mathf.Deg2Rad));
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(scene.playerHeight, gte));
@@ -475,6 +475,7 @@ namespace SplashEdit.RuntimeCode
writer.Write((ushort)0); // padding
// Sentinel tpage = 0xFFFF marks untextured
// haha funny word. Sentinel, sentinel, sentinel. I could keep saying it forever.
writer.Write((ushort)0xFFFF);
writer.Write((ushort)0);
writer.Write((ushort)0);
@@ -828,6 +829,9 @@ namespace SplashEdit.RuntimeCode
// ══════════════════════════════════════════════════════
// DEAD ZONE — pixel/audio bulk data (freed after VRAM/SPU upload)
// Everything written after this point is not needed at runtime.
// You may be asking why we don't just put pixel/audio data in separate files
// or why don't we put this data at the end of the file to begin with. The answer is
// Very simple and I'm going to tell it to you right now... OH GOD I FORGOT TO TURN OFF THE STOVE (runs away)
// ══════════════════════════════════════════════════════
AlignToFourBytes(writer);
long pixelDataStart = writer.BaseStream.Position;

View File

@@ -252,9 +252,9 @@ namespace SplashEdit.RuntimeCode
if (BitDepth == PSXBPP.TEX_16BIT)
{
for (int y = 0; y < Width; y++)
for (int y = 0; y < Height; y++)
{
for (int x = 0; x < Height; x++)
for (int x = 0; x < Width; x++)
{
tex.SetPixel(x, Height - 1 - y, ImageData[x, y].GetUnityColor());
}

View File

@@ -1,8 +1,5 @@
namespace SplashEdit.RuntimeCode
{
/// <summary>
/// Cutscene track types. Must match the C++ TrackType enum in cutscene.hh.
/// </summary>
public enum PSXTrackType : byte
{
CameraPosition = 0,
@@ -10,7 +7,6 @@ namespace SplashEdit.RuntimeCode
ObjectPosition = 2,
ObjectRotationY = 3,
ObjectActive = 4,
// UI track types (v13+)
UICanvasVisible = 5,
UIElementVisible = 6,
UIProgress = 7,

View File

@@ -2,6 +2,7 @@ using UnityEngine;
namespace SplashEdit.RuntimeCode
{
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXTriggerBox.png")]
public class PSXTriggerBox : MonoBehaviour
{
[SerializeField] private Vector3 size = Vector3.one;

View File

@@ -10,6 +10,7 @@ namespace SplashEdit.RuntimeCode
[RequireComponent(typeof(RectTransform))]
[DisallowMultipleComponent]
[AddComponentMenu("PSX/UI/PSX UI Box")]
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXUIBox.png")]
public class PSXUIBox : MonoBehaviour
{
[Tooltip("Name used to reference this element from Lua (max 24 chars).")]

View File

@@ -10,6 +10,7 @@ namespace SplashEdit.RuntimeCode
[RequireComponent(typeof(RectTransform))]
[DisallowMultipleComponent]
[AddComponentMenu("PSX/UI/PSX UI Image")]
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXUIImage.png")]
public class PSXUIImage : MonoBehaviour
{
[Tooltip("Name used to reference this element from Lua (max 24 chars).")]

View File

@@ -10,6 +10,7 @@ namespace SplashEdit.RuntimeCode
[RequireComponent(typeof(RectTransform))]
[DisallowMultipleComponent]
[AddComponentMenu("PSX/UI/PSX UI Progress Bar")]
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXUIProgressBar.png")]
public class PSXUIProgressBar : MonoBehaviour
{
[Tooltip("Name used to reference this element from Lua (max 24 chars).")]

View File

@@ -10,6 +10,7 @@ namespace SplashEdit.RuntimeCode
[RequireComponent(typeof(RectTransform))]
[DisallowMultipleComponent]
[AddComponentMenu("PSX/UI/PSX UI Text")]
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXUIText.png")]
public class PSXUIText : MonoBehaviour
{
[Tooltip("Name used to reference this element from Lua (max 24 chars).")]

View File

@@ -222,16 +222,16 @@ namespace SplashEdit.RuntimeCode
{
if (bvh == null) return 0;
// BVH nodes: each node has AABB (24 bytes) + child/tri info (8 bytes) = 32 bytes
// Triangle refs: 2 bytes each (uint16)
return (long)bvh.NodeCount * 32 + (long)bvh.TriangleRefCount * 2;
// Triangle refs: 4 bytes each (uint32)
return (long)bvh.NodeCount * 32 + (long)bvh.TriangleRefCount * 4;
}
private static long EstimateCollisionSize(PSXCollisionExporter collision)
{
if (collision == null || collision.MeshCount == 0) return 0;
// Each collision mesh header: AABB + triangle count (28 bytes)
// Each collision triangle: 3 vertices * 6 bytes = 18 bytes + normal 6 bytes = 24 bytes
return (long)collision.MeshCount * 28 + (long)collision.TriangleCount * 24;
// Each collision mesh header: AABB (24) + tri count (2) + flags (2) + offset (4) = 32 bytes
// Each collision triangle: 3 verts * 12 bytes + normal 12 bytes + flags 4 bytes = 52 bytes
return (long)collision.MeshCount * 32 + (long)collision.TriangleCount * 52;
}
private static long EstimateNavRegionSize(PSXNavRegionBuilder nav)

View File

@@ -321,7 +321,7 @@ namespace SplashEdit.RuntimeCode
// Iterate over possible CLUT positions in VRAM.
for (ushort x = 0; x < VramWidth; x += 16)
{
for (ushort y = 0; y <= VramHeight; y++)
for (ushort y = 0; y < VramHeight; y++)
{
var candidate = new Rect(x, y, clutWidth, clutHeight);
if (IsPlacementValid(candidate))

View File

@@ -331,6 +331,30 @@ namespace SplashEdit.RuntimeCode
{
private static int MaxTextureSize => 256;
/// <summary>
/// If a directory contains exactly one subdirectory (common after archive extraction),
/// flatten its contents up one level and remove the nested directory.
/// </summary>
public static void FixNestedDirectory(string dir)
{
var subdirs = System.IO.Directory.GetDirectories(dir);
if (subdirs.Length == 1)
{
string nested = subdirs[0];
foreach (string file in System.IO.Directory.GetFiles(nested))
{
string dest = System.IO.Path.Combine(dir, System.IO.Path.GetFileName(file));
if (!System.IO.File.Exists(dest)) System.IO.File.Move(file, dest);
}
foreach (string sub in System.IO.Directory.GetDirectories(nested))
{
string dest = System.IO.Path.Combine(dir, System.IO.Path.GetFileName(sub));
if (!System.IO.Directory.Exists(dest)) System.IO.Directory.Move(sub, dest);
}
try { System.IO.Directory.Delete(nested, true); } catch { }
}
}
public static (Rect, Rect) BufferForResolution(Vector2 selectedResolution, bool verticalLayout, Vector2 offset = default)
{
if (offset == default)