From 7b127b345bd951cdb0cc32b27e059d31387bc711 Mon Sep 17 00:00:00 2001 From: jracek Date: Mon, 17 Mar 2025 14:32:54 +0100 Subject: [PATCH] Preparation for release. Comments, fixes, README --- Editor/PSXSceneExporterEditor.cs | 23 +- Editor/QuantizedPreviewWindow.cs | 314 ++++++++++----------- Editor/VramEditorWindow.cs | 454 +++++++++++++++++-------------- README.md | 122 +++++++++ README.md.meta | 7 + Runtime/ImageProcessing.cs | 2 +- Runtime/PSXData.cs | 18 +- Runtime/PSXMesh.cs | 82 +++++- Runtime/PSXObjectExporter.cs | 27 +- Runtime/PSXSceneExporter.cs | 121 ++++---- Runtime/PSXTexture2D.cs | 5 +- Runtime/TexturePacker.cs | 120 ++++++-- Runtime/Utils.cs | 20 +- 13 files changed, 839 insertions(+), 476 deletions(-) create mode 100644 README.md create mode 100644 README.md.meta diff --git a/Editor/PSXSceneExporterEditor.cs b/Editor/PSXSceneExporterEditor.cs index b4cc044..85cfdde 100644 --- a/Editor/PSXSceneExporterEditor.cs +++ b/Editor/PSXSceneExporterEditor.cs @@ -1,19 +1,22 @@ using UnityEngine; using UnityEditor; -using PSXSplash.RuntimeCode; +using SplashEdit.RuntimeCode; -[CustomEditor(typeof(PSXSceneExporter))] -public class PSXSceneExporterEditor : Editor +namespace SplashEdit.EditorCode { - public override void OnInspectorGUI() + [CustomEditor(typeof(PSXSceneExporter))] + public class PSXSceneExporterEditor : Editor { - DrawDefaultInspector(); - - PSXSceneExporter comp = (PSXSceneExporter)target; - if (GUILayout.Button("Export")) + public override void OnInspectorGUI() { - comp.Export(); - } + DrawDefaultInspector(); + PSXSceneExporter comp = (PSXSceneExporter)target; + if (GUILayout.Button("Export")) + { + comp.Export(); + } + + } } } \ No newline at end of file diff --git a/Editor/QuantizedPreviewWindow.cs b/Editor/QuantizedPreviewWindow.cs index 2a82d83..1a2038c 100644 --- a/Editor/QuantizedPreviewWindow.cs +++ b/Editor/QuantizedPreviewWindow.cs @@ -1,185 +1,187 @@ using System.Collections.Generic; using System.IO; -using PSXSplash.RuntimeCode; +using SplashEdit.RuntimeCode; using UnityEditor; using UnityEngine; using UnityEngine.Rendering; -public class QuantizedPreviewWindow : EditorWindow +namespace SplashEdit.EditorCode { - private Texture2D originalTexture; - private Texture2D quantizedTexture; - private Texture2D vramTexture; // New VRAM Texture - private List clut; // Changed to 1D array - private ushort[] indexedPixelData; // New field for indexed pixel data - private PSXBPP bpp; - private readonly int previewSize = 256; - - [MenuItem("Window/Quantized Preview")] - public static void ShowWindow() + public class QuantizedPreviewWindow : EditorWindow { - QuantizedPreviewWindow win = GetWindow("Quantized Preview"); - win.minSize = new Vector2(800, 700); - } + private Texture2D originalTexture; + private Texture2D quantizedTexture; + private Texture2D vramTexture; // VRAM representation of the texture + private List clut; // Color Lookup Table (CLUT), stored as a 1D list + private ushort[] indexedPixelData; // Indexed pixel data for VRAM storage + private PSXBPP bpp; + private readonly int previewSize = 256; - private void OnGUI() - { - GUILayout.Label("Quantized Preview", EditorStyles.boldLabel); - - originalTexture = (Texture2D)EditorGUILayout.ObjectField("Original Texture", originalTexture, typeof(Texture2D), false); - - - bpp = (PSXBPP)EditorGUILayout.EnumPopup("Bit Depth", bpp); - - - if (GUILayout.Button("Generate Quantized Preview") && originalTexture != null) + [MenuItem("Window/Quantized Preview")] + public static void ShowWindow() { - GenerateQuantizedPreview(); + // Creates and displays the window + QuantizedPreviewWindow win = GetWindow("Quantized Preview"); + win.minSize = new Vector2(800, 700); } - GUILayout.BeginHorizontal(); - - if (originalTexture != null) + private void OnGUI() { - GUILayout.BeginVertical(); - GUILayout.Label("Original Texture"); - DrawTexturePreview(originalTexture, previewSize, false); - GUILayout.EndVertical(); - } + GUILayout.Label("Quantized Preview", EditorStyles.boldLabel); - if (vramTexture != null) - { - GUILayout.BeginVertical(); - GUILayout.Label("VRAM View (Indexed Data as 16bpp)"); - DrawTexturePreview(vramTexture, previewSize); - GUILayout.EndVertical(); - } + // Texture input field + originalTexture = (Texture2D)EditorGUILayout.ObjectField("Original Texture", originalTexture, typeof(Texture2D), false); - if (quantizedTexture != null) - { - GUILayout.BeginVertical(); - GUILayout.Label("Quantized Texture"); - DrawTexturePreview(quantizedTexture, previewSize); - GUILayout.EndVertical(); - } + // Dropdown for bit depth selection + bpp = (PSXBPP)EditorGUILayout.EnumPopup("Bit Depth", bpp); - GUILayout.EndHorizontal(); - - if (clut != null) - { - GUILayout.Label("Color Lookup Table (CLUT)"); - DrawCLUT(); - } - - GUILayout.Space(10); - - if (indexedPixelData != null) - { - if (GUILayout.Button("Export texute data")) + // Button to generate the quantized preview + if (GUILayout.Button("Generate Quantized Preview") && originalTexture != null) { - string path = EditorUtility.SaveFilePanel( - "Save texture data", - "", - "pixel_data", - "bin" - ); - - if (!string.IsNullOrEmpty(path)) - { - using (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write)) - using (BinaryWriter writer = new BinaryWriter(fileStream)) - { - foreach (ushort value in indexedPixelData) - { - writer.Write(value); - } - } - } + GenerateQuantizedPreview(); } - } - if (clut != null) - { - if (GUILayout.Button("Export clut data")) - { - string path = EditorUtility.SaveFilePanel( - "Save clut data", - "", - "clut_data", - "bin" - ); - - if (!string.IsNullOrEmpty(path)) - { - using (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write)) - using (BinaryWriter writer = new BinaryWriter(fileStream)) - { - foreach (VRAMPixel value in clut) - { - writer.Write(value.Pack()); - } - } - } - } - } - } - - private void GenerateQuantizedPreview() - { - - PSXTexture2D psxTex = PSXTexture2D.CreateFromTexture2D(originalTexture, bpp); - - quantizedTexture = psxTex.GeneratePreview(); - vramTexture = psxTex.GenerateVramPreview(); - clut = psxTex.ColorPalette; - - } - - - - - - private void DrawTexturePreview(Texture2D texture, int size, bool flipY = true) - { - Rect rect = GUILayoutUtility.GetRect(size, size, GUILayout.ExpandWidth(false)); - EditorGUI.DrawPreviewTexture(rect, texture, null, ScaleMode.ScaleToFit, 0, 0, ColorWriteMask.All); - } - - - private void DrawCLUT() - { - if (clut == null) return; - - int swatchSize = 20; - int maxColorsPerRow = 40; - - GUILayout.Space(10); - - int totalColors = clut.Count; - int totalRows = Mathf.CeilToInt((float)totalColors / maxColorsPerRow); - - for (int row = 0; row < totalRows; row++) - { GUILayout.BeginHorizontal(); - int colorsInRow = Mathf.Min(maxColorsPerRow, totalColors - row * maxColorsPerRow); - - for (int col = 0; col < colorsInRow; col++) + // Display the original texture + if (originalTexture != null) { - int index = row * maxColorsPerRow + col; + GUILayout.BeginVertical(); + GUILayout.Label("Original Texture"); + DrawTexturePreview(originalTexture, previewSize, false); + GUILayout.EndVertical(); + } - Vector3 color = new Vector3( - clut[index].R / 31.0f, // Red: bits 0–4 - clut[index].G / 31.0f, // Green: bits 5–9 - clut[index].B / 31.0f // Blue: bits 10–14 - ); + // Display the VRAM view of the texture + if (vramTexture != null) + { + GUILayout.BeginVertical(); + GUILayout.Label("VRAM View (Indexed Data as 16bpp)"); + DrawTexturePreview(vramTexture, previewSize); + GUILayout.EndVertical(); + } - Rect rect = GUILayoutUtility.GetRect(swatchSize, swatchSize, GUILayout.ExpandWidth(false)); - EditorGUI.DrawRect(rect, new Color(color.x, color.y, color.z)); + // Display the quantized texture + if (quantizedTexture != null) + { + GUILayout.BeginVertical(); + GUILayout.Label("Quantized Texture"); + DrawTexturePreview(quantizedTexture, previewSize); + GUILayout.EndVertical(); } GUILayout.EndHorizontal(); + + // Display the Color Lookup Table (CLUT) + if (clut != null) + { + GUILayout.Label("Color Lookup Table (CLUT)"); + DrawCLUT(); + } + + GUILayout.Space(10); + + // Export indexed pixel data + if (indexedPixelData != null) + { + if (GUILayout.Button("Export texture data")) + { + string path = EditorUtility.SaveFilePanel("Save texture data", "", "pixel_data", "bin"); + + if (!string.IsNullOrEmpty(path)) + { + using (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write)) + using (BinaryWriter writer = new BinaryWriter(fileStream)) + { + foreach (ushort value in indexedPixelData) + { + writer.Write(value); + } + } + } + } + } + + // Export CLUT data + if (clut != null) + { + if (GUILayout.Button("Export CLUT data")) + { + string path = EditorUtility.SaveFilePanel("Save CLUT data", "", "clut_data", "bin"); + + if (!string.IsNullOrEmpty(path)) + { + using (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write)) + using (BinaryWriter writer = new BinaryWriter(fileStream)) + { + foreach (VRAMPixel value in clut) + { + writer.Write(value.Pack()); // Convert VRAMPixel data into a binary format + } + } + } + } + } + } + + private void GenerateQuantizedPreview() + { + // Converts the texture using PSXTexture2D and stores the processed data + PSXTexture2D psxTex = PSXTexture2D.CreateFromTexture2D(originalTexture, bpp); + + // Generate the quantized texture preview + quantizedTexture = psxTex.GeneratePreview(); + + // Generate the VRAM representation of the texture + vramTexture = psxTex.GenerateVramPreview(); + + // Store the Color Lookup Table (CLUT) + clut = psxTex.ColorPalette; + } + + private void DrawTexturePreview(Texture2D texture, int size, bool flipY = true) + { + // Renders a texture preview within the editor window + Rect rect = GUILayoutUtility.GetRect(size, size, GUILayout.ExpandWidth(false)); + EditorGUI.DrawPreviewTexture(rect, texture, null, ScaleMode.ScaleToFit, 0, 0, ColorWriteMask.All); + } + + private void DrawCLUT() + { + if (clut == null) return; + + int swatchSize = 20; + int maxColorsPerRow = 40; // Number of colors displayed per row + + GUILayout.Space(10); + + int totalColors = clut.Count; + int totalRows = Mathf.CeilToInt((float)totalColors / maxColorsPerRow); + + for (int row = 0; row < totalRows; row++) + { + GUILayout.BeginHorizontal(); + + int colorsInRow = Mathf.Min(maxColorsPerRow, totalColors - row * maxColorsPerRow); + + for (int col = 0; col < colorsInRow; col++) + { + int index = row * maxColorsPerRow + col; + + // Convert the CLUT colors from 5-bit to float values (0-1 range) + Vector3 color = new Vector3( + clut[index].R / 31.0f, // Red: bits 0–4 + clut[index].G / 31.0f, // Green: bits 5–9 + clut[index].B / 31.0f // Blue: bits 10–14 + ); + + // Create a small color preview box for each color in the CLUT + Rect rect = GUILayoutUtility.GetRect(swatchSize, swatchSize, GUILayout.ExpandWidth(false)); + EditorGUI.DrawRect(rect, new Color(color.x, color.y, color.z)); + } + + GUILayout.EndHorizontal(); + } } } - -} +} \ No newline at end of file diff --git a/Editor/VramEditorWindow.cs b/Editor/VramEditorWindow.cs index 0865eee..1cff27a 100644 --- a/Editor/VramEditorWindow.cs +++ b/Editor/VramEditorWindow.cs @@ -1,31 +1,33 @@ using System.Collections.Generic; using System.IO; -using PSXSplash.RuntimeCode; +using SplashEdit.RuntimeCode; using Unity.Collections; using UnityEditor; using UnityEngine; using UnityEngine.Rendering; -public class VRAMEditorWindow : EditorWindow + +namespace SplashEdit.EditorCode { - - private const int VramWidth = 1024; - private const int VramHeight = 512; - private List prohibitedAreas = new List(); - private Vector2 scrollPosition; - private Texture2D vramImage; - private Vector2 selectedResolution = new Vector2(320, 240); - private bool dualBuffering = true; - private bool verticalLayout = true; - 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 = + public class VRAMEditorWindow : EditorWindow { + private const int VramWidth = 1024; + private const int VramHeight = 512; + private List prohibitedAreas = new List(); + private Vector2 scrollPosition; + private Texture2D vramImage; + private Vector2 selectedResolution = new Vector2(320, 240); + private bool dualBuffering = true; + private bool verticalLayout = true; + 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 = + { new Vector2(256, 240), new Vector2(256, 480), new Vector2(320, 240), new Vector2(320, 480), new Vector2(368, 240), new Vector2(368, 480), @@ -33,232 +35,270 @@ public class VRAMEditorWindow : EditorWindow new Vector2(640, 240), new Vector2(640, 480) }; - [MenuItem("Window/VRAM Editor")] - public static void ShowWindow() - { - GetWindow("VRAM Editor"); - } - - private void OnEnable() - { - vramImage = new Texture2D(VramWidth, VramHeight); - NativeArray blackPixels = new NativeArray(VramWidth * VramHeight, Allocator.Temp); - vramImage.SetPixelData(blackPixels, 0); - vramImage.Apply(); - blackPixels.Dispose(); - - LoadData(); - } - - public static void PasteTexture(Texture2D baseTexture, Texture2D overlayTexture, int posX, int posY) - { - if (baseTexture == null || overlayTexture == null) + [MenuItem("Window/VRAM Editor")] + public static void ShowWindow() { - Debug.LogError("Textures cannot be null!"); - return; + VRAMEditorWindow window = GetWindow("VRAM Editor"); + // Set minimum window dimensions. + window.minSize = new Vector2(1600, 600); } - Color[] overlayPixels = overlayTexture.GetPixels(); - Color[] basePixels = baseTexture.GetPixels(); - - int baseWidth = baseTexture.width; - int baseHeight = baseTexture.height; - int overlayWidth = overlayTexture.width; - int overlayHeight = overlayTexture.height; - - for (int y = 0; y < overlayHeight; y++) + private void OnEnable() { - for (int x = 0; x < overlayWidth; x++) - { - int baseX = posX + x; - int baseY = posY + y; - if (baseX >= 0 && baseX < baseWidth && baseY >= 0 && baseY < baseHeight) - { - int baseIndex = baseY * baseWidth + baseX; - int overlayIndex = y * overlayWidth + x; + // Initialize VRAM texture with black pixels. + vramImage = new Texture2D(VramWidth, VramHeight); + NativeArray blackPixels = new NativeArray(VramWidth * VramHeight, Allocator.Temp); + vramImage.SetPixelData(blackPixels, 0); + vramImage.Apply(); + blackPixels.Dispose(); - basePixels[baseIndex] = overlayPixels[overlayIndex]; + // Ensure minimum window size is applied. + this.minSize = new Vector2(800, 600); + + LoadData(); + } + + /// + /// Pastes an overlay texture onto a base texture at the specified position. + /// + public static void PasteTexture(Texture2D baseTexture, Texture2D overlayTexture, int posX, int posY) + { + if (baseTexture == null || overlayTexture == null) + { + Debug.LogError("Textures cannot be null!"); + return; + } + + Color[] overlayPixels = overlayTexture.GetPixels(); + Color[] basePixels = baseTexture.GetPixels(); + + int baseWidth = baseTexture.width; + int baseHeight = baseTexture.height; + int overlayWidth = overlayTexture.width; + int overlayHeight = overlayTexture.height; + + // Copy each overlay pixel into the base texture if within bounds. + for (int y = 0; y < overlayHeight; y++) + { + for (int x = 0; x < overlayWidth; x++) + { + int baseX = posX + x; + int baseY = posY + y; + if (baseX >= 0 && baseX < baseWidth && baseY >= 0 && baseY < baseHeight) + { + int baseIndex = baseY * baseWidth + baseX; + int overlayIndex = y * overlayWidth + x; + basePixels[baseIndex] = overlayPixels[overlayIndex]; + } } } + + baseTexture.SetPixels(basePixels); + baseTexture.Apply(); } - baseTexture.SetPixels(basePixels); - baseTexture.Apply(); - } - private void PackTextures() - { - - vramImage = new Texture2D(VramWidth, VramHeight); - NativeArray blackPixels = new NativeArray(VramWidth * VramHeight, Allocator.Temp); - vramImage.SetPixelData(blackPixels, 0); - vramImage.Apply(); - blackPixels.Dispose(); - - PSXObjectExporter[] objects = FindObjectsByType(FindObjectsSortMode.None); - foreach (PSXObjectExporter exp in objects) + /// + /// Packs PSX textures into VRAM, rebuilds the VRAM texture and writes binary data to an output file. + /// + private void PackTextures() { - exp.CreatePSXTexture2D(); - } + // Reinitialize VRAM texture with black pixels. + vramImage = new Texture2D(VramWidth, VramHeight); + NativeArray blackPixels = new NativeArray(VramWidth * VramHeight, Allocator.Temp); + vramImage.SetPixelData(blackPixels, 0); + vramImage.Apply(); + blackPixels.Dispose(); - Rect buffer1 = new Rect(0, 0, selectedResolution.x, selectedResolution.y); - Rect buffer2 = verticalLayout ? new Rect(0, 256, selectedResolution.x, selectedResolution.y) - : new Rect(selectedResolution.x, 0, selectedResolution.x, selectedResolution.y); - - - List framebuffers = new List { buffer1 }; - if (dualBuffering) - { - framebuffers.Add(buffer2); - } - - VRAMPacker tp = new VRAMPacker(framebuffers, prohibitedAreas); - var packed = tp.PackTexturesIntoVRAM(objects); - - - for (int y = 0; y < VramHeight; y++) - { - for (int x = 0; x < VramWidth; x++) + // Retrieve all PSXObjectExporter objects and create their PSX textures. + PSXObjectExporter[] objects = FindObjectsByType(FindObjectsSortMode.None); + foreach (PSXObjectExporter exp in objects) { - vramImage.SetPixel(x, VramHeight - y - 1, packed._vramPixels[x, y].GetUnityColor()); + exp.CreatePSXTexture2D(); } - } - vramImage.Apply(); - string path = EditorUtility.SaveFilePanel("Select Output File", "", "output", "bin"); + // Define framebuffer regions based on selected resolution and layout. + Rect buffer1 = new Rect(0, 0, selectedResolution.x, selectedResolution.y); + Rect buffer2 = verticalLayout ? new Rect(0, 256, selectedResolution.x, selectedResolution.y) + : new Rect(selectedResolution.x, 0, selectedResolution.x, selectedResolution.y); - using (BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create))) - { + List framebuffers = new List { buffer1 }; + if (dualBuffering) + { + framebuffers.Add(buffer2); + } + + // Pack textures into VRAM using the VRAMPacker. + VRAMPacker tp = new VRAMPacker(framebuffers, prohibitedAreas); + var packed = tp.PackTexturesIntoVRAM(objects); + + // Copy packed VRAM pixel data into the texture. for (int y = 0; y < VramHeight; y++) { for (int x = 0; x < VramWidth; x++) { - writer.Write(packed._vramPixels[x, y].Pack()); + vramImage.SetPixel(x, VramHeight - y - 1, packed._vramPixels[x, y].GetUnityColor()); + } + } + vramImage.Apply(); + + // 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))) + { + for (int y = 0; y < VramHeight; y++) + { + for (int x = 0; x < VramWidth; x++) + { + writer.Write(packed._vramPixels[x, y].Pack()); + } } } } - } - - private void OnGUI() - { - GUILayout.BeginHorizontal(); - GUILayout.BeginVertical(); - GUILayout.Label("VRAM Editor", EditorStyles.boldLabel); - - selectedResolution = resolutions[EditorGUILayout.Popup("Resolution", System.Array.IndexOf(resolutions, selectedResolution), - new string[] { "256x240", "256x480", "320x240", "320x480", "368x240", "368x480", "512x240", "512x480", "640x240", "640x480" })]; - - bool canDBHorizontal = selectedResolution[0] * 2 <= 1024; - bool canDBVertical = selectedResolution[1] * 2 <= 512; - - if (canDBHorizontal || canDBVertical) + private void OnGUI() { - dualBuffering = EditorGUILayout.Toggle("Dual Buffering", dualBuffering); - } - else { dualBuffering = false; } + GUILayout.BeginHorizontal(); + GUILayout.BeginVertical(); + GUILayout.Label("VRAM Editor", EditorStyles.boldLabel); - if (canDBVertical && canDBHorizontal) - { - verticalLayout = EditorGUILayout.Toggle("Vertical", verticalLayout); - } - else if (canDBVertical) { verticalLayout = true; } - else - { - verticalLayout = false; - } + // Dropdown for resolution selection. + selectedResolution = resolutions[EditorGUILayout.Popup("Resolution", System.Array.IndexOf(resolutions, selectedResolution), + new string[] { "256x240", "256x480", "320x240", "320x480", "368x240", "368x480", "512x240", "512x480", "640x240", "640x480" })]; - GUILayout.Space(10); - GUILayout.Label("Prohibited areas", EditorStyles.boldLabel); - scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.MaxHeight(150f)); + // Check resolution constraints for dual buffering. + bool canDBHorizontal = selectedResolution[0] * 2 <= 1024; + bool canDBVertical = selectedResolution[1] * 2 <= 512; - 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); - area.Width = EditorGUILayout.IntField("Width", area.Width); - area.Height = EditorGUILayout.IntField("Height", area.Height); - - if (GUILayout.Button("Remove")) + if (canDBHorizontal || canDBVertical) { - prohibitedAreas.RemoveAt(i); - break; + dualBuffering = EditorGUILayout.Toggle("Dual Buffering", dualBuffering); + } + else + { + dualBuffering = false; + } + + if (canDBVertical && canDBHorizontal) + { + verticalLayout = EditorGUILayout.Toggle("Vertical", verticalLayout); + } + else if (canDBVertical) + { + verticalLayout = true; + } + else + { + verticalLayout = false; } - prohibitedAreas[i] = area; GUILayout.Space(10); + GUILayout.Label("Prohibited areas", EditorStyles.boldLabel); + scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.MaxHeight(150f)); + + // List and edit each prohibited area. + 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); + area.Width = EditorGUILayout.IntField("Width", area.Width); + area.Height = EditorGUILayout.IntField("Height", area.Height); + + if (GUILayout.Button("Remove")) + { + prohibitedAreas.RemoveAt(i); + break; + } + + prohibitedAreas[i] = area; + GUILayout.Space(10); + } + GUILayout.EndScrollView(); + GUILayout.Space(10); + if (GUILayout.Button("Add Prohibited Area")) + { + prohibitedAreas.Add(new ProhibitedArea()); + } + + // Button to initiate texture packing. + if (GUILayout.Button("Pack Textures")) + { + PackTextures(); + } + + // Button to save settings; saving now occurs only on button press. + if (GUILayout.Button("Save Settings")) + { + StoreData(); + } + + GUILayout.EndVertical(); + + // Display VRAM image preview. + Rect vramRect = GUILayoutUtility.GetRect(VramWidth, VramHeight, GUILayout.ExpandWidth(false)); + EditorGUI.DrawPreviewTexture(vramRect, vramImage, null, ScaleMode.ScaleToFit, 0, 0, ColorWriteMask.All); + + // Draw framebuffer overlays. + Rect buffer1 = new Rect(vramRect.x, vramRect.y, selectedResolution.x, selectedResolution.y); + Rect buffer2 = verticalLayout ? new Rect(vramRect.x, 256, selectedResolution.x, selectedResolution.y) + : new Rect(vramRect.x + selectedResolution.x, vramRect.y, selectedResolution.x, selectedResolution.y); + + EditorGUI.DrawRect(buffer1, bufferColor1); + GUI.Label(new Rect(buffer1.center.x - 40, buffer1.center.y - 10, 120, 20), "Framebuffer A", EditorStyles.boldLabel); + GUILayout.Space(10); + if (dualBuffering) + { + EditorGUI.DrawRect(buffer2, bufferColor2); + GUI.Label(new Rect(buffer2.center.x - 40, buffer2.center.y - 10, 120, 20), "Framebuffer B", EditorStyles.boldLabel); + } + + // Draw overlays for each prohibited area. + foreach (ProhibitedArea area in prohibitedAreas) + { + Rect areaRect = new Rect(vramRect.x + area.X, vramRect.y + area.Y, area.Width, area.Height); + EditorGUI.DrawRect(areaRect, prohibitedColor); + } + + GUILayout.EndHorizontal(); } - GUILayout.EndScrollView(); - GUILayout.Space(10); - if (GUILayout.Button("Add Prohibited Area")) + /// + /// Loads stored PSX data from the asset. + /// + private void LoadData() { - prohibitedAreas.Add(new ProhibitedArea()); + _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; } - if (GUILayout.Button("Pack Textures")) + /// + /// Stores current configuration to the PSX data asset. + /// This is now triggered manually via the "Save Settings" button. + /// + private void StoreData() { - PackTextures(); - } + if (_psxData != null) + { + _psxData.OutputResolution = selectedResolution; + _psxData.DualBuffering = dualBuffering; + _psxData.VerticalBuffering = verticalLayout; + _psxData.ProhibitedAreas = prohibitedAreas; - GUILayout.EndVertical(); - - Rect vramRect = GUILayoutUtility.GetRect(VramWidth, VramHeight, GUILayout.ExpandWidth(false)); - EditorGUI.DrawPreviewTexture(vramRect, vramImage, null, ScaleMode.ScaleToFit, 0, 0, ColorWriteMask.All); - - Rect buffer1 = new Rect(vramRect.x, vramRect.y, selectedResolution.x, selectedResolution.y); - Rect buffer2 = verticalLayout ? new Rect(vramRect.x, 256, selectedResolution.x, selectedResolution.y) - : new Rect(vramRect.x + selectedResolution.x, vramRect.y, selectedResolution.x, selectedResolution.y); - - EditorGUI.DrawRect(buffer1, bufferColor1); - GUI.Label(new Rect(buffer1.center.x - 40, buffer1.center.y - 10, 120, 20), "Framebuffer A", EditorStyles.boldLabel); - GUILayout.Space(10); - if (dualBuffering) - { - EditorGUI.DrawRect(buffer2, bufferColor2); - GUI.Label(new Rect(buffer2.center.x - 40, buffer2.center.y - 10, 120, 20), "Framebuffer B", EditorStyles.boldLabel); - } - - foreach (ProhibitedArea area in prohibitedAreas) - { - Rect areaRect = new Rect(vramRect.x + area.X, vramRect.y + area.Y, area.Width, area.Height); - EditorGUI.DrawRect(areaRect, prohibitedColor); - } - - GUILayout.EndHorizontal(); - StoreData(); - } - - 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; - } - - 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(); + EditorUtility.SetDirty(_psxData); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + } } } } \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..efdddd1 --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ +# SplashEdit + +SplashEdit is a Unity Package that converts your Unity scenes into authentic PSX worlds by exporting binary data loadable in a PlayStation 1 game. It streamlines the export process for your scenes and offers additional tools for VRAM management and texture quantization. + +## Features + +- **Automatic Scene Exporting:** + Export your scene with a single click using the PSX Scene Exporter component. This process automatically packs textures into the PSX's 2D VRAM. +- **Texture Packing & Quantization:** + Convert and preview your textures in a PSX-compatible format with built-in quantization tools. + +## Installation + +Install SplashEdit directly from the Git repository using Unity's Package Manager: + +1. **Open Unity's Package Manager:** + Go to `Window` → `Package Manager`. + +2. **Add Package from Git URL:** + Click the **+** button in the upper left corner and select **"Add package from git URL..."**. + Enter the Git URL for SplashEdit: `https://github.com/psxsplash/splashedit.git` + Click **Add** and wait for the package to install. + +## Usage + +### General Scene Exporting + +If you only need to export the scene, follow these steps: + +1. **PSX Object Exporter:** +- Attach the **PSX Object Exporter** component to every GameObject you wish to export. +- Set the desired bit depth for each object's texture in the component settings. + +2. **PSX Scene Exporter:** +- Add the **PSX Scene Exporter** component to a GameObject in your scene (using an empty GameObject is recommended for organization). +- Click the export button in the PSX Scene Exporter. You will be prompted to choose an output file location. +- The exporter will automatically handle texture packing into the PSX's 2D VRAM. + +### Additional Features + +SplashEdit also includes extra tools to enhance your workflow: + +1. **VRAM Editor:** +- Access the VRAM Editor via Unity's **Window** context menu. +- Set framebuffer locations and preview texture packing. +- **Important:** Click on **Save Settings** in the VRAM Editor to inform the PSX Scene Exporter where to pack textures. +- When you click **Pack Textures** in the VRAM Editor, a file selection dialog will appear. + - Selecting a file will save only the VRAM data. + - If you do not wish to save VRAM, simply close the dialog. + **Note:** This action only exports the VRAM. For a complete scene export (including VRAM), use the PSX Scene Exporter component. + +2. **Quantized Texture Preview:** +- Preview how your textures will look after quantization before exporting. + +## Texture Requirements + +- **Power of Two:** +All textures must have dimensions that are a power of two (e.g., 64x64, 128x128, 256x256) with a maximum size of **256x256**. +- **No Automatic Downscaling:** +SplashEdit does not automatically downscale textures that exceed these limits. +- **READ/WRITE Enabled:** +Ensure all textures have **READ/WRITE enabled** in Unity. + +## Output Format + +The binary file output by SplashEdit is structured as follows, allowing a programmer to write a parser in C based solely on this specification. + +1. **VRAM Data (1 MB):** + - The file begins with a 1 MB block of VRAM data. + - This data is generated by iterating through a 2D array (`vramPixels`) in row-major order. + - Each pixel is written using its `.Pack()` method (resulting in one byte per pixel). + +2. **Object Count:** + - Immediately following the VRAM data, a 2-byte unsigned short is written indicating the number of exported objects (PSXObjectExporters). + +3. **Per-Object Data:** +For each exported object, the following data is written sequentially: + + - **Triangle Count (2 bytes):** + An unsigned short representing the number of triangles in the object's mesh. + + - **Texture Information:** + - **Bit Depth (1 byte):** + The bit depth of the object's texture. + - **Texpage Coordinates (2 bytes total):** + Two 1-byte values for `TexpageX` and `TexpageY`. + - **CLUT Packing (4 bytes total):** + Two unsigned shorts (2 bytes each) for `ClutPackingX` and `ClutPackingY`. + + - **Packing Byte (1 byte):** + + + - **Triangles Data:** + For each triangle in the object's mesh, data for its three vertices is written in sequence. Each vertex consists of: + + - **Position (6 bytes):** + Three signed shorts (2 bytes each) representing `vx`, `vy`, and `vz`. + - **Normal (6 bytes):** + Three signed shorts representing `nx`, `ny`, and `nz`. + - **Texture Coordinates (2 bytes):** + - **U coordinate (1 byte):** + The U coordinate relative to texpage start + - **V coordinate (1 byte):** + The V coordinate relative to texpage start + - **Color (3 bytes):** + Three bytes representing the RGB values. + - **Padding (7 bytes):** + Seven bytes of zero padding. + + Each vertex is 24 bytes in total, making each triangle 72 bytes (3 vertices × 24 bytes). + + +## Contributing + +Contributions are welcome! To contribute: + +1. Fork the repository. +3. Submit a pull request with your changes. + +For major changes, please open an issue first to discuss your ideas. + + diff --git a/README.md.meta b/README.md.meta new file mode 100644 index 0000000..36e6b35 --- /dev/null +++ b/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4df40ce535b32f3a4b30ce0803fa699a +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ImageProcessing.cs b/Runtime/ImageProcessing.cs index be2497b..8a46be2 100644 --- a/Runtime/ImageProcessing.cs +++ b/Runtime/ImageProcessing.cs @@ -3,7 +3,7 @@ using System.Linq; using UnityEngine; -namespace PSXSplash.RuntimeCode +namespace SplashEdit.RuntimeCode { /// diff --git a/Runtime/PSXData.cs b/Runtime/PSXData.cs index b23df5d..0cc83ab 100644 --- a/Runtime/PSXData.cs +++ b/Runtime/PSXData.cs @@ -1,12 +1,14 @@ using System.Collections.Generic; -using PSXSplash.RuntimeCode; using UnityEngine; -[CreateAssetMenu(fileName = "PSXData", menuName = "Scriptable Objects/PSXData")] -public class PSXData : ScriptableObject +namespace SplashEdit.RuntimeCode { - public Vector2 OutputResolution = new Vector2(320, 240); - public bool DualBuffering = true; - public bool VerticalBuffering = true; - public List ProhibitedAreas = new List(); -} + [CreateAssetMenu(fileName = "PSXData", menuName = "Scriptable Objects/PSXData")] + public class PSXData : ScriptableObject + { + public Vector2 OutputResolution = new Vector2(320, 240); + public bool DualBuffering = true; + public bool VerticalBuffering = true; + public List ProhibitedAreas = new List(); + } +} \ No newline at end of file diff --git a/Runtime/PSXMesh.cs b/Runtime/PSXMesh.cs index 809f762..618864f 100644 --- a/Runtime/PSXMesh.cs +++ b/Runtime/PSXMesh.cs @@ -1,14 +1,26 @@ using System.Collections.Generic; using UnityEngine; -namespace PSXSplash.RuntimeCode +namespace SplashEdit.RuntimeCode { + /// + /// Represents a vertex formatted for the PSX (PlayStation) style rendering. + /// public struct PSXVertex { + // Position components in fixed-point format. public short vx, vy, vz; + // Normal vector components in fixed-point format. + public short nx, ny, nz; + // Texture coordinates. public byte u, v; + // Vertex color components. + public byte r, g, b; } + /// + /// Represents a triangle defined by three PSX vertices. + /// public struct Tri { public PSXVertex v0; @@ -16,50 +28,104 @@ namespace PSXSplash.RuntimeCode public PSXVertex v2; } + /// + /// A mesh structure that holds a list of triangles converted from a Unity mesh into the PSX format. + /// [System.Serializable] public class PSXMesh { public List Triangles; + /// + /// Creates a PSXMesh from a Unity Mesh by converting its vertices, normals, UVs, and applying shading. + /// + /// The Unity mesh to convert. + /// Width of the texture (default is 256). + /// 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, int textureWidth = 256, int textureHeight = 256, Transform transform = null) { 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; + // 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) { int vid0 = indices[i]; int vid1 = indices[i + 1]; int vid2 = indices[i + 2]; - // Convert to world space only if a transform is provided + // 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]; - PSXVertex psxV0 = ConvertToPSXVertex(v0, uv[vid0], textureWidth, textureHeight); - PSXVertex psxV1 = ConvertToPSXVertex(v1, uv[vid1], textureWidth, textureHeight); - PSXVertex psxV2 = ConvertToPSXVertex(v2, uv[vid2], textureWidth, textureHeight); + // Convert vertices to PSX format including fixed-point conversion and shading. + PSXVertex psxV0 = ConvertToPSXVertex(v0, normals[vid0], uv[vid0], lightDir, lightColor, textureWidth, textureHeight); + PSXVertex psxV1 = ConvertToPSXVertex(v1, normals[vid1], uv[vid1], lightDir, lightColor, textureWidth, textureHeight); + PSXVertex psxV2 = ConvertToPSXVertex(v2, normals[vid2], uv[vid2], lightDir, lightColor, textureWidth, textureHeight); + // Add the constructed triangle to the mesh. psxMesh.Triangles.Add(new Tri { v0 = psxV0, v1 = psxV1, v2 = psxV2 }); } return psxMesh; } - private static PSXVertex ConvertToPSXVertex(Vector3 vertex, Vector2 uv, int textureWidth, int textureHeight) + /// + /// Converts a Unity vertex into a PSXVertex by applying fixed-point conversion, shading, and UV mapping. + /// + /// The position of the vertex. + /// The normal vector at the vertex. + /// Texture coordinates for the vertex. + /// The light direction used for shading calculations. + /// The color of the light affecting the vertex. + /// 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, Vector3 normal, Vector2 uv, Vector3 lightDir, Color lightColor, 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)); + // Remap the intensity to a specific range for a softer shading effect. + lightIntensity = Mathf.Lerp(0.4f, 0.7f, lightIntensity); + + // Compute the final shaded color by multiplying the light color by the intensity. + Color shadedColor = lightColor * lightIntensity; + PSXVertex psxVertex = new PSXVertex { + // Convert position to fixed-point, clamping values to a defined range. vx = (short)(Mathf.Clamp(vertex.x, -4f, 3.999f) * 4096), vy = (short)(Mathf.Clamp(-vertex.y, -4f, 3.999f) * 4096), vz = (short)(Mathf.Clamp(vertex.z, -4f, 3.999f) * 4096), - u = (byte)(Mathf.Clamp((uv.x * (textureWidth-1)), 0, 255)), - v = (byte)(Mathf.Clamp(((1.0f - uv.y) * (textureHeight-1)), 0, 255)) + + // Convert normals to fixed-point. + nx = (short)(Mathf.Clamp(normal.x, -4f, 3.999f) * 4096), + ny = (short)(Mathf.Clamp(-normal.y, -4f, 3.999f) * 4096), + nz = (short)(Mathf.Clamp(normal.z, -4f, 3.999f) * 4096), + + // 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)), + + // 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 db959b5..38d7dc7 100644 --- a/Runtime/PSXObjectExporter.cs +++ b/Runtime/PSXObjectExporter.cs @@ -1,41 +1,50 @@ using UnityEngine; -namespace PSXSplash.RuntimeCode +namespace SplashEdit.RuntimeCode { public class PSXObjectExporter : MonoBehaviour { - public PSXBPP BitDepth; - public bool MeshIsStatic = true; + 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; + public PSXTexture2D Texture; // Stores the converted PlayStation-style texture [HideInInspector] - public PSXMesh Mesh; + public PSXMesh Mesh; // Stores the converted PlayStation-style mesh + /// + /// Converts the object's material texture into a PlayStation-compatible texture. + /// public void CreatePSXTexture2D() { Renderer renderer = GetComponent(); if (renderer != null && renderer.sharedMaterial != null && renderer.sharedMaterial.mainTexture is Texture2D texture) { Texture = PSXTexture2D.CreateFromTexture2D(texture, BitDepth); - Texture.OriginalTexture = texture; + Texture.OriginalTexture = texture; // Stores reference to the original texture } } + /// + /// Converts the object's mesh into a PlayStation-compatible mesh. + /// public void CreatePSXMesh() { MeshFilter meshFilter = gameObject.GetComponent(); if (meshFilter != null) { - if(MeshIsStatic) { + if (MeshIsStatic) + { + // Static meshes take object transformation into account Mesh = PSXMesh.CreateFromUnityMesh(meshFilter.sharedMesh, Texture.Width, Texture.Height, transform); } - else { + else + { + // Dynamic meshes do not consider object transformation Mesh = PSXMesh.CreateFromUnityMesh(meshFilter.sharedMesh, Texture.Width, Texture.Height); } } } } } - diff --git a/Runtime/PSXSceneExporter.cs b/Runtime/PSXSceneExporter.cs index 597ae68..5799463 100644 --- a/Runtime/PSXSceneExporter.cs +++ b/Runtime/PSXSceneExporter.cs @@ -5,7 +5,7 @@ using UnityEditor.Overlays; using UnityEngine; using UnityEngine.SceneManagement; -namespace PSXSplash.RuntimeCode +namespace SplashEdit.RuntimeCode { [ExecuteInEditMode] @@ -21,7 +21,7 @@ namespace PSXSplash.RuntimeCode private bool verticalLayout; private List prohibitedAreas; private VRAMPixel[,] vramPixels; - + public void Export() @@ -54,57 +54,82 @@ namespace PSXSplash.RuntimeCode var packed = tp.PackTexturesIntoVRAM(_exporters); _exporters = packed.processedObjects; vramPixels = packed._vramPixels; - + } - void ExportFile() { + void ExportFile() + { string path = EditorUtility.SaveFilePanel("Select Output File", "", "output", "bin"); - int totalFaces = 0; - using (BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create))) + int totalFaces = 0; + using (BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create))) + { + // VramPixels are always 1MB + for (int y = 0; y < vramPixels.GetLength(1); y++) { - // 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()); - } - } - writer.Write((ushort) _exporters.Length); - foreach(PSXObjectExporter exporter in _exporters) { - - int expander = 16 / ((int) exporter.Texture.BitDepth); - - 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); - foreach(Tri tri in exporter.Mesh.Triangles) { - writer.Write((short)tri.v0.vx); - writer.Write((short)tri.v0.vy); - writer.Write((short)tri.v0.vz); - writer.Write((byte)(tri.v0.u + exporter.Texture.PackingX * expander)); - writer.Write((byte)(tri.v0.v + exporter.Texture.PackingY)); - - writer.Write((short)tri.v1.vx); - writer.Write((short)tri.v1.vy); - writer.Write((short)tri.v1.vz); - writer.Write((byte)(tri.v1.u + exporter.Texture.PackingX * expander)); - writer.Write((byte)(tri.v1.v + exporter.Texture.PackingY)); - - writer.Write((short)tri.v2.vx); - writer.Write((short)tri.v2.vy); - writer.Write((short)tri.v2.vz); - writer.Write((byte)(tri.v2.u + exporter.Texture.PackingX * expander)); - writer.Write((byte)(tri.v2.v + exporter.Texture.PackingY)); - } - } + for (int x = 0; x < vramPixels.GetLength(0); x++) + { + writer.Write(vramPixels[x, y].Pack()); + } } - Debug.Log(totalFaces); + writer.Write((ushort)_exporters.Length); + foreach (PSXObjectExporter exporter in _exporters) + { + + int expander = 16 / ((int)exporter.Texture.BitDepth); + + 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); + foreach (Tri tri in exporter.Mesh.Triangles) + { + 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); + 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); + + } + } + } + Debug.Log(totalFaces); } public void LoadData() diff --git a/Runtime/PSXTexture2D.cs b/Runtime/PSXTexture2D.cs index 49614e4..975b26b 100644 --- a/Runtime/PSXTexture2D.cs +++ b/Runtime/PSXTexture2D.cs @@ -1,9 +1,8 @@ using System.Collections.Generic; using UnityEngine; -using static PSXSplash.RuntimeCode.TextureQuantizer; -namespace PSXSplash.RuntimeCode +namespace SplashEdit.RuntimeCode { /// @@ -162,7 +161,7 @@ namespace PSXSplash.RuntimeCode psxTex._maxColors = (int)Mathf.Pow((int)bitDepth, 2); - QuantizedResult result = Quantize(inputTexture, psxTex._maxColors); + TextureQuantizer.QuantizedResult result = TextureQuantizer.Quantize(inputTexture, psxTex._maxColors); foreach (Vector3 color in result.Palette) { diff --git a/Runtime/TexturePacker.cs b/Runtime/TexturePacker.cs index f06819a..fd2a9c8 100644 --- a/Runtime/TexturePacker.cs +++ b/Runtime/TexturePacker.cs @@ -1,76 +1,102 @@ using System.Collections.Generic; using System.Linq; -using NUnit.Framework; using UnityEngine; - - -namespace PSXSplash.RuntimeCode +namespace SplashEdit.RuntimeCode { - + /// + /// Represents a texture atlas that groups PSX textures by bit depth. + /// Each atlas has a fixed height and a configurable width based on texture bit depth. + /// public class TextureAtlas { - public PSXBPP BitDepth; - public int PositionX; - public int PositionY; - public int Width; - public const int Height = 256; - public List ContainedTextures = new List(); + public PSXBPP BitDepth; // Bit depth of textures in this atlas. + public int PositionX; // X position of the atlas in VRAM. + 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 List ContainedTextures = new List(); // Textures packed in this atlas. } + /// + /// Packs PSX textures into a simulated VRAM. + /// It manages texture atlases, placement of textures, and allocation of color lookup tables (CLUTs). + /// public class VRAMPacker { private List _textureAtlases = new List(); - private List _reservedAreas; - private List _finalizedAtlases = new List(); - private List _allocatedCLUTs = new List(); + private List _reservedAreas; // Areas in VRAM where no textures can be placed. + private List _finalizedAtlases = new List(); // Atlases that have been successfully placed. + private List _allocatedCLUTs = new List(); // Allocated regions for CLUTs. private const int VRAM_WIDTH = 1024; private const int VRAM_HEIGHT = 512; - private VRAMPixel[,] _vramPixels; + private VRAMPixel[,] _vramPixels; // Simulated VRAM pixel data. + /// + /// Initializes the VRAMPacker with reserved areas from prohibited regions and framebuffers. + /// + /// Framebuffers to reserve in VRAM. + /// Additional prohibited areas as ProhibitedArea instances. public VRAMPacker(List framebuffers, List reservedAreas) { + // Convert ProhibitedArea instances to Unity Rects. List areasConvertedToRect = new List(); foreach (ProhibitedArea area in reservedAreas) { areasConvertedToRect.Add(new Rect(area.X, area.Y, area.Width, area.Height)); } _reservedAreas = areasConvertedToRect; + + // Reserve the two framebuffers. _reservedAreas.Add(framebuffers[0]); _reservedAreas.Add(framebuffers[1]); + _vramPixels = new VRAMPixel[VRAM_WIDTH, VRAM_HEIGHT]; } + /// + /// Packs the textures from the provided PSXObjectExporter array into VRAM. + /// 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, 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); foreach (var group in groupedObjects) { + // Determine atlas width based on texture bit depth. int atlasWidth = group.Key switch { PSXBPP.TEX_16BIT => 256, - PSXBPP.TEX_8BIT => 128, - PSXBPP.TEX_4BIT => 64, - _ => 256 + PSXBPP.TEX_8BIT => 128, + PSXBPP.TEX_4BIT => 64, + _ => 256 }; + // Create a new 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)) { - /*if (uniqueTextures.Any(tex => tex.OriginalTexture.GetInstanceID() == obj.Texture.OriginalTexture.GetInstanceID() && tex.BitDepth == obj.Texture.BitDepth)) + // Remove duplicate textures + 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)) { + // If failed, create a new atlas and try again. atlas = new TextureAtlas { BitDepth = group.Key, Width = atlasWidth, PositionX = 0, PositionY = 0 }; _textureAtlases.Add(atlas); if (!TryPlaceTextureInAtlas(atlas, obj.Texture)) @@ -83,20 +109,33 @@ namespace PSXSplash.RuntimeCode } } + // Arrange atlases in the VRAM space. 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, _vramPixels); } + /// + /// Attempts to place a texture within the given atlas. + /// Iterates over possible positions and checks for overlapping textures. + /// + /// The atlas where the texture should be placed. + /// The texture to place. + /// True if the texture was placed successfully; otherwise, false. private bool TryPlaceTextureInAtlas(TextureAtlas atlas, PSXTexture2D texture) { + // Iterate over potential Y positions. for (byte y = 0; y <= TextureAtlas.Height - texture.Height; y++) { + // Iterate over potential X positions within the atlas. for (byte x = 0; x <= atlas.Width - texture.QuantizedWidth; x++) { var candidateRect = new Rect(x, y, texture.QuantizedWidth, texture.Height); + // Check if candidateRect overlaps with any already placed texture. if (!atlas.ContainedTextures.Any(tex => new Rect(tex.PackingX, tex.PackingY, tex.QuantizedWidth, tex.Height).Overlaps(candidateRect))) { texture.PackingX = x; @@ -109,17 +148,25 @@ namespace PSXSplash.RuntimeCode return false; } + /// + /// Arranges all texture atlases into the VRAM, ensuring they do not overlap reserved areas. + /// Also assigns texpage indices for textures based on atlas position. + /// private void ArrangeAtlasesInVRAM() { + // Process each bit depth category in order. foreach (var bitDepth in new[] { PSXBPP.TEX_16BIT, PSXBPP.TEX_8BIT, PSXBPP.TEX_4BIT }) { foreach (var atlas in _textureAtlases.Where(a => a.BitDepth == bitDepth)) { bool placed = false; + // Try every possible row (stepping by atlas height). for (int y = 0; y <= VRAM_HEIGHT - TextureAtlas.Height; y += 256) { + // Try every possible column (stepping by 64 pixels). for (int x = 0; x <= VRAM_WIDTH - atlas.Width; x += 64) { + // Only consider atlases that haven't been placed yet. if (atlas.PositionX == 0 && atlas.PositionY == 0) { var candidateRect = new Rect(x, y, atlas.Width, TextureAtlas.Height); @@ -136,11 +183,11 @@ namespace PSXSplash.RuntimeCode } if (placed) { + // Assign texpage coordinates for each texture within the atlas. foreach (PSXTexture2D texture in atlas.ContainedTextures) { int colIndex = atlas.PositionX / 64; int rowIndex = atlas.PositionY / 256; - texture.TexpageX = (byte)colIndex; texture.TexpageY = (byte)rowIndex; } @@ -155,10 +202,14 @@ namespace PSXSplash.RuntimeCode } } + /// + /// Allocates color lookup table (CLUT) regions in VRAM for textures with palettes. + /// private void AllocateCLUTs() { foreach (var texture in _finalizedAtlases.SelectMany(atlas => atlas.ContainedTextures)) { + // Skip textures without a color palette. if (texture.ColorPalette == null || texture.ColorPalette.Count == 0) continue; @@ -166,6 +217,7 @@ namespace PSXSplash.RuntimeCode int clutHeight = 1; bool placed = false; + // Iterate over possible CLUT positions in VRAM. for (ushort x = 0; x < VRAM_WIDTH; x += 16) { for (ushort y = 0; y <= VRAM_HEIGHT; y++) @@ -190,13 +242,16 @@ namespace PSXSplash.RuntimeCode } } + /// + /// Builds the final VRAM by copying texture image data and color palettes into the VRAM pixel array. + /// private void BuildVram() { foreach (TextureAtlas atlas in _finalizedAtlases) { 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++) @@ -205,6 +260,7 @@ namespace PSXSplash.RuntimeCode } } + // For non-16-bit textures, copy the color palette into VRAM. if (texture.BitDepth != PSXBPP.TEX_16BIT) { for (int x = 0; x < texture.ColorPalette.Count; x++) @@ -216,19 +272,35 @@ namespace PSXSplash.RuntimeCode } } + /// + /// Checks if a given rectangle can be placed in VRAM without overlapping existing atlases, + /// reserved areas, or allocated CLUT regions. + /// + /// The rectangle representing a candidate placement. + /// True if the placement is valid; otherwise, false. private bool IsPlacementValid(Rect rect) { - + // Ensure the rectangle fits within VRAM boundaries. if (rect.x + rect.width > VRAM_WIDTH) return false; if (rect.y + rect.height > VRAM_HEIGHT) return false; + // Check for overlaps with existing atlases. bool overlapsAtlas = _finalizedAtlases.Any(a => new Rect(a.PositionX, a.PositionY, a.Width, TextureAtlas.Height).Overlaps(rect)); + // Check for overlaps with reserved VRAM areas. bool overlapsReserved = _reservedAreas.Any(r => r.Overlaps(rect)); + // Check for overlaps with already allocated CLUT regions. bool overlapsCLUT = _allocatedCLUTs.Any(c => c.Overlaps(rect)); return !(overlapsAtlas || overlapsReserved || overlapsCLUT); } + /// + /// Calculates the texpage index from given VRAM coordinates. + /// This helper method divides VRAM into columns and rows. + /// + /// The X coordinate in VRAM. + /// The Y coordinate in VRAM. + /// The calculated texpage index. private int CalculateTexpage(int x, int y) { int columns = 16; @@ -237,4 +309,4 @@ namespace PSXSplash.RuntimeCode return (rowIndex * columns) + colIndex; } } -} \ No newline at end of file +} diff --git a/Runtime/Utils.cs b/Runtime/Utils.cs index acc42db..1c17997 100644 --- a/Runtime/Utils.cs +++ b/Runtime/Utils.cs @@ -1,14 +1,26 @@ using UnityEngine; -namespace PSXSplash.RuntimeCode +namespace SplashEdit.RuntimeCode { + /// + /// 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. + /// public class ProhibitedArea { + // X and Y coordinates of the prohibited area in VRAM. public int X; public int Y; + // Width and height of the prohibited area. public int Width; public int Height; + /// + /// Creates a ProhibitedArea instance from a Unity Rect. + /// The floating-point values of the Rect are rounded to the nearest integer. + /// + /// The Unity Rect representing the prohibited area. + /// A new ProhibitedArea with integer dimensions. public static ProhibitedArea FromUnityRect(Rect rect) { return new ProhibitedArea @@ -20,9 +32,13 @@ namespace PSXSplash.RuntimeCode }; } + /// + /// Converts the ProhibitedArea back into a Unity Rect. + /// + /// A Unity Rect with the same area as defined by this ProhibitedArea. public Rect ToUnityRect() { return new Rect(X, Y, Width, Height); } } -} \ No newline at end of file +}