psst
This commit is contained in:
693
Runtime/PSXSceneWriter.cs
Normal file
693
Runtime/PSXSceneWriter.cs
Normal file
@@ -0,0 +1,693 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SplashEdit.RuntimeCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Pure binary serializer for the splashpack v8 format.
|
||||
/// All I/O extracted from PSXSceneExporter so the MonoBehaviour stays thin.
|
||||
/// </summary>
|
||||
public static class PSXSceneWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// All scene data needed to produce a .bin file.
|
||||
/// Populated by PSXSceneExporter before calling <see cref="Write"/>.
|
||||
/// </summary>
|
||||
public struct SceneData
|
||||
{
|
||||
public PSXObjectExporter[] exporters;
|
||||
public TextureAtlas[] atlases;
|
||||
public PSXInteractable[] interactables;
|
||||
public AudioClipExport[] audioClips;
|
||||
public PSXCollisionExporter collisionExporter;
|
||||
public PSXNavRegionBuilder navRegionBuilder;
|
||||
public PSXRoomBuilder roomBuilder;
|
||||
public BVH bvh;
|
||||
public LuaFile sceneLuaFile;
|
||||
public float gteScaling;
|
||||
|
||||
// Player
|
||||
public Vector3 playerPos;
|
||||
public Quaternion playerRot;
|
||||
public float playerHeight;
|
||||
public float playerRadius;
|
||||
public float moveSpeed;
|
||||
public float sprintSpeed;
|
||||
public float jumpHeight;
|
||||
public float gravity;
|
||||
|
||||
// Scene configuration (v11)
|
||||
public int sceneType; // 0=exterior, 1=interior
|
||||
public bool fogEnabled;
|
||||
public Color fogColor;
|
||||
public int fogDensity; // 1-10
|
||||
}
|
||||
|
||||
// ─── Offset bookkeeping ───
|
||||
|
||||
private sealed class OffsetData
|
||||
{
|
||||
public readonly List<long> PlaceholderPositions = new List<long>();
|
||||
public readonly List<long> DataOffsets = new List<long>();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Public API
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// Serialize the scene to a splashpack v8 binary file.
|
||||
/// </summary>
|
||||
/// <param name="path">Absolute file path to write.</param>
|
||||
/// <param name="scene">Pre-built scene data.</param>
|
||||
/// <param name="log">Optional callback for progress messages.</param>
|
||||
public static void Write(string path, in SceneData scene, Action<string, LogType> log = null)
|
||||
{
|
||||
float gte = scene.gteScaling;
|
||||
int totalFaces = 0;
|
||||
|
||||
OffsetData luaOffset = new OffsetData();
|
||||
OffsetData meshOffset = new OffsetData();
|
||||
OffsetData atlasOffset = new OffsetData();
|
||||
OffsetData clutOffset = new OffsetData();
|
||||
|
||||
int clutCount = 0;
|
||||
List<LuaFile> luaFiles = new List<LuaFile>();
|
||||
|
||||
// Count CLUTs
|
||||
foreach (TextureAtlas atlas in scene.atlases)
|
||||
{
|
||||
foreach (var texture in atlas.ContainedTextures)
|
||||
{
|
||||
if (texture.ColorPalette != null)
|
||||
clutCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect unique Lua files
|
||||
foreach (PSXObjectExporter exporter in scene.exporters)
|
||||
{
|
||||
if (exporter.LuaFile != null && !luaFiles.Contains(exporter.LuaFile))
|
||||
luaFiles.Add(exporter.LuaFile);
|
||||
}
|
||||
if (scene.sceneLuaFile != null && !luaFiles.Contains(scene.sceneLuaFile))
|
||||
luaFiles.Add(scene.sceneLuaFile);
|
||||
|
||||
using (BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)))
|
||||
{
|
||||
int colliderCount = 0;
|
||||
foreach (var e in scene.exporters)
|
||||
{
|
||||
if (e.CollisionType != PSXCollisionType.None)
|
||||
colliderCount++;
|
||||
}
|
||||
|
||||
// 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 (72 bytes total — splashpack v8)
|
||||
// ──────────────────────────────────────────────────────
|
||||
writer.Write('S');
|
||||
writer.Write('P');
|
||||
writer.Write((ushort)11); // version
|
||||
writer.Write((ushort)luaFiles.Count);
|
||||
writer.Write((ushort)scene.exporters.Length);
|
||||
writer.Write((ushort)0); // navmeshCount (legacy)
|
||||
writer.Write((ushort)scene.atlases.Length);
|
||||
writer.Write((ushort)clutCount);
|
||||
writer.Write((ushort)colliderCount);
|
||||
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((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((ushort)PSXTrig.ConvertCoordinateToPSX(scene.playerHeight, gte));
|
||||
|
||||
// Scene Lua index
|
||||
if (scene.sceneLuaFile != null)
|
||||
writer.Write((short)luaFiles.IndexOf(scene.sceneLuaFile));
|
||||
else
|
||||
writer.Write((short)-1);
|
||||
|
||||
// BVH info
|
||||
writer.Write((ushort)scene.bvh.NodeCount);
|
||||
writer.Write((ushort)scene.bvh.TriangleRefCount);
|
||||
|
||||
// Component counts (version 4)
|
||||
writer.Write((ushort)scene.interactables.Length);
|
||||
writer.Write((ushort)0); // healthCount (removed)
|
||||
writer.Write((ushort)0); // timerCount (removed)
|
||||
writer.Write((ushort)0); // spawnerCount (removed)
|
||||
|
||||
// NavGrid (version 5, legacy)
|
||||
writer.Write((ushort)0);
|
||||
writer.Write((ushort)0);
|
||||
|
||||
// Scene type (version 6)
|
||||
writer.Write((ushort)scene.sceneType); // 0=exterior, 1=interior
|
||||
writer.Write((ushort)0);
|
||||
|
||||
// World collision + nav regions (version 7)
|
||||
writer.Write((ushort)scene.collisionExporter.MeshCount);
|
||||
writer.Write((ushort)scene.collisionExporter.TriangleCount);
|
||||
writer.Write((ushort)scene.navRegionBuilder.RegionCount);
|
||||
writer.Write((ushort)scene.navRegionBuilder.PortalCount);
|
||||
|
||||
// Movement parameters (version 8, 12 bytes)
|
||||
{
|
||||
const float fps = 30f;
|
||||
float movePerFrame = scene.moveSpeed / fps / gte;
|
||||
float sprintPerFrame = scene.sprintSpeed / fps / gte;
|
||||
writer.Write((ushort)Mathf.Clamp(Mathf.RoundToInt(movePerFrame * 4096f), 0, 65535));
|
||||
writer.Write((ushort)Mathf.Clamp(Mathf.RoundToInt(sprintPerFrame * 4096f), 0, 65535));
|
||||
|
||||
float jumpVel = Mathf.Sqrt(2f * scene.gravity * scene.jumpHeight) / gte;
|
||||
writer.Write((ushort)Mathf.Clamp(Mathf.RoundToInt(jumpVel * 4096f), 0, 65535));
|
||||
|
||||
float gravPsx = scene.gravity / gte;
|
||||
writer.Write((ushort)Mathf.Clamp(Mathf.RoundToInt(gravPsx * 4096f), 0, 65535));
|
||||
|
||||
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(scene.playerRadius, gte));
|
||||
writer.Write((ushort)0); // padding
|
||||
}
|
||||
|
||||
// Name table offset placeholder (version 9, 4 bytes)
|
||||
long nameTableOffsetPos = writer.BaseStream.Position;
|
||||
writer.Write((uint)0); // placeholder for name table offset
|
||||
|
||||
// Audio clip info (version 10, 8 bytes)
|
||||
int audioClipCount = scene.audioClips?.Length ?? 0;
|
||||
writer.Write((ushort)audioClipCount);
|
||||
writer.Write((ushort)0); // padding
|
||||
long audioTableOffsetPos = writer.BaseStream.Position;
|
||||
writer.Write((uint)0); // placeholder for audio table offset
|
||||
|
||||
// Fog + room/portal header (version 11, 12 bytes)
|
||||
{
|
||||
writer.Write((byte)(scene.fogEnabled ? 1 : 0));
|
||||
writer.Write((byte)Mathf.Clamp(Mathf.RoundToInt(scene.fogColor.r * 255f), 0, 255));
|
||||
writer.Write((byte)Mathf.Clamp(Mathf.RoundToInt(scene.fogColor.g * 255f), 0, 255));
|
||||
writer.Write((byte)Mathf.Clamp(Mathf.RoundToInt(scene.fogColor.b * 255f), 0, 255));
|
||||
writer.Write((byte)Mathf.Clamp(scene.fogDensity, 1, 10));
|
||||
writer.Write((byte)0); // reserved
|
||||
int roomCount = scene.roomBuilder?.RoomCount ?? 0;
|
||||
int portalCount = scene.roomBuilder?.PortalCount ?? 0;
|
||||
int roomTriRefCount = scene.roomBuilder?.TotalTriRefCount ?? 0;
|
||||
// roomCount is the room count NOT including catch-all; the binary adds +1 for it
|
||||
writer.Write((ushort)(roomCount > 0 ? roomCount + 1 : 0));
|
||||
writer.Write((ushort)portalCount);
|
||||
writer.Write((ushort)roomTriRefCount);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// Lua file metadata
|
||||
// ──────────────────────────────────────────────────────
|
||||
foreach (LuaFile luaFile in luaFiles)
|
||||
{
|
||||
luaOffset.PlaceholderPositions.Add(writer.BaseStream.Position);
|
||||
writer.Write((int)0); // placeholder
|
||||
writer.Write((uint)Encoding.UTF8.GetByteCount(luaFile.LuaScript));
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// GameObject section
|
||||
// ──────────────────────────────────────────────────────
|
||||
Dictionary<PSXObjectExporter, int> interactableIndices = new Dictionary<PSXObjectExporter, int>();
|
||||
for (int i = 0; i < scene.interactables.Length; i++)
|
||||
{
|
||||
var exp = scene.interactables[i].GetComponent<PSXObjectExporter>();
|
||||
if (exp != null) interactableIndices[exp] = i;
|
||||
}
|
||||
|
||||
foreach (PSXObjectExporter exporter in scene.exporters)
|
||||
{
|
||||
meshOffset.PlaceholderPositions.Add(writer.BaseStream.Position);
|
||||
writer.Write((int)0); // placeholder
|
||||
|
||||
// Transform — position as 20.12 fixed-point
|
||||
Vector3 pos = exporter.transform.localToWorldMatrix.GetPosition();
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(pos.x / gte));
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(-pos.y / gte));
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(pos.z / gte));
|
||||
|
||||
int[,] rot = PSXTrig.ConvertRotationToPSXMatrix(exporter.transform.rotation);
|
||||
for (int r = 0; r < 3; r++)
|
||||
for (int c = 0; c < 3; c++)
|
||||
writer.Write((int)rot[r, c]);
|
||||
|
||||
writer.Write((ushort)exporter.Mesh.Triangles.Count);
|
||||
|
||||
if (exporter.LuaFile != null)
|
||||
writer.Write((short)luaFiles.IndexOf(exporter.LuaFile));
|
||||
else
|
||||
writer.Write((short)-1);
|
||||
|
||||
// Bitfield (LSB = isActive)
|
||||
writer.Write(exporter.IsActive ? 1 : 0);
|
||||
|
||||
// Component indices (8 bytes)
|
||||
writer.Write(interactableIndices.TryGetValue(exporter, out int interactIdx) ? (ushort)interactIdx : (ushort)0xFFFF);
|
||||
writer.Write((ushort)0xFFFF); // _reserved0 (legacy healthIndex)
|
||||
writer.Write((uint)0); // eventMask (runtime-only, must be zero)
|
||||
|
||||
// World-space AABB (24 bytes)
|
||||
WriteObjectAABB(writer, exporter, gte);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// Collider metadata (32 bytes each)
|
||||
// ──────────────────────────────────────────────────────
|
||||
for (int exporterIdx = 0; exporterIdx < scene.exporters.Length; exporterIdx++)
|
||||
{
|
||||
PSXObjectExporter exporter = scene.exporters[exporterIdx];
|
||||
if (exporter.CollisionType == PSXCollisionType.None)
|
||||
continue;
|
||||
|
||||
MeshFilter meshFilter = exporter.GetComponent<MeshFilter>();
|
||||
Mesh collisionMesh = exporter.CustomCollisionMesh != null
|
||||
? exporter.CustomCollisionMesh
|
||||
: meshFilter?.sharedMesh;
|
||||
|
||||
if (collisionMesh == null)
|
||||
continue;
|
||||
|
||||
WriteWorldAABB(writer, exporter, collisionMesh.bounds, gte);
|
||||
|
||||
// Collision metadata (8 bytes)
|
||||
writer.Write((byte)exporter.CollisionType);
|
||||
writer.Write((byte)(1 << (exporter.CollisionLayer - 1)));
|
||||
writer.Write((ushort)exporterIdx);
|
||||
writer.Write((uint)0); // padding
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// BVH data (inline)
|
||||
// ──────────────────────────────────────────────────────
|
||||
AlignToFourBytes(writer);
|
||||
scene.bvh.WriteToBinary(writer, gte);
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// Interactable components (24 bytes each)
|
||||
// ──────────────────────────────────────────────────────
|
||||
AlignToFourBytes(writer);
|
||||
foreach (PSXInteractable interactable in scene.interactables)
|
||||
{
|
||||
var exp = interactable.GetComponent<PSXObjectExporter>();
|
||||
int goIndex = exporterIndex.TryGetValue(exp, out int idx) ? idx : 0xFFFF;
|
||||
|
||||
float radiusSq = interactable.InteractionRadius * interactable.InteractionRadius;
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(radiusSq / (gte * gte)));
|
||||
|
||||
Vector3 offset = interactable.InteractionOffset;
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(offset.x / gte));
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(-offset.y / gte));
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(offset.z / gte));
|
||||
|
||||
writer.Write((byte)interactable.InteractButton);
|
||||
byte flags = 0;
|
||||
if (interactable.IsRepeatable) flags |= 0x01;
|
||||
if (interactable.ShowPrompt) flags |= 0x02;
|
||||
if (interactable.RequireLineOfSight) flags |= 0x04;
|
||||
writer.Write(flags);
|
||||
writer.Write(interactable.CooldownFrames);
|
||||
|
||||
writer.Write((ushort)0); // currentCooldown (runtime)
|
||||
writer.Write((ushort)goIndex);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// World collision soup (version 7+)
|
||||
// ──────────────────────────────────────────────────────
|
||||
if (scene.collisionExporter.MeshCount > 0)
|
||||
{
|
||||
AlignToFourBytes(writer);
|
||||
scene.collisionExporter.WriteToBinary(writer, gte);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// Nav region data (version 7+)
|
||||
// ──────────────────────────────────────────────────────
|
||||
if (scene.navRegionBuilder.RegionCount > 0)
|
||||
{
|
||||
AlignToFourBytes(writer);
|
||||
scene.navRegionBuilder.WriteToBinary(writer, gte);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// Room/portal data (version 11, interior scenes)
|
||||
// Must be in the sequential cursor section (after nav regions,
|
||||
// before atlas metadata) so the C++ reader can find it.
|
||||
// ──────────────────────────────────────────────────────
|
||||
if (scene.roomBuilder != null && scene.roomBuilder.RoomCount > 0)
|
||||
{
|
||||
AlignToFourBytes(writer);
|
||||
scene.roomBuilder.WriteToBinary(writer, scene.gteScaling);
|
||||
log?.Invoke($"Room/portal data: {scene.roomBuilder.RoomCount} rooms, {scene.roomBuilder.PortalCount} portals, {scene.roomBuilder.TotalTriRefCount} tri-refs.", LogType.Log);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// Atlas metadata
|
||||
// ──────────────────────────────────────────────────────
|
||||
foreach (TextureAtlas atlas in scene.atlases)
|
||||
{
|
||||
atlasOffset.PlaceholderPositions.Add(writer.BaseStream.Position);
|
||||
writer.Write((int)0); // placeholder
|
||||
writer.Write((ushort)atlas.Width);
|
||||
writer.Write((ushort)TextureAtlas.Height);
|
||||
writer.Write((ushort)atlas.PositionX);
|
||||
writer.Write((ushort)atlas.PositionY);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// CLUT metadata
|
||||
// ──────────────────────────────────────────────────────
|
||||
foreach (TextureAtlas atlas in scene.atlases)
|
||||
{
|
||||
foreach (var texture in atlas.ContainedTextures)
|
||||
{
|
||||
if (texture.ColorPalette != null)
|
||||
{
|
||||
clutOffset.PlaceholderPositions.Add(writer.BaseStream.Position);
|
||||
writer.Write((int)0); // placeholder
|
||||
writer.Write((ushort)texture.ClutPackingX);
|
||||
writer.Write((ushort)texture.ClutPackingY);
|
||||
writer.Write((ushort)texture.ColorPalette.Count);
|
||||
writer.Write((ushort)0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// Data sections
|
||||
// ══════════════════════════════════════════════════════
|
||||
|
||||
// Lua data
|
||||
foreach (LuaFile luaFile in luaFiles)
|
||||
{
|
||||
AlignToFourBytes(writer);
|
||||
luaOffset.DataOffsets.Add(writer.BaseStream.Position);
|
||||
writer.Write(Encoding.UTF8.GetBytes(luaFile.LuaScript));
|
||||
}
|
||||
|
||||
// Mesh data
|
||||
foreach (PSXObjectExporter exporter in scene.exporters)
|
||||
{
|
||||
AlignToFourBytes(writer);
|
||||
meshOffset.DataOffsets.Add(writer.BaseStream.Position);
|
||||
totalFaces += exporter.Mesh.Triangles.Count;
|
||||
|
||||
foreach (Tri tri in exporter.Mesh.Triangles)
|
||||
{
|
||||
// Vertex positions (3 × 6 bytes)
|
||||
WriteVertexPosition(writer, tri.v0);
|
||||
WriteVertexPosition(writer, tri.v1);
|
||||
WriteVertexPosition(writer, tri.v2);
|
||||
|
||||
// Normal for v0 only
|
||||
WriteVertexNormals(writer, tri.v0);
|
||||
|
||||
// Vertex colors (3 × 4 bytes)
|
||||
WriteVertexColor(writer, tri.v0);
|
||||
WriteVertexColor(writer, tri.v1);
|
||||
WriteVertexColor(writer, tri.v2);
|
||||
|
||||
if (tri.IsUntextured)
|
||||
{
|
||||
// Zero UVs
|
||||
writer.Write((byte)0); writer.Write((byte)0);
|
||||
writer.Write((byte)0); writer.Write((byte)0);
|
||||
writer.Write((byte)0); writer.Write((byte)0);
|
||||
writer.Write((ushort)0); // padding
|
||||
|
||||
// Sentinel tpage = 0xFFFF marks untextured
|
||||
writer.Write((ushort)0xFFFF);
|
||||
writer.Write((ushort)0);
|
||||
writer.Write((ushort)0);
|
||||
writer.Write((ushort)0);
|
||||
}
|
||||
else
|
||||
{
|
||||
PSXTexture2D tex = exporter.GetTexture(tri.TextureIndex);
|
||||
int expander = 16 / (int)tex.BitDepth;
|
||||
|
||||
WriteVertexUV(writer, tri.v0, tex, expander);
|
||||
WriteVertexUV(writer, tri.v1, tex, expander);
|
||||
WriteVertexUV(writer, tri.v2, tex, expander);
|
||||
writer.Write((ushort)0); // padding
|
||||
|
||||
TPageAttr tpage = new TPageAttr();
|
||||
tpage.SetPageX(tex.TexpageX);
|
||||
tpage.SetPageY(tex.TexpageY);
|
||||
tpage.Set(tex.BitDepth.ToColorMode());
|
||||
tpage.SetDithering(true);
|
||||
writer.Write((ushort)tpage.info);
|
||||
writer.Write((ushort)tex.ClutPackingX);
|
||||
writer.Write((ushort)tex.ClutPackingY);
|
||||
writer.Write((ushort)0); // padding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Atlas pixel data
|
||||
foreach (TextureAtlas atlas in scene.atlases)
|
||||
{
|
||||
AlignToFourBytes(writer);
|
||||
atlasOffset.DataOffsets.Add(writer.BaseStream.Position);
|
||||
|
||||
for (int y = 0; y < atlas.vramPixels.GetLength(1); y++)
|
||||
for (int x = 0; x < atlas.vramPixels.GetLength(0); x++)
|
||||
writer.Write(atlas.vramPixels[x, y].Pack());
|
||||
}
|
||||
|
||||
// CLUT data
|
||||
foreach (TextureAtlas atlas in scene.atlases)
|
||||
{
|
||||
foreach (var texture in atlas.ContainedTextures)
|
||||
{
|
||||
if (texture.ColorPalette != null)
|
||||
{
|
||||
AlignToFourBytes(writer);
|
||||
clutOffset.DataOffsets.Add(writer.BaseStream.Position);
|
||||
|
||||
foreach (VRAMPixel color in texture.ColorPalette)
|
||||
writer.Write((ushort)color.Pack());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// Object name table (version 9)
|
||||
// ──────────────────────────────────────────────────────
|
||||
AlignToFourBytes(writer);
|
||||
long nameTableStart = writer.BaseStream.Position;
|
||||
foreach (PSXObjectExporter exporter in scene.exporters)
|
||||
{
|
||||
string objName = exporter.gameObject.name;
|
||||
if (objName.Length > 24) objName = objName.Substring(0, 24);
|
||||
byte[] nameBytes = Encoding.UTF8.GetBytes(objName);
|
||||
writer.Write((byte)nameBytes.Length);
|
||||
writer.Write(nameBytes);
|
||||
writer.Write((byte)0); // null terminator
|
||||
}
|
||||
|
||||
// Backfill name table offset
|
||||
{
|
||||
long endPos = writer.BaseStream.Position;
|
||||
writer.Seek((int)nameTableOffsetPos, SeekOrigin.Begin);
|
||||
writer.Write((uint)nameTableStart);
|
||||
writer.Seek((int)endPos, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// Audio clip data (version 10)
|
||||
// ──────────────────────────────────────────────────────
|
||||
if (audioClipCount > 0 && scene.audioClips != null)
|
||||
{
|
||||
// Write audio table: per clip metadata (12 bytes each)
|
||||
AlignToFourBytes(writer);
|
||||
long audioTableStart = writer.BaseStream.Position;
|
||||
|
||||
// First pass: write metadata placeholders (16 bytes each)
|
||||
List<long> audioDataOffsetPositions = new List<long>();
|
||||
List<long> audioNameOffsetPositions = new List<long>();
|
||||
for (int i = 0; i < audioClipCount; i++)
|
||||
{
|
||||
var clip = scene.audioClips[i];
|
||||
audioDataOffsetPositions.Add(writer.BaseStream.Position);
|
||||
writer.Write((uint)0); // dataOffset placeholder
|
||||
writer.Write((uint)(clip.adpcmData?.Length ?? 0)); // sizeBytes
|
||||
writer.Write((ushort)clip.sampleRate);
|
||||
string name = clip.clipName ?? "";
|
||||
writer.Write((byte)(clip.loop ? 1 : 0));
|
||||
writer.Write((byte)System.Math.Min(name.Length, 255));
|
||||
audioNameOffsetPositions.Add(writer.BaseStream.Position);
|
||||
writer.Write((uint)0); // nameOffset placeholder
|
||||
}
|
||||
|
||||
// Second pass: write ADPCM data and backfill offsets
|
||||
for (int i = 0; i < audioClipCount; i++)
|
||||
{
|
||||
byte[] data = scene.audioClips[i].adpcmData;
|
||||
if (data != null && data.Length > 0)
|
||||
{
|
||||
AlignToFourBytes(writer);
|
||||
long dataPos = writer.BaseStream.Position;
|
||||
writer.Write(data);
|
||||
|
||||
// Backfill data offset
|
||||
long curPos = writer.BaseStream.Position;
|
||||
writer.Seek((int)audioDataOffsetPositions[i], SeekOrigin.Begin);
|
||||
writer.Write((uint)dataPos);
|
||||
writer.Seek((int)curPos, SeekOrigin.Begin);
|
||||
}
|
||||
}
|
||||
|
||||
// Backfill audio table offset in header
|
||||
{
|
||||
long curPos = writer.BaseStream.Position;
|
||||
writer.Seek((int)audioTableOffsetPos, SeekOrigin.Begin);
|
||||
writer.Write((uint)audioTableStart);
|
||||
writer.Seek((int)curPos, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
int totalAudioBytes = 0;
|
||||
foreach (var clip in scene.audioClips)
|
||||
if (clip.adpcmData != null) totalAudioBytes += clip.adpcmData.Length;
|
||||
|
||||
// Third pass: write audio clip names and backfill name offsets
|
||||
for (int i = 0; i < audioClipCount; i++)
|
||||
{
|
||||
string name = scene.audioClips[i].clipName ?? "";
|
||||
if (name.Length > 255) name = name.Substring(0, 255);
|
||||
long namePos = writer.BaseStream.Position;
|
||||
byte[] nameBytes = System.Text.Encoding.ASCII.GetBytes(name);
|
||||
writer.Write(nameBytes);
|
||||
writer.Write((byte)0); // null terminator
|
||||
|
||||
long curPos = writer.BaseStream.Position;
|
||||
writer.Seek((int)audioNameOffsetPositions[i], SeekOrigin.Begin);
|
||||
writer.Write((uint)namePos);
|
||||
writer.Seek((int)curPos, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
log?.Invoke($"{audioClipCount} audio clips ({totalAudioBytes / 1024}KB ADPCM) written.", LogType.Log);
|
||||
}
|
||||
|
||||
// Backfill offsets
|
||||
BackfillOffsets(writer, luaOffset, "lua", log);
|
||||
BackfillOffsets(writer, meshOffset, "mesh", log);
|
||||
BackfillOffsets(writer, atlasOffset, "atlas", log);
|
||||
BackfillOffsets(writer, clutOffset, "clut", log);
|
||||
}
|
||||
|
||||
log?.Invoke($"{totalFaces} faces written to {Path.GetFileName(path)}", LogType.Log);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Static helpers
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
private static void WriteVertexPosition(BinaryWriter w, PSXVertex v)
|
||||
{
|
||||
w.Write((short)v.vx);
|
||||
w.Write((short)v.vy);
|
||||
w.Write((short)v.vz);
|
||||
}
|
||||
|
||||
private static void WriteVertexNormals(BinaryWriter w, PSXVertex v)
|
||||
{
|
||||
w.Write((short)v.nx);
|
||||
w.Write((short)v.ny);
|
||||
w.Write((short)v.nz);
|
||||
}
|
||||
|
||||
private static void WriteVertexColor(BinaryWriter w, PSXVertex v)
|
||||
{
|
||||
w.Write((byte)v.r);
|
||||
w.Write((byte)v.g);
|
||||
w.Write((byte)v.b);
|
||||
w.Write((byte)0); // padding
|
||||
}
|
||||
|
||||
private static void WriteVertexUV(BinaryWriter w, PSXVertex v, PSXTexture2D t, int expander)
|
||||
{
|
||||
w.Write((byte)(v.u + t.PackingX * expander));
|
||||
w.Write((byte)(v.v + t.PackingY));
|
||||
}
|
||||
|
||||
private static void WriteObjectAABB(BinaryWriter writer, PSXObjectExporter exporter, float gte)
|
||||
{
|
||||
MeshFilter mf = exporter.GetComponent<MeshFilter>();
|
||||
Mesh mesh = mf?.sharedMesh;
|
||||
if (mesh != null)
|
||||
{
|
||||
WriteWorldAABB(writer, exporter, mesh.bounds, gte);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int z = 0; z < 6; z++) writer.Write((int)0);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteWorldAABB(BinaryWriter writer, PSXObjectExporter exporter, Bounds localBounds, float gte)
|
||||
{
|
||||
Vector3 ext = localBounds.extents;
|
||||
Vector3 center = localBounds.center;
|
||||
Vector3 aabbMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
|
||||
Vector3 aabbMax = new Vector3(float.MinValue, float.MinValue, float.MinValue);
|
||||
|
||||
// Compute world-space AABB from 8 transformed corners
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
Vector3 corner = center + new Vector3(
|
||||
(i & 1) != 0 ? ext.x : -ext.x,
|
||||
(i & 2) != 0 ? ext.y : -ext.y,
|
||||
(i & 4) != 0 ? ext.z : -ext.z
|
||||
);
|
||||
Vector3 world = exporter.transform.TransformPoint(corner);
|
||||
aabbMin = Vector3.Min(aabbMin, world);
|
||||
aabbMax = Vector3.Max(aabbMax, world);
|
||||
}
|
||||
|
||||
// PS1 coordinate space (negate Y, swap min/max)
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(aabbMin.x / gte));
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(-aabbMax.y / gte));
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(aabbMin.z / gte));
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(aabbMax.x / gte));
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(-aabbMin.y / gte));
|
||||
writer.Write(PSXTrig.ConvertWorldToFixed12(aabbMax.z / gte));
|
||||
}
|
||||
|
||||
private static void AlignToFourBytes(BinaryWriter writer)
|
||||
{
|
||||
long pos = writer.BaseStream.Position;
|
||||
int padding = (int)(4 - (pos % 4)) % 4;
|
||||
if (padding > 0)
|
||||
writer.Write(new byte[padding]);
|
||||
}
|
||||
|
||||
private static void BackfillOffsets(BinaryWriter writer, OffsetData data, string sectionName, Action<string, LogType> log)
|
||||
{
|
||||
if (data.PlaceholderPositions.Count != data.DataOffsets.Count)
|
||||
{
|
||||
log?.Invoke($"Offset mismatch in {sectionName}: {data.PlaceholderPositions.Count} placeholders vs {data.DataOffsets.Count} data blocks", LogType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < data.PlaceholderPositions.Count; i++)
|
||||
{
|
||||
writer.Seek((int)data.PlaceholderPositions[i], SeekOrigin.Begin);
|
||||
writer.Write((int)data.DataOffsets[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user