Preparation for release. Comments, fixes, README

This commit is contained in:
2025-03-17 14:32:54 +01:00
parent 8a6679dff6
commit 7b127b345b
13 changed files with 839 additions and 476 deletions

View File

@@ -1,19 +1,22 @@
using UnityEngine; using UnityEngine;
using UnityEditor; using UnityEditor;
using PSXSplash.RuntimeCode; using SplashEdit.RuntimeCode;
[CustomEditor(typeof(PSXSceneExporter))] namespace SplashEdit.EditorCode
public class PSXSceneExporterEditor : Editor
{ {
public override void OnInspectorGUI() [CustomEditor(typeof(PSXSceneExporter))]
public class PSXSceneExporterEditor : Editor
{ {
DrawDefaultInspector(); public override void OnInspectorGUI()
PSXSceneExporter comp = (PSXSceneExporter)target;
if (GUILayout.Button("Export"))
{ {
comp.Export(); DrawDefaultInspector();
}
PSXSceneExporter comp = (PSXSceneExporter)target;
if (GUILayout.Button("Export"))
{
comp.Export();
}
}
} }
} }

View File

@@ -1,185 +1,187 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using PSXSplash.RuntimeCode; using SplashEdit.RuntimeCode;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using UnityEngine.Rendering; using UnityEngine.Rendering;
public class QuantizedPreviewWindow : EditorWindow namespace SplashEdit.EditorCode
{ {
private Texture2D originalTexture; public class QuantizedPreviewWindow : EditorWindow
private Texture2D quantizedTexture;
private Texture2D vramTexture; // New VRAM Texture
private List<VRAMPixel> 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()
{ {
QuantizedPreviewWindow win = GetWindow<QuantizedPreviewWindow>("Quantized Preview"); private Texture2D originalTexture;
win.minSize = new Vector2(800, 700); private Texture2D quantizedTexture;
} private Texture2D vramTexture; // VRAM representation of the texture
private List<VRAMPixel> 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() [MenuItem("Window/Quantized Preview")]
{ public static void ShowWindow()
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)
{ {
GenerateQuantizedPreview(); // Creates and displays the window
QuantizedPreviewWindow win = GetWindow<QuantizedPreviewWindow>("Quantized Preview");
win.minSize = new Vector2(800, 700);
} }
GUILayout.BeginHorizontal(); private void OnGUI()
if (originalTexture != null)
{ {
GUILayout.BeginVertical(); GUILayout.Label("Quantized Preview", EditorStyles.boldLabel);
GUILayout.Label("Original Texture");
DrawTexturePreview(originalTexture, previewSize, false);
GUILayout.EndVertical();
}
if (vramTexture != null) // Texture input field
{ originalTexture = (Texture2D)EditorGUILayout.ObjectField("Original Texture", originalTexture, typeof(Texture2D), false);
GUILayout.BeginVertical();
GUILayout.Label("VRAM View (Indexed Data as 16bpp)");
DrawTexturePreview(vramTexture, previewSize);
GUILayout.EndVertical();
}
if (quantizedTexture != null) // Dropdown for bit depth selection
{ bpp = (PSXBPP)EditorGUILayout.EnumPopup("Bit Depth", bpp);
GUILayout.BeginVertical();
GUILayout.Label("Quantized Texture");
DrawTexturePreview(quantizedTexture, previewSize);
GUILayout.EndVertical();
}
GUILayout.EndHorizontal(); // Button to generate the quantized preview
if (GUILayout.Button("Generate Quantized Preview") && originalTexture != null)
if (clut != null)
{
GUILayout.Label("Color Lookup Table (CLUT)");
DrawCLUT();
}
GUILayout.Space(10);
if (indexedPixelData != null)
{
if (GUILayout.Button("Export texute data"))
{ {
string path = EditorUtility.SaveFilePanel( GenerateQuantizedPreview();
"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);
}
}
}
} }
}
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(); GUILayout.BeginHorizontal();
int colorsInRow = Mathf.Min(maxColorsPerRow, totalColors - row * maxColorsPerRow); // Display the original texture
if (originalTexture != null)
for (int col = 0; col < colorsInRow; col++)
{ {
int index = row * maxColorsPerRow + col; GUILayout.BeginVertical();
GUILayout.Label("Original Texture");
DrawTexturePreview(originalTexture, previewSize, false);
GUILayout.EndVertical();
}
Vector3 color = new Vector3( // Display the VRAM view of the texture
clut[index].R / 31.0f, // Red: bits 04 if (vramTexture != null)
clut[index].G / 31.0f, // Green: bits 59 {
clut[index].B / 31.0f // Blue: bits 1014 GUILayout.BeginVertical();
); GUILayout.Label("VRAM View (Indexed Data as 16bpp)");
DrawTexturePreview(vramTexture, previewSize);
GUILayout.EndVertical();
}
Rect rect = GUILayoutUtility.GetRect(swatchSize, swatchSize, GUILayout.ExpandWidth(false)); // Display the quantized texture
EditorGUI.DrawRect(rect, new Color(color.x, color.y, color.z)); if (quantizedTexture != null)
{
GUILayout.BeginVertical();
GUILayout.Label("Quantized Texture");
DrawTexturePreview(quantizedTexture, previewSize);
GUILayout.EndVertical();
} }
GUILayout.EndHorizontal(); 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 04
clut[index].G / 31.0f, // Green: bits 59
clut[index].B / 31.0f // Blue: bits 1014
);
// 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();
}
} }
} }
} }

