Revamped collision system

This commit is contained in:
Jan Racek
2026-03-27 16:39:42 +01:00
parent d29ef569b3
commit 1c48b8b425
16 changed files with 318 additions and 136 deletions

View File

@@ -15,7 +15,6 @@ namespace SplashEdit.RuntimeCode
Solid = 0x01,
Slope = 0x02,
Stairs = 0x04,
Trigger = 0x08,
NoWalk = 0x10,
}
@@ -84,8 +83,8 @@ namespace SplashEdit.RuntimeCode
foreach (var exporter in exporters)
{
// Dynamic objects are handled by the runtime collision system, skip them
if (!exporter.StaticCollider && exporter.CollisionType != PSXCollisionType.None)
// Dynamic objects use runtime AABB colliders, skip them
if (exporter.CollisionType == PSXCollisionType.Dynamic)
continue;
PSXCollisionType effectiveType = exporter.CollisionType;
@@ -94,7 +93,7 @@ namespace SplashEdit.RuntimeCode
{
if (autoIncludeSolid)
{
effectiveType = PSXCollisionType.Solid;
effectiveType = PSXCollisionType.Static;
autoIncluded++;
}
else
@@ -103,11 +102,8 @@ namespace SplashEdit.RuntimeCode
}
}
// Get the collision mesh (custom or render mesh)
MeshFilter mf = exporter.GetComponent<MeshFilter>();
Mesh collisionMesh = exporter.CustomCollisionMesh != null
? exporter.CustomCollisionMesh
: mf?.sharedMesh;
Mesh collisionMesh = mf?.sharedMesh;
if (collisionMesh == null)
continue;
@@ -133,37 +129,25 @@ namespace SplashEdit.RuntimeCode
// Determine surface flags
byte flags = 0;
if (effectiveType == PSXCollisionType.Trigger)
// Floor-like: normal.y > cosWalkable
float dotUp = normal.y;
if (dotUp > cosWalkable)
{
flags = (byte)PSXSurfaceFlag.Trigger;
flags = (byte)PSXSurfaceFlag.Solid;
if (dotUp < 0.95f && dotUp > cosWalkable)
{
flags |= (byte)PSXSurfaceFlag.Stairs;
}
}
else if (dotUp > 0.0f)
{
flags = (byte)(PSXSurfaceFlag.Solid | PSXSurfaceFlag.Slope);
}
else
{
// Floor-like: normal.y > cosWalkable
// Note: Unity Y is up; PS1 Y is down. We export in Unity space
// and convert to PS1 space during WriteToBinary.
float dotUp = normal.y;
if (dotUp > cosWalkable)
{
flags = (byte)PSXSurfaceFlag.Solid;
// Check if stairs (tagged on exporter or steep-ish)
if (dotUp < 0.95f && dotUp > cosWalkable)
{
flags |= (byte)PSXSurfaceFlag.Stairs;
}
}
else if (dotUp > 0.0f)
{
// Slope too steep to walk on
flags = (byte)(PSXSurfaceFlag.Solid | PSXSurfaceFlag.Slope);
}
else
{
// Wall or ceiling
flags = (byte)PSXSurfaceFlag.Solid;
}
flags = (byte)PSXSurfaceFlag.Solid;
}
_allTriangles.Add(new CollisionTriExport

View File

@@ -187,23 +187,48 @@ namespace SplashEdit.RuntimeCode
}
}
// ── Font pixel data (written BEFORE the UI table, alongside atlas/CLUT data) ──
// The C++ parser expects canvas descriptors immediately after font descriptors
// (font pixel data is at absolute offsets, not inline). Write pixel data here
// so it doesn't sit between font descriptors and canvas descriptors.
List<long> fontDataOffsetPositions = new List<long>();
List<long> fontPixelDataPositions = new List<long>();
if (fonts != null)
{
for (int fi = 0; fi < fonts.Length; fi++)
{
var font = fonts[fi];
if (font.PixelData == null || font.PixelData.Length == 0)
{
fontPixelDataPositions.Add(0);
continue;
}
AlignToFourBytes(writer);
long dataPos = writer.BaseStream.Position;
writer.Write(font.PixelData);
fontPixelDataPositions.Add(dataPos);
}
}
// ── UI table (same format as splashpack UI section) ──
AlignToFourBytes(writer);
long uiTableStart = writer.BaseStream.Position;
// ── Font descriptors (112 bytes each) ──
List<long> fontDataOffsetPositions = new List<long>();
if (fonts != null)
{
foreach (var font in fonts)
for (int fi = 0; fi < fonts.Length; fi++)
{
var font = fonts[fi];
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
// dataOffset: use the pre-written pixel data position
long pixPos = fontPixelDataPositions[fi];
writer.Write((uint)pixPos); // [8-11] dataOffset (0 if no data)
writer.Write((uint)(font.PixelData?.Length ?? 0)); // [12-15] dataSize
if (font.AdvanceWidths != null && font.AdvanceWidths.Length >= 96)
writer.Write(font.AdvanceWidths, 0, 96);
@@ -212,26 +237,13 @@ namespace SplashEdit.RuntimeCode
}
}
// ── Font pixel data ──
if (fonts != null)
{
for (int fi = 0; fi < fonts.Length; fi++)
{
var font = fonts[fi];
if (font.PixelData == null || font.PixelData.Length == 0) continue;
AlignToFourBytes(writer);
long dataPos = writer.BaseStream.Position;
writer.Write(font.PixelData);
long curPos = writer.BaseStream.Position;
writer.Seek((int)fontDataOffsetPositions[fi], SeekOrigin.Begin);
writer.Write((uint)dataPos);
writer.Seek((int)curPos, SeekOrigin.Begin);
}
}
// Canvas descriptors now follow immediately after font descriptors
// (no font pixel data in between — it was written above).
// ── Canvas descriptor (12 bytes) ──
// Must align here: the C++ parser aligns fontDataEnd to 4 bytes
// when skipping past font pixel data to find the canvas descriptor.
AlignToFourBytes(writer);
var elements = canvas.Elements ?? new PSXUIElementData[0];
string cvName = canvas.Name ?? "loading";
if (cvName.Length > 24) cvName = cvName.Substring(0, 24);

