diff --git a/Editor/VramEditorWindow.cs b/Editor/VramEditorWindow.cs index 1cff27a..0d15465 100644 --- a/Editor/VramEditorWindow.cs +++ b/Editor/VramEditorWindow.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using SplashEdit.RuntimeCode; using Unity.Collections; using UnityEditor; @@ -22,8 +23,6 @@ namespace SplashEdit.EditorCode private Color bufferColor1 = new Color(1, 0, 0, 0.5f); private Color bufferColor2 = new Color(0, 1, 0, 0.5f); private Color prohibitedColor = new Color(1, 0, 0, 0.3f); - - private static string _psxDataPath = "Assets/PSXData.asset"; private PSXData _psxData; private static readonly Vector2[] resolutions = @@ -55,7 +54,11 @@ namespace SplashEdit.EditorCode // Ensure minimum window size is applied. this.minSize = new Vector2(800, 600); - LoadData(); + _psxData = DataStorage.LoadData(); + selectedResolution = _psxData.OutputResolution; + dualBuffering = _psxData.DualBuffering; + verticalLayout = _psxData.VerticalBuffering; + prohibitedAreas = _psxData.ProhibitedAreas; } /// @@ -143,13 +146,17 @@ namespace SplashEdit.EditorCode // Prompt the user to select a file location and save the VRAM data. string path = EditorUtility.SaveFilePanel("Select Output File", "", "output", "bin"); - using (BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create))) + + if (path != string.Empty) { - for (int y = 0; y < VramHeight; y++) + using (BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create))) { - for (int x = 0; x < VramWidth; x++) + for (int y = 0; y < VramHeight; y++) { - writer.Write(packed._vramPixels[x, y].Pack()); + for (int x = 0; x < VramWidth; x++) + { + writer.Write(packed._vramPixels[x, y].Pack()); + } } } } @@ -192,30 +199,51 @@ namespace SplashEdit.EditorCode } GUILayout.Space(10); - GUILayout.Label("Prohibited areas", EditorStyles.boldLabel); - scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.MaxHeight(150f)); + GUILayout.Label("Prohibited Areas", EditorStyles.boldLabel); + GUILayout.Space(10); + + scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, true, GUILayout.MinHeight(300f), GUILayout.ExpandWidth(true)); // List and edit each prohibited area. + List toRemove = new List(); + for (int i = 0; i < prohibitedAreas.Count; i++) { var area = prohibitedAreas[i]; - area.X = EditorGUILayout.IntField("X", area.X); - area.Y = EditorGUILayout.IntField("Y", area.Y); + GUI.backgroundColor = new Color(0.95f, 0.95f, 0.95f); + GUILayout.BeginVertical("box"); + + GUI.backgroundColor = Color.white; + + // Display fields for editing the area + area.X = EditorGUILayout.IntField("X Coordinate", area.X); + area.Y = EditorGUILayout.IntField("Y Coordinate", area.Y); area.Width = EditorGUILayout.IntField("Width", area.Width); area.Height = EditorGUILayout.IntField("Height", area.Height); - if (GUILayout.Button("Remove")) + + if (GUILayout.Button("Remove", GUILayout.Height(30))) { - prohibitedAreas.RemoveAt(i); - break; + toRemove.Add(i); // Mark for removal } + prohibitedAreas[i] = area; + + GUILayout.EndVertical(); GUILayout.Space(10); } + + // Remove the areas marked for deletion outside the loop to avoid skipping elements + foreach (var index in toRemove.OrderByDescending(x => x)) + { + prohibitedAreas.RemoveAt(index); + } + GUILayout.EndScrollView(); GUILayout.Space(10); + if (GUILayout.Button("Add Prohibited Area")) { prohibitedAreas.Add(new ProhibitedArea()); @@ -230,7 +258,13 @@ namespace SplashEdit.EditorCode // Button to save settings; saving now occurs only on button press. if (GUILayout.Button("Save Settings")) { - StoreData(); + _psxData.OutputResolution = selectedResolution; + _psxData.DualBuffering = dualBuffering; + _psxData.VerticalBuffering = verticalLayout; + _psxData.ProhibitedAreas = prohibitedAreas; + + DataStorage.StoreData(_psxData); + EditorUtility.DisplayDialog("splashedit", "Vram configuration saved", "OK"); } GUILayout.EndVertical(); @@ -263,42 +297,10 @@ namespace SplashEdit.EditorCode GUILayout.EndHorizontal(); } - /// - /// Loads stored PSX data from the asset. - /// - private void LoadData() - { - _psxData = AssetDatabase.LoadAssetAtPath(_psxDataPath); - if (!_psxData) - { - _psxData = CreateInstance(); - AssetDatabase.CreateAsset(_psxData, _psxDataPath); - AssetDatabase.SaveAssets(); - } - - selectedResolution = _psxData.OutputResolution; - dualBuffering = _psxData.DualBuffering; - verticalLayout = _psxData.VerticalBuffering; - prohibitedAreas = _psxData.ProhibitedAreas; - } - /// /// Stores current configuration to the PSX data asset. /// This is now triggered manually via the "Save Settings" button. /// - private void StoreData() - { - if (_psxData != null) - { - _psxData.OutputResolution = selectedResolution; - _psxData.DualBuffering = dualBuffering; - _psxData.VerticalBuffering = verticalLayout; - _psxData.ProhibitedAreas = prohibitedAreas; - EditorUtility.SetDirty(_psxData); - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - } - } } } \ No newline at end of file diff --git a/Runtime/PSXMesh.cs b/Runtime/PSXMesh.cs index 7a28160..f282fff 100644 --- a/Runtime/PSXMesh.cs +++ b/Runtime/PSXMesh.cs @@ -45,7 +45,7 @@ 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, int textureWidth = 256, int textureHeight = 256, Transform transform = null) + public static PSXMesh CreateFromUnityMesh(Mesh mesh, float GTEScaling, Transform transform, bool isStatic, int textureWidth = 256, int textureHeight = 256) { PSXMesh psxMesh = new PSXMesh { Triangles = new List() }; @@ -67,10 +67,27 @@ namespace SplashEdit.RuntimeCode int vid1 = indices[i + 1]; int vid2 = indices[i + 2]; + Vector3 v0, v1, v2; + // Transform vertices to world space if a transform is provided. - Vector3 v0 = transform ? transform.TransformPoint(vertices[vid0]) : vertices[vid0]; - Vector3 v1 = transform ? transform.TransformPoint(vertices[vid1]) : vertices[vid1]; - Vector3 v2 = transform ? transform.TransformPoint(vertices[vid2]) : vertices[vid2]; + + if (isStatic) + { + 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); + + } // Convert vertices to PSX format including fixed-point conversion and shading. PSXVertex psxV0 = ConvertToPSXVertex(v0, GTEScaling, normals[vid0], uv[vid0], lightDir, lightColor, textureWidth, textureHeight); @@ -116,13 +133,13 @@ namespace SplashEdit.RuntimeCode nz = (short)PSXTrig.ConvertCoordinateToPSX(normal.z), // Map UV coordinates to a byte range after scaling based on texture dimensions. - u = (byte)(Mathf.Clamp((uv.x * (textureWidth - 1)), 0, 255)), - v = (byte)(Mathf.Clamp(((1.0f - uv.y) * (textureHeight - 1)), 0, 255)), + u = (byte)Mathf.Clamp(uv.x * (textureWidth - 1), 0, 255), + 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)) + 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 a3c12b9..f4b343f 100644 --- a/Runtime/PSXObjectExporter.cs +++ b/Runtime/PSXObjectExporter.cs @@ -34,16 +34,7 @@ namespace SplashEdit.RuntimeCode MeshFilter meshFilter = gameObject.GetComponent(); if (meshFilter != null) { - if (MeshIsStatic) - { - // Static meshes take object transformation into account - Mesh = PSXMesh.CreateFromUnityMesh(meshFilter.sharedMesh, GTEScaling, Texture.Width, Texture.Height, transform); - } - else - { - // Dynamic meshes do not consider object transformation - Mesh = PSXMesh.CreateFromUnityMesh(meshFilter.sharedMesh, GTEScaling, Texture.Width, Texture.Height); - } + Mesh = PSXMesh.CreateFromUnityMesh(meshFilter.sharedMesh, GTEScaling, transform, MeshIsStatic, Texture.Width, Texture.Height); } } } diff --git a/Runtime/PSXSceneExporter.cs b/Runtime/PSXSceneExporter.cs index 3fcab20..1808e88 100644 --- a/Runtime/PSXSceneExporter.cs +++ b/Runtime/PSXSceneExporter.cs @@ -16,19 +16,20 @@ namespace SplashEdit.RuntimeCode private TextureAtlas[] _atlases; private PSXData _psxData; - private readonly string _psxDataPath = "Assets/PSXData.asset"; private Vector2 selectedResolution; private bool dualBuffering; private bool verticalLayout; private List prohibitedAreas; - private VRAMPixel[,] vramPixels; - - public void Export() { - LoadData(); + _psxData = DataStorage.LoadData(); + selectedResolution = _psxData.OutputResolution; + dualBuffering = _psxData.DualBuffering; + verticalLayout = _psxData.VerticalBuffering; + prohibitedAreas = _psxData.ProhibitedAreas; + _exporters = FindObjectsByType(FindObjectsSortMode.None); foreach (PSXObjectExporter exp in _exporters) { @@ -59,8 +60,37 @@ 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() { + string path = EditorUtility.SaveFilePanel("Select Output File", "", "output", "bin"); int totalFaces = 0; @@ -86,12 +116,12 @@ namespace SplashEdit.RuntimeCode // 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)); - + // Write object's transform + writer.Write((int)PSXTrig.ConvertCoordinateToPSX(exporter.transform.localToWorldMatrix.GetPosition().x, GTEScaling)); + writer.Write((int)PSXTrig.ConvertCoordinateToPSX(-exporter.transform.localToWorldMatrix.GetPosition().y, GTEScaling)); + writer.Write((int)PSXTrig.ConvertCoordinateToPSX(exporter.transform.localToWorldMatrix.GetPosition().z, GTEScaling)); 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]); @@ -277,35 +307,7 @@ namespace SplashEdit.RuntimeCode { 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); - - if (!_psxData) - { - _psxData = ScriptableObject.CreateInstance(); - AssetDatabase.CreateAsset(_psxData, _psxDataPath); - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - } - - selectedResolution = _psxData.OutputResolution; - dualBuffering = _psxData.DualBuffering; - verticalLayout = _psxData.VerticalBuffering; - prohibitedAreas = _psxData.ProhibitedAreas; - } - - 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/Utils.cs b/Runtime/Utils.cs index 49ace69..26f1c53 100644 --- a/Runtime/Utils.cs +++ b/Runtime/Utils.cs @@ -1,8 +1,39 @@ using System.Runtime.InteropServices; +using UnityEditor; using UnityEngine; namespace SplashEdit.RuntimeCode { + + public static class DataStorage + { + private static readonly string psxDataPath = "Assets/PSXData.asset"; + public static PSXData LoadData() + { + PSXData psxData = AssetDatabase.LoadAssetAtPath(psxDataPath); + + if (!psxData) + { + psxData = ScriptableObject.CreateInstance(); + AssetDatabase.CreateAsset(psxData, psxDataPath); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + } + + return psxData; + } + + public static void StoreData(PSXData psxData) + { + if (psxData != null) + { + EditorUtility.SetDirty(psxData); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + } + } + } + /// /// Represents a prohibited area in PlayStation 2D VRAM where textures should not be packed. /// This class provides conversion methods to and from Unity's Rect structure. @@ -42,48 +73,83 @@ namespace SplashEdit.RuntimeCode return new Rect(X, Y, Width, Height); } } + /// + /// A utility class containing methods for converting Unity-specific data formats to PSX-compatible formats. + /// This includes converting coordinates and rotations to PSX's 3.12 fixed-point format. + /// public static class PSXTrig { - + /// + /// Converts a floating-point coordinate to a PSX-compatible 3.12 fixed-point format. + /// The value is clamped to the range [-4, 3.999] and scaled by the provided GTEScaling factor. + /// + /// The coordinate value to convert. + /// A scaling factor for the value (default is 1.0f). + /// The converted coordinate in 3.12 fixed-point format. public static short ConvertCoordinateToPSX(float value, float GTEScaling = 1.0f) { - return (short)(Mathf.Clamp(value/GTEScaling, -4f, 3.999f) * 4096); + return (short)(Mathf.Clamp(value / GTEScaling, -4f, 3.999f) * 4096); } - + /// + /// Converts a quaternion rotation to a PSX-compatible 3x3 rotation matrix. + /// The matrix is adjusted for the difference in the Y-axis orientation between Unity (Y-up) and PSX (Y-down). + /// Each matrix element is converted to a 3.12 fixed-point format. + /// + /// The quaternion representing the rotation to convert. + /// A 3x3 matrix representing the PSX-compatible rotation. 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; + // Convert the quaternion to a Unity rotation matrix. + Matrix4x4 unityMatrix = Matrix4x4.Rotate(rotation); - // Create the 3x3 rotation matrix - int[,] psxMatrix = new int[3, 3] + // Flip the Y-axis to match PSX's Y-down convention. + float[,] fixedMatrix = new float[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)) } + { unityMatrix.m00, -unityMatrix.m01, unityMatrix.m02 }, // Flip Y + { -unityMatrix.m10, unityMatrix.m11, -unityMatrix.m12 }, // Flip Y + { unityMatrix.m20, -unityMatrix.m21, unityMatrix.m22 } // Flip Y }; + // Convert the Unity matrix to PSX fixed-point format. + int[,] psxMatrix = new int[3, 3]; + for (int i = 0; i < 3; i++) + { + for (int j = 0; j < 3; j++) + { + psxMatrix[i, j] = ConvertToFixed12(fixedMatrix[i, j]); + } + } + return psxMatrix; } - private static int ConvertToFixed12(float value) + /// + /// Converts a floating-point value to a 3.12 fixed-point format (PSX format). + /// The value is scaled by a factor of 4096 and clamped to the range of a signed 16-bit integer. + /// + /// The floating-point value to convert. + /// The converted value in 3.12 fixed-point format as a 16-bit signed integer. + public static short ConvertToFixed12(float value) { - return (int)(value * 4096.0f); // 2^12 = 4096 + int fixedValue = Mathf.RoundToInt(value * 4096.0f); // Scale to 3.12 format + return (short)Mathf.Clamp(fixedValue, -32768, 32767); // Clamp to signed 16-bit } } - [StructLayout(LayoutKind.Sequential, Pack = 1)] + /// + /// Represents the attributes of a texture page in the PSX graphics system. + /// Provides methods for setting various properties such as the page coordinates, transparency type, color mode, dithering, and display area. + /// public struct TPageAttr { - public ushort info; + public ushort info; // Stores the packed attribute information as a 16-bit unsigned integer. + /// + /// Sets the X-coordinate of the texture page. + /// The lower 4 bits of the 'info' field are used to store the X value. + /// + /// The X-coordinate value (0 to 15). + /// The updated TPageAttr instance. public TPageAttr SetPageX(byte x) { info &= 0xFFF0; // Clear lower 4 bits @@ -92,6 +158,12 @@ namespace SplashEdit.RuntimeCode return this; } + /// + /// Sets the Y-coordinate of the texture page. + /// The 4th bit of the 'info' field is used to store the Y value (0 or 1). + /// + /// The Y-coordinate value (0 or 1). + /// The updated TPageAttr instance. public TPageAttr SetPageY(byte y) { info &= 0xFFEF; // Clear bit 4 @@ -100,6 +172,12 @@ namespace SplashEdit.RuntimeCode return this; } + /// + /// Sets the transparency type of the texture page. + /// The transparency type is stored in bits 5 and 6 of the 'info' field. + /// + /// The transparency type to set. + /// The updated TPageAttr instance. public TPageAttr Set(SemiTrans trans) { info &= 0xFF9F; // Clear bits 5 and 6 @@ -108,6 +186,12 @@ namespace SplashEdit.RuntimeCode return this; } + /// + /// Sets the color mode of the texture page. + /// The color mode is stored in bits 7 and 8 of the 'info' field. + /// + /// The color mode to set (4-bit, 8-bit, or 16-bit). + /// The updated TPageAttr instance. public TPageAttr Set(ColorMode mode) { info &= 0xFE7F; // Clear bits 7 and 8 @@ -116,31 +200,54 @@ namespace SplashEdit.RuntimeCode return this; } + /// + /// Enables or disables dithering for the texture page. + /// Dithering is stored in bit 9 of the 'info' field. + /// + /// True to enable dithering, false to disable it. + /// The updated TPageAttr instance. public TPageAttr SetDithering(bool dithering) { if (dithering) - info |= 0x0200; + info |= 0x0200; // Set bit 9 to enable dithering else - info &= 0xFDFF; + info &= 0xFDFF; // Clear bit 9 to disable dithering return this; } + /// + /// Disables the display area for the texture page. + /// This will clear bit 10 of the 'info' field. + /// + /// The updated TPageAttr instance. public TPageAttr DisableDisplayArea() { info &= 0xFBFF; // Clear bit 10 return this; } + /// + /// Enables the display area for the texture page. + /// This will set bit 10 of the 'info' field. + /// + /// The updated TPageAttr instance. public TPageAttr EnableDisplayArea() { - info |= 0x0400; // Set bit 10 + info |= 0x0400; // Set bit 10 to enable display area return this; } + /// + /// Returns a string representation of the TPageAttr instance, showing the 'info' value in hexadecimal. + /// + /// A string representing the 'info' value in hexadecimal format. public override string ToString() => $"Info: 0x{info:X4}"; - // Define the enums for SemiTrans and ColorMode (assuming their values) + + /// + /// Defines the transparency types for a texture page. + /// public enum SemiTrans : uint { None = 0, @@ -149,6 +256,9 @@ namespace SplashEdit.RuntimeCode Type3 = 3 } + /// + /// Defines the color modes for a texture page. + /// public enum ColorMode : uint { Mode4Bit = 0,