View File

@@ -1,31 +1,33 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using PSXSplash.RuntimeCode; using SplashEdit.RuntimeCode;
using Unity.Collections; using Unity.Collections;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using UnityEngine.Rendering; using UnityEngine.Rendering;
public class VRAMEditorWindow : EditorWindow
namespace SplashEdit.EditorCode
{ {
public class VRAMEditorWindow : EditorWindow
private const int VramWidth = 1024;
private const int VramHeight = 512;
private List<ProhibitedArea> prohibitedAreas = new List<ProhibitedArea>();
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 =
{ {
private const int VramWidth = 1024;
private const int VramHeight = 512;
private List<ProhibitedArea> prohibitedAreas = new List<ProhibitedArea>();
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(256, 240), new Vector2(256, 480),
new Vector2(320, 240), new Vector2(320, 480), new Vector2(320, 240), new Vector2(320, 480),
new Vector2(368, 240), new Vector2(368, 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) new Vector2(640, 240), new Vector2(640, 480)
}; };
[MenuItem("Window/VRAM Editor")] [MenuItem("Window/VRAM Editor")]
public static void ShowWindow() public static void ShowWindow()
{
GetWindow<VRAMEditorWindow>("VRAM Editor");
}
private void OnEnable()
{
vramImage = new Texture2D(VramWidth, VramHeight);
NativeArray<Color32> blackPixels = new NativeArray<Color32>(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)
{ {
Debug.LogError("Textures cannot be null!"); VRAMEditorWindow window = GetWindow<VRAMEditorWindow>("VRAM Editor");
return; // Set minimum window dimensions.
window.minSize = new Vector2(1600, 600);
} }
Color[] overlayPixels = overlayTexture.GetPixels(); private void OnEnable()
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++)
{ {
for (int x = 0; x < overlayWidth; x++) // Initialize VRAM texture with black pixels.
{ vramImage = new Texture2D(VramWidth, VramHeight);
int baseX = posX + x; NativeArray<Color32> blackPixels = new NativeArray<Color32>(VramWidth * VramHeight, Allocator.Temp);
int baseY = posY + y; vramImage.SetPixelData(blackPixels, 0);
if (baseX >= 0 && baseX < baseWidth && baseY >= 0 && baseY < baseHeight) vramImage.Apply();
{ blackPixels.Dispose();
int baseIndex = baseY * baseWidth + baseX;
int overlayIndex = y * overlayWidth + x;
basePixels[baseIndex] = overlayPixels[overlayIndex]; // Ensure minimum window size is applied.
this.minSize = new Vector2(800, 600);
LoadData();
}
/// <summary>
/// Pastes an overlay texture onto a base texture at the specified position.
/// </summary>
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); /// <summary>
baseTexture.Apply(); /// Packs PSX textures into VRAM, rebuilds the VRAM texture and writes binary data to an output file.
} /// </summary>
private void PackTextures() private void PackTextures()
{
vramImage = new Texture2D(VramWidth, VramHeight);
NativeArray<Color32> blackPixels = new NativeArray<Color32>(VramWidth * VramHeight, Allocator.Temp);
vramImage.SetPixelData(blackPixels, 0);
vramImage.Apply();
blackPixels.Dispose();
PSXObjectExporter[] objects = FindObjectsByType<PSXObjectExporter>(FindObjectsSortMode.None);
foreach (PSXObjectExporter exp in objects)
{ {
exp.CreatePSXTexture2D(); // Reinitialize VRAM texture with black pixels.
} vramImage = new Texture2D(VramWidth, VramHeight);
NativeArray<Color32> blackPixels = new NativeArray<Color32>(VramWidth * VramHeight, Allocator.Temp);
vramImage.SetPixelData(blackPixels, 0);
vramImage.Apply();
blackPixels.Dispose();
Rect buffer1 = new Rect(0, 0, selectedResolution.x, selectedResolution.y); // Retrieve all PSXObjectExporter objects and create their PSX textures.
Rect buffer2 = verticalLayout ? new Rect(0, 256, selectedResolution.x, selectedResolution.y) PSXObjectExporter[] objects = FindObjectsByType<PSXObjectExporter>(FindObjectsSortMode.None);
: new Rect(selectedResolution.x, 0, selectedResolution.x, selectedResolution.y); foreach (PSXObjectExporter exp in objects)
List<Rect> framebuffers = new List<Rect> { 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++)
{ {
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<Rect> framebuffers = new List<Rect> { 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 y = 0; y < VramHeight; y++)
{ {
for (int x = 0; x < VramWidth; x++) 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()
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)
{ {
dualBuffering = EditorGUILayout.Toggle("Dual Buffering", dualBuffering); GUILayout.BeginHorizontal();
} GUILayout.BeginVertical();
else { dualBuffering = false; } GUILayout.Label("VRAM Editor", EditorStyles.boldLabel);
if (canDBVertical && canDBHorizontal) // Dropdown for resolution selection.
{ selectedResolution = resolutions[EditorGUILayout.Popup("Resolution", System.Array.IndexOf(resolutions, selectedResolution),
verticalLayout = EditorGUILayout.Toggle("Vertical", verticalLayout); new string[] { "256x240", "256x480", "320x240", "320x480", "368x240", "368x480", "512x240", "512x480", "640x240", "640x480" })];
}
else if (canDBVertical) { verticalLayout = true; }
else
{
verticalLayout = false;
}
GUILayout.Space(10); // Check resolution constraints for dual buffering.
GUILayout.Label("Prohibited areas", EditorStyles.boldLabel); bool canDBHorizontal = selectedResolution[0] * 2 <= 1024;
scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.MaxHeight(150f)); bool canDBVertical = selectedResolution[1] * 2 <= 512;
for (int i = 0; i < prohibitedAreas.Count; i++) if (canDBHorizontal || canDBVertical)
{
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); dualBuffering = EditorGUILayout.Toggle("Dual Buffering", dualBuffering);
break; }
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.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(); /// <summary>
GUILayout.Space(10); /// Loads stored PSX data from the asset.
if (GUILayout.Button("Add Prohibited Area")) /// </summary>
private void LoadData()
{ {
prohibitedAreas.Add(new ProhibitedArea()); _psxData = AssetDatabase.LoadAssetAtPath<PSXData>(_psxDataPath);
if (!_psxData)
{
_psxData = CreateInstance<PSXData>();
AssetDatabase.CreateAsset(_psxData, _psxDataPath);
AssetDatabase.SaveAssets();
}
selectedResolution = _psxData.OutputResolution;
dualBuffering = _psxData.DualBuffering;
verticalLayout = _psxData.VerticalBuffering;
prohibitedAreas = _psxData.ProhibitedAreas;
} }
if (GUILayout.Button("Pack Textures")) /// <summary>
/// Stores current configuration to the PSX data asset.
/// This is now triggered manually via the "Save Settings" button.
/// </summary>
private void StoreData()
{ {
PackTextures(); if (_psxData != null)
} {
_psxData.OutputResolution = selectedResolution;
_psxData.DualBuffering = dualBuffering;
_psxData.VerticalBuffering = verticalLayout;
_psxData.ProhibitedAreas = prohibitedAreas;
GUILayout.EndVertical(); EditorUtility.SetDirty(_psxData);
AssetDatabase.SaveAssets();
Rect vramRect = GUILayoutUtility.GetRect(VramWidth, VramHeight, GUILayout.ExpandWidth(false)); AssetDatabase.Refresh();
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<PSXData>(_psxDataPath);
if (!_psxData)
{
_psxData = CreateInstance<PSXData>();
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();
} }
} }
} }

122
README.md Normal file
View File

@@ -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.

7
README.md.meta Normal file
View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 4df40ce535b32f3a4b30ce0803fa699a
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -3,7 +3,7 @@ using System.Linq;
using UnityEngine; using UnityEngine;
namespace PSXSplash.RuntimeCode namespace SplashEdit.RuntimeCode
{ {
/// <summary> /// <summary>

View File

@@ -1,12 +1,14 @@
using System.Collections.Generic; using System.Collections.Generic;
using PSXSplash.RuntimeCode;
using UnityEngine; using UnityEngine;
[CreateAssetMenu(fileName = "PSXData", menuName = "Scriptable Objects/PSXData")] namespace SplashEdit.RuntimeCode
public class PSXData : ScriptableObject
{ {
public Vector2 OutputResolution = new Vector2(320, 240); [CreateAssetMenu(fileName = "PSXData", menuName = "Scriptable Objects/PSXData")]
public bool DualBuffering = true; public class PSXData : ScriptableObject
public bool VerticalBuffering = true; {
public List<ProhibitedArea> ProhibitedAreas = new List<ProhibitedArea>(); public Vector2 OutputResolution = new Vector2(320, 240);
public bool DualBuffering = true;
public bool VerticalBuffering = true;
public List<ProhibitedArea> ProhibitedAreas = new List<ProhibitedArea>();
}
} }

View File

@@ -1,14 +1,26 @@
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine; using UnityEngine;
namespace PSXSplash.RuntimeCode namespace SplashEdit.RuntimeCode
{ {
/// <summary>
/// Represents a vertex formatted for the PSX (PlayStation) style rendering.
/// </summary>
public struct PSXVertex public struct PSXVertex
{ {
// Position components in fixed-point format.
public short vx, vy, vz; public short vx, vy, vz;
// Normal vector components in fixed-point format.
public short nx, ny, nz;
// Texture coordinates.
public byte u, v; public byte u, v;
// Vertex color components.
public byte r, g, b;
} }
/// <summary>
/// Represents a triangle defined by three PSX vertices.
/// </summary>
public struct Tri public struct Tri
{ {
public PSXVertex v0; public PSXVertex v0;
@@ -16,50 +28,104 @@ namespace PSXSplash.RuntimeCode
public PSXVertex v2; public PSXVertex v2;
} }
/// <summary>
/// A mesh structure that holds a list of triangles converted from a Unity mesh into the PSX format.
/// </summary>
[System.Serializable] [System.Serializable]
public class PSXMesh public class PSXMesh
{ {
public List<Tri> Triangles; public List<Tri> Triangles;
/// <summary>
/// Creates a PSXMesh from a Unity Mesh by converting its vertices, normals, UVs, and applying shading.
/// </summary>
/// <param name="mesh">The Unity mesh to convert.</param>
/// <param name="textureWidth">Width of the texture (default is 256).</param>
/// <param name="textureHeight">Height of the texture (default is 256).</param>
/// <param name="transform">Optional transform to convert vertices to world space.</param>
/// <returns>A new PSXMesh containing the converted triangles.</returns>
public static PSXMesh CreateFromUnityMesh(Mesh mesh, int textureWidth = 256, int textureHeight = 256, Transform transform = null) public static PSXMesh CreateFromUnityMesh(Mesh mesh, int textureWidth = 256, int textureHeight = 256, Transform transform = null)
{ {
PSXMesh psxMesh = new PSXMesh { Triangles = new List<Tri>() }; PSXMesh psxMesh = new PSXMesh { Triangles = new List<Tri>() };
// Get mesh data arrays.
Vector3[] vertices = mesh.vertices; Vector3[] vertices = mesh.vertices;
Vector3[] normals = mesh.normals;
Vector2[] uv = mesh.uv; Vector2[] uv = mesh.uv;
int[] indices = mesh.triangles; 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) for (int i = 0; i < indices.Length; i += 3)
{ {
int vid0 = indices[i]; int vid0 = indices[i];
int vid1 = indices[i + 1]; int vid1 = indices[i + 1];
int vid2 = indices[i + 2]; 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 v0 = transform ? transform.TransformPoint(vertices[vid0]) : vertices[vid0];
Vector3 v1 = transform ? transform.TransformPoint(vertices[vid1]) : vertices[vid1]; Vector3 v1 = transform ? transform.TransformPoint(vertices[vid1]) : vertices[vid1];
Vector3 v2 = transform ? transform.TransformPoint(vertices[vid2]) : vertices[vid2]; Vector3 v2 = transform ? transform.TransformPoint(vertices[vid2]) : vertices[vid2];
PSXVertex psxV0 = ConvertToPSXVertex(v0, uv[vid0], textureWidth, textureHeight); // Convert vertices to PSX format including fixed-point conversion and shading.
PSXVertex psxV1 = ConvertToPSXVertex(v1, uv[vid1], textureWidth, textureHeight); PSXVertex psxV0 = ConvertToPSXVertex(v0, normals[vid0], uv[vid0], lightDir, lightColor, textureWidth, textureHeight);
PSXVertex psxV2 = ConvertToPSXVertex(v2, uv[vid2], 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 }); psxMesh.Triangles.Add(new Tri { v0 = psxV0, v1 = psxV1, v2 = psxV2 });
} }
return psxMesh; return psxMesh;
} }
private static PSXVertex ConvertToPSXVertex(Vector3 vertex, Vector2 uv, int textureWidth, int textureHeight) /// <summary>
/// Converts a Unity vertex into a PSXVertex by applying fixed-point conversion, shading, and UV mapping.
/// </summary>
/// <param name="vertex">The position of the vertex.</param>
/// <param name="normal">The normal vector at the vertex.</param>
/// <param name="uv">Texture coordinates for the vertex.</param>
/// <param name="lightDir">The light direction used for shading calculations.</param>
/// <param name="lightColor">The color of the light affecting the vertex.</param>
/// <param name="textureWidth">Width of the texture for UV scaling.</param>
/// <param name="textureHeight">Height of the texture for UV scaling.</param>
/// <returns>A PSXVertex with converted coordinates, normals, UVs, and color.</returns>
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 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), vx = (short)(Mathf.Clamp(vertex.x, -4f, 3.999f) * 4096),
vy = (short)(Mathf.Clamp(-vertex.y, -4f, 3.999f) * 4096), vy = (short)(Mathf.Clamp(-vertex.y, -4f, 3.999f) * 4096),
vz = (short)(Mathf.Clamp(vertex.z, -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; return psxVertex;
} }
} }

View File

@@ -1,41 +1,50 @@
using UnityEngine; using UnityEngine;
namespace PSXSplash.RuntimeCode namespace SplashEdit.RuntimeCode
{ {
public class PSXObjectExporter : MonoBehaviour public class PSXObjectExporter : MonoBehaviour
{ {
public PSXBPP BitDepth; public PSXBPP BitDepth = PSXBPP.TEX_8BIT; // Defines the bit depth of the texture (e.g., 4BPP, 8BPP)
public bool MeshIsStatic = true; 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] [HideInInspector]
public PSXTexture2D Texture; public PSXTexture2D Texture; // Stores the converted PlayStation-style texture
[HideInInspector] [HideInInspector]
public PSXMesh Mesh; public PSXMesh Mesh; // Stores the converted PlayStation-style mesh
/// <summary>
/// Converts the object's material texture into a PlayStation-compatible texture.
/// </summary>
public void CreatePSXTexture2D() public void CreatePSXTexture2D()
{ {
Renderer renderer = GetComponent<Renderer>(); Renderer renderer = GetComponent<Renderer>();
if (renderer != null && renderer.sharedMaterial != null && renderer.sharedMaterial.mainTexture is Texture2D texture) if (renderer != null && renderer.sharedMaterial != null && renderer.sharedMaterial.mainTexture is Texture2D texture)
{ {
Texture = PSXTexture2D.CreateFromTexture2D(texture, BitDepth); Texture = PSXTexture2D.CreateFromTexture2D(texture, BitDepth);
Texture.OriginalTexture = texture; Texture.OriginalTexture = texture; // Stores reference to the original texture
} }
} }
/// <summary>
/// Converts the object's mesh into a PlayStation-compatible mesh.
/// </summary>
public void CreatePSXMesh() public void CreatePSXMesh()
{ {
MeshFilter meshFilter = gameObject.GetComponent<MeshFilter>(); MeshFilter meshFilter = gameObject.GetComponent<MeshFilter>();
if (meshFilter != null) if (meshFilter != null)
{ {
if(MeshIsStatic) { if (MeshIsStatic)
{
// Static meshes take object transformation into account
Mesh = PSXMesh.CreateFromUnityMesh(meshFilter.sharedMesh, Texture.Width, Texture.Height, transform); 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); Mesh = PSXMesh.CreateFromUnityMesh(meshFilter.sharedMesh, Texture.Width, Texture.Height);
} }
} }
} }
} }
} }

View File

@@ -5,7 +5,7 @@ using UnityEditor.Overlays;
using UnityEngine; using UnityEngine;
using UnityEngine.SceneManagement; using UnityEngine.SceneManagement;
namespace PSXSplash.RuntimeCode namespace SplashEdit.RuntimeCode
{ {
[ExecuteInEditMode] [ExecuteInEditMode]
@@ -57,54 +57,79 @@ namespace PSXSplash.RuntimeCode
} }
void ExportFile() { void ExportFile()
{
string path = EditorUtility.SaveFilePanel("Select Output File", "", "output", "bin"); string path = EditorUtility.SaveFilePanel("Select Output File", "", "output", "bin");
int totalFaces = 0; int totalFaces = 0;
using (BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create))) 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 x = 0; x < vramPixels.GetLength(0); x++)
for (int y = 0; y < vramPixels.GetLength(1); y++) {
{ writer.Write(vramPixels[x, y].Pack());
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));
}
}
} }
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() public void LoadData()

