diff --git a/Runtime/PSXSceneExporter.cs b/Runtime/PSXSceneExporter.cs index 5799463..9ac4faa 100644 --- a/Runtime/PSXSceneExporter.cs +++ b/Runtime/PSXSceneExporter.cs @@ -1,9 +1,13 @@ +using System; using System.Collections.Generic; using System.IO; +using System.Linq; using UnityEditor; using UnityEditor.Overlays; using UnityEngine; +using UnityEngine.Diagnostics; using UnityEngine.SceneManagement; +using UnityEngine.TextCore.Text; namespace SplashEdit.RuntimeCode { @@ -12,6 +16,7 @@ namespace SplashEdit.RuntimeCode public class PSXSceneExporter : MonoBehaviour { private PSXObjectExporter[] _exporters; + private TextureAtlas[] _atlases; private PSXData _psxData; private readonly string _psxDataPath = "Assets/PSXData.asset"; @@ -53,7 +58,7 @@ namespace SplashEdit.RuntimeCode VRAMPacker tp = new VRAMPacker(framebuffers, prohibitedAreas); var packed = tp.PackTexturesIntoVRAM(_exporters); _exporters = packed.processedObjects; - vramPixels = packed._vramPixels; + _atlases = packed.atlases; } @@ -61,77 +66,201 @@ namespace SplashEdit.RuntimeCode { string path = EditorUtility.SaveFilePanel("Select Output File", "", "output", "bin"); int totalFaces = 0; + + // Lists for mesh data offsets. + List offsetPlaceholderPositions = new List(); + List meshDataOffsets = new List(); + + // Lists for atlas data offsets. + List atlasOffsetPlaceholderPositions = new List(); + List atlasDataOffsets = new List(); + using (BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create))) { - // VramPixels are always 1MB - for (int y = 0; y < vramPixels.GetLength(1); y++) - { - for (int x = 0; x < vramPixels.GetLength(0); x++) - { - writer.Write(vramPixels[x, y].Pack()); - } - } + // Header + writer.Write('S'); + writer.Write('P'); + writer.Write((ushort)1); writer.Write((ushort)_exporters.Length); + writer.Write((ushort)_atlases.Length); + // Start of Metadata section + + // GameObject section (exporters) foreach (PSXObjectExporter exporter in _exporters) { + // Write object's position + writer.Write((int)PSXTrig.ConvertCoordinateToPSX(transform.position.x)); + writer.Write((int)PSXTrig.ConvertCoordinateToPSX(transform.position.y)); + writer.Write((int)PSXTrig.ConvertCoordinateToPSX(transform.position.z)); - int expander = 16 / ((int)exporter.Texture.BitDepth); + int[,] rotationMatrix = PSXTrig.ConvertRotationToPSXMatrix(exporter.transform.rotation); + writer.Write((int)rotationMatrix[0, 0]); + writer.Write((int)rotationMatrix[0, 1]); + writer.Write((int)rotationMatrix[0, 2]); + writer.Write((int)rotationMatrix[1, 0]); + writer.Write((int)rotationMatrix[1, 1]); + writer.Write((int)rotationMatrix[1, 2]); + writer.Write((int)rotationMatrix[2, 0]); + writer.Write((int)rotationMatrix[2, 1]); + writer.Write((int)rotationMatrix[2, 2]); + + // Set up texture page attributes + TPageAttr tpage = new TPageAttr(); + tpage.SetPageX(exporter.Texture.TexpageX); + tpage.SetPageX(exporter.Texture.TexpageY); + switch (exporter.Texture.BitDepth) + { + case PSXBPP.TEX_4BIT: + tpage.Set(ColorMode.Mode4Bit); + break; + case PSXBPP.TEX_8BIT: + tpage.Set(ColorMode.Mode8Bit); + break; + case PSXBPP.TEX_16BIT: + tpage.Set(ColorMode.Mode16Bit); + break; + } + tpage.SetDithering(true); + writer.Write((ushort)tpage.info); + writer.Write((ushort)exporter.Mesh.Triangles.Count); + + // Write placeholder for mesh data offset and record its position. + offsetPlaceholderPositions.Add(writer.BaseStream.Position); + writer.Write((int)0); // 4-byte placeholder for mesh data offset. + } + + // Atlas metadata section + foreach (TextureAtlas atlas in _atlases) + { + // Write placeholder for texture atlas raw data offset. + atlasOffsetPlaceholderPositions.Add(writer.BaseStream.Position); + writer.Write((int)0); // 4-byte placeholder for atlas data offset. + + writer.Write((ushort)atlas.Width); + writer.Write((ushort)TextureAtlas.Height); + writer.Write((ushort)atlas.PositionX); + writer.Write((ushort)atlas.PositionY); + } + + // Start of data section + + // Mesh data section: Write mesh data for each exporter. + foreach (PSXObjectExporter exporter in _exporters) + { + // Record the current offset for this exporter's mesh data. + long meshDataOffset = writer.BaseStream.Position; + meshDataOffsets.Add(meshDataOffset); totalFaces += exporter.Mesh.Triangles.Count; - writer.Write((ushort)exporter.Mesh.Triangles.Count); - writer.Write((byte)exporter.Texture.BitDepth); - writer.Write((byte)exporter.Texture.TexpageX); - writer.Write((byte)exporter.Texture.TexpageY); - writer.Write((ushort)exporter.Texture.ClutPackingX); - writer.Write((ushort)exporter.Texture.ClutPackingY); - writer.Write((byte)0); + + int expander = 16 / ((int)exporter.Texture.BitDepth); foreach (Tri tri in exporter.Mesh.Triangles) { + // Write vertices coordinates writer.Write((short)tri.v0.vx); writer.Write((short)tri.v0.vy); writer.Write((short)tri.v0.vz); - writer.Write((short)tri.v0.nx); - writer.Write((short)tri.v0.ny); - writer.Write((short)tri.v0.nz); - writer.Write((byte)(tri.v0.u + exporter.Texture.PackingX * expander)); - writer.Write((byte)(tri.v0.v + exporter.Texture.PackingY)); - writer.Write((byte) tri.v0.r); - writer.Write((byte) tri.v0.g); - writer.Write((byte) tri.v0.b); - for(int i = 0; i < 7; i ++) writer.Write((byte) 0); writer.Write((short)tri.v1.vx); writer.Write((short)tri.v1.vy); writer.Write((short)tri.v1.vz); - writer.Write((short)tri.v1.nx); - writer.Write((short)tri.v1.ny); - writer.Write((short)tri.v1.nz); - writer.Write((byte)(tri.v1.u + exporter.Texture.PackingX * expander)); - writer.Write((byte)(tri.v1.v + exporter.Texture.PackingY)); - writer.Write((byte) tri.v1.r); - writer.Write((byte) tri.v1.g); - writer.Write((byte) tri.v1.b); - for(int i = 0; i < 7; i ++) writer.Write((byte) 0); writer.Write((short)tri.v2.vx); writer.Write((short)tri.v2.vy); writer.Write((short)tri.v2.vz); - writer.Write((short)tri.v2.nx); - writer.Write((short)tri.v2.ny); - writer.Write((short)tri.v2.nz); + + // Write vertex normals for v0 only + writer.Write((short)tri.v0.nx); + writer.Write((short)tri.v0.ny); + writer.Write((short)tri.v0.nz); + + // Write UVs for each vertex, adjusting for texture packing + writer.Write((byte)(tri.v0.u + exporter.Texture.PackingX * expander)); + writer.Write((byte)(tri.v0.v + exporter.Texture.PackingY)); + + writer.Write((byte)(tri.v1.u + exporter.Texture.PackingX * expander)); + writer.Write((byte)(tri.v1.v + exporter.Texture.PackingY)); + writer.Write((byte)(tri.v2.u + exporter.Texture.PackingX * expander)); writer.Write((byte)(tri.v2.v + exporter.Texture.PackingY)); - writer.Write((byte) tri.v2.r); - writer.Write((byte) tri.v2.g); - writer.Write((byte) tri.v2.b); - for(int i = 0; i < 7; i ++) writer.Write((byte) 0); + writer.Write((ushort)0); // padding + + // Write vertex colors with padding + writer.Write((byte)tri.v0.r); + writer.Write((byte)tri.v0.g); + writer.Write((byte)tri.v0.b); + writer.Write((byte)0); // padding + + writer.Write((byte)tri.v1.r); + writer.Write((byte)tri.v1.g); + writer.Write((byte)tri.v1.b); + writer.Write((byte)0); // padding + + writer.Write((byte)tri.v2.r); + writer.Write((byte)tri.v2.g); + writer.Write((byte)tri.v2.b); + writer.Write((byte)0); // padding } } + + // Atlas data section: Write raw texture data for each atlas. + foreach (TextureAtlas atlas in _atlases) + { + AlignToFourBytes(writer); + // Record the current offset for this atlas's data. + long atlasDataOffset = writer.BaseStream.Position; + atlasDataOffsets.Add(atlasDataOffset); + + // Write the atlas's raw texture data. + 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()); + } + } + } + + // Backfill the mesh data offsets into the metadata section. + if (offsetPlaceholderPositions.Count == meshDataOffsets.Count) + { + for (int i = 0; i < offsetPlaceholderPositions.Count; i++) + { + writer.Seek((int)offsetPlaceholderPositions[i], SeekOrigin.Begin); + writer.Write((int)meshDataOffsets[i]); + } + } + else + { + Debug.LogError("Mismatch between metadata mesh offset placeholders and mesh data blocks!"); + } + + // Backfill the atlas data offsets into the metadata section. + if (atlasOffsetPlaceholderPositions.Count == atlasDataOffsets.Count) + { + for (int i = 0; i < atlasOffsetPlaceholderPositions.Count; i++) + { + writer.Seek((int)atlasOffsetPlaceholderPositions[i], SeekOrigin.Begin); + writer.Write((int)atlasDataOffsets[i]); + } + } + else + { + Debug.LogError("Mismatch between atlas offset placeholders and atlas data blocks!"); + } } Debug.Log(totalFaces); } + void AlignToFourBytes(BinaryWriter writer) + { + long position = writer.BaseStream.Position; + int padding = (int)(4 - (position % 4)) % 4; // Compute needed padding + Debug.Log($"aligned {padding} bytes"); + writer.Write(new byte[padding]); // Write zero padding + } + public void LoadData() { _psxData = AssetDatabase.LoadAssetAtPath(_psxDataPath); diff --git a/Runtime/TexturePacker.cs b/Runtime/TexturePacker.cs index fd2a9c8..4c7e4ee 100644 --- a/Runtime/TexturePacker.cs +++ b/Runtime/TexturePacker.cs @@ -15,6 +15,7 @@ namespace SplashEdit.RuntimeCode public int PositionY; // Y position of the atlas in VRAM. public int Width; // Width of the atlas. public const int Height = 256; // Fixed height for all atlases. + public VRAMPixel[,] vramPixels; public List ContainedTextures = new List(); // Textures packed in this atlas. } @@ -62,7 +63,7 @@ namespace SplashEdit.RuntimeCode /// /// Array of PSXObjectExporter objects to process. /// Tuple containing processed objects and the VRAM pixel array. - public (PSXObjectExporter[] processedObjects, VRAMPixel[,] _vramPixels) PackTexturesIntoVRAM(PSXObjectExporter[] objects) + public (PSXObjectExporter[] processedObjects, TextureAtlas[] atlases, VRAMPixel[,] _vramPixels) PackTexturesIntoVRAM(PSXObjectExporter[] objects) { List uniqueTextures = new List(); // Group objects by texture bit depth (high to low). @@ -87,11 +88,11 @@ namespace SplashEdit.RuntimeCode foreach (var obj in group.OrderByDescending(obj => obj.Texture.QuantizedWidth * obj.Texture.Height)) { // Remove duplicate textures - if (uniqueTextures.Any(tex => tex.OriginalTexture.GetInstanceID() == obj.Texture.OriginalTexture.GetInstanceID() && tex.BitDepth == obj.Texture.BitDepth)) + /*if (uniqueTextures.Any(tex => tex.OriginalTexture.GetInstanceID() == obj.Texture.OriginalTexture.GetInstanceID() && tex.BitDepth == obj.Texture.BitDepth)) { obj.Texture = uniqueTextures.First(tex => tex.OriginalTexture.GetInstanceID() == obj.Texture.OriginalTexture.GetInstanceID()); continue; - } + }*/ // Try to place the texture in the current atlas. if (!TryPlaceTextureInAtlas(atlas, obj.Texture)) @@ -116,7 +117,7 @@ namespace SplashEdit.RuntimeCode // Build the final VRAM pixel array from placed textures and CLUTs. BuildVram(); - return (objects, _vramPixels); + return (objects, _finalizedAtlases.ToArray(), _vramPixels); } /// @@ -249,13 +250,17 @@ namespace SplashEdit.RuntimeCode { foreach (TextureAtlas atlas in _finalizedAtlases) { + atlas.vramPixels = new VRAMPixel[atlas.Width, TextureAtlas.Height]; + foreach (PSXTexture2D texture in atlas.ContainedTextures) { + // Copy texture image data into VRAM using atlas and texture packing offsets. for (int y = 0; y < texture.Height; y++) { for (int x = 0; x < texture.QuantizedWidth; x++) { + atlas.vramPixels[x + texture.PackingX, y + texture.PackingY] = texture.ImageData[x, y]; _vramPixels[x + atlas.PositionX + texture.PackingX, y + atlas.PositionY + texture.PackingY] = texture.ImageData[x, y]; } } diff --git a/Runtime/Utils.cs b/Runtime/Utils.cs index 1c17997..a9cdf96 100644 --- a/Runtime/Utils.cs +++ b/Runtime/Utils.cs @@ -1,3 +1,4 @@ +using System.Runtime.InteropServices; using UnityEngine; namespace SplashEdit.RuntimeCode @@ -41,4 +42,123 @@ namespace SplashEdit.RuntimeCode return new Rect(X, Y, Width, Height); } } + public static class PSXTrig + { + + public static short ConvertCoordinateToPSX(float value) + { + return (short)(Mathf.Clamp(value, -4f, 3.999f) * 4096); + } + + public static short ConvertRadiansToPSX(float value) + { + return (short)(Mathf.Clamp(value, -4f, 3.999f) * 4096f / Mathf.PI); + } + + public static int[,] ConvertRotationToPSXMatrix(Quaternion rotation) + { + float xx = rotation.x * rotation.x; + float yy = rotation.y * rotation.y; + float zz = rotation.z * rotation.z; + float xy = rotation.x * rotation.y; + float xz = rotation.x * rotation.z; + float yz = rotation.y * rotation.z; + float wx = rotation.w * rotation.x; + float wy = rotation.w * rotation.y; + float wz = rotation.w * rotation.z; + + // Create the 3x3 rotation matrix + int[,] psxMatrix = new int[3, 3] + { + { ConvertToFixed12(1.0f - 2.0f * (yy + zz)), ConvertToFixed12(2.0f * (xy - wz)), ConvertToFixed12(2.0f * (xz + wy)) }, + { ConvertToFixed12(2.0f * (xy + wz)), ConvertToFixed12(1.0f - 2.0f * (xx + zz)), ConvertToFixed12(2.0f * (yz - wx)) }, + { ConvertToFixed12(2.0f * (xz - wy)), ConvertToFixed12(2.0f * (yz + wx)), ConvertToFixed12(1.0f - 2.0f * (xx + yy)) } + }; + + return psxMatrix; + } + + private static int ConvertToFixed12(float value) + { + return (int)(value * 4096.0f); // 2^12 = 4096 + } + } + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct TPageAttr + { + public ushort info; + + public TPageAttr SetPageX(byte x) + { + info &= 0xFFF0; // Clear lower 4 bits + x &= 0x0F; // Ensure only lower 4 bits are used + info |= x; + return this; + } + + public TPageAttr SetPageY(byte y) + { + info &= 0xFFEF; // Clear bit 4 + y &= 0x01; // Ensure only lower 1 bit is used + info |= (ushort)(y << 4); + return this; + } + + public TPageAttr Set(SemiTrans trans) + { + info &= 0xFF9F; // Clear bits 5 and 6 + uint t = (uint)trans; + info |= (ushort)(t << 5); + return this; + } + + public TPageAttr Set(ColorMode mode) + { + info &= 0xFE7F; // Clear bits 7 and 8 + uint m = (uint)mode; + info |= (ushort)(m << 7); + return this; + } + + public TPageAttr SetDithering(bool dithering) + { + if (dithering) + info |= 0x0200; + else + info &= 0xFDFF; + return this; + } + + public TPageAttr DisableDisplayArea() + { + info &= 0xFBFF; // Clear bit 10 + return this; + } + + public TPageAttr EnableDisplayArea() + { + info |= 0x0400; // Set bit 10 + return this; + } + + public override string ToString() => $"Info: 0x{info:X4}"; + } + + // Define the enums for SemiTrans and ColorMode (assuming their values) + public enum SemiTrans : uint + { + None = 0, + Type1 = 1, + Type2 = 2, + Type3 = 3 + } + + public enum ColorMode : uint + { + Mode4Bit = 0, + Mode8Bit = 1, + Mode16Bit = 2 + } + } +