From da12ade58005eeed9e4d1c65c435f4ee9b8c98c0 Mon Sep 17 00:00:00 2001 From: jracek Date: Sun, 30 Mar 2025 21:31:02 +0200 Subject: [PATCH] Improved light baking, updated splashpack structure and documentation --- Editor/VramEditorWindow.cs | 6 +- Runtime/PSXLightingBaker.cs | 88 ++++++++++++++++ Runtime/PSXLightingBaker.cs.meta | 2 + Runtime/PSXMesh.cs | 167 ++++++++++++++++++++--------- Runtime/PSXObjectExporter.cs | 102 +++++++++++++++--- Runtime/PSXSceneExporter.cs | 175 ++++++++++++++++--------------- Runtime/TexturePacker.cs | 72 +++++++++---- Runtime/Utils.cs | 30 ++++-- doc/splashbundle.md | 108 ++++++++++--------- 9 files changed, 523 insertions(+), 227 deletions(-) create mode 100644 Runtime/PSXLightingBaker.cs create mode 100644 Runtime/PSXLightingBaker.cs.meta diff --git a/Editor/VramEditorWindow.cs b/Editor/VramEditorWindow.cs index 0d15465..3564e65 100644 --- a/Editor/VramEditorWindow.cs +++ b/Editor/VramEditorWindow.cs @@ -116,7 +116,7 @@ namespace SplashEdit.EditorCode PSXObjectExporter[] objects = FindObjectsByType(FindObjectsSortMode.None); foreach (PSXObjectExporter exp in objects) { - exp.CreatePSXTexture2D(); + exp.CreatePSXTextures2D(); } // Define framebuffer regions based on selected resolution and layout. @@ -139,7 +139,7 @@ namespace SplashEdit.EditorCode { for (int x = 0; x < VramWidth; x++) { - vramImage.SetPixel(x, VramHeight - y - 1, packed._vramPixels[x, y].GetUnityColor()); + vramImage.SetPixel(x, VramHeight - y - 1, packed.vramPixels[x, y].GetUnityColor()); } } vramImage.Apply(); @@ -155,7 +155,7 @@ namespace SplashEdit.EditorCode { for (int x = 0; x < VramWidth; x++) { - writer.Write(packed._vramPixels[x, y].Pack()); + writer.Write(packed.vramPixels[x, y].Pack()); } } } diff --git a/Runtime/PSXLightingBaker.cs b/Runtime/PSXLightingBaker.cs new file mode 100644 index 0000000..c9a8337 --- /dev/null +++ b/Runtime/PSXLightingBaker.cs @@ -0,0 +1,88 @@ +using UnityEngine; + +public static class PSXLightingBaker +{ + /// + /// Computes the per-vertex lighting from all scene light sources. + /// Incorporates ambient, diffuse, and spotlight falloff. + /// + /// The world-space position of the vertex. + /// The normalized world-space normal of the vertex. + /// A Color representing the lit vertex. + public static Color ComputeLighting(Vector3 vertex, Vector3 normal) + { + Color finalColor = Color.black; + + Light[] lights = Object.FindObjectsByType(FindObjectsSortMode.None); + + foreach (Light light in lights) + { + if (!light.enabled) + continue; + + Color lightContribution = Color.black; + + if (light.type == LightType.Directional) + { + Vector3 lightDir = -light.transform.forward; + float NdotL = Mathf.Max(0f, Vector3.Dot(normal, lightDir)); + lightContribution = light.color * light.intensity * NdotL; + } + else if (light.type == LightType.Point) + { + Vector3 lightDir = light.transform.position - vertex; + float distance = lightDir.magnitude; + lightDir.Normalize(); + + float NdotL = Mathf.Max(0f, Vector3.Dot(normal, lightDir)); + float attenuation = 1.0f / Mathf.Max(distance * distance, 0.0001f); + lightContribution = light.color * light.intensity * NdotL * attenuation; + } + else if (light.type == LightType.Spot) + { + Vector3 L = light.transform.position - vertex; + float distance = L.magnitude; + L = L / distance; + + float NdotL = Mathf.Max(0f, Vector3.Dot(normal, L)); + float attenuation = 1.0f / Mathf.Max(distance * distance, 0.0001f); + + float outerAngleRad = (light.spotAngle * 0.5f) * Mathf.Deg2Rad; + float innerAngleRad = outerAngleRad * 0.8f; + + if (light is Light spotLight) + { + if (spotLight.innerSpotAngle > 0) + { + innerAngleRad = (spotLight.innerSpotAngle * 0.5f) * Mathf.Deg2Rad; + } + } + + float cosOuter = Mathf.Cos(outerAngleRad); + float cosInner = Mathf.Cos(innerAngleRad); + float cosAngle = Vector3.Dot(L, -light.transform.forward); + + if (cosAngle >= cosOuter) + { + float spotFactor = Mathf.Clamp01((cosAngle - cosOuter) / (cosInner - cosOuter)); + spotFactor = Mathf.Pow(spotFactor, 4.0f); + + lightContribution = light.color * light.intensity * NdotL * attenuation * spotFactor; + } + else + { + lightContribution = Color.black; + } + } + + finalColor += lightContribution; + } + + 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); + finalColor.a = 1f; + + return finalColor; + } +} diff --git a/Runtime/PSXLightingBaker.cs.meta b/Runtime/PSXLightingBaker.cs.meta new file mode 100644 index 0000000..7493f55 --- /dev/null +++ b/Runtime/PSXLightingBaker.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b707b7d499862621fb6c82aba4caa183 \ No newline at end of file diff --git a/Runtime/PSXMesh.cs b/Runtime/PSXMesh.cs index f282fff..43f4d88 100644 --- a/Runtime/PSXMesh.cs +++ b/Runtime/PSXMesh.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using UnityEngine; using UnityEngine.Diagnostics; @@ -27,6 +28,8 @@ namespace SplashEdit.RuntimeCode public PSXVertex v0; public PSXVertex v1; public PSXVertex v2; + + public PSXTexture2D Texture; } /// @@ -37,6 +40,40 @@ namespace SplashEdit.RuntimeCode { public List Triangles; + private static Vector3[] RecalculateSmoothNormals(Mesh mesh) + { + Vector3[] normals = new Vector3[mesh.vertexCount]; + Dictionary> vertexMap = new Dictionary>(); + + for (int i = 0; i < mesh.vertexCount; i++) + { + Vector3 vertex = mesh.vertices[i]; + if (!vertexMap.ContainsKey(vertex)) + { + vertexMap[vertex] = new List(); + } + vertexMap[vertex].Add(i); + } + + foreach (var kvp in vertexMap) + { + Vector3 smoothNormal = Vector3.zero; + foreach (int index in kvp.Value) + { + smoothNormal += mesh.normals[index]; + } + smoothNormal.Normalize(); + + foreach (int index in kvp.Value) + { + normals[index] = smoothNormal; + } + } + + return normals; + } + + /// /// Creates a PSXMesh from a Unity Mesh by converting its vertices, normals, UVs, and applying shading. /// @@ -45,57 +82,99 @@ namespace SplashEdit.RuntimeCode /// Height of the texture (default is 256). /// Optional transform to convert vertices to world space. /// A new PSXMesh containing the converted triangles. - public static PSXMesh CreateFromUnityMesh(Mesh mesh, float GTEScaling, Transform transform, bool isStatic, int textureWidth = 256, int textureHeight = 256) + public static PSXMesh CreateFromUnityRenderer(Renderer renderer, float GTEScaling, Transform transform, List textures) { PSXMesh psxMesh = new PSXMesh { Triangles = new List() }; - // Get mesh data arrays. - Vector3[] vertices = mesh.vertices; - Vector3[] normals = mesh.normals; - Vector2[] uv = mesh.uv; - int[] indices = mesh.triangles; + // Get materials and mesh. + Material[] materials = renderer.sharedMaterials; + Mesh mesh = renderer.GetComponent().sharedMesh; - // Determine the primary light's direction and color for shading. - Light mainLight = RenderSettings.sun; - Vector3 lightDir = mainLight ? mainLight.transform.forward : Vector3.down; // Fixed: Removed negation. - Color lightColor = mainLight ? mainLight.color * mainLight.intensity : Color.white; - - // Iterate over each triangle (group of 3 indices). - for (int i = 0; i < indices.Length; i += 3) + // Iterate over each submesh. + for (int submeshIndex = 0; submeshIndex < materials.Length; submeshIndex++) { - int vid0 = indices[i]; - int vid1 = indices[i + 1]; - int vid2 = indices[i + 2]; + // Get the triangles for this submesh. + int[] submeshTriangles = mesh.GetTriangles(submeshIndex); - Vector3 v0, v1, v2; + // Get the material for this submesh. + Material material = materials[submeshIndex]; - // Transform vertices to world space if a transform is provided. + // Get the corresponding texture for this material (assume mainTexture). + Texture2D texture = material.mainTexture as Texture2D; + PSXTexture2D psxTexture = null; - if (isStatic) + if (texture != null) { - v0 = transform.TransformPoint(vertices[vid0]); - v1 = transform.TransformPoint(vertices[vid1]); - v2 = transform.TransformPoint(vertices[vid2]); - } - else - { - // Extract ONLY world scale - Vector3 worldScale = transform.lossyScale; - - // Apply scale *before* transformation, ensuring rotation isn’t affected - v0 = Vector3.Scale(vertices[vid0], worldScale); - v1 = Vector3.Scale(vertices[vid1], worldScale); - v2 = Vector3.Scale(vertices[vid2], worldScale); - + // Find the corresponding PSX texture based on the Unity texture. + psxTexture = textures.FirstOrDefault(t => t.OriginalTexture == texture); } - // Convert vertices to PSX format including fixed-point conversion and shading. - PSXVertex psxV0 = ConvertToPSXVertex(v0, GTEScaling, normals[vid0], uv[vid0], lightDir, lightColor, textureWidth, textureHeight); - PSXVertex psxV1 = ConvertToPSXVertex(v1, GTEScaling, normals[vid1], uv[vid1], lightDir, lightColor, textureWidth, textureHeight); - PSXVertex psxV2 = ConvertToPSXVertex(v2, GTEScaling, normals[vid2], uv[vid2], lightDir, lightColor, textureWidth, textureHeight); + if (psxTexture == null) + { + continue; + } - // Add the constructed triangle to the mesh. - psxMesh.Triangles.Add(new Tri { v0 = psxV0, v1 = psxV1, v2 = psxV2 }); + // Get mesh data arrays. + Vector3[] vertices = mesh.vertices; + Vector3[] normals = mesh.normals;// Assuming this function recalculates normals + Vector3[] smoothNormals = RecalculateSmoothNormals(mesh); + Vector2[] uv = mesh.uv; + + // Iterate through the triangles of the submesh. + for (int i = 0; i < submeshTriangles.Length; i += 3) + { + int vid0 = submeshTriangles[i]; + int vid1 = submeshTriangles[i + 1]; + int vid2 = submeshTriangles[i + 2]; + + Vector3 faceNormal = Vector3.Cross(vertices[vid1] - vertices[vid0], vertices[vid2] - vertices[vid0]).normalized; + + if (Vector3.Dot(faceNormal, normals[vid0]) < 0) + { + (vid1, vid2) = (vid2, vid1); + } + + // Scale the vertices based on world scale. + Vector3 v0 = Vector3.Scale(vertices[vid0], transform.lossyScale); + Vector3 v1 = Vector3.Scale(vertices[vid1], transform.lossyScale); + Vector3 v2 = Vector3.Scale(vertices[vid2], transform.lossyScale); + + // Transform the vertices to world space. + Vector3 wv0 = transform.TransformPoint(vertices[vid0]); + Vector3 wv1 = transform.TransformPoint(vertices[vid1]); + Vector3 wv2 = transform.TransformPoint(vertices[vid2]); + + // Transform the normals to world space. + Vector3 wn0 = transform.TransformDirection(smoothNormals[vid0]).normalized; + Vector3 wn1 = transform.TransformDirection(smoothNormals[vid1]).normalized; + Vector3 wn2 = transform.TransformDirection(smoothNormals[vid2]).normalized; + + // Compute lighting for each vertex (this can be a custom function). + Color cv0 = PSXLightingBaker.ComputeLighting(wv0, wn0); + Color cv1 = PSXLightingBaker.ComputeLighting(wv1, wn1); + Color cv2 = PSXLightingBaker.ComputeLighting(wv2, wn2); + + // Convert vertices to PSX format, including fixed-point conversion and shading. + PSXVertex psxV0 = ConvertToPSXVertex(v0, GTEScaling, normals[vid0], uv[vid0], psxTexture?.Width ?? 0, psxTexture?.Height ?? 0); + PSXVertex psxV1 = ConvertToPSXVertex(v1, GTEScaling, normals[vid1], uv[vid1], psxTexture?.Width ?? 0, psxTexture?.Height ?? 0); + PSXVertex psxV2 = ConvertToPSXVertex(v2, GTEScaling, normals[vid2], uv[vid2], psxTexture?.Width ?? 0, psxTexture?.Height ?? 0); + + // Apply lighting to the colors. + psxV0.r = (byte)Mathf.Clamp(cv0.r * 255, 0, 255); + psxV0.g = (byte)Mathf.Clamp(cv0.g * 255, 0, 255); + psxV0.b = (byte)Mathf.Clamp(cv0.b * 255, 0, 255); + + psxV1.r = (byte)Mathf.Clamp(cv1.r * 255, 0, 255); + psxV1.g = (byte)Mathf.Clamp(cv1.g * 255, 0, 255); + psxV1.b = (byte)Mathf.Clamp(cv1.b * 255, 0, 255); + + psxV2.r = (byte)Mathf.Clamp(cv2.r * 255, 0, 255); + psxV2.g = (byte)Mathf.Clamp(cv2.g * 255, 0, 255); + psxV2.b = (byte)Mathf.Clamp(cv2.b * 255, 0, 255); + + // Add the constructed triangle to the mesh. + psxMesh.Triangles.Add(new Tri { v0 = psxV0, v1 = psxV1, v2 = psxV2, Texture = psxTexture }); + } } return psxMesh; @@ -112,13 +191,9 @@ namespace SplashEdit.RuntimeCode /// Width of the texture for UV scaling. /// Height of the texture for UV scaling. /// A PSXVertex with converted coordinates, normals, UVs, and color. - private static PSXVertex ConvertToPSXVertex(Vector3 vertex, float GTEScaling, Vector3 normal, Vector2 uv, Vector3 lightDir, Color lightColor, int textureWidth, int textureHeight) + private static PSXVertex ConvertToPSXVertex(Vector3 vertex, float GTEScaling, Vector3 normal, Vector2 uv, int textureWidth, int textureHeight) { - // Calculate light intensity based on the angle between the normalized normal and light direction. - float lightIntensity = Mathf.Clamp01(Vector3.Dot(normal.normalized, lightDir)); - // Compute the final shaded color by multiplying the light color by the intensity. - Color shadedColor = lightColor * lightIntensity; PSXVertex psxVertex = new PSXVertex { @@ -137,9 +212,7 @@ namespace SplashEdit.RuntimeCode v = (byte)Mathf.Clamp((1.0f - uv.y) * (textureHeight - 1), 0, 255), // Convert the computed color to a byte range. - r = (byte)Mathf.Clamp(shadedColor.r * 255, 0, 255), - g = (byte)Mathf.Clamp(shadedColor.g * 255, 0, 255), - b = (byte)Mathf.Clamp(shadedColor.b * 255, 0, 255) + }; return psxVertex; diff --git a/Runtime/PSXObjectExporter.cs b/Runtime/PSXObjectExporter.cs index f4b343f..e5a5826 100644 --- a/Runtime/PSXObjectExporter.cs +++ b/Runtime/PSXObjectExporter.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using UnityEngine; namespace SplashEdit.RuntimeCode @@ -5,36 +6,111 @@ namespace SplashEdit.RuntimeCode public class PSXObjectExporter : MonoBehaviour { public PSXBPP BitDepth = PSXBPP.TEX_8BIT; // Defines the bit depth of the texture (e.g., 4BPP, 8BPP) - public bool MeshIsStatic = true; // Determines if the mesh is static, affecting how it's processed. Non-static meshes don't export correctly as of now. [HideInInspector] - public PSXTexture2D Texture; // Stores the converted PlayStation-style texture + public List Textures = new List(); // Stores the converted PlayStation-style texture [HideInInspector] public PSXMesh Mesh; // Stores the converted PlayStation-style mesh - /// - /// Converts the object's material texture into a PlayStation-compatible texture. - /// - public void CreatePSXTexture2D() + public bool PreviewNormals = false; + public float normalPreviewLength = 0.5f; // Length of the normal lines + + private void OnDrawGizmos() { - Renderer renderer = GetComponent(); - if (renderer != null && renderer.sharedMaterial != null && renderer.sharedMaterial.mainTexture is Texture2D texture) + + if (PreviewNormals) { - Texture = PSXTexture2D.CreateFromTexture2D(texture, BitDepth); - Texture.OriginalTexture = texture; // Stores reference to the original texture + MeshFilter filter = GetComponent(); + + if (filter != null) + { + + Mesh mesh = filter.sharedMesh; + Vector3[] vertices = mesh.vertices; + Vector3[] normals = mesh.normals; + + Gizmos.color = Color.green; // Normal color + + for (int i = 0; i < vertices.Length; i++) + { + Vector3 worldVertex = transform.TransformPoint(vertices[i]); // Convert to world space + Vector3 worldNormal = transform.TransformDirection(normals[i]); // Transform normal to world space + + Gizmos.DrawLine(worldVertex, worldVertex + worldNormal * normalPreviewLength); + } + } + } } + + /// + /// Converts the object's material texture into a PlayStation-compatible texture. + /// + /// + public void CreatePSXTextures2D() + { + Renderer renderer = GetComponent(); + if (renderer != null) + { + Material[] materials = renderer.sharedMaterials; + Textures = new List(); // Ensure the list is initialized + + foreach (Material mat in materials) + { + if (mat != null && mat.mainTexture != null) + { + Texture mainTexture = mat.mainTexture; + Texture2D tex2D = null; + + // Check if it's already a Texture2D + if (mainTexture is Texture2D existingTex2D) + { + tex2D = existingTex2D; + } + else + { + // If not a Texture2D, try to convert + tex2D = ConvertToTexture2D(mainTexture); + } + + if (tex2D != null) + { + PSXTexture2D tex = PSXTexture2D.CreateFromTexture2D(tex2D, BitDepth); + tex.OriginalTexture = tex2D; // Store reference to the original texture + Textures.Add(tex); + } + } + } + } + } + + private Texture2D ConvertToTexture2D(Texture texture) + { + // Create a new Texture2D with the same dimensions and format + Texture2D texture2D = new Texture2D(texture.width, texture.height, TextureFormat.RGBA32, false); + + // Read the texture pixels + RenderTexture currentActiveRT = RenderTexture.active; + RenderTexture.active = texture as RenderTexture; + + texture2D.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0); + texture2D.Apply(); + + RenderTexture.active = currentActiveRT; + + return texture2D; + } /// /// Converts the object's mesh into a PlayStation-compatible mesh. /// public void CreatePSXMesh(float GTEScaling) { - MeshFilter meshFilter = gameObject.GetComponent(); - if (meshFilter != null) + Renderer renderer = GetComponent(); + if (renderer != null) { - Mesh = PSXMesh.CreateFromUnityMesh(meshFilter.sharedMesh, GTEScaling, transform, MeshIsStatic, Texture.Width, Texture.Height); + Mesh = PSXMesh.CreateFromUnityRenderer(renderer, GTEScaling, transform, Textures); } } } diff --git a/Runtime/PSXSceneExporter.cs b/Runtime/PSXSceneExporter.cs index 1808e88..9c667b3 100644 --- a/Runtime/PSXSceneExporter.cs +++ b/Runtime/PSXSceneExporter.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using UnityEditor; using UnityEngine; @@ -33,7 +34,7 @@ namespace SplashEdit.RuntimeCode _exporters = FindObjectsByType(FindObjectsSortMode.None); foreach (PSXObjectExporter exp in _exporters) { - exp.CreatePSXTexture2D(); + exp.CreatePSXTextures2D(); exp.CreatePSXMesh(GTEScaling); } PackTextures(); @@ -60,34 +61,6 @@ namespace SplashEdit.RuntimeCode } - public static string PSXMatrixToStringMultiline(int[,] matrix) - { - return $@" -RT11={matrix[0, 0],6} RT12={matrix[0, 1],6} RT13={matrix[0, 2],6} -RT21={matrix[1, 0],6} RT22={matrix[1, 1],6} RT23={matrix[1, 2],6} -RT31={matrix[2, 0],6} RT32={matrix[2, 1],6} RT33={matrix[2, 2],6}"; - } - - public static Vector3 ConvertPSXMatrixToEulerAngles(int[,] psxMatrix) - { - // Convert PSX fixed-point (s3.12) to float - float r00 = psxMatrix[0, 0] / 4096.0f; - float r01 = psxMatrix[0, 1] / 4096.0f; - float r02 = psxMatrix[0, 2] / 4096.0f; - float r10 = psxMatrix[1, 0] / 4096.0f; - float r11 = psxMatrix[1, 1] / 4096.0f; - float r12 = psxMatrix[1, 2] / 4096.0f; - float r20 = psxMatrix[2, 0] / 4096.0f; - float r21 = psxMatrix[2, 1] / 4096.0f; - float r22 = psxMatrix[2, 2] / 4096.0f; - - // Compute Euler angles (YXZ order for Unity) - float thetaX = Mathf.Asin(-r21) * Mathf.Rad2Deg; // X Rotation - float thetaY = Mathf.Atan2(r20, r22) * Mathf.Rad2Deg; // Y Rotation - float thetaZ = Mathf.Atan2(r01, r11) * Mathf.Rad2Deg; // Z Rotation - - return new Vector3(thetaX, thetaY, thetaZ); - } void ExportFile() { @@ -102,6 +75,19 @@ RT31={matrix[2, 0],6} RT32={matrix[2, 1],6} RT33={matrix[2, 2],6}"; List atlasOffsetPlaceholderPositions = new List(); List atlasDataOffsets = new List(); + int clutCount = 0; + + // Cluts + foreach (TextureAtlas atlas in _atlases) + { + foreach (var texture in atlas.ContainedTextures) + { + if (texture.ColorPalette != null) + { + clutCount++; + } + } + } using (BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create))) { @@ -111,6 +97,8 @@ RT31={matrix[2, 0],6} RT32={matrix[2, 1],6} RT33={matrix[2, 2],6}"; writer.Write((ushort)1); writer.Write((ushort)_exporters.Length); writer.Write((ushort)_atlases.Length); + writer.Write((ushort)clutCount); + writer.Write((ushort)0); // Start of Metadata section // GameObject section (exporters) @@ -132,51 +120,12 @@ RT31={matrix[2, 0],6} RT32={matrix[2, 1],6} RT33={matrix[2, 2],6}"; writer.Write((int)rotationMatrix[2, 1]); writer.Write((int)rotationMatrix[2, 2]); - writer.Write((ushort)exporter.Mesh.Triangles.Count); - - // Set up texture page attributes - TPageAttr tpage = new TPageAttr(); - tpage.SetPageX(exporter.Texture.TexpageX); - tpage.SetPageY(exporter.Texture.TexpageY); - switch (exporter.Texture.BitDepth) - { - case PSXBPP.TEX_4BIT: - tpage.Set(TPageAttr.ColorMode.Mode4Bit); - break; - case PSXBPP.TEX_8BIT: - tpage.Set(TPageAttr.ColorMode.Mode8Bit); - break; - case PSXBPP.TEX_16BIT: - tpage.Set(TPageAttr.ColorMode.Mode16Bit); - break; - } - tpage.SetDithering(true); - writer.Write((ushort)tpage.info); - writer.Write((ushort)exporter.Texture.ClutPackingX); - writer.Write((ushort)exporter.Texture.ClutPackingY); - if (exporter.Texture.BitDepth != PSXBPP.TEX_16BIT) - { - foreach (VRAMPixel color in exporter.Texture.ColorPalette) - { - writer.Write((ushort)color.Pack()); - } - for (int i = exporter.Texture.ColorPalette.Count; i < 256; i++) - { - writer.Write((ushort)0); - } - } - else - { - for (int i = 0; i < 256; i++) - { - writer.Write((ushort)0); - } - } - // 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. + + writer.Write((int)exporter.Mesh.Triangles.Count); } // Atlas metadata section @@ -192,20 +141,45 @@ RT31={matrix[2, 0],6} RT32={matrix[2, 1],6} RT33={matrix[2, 2],6}"; writer.Write((ushort)atlas.PositionY); } + // Cluts + foreach (TextureAtlas atlas in _atlases) + { + foreach (var texture in atlas.ContainedTextures) + { + if (texture.ColorPalette != null) + { + foreach (VRAMPixel clutPixel in texture.ColorPalette) + { + writer.Write((ushort)clutPixel.Pack()); + } + for (int i = texture.ColorPalette.Count; i < 256; i++) + { + writer.Write((ushort)0); + } + writer.Write((ushort)texture.ClutPackingX); + writer.Write((ushort)texture.ClutPackingY); + writer.Write((ushort)texture.ColorPalette.Count); + writer.Write((ushort)0); + } + } + } + // Start of data section // Mesh data section: Write mesh data for each exporter. foreach (PSXObjectExporter exporter in _exporters) { + AlignToFourBytes(writer); // Record the current offset for this exporter's mesh data. long meshDataOffset = writer.BaseStream.Position; meshDataOffsets.Add(meshDataOffset); totalFaces += exporter.Mesh.Triangles.Count; - int expander = 16 / ((int)exporter.Texture.BitDepth); + foreach (Tri tri in exporter.Mesh.Triangles) { + int expander = 16 / ((int)tri.Texture.BitDepth); // Write vertices coordinates writer.Write((short)tri.v0.vx); writer.Write((short)tri.v0.vy); @@ -224,18 +198,6 @@ RT31={matrix[2, 0],6} RT32={matrix[2, 1],6} RT33={matrix[2, 2],6}"; 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((ushort)0); // padding - // Write vertex colors with padding writer.Write((byte)tri.v0.r); writer.Write((byte)tri.v0.g); @@ -251,6 +213,40 @@ RT31={matrix[2, 0],6} RT32={matrix[2, 1],6} RT33={matrix[2, 2],6}"; writer.Write((byte)tri.v2.g); writer.Write((byte)tri.v2.b); writer.Write((byte)0); // padding + + // Write UVs for each vertex, adjusting for texture packing + writer.Write((byte)(tri.v0.u + tri.Texture.PackingX * expander)); + writer.Write((byte)(tri.v0.v + tri.Texture.PackingY)); + + writer.Write((byte)(tri.v1.u + tri.Texture.PackingX * expander)); + writer.Write((byte)(tri.v1.v + tri.Texture.PackingY)); + + writer.Write((byte)(tri.v2.u + tri.Texture.PackingX * expander)); + writer.Write((byte)(tri.v2.v + tri.Texture.PackingY)); + + writer.Write((ushort)0); // padding + + + TPageAttr tpage = new TPageAttr(); + tpage.SetPageX(tri.Texture.TexpageX); + tpage.SetPageY(tri.Texture.TexpageY); + switch (tri.Texture.BitDepth) + { + case PSXBPP.TEX_4BIT: + tpage.Set(TPageAttr.ColorMode.Mode4Bit); + break; + case PSXBPP.TEX_8BIT: + tpage.Set(TPageAttr.ColorMode.Mode8Bit); + break; + case PSXBPP.TEX_16BIT: + tpage.Set(TPageAttr.ColorMode.Mode16Bit); + break; + } + tpage.SetDithering(true); + writer.Write((ushort)tpage.info); + writer.Write((ushort)tri.Texture.ClutPackingX); + writer.Write((ushort)tri.Texture.ClutPackingY); + writer.Write((ushort)0); } } @@ -309,5 +305,18 @@ RT31={matrix[2, 0],6} RT32={matrix[2, 1],6} RT33={matrix[2, 2],6}"; int padding = (int)(4 - (position % 4)) % 4; // Compute needed padding writer.Write(new byte[padding]); // Write zero padding } + + void OnDrawGizmos() + + { + + Gizmos.DrawIcon(transform.position, "Packages/net.psxsplash.splashedit/Icons/PSXSceneExporter.png", true); + Vector3 sceneOrigin = new Vector3(0, 0, 0); + Vector3 cubeSize = new Vector3(8.0f * GTEScaling, 8.0f * GTEScaling, 8.0f * GTEScaling); + Gizmos.color = Color.red; + Gizmos.DrawWireCube(sceneOrigin, cubeSize); + + } + } } diff --git a/Runtime/TexturePacker.cs b/Runtime/TexturePacker.cs index 25be041..f302a55 100644 --- a/Runtime/TexturePacker.cs +++ b/Runtime/TexturePacker.cs @@ -59,17 +59,31 @@ namespace SplashEdit.RuntimeCode /// /// Packs the textures from the provided PSXObjectExporter array into VRAM. + /// Each exporter now holds a list of textures. + /// Duplicates (textures with the same underlying OriginalTexture and BitDepth) across all exporters are merged. /// Returns the processed objects and the final VRAM pixel array. /// /// Array of PSXObjectExporter objects to process. - /// Tuple containing processed objects and the VRAM pixel array. - public (PSXObjectExporter[] processedObjects, TextureAtlas[] atlases, VRAMPixel[,] _vramPixels) PackTexturesIntoVRAM(PSXObjectExporter[] objects) + /// Tuple containing processed objects, texture atlases, and the VRAM pixel array. + public (PSXObjectExporter[] processedObjects, TextureAtlas[] atlases, VRAMPixel[,] vramPixels) PackTexturesIntoVRAM(PSXObjectExporter[] objects) { - List uniqueTextures = new List(); - // Group objects by texture bit depth (high to low). - var groupedObjects = objects.GroupBy(obj => obj.Texture.BitDepth).OrderByDescending(g => g.Key); + // Gather all textures from all exporters. + List allTextures = new List(); + foreach (var obj in objects) + { + allTextures.AddRange(obj.Textures); + } - foreach (var group in groupedObjects) + // List to track unique textures. + List uniqueTextures = new List(); + + // Group textures by bit depth (highest first). + var texturesByBitDepth = allTextures + .GroupBy(tex => tex.BitDepth) + .OrderByDescending(g => g.Key); + + // Process each group. + foreach (var group in texturesByBitDepth) { // Determine atlas width based on texture bit depth. int atlasWidth = group.Key switch @@ -80,33 +94,48 @@ namespace SplashEdit.RuntimeCode _ => 256 }; - // Create a new atlas for this group. + // Create an initial atlas for this group. TextureAtlas atlas = new TextureAtlas { BitDepth = group.Key, Width = atlasWidth, PositionX = 0, PositionY = 0 }; _textureAtlases.Add(atlas); - // Process each texture in descending order of area (width * height). - foreach (var obj in group.OrderByDescending(obj => obj.Texture.QuantizedWidth * obj.Texture.Height)) + // Process each texture in descending order of area. + foreach (var texture in group.OrderByDescending(tex => tex.QuantizedWidth * tex.Height)) { - // Remove duplicate textures - /*if (uniqueTextures.Any(tex => tex.OriginalTexture.GetInstanceID() == obj.Texture.OriginalTexture.GetInstanceID() && tex.BitDepth == obj.Texture.BitDepth)) + // Check for duplicates in the entire set. + if (uniqueTextures.Any(tex => tex.OriginalTexture.GetInstanceID() == texture.OriginalTexture.GetInstanceID() && + tex.BitDepth == texture.BitDepth)) { - obj.Texture = uniqueTextures.First(tex => tex.OriginalTexture.GetInstanceID() == obj.Texture.OriginalTexture.GetInstanceID()); + // Skip packing this texture – it will be replaced later. continue; - }*/ + } // Try to place the texture in the current atlas. - if (!TryPlaceTextureInAtlas(atlas, obj.Texture)) + if (!TryPlaceTextureInAtlas(atlas, texture)) { - // If failed, create a new atlas and try again. + // If failed, create a new atlas for this bit depth group and try again. atlas = new TextureAtlas { BitDepth = group.Key, Width = atlasWidth, PositionX = 0, PositionY = 0 }; _textureAtlases.Add(atlas); - if (!TryPlaceTextureInAtlas(atlas, obj.Texture)) + if (!TryPlaceTextureInAtlas(atlas, texture)) { - Debug.LogError($"Failed to pack texture {obj.Texture}. It might not fit."); - break; + Debug.LogError($"Failed to pack texture {texture}. It might not fit."); + continue; } } - uniqueTextures.Add(obj.Texture); + uniqueTextures.Add(texture); + } + } + + // Now update every exporter so that duplicate textures reference the unique instance. + foreach (var obj in objects) + { + for (int i = 0; i < obj.Textures.Count; i++) + { + var unique = uniqueTextures.FirstOrDefault(tex => tex.OriginalTexture.GetInstanceID() == obj.Textures[i].OriginalTexture.GetInstanceID() && + tex.BitDepth == obj.Textures[i].BitDepth); + if (unique != null) + { + obj.Textures[i] = unique; + } } } @@ -114,7 +143,6 @@ namespace SplashEdit.RuntimeCode ArrangeAtlasesInVRAM(); // Allocate color lookup tables (CLUTs) for textures that use palettes. AllocateCLUTs(); - // Build the final VRAM pixel array from placed textures and CLUTs. BuildVram(); return (objects, _finalizedAtlases.ToArray(), _vramPixels); @@ -227,7 +255,7 @@ namespace SplashEdit.RuntimeCode if (IsPlacementValid(candidate)) { _allocatedCLUTs.Add(candidate); - texture.ClutPackingX = (ushort)(x/16); + texture.ClutPackingX = (ushort)(x / 16); texture.ClutPackingY = y; placed = true; break; @@ -254,7 +282,6 @@ namespace SplashEdit.RuntimeCode 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++) { @@ -298,6 +325,5 @@ namespace SplashEdit.RuntimeCode return !(overlapsAtlas || overlapsReserved || overlapsCLUT); } - } } diff --git a/Runtime/Utils.cs b/Runtime/Utils.cs index 26f1c53..6800391 100644 --- a/Runtime/Utils.cs +++ b/Runtime/Utils.cs @@ -100,18 +100,34 @@ namespace SplashEdit.RuntimeCode /// A 3x3 matrix representing the PSX-compatible rotation. public static int[,] ConvertRotationToPSXMatrix(Quaternion rotation) { - // Convert the quaternion to a Unity rotation matrix. - Matrix4x4 unityMatrix = Matrix4x4.Rotate(rotation); + // Standard quaternion-to-matrix conversion. + float x = rotation.x, y = rotation.y, z = rotation.z, w = rotation.w; - // Flip the Y-axis to match PSX's Y-down convention. + float m00 = 1f - 2f * (y * y + z * z); + float m01 = 2f * (x * y - z * w); + float m02 = 2f * (x * z + y * w); + + float m10 = 2f * (x * y + z * w); + float m11 = 1f - 2f * (x * x + z * z); + float m12 = 2f * (y * z - x * w); + + float m20 = 2f * (x * z - y * w); + float m21 = 2f * (y * z + x * w); + float m22 = 1f - 2f * (x * x + y * y); + + // Apply Y-axis flip to match the PSX's Y-down convention. + // This replicates the behavior of: + // { m00, -m01, m02 }, + // { -m10, m11, -m12 }, + // { m20, -m21, m22 } float[,] fixedMatrix = new float[3, 3] { - { unityMatrix.m00, -unityMatrix.m01, unityMatrix.m02 }, // Flip Y - { -unityMatrix.m10, unityMatrix.m11, -unityMatrix.m12 }, // Flip Y - { unityMatrix.m20, -unityMatrix.m21, unityMatrix.m22 } // Flip Y + { m00, -m01, m02 }, + { -m10, m11, -m12 }, + { m20, -m21, m22 } }; - // Convert the Unity matrix to PSX fixed-point format. + // Convert to PSX fixed-point format. int[,] psxMatrix = new int[3, 3]; for (int i = 0; i < 3; i++) { diff --git a/doc/splashbundle.md b/doc/splashbundle.md index d7bacf5..e91cc8a 100644 --- a/doc/splashbundle.md +++ b/doc/splashbundle.md @@ -1,87 +1,93 @@ # SPLASHPACK Binary File Format Specification -This specification describes the binary file layout for the SP exporter. All numeric values are stored in little‐endian format. All offsets are counted from the beginning of the file. +All numeric values are stored in little‐endian format. All offsets are counted from the beginning of the file. --- -## 1. File Header +## 1. File Header (12 bytes) -| Offset | Size | Type | Description | -| ------- | ---- | ------ | --------------------------------------------------------------- | -| 0x00 | 1 | char | `'S'` – File identifier | -| 0x01 | 1 | char | `'P'` – File identifier | -| 0x02 | 2 | uint16 | Version number (currently **1**) | -| 0x04 | 2 | uint16 | Number of GameObject (exporter) descriptors | -| 0x06 | 2 | uint16 | Number of Texture Atlas descriptors | +| Offset | Size | Type | Description | +| ------ | ---- | ------ | ----------------------------------- | +| 0x00 | 2 | char | `'SP'` – File magic | +| 0x02 | 2 | uint16 | Version number (currently **1**) | +| 0x04 | 2 | uint16 | Number of Exporter descriptors | +| 0x06 | 2 | uint16 | Number of Texture Atlas descriptors | +| 0x08 | 2 | uint16 | Number of CLUT descriptors | +| 0x0A | 2 | uint16 | Reserved (always 0) | --- ## 2. Metadata Section -The metadata section is split into two parts: **Object Descriptors** and **Atlas Descriptors**. +The metadata section comprises three groups of descriptors. -### 2.1 Object (Exporter) Descriptors +### 2.1 Exporter Descriptors (56 bytes each) -| Offset (per entry) | Size | Type | Description | -| ------------------ | -------- | ------- | -------------------------------------------------------------------- | -| 0x00 | 4 | int | X coordinate (GTE-converted) | -| 0x04 | 4 | int | Y coordinate (GTE-converted) | -| 0x08 | 4 | int | Z coordinate (GTE-converted) | -| 0x0C | 36 | int[9] | Rotation matrix (3×3, row-major order) | -| 0x30 | 2 | uint16 | Number of triangles in the mesh | -| 0x32 | 2 | uint16 | Texture page attributes (encoded from page X/Y, bit depth, dithering) | -| 0x34 | 2 | uint16 | CLUT packing X coordinate | -| 0x36 | 2 | uint16 | CLUT packing Y coordinate | -| 0x38 | 512 | uint16[256] | Color palette (filled with zeros if 16-bit textures) | -| 0x438 | 4 | int | Mesh data offset placeholder | +Each exporter descriptor stores the transform and mesh metadata for one GameObject. -*Each object descriptor occupies **0x38** bytes.* +| Offset (per entry) | Size | Type | Description | +| ------------------ | ---- | ------ | --------------------------------- | +| 0x00 | 4 | int | X coordinate (Fixed-point) | +| 0x04 | 4 | int | Y coordinate (Fixed-point) | +| 0x08 | 4 | int | Z coordinate (Fixed-point) | +| 0x0C | 36 | int[9] | 3×3 Rotation matrix (Fixed-point) | +| 0x30 | 4 | int | **Mesh Data Offset Placeholder** | +| 0x34 | 4 | int | Triangle count in the mesh | ---- +### 2.2 Texture Atlas Descriptors (12 bytes each) -### 2.2 Texture Atlas Descriptors +Each texture atlas descriptor holds atlas layout data and a placeholder for the atlas raw data offset. -For each texture atlas, the following fields are stored sequentially: +| Offset (per entry) | Size | Type | Description | +| ------------------ | ---- | ------ | -------------------------------------------------------- | +| 0x00 | 4 | int | **Atlas Data Offset Placeholder** | +| 0x04 | 2 | uint16 | Atlas width | +| 0x06 | 2 | uint16 | Atlas height (currently always 256, for future-proofing) | +| 0x08 | 2 | uint16 | Atlas position X – relative to VRAM origin | +| 0x0A | 2 | uint16 | Atlas position Y – relative to VRAM origin | -| Offset (per entry) | Size | Type | Description | -| ------------------ | ---- | ------ | -------------------------------------------------------------- | -| 0x00 | 4 | int | Atlas raw data offset placeholder | -| 0x04 | 2 | uint16 | Atlas width | -| 0x06 | 2 | uint16 | Atlas height (always 256, but defined for future-proofing) | -| 0x08 | 2 | uint16 | Atlas position X – relative to VRAM origin | -| 0x0A | 2 | uint16 | Atlas position Y – relative to VRAM origin | +### 2.3 CLUT Descriptors (520 bytes each) -*Each atlas descriptor occupies **0x0C** bytes.* +CLUTs are the only data which is stored in the Metadata section. +For each CLUT (Color Lookup Table) associated with an atlas texture that has a palette: + +| Offset (per entry) | Size | Type | Description | +| ------------------ | ---- | ----------- | --------------------------------------------------------------------------------- | +| 0x00 | 512 | uint16[256] | Color palette entries (each entry is 2 bytes 16bpp). If unused, entries are zero. | +| 0x200 | 2 | uint16 | CLUT packing X coordinate - already in 16 pixel steps | +| 0x202 | 2 | uint16 | CLUT packing Y coordinate | +| 0x204 | 2 | uint16 | Palette count (number of valid palette entries) | +| 0x206 | 2 | uint16 | Reserved (always 0) | --- ## 3. Data Section -The data section is composed of two distinct blocks: **Mesh Data Blocks** and **Atlas Data Blocks**. +The data section contains the actual mesh and atlas raw data. ### 3.1 Mesh Data Blocks -For each exporter, a mesh data block is written. The starting offset of each block (stored in its corresponding object descriptor) is counted from the beginning of the file. Within each mesh data block, data for every triangle is stored sequentially using the following layout: +For each exporter, a mesh data block is written at the offset specified in its descriptor. Each mesh block contains data for all triangles of the associated mesh. -| Field | Size per element | Type | Description | -| ----------------------------- | ----------------------- | -------- | ---------------------------------------------------------------------------------------- | -| **Vertex Coordinates** | 3 × 2 bytes per vertex | int16 | For each vertex (v0, v1, v2): X, Y, Z coordinates | -| **Vertex Normal** | 3 × 2 bytes | int16 | For vertex v0 only: Normal vector components (nx, ny, nz) | -| **Texture Coordinates (UVs)** | 1 + 1 bytes per vertex | uint8 | For each vertex (v0, v1, v2): U and V coordinates (adjusted by texture packing factors) | -| **UV Padding** | 2 bytes | uint16 | Padding (set to zero) | -| **Vertex Colors** | 3 + 1 bytes per vertex | uint8 | For each vertex (v0, v1, v2): Color channels (red, green, blue) and 1 byte of padding | +#### **Triangle Data Layout (per triangle – 52 bytes total):** -*The overall size per triangle is calculated based on the fixed sizes above multiplied by the number of vertices and triangles.* +| Field | Size | Description | +| ----------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Vertex Coordinates** | 3 vertices × 3 × 2 bytes = 18 bytes | For each vertex (v0, v1, v2): X, Y, Z coordinates (int16) | +| **Vertex Normal** | 3 × 2 bytes = 6 bytes | Normal vector for vertex v0 (int16: nx, ny, nz) | +| **Vertex Colors** | 3 vertices × (3 bytes color + 1 byte padding) = 12 bytes | For each vertex (v0, v1, v2): Red, Green, Blue (uint8) plus 1 byte padding | +| **Texture Coordinates (UVs)** | 3 vertices × 2 bytes = 6 bytes | For each vertex (v0, v1, v2): U and V coordinates (uint8), adjusted by texture packing factors | +| **UV Padding** | 2 bytes | Padding (uint16, set to zero) | +| **Texture Attributes** | 2 + 2 + 2 + 2 = 8 bytes | Contains:
• Texture page attributes (uint16 – encoded from page X/Y, bit depth, dithering)
• Texture CLUT packing X (uint16)
• Texture CLUT packing Y (uint16)
• Reserved (uint16, set to zero) | ---- +*Triangles are written sequentially. Prior to writing each mesh data block, the file pointer is aligned to a 4-byte boundary.* ### 3.2 Atlas Data Blocks -For each texture atlas, the raw texture data is stored as a 2D array. Before writing each atlas data block, the file pointer is aligned to a 4-byte boundary. The starting offset of each atlas block (stored in its corresponding atlas descriptor) is counted from the beginning of the file. +For each atlas, a raw texture data block is written at the offset specified in its descriptor. Before writing, the file pointer is aligned to a 4-byte boundary. -| Field | Description | -| ------------- | --------------------------------------------------------------------------------------------------- | -| **Raw Texture Data** | The atlas data is written pixel by pixel. +- **Raw Texture Data:** + The atlas data is written pixel by pixel as returned by the pixel packing function. The total size equals + *(Atlas Width × Atlas Height)* The data is prepared for a DMA transfer to the VRAM. --- -