View File

@@ -1,9 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine; using UnityEngine;
using static PSXSplash.RuntimeCode.TextureQuantizer;
namespace PSXSplash.RuntimeCode namespace SplashEdit.RuntimeCode
{ {
/// <summary> /// <summary>
@@ -162,7 +161,7 @@ namespace PSXSplash.RuntimeCode
psxTex._maxColors = (int)Mathf.Pow((int)bitDepth, 2); 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) foreach (Vector3 color in result.Palette)
{ {

View File

@@ -1,76 +1,102 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework;
using UnityEngine; using UnityEngine;
namespace SplashEdit.RuntimeCode
namespace PSXSplash.RuntimeCode
{ {
/// <summary>
/// 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.
/// </summary>
public class TextureAtlas public class TextureAtlas
{ {
public PSXBPP BitDepth; public PSXBPP BitDepth; // Bit depth of textures in this atlas.
public int PositionX; public int PositionX; // X position of the atlas in VRAM.
public int PositionY; public int PositionY; // Y position of the atlas in VRAM.
public int Width; public int Width; // Width of the atlas.
public const int Height = 256; public const int Height = 256; // Fixed height for all atlases.
public List<PSXTexture2D> ContainedTextures = new List<PSXTexture2D>(); public List<PSXTexture2D> ContainedTextures = new List<PSXTexture2D>(); // Textures packed in this atlas.
} }
/// <summary>
/// Packs PSX textures into a simulated VRAM.
/// It manages texture atlases, placement of textures, and allocation of color lookup tables (CLUTs).
/// </summary>
public class VRAMPacker public class VRAMPacker
{ {
private List<TextureAtlas> _textureAtlases = new List<TextureAtlas>(); private List<TextureAtlas> _textureAtlases = new List<TextureAtlas>();
private List<Rect> _reservedAreas; private List<Rect> _reservedAreas; // Areas in VRAM where no textures can be placed.
private List<TextureAtlas> _finalizedAtlases = new List<TextureAtlas>(); private List<TextureAtlas> _finalizedAtlases = new List<TextureAtlas>(); // Atlases that have been successfully placed.
private List<Rect> _allocatedCLUTs = new List<Rect>(); private List<Rect> _allocatedCLUTs = new List<Rect>(); // Allocated regions for CLUTs.
private const int VRAM_WIDTH = 1024; private const int VRAM_WIDTH = 1024;
private const int VRAM_HEIGHT = 512; private const int VRAM_HEIGHT = 512;
private VRAMPixel[,] _vramPixels; private VRAMPixel[,] _vramPixels; // Simulated VRAM pixel data.
/// <summary>
/// Initializes the VRAMPacker with reserved areas from prohibited regions and framebuffers.
/// </summary>
/// <param name="framebuffers">Framebuffers to reserve in VRAM.</param>
/// <param name="reservedAreas">Additional prohibited areas as ProhibitedArea instances.</param>
public VRAMPacker(List<Rect> framebuffers, List<ProhibitedArea> reservedAreas) public VRAMPacker(List<Rect> framebuffers, List<ProhibitedArea> reservedAreas)
{ {
// Convert ProhibitedArea instances to Unity Rects.
List<Rect> areasConvertedToRect = new List<Rect>(); List<Rect> areasConvertedToRect = new List<Rect>();
foreach (ProhibitedArea area in reservedAreas) foreach (ProhibitedArea area in reservedAreas)
{ {
areasConvertedToRect.Add(new Rect(area.X, area.Y, area.Width, area.Height)); areasConvertedToRect.Add(new Rect(area.X, area.Y, area.Width, area.Height));
} }
_reservedAreas = areasConvertedToRect; _reservedAreas = areasConvertedToRect;
// Reserve the two framebuffers.
_reservedAreas.Add(framebuffers[0]); _reservedAreas.Add(framebuffers[0]);
_reservedAreas.Add(framebuffers[1]); _reservedAreas.Add(framebuffers[1]);
_vramPixels = new VRAMPixel[VRAM_WIDTH, VRAM_HEIGHT]; _vramPixels = new VRAMPixel[VRAM_WIDTH, VRAM_HEIGHT];
} }
/// <summary>
/// Packs the textures from the provided PSXObjectExporter array into VRAM.
/// Returns the processed objects and the final VRAM pixel array.
/// </summary>
/// <param name="objects">Array of PSXObjectExporter objects to process.</param>
/// <returns>Tuple containing processed objects and the VRAM pixel array.</returns>
public (PSXObjectExporter[] processedObjects, VRAMPixel[,] _vramPixels) PackTexturesIntoVRAM(PSXObjectExporter[] objects) public (PSXObjectExporter[] processedObjects, VRAMPixel[,] _vramPixels) PackTexturesIntoVRAM(PSXObjectExporter[] objects)
{ {
List<PSXTexture2D> uniqueTextures = new List<PSXTexture2D>(); List<PSXTexture2D> uniqueTextures = new List<PSXTexture2D>();
// Group objects by texture bit depth (high to low).
var groupedObjects = objects.GroupBy(obj => obj.Texture.BitDepth).OrderByDescending(g => g.Key); var groupedObjects = objects.GroupBy(obj => obj.Texture.BitDepth).OrderByDescending(g => g.Key);
foreach (var group in groupedObjects) foreach (var group in groupedObjects)
{ {
// Determine atlas width based on texture bit depth.
int atlasWidth = group.Key switch int atlasWidth = group.Key switch
{ {
PSXBPP.TEX_16BIT => 256, PSXBPP.TEX_16BIT => 256,
PSXBPP.TEX_8BIT => 128, PSXBPP.TEX_8BIT => 128,
PSXBPP.TEX_4BIT => 64, PSXBPP.TEX_4BIT => 64,
_ => 256 _ => 256
}; };
// Create a new atlas for this group.
TextureAtlas atlas = new TextureAtlas { BitDepth = group.Key, Width = atlasWidth, PositionX = 0, PositionY = 0 }; TextureAtlas atlas = new TextureAtlas { BitDepth = group.Key, Width = atlasWidth, PositionX = 0, PositionY = 0 };
_textureAtlases.Add(atlas); _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)) 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()); obj.Texture = uniqueTextures.First(tex => tex.OriginalTexture.GetInstanceID() == obj.Texture.OriginalTexture.GetInstanceID());
continue; continue;
}*/ }
// Try to place the texture in the current atlas.
if (!TryPlaceTextureInAtlas(atlas, obj.Texture)) 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 }; atlas = new TextureAtlas { BitDepth = group.Key, Width = atlasWidth, PositionX = 0, PositionY = 0 };
_textureAtlases.Add(atlas); _textureAtlases.Add(atlas);
if (!TryPlaceTextureInAtlas(atlas, obj.Texture)) if (!TryPlaceTextureInAtlas(atlas, obj.Texture))
@@ -83,20 +109,33 @@ namespace PSXSplash.RuntimeCode
} }
} }
// Arrange atlases in the VRAM space.
ArrangeAtlasesInVRAM(); ArrangeAtlasesInVRAM();
// Allocate color lookup tables (CLUTs) for textures that use palettes.
AllocateCLUTs(); AllocateCLUTs();
// Build the final VRAM pixel array from placed textures and CLUTs.
BuildVram(); BuildVram();
return (objects, _vramPixels); return (objects, _vramPixels);
} }
/// <summary>
/// Attempts to place a texture within the given atlas.
/// Iterates over possible positions and checks for overlapping textures.
/// </summary>
/// <param name="atlas">The atlas where the texture should be placed.</param>
/// <param name="texture">The texture to place.</param>
/// <returns>True if the texture was placed successfully; otherwise, false.</returns>
private bool TryPlaceTextureInAtlas(TextureAtlas atlas, PSXTexture2D texture) private bool TryPlaceTextureInAtlas(TextureAtlas atlas, PSXTexture2D texture)
{ {
// Iterate over potential Y positions.
for (byte y = 0; y <= TextureAtlas.Height - texture.Height; y++) 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++) for (byte x = 0; x <= atlas.Width - texture.QuantizedWidth; x++)
{ {
var candidateRect = new Rect(x, y, texture.QuantizedWidth, texture.Height); 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))) if (!atlas.ContainedTextures.Any(tex => new Rect(tex.PackingX, tex.PackingY, tex.QuantizedWidth, tex.Height).Overlaps(candidateRect)))
{ {
texture.PackingX = x; texture.PackingX = x;
@@ -109,17 +148,25 @@ namespace PSXSplash.RuntimeCode
return false; return false;
} }
/// <summary>
/// Arranges all texture atlases into the VRAM, ensuring they do not overlap reserved areas.
/// Also assigns texpage indices for textures based on atlas position.
/// </summary>
private void ArrangeAtlasesInVRAM() 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 bitDepth in new[] { PSXBPP.TEX_16BIT, PSXBPP.TEX_8BIT, PSXBPP.TEX_4BIT })
{ {
foreach (var atlas in _textureAtlases.Where(a => a.BitDepth == bitDepth)) foreach (var atlas in _textureAtlases.Where(a => a.BitDepth == bitDepth))
{ {
bool placed = false; bool placed = false;
// Try every possible row (stepping by atlas height).
for (int y = 0; y <= VRAM_HEIGHT - TextureAtlas.Height; y += 256) 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) 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) if (atlas.PositionX == 0 && atlas.PositionY == 0)
{ {
var candidateRect = new Rect(x, y, atlas.Width, TextureAtlas.Height); var candidateRect = new Rect(x, y, atlas.Width, TextureAtlas.Height);
@@ -136,11 +183,11 @@ namespace PSXSplash.RuntimeCode
} }
if (placed) if (placed)
{ {
// Assign texpage coordinates for each texture within the atlas.
foreach (PSXTexture2D texture in atlas.ContainedTextures) foreach (PSXTexture2D texture in atlas.ContainedTextures)
{ {
int colIndex = atlas.PositionX / 64; int colIndex = atlas.PositionX / 64;
int rowIndex = atlas.PositionY / 256; int rowIndex = atlas.PositionY / 256;
texture.TexpageX = (byte)colIndex; texture.TexpageX = (byte)colIndex;
texture.TexpageY = (byte)rowIndex; texture.TexpageY = (byte)rowIndex;
} }
@@ -155,10 +202,14 @@ namespace PSXSplash.RuntimeCode
} }
} }
/// <summary>
/// Allocates color lookup table (CLUT) regions in VRAM for textures with palettes.
/// </summary>
private void AllocateCLUTs() private void AllocateCLUTs()
{ {
foreach (var texture in _finalizedAtlases.SelectMany(atlas => atlas.ContainedTextures)) foreach (var texture in _finalizedAtlases.SelectMany(atlas => atlas.ContainedTextures))
{ {
// Skip textures without a color palette.
if (texture.ColorPalette == null || texture.ColorPalette.Count == 0) if (texture.ColorPalette == null || texture.ColorPalette.Count == 0)
continue; continue;
@@ -166,6 +217,7 @@ namespace PSXSplash.RuntimeCode
int clutHeight = 1; int clutHeight = 1;
bool placed = false; bool placed = false;
// Iterate over possible CLUT positions in VRAM.
for (ushort x = 0; x < VRAM_WIDTH; x += 16) for (ushort x = 0; x < VRAM_WIDTH; x += 16)
{ {
for (ushort y = 0; y <= VRAM_HEIGHT; y++) for (ushort y = 0; y <= VRAM_HEIGHT; y++)
@@ -190,13 +242,16 @@ namespace PSXSplash.RuntimeCode
} }
} }
/// <summary>
/// Builds the final VRAM by copying texture image data and color palettes into the VRAM pixel array.
/// </summary>
private void BuildVram() private void BuildVram()
{ {
foreach (TextureAtlas atlas in _finalizedAtlases) foreach (TextureAtlas atlas in _finalizedAtlases)
{ {
foreach (PSXTexture2D texture in atlas.ContainedTextures) 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 y = 0; y < texture.Height; y++)
{ {
for (int x = 0; x < texture.QuantizedWidth; x++) 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) if (texture.BitDepth != PSXBPP.TEX_16BIT)
{ {
for (int x = 0; x < texture.ColorPalette.Count; x++) for (int x = 0; x < texture.ColorPalette.Count; x++)
@@ -216,19 +272,35 @@ namespace PSXSplash.RuntimeCode
} }
} }
/// <summary>
/// Checks if a given rectangle can be placed in VRAM without overlapping existing atlases,
/// reserved areas, or allocated CLUT regions.
/// </summary>
/// <param name="rect">The rectangle representing a candidate placement.</param>
/// <returns>True if the placement is valid; otherwise, false.</returns>
private bool IsPlacementValid(Rect rect) private bool IsPlacementValid(Rect rect)
{ {
// Ensure the rectangle fits within VRAM boundaries.
if (rect.x + rect.width > VRAM_WIDTH) return false; if (rect.x + rect.width > VRAM_WIDTH) return false;
if (rect.y + rect.height > VRAM_HEIGHT) 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)); 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)); bool overlapsReserved = _reservedAreas.Any(r => r.Overlaps(rect));
// Check for overlaps with already allocated CLUT regions.
bool overlapsCLUT = _allocatedCLUTs.Any(c => c.Overlaps(rect)); bool overlapsCLUT = _allocatedCLUTs.Any(c => c.Overlaps(rect));
return !(overlapsAtlas || overlapsReserved || overlapsCLUT); return !(overlapsAtlas || overlapsReserved || overlapsCLUT);
} }
/// <summary>
/// Calculates the texpage index from given VRAM coordinates.
/// This helper method divides VRAM into columns and rows.
/// </summary>
/// <param name="x">The X coordinate in VRAM.</param>
/// <param name="y">The Y coordinate in VRAM.</param>
/// <returns>The calculated texpage index.</returns>
private int CalculateTexpage(int x, int y) private int CalculateTexpage(int x, int y)
{ {
int columns = 16; int columns = 16;

View File

@@ -1,14 +1,26 @@
using UnityEngine; using UnityEngine;
namespace PSXSplash.RuntimeCode namespace SplashEdit.RuntimeCode
{ {
/// <summary>
/// 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.
/// </summary>
public class ProhibitedArea public class ProhibitedArea
{ {
// X and Y coordinates of the prohibited area in VRAM.
public int X; public int X;
public int Y; public int Y;
// Width and height of the prohibited area.
public int Width; public int Width;
public int Height; public int Height;
/// <summary>
/// Creates a ProhibitedArea instance from a Unity Rect.
/// The floating-point values of the Rect are rounded to the nearest integer.
/// </summary>
/// <param name="rect">The Unity Rect representing the prohibited area.</param>
/// <returns>A new ProhibitedArea with integer dimensions.</returns>
public static ProhibitedArea FromUnityRect(Rect rect) public static ProhibitedArea FromUnityRect(Rect rect)
{ {
return new ProhibitedArea return new ProhibitedArea
@@ -20,6 +32,10 @@ namespace PSXSplash.RuntimeCode
}; };
} }
/// <summary>
/// Converts the ProhibitedArea back into a Unity Rect.
/// </summary>
/// <returns>A Unity Rect with the same area as defined by this ProhibitedArea.</returns>
public Rect ToUnityRect() public Rect ToUnityRect()
{ {
return new Rect(X, Y, Width, Height); return new Rect(X, Y, Width, Height);