View File

@@ -312,6 +312,8 @@ namespace SplashEdit.RuntimeCode
{
foreach (var exporter in exporters)
{
if (exporter.CollisionType == PSXCollisionType.Dynamic)
continue;
MeshFilter mf = exporter.GetComponent<MeshFilter>();
Mesh mesh = mf?.sharedMesh;

View File

@@ -8,9 +8,8 @@ namespace SplashEdit.RuntimeCode
public enum PSXCollisionType
{
None = 0,
Solid = 1,
Trigger = 2,
Platform = 3
Static = 1,
Dynamic = 2
}
[RequireComponent(typeof(Renderer))]
@@ -29,22 +28,11 @@ namespace SplashEdit.RuntimeCode
[SerializeField] private PSXBPP bitDepth = PSXBPP.TEX_8BIT;
[SerializeField] private LuaFile luaFile;
[FormerlySerializedAs("collisionType")]
[SerializeField] private PSXCollisionType collisionType = PSXCollisionType.None;
[SerializeField] private bool staticCollider = true;
[SerializeField] private bool exportCollisionMesh = false;
[SerializeField] private Mesh customCollisionMesh;
[Range(1, 8)]
[SerializeField] private int collisionLayer = 1;
[SerializeField] private bool generateNavigation = false;
public PSXBPP BitDepth => bitDepth;
public PSXCollisionType CollisionType => collisionType;
public bool StaticCollider => staticCollider;
public bool ExportCollisionMesh => exportCollisionMesh;
public Mesh CustomCollisionMesh => customCollisionMesh;
public int CollisionLayer => collisionLayer;
public bool GenerateNavigation => generateNavigation;
private readonly Dictionary<(int, PSXBPP), PSXTexture2D> cache = new();

View File

@@ -58,6 +58,7 @@ namespace SplashEdit.RuntimeCode
// Component arrays
private PSXInteractable[] _interactables;
private PSXAudioSource[] _audioSources;
private PSXTriggerBox[] _triggerBoxes;
// Phase 3+4: World collision and nav regions
private PSXCollisionExporter _collisionExporter;
@@ -120,6 +121,7 @@ namespace SplashEdit.RuntimeCode
// Collect components
_interactables = FindObjectsByType<PSXInteractable>(FindObjectsSortMode.None);
_audioSources = FindObjectsByType<PSXAudioSource>(FindObjectsSortMode.None);
_triggerBoxes = FindObjectsByType<PSXTriggerBox>(FindObjectsSortMode.None);
// Collect UI image textures for VRAM packing alongside 3D textures
PSXUIImage[] uiImages = FindObjectsByType<PSXUIImage>(FindObjectsSortMode.None);
@@ -164,7 +166,7 @@ namespace SplashEdit.RuntimeCode
_collisionExporter = new PSXCollisionExporter();
_collisionExporter.Build(_exporters, GTEScaling);
if (_collisionExporter.MeshCount == 0)
Debug.LogWarning("No collision meshes! Set CollisionType=Solid on your floor/wall objects.");
Debug.LogWarning("No collision meshes! Set CollisionType=Static on your floor/wall objects.");
// Phase 4+5: Room volumes are needed by BOTH the nav region builder
// (for spatial room assignment) and the room builder (for triangle assignment).
@@ -295,6 +297,7 @@ namespace SplashEdit.RuntimeCode
audioSources = _audioSources,
canvases = _canvases,
fonts = _fonts,
triggerBoxes = _triggerBoxes,
};
PSXSceneWriter.Write(path, in scene, (msg, type) =>

View File

@@ -39,6 +39,9 @@ namespace SplashEdit.RuntimeCode
// Custom fonts (v13, embedded in UI block)
public PSXFontData[] fonts;
// Trigger boxes (v16)
public PSXTriggerBox[] triggerBoxes;
// Player
public Vector3 playerPos;
public Quaternion playerRot;
@@ -105,32 +108,40 @@ namespace SplashEdit.RuntimeCode
}
if (scene.sceneLuaFile != null && !luaFiles.Contains(scene.sceneLuaFile))
luaFiles.Add(scene.sceneLuaFile);
// Trigger box Lua files
if (scene.triggerBoxes != null)
{
foreach (var tb in scene.triggerBoxes)
{
if (tb.LuaFile != null && !luaFiles.Contains(tb.LuaFile))
luaFiles.Add(tb.LuaFile);
}
}
using (BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)))
{
int colliderCount = 0;
foreach (var e in scene.exporters)
{
if (e.CollisionType == PSXCollisionType.None || e.StaticCollider)
continue;
Mesh cm = e.CustomCollisionMesh != null
? e.CustomCollisionMesh
: e.GetComponent<MeshFilter>()?.sharedMesh;
if (cm != null)
if (e.CollisionType != PSXCollisionType.Dynamic) continue;
MeshFilter mf = e.GetComponent<MeshFilter>();
if (mf?.sharedMesh != null)
colliderCount++;
}
int triggerBoxCount = scene.triggerBoxes?.Length ?? 0;
// Build exporter index lookup for components
Dictionary<PSXObjectExporter, int> exporterIndex = new Dictionary<PSXObjectExporter, int>();
for (int i = 0; i < scene.exporters.Length; i++)
exporterIndex[scene.exporters[i]] = i;
// ──────────────────────────────────────────────────────
// Header (104 bytes — splashpack v15)
// Header (104 bytes — splashpack v16)
// ──────────────────────────────────────────────────────
writer.Write('S');
writer.Write('P');
writer.Write((ushort)15);
writer.Write((ushort)16);
writer.Write((ushort)luaFiles.Count);
writer.Write((ushort)scene.exporters.Length);
writer.Write((ushort)scene.atlases.Length);
@@ -156,7 +167,7 @@ namespace SplashEdit.RuntimeCode
writer.Write((ushort)scene.bvh.TriangleRefCount);
writer.Write((ushort)scene.sceneType);
writer.Write((ushort)0); // pad0
writer.Write((ushort)triggerBoxCount); // was pad0
writer.Write((ushort)scene.collisionExporter.MeshCount);
writer.Write((ushort)scene.collisionExporter.TriangleCount);
@@ -278,29 +289,50 @@ namespace SplashEdit.RuntimeCode
}
// ──────────────────────────────────────────────────────
// Collider metadata (32 bytes each)
// Collider metadata (32 bytes each) — Dynamic objects only
// ──────────────────────────────────────────────────────
for (int exporterIdx = 0; exporterIdx < scene.exporters.Length; exporterIdx++)
{
PSXObjectExporter exporter = scene.exporters[exporterIdx];
if (exporter.CollisionType == PSXCollisionType.None || exporter.StaticCollider)
continue;
if (exporter.CollisionType != PSXCollisionType.Dynamic) continue;
MeshFilter meshFilter = exporter.GetComponent<MeshFilter>();
Mesh collisionMesh = exporter.CustomCollisionMesh != null
? exporter.CustomCollisionMesh
: meshFilter?.sharedMesh;
Mesh renderMesh = meshFilter?.sharedMesh;
if (renderMesh == null) continue;
if (collisionMesh == null)
continue;
WriteWorldAABB(writer, exporter, renderMesh.bounds, gte);
WriteWorldAABB(writer, exporter, collisionMesh.bounds, gte);
// Collision metadata (8 bytes)
writer.Write((byte)exporter.CollisionType);
writer.Write((byte)(1 << (exporter.CollisionLayer - 1)));
writer.Write((byte)1); // CollisionType::Solid on C++ side
writer.Write((byte)0xFF); // layerMask (all layers)
writer.Write((ushort)exporterIdx);
writer.Write((uint)0); // padding
writer.Write((uint)0);
}
// ──────────────────────────────────────────────────────
// Trigger box metadata (32 bytes each)
// ──────────────────────────────────────────────────────
if (scene.triggerBoxes != null)
{
foreach (var tb in scene.triggerBoxes)
{
Bounds wb = tb.GetWorldBounds();
Vector3 wMin = wb.min;
Vector3 wMax = wb.max;
writer.Write(PSXTrig.ConvertWorldToFixed12(wMin.x / gte));
writer.Write(PSXTrig.ConvertWorldToFixed12(-wMax.y / gte));
writer.Write(PSXTrig.ConvertWorldToFixed12(wMin.z / gte));
writer.Write(PSXTrig.ConvertWorldToFixed12(wMax.x / gte));
writer.Write(PSXTrig.ConvertWorldToFixed12(-wMin.y / gte));
writer.Write(PSXTrig.ConvertWorldToFixed12(wMax.z / gte));
if (tb.LuaFile != null)
writer.Write((short)luaFiles.IndexOf(tb.LuaFile));
else
writer.Write((short)-1);
writer.Write((ushort)0); // padding
writer.Write((uint)0); // padding
}
}
// ──────────────────────────────────────────────────────

