psst
This commit is contained in:
439
Runtime/PSXRoom.cs
Normal file
439
Runtime/PSXRoom.cs
Normal file
@@ -0,0 +1,439 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SplashEdit.RuntimeCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a convex room volume for the portal/room occlusion system.
|
||||
/// Place one of these per room in the scene. Geometry is assigned to rooms by
|
||||
/// centroid containment during export. Portals between adjacent rooms are detected
|
||||
/// automatically.
|
||||
///
|
||||
/// This is independent of the navregion/portal system used for navigation.
|
||||
/// </summary>
|
||||
[ExecuteInEditMode]
|
||||
public class PSXRoom : MonoBehaviour
|
||||
{
|
||||
[Tooltip("Optional display name for this room (used in editor gizmos).")]
|
||||
public string RoomName = "";
|
||||
|
||||
[Tooltip("Size of the room volume in local space. Defaults to the object's scale.")]
|
||||
public Vector3 VolumeSize = Vector3.one;
|
||||
|
||||
[Tooltip("Offset of the volume center relative to the transform position.")]
|
||||
public Vector3 VolumeOffset = Vector3.zero;
|
||||
|
||||
/// <summary>World-space AABB of this room.</summary>
|
||||
public Bounds GetWorldBounds()
|
||||
{
|
||||
Vector3 center = transform.TransformPoint(VolumeOffset);
|
||||
// Transform the 8 corners to get a world-space AABB
|
||||
Vector3 halfSize = VolumeSize * 0.5f;
|
||||
Vector3 wMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
|
||||
Vector3 wMax = new Vector3(float.MinValue, float.MinValue, float.MinValue);
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
Vector3 corner = VolumeOffset + 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);
|
||||
wMin = Vector3.Min(wMin, world);
|
||||
wMax = Vector3.Max(wMax, world);
|
||||
}
|
||||
Bounds b = new Bounds();
|
||||
b.SetMinMax(wMin, wMax);
|
||||
return b;
|
||||
}
|
||||
|
||||
/// <summary>Check if a world-space point is inside this room volume.</summary>
|
||||
public bool ContainsPoint(Vector3 worldPoint)
|
||||
{
|
||||
return GetWorldBounds().Contains(worldPoint);
|
||||
}
|
||||
|
||||
void OnDrawGizmos()
|
||||
{
|
||||
Gizmos.color = new Color(0.2f, 0.8f, 0.4f, 0.15f);
|
||||
Gizmos.matrix = transform.localToWorldMatrix;
|
||||
Gizmos.DrawCube(VolumeOffset, VolumeSize);
|
||||
Gizmos.color = new Color(0.2f, 0.8f, 0.4f, 0.6f);
|
||||
Gizmos.DrawWireCube(VolumeOffset, VolumeSize);
|
||||
Gizmos.matrix = Matrix4x4.identity;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (!string.IsNullOrEmpty(RoomName))
|
||||
{
|
||||
UnityEditor.Handles.Label(transform.TransformPoint(VolumeOffset),
|
||||
RoomName, new GUIStyle { normal = { textColor = Color.green } });
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Portal between two PSXRoom volumes, stored during export.
|
||||
/// Built from PSXPortalLink scene components.
|
||||
/// </summary>
|
||||
public struct PSXPortal
|
||||
{
|
||||
public int roomA;
|
||||
public int roomB;
|
||||
public Vector3 center; // World-space portal center (from PSXPortalLink transform)
|
||||
public Vector2 portalSize; // Portal opening size in world units (width, height)
|
||||
public Vector3 normal; // Portal facing direction (from PSXPortalLink transform.forward)
|
||||
public Vector3 right; // Portal local right axis (world space)
|
||||
public Vector3 up; // Portal local up axis (world space)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds and exports the room/portal system for a scene.
|
||||
/// Called during PSXSceneExporter.Export().
|
||||
/// Portals are user-defined via PSXPortalLink components instead of auto-detected.
|
||||
/// </summary>
|
||||
public class PSXRoomBuilder
|
||||
{
|
||||
private PSXRoom[] _rooms;
|
||||
private List<PSXPortal> _portals = new List<PSXPortal>();
|
||||
private List<BVH.TriangleRef>[] _roomTriRefs;
|
||||
private List<BVH.TriangleRef> _catchAllTriRefs = new List<BVH.TriangleRef>();
|
||||
|
||||
public int RoomCount => _rooms?.Length ?? 0;
|
||||
public int PortalCount => _portals?.Count ?? 0;
|
||||
public int TotalTriRefCount
|
||||
{
|
||||
get
|
||||
{
|
||||
int count = 0;
|
||||
if (_roomTriRefs != null)
|
||||
foreach (var list in _roomTriRefs) count += list.Count;
|
||||
count += _catchAllTriRefs.Count;
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build the room system: assign triangles to rooms and read user-defined portals.
|
||||
/// </summary>
|
||||
/// <param name="rooms">All PSXRoom components in the scene.</param>
|
||||
/// <param name="portalLinks">All PSXPortalLink components (user-placed portals).</param>
|
||||
/// <param name="exporters">All object exporters (for triangle centroid testing).</param>
|
||||
/// <param name="gteScaling">GTE coordinate scaling factor.</param>
|
||||
public void Build(PSXRoom[] rooms, PSXPortalLink[] portalLinks,
|
||||
PSXObjectExporter[] exporters, float gteScaling)
|
||||
{
|
||||
_rooms = rooms;
|
||||
if (rooms == null || rooms.Length == 0) return;
|
||||
|
||||
_roomTriRefs = new List<BVH.TriangleRef>[rooms.Length];
|
||||
for (int i = 0; i < rooms.Length; i++)
|
||||
_roomTriRefs[i] = new List<BVH.TriangleRef>();
|
||||
_catchAllTriRefs.Clear();
|
||||
_portals.Clear();
|
||||
|
||||
// Assign each triangle to a room by vertex majority containment.
|
||||
// For each triangle, test all 3 world-space vertices against each room's AABB
|
||||
// (expanded by a margin to catch boundary geometry). The room containing the
|
||||
// most vertices wins. Ties broken by centroid. This prevents boundary triangles
|
||||
// (doorway walls, floor edges) from being assigned to the wrong room.
|
||||
const float ROOM_MARGIN = 0.5f; // expand AABBs by this much for testing
|
||||
Bounds[] roomBounds = new Bounds[rooms.Length];
|
||||
for (int i = 0; i < rooms.Length; i++)
|
||||
{
|
||||
roomBounds[i] = rooms[i].GetWorldBounds();
|
||||
roomBounds[i].Expand(ROOM_MARGIN * 2f); // Expand in all directions
|
||||
}
|
||||
|
||||
for (int objIdx = 0; objIdx < exporters.Length; objIdx++)
|
||||
{
|
||||
var exporter = exporters[objIdx];
|
||||
if (!exporter.IsActive) continue;
|
||||
MeshFilter mf = exporter.GetComponent<MeshFilter>();
|
||||
if (mf == null || mf.sharedMesh == null) continue;
|
||||
Mesh mesh = mf.sharedMesh;
|
||||
Vector3[] vertices = mesh.vertices;
|
||||
int[] indices = mesh.triangles;
|
||||
Matrix4x4 worldMatrix = exporter.transform.localToWorldMatrix;
|
||||
|
||||
for (int i = 0; i < indices.Length; i += 3)
|
||||
{
|
||||
Vector3 v0 = worldMatrix.MultiplyPoint3x4(vertices[indices[i]]);
|
||||
Vector3 v1 = worldMatrix.MultiplyPoint3x4(vertices[indices[i + 1]]);
|
||||
Vector3 v2 = worldMatrix.MultiplyPoint3x4(vertices[indices[i + 2]]);
|
||||
|
||||
// Test all 3 vertices against each room, pick room with most hits.
|
||||
int bestRoom = -1;
|
||||
int bestHits = 0;
|
||||
float bestDist = float.MaxValue;
|
||||
for (int r = 0; r < rooms.Length; r++)
|
||||
{
|
||||
int hits = 0;
|
||||
if (roomBounds[r].Contains(v0)) hits++;
|
||||
if (roomBounds[r].Contains(v1)) hits++;
|
||||
if (roomBounds[r].Contains(v2)) hits++;
|
||||
|
||||
if (hits > bestHits)
|
||||
{
|
||||
bestHits = hits;
|
||||
bestRoom = r;
|
||||
bestDist = (roomBounds[r].center - (v0 + v1 + v2) / 3f).sqrMagnitude;
|
||||
}
|
||||
else if (hits == bestHits && hits > 0)
|
||||
{
|
||||
// Tie-break: pick room whose center is closest to centroid
|
||||
float dist = (roomBounds[r].center - (v0 + v1 + v2) / 3f).sqrMagnitude;
|
||||
if (dist < bestDist)
|
||||
{
|
||||
bestRoom = r;
|
||||
bestDist = dist;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var triRef = new BVH.TriangleRef(objIdx, i / 3);
|
||||
if (bestRoom >= 0)
|
||||
_roomTriRefs[bestRoom].Add(triRef);
|
||||
else
|
||||
_catchAllTriRefs.Add(triRef);
|
||||
}
|
||||
}
|
||||
|
||||
// Build portals from user-placed PSXPortalLink components.
|
||||
// (Must happen before boundary duplication so we know which rooms are adjacent.)
|
||||
BuildPortals(portalLinks);
|
||||
|
||||
// Phase 3: Duplicate boundary triangles into both rooms at portal boundaries.
|
||||
// When a triangle has vertices in multiple rooms, it was assigned to only
|
||||
// the "best" room. For triangles near a portal, also add them to the adjacent
|
||||
// room so doorway/boundary geometry is visible from both sides.
|
||||
DuplicateBoundaryTriangles(exporters, roomBounds);
|
||||
|
||||
// Sort each room's tri-refs by objectIndex for GTE matrix batching.
|
||||
for (int i = 0; i < _roomTriRefs.Length; i++)
|
||||
_roomTriRefs[i].Sort((a, b) => a.objectIndex.CompareTo(b.objectIndex));
|
||||
_catchAllTriRefs.Sort((a, b) => a.objectIndex.CompareTo(b.objectIndex));
|
||||
|
||||
Debug.Log($"PSXRoomBuilder: {rooms.Length} rooms, {_portals.Count} portals, " +
|
||||
$"{TotalTriRefCount} tri-refs ({_catchAllTriRefs.Count} catch-all)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For each portal, find triangles assigned to one adjacent room whose vertices
|
||||
/// also touch the other adjacent room. Duplicate those triangles into the other
|
||||
/// room so boundary geometry (doorway walls, floor edges) is visible from both sides.
|
||||
/// </summary>
|
||||
private void DuplicateBoundaryTriangles(PSXObjectExporter[] exporters, Bounds[] roomBounds)
|
||||
{
|
||||
if (_portals.Count == 0) return;
|
||||
|
||||
int duplicated = 0;
|
||||
// Build a set of existing tri-refs per room for O(1) duplicate checking
|
||||
var roomSets = new HashSet<long>[_rooms.Length];
|
||||
for (int i = 0; i < _rooms.Length; i++)
|
||||
{
|
||||
roomSets[i] = new HashSet<long>();
|
||||
foreach (var tr in _roomTriRefs[i])
|
||||
roomSets[i].Add(((long)tr.objectIndex << 16) | tr.triangleIndex);
|
||||
}
|
||||
|
||||
foreach (var portal in _portals)
|
||||
{
|
||||
int rA = portal.roomA, rB = portal.roomB;
|
||||
if (rA < 0 || rA >= _rooms.Length || rB < 0 || rB >= _rooms.Length) continue;
|
||||
|
||||
// For each triangle in room A, check if any vertex is inside room B's AABB.
|
||||
// If so, add a copy to room B (and vice versa).
|
||||
DuplicateDirection(rA, rB, exporters, roomBounds, roomSets, ref duplicated);
|
||||
DuplicateDirection(rB, rA, exporters, roomBounds, roomSets, ref duplicated);
|
||||
}
|
||||
|
||||
if (duplicated > 0)
|
||||
Debug.Log($"PSXRoomBuilder: Duplicated {duplicated} boundary triangles across portal edges.");
|
||||
}
|
||||
|
||||
private void DuplicateDirection(int srcRoom, int dstRoom,
|
||||
PSXObjectExporter[] exporters, Bounds[] roomBounds,
|
||||
HashSet<long>[] roomSets, ref int duplicated)
|
||||
{
|
||||
var srcList = new List<BVH.TriangleRef>(_roomTriRefs[srcRoom]);
|
||||
foreach (var triRef in srcList)
|
||||
{
|
||||
long key = ((long)triRef.objectIndex << 16) | triRef.triangleIndex;
|
||||
if (roomSets[dstRoom].Contains(key)) continue; // Already in dst
|
||||
|
||||
if (triRef.objectIndex >= exporters.Length) continue;
|
||||
var exporter = exporters[triRef.objectIndex];
|
||||
MeshFilter mf = exporter.GetComponent<MeshFilter>();
|
||||
if (mf == null || mf.sharedMesh == null) continue;
|
||||
Mesh mesh = mf.sharedMesh;
|
||||
Vector3[] vertices = mesh.vertices;
|
||||
int[] indices = mesh.triangles;
|
||||
Matrix4x4 worldMatrix = exporter.transform.localToWorldMatrix;
|
||||
|
||||
int triStart = triRef.triangleIndex * 3;
|
||||
if (triStart + 2 >= indices.Length) continue;
|
||||
Vector3 v0 = worldMatrix.MultiplyPoint3x4(vertices[indices[triStart]]);
|
||||
Vector3 v1 = worldMatrix.MultiplyPoint3x4(vertices[indices[triStart + 1]]);
|
||||
Vector3 v2 = worldMatrix.MultiplyPoint3x4(vertices[indices[triStart + 2]]);
|
||||
|
||||
// Check if any vertex is inside the destination room's AABB
|
||||
if (roomBounds[dstRoom].Contains(v0) ||
|
||||
roomBounds[dstRoom].Contains(v1) ||
|
||||
roomBounds[dstRoom].Contains(v2))
|
||||
{
|
||||
_roomTriRefs[dstRoom].Add(triRef);
|
||||
roomSets[dstRoom].Add(key);
|
||||
duplicated++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert PSXPortalLink components into PSXPortal entries.
|
||||
/// Maps PSXRoom references to room indices, validates, and stores center positions.
|
||||
/// </summary>
|
||||
private void BuildPortals(PSXPortalLink[] portalLinks)
|
||||
{
|
||||
if (portalLinks == null) return;
|
||||
|
||||
// Build a fast lookup: PSXRoom instance → index.
|
||||
var roomIndex = new Dictionary<PSXRoom, int>();
|
||||
for (int i = 0; i < _rooms.Length; i++)
|
||||
roomIndex[_rooms[i]] = i;
|
||||
|
||||
foreach (var link in portalLinks)
|
||||
{
|
||||
if (link == null) continue;
|
||||
if (link.RoomA == null || link.RoomB == null)
|
||||
{
|
||||
Debug.LogWarning($"PSXPortalLink '{link.name}' has unassigned room references — skipped.");
|
||||
continue;
|
||||
}
|
||||
if (link.RoomA == link.RoomB)
|
||||
{
|
||||
Debug.LogWarning($"PSXPortalLink '{link.name}' references the same room twice — skipped.");
|
||||
continue;
|
||||
}
|
||||
if (!roomIndex.TryGetValue(link.RoomA, out int idxA))
|
||||
{
|
||||
Debug.LogWarning($"PSXPortalLink '{link.name}': RoomA '{link.RoomA.name}' is not a known PSXRoom — skipped.");
|
||||
continue;
|
||||
}
|
||||
if (!roomIndex.TryGetValue(link.RoomB, out int idxB))
|
||||
{
|
||||
Debug.LogWarning($"PSXPortalLink '{link.name}': RoomB '{link.RoomB.name}' is not a known PSXRoom — skipped.");
|
||||
continue;
|
||||
}
|
||||
|
||||
_portals.Add(new PSXPortal
|
||||
{
|
||||
roomA = idxA,
|
||||
roomB = idxB,
|
||||
center = link.transform.position,
|
||||
portalSize = link.PortalSize,
|
||||
normal = link.transform.forward,
|
||||
right = link.transform.right,
|
||||
up = link.transform.up
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write room/portal data to the splashpack binary.
|
||||
/// </summary>
|
||||
public void WriteToBinary(System.IO.BinaryWriter writer, float gteScaling)
|
||||
{
|
||||
if (_rooms == null || _rooms.Length == 0) return;
|
||||
|
||||
// Per-room data (32 bytes each): AABB (24) + firstTriRef (2) + triRefCount (2) + pad (4)
|
||||
int runningTriRefOffset = 0;
|
||||
for (int i = 0; i < _rooms.Length; i++)
|
||||
{
|
||||
Bounds wb = _rooms[i].GetWorldBounds();
|
||||
// PS1 coordinate space (negate Y, swap min/max Y)
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(wb.min.x / gteScaling));
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(-wb.max.y / gteScaling));
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(wb.min.z / gteScaling));
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(wb.max.x / gteScaling));
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(-wb.min.y / gteScaling));
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(wb.max.z / gteScaling));
|
||||
|
||||
writer.Write((ushort)runningTriRefOffset);
|
||||
writer.Write((ushort)_roomTriRefs[i].Count);
|
||||
writer.Write((uint)0); // padding
|
||||
runningTriRefOffset += _roomTriRefs[i].Count;
|
||||
}
|
||||
|
||||
// Catch-all room (always rendered) — written as an extra "room" entry
|
||||
{
|
||||
// Catch-all AABB: max world extents
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(-1000f / gteScaling));
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(-1000f / gteScaling));
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(-1000f / gteScaling));
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(1000f / gteScaling));
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(1000f / gteScaling));
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(1000f / gteScaling));
|
||||
writer.Write((ushort)runningTriRefOffset);
|
||||
writer.Write((ushort)_catchAllTriRefs.Count);
|
||||
writer.Write((uint)0);
|
||||
}
|
||||
|
||||
// Per-portal data (40 bytes each):
|
||||
// roomA(2) + roomB(2) + center(12) + halfW(2) + halfH(2) +
|
||||
// normal(6) + pad(2) + right(6) + up(6)
|
||||
foreach (var portal in _portals)
|
||||
{
|
||||
writer.Write((ushort)portal.roomA);
|
||||
writer.Write((ushort)portal.roomB);
|
||||
// Center of portal (PS1 coords: negate Y)
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(portal.center.x / gteScaling));
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(-portal.center.y / gteScaling));
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(portal.center.z / gteScaling));
|
||||
// Portal half-size in GTE units (fp12)
|
||||
float halfW = portal.portalSize.x * 0.5f;
|
||||
float halfH = portal.portalSize.y * 0.5f;
|
||||
writer.Write((short)Mathf.Clamp(Mathf.RoundToInt(halfW / gteScaling * 4096f), 1, 32767));
|
||||
writer.Write((short)Mathf.Clamp(Mathf.RoundToInt(halfH / gteScaling * 4096f), 1, 32767));
|
||||
// Portal facing normal (PS1 coords: negate Y) - 4.12 fixed-point unit vector
|
||||
writer.Write((short)Mathf.Clamp(Mathf.RoundToInt(portal.normal.x * 4096f), -32768, 32767));
|
||||
writer.Write((short)Mathf.Clamp(Mathf.RoundToInt(-portal.normal.y * 4096f), -32768, 32767));
|
||||
writer.Write((short)Mathf.Clamp(Mathf.RoundToInt(portal.normal.z * 4096f), -32768, 32767));
|
||||
writer.Write((short)0); // pad
|
||||
// Portal right axis (PS1 coords: negate Y) - 4.12 fixed-point unit vector
|
||||
writer.Write((short)Mathf.Clamp(Mathf.RoundToInt(portal.right.x * 4096f), -32768, 32767));
|
||||
writer.Write((short)Mathf.Clamp(Mathf.RoundToInt(-portal.right.y * 4096f), -32768, 32767));
|
||||
writer.Write((short)Mathf.Clamp(Mathf.RoundToInt(portal.right.z * 4096f), -32768, 32767));
|
||||
// Portal up axis (PS1 coords: negate Y) - 4.12 fixed-point unit vector
|
||||
writer.Write((short)Mathf.Clamp(Mathf.RoundToInt(portal.up.x * 4096f), -32768, 32767));
|
||||
writer.Write((short)Mathf.Clamp(Mathf.RoundToInt(-portal.up.y * 4096f), -32768, 32767));
|
||||
writer.Write((short)Mathf.Clamp(Mathf.RoundToInt(portal.up.z * 4096f), -32768, 32767));
|
||||
}
|
||||
|
||||
// Triangle refs (4 bytes each) — rooms in order, then catch-all
|
||||
for (int i = 0; i < _rooms.Length; i++)
|
||||
{
|
||||
foreach (var triRef in _roomTriRefs[i])
|
||||
{
|
||||
writer.Write(triRef.objectIndex);
|
||||
writer.Write(triRef.triangleIndex);
|
||||
}
|
||||
}
|
||||
foreach (var triRef in _catchAllTriRefs)
|
||||
{
|
||||
writer.Write(triRef.objectIndex);
|
||||
writer.Write(triRef.triangleIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public int GetBinarySize()
|
||||
{
|
||||
if (_rooms == null || _rooms.Length == 0) return 0;
|
||||
int roomDataSize = (_rooms.Length + 1) * 32; // +1 for catch-all
|
||||
int portalDataSize = _portals.Count * 40;
|
||||
int triRefSize = TotalTriRefCount * 4;
|
||||
return roomDataSize + portalDataSize + triRefSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user