37
Runtime/PSXTriggerBox.cs Normal file
View File

@@ -0,0 +1,37 @@
using UnityEngine;
namespace SplashEdit.RuntimeCode
{
public class PSXTriggerBox : MonoBehaviour
{
[SerializeField] private Vector3 size = Vector3.one;
[SerializeField] private LuaFile luaFile;
public Vector3 Size => size;
public LuaFile LuaFile => luaFile;
public Bounds GetWorldBounds()
{
Vector3 halfSize = size * 0.5f;
Vector3 worldCenter = transform.position;
Vector3 worldMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
Vector3 worldMax = new Vector3(float.MinValue, float.MinValue, float.MinValue);
for (int i = 0; i < 8; i++)
{
Vector3 corner = new Vector3(
(i & 1) != 0 ? halfSize.x : -halfSize.x,
(i & 2) != 0 ? halfSize.y : -halfSize.y,
(i & 4) != 0 ? halfSize.z : -halfSize.z
);
Vector3 world = transform.TransformPoint(corner);
worldMin = Vector3.Min(worldMin, world);
worldMax = Vector3.Max(worldMax, world);
}
Bounds b = new Bounds();
b.SetMinMax(worldMin, worldMax);
return b;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 72b9d8d2e8eafba46a30ba345beb9692

View File

@@ -161,10 +161,23 @@ namespace SplashEdit.RuntimeCode
List<PSXUIElementData> elements = new List<PSXUIElementData>();
Debug.Log($"[UIExporter] Canvas '{canvas.CanvasName}' on '{canvas.gameObject.name}' " +
$"canvasW={canvasW} canvasH={canvasH} childCount={canvas.transform.childCount}");
// Log what each collector finds
int prevCount = elements.Count;
CollectImages(canvas.transform, canvasRect, scaleX, scaleY, resolution, elements);
Debug.Log($"[UIExporter] Images: {elements.Count - prevCount}");
prevCount = elements.Count;
CollectBoxes(canvas.transform, canvasRect, scaleX, scaleY, resolution, elements);
Debug.Log($"[UIExporter] Boxes: {elements.Count - prevCount}");
prevCount = elements.Count;
CollectTexts(canvas.transform, canvasRect, scaleX, scaleY, resolution, elements, uniqueFonts);
Debug.Log($"[UIExporter] Texts: {elements.Count - prevCount}");
prevCount = elements.Count;
CollectProgressBars(canvas.transform, canvasRect, scaleX, scaleY, resolution, elements);
Debug.Log($"[UIExporter] ProgressBars: {elements.Count - prevCount}");
Debug.Log($"[UIExporter] TOTAL elements: {elements.Count}");
string name = canvas.CanvasName ?? "canvas";
if (name.Length > 24) name = name.Substring(0, 24);