Merge pull request #3 from psxsplash/splashpack
Merge splashpack into main
This commit is contained in:
@@ -14,7 +14,7 @@ namespace SplashEdit.EditorCode
|
||||
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 PSXBPP bpp = PSXBPP.TEX_4BIT;
|
||||
private readonly int previewSize = 256;
|
||||
|
||||
[MenuItem("Window/Quantized Preview")]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SplashEdit.RuntimeCode;
|
||||
using Unity.Collections;
|
||||
using UnityEditor;
|
||||
@@ -11,8 +12,10 @@ namespace SplashEdit.EditorCode
|
||||
{
|
||||
public class VRAMEditorWindow : EditorWindow
|
||||
{
|
||||
private const int VramWidth = 1024;
|
||||
private const int VramHeight = 512;
|
||||
private int VramWidth => VRAMPacker.VramWidth;
|
||||
private int VramHeight => VRAMPacker.VramHeight;
|
||||
|
||||
private static readonly Vector2 MinSize = new Vector2(800, 600);
|
||||
private List<ProhibitedArea> prohibitedAreas = new List<ProhibitedArea>();
|
||||
private Vector2 scrollPosition;
|
||||
private Texture2D vramImage;
|
||||
@@ -22,8 +25,6 @@ namespace SplashEdit.EditorCode
|
||||
private Color bufferColor1 = new Color(1, 0, 0, 0.5f);
|
||||
private Color bufferColor2 = new Color(0, 1, 0, 0.5f);
|
||||
private Color prohibitedColor = new Color(1, 0, 0, 0.3f);
|
||||
|
||||
private static string _psxDataPath = "Assets/PSXData.asset";
|
||||
private PSXData _psxData;
|
||||
|
||||
private static readonly Vector2[] resolutions =
|
||||
@@ -33,14 +34,15 @@ namespace SplashEdit.EditorCode
|
||||
new Vector2(368, 240), new Vector2(368, 480),
|
||||
new Vector2(512, 240), new Vector2(512, 480),
|
||||
new Vector2(640, 240), new Vector2(640, 480)
|
||||
};
|
||||
};
|
||||
private static string[] resolutionsStrings => resolutions.Select(c => $"{c.x}x{c.y}").ToArray();
|
||||
|
||||
[MenuItem("Window/VRAM Editor")]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
VRAMEditorWindow window = GetWindow<VRAMEditorWindow>("VRAM Editor");
|
||||
// Set minimum window dimensions.
|
||||
window.minSize = new Vector2(1600, 600);
|
||||
window.minSize = MinSize;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
@@ -53,9 +55,9 @@ namespace SplashEdit.EditorCode
|
||||
blackPixels.Dispose();
|
||||
|
||||
// Ensure minimum window size is applied.
|
||||
this.minSize = new Vector2(800, 600);
|
||||
this.minSize = MinSize;
|
||||
|
||||
LoadData();
|
||||
_psxData = DataStorage.LoadData(out selectedResolution, out dualBuffering, out verticalLayout, out prohibitedAreas);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -113,13 +115,11 @@ namespace SplashEdit.EditorCode
|
||||
PSXObjectExporter[] objects = FindObjectsByType<PSXObjectExporter>(FindObjectsSortMode.None);
|
||||
foreach (PSXObjectExporter exp in objects)
|
||||
{
|
||||
exp.CreatePSXTexture2D();
|
||||
exp.CreatePSXTextures2D();
|
||||
}
|
||||
|
||||
// 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);
|
||||
(Rect buffer1, Rect buffer2) = Utils.BufferForResolution(selectedResolution, verticalLayout);
|
||||
|
||||
List<Rect> framebuffers = new List<Rect> { buffer1 };
|
||||
if (dualBuffering)
|
||||
@@ -136,20 +136,24 @@ namespace SplashEdit.EditorCode
|
||||
{
|
||||
for (int x = 0; x < VramWidth; x++)
|
||||
{
|
||||
vramImage.SetPixel(x, VramHeight - y - 1, packed._vramPixels[x, y].GetUnityColor());
|
||||
vramImage.SetPixel(x, VramHeight - y - 1, packed.vramPixels[x, y].GetUnityColor());
|
||||
}
|
||||
}
|
||||
vramImage.Apply();
|
||||
|
||||
// Prompt the user to select a file location and save the VRAM data.
|
||||
string path = EditorUtility.SaveFilePanel("Select Output File", "", "output", "bin");
|
||||
using (BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)))
|
||||
|
||||
if (path != string.Empty)
|
||||
{
|
||||
for (int y = 0; y < VramHeight; y++)
|
||||
using (BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)))
|
||||
{
|
||||
for (int x = 0; x < VramWidth; x++)
|
||||
for (int y = 0; y < VramHeight; y++)
|
||||
{
|
||||
writer.Write(packed._vramPixels[x, y].Pack());
|
||||
for (int x = 0; x < VramWidth; x++)
|
||||
{
|
||||
writer.Write(packed.vramPixels[x, y].Pack());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,12 +166,11 @@ namespace SplashEdit.EditorCode
|
||||
GUILayout.Label("VRAM Editor", EditorStyles.boldLabel);
|
||||
|
||||
// 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" })];
|
||||
selectedResolution = resolutions[EditorGUILayout.Popup("Resolution", System.Array.IndexOf(resolutions, selectedResolution), resolutionsStrings)];
|
||||
|
||||
// Check resolution constraints for dual buffering.
|
||||
bool canDBHorizontal = selectedResolution[0] * 2 <= 1024;
|
||||
bool canDBVertical = selectedResolution[1] * 2 <= 512;
|
||||
bool canDBHorizontal = selectedResolution.x * 2 <= VramWidth;
|
||||
bool canDBVertical = selectedResolution.y * 2 <= VramHeight;
|
||||
|
||||
if (canDBHorizontal || canDBVertical)
|
||||
{
|
||||
@@ -192,30 +195,51 @@ namespace SplashEdit.EditorCode
|
||||
}
|
||||
|
||||
GUILayout.Space(10);
|
||||
GUILayout.Label("Prohibited areas", EditorStyles.boldLabel);
|
||||
scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.MaxHeight(150f));
|
||||
GUILayout.Label("Prohibited Areas", EditorStyles.boldLabel);
|
||||
GUILayout.Space(10);
|
||||
|
||||
scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, true, GUILayout.MinHeight(300f), GUILayout.ExpandWidth(true));
|
||||
|
||||
// List and edit each prohibited area.
|
||||
List<int> toRemove = new List<int>();
|
||||
|
||||
for (int i = 0; i < prohibitedAreas.Count; i++)
|
||||
{
|
||||
var area = prohibitedAreas[i];
|
||||
|
||||
area.X = EditorGUILayout.IntField("X", area.X);
|
||||
area.Y = EditorGUILayout.IntField("Y", area.Y);
|
||||
GUI.backgroundColor = new Color(0.95f, 0.95f, 0.95f);
|
||||
GUILayout.BeginVertical("box");
|
||||
|
||||
GUI.backgroundColor = Color.white;
|
||||
|
||||
// Display fields for editing the area
|
||||
area.X = EditorGUILayout.IntField("X Coordinate", area.X);
|
||||
area.Y = EditorGUILayout.IntField("Y Coordinate", area.Y);
|
||||
area.Width = EditorGUILayout.IntField("Width", area.Width);
|
||||
area.Height = EditorGUILayout.IntField("Height", area.Height);
|
||||
|
||||
if (GUILayout.Button("Remove"))
|
||||
|
||||
if (GUILayout.Button("Remove", GUILayout.Height(30)))
|
||||
{
|
||||
prohibitedAreas.RemoveAt(i);
|
||||
break;
|
||||
toRemove.Add(i); // Mark for removal
|
||||
}
|
||||
|
||||
|
||||
prohibitedAreas[i] = area;
|
||||
|
||||
GUILayout.EndVertical();
|
||||
GUILayout.Space(10);
|
||||
}
|
||||
|
||||
// Remove the areas marked for deletion outside the loop to avoid skipping elements
|
||||
foreach (var index in toRemove.OrderByDescending(x => x))
|
||||
{
|
||||
prohibitedAreas.RemoveAt(index);
|
||||
}
|
||||
|
||||
GUILayout.EndScrollView();
|
||||
GUILayout.Space(10);
|
||||
|
||||
if (GUILayout.Button("Add Prohibited Area"))
|
||||
{
|
||||
prohibitedAreas.Add(new ProhibitedArea());
|
||||
@@ -230,19 +254,26 @@ namespace SplashEdit.EditorCode
|
||||
// Button to save settings; saving now occurs only on button press.
|
||||
if (GUILayout.Button("Save Settings"))
|
||||
{
|
||||
StoreData();
|
||||
_psxData.OutputResolution = selectedResolution;
|
||||
_psxData.DualBuffering = dualBuffering;
|
||||
_psxData.VerticalBuffering = verticalLayout;
|
||||
_psxData.ProhibitedAreas = prohibitedAreas;
|
||||
|
||||
DataStorage.StoreData(_psxData);
|
||||
EditorUtility.DisplayDialog("splashedit", "Vram configuration saved", "OK");
|
||||
}
|
||||
|
||||
GUILayout.EndVertical();
|
||||
|
||||
// Display VRAM image preview.
|
||||
Rect vramRect = GUILayoutUtility.GetRect(VramWidth, VramHeight, GUILayout.ExpandWidth(false));
|
||||
EditorGUI.DrawPreviewTexture(vramRect, vramImage, null, ScaleMode.ScaleToFit, 0, 0, ColorWriteMask.All);
|
||||
if (vramImage)
|
||||
{
|
||||
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);
|
||||
(Rect buffer1, Rect buffer2) = Utils.BufferForResolution(selectedResolution, verticalLayout, vramRect.min);
|
||||
|
||||
EditorGUI.DrawRect(buffer1, bufferColor1);
|
||||
GUI.Label(new Rect(buffer1.center.x - 40, buffer1.center.y - 10, 120, 20), "Framebuffer A", EditorStyles.boldLabel);
|
||||
@@ -263,42 +294,10 @@ namespace SplashEdit.EditorCode
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads stored PSX data from the asset.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores current configuration to the PSX data asset.
|
||||
/// This is now triggered manually via the "Save Settings" button.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
LICENSE.meta
Normal file
7
LICENSE.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c1679c9d58898f14494d614dfe5f76a6
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
88
Runtime/PSXLightingBaker.cs
Normal file
88
Runtime/PSXLightingBaker.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using UnityEngine;
|
||||
|
||||
public static class PSXLightingBaker
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes the per-vertex lighting from all scene light sources.
|
||||
/// Incorporates ambient, diffuse, and spotlight falloff.
|
||||
/// </summary>
|
||||
/// <param name="vertex">The world-space position of the vertex.</param>
|
||||
/// <param name="normal">The normalized world-space normal of the vertex.</param>
|
||||
/// <returns>A Color representing the lit vertex.</returns>
|
||||
public static Color ComputeLighting(Vector3 vertex, Vector3 normal)
|
||||
{
|
||||
Color finalColor = Color.black;
|
||||
|
||||
Light[] lights = Object.FindObjectsByType<Light>(FindObjectsSortMode.None);
|
||||
|
||||
foreach (Light light in lights)
|
||||
{
|
||||
if (!light.enabled)
|
||||
continue;
|
||||
|
||||
Color lightContribution = Color.black;
|
||||
|
||||
if (light.type == LightType.Directional)
|
||||
{
|
||||
Vector3 lightDir = -light.transform.forward;
|
||||
float NdotL = Mathf.Max(0f, Vector3.Dot(normal, lightDir));
|
||||
lightContribution = light.color * light.intensity * NdotL;
|
||||
}
|
||||
else if (light.type == LightType.Point)
|
||||
{
|
||||
Vector3 lightDir = light.transform.position - vertex;
|
||||
float distance = lightDir.magnitude;
|
||||
lightDir.Normalize();
|
||||
|
||||
float NdotL = Mathf.Max(0f, Vector3.Dot(normal, lightDir));
|
||||
float attenuation = 1.0f / Mathf.Max(distance * distance, 0.0001f);
|
||||
lightContribution = light.color * light.intensity * NdotL * attenuation;
|
||||
}
|
||||
else if (light.type == LightType.Spot)
|
||||
{
|
||||
Vector3 L = light.transform.position - vertex;
|
||||
float distance = L.magnitude;
|
||||
L = L / distance;
|
||||
|
||||
float NdotL = Mathf.Max(0f, Vector3.Dot(normal, L));
|
||||
float attenuation = 1.0f / Mathf.Max(distance * distance, 0.0001f);
|
||||
|
||||
float outerAngleRad = (light.spotAngle * 0.5f) * Mathf.Deg2Rad;
|
||||
float innerAngleRad = outerAngleRad * 0.8f;
|
||||
|
||||
if (light is Light spotLight)
|
||||
{
|
||||
if (spotLight.innerSpotAngle > 0)
|
||||
{
|
||||
innerAngleRad = (spotLight.innerSpotAngle * 0.5f) * Mathf.Deg2Rad;
|
||||
}
|
||||
}
|
||||
|
||||
float cosOuter = Mathf.Cos(outerAngleRad);
|
||||
float cosInner = Mathf.Cos(innerAngleRad);
|
||||
float cosAngle = Vector3.Dot(L, -light.transform.forward);
|
||||
|
||||
if (cosAngle >= cosOuter)
|
||||
{
|
||||
float spotFactor = Mathf.Clamp01((cosAngle - cosOuter) / (cosInner - cosOuter));
|
||||
spotFactor = Mathf.Pow(spotFactor, 4.0f);
|
||||
|
||||
lightContribution = light.color * light.intensity * NdotL * attenuation * spotFactor;
|
||||
}
|
||||
else
|
||||
{
|
||||
lightContribution = Color.black;
|
||||
}
|
||||
}
|
||||
|
||||
finalColor += lightContribution;
|
||||
}
|
||||
|
||||
finalColor.r = Mathf.Clamp(finalColor.r, 0.0f, 0.8f);
|
||||
finalColor.g = Mathf.Clamp(finalColor.g, 0.0f, 0.8f);
|
||||
finalColor.b = Mathf.Clamp(finalColor.b, 0.0f, 0.8f);
|
||||
finalColor.a = 1f;
|
||||
|
||||
return finalColor;
|
||||
}
|
||||
}
|
||||
2
Runtime/PSXLightingBaker.cs.meta
Normal file
2
Runtime/PSXLightingBaker.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b707b7d499862621fb6c82aba4caa183
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SplashEdit.RuntimeCode
|
||||
@@ -26,6 +27,9 @@ namespace SplashEdit.RuntimeCode
|
||||
public PSXVertex v0;
|
||||
public PSXVertex v1;
|
||||
public PSXVertex v2;
|
||||
|
||||
public PSXTexture2D Texture;
|
||||
public readonly PSXVertex[] Vertexes => new PSXVertex[] { v0, v1, v2 };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -36,6 +40,40 @@ namespace SplashEdit.RuntimeCode
|
||||
{
|
||||
public List<Tri> Triangles;
|
||||
|
||||
private static Vector3[] RecalculateSmoothNormals(Mesh mesh)
|
||||
{
|
||||
Vector3[] normals = new Vector3[mesh.vertexCount];
|
||||
Dictionary<Vector3, List<int>> vertexMap = new Dictionary<Vector3, List<int>>();
|
||||
|
||||
for (int i = 0; i < mesh.vertexCount; i++)
|
||||
{
|
||||
Vector3 vertex = mesh.vertices[i];
|
||||
if (!vertexMap.ContainsKey(vertex))
|
||||
{
|
||||
vertexMap[vertex] = new List<int>();
|
||||
}
|
||||
vertexMap[vertex].Add(i);
|
||||
}
|
||||
|
||||
foreach (var kvp in vertexMap)
|
||||
{
|
||||
Vector3 smoothNormal = Vector3.zero;
|
||||
foreach (int index in kvp.Value)
|
||||
{
|
||||
smoothNormal += mesh.normals[index];
|
||||
}
|
||||
smoothNormal.Normalize();
|
||||
|
||||
foreach (int index in kvp.Value)
|
||||
{
|
||||
normals[index] = smoothNormal;
|
||||
}
|
||||
}
|
||||
|
||||
return normals;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a PSXMesh from a Unity Mesh by converting its vertices, normals, UVs, and applying shading.
|
||||
/// </summary>
|
||||
@@ -44,40 +82,74 @@ namespace SplashEdit.RuntimeCode
|
||||
/// <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 CreateFromUnityRenderer(Renderer renderer, float GTEScaling, Transform transform, List<PSXTexture2D> textures)
|
||||
{
|
||||
PSXMesh psxMesh = new PSXMesh { Triangles = new List<Tri>() };
|
||||
|
||||
// Get mesh data arrays.
|
||||
Vector3[] vertices = mesh.vertices;
|
||||
Vector3[] normals = mesh.normals;
|
||||
Vector2[] uv = mesh.uv;
|
||||
int[] indices = mesh.triangles;
|
||||
// Get materials and mesh.
|
||||
Material[] materials = renderer.sharedMaterials;
|
||||
Mesh mesh = renderer.GetComponent<MeshFilter>().sharedMesh;
|
||||
|
||||
// Determine the primary light's direction and color for shading.
|
||||
Light mainLight = RenderSettings.sun;
|
||||
Vector3 lightDir = mainLight ? mainLight.transform.forward : Vector3.down; // Fixed: Removed negation.
|
||||
Color lightColor = mainLight ? mainLight.color * mainLight.intensity : Color.white;
|
||||
|
||||
// Iterate over each triangle (group of 3 indices).
|
||||
for (int i = 0; i < indices.Length; i += 3)
|
||||
// Iterate over each submesh.
|
||||
for (int submeshIndex = 0; submeshIndex < materials.Length; submeshIndex++)
|
||||
{
|
||||
int vid0 = indices[i];
|
||||
int vid1 = indices[i + 1];
|
||||
int vid2 = indices[i + 2];
|
||||
// Get the triangles for this submesh.
|
||||
int[] submeshTriangles = mesh.GetTriangles(submeshIndex);
|
||||
|
||||
// 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];
|
||||
// Get the material for this submesh.
|
||||
Material material = materials[submeshIndex];
|
||||
|
||||
// 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);
|
||||
// Get the corresponding texture for this material (assume mainTexture).
|
||||
Texture2D texture = material.mainTexture as Texture2D;
|
||||
PSXTexture2D psxTexture = null;
|
||||
|
||||
// Add the constructed triangle to the mesh.
|
||||
psxMesh.Triangles.Add(new Tri { v0 = psxV0, v1 = psxV1, v2 = psxV2 });
|
||||
if (texture != null)
|
||||
{
|
||||
// Find the corresponding PSX texture based on the Unity texture.
|
||||
psxTexture = textures.FirstOrDefault(t => t.OriginalTexture == texture);
|
||||
}
|
||||
|
||||
if (psxTexture == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get mesh data arrays.
|
||||
mesh.RecalculateNormals();
|
||||
Vector3[] vertices = mesh.vertices;
|
||||
Vector3[] normals = mesh.normals;
|
||||
Vector3[] smoothNormals = RecalculateSmoothNormals(mesh);
|
||||
Vector2[] uv = mesh.uv;
|
||||
PSXVertex convertData(int index)
|
||||
{
|
||||
// Scale the vertex based on world scale.
|
||||
Vector3 v = Vector3.Scale(vertices[index], transform.lossyScale);
|
||||
// Transform the vertex to world space.
|
||||
Vector3 wv = transform.TransformPoint(vertices[index]);
|
||||
// Transform the normals to world space.
|
||||
Vector3 wn = transform.TransformDirection(smoothNormals[index]).normalized;
|
||||
// Compute lighting for each vertex (this can be a custom function).
|
||||
Color c = PSXLightingBaker.ComputeLighting(wv, wn);
|
||||
// Convert vertex to PSX format, including fixed-point conversion and shading.
|
||||
return ConvertToPSXVertex(v, GTEScaling, normals[index], uv[index], psxTexture?.Width, psxTexture?.Height, c);
|
||||
}
|
||||
// Iterate through the triangles of the submesh.
|
||||
for (int i = 0; i < submeshTriangles.Length; i += 3)
|
||||
{
|
||||
int vid0 = submeshTriangles[i];
|
||||
int vid1 = submeshTriangles[i + 1];
|
||||
int vid2 = submeshTriangles[i + 2];
|
||||
|
||||
Vector3 faceNormal = Vector3.Cross(vertices[vid1] - vertices[vid0], vertices[vid2] - vertices[vid0]).normalized;
|
||||
|
||||
if (Vector3.Dot(faceNormal, normals[vid0]) < 0)
|
||||
{
|
||||
(vid1, vid2) = (vid2, vid1);
|
||||
}
|
||||
|
||||
// Add the constructed triangle to the mesh.
|
||||
psxMesh.Triangles.Add(new Tri { v0 = convertData(vid0), v1 = convertData(vid0), v2 = convertData(vid0), Texture = psxTexture });
|
||||
}
|
||||
}
|
||||
|
||||
return psxMesh;
|
||||
@@ -94,36 +166,30 @@ namespace SplashEdit.RuntimeCode
|
||||
/// <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)
|
||||
private static PSXVertex ConvertToPSXVertex(Vector3 vertex, float GTEScaling, Vector3 normal, Vector2 uv, int? textureWidth, int? textureHeight, Color color)
|
||||
{
|
||||
// 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;
|
||||
|
||||
int width = textureWidth ?? 0;
|
||||
int height = textureHeight ?? 0;
|
||||
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),
|
||||
vx = PSXTrig.ConvertCoordinateToPSX(vertex.x, GTEScaling),
|
||||
vy = PSXTrig.ConvertCoordinateToPSX(-vertex.y, GTEScaling),
|
||||
vz = PSXTrig.ConvertCoordinateToPSX(vertex.z, GTEScaling),
|
||||
|
||||
// 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),
|
||||
nx = PSXTrig.ConvertCoordinateToPSX(normal.x),
|
||||
ny = PSXTrig.ConvertCoordinateToPSX(-normal.y),
|
||||
nz = PSXTrig.ConvertCoordinateToPSX(normal.z),
|
||||
|
||||
// Map UV coordinates to a byte range after scaling based on texture dimensions.
|
||||
u = (byte)(Mathf.Clamp((uv.x * (textureWidth - 1)), 0, 255)),
|
||||
v = (byte)(Mathf.Clamp(((1.0f - uv.y) * (textureHeight - 1)), 0, 255)),
|
||||
u = (byte)Mathf.Clamp(uv.x * (width - 1), 0, 255),
|
||||
v = (byte)Mathf.Clamp((1.0f - uv.y) * (height - 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))
|
||||
// Apply lighting to the colors.
|
||||
r = Utils.Clamp0255(color.r),
|
||||
g = Utils.Clamp0255(color.g),
|
||||
b = Utils.Clamp0255(color.b),
|
||||
};
|
||||
|
||||
return psxVertex;
|
||||
|
||||
@@ -1,49 +1,115 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SplashEdit.RuntimeCode
|
||||
{
|
||||
[RequireComponent(typeof(Renderer))]
|
||||
public class PSXObjectExporter : MonoBehaviour
|
||||
{
|
||||
public PSXBPP BitDepth = PSXBPP.TEX_8BIT; // Defines the bit depth of the texture (e.g., 4BPP, 8BPP)
|
||||
public bool MeshIsStatic = true; // Determines if the mesh is static, affecting how it's processed. Non-static meshes don't export correctly as of now.
|
||||
|
||||
[HideInInspector]
|
||||
public PSXTexture2D Texture; // Stores the converted PlayStation-style texture
|
||||
public List<PSXTexture2D> Textures { get; set; } = new List<PSXTexture2D>(); // Stores the converted PlayStation-style texture
|
||||
public PSXMesh Mesh { get; set; } // Stores the converted PlayStation-style mesh
|
||||
|
||||
|
||||
[SerializeField] private bool PreviewNormals = false;
|
||||
[SerializeField] private float normalPreviewLength = 0.5f; // Length of the normal lines
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
|
||||
if (PreviewNormals)
|
||||
{
|
||||
MeshFilter filter = GetComponent<MeshFilter>();
|
||||
|
||||
if (filter != null)
|
||||
{
|
||||
|
||||
Mesh mesh = filter.sharedMesh;
|
||||
Vector3[] vertices = mesh.vertices;
|
||||
Vector3[] normals = mesh.normals;
|
||||
|
||||
Gizmos.color = Color.green; // Normal color
|
||||
|
||||
for (int i = 0; i < vertices.Length; i++)
|
||||
{
|
||||
Vector3 worldVertex = transform.TransformPoint(vertices[i]); // Convert to world space
|
||||
Vector3 worldNormal = transform.TransformDirection(normals[i]); // Transform normal to world space
|
||||
|
||||
Gizmos.DrawLine(worldVertex, worldVertex + worldNormal * normalPreviewLength);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[HideInInspector]
|
||||
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 CreatePSXTextures2D()
|
||||
{
|
||||
Renderer renderer = GetComponent<Renderer>();
|
||||
if (renderer != null && renderer.sharedMaterial != null && renderer.sharedMaterial.mainTexture is Texture2D texture)
|
||||
Textures.Clear();
|
||||
if (renderer != null)
|
||||
{
|
||||
Texture = PSXTexture2D.CreateFromTexture2D(texture, BitDepth);
|
||||
Texture.OriginalTexture = texture; // Stores reference to the original texture
|
||||
Material[] materials = renderer.sharedMaterials;
|
||||
|
||||
foreach (Material mat in materials)
|
||||
{
|
||||
if (mat != null && mat.mainTexture != null)
|
||||
{
|
||||
Texture mainTexture = mat.mainTexture;
|
||||
Texture2D tex2D = null;
|
||||
|
||||
// Check if it's already a Texture2D
|
||||
if (mainTexture is Texture2D existingTex2D)
|
||||
{
|
||||
tex2D = existingTex2D;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If not a Texture2D, try to convert
|
||||
tex2D = ConvertToTexture2D(mainTexture);
|
||||
}
|
||||
|
||||
if (tex2D != null)
|
||||
{
|
||||
PSXTexture2D tex = PSXTexture2D.CreateFromTexture2D(tex2D, BitDepth);
|
||||
tex.OriginalTexture = tex2D; // Store reference to the original texture
|
||||
Textures.Add(tex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Texture2D ConvertToTexture2D(Texture texture)
|
||||
{
|
||||
// Create a new Texture2D with the same dimensions and format
|
||||
Texture2D texture2D = new Texture2D(texture.width, texture.height, TextureFormat.RGBA32, false);
|
||||
|
||||
// Read the texture pixels
|
||||
RenderTexture currentActiveRT = RenderTexture.active;
|
||||
RenderTexture.active = texture as RenderTexture;
|
||||
|
||||
texture2D.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0);
|
||||
texture2D.Apply();
|
||||
|
||||
RenderTexture.active = currentActiveRT;
|
||||
|
||||
return texture2D;
|
||||
}
|
||||
/// <summary>
|
||||
/// Converts the object's mesh into a PlayStation-compatible mesh.
|
||||
/// </summary>
|
||||
public void CreatePSXMesh()
|
||||
public void CreatePSXMesh(float GTEScaling)
|
||||
{
|
||||
MeshFilter meshFilter = gameObject.GetComponent<MeshFilter>();
|
||||
if (meshFilter != null)
|
||||
Renderer renderer = GetComponent<Renderer>();
|
||||
if (renderer != null)
|
||||
{
|
||||
if (MeshIsStatic)
|
||||
{
|
||||
// Static meshes take object transformation into account
|
||||
Mesh = PSXMesh.CreateFromUnityMesh(meshFilter.sharedMesh, Texture.Width, Texture.Height, transform);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Dynamic meshes do not consider object transformation
|
||||
Mesh = PSXMesh.CreateFromUnityMesh(meshFilter.sharedMesh, Texture.Width, Texture.Height);
|
||||
}
|
||||
Mesh = PSXMesh.CreateFromUnityRenderer(renderer, GTEScaling, transform, Textures);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,158 +1,303 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Overlays;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace SplashEdit.RuntimeCode
|
||||
{
|
||||
|
||||
[ExecuteInEditMode]
|
||||
public class PSXSceneExporter : MonoBehaviour
|
||||
{
|
||||
private PSXObjectExporter[] _exporters;
|
||||
|
||||
private PSXData _psxData;
|
||||
private readonly string _psxDataPath = "Assets/PSXData.asset";
|
||||
|
||||
private Vector2 selectedResolution;
|
||||
private bool dualBuffering;
|
||||
private bool verticalLayout;
|
||||
private List<ProhibitedArea> prohibitedAreas;
|
||||
private VRAMPixel[,] vramPixels;
|
||||
|
||||
|
||||
|
||||
public void Export()
|
||||
{
|
||||
LoadData();
|
||||
_exporters = FindObjectsByType<PSXObjectExporter>(FindObjectsSortMode.None);
|
||||
foreach (PSXObjectExporter exp in _exporters)
|
||||
{
|
||||
exp.CreatePSXTexture2D();
|
||||
exp.CreatePSXMesh();
|
||||
}
|
||||
PackTextures();
|
||||
ExportFile();
|
||||
}
|
||||
|
||||
void PackTextures()
|
||||
[ExecuteInEditMode]
|
||||
public class PSXSceneExporter : MonoBehaviour
|
||||
{
|
||||
|
||||
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);
|
||||
public float GTEScaling = 100.0f;
|
||||
|
||||
List<Rect> framebuffers = new List<Rect> { buffer1 };
|
||||
if (dualBuffering)
|
||||
{
|
||||
framebuffers.Add(buffer2);
|
||||
}
|
||||
private PSXObjectExporter[] _exporters;
|
||||
private TextureAtlas[] _atlases;
|
||||
|
||||
VRAMPacker tp = new VRAMPacker(framebuffers, prohibitedAreas);
|
||||
var packed = tp.PackTexturesIntoVRAM(_exporters);
|
||||
_exporters = packed.processedObjects;
|
||||
vramPixels = packed._vramPixels;
|
||||
private PSXData _psxData;
|
||||
|
||||
}
|
||||
private Vector2 selectedResolution;
|
||||
private bool dualBuffering;
|
||||
private bool verticalLayout;
|
||||
private List<ProhibitedArea> prohibitedAreas;
|
||||
|
||||
void ExportFile()
|
||||
{
|
||||
string path = EditorUtility.SaveFilePanel("Select Output File", "", "output", "bin");
|
||||
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++)
|
||||
public void Export()
|
||||
{
|
||||
for (int x = 0; x < vramPixels.GetLength(0); x++)
|
||||
{
|
||||
writer.Write(vramPixels[x, y].Pack());
|
||||
}
|
||||
_psxData = DataStorage.LoadData(out selectedResolution, out dualBuffering, out verticalLayout, out prohibitedAreas);
|
||||
|
||||
_exporters = FindObjectsByType<PSXObjectExporter>(FindObjectsSortMode.None);
|
||||
foreach (PSXObjectExporter exp in _exporters)
|
||||
{
|
||||
exp.CreatePSXTextures2D();
|
||||
exp.CreatePSXMesh(GTEScaling);
|
||||
}
|
||||
PackTextures();
|
||||
ExportFile();
|
||||
}
|
||||
writer.Write((ushort)_exporters.Length);
|
||||
foreach (PSXObjectExporter exporter in _exporters)
|
||||
|
||||
void PackTextures()
|
||||
{
|
||||
(Rect buffer1, Rect buffer2) = Utils.BufferForResolution(selectedResolution, verticalLayout);
|
||||
|
||||
List<Rect> framebuffers = new List<Rect> { buffer1 };
|
||||
if (dualBuffering)
|
||||
{
|
||||
framebuffers.Add(buffer2);
|
||||
}
|
||||
|
||||
VRAMPacker tp = new VRAMPacker(framebuffers, prohibitedAreas);
|
||||
var packed = tp.PackTexturesIntoVRAM(_exporters);
|
||||
_exporters = packed.processedObjects;
|
||||
_atlases = packed.atlases;
|
||||
|
||||
}
|
||||
|
||||
void ExportFile()
|
||||
{
|
||||
|
||||
int expander = 16 / ((int)exporter.Texture.BitDepth);
|
||||
string path = EditorUtility.SaveFilePanel("Select Output File", "", "output", "bin");
|
||||
int totalFaces = 0;
|
||||
|
||||
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);
|
||||
// Lists for mesh data offsets.
|
||||
List<long> offsetPlaceholderPositions = new List<long>();
|
||||
List<long> meshDataOffsets = new List<long>();
|
||||
|
||||
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);
|
||||
// Lists for atlas data offsets.
|
||||
List<long> atlasOffsetPlaceholderPositions = new List<long>();
|
||||
List<long> atlasDataOffsets = new List<long>();
|
||||
|
||||
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);
|
||||
int clutCount = 0;
|
||||
|
||||
}
|
||||
// Cluts
|
||||
foreach (TextureAtlas atlas in _atlases)
|
||||
{
|
||||
foreach (var texture in atlas.ContainedTextures)
|
||||
{
|
||||
if (texture.ColorPalette != null)
|
||||
{
|
||||
clutCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using (BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)))
|
||||
{
|
||||
// Header
|
||||
writer.Write('S');
|
||||
writer.Write('P');
|
||||
writer.Write((ushort)1);
|
||||
writer.Write((ushort)_exporters.Length);
|
||||
writer.Write((ushort)_atlases.Length);
|
||||
writer.Write((ushort)clutCount);
|
||||
writer.Write((ushort)0);
|
||||
// Start of Metadata section
|
||||
|
||||
// GameObject section (exporters)
|
||||
foreach (PSXObjectExporter exporter in _exporters)
|
||||
{
|
||||
// Write object's transform
|
||||
writer.Write((int)PSXTrig.ConvertCoordinateToPSX(exporter.transform.localToWorldMatrix.GetPosition().x, GTEScaling));
|
||||
writer.Write((int)PSXTrig.ConvertCoordinateToPSX(-exporter.transform.localToWorldMatrix.GetPosition().y, GTEScaling));
|
||||
writer.Write((int)PSXTrig.ConvertCoordinateToPSX(exporter.transform.localToWorldMatrix.GetPosition().z, GTEScaling));
|
||||
int[,] rotationMatrix = PSXTrig.ConvertRotationToPSXMatrix(exporter.transform.rotation);
|
||||
|
||||
writer.Write((int)rotationMatrix[0, 0]);
|
||||
writer.Write((int)rotationMatrix[0, 1]);
|
||||
writer.Write((int)rotationMatrix[0, 2]);
|
||||
writer.Write((int)rotationMatrix[1, 0]);
|
||||
writer.Write((int)rotationMatrix[1, 1]);
|
||||
writer.Write((int)rotationMatrix[1, 2]);
|
||||
writer.Write((int)rotationMatrix[2, 0]);
|
||||
writer.Write((int)rotationMatrix[2, 1]);
|
||||
writer.Write((int)rotationMatrix[2, 2]);
|
||||
|
||||
|
||||
// Write placeholder for mesh data offset and record its position.
|
||||
offsetPlaceholderPositions.Add(writer.BaseStream.Position);
|
||||
writer.Write((int)0); // 4-byte placeholder for mesh data offset.
|
||||
|
||||
writer.Write((int)exporter.Mesh.Triangles.Count);
|
||||
}
|
||||
|
||||
// Atlas metadata section
|
||||
foreach (TextureAtlas atlas in _atlases)
|
||||
{
|
||||
// Write placeholder for texture atlas raw data offset.
|
||||
atlasOffsetPlaceholderPositions.Add(writer.BaseStream.Position);
|
||||
writer.Write((int)0); // 4-byte placeholder for atlas data offset.
|
||||
|
||||
writer.Write((ushort)atlas.Width);
|
||||
writer.Write((ushort)TextureAtlas.Height);
|
||||
writer.Write((ushort)atlas.PositionX);
|
||||
writer.Write((ushort)atlas.PositionY);
|
||||
}
|
||||
|
||||
// Cluts
|
||||
foreach (TextureAtlas atlas in _atlases)
|
||||
{
|
||||
foreach (var texture in atlas.ContainedTextures)
|
||||
{
|
||||
if (texture.ColorPalette != null)
|
||||
{
|
||||
foreach (VRAMPixel clutPixel in texture.ColorPalette)
|
||||
{
|
||||
writer.Write((ushort)clutPixel.Pack());
|
||||
}
|
||||
for (int i = texture.ColorPalette.Count; i < 256; i++)
|
||||
{
|
||||
writer.Write((ushort)0);
|
||||
}
|
||||
writer.Write((ushort)texture.ClutPackingX);
|
||||
writer.Write((ushort)texture.ClutPackingY);
|
||||
writer.Write((ushort)texture.ColorPalette.Count);
|
||||
writer.Write((ushort)0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start of data section
|
||||
|
||||
// Mesh data section: Write mesh data for each exporter.
|
||||
foreach (PSXObjectExporter exporter in _exporters)
|
||||
{
|
||||
AlignToFourBytes(writer);
|
||||
// Record the current offset for this exporter's mesh data.
|
||||
long meshDataOffset = writer.BaseStream.Position;
|
||||
meshDataOffsets.Add(meshDataOffset);
|
||||
|
||||
totalFaces += exporter.Mesh.Triangles.Count;
|
||||
|
||||
void writeVertexPosition(PSXVertex v)
|
||||
{
|
||||
writer.Write((short)v.vx);
|
||||
writer.Write((short)v.vy);
|
||||
writer.Write((short)v.vz);
|
||||
}
|
||||
void writeVertexNormals(PSXVertex v)
|
||||
{
|
||||
writer.Write((short)v.nx);
|
||||
writer.Write((short)v.ny);
|
||||
writer.Write((short)v.nz);
|
||||
}
|
||||
void writeVertexColor(PSXVertex v)
|
||||
{
|
||||
writer.Write((byte)v.r);
|
||||
writer.Write((byte)v.g);
|
||||
writer.Write((byte)v.b);
|
||||
writer.Write((byte)0); // padding
|
||||
}
|
||||
void writeVertexUV(PSXVertex v, PSXTexture2D t ,int expander)
|
||||
{
|
||||
writer.Write((byte)(v.u + t.PackingX * expander));
|
||||
writer.Write((byte)(v.v + t.PackingY));
|
||||
}
|
||||
void foreachVertexDo(Tri tri, Action<PSXVertex> action)
|
||||
{
|
||||
for (int i = 0; i < tri.Vertexes.Length; i++)
|
||||
{
|
||||
action(tri.Vertexes[i]);
|
||||
}
|
||||
}
|
||||
foreach (Tri tri in exporter.Mesh.Triangles)
|
||||
{
|
||||
int expander = 16 / ((int)tri.Texture.BitDepth);
|
||||
// Write vertices coordinates
|
||||
foreachVertexDo(tri, (v) => writeVertexPosition(v));
|
||||
|
||||
// Write vertex normals for v0 only
|
||||
writeVertexNormals(tri.v0);
|
||||
|
||||
// Write vertex colors with padding
|
||||
foreachVertexDo(tri, (v) => writeVertexColor(v));
|
||||
|
||||
// Write UVs for each vertex, adjusting for texture packing
|
||||
foreachVertexDo(tri, (v) => writeVertexUV(v, tri.Texture, expander));
|
||||
|
||||
writer.Write((ushort)0); // padding
|
||||
|
||||
|
||||
TPageAttr tpage = new TPageAttr();
|
||||
tpage.SetPageX(tri.Texture.TexpageX);
|
||||
tpage.SetPageY(tri.Texture.TexpageY);
|
||||
tpage.Set(tri.Texture.BitDepth.ToColorMode());
|
||||
tpage.SetDithering(true);
|
||||
writer.Write((ushort)tpage.info);
|
||||
writer.Write((ushort)tri.Texture.ClutPackingX);
|
||||
writer.Write((ushort)tri.Texture.ClutPackingY);
|
||||
writer.Write((ushort)0);
|
||||
}
|
||||
}
|
||||
|
||||
// Atlas data section: Write raw texture data for each atlas.
|
||||
foreach (TextureAtlas atlas in _atlases)
|
||||
{
|
||||
AlignToFourBytes(writer);
|
||||
// Record the current offset for this atlas's data.
|
||||
long atlasDataOffset = writer.BaseStream.Position;
|
||||
atlasDataOffsets.Add(atlasDataOffset);
|
||||
|
||||
// Write the atlas's raw texture data.
|
||||
for (int y = 0; y < atlas.vramPixels.GetLength(1); y++)
|
||||
{
|
||||
for (int x = 0; x < atlas.vramPixels.GetLength(0); x++)
|
||||
{
|
||||
writer.Write(atlas.vramPixels[x, y].Pack());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backfill the mesh data offsets into the metadata section.
|
||||
if (offsetPlaceholderPositions.Count == meshDataOffsets.Count)
|
||||
{
|
||||
for (int i = 0; i < offsetPlaceholderPositions.Count; i++)
|
||||
{
|
||||
writer.Seek((int)offsetPlaceholderPositions[i], SeekOrigin.Begin);
|
||||
writer.Write((int)meshDataOffsets[i]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("Mismatch between metadata mesh offset placeholders and mesh data blocks!");
|
||||
}
|
||||
|
||||
// Backfill the atlas data offsets into the metadata section.
|
||||
if (atlasOffsetPlaceholderPositions.Count == atlasDataOffsets.Count)
|
||||
{
|
||||
for (int i = 0; i < atlasOffsetPlaceholderPositions.Count; i++)
|
||||
{
|
||||
writer.Seek((int)atlasOffsetPlaceholderPositions[i], SeekOrigin.Begin);
|
||||
writer.Write((int)atlasDataOffsets[i]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("Mismatch between atlas offset placeholders and atlas data blocks!");
|
||||
}
|
||||
}
|
||||
Debug.Log(totalFaces);
|
||||
}
|
||||
}
|
||||
Debug.Log(totalFaces);
|
||||
|
||||
void AlignToFourBytes(BinaryWriter writer)
|
||||
{
|
||||
long position = writer.BaseStream.Position;
|
||||
int padding = (int)(4 - (position % 4)) % 4; // Compute needed padding
|
||||
writer.Write(new byte[padding]); // Write zero padding
|
||||
}
|
||||
|
||||
void OnDrawGizmos()
|
||||
|
||||
{
|
||||
|
||||
Gizmos.DrawIcon(transform.position, "Packages/net.psxsplash.splashedit/Icons/PSXSceneExporter.png", true);
|
||||
Vector3 sceneOrigin = new Vector3(0, 0, 0);
|
||||
Vector3 cubeSize = new Vector3(8.0f * GTEScaling, 8.0f * GTEScaling, 8.0f * GTEScaling);
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawWireCube(sceneOrigin, cubeSize);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void LoadData()
|
||||
{
|
||||
_psxData = AssetDatabase.LoadAssetAtPath<PSXData>(_psxDataPath);
|
||||
|
||||
if (!_psxData)
|
||||
{
|
||||
_psxData = ScriptableObject.CreateInstance<PSXData>();
|
||||
AssetDatabase.CreateAsset(_psxData, _psxDataPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
selectedResolution = _psxData.OutputResolution;
|
||||
dualBuffering = _psxData.DualBuffering;
|
||||
verticalLayout = _psxData.VerticalBuffering;
|
||||
prohibitedAreas = _psxData.ProhibitedAreas;
|
||||
}
|
||||
|
||||
void OnDrawGizmos()
|
||||
{
|
||||
Gizmos.DrawIcon(transform.position, "Packages/net.psxsplash.splashedit/Icons/PSXSceneExporter.png", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -159,7 +160,7 @@ namespace SplashEdit.RuntimeCode
|
||||
return psxTex;
|
||||
}
|
||||
|
||||
psxTex._maxColors = (int)Mathf.Pow((int)bitDepth, 2);
|
||||
psxTex._maxColors = (int)Mathf.Pow(2, (int)bitDepth);
|
||||
|
||||
TextureQuantizer.QuantizedResult result = TextureQuantizer.Quantize(inputTexture, psxTex._maxColors);
|
||||
|
||||
@@ -230,7 +231,7 @@ namespace SplashEdit.RuntimeCode
|
||||
{
|
||||
for (int x = 0; x < Height; x++)
|
||||
{
|
||||
tex.SetPixel(x, y, ImageData[x, y].GetUnityColor());
|
||||
tex.SetPixel(x, Height - 1 - y, ImageData[x, y].GetUnityColor());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,5 +276,15 @@ namespace SplashEdit.RuntimeCode
|
||||
return vramTexture;
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
/// Check if we need to update stored texture
|
||||
/// </summary>
|
||||
/// <param name="bitDepth">new settings for color bit depth</param>
|
||||
/// <param name="texture">new texture</param>
|
||||
/// <returns>return true if sored texture is different from a new one</returns>
|
||||
internal bool NeedUpdate(PSXBPP bitDepth, Texture2D texture)
|
||||
{
|
||||
return BitDepth != bitDepth || texture.GetInstanceID() != texture.GetInstanceID();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ namespace SplashEdit.RuntimeCode
|
||||
public int PositionY; // Y position of the atlas in VRAM.
|
||||
public int Width; // Width of the atlas.
|
||||
public const int Height = 256; // Fixed height for all atlases.
|
||||
public VRAMPixel[,] vramPixels;
|
||||
public List<PSXTexture2D> ContainedTextures = new List<PSXTexture2D>(); // Textures packed in this atlas.
|
||||
}
|
||||
|
||||
@@ -29,8 +30,8 @@ namespace SplashEdit.RuntimeCode
|
||||
private List<TextureAtlas> _finalizedAtlases = new List<TextureAtlas>(); // Atlases that have been successfully placed.
|
||||
private List<Rect> _allocatedCLUTs = new List<Rect>(); // Allocated regions for CLUTs.
|
||||
|
||||
private const int VRAM_WIDTH = 1024;
|
||||
private const int VRAM_HEIGHT = 512;
|
||||
public static readonly int VramWidth = 1024;
|
||||
public static readonly int VramHeight = 512;
|
||||
|
||||
private VRAMPixel[,] _vramPixels; // Simulated VRAM pixel data.
|
||||
|
||||
@@ -53,59 +54,87 @@ namespace SplashEdit.RuntimeCode
|
||||
_reservedAreas.Add(framebuffers[0]);
|
||||
_reservedAreas.Add(framebuffers[1]);
|
||||
|
||||
_vramPixels = new VRAMPixel[VRAM_WIDTH, VRAM_HEIGHT];
|
||||
_vramPixels = new VRAMPixel[VramWidth, VramHeight];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Packs the textures from the provided PSXObjectExporter array into VRAM.
|
||||
/// Each exporter now holds a list of textures.
|
||||
/// Duplicates (textures with the same underlying OriginalTexture and BitDepth) across all exporters are merged.
|
||||
/// Returns the processed objects and the final VRAM pixel array.
|
||||
/// </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)
|
||||
/// <returns>Tuple containing processed objects, texture atlases, and the VRAM pixel array.</returns>
|
||||
public (PSXObjectExporter[] processedObjects, TextureAtlas[] atlases, VRAMPixel[,] vramPixels) PackTexturesIntoVRAM(PSXObjectExporter[] objects)
|
||||
{
|
||||
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);
|
||||
// Gather all textures from all exporters.
|
||||
List<PSXTexture2D> allTextures = new List<PSXTexture2D>();
|
||||
foreach (var obj in objects)
|
||||
{
|
||||
allTextures.AddRange(obj.Textures);
|
||||
}
|
||||
|
||||
foreach (var group in groupedObjects)
|
||||
// List to track unique textures.
|
||||
List<PSXTexture2D> uniqueTextures = new List<PSXTexture2D>();
|
||||
|
||||
// Group textures by bit depth (highest first).
|
||||
var texturesByBitDepth = allTextures
|
||||
.GroupBy(tex => tex.BitDepth)
|
||||
.OrderByDescending(g => g.Key);
|
||||
|
||||
// Process each group.
|
||||
foreach (var group in texturesByBitDepth)
|
||||
{
|
||||
// Determine atlas width based on texture bit depth.
|
||||
int atlasWidth = group.Key switch
|
||||
{
|
||||
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.
|
||||
// Create an initial atlas for this group.
|
||||
TextureAtlas atlas = new TextureAtlas { BitDepth = group.Key, Width = atlasWidth, PositionX = 0, PositionY = 0 };
|
||||
_textureAtlases.Add(atlas);
|
||||
|
||||
// Process each texture in descending order of area (width * height).
|
||||
foreach (var obj in group.OrderByDescending(obj => obj.Texture.QuantizedWidth * obj.Texture.Height))
|
||||
// Process each texture in descending order of area.
|
||||
foreach (var texture in group.OrderByDescending(tex => tex.QuantizedWidth * tex.Height))
|
||||
{
|
||||
// Remove duplicate textures
|
||||
if (uniqueTextures.Any(tex => tex.OriginalTexture.GetInstanceID() == obj.Texture.OriginalTexture.GetInstanceID() && tex.BitDepth == obj.Texture.BitDepth))
|
||||
if (uniqueTextures.Any(tex => tex.OriginalTexture.GetInstanceID() == texture.OriginalTexture.GetInstanceID() && tex.BitDepth == texture.BitDepth))
|
||||
{
|
||||
obj.Texture = uniqueTextures.First(tex => tex.OriginalTexture.GetInstanceID() == obj.Texture.OriginalTexture.GetInstanceID());
|
||||
// Skip packing this texture – it will be replaced later.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to place the texture in the current atlas.
|
||||
if (!TryPlaceTextureInAtlas(atlas, obj.Texture))
|
||||
if (!TryPlaceTextureInAtlas(atlas, texture))
|
||||
{
|
||||
// If failed, create a new atlas and try again.
|
||||
// If failed, create a new atlas for this bit depth group and try again.
|
||||
atlas = new TextureAtlas { BitDepth = group.Key, Width = atlasWidth, PositionX = 0, PositionY = 0 };
|
||||
_textureAtlases.Add(atlas);
|
||||
if (!TryPlaceTextureInAtlas(atlas, obj.Texture))
|
||||
if (!TryPlaceTextureInAtlas(atlas, texture))
|
||||
{
|
||||
Debug.LogError($"Failed to pack texture {obj.Texture}. It might not fit.");
|
||||
break;
|
||||
Debug.LogError($"Failed to pack texture {texture}. It might not fit.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
uniqueTextures.Add(obj.Texture);
|
||||
uniqueTextures.Add(texture);
|
||||
}
|
||||
}
|
||||
|
||||
// Now update every exporter so that duplicate textures reference the unique instance.
|
||||
foreach (var obj in objects)
|
||||
{
|
||||
for (int i = 0; i < obj.Textures.Count; i++)
|
||||
{
|
||||
var unique = uniqueTextures.FirstOrDefault(tex => tex.OriginalTexture.GetInstanceID() == obj.Textures[i].OriginalTexture.GetInstanceID() &&
|
||||
tex.BitDepth == obj.Textures[i].BitDepth);
|
||||
if (unique != null)
|
||||
{
|
||||
obj.Textures[i] = unique;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,10 +142,9 @@ namespace SplashEdit.RuntimeCode
|
||||
ArrangeAtlasesInVRAM();
|
||||
// Allocate color lookup tables (CLUTs) for textures that use palettes.
|
||||
AllocateCLUTs();
|
||||
|
||||
// Build the final VRAM pixel array from placed textures and CLUTs.
|
||||
BuildVram();
|
||||
return (objects, _vramPixels);
|
||||
return (objects, _finalizedAtlases.ToArray(), _vramPixels);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -161,10 +189,10 @@ namespace SplashEdit.RuntimeCode
|
||||
{
|
||||
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 <= VramHeight - 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 <= VramWidth - atlas.Width; x += 64)
|
||||
{
|
||||
// Only consider atlases that haven't been placed yet.
|
||||
if (atlas.PositionX == 0 && atlas.PositionY == 0)
|
||||
@@ -218,15 +246,15 @@ namespace SplashEdit.RuntimeCode
|
||||
bool placed = false;
|
||||
|
||||
// Iterate over possible CLUT positions in VRAM.
|
||||
for (ushort x = 0; x < VRAM_WIDTH; x += 16)
|
||||
for (ushort x = 0; x < VramWidth; x += 16)
|
||||
{
|
||||
for (ushort y = 0; y <= VRAM_HEIGHT; y++)
|
||||
for (ushort y = 0; y <= VramHeight; y++)
|
||||
{
|
||||
var candidate = new Rect(x, y, clutWidth, clutHeight);
|
||||
if (IsPlacementValid(candidate))
|
||||
{
|
||||
_allocatedCLUTs.Add(candidate);
|
||||
texture.ClutPackingX = x;
|
||||
texture.ClutPackingX = (ushort)(x / 16);
|
||||
texture.ClutPackingY = y;
|
||||
placed = true;
|
||||
break;
|
||||
@@ -249,6 +277,8 @@ namespace SplashEdit.RuntimeCode
|
||||
{
|
||||
foreach (TextureAtlas atlas in _finalizedAtlases)
|
||||
{
|
||||
atlas.vramPixels = new VRAMPixel[atlas.Width, TextureAtlas.Height];
|
||||
|
||||
foreach (PSXTexture2D texture in atlas.ContainedTextures)
|
||||
{
|
||||
// Copy texture image data into VRAM using atlas and texture packing offsets.
|
||||
@@ -256,6 +286,7 @@ namespace SplashEdit.RuntimeCode
|
||||
{
|
||||
for (int x = 0; x < texture.QuantizedWidth; x++)
|
||||
{
|
||||
atlas.vramPixels[x + texture.PackingX, y + texture.PackingY] = texture.ImageData[x, y];
|
||||
_vramPixels[x + atlas.PositionX + texture.PackingX, y + atlas.PositionY + texture.PackingY] = texture.ImageData[x, y];
|
||||
}
|
||||
}
|
||||
@@ -281,8 +312,8 @@ namespace SplashEdit.RuntimeCode
|
||||
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;
|
||||
if (rect.x + rect.width > VramWidth) return false;
|
||||
if (rect.y + rect.height > VramHeight) 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));
|
||||
@@ -293,20 +324,5 @@ namespace SplashEdit.RuntimeCode
|
||||
|
||||
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)
|
||||
{
|
||||
int columns = 16;
|
||||
int colIndex = x / 64;
|
||||
int rowIndex = y / 256;
|
||||
return (rowIndex * columns) + colIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
294
Runtime/Utils.cs
294
Runtime/Utils.cs
@@ -1,7 +1,61 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using UnityEditor;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SplashEdit.RuntimeCode
|
||||
{
|
||||
|
||||
public static class DataStorage
|
||||
{
|
||||
private static readonly string psxDataPath = "Assets/PSXData.asset";
|
||||
|
||||
/// <summary>
|
||||
/// Loads stored PSX data from the asset.
|
||||
/// </summary>
|
||||
public static PSXData LoadData(out Vector2 selectedResolution, out bool dualBuffering, out bool verticalLayout, out List<ProhibitedArea> prohibitedAreas)
|
||||
{
|
||||
var _psxData = AssetDatabase.LoadAssetAtPath<PSXData>(psxDataPath);
|
||||
if (!_psxData)
|
||||
{
|
||||
_psxData = ScriptableObject.CreateInstance<PSXData>();
|
||||
AssetDatabase.CreateAsset(_psxData, psxDataPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
|
||||
selectedResolution = _psxData.OutputResolution;
|
||||
dualBuffering = _psxData.DualBuffering;
|
||||
verticalLayout = _psxData.VerticalBuffering;
|
||||
prohibitedAreas = _psxData.ProhibitedAreas;
|
||||
return _psxData;
|
||||
}
|
||||
public static PSXData LoadData()
|
||||
{
|
||||
PSXData psxData = AssetDatabase.LoadAssetAtPath<PSXData>(psxDataPath);
|
||||
|
||||
if (!psxData)
|
||||
{
|
||||
psxData = ScriptableObject.CreateInstance<PSXData>();
|
||||
AssetDatabase.CreateAsset(psxData, psxDataPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
return psxData;
|
||||
}
|
||||
|
||||
public static void StoreData(PSXData psxData)
|
||||
{
|
||||
if (psxData != null)
|
||||
{
|
||||
EditorUtility.SetDirty(psxData);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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.
|
||||
@@ -41,4 +95,244 @@ namespace SplashEdit.RuntimeCode
|
||||
return new Rect(X, Y, Width, Height);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// A utility class containing methods for converting Unity-specific data formats to PSX-compatible formats.
|
||||
/// This includes converting coordinates and rotations to PSX's 3.12 fixed-point format.
|
||||
/// </summary>
|
||||
public static class PSXTrig
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a floating-point coordinate to a PSX-compatible 3.12 fixed-point format.
|
||||
/// The value is clamped to the range [-4, 3.999] and scaled by the provided GTEScaling factor.
|
||||
/// </summary>
|
||||
/// <param name="value">The coordinate value to convert.</param>
|
||||
/// <param name="GTEScaling">A scaling factor for the value (default is 1.0f).</param>
|
||||
/// <returns>The converted coordinate in 3.12 fixed-point format.</returns>
|
||||
public static short ConvertCoordinateToPSX(float value, float GTEScaling = 1.0f)
|
||||
{
|
||||
return (short)(Mathf.Clamp(value / GTEScaling, -4f, 3.999f) * 4096);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a quaternion rotation to a PSX-compatible 3x3 rotation matrix.
|
||||
/// The matrix is adjusted for the difference in the Y-axis orientation between Unity (Y-up) and PSX (Y-down).
|
||||
/// Each matrix element is converted to a 3.12 fixed-point format.
|
||||
/// </summary>
|
||||
/// <param name="rotation">The quaternion representing the rotation to convert.</param>
|
||||
/// <returns>A 3x3 matrix representing the PSX-compatible rotation.</returns>
|
||||
public static int[,] ConvertRotationToPSXMatrix(Quaternion rotation)
|
||||
{
|
||||
// Standard quaternion-to-matrix conversion.
|
||||
float x = rotation.x, y = rotation.y, z = rotation.z, w = rotation.w;
|
||||
|
||||
float m00 = 1f - 2f * (y * y + z * z);
|
||||
float m01 = 2f * (x * y - z * w);
|
||||
float m02 = 2f * (x * z + y * w);
|
||||
|
||||
float m10 = 2f * (x * y + z * w);
|
||||
float m11 = 1f - 2f * (x * x + z * z);
|
||||
float m12 = 2f * (y * z - x * w);
|
||||
|
||||
float m20 = 2f * (x * z - y * w);
|
||||
float m21 = 2f * (y * z + x * w);
|
||||
float m22 = 1f - 2f * (x * x + y * y);
|
||||
|
||||
// Apply Y-axis flip to match the PSX's Y-down convention.
|
||||
// This replicates the behavior of:
|
||||
// { m00, -m01, m02 },
|
||||
// { -m10, m11, -m12 },
|
||||
// { m20, -m21, m22 }
|
||||
float[,] fixedMatrix = new float[3, 3]
|
||||
{
|
||||
{ m00, -m01, m02 },
|
||||
{ -m10, m11, -m12 },
|
||||
{ m20, -m21, m22 }
|
||||
};
|
||||
|
||||
// Convert to PSX fixed-point format.
|
||||
int[,] psxMatrix = new int[3, 3];
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
for (int j = 0; j < 3; j++)
|
||||
{
|
||||
psxMatrix[i, j] = ConvertToFixed12(fixedMatrix[i, j]);
|
||||
}
|
||||
}
|
||||
|
||||
return psxMatrix;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a floating-point value to a 3.12 fixed-point format (PSX format).
|
||||
/// The value is scaled by a factor of 4096 and clamped to the range of a signed 16-bit integer.
|
||||
/// </summary>
|
||||
/// <param name="value">The floating-point value to convert.</param>
|
||||
/// <returns>The converted value in 3.12 fixed-point format as a 16-bit signed integer.</returns>
|
||||
public static short ConvertToFixed12(float value)
|
||||
{
|
||||
int fixedValue = Mathf.RoundToInt(value * 4096.0f); // Scale to 3.12 format
|
||||
return (short)Mathf.Clamp(fixedValue, -32768, 32767); // Clamp to signed 16-bit
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Represents the attributes of a texture page in the PSX graphics system.
|
||||
/// Provides methods for setting various properties such as the page coordinates, transparency type, color mode, dithering, and display area.
|
||||
/// </summary>
|
||||
public struct TPageAttr
|
||||
{
|
||||
public ushort info; // Stores the packed attribute information as a 16-bit unsigned integer.
|
||||
|
||||
/// <summary>
|
||||
/// Sets the X-coordinate of the texture page.
|
||||
/// The lower 4 bits of the 'info' field are used to store the X value.
|
||||
/// </summary>
|
||||
/// <param name="x">The X-coordinate value (0 to 15).</param>
|
||||
/// <returns>The updated TPageAttr instance.</returns>
|
||||
public TPageAttr SetPageX(byte x)
|
||||
{
|
||||
info &= 0xFFF0; // Clear lower 4 bits
|
||||
x &= 0x0F; // Ensure only lower 4 bits are used
|
||||
info |= x;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Y-coordinate of the texture page.
|
||||
/// The 4th bit of the 'info' field is used to store the Y value (0 or 1).
|
||||
/// </summary>
|
||||
/// <param name="y">The Y-coordinate value (0 or 1).</param>
|
||||
/// <returns>The updated TPageAttr instance.</returns>
|
||||
public TPageAttr SetPageY(byte y)
|
||||
{
|
||||
info &= 0xFFEF; // Clear bit 4
|
||||
y &= 0x01; // Ensure only lower 1 bit is used
|
||||
info |= (ushort)(y << 4);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the transparency type of the texture page.
|
||||
/// The transparency type is stored in bits 5 and 6 of the 'info' field.
|
||||
/// </summary>
|
||||
/// <param name="trans">The transparency type to set.</param>
|
||||
/// <returns>The updated TPageAttr instance.</returns>
|
||||
public TPageAttr Set(SemiTrans trans)
|
||||
{
|
||||
info &= 0xFF9F; // Clear bits 5 and 6
|
||||
uint t = (uint)trans;
|
||||
info |= (ushort)(t << 5);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the color mode of the texture page.
|
||||
/// The color mode is stored in bits 7 and 8 of the 'info' field.
|
||||
/// </summary>
|
||||
/// <param name="mode">The color mode to set (4-bit, 8-bit, or 16-bit).</param>
|
||||
/// <returns>The updated TPageAttr instance.</returns>
|
||||
public TPageAttr Set(ColorMode mode)
|
||||
{
|
||||
info &= 0xFE7F; // Clear bits 7 and 8
|
||||
uint m = (uint)mode;
|
||||
info |= (ushort)(m << 7);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables dithering for the texture page.
|
||||
/// Dithering is stored in bit 9 of the 'info' field.
|
||||
/// </summary>
|
||||
/// <param name="dithering">True to enable dithering, false to disable it.</param>
|
||||
/// <returns>The updated TPageAttr instance.</returns>
|
||||
public TPageAttr SetDithering(bool dithering)
|
||||
{
|
||||
if (dithering)
|
||||
info |= 0x0200; // Set bit 9 to enable dithering
|
||||
else
|
||||
info &= 0xFDFF; // Clear bit 9 to disable dithering
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disables the display area for the texture page.
|
||||
/// This will clear bit 10 of the 'info' field.
|
||||
/// </summary>
|
||||
/// <returns>The updated TPageAttr instance.</returns>
|
||||
public TPageAttr DisableDisplayArea()
|
||||
{
|
||||
info &= 0xFBFF; // Clear bit 10
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables the display area for the texture page.
|
||||
/// This will set bit 10 of the 'info' field.
|
||||
/// </summary>
|
||||
/// <returns>The updated TPageAttr instance.</returns>
|
||||
public TPageAttr EnableDisplayArea()
|
||||
{
|
||||
info |= 0x0400; // Set bit 10 to enable display area
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the TPageAttr instance, showing the 'info' value in hexadecimal.
|
||||
/// </summary>
|
||||
/// <returns>A string representing the 'info' value in hexadecimal format.</returns>
|
||||
public override string ToString() => $"Info: 0x{info:X4}";
|
||||
|
||||
// Define the enums for SemiTrans and ColorMode (assuming their values)
|
||||
|
||||
/// <summary>
|
||||
/// Defines the transparency types for a texture page.
|
||||
/// </summary>
|
||||
public enum SemiTrans : uint
|
||||
{
|
||||
None = 0,
|
||||
Type1 = 1,
|
||||
Type2 = 2,
|
||||
Type3 = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the color modes for a texture page.
|
||||
/// </summary>
|
||||
public enum ColorMode : uint
|
||||
{
|
||||
Mode4Bit = 0,
|
||||
Mode8Bit = 1,
|
||||
Mode16Bit = 2
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class Utils
|
||||
{
|
||||
public static (Rect, Rect) BufferForResolution(Vector2 selectedResolution, bool verticalLayout, Vector2 offset = default)
|
||||
{
|
||||
if (offset == default)
|
||||
{
|
||||
offset = Vector2.zero;
|
||||
}
|
||||
Rect buffer1 = new Rect(offset.x, offset.y, selectedResolution.x, selectedResolution.y);
|
||||
Rect buffer2 = verticalLayout ? new Rect(offset.x, 256, selectedResolution.x, selectedResolution.y)
|
||||
: new Rect(offset.x + selectedResolution.x, offset.y, selectedResolution.x, selectedResolution.y);
|
||||
return (buffer1, buffer2);
|
||||
}
|
||||
|
||||
public static TPageAttr.ColorMode ToColorMode(this PSXBPP depth)
|
||||
{
|
||||
return depth switch
|
||||
{
|
||||
PSXBPP.TEX_4BIT => TPageAttr.ColorMode.Mode4Bit,
|
||||
PSXBPP.TEX_8BIT => TPageAttr.ColorMode.Mode8Bit,
|
||||
PSXBPP.TEX_16BIT => TPageAttr.ColorMode.Mode16Bit,
|
||||
_ => throw new System.NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public static byte Clamp0255(float v) => (byte)(Mathf.Clamp(v, 0, 255));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
8
doc.meta
Normal file
8
doc.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 311ff9868024b5078bd12a6c2252a4ef
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
93
doc/splashbundle.md
Normal file
93
doc/splashbundle.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# SPLASHPACK Binary File Format Specification
|
||||
|
||||
All numeric values are stored in little‐endian format. All offsets are counted from the beginning of the file.
|
||||
|
||||
---
|
||||
|
||||
## 1. File Header (12 bytes)
|
||||
|
||||
| Offset | Size | Type | Description |
|
||||
| ------ | ---- | ------ | ----------------------------------- |
|
||||
| 0x00 | 2 | char | `'SP'` – File magic |
|
||||
| 0x02 | 2 | uint16 | Version number (currently **1**) |
|
||||
| 0x04 | 2 | uint16 | Number of Exporter descriptors |
|
||||
| 0x06 | 2 | uint16 | Number of Texture Atlas descriptors |
|
||||
| 0x08 | 2 | uint16 | Number of CLUT descriptors |
|
||||
| 0x0A | 2 | uint16 | Reserved (always 0) |
|
||||
|
||||
---
|
||||
|
||||
## 2. Metadata Section
|
||||
|
||||
The metadata section comprises three groups of descriptors.
|
||||
|
||||
### 2.1 Exporter Descriptors (56 bytes each)
|
||||
|
||||
Each exporter descriptor stores the transform and mesh metadata for one GameObject.
|
||||
|
||||
| Offset (per entry) | Size | Type | Description |
|
||||
| ------------------ | ---- | ------ | --------------------------------- |
|
||||
| 0x00 | 4 | int | X coordinate (Fixed-point) |
|
||||
| 0x04 | 4 | int | Y coordinate (Fixed-point) |
|
||||
| 0x08 | 4 | int | Z coordinate (Fixed-point) |
|
||||
| 0x0C | 36 | int[9] | 3×3 Rotation matrix (Fixed-point) |
|
||||
| 0x30 | 4 | int | **Mesh Data Offset Placeholder** |
|
||||
| 0x34 | 4 | int | Triangle count in the mesh |
|
||||
|
||||
### 2.2 Texture Atlas Descriptors (12 bytes each)
|
||||
|
||||
Each texture atlas descriptor holds atlas layout data and a placeholder for the atlas raw data offset.
|
||||
|
||||
| Offset (per entry) | Size | Type | Description |
|
||||
| ------------------ | ---- | ------ | -------------------------------------------------------- |
|
||||
| 0x00 | 4 | int | **Atlas Data Offset Placeholder** |
|
||||
| 0x04 | 2 | uint16 | Atlas width |
|
||||
| 0x06 | 2 | uint16 | Atlas height (currently always 256, for future-proofing) |
|
||||
| 0x08 | 2 | uint16 | Atlas position X – relative to VRAM origin |
|
||||
| 0x0A | 2 | uint16 | Atlas position Y – relative to VRAM origin |
|
||||
|
||||
### 2.3 CLUT Descriptors (520 bytes each)
|
||||
|
||||
CLUTs are the only data which is stored in the Metadata section.
|
||||
For each CLUT (Color Lookup Table) associated with an atlas texture that has a palette:
|
||||
|
||||
| Offset (per entry) | Size | Type | Description |
|
||||
| ------------------ | ---- | ----------- | --------------------------------------------------------------------------------- |
|
||||
| 0x00 | 512 | uint16[256] | Color palette entries (each entry is 2 bytes 16bpp). If unused, entries are zero. |
|
||||
| 0x200 | 2 | uint16 | CLUT packing X coordinate - already in 16 pixel steps |
|
||||
| 0x202 | 2 | uint16 | CLUT packing Y coordinate |
|
||||
| 0x204 | 2 | uint16 | Palette count (number of valid palette entries) |
|
||||
| 0x206 | 2 | uint16 | Reserved (always 0) |
|
||||
|
||||
---
|
||||
|
||||
## 3. Data Section
|
||||
|
||||
The data section contains the actual mesh and atlas raw data.
|
||||
|
||||
### 3.1 Mesh Data Blocks
|
||||
|
||||
For each exporter, a mesh data block is written at the offset specified in its descriptor. Each mesh block contains data for all triangles of the associated mesh.
|
||||
|
||||
#### **Triangle Data Layout (per triangle – 52 bytes total):**
|
||||
|
||||
| Field | Size | Description |
|
||||
| ----------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Vertex Coordinates** | 3 vertices × 3 × 2 bytes = 18 bytes | For each vertex (v0, v1, v2): X, Y, Z coordinates (int16) |
|
||||
| **Vertex Normal** | 3 × 2 bytes = 6 bytes | Normal vector for vertex v0 (int16: nx, ny, nz) |
|
||||
| **Vertex Colors** | 3 vertices × (3 bytes color + 1 byte padding) = 12 bytes | For each vertex (v0, v1, v2): Red, Green, Blue (uint8) plus 1 byte padding |
|
||||
| **Texture Coordinates (UVs)** | 3 vertices × 2 bytes = 6 bytes | For each vertex (v0, v1, v2): U and V coordinates (uint8), adjusted by texture packing factors |
|
||||
| **UV Padding** | 2 bytes | Padding (uint16, set to zero) |
|
||||
| **Texture Attributes** | 2 + 2 + 2 + 2 = 8 bytes | Contains: <br> • Texture page attributes (uint16 – encoded from page X/Y, bit depth, dithering)<br> • Texture CLUT packing X (uint16)<br> • Texture CLUT packing Y (uint16)<br> • Reserved (uint16, set to zero) |
|
||||
|
||||
*Triangles are written sequentially. Prior to writing each mesh data block, the file pointer is aligned to a 4-byte boundary.*
|
||||
|
||||
### 3.2 Atlas Data Blocks
|
||||
|
||||
For each atlas, a raw texture data block is written at the offset specified in its descriptor. Before writing, the file pointer is aligned to a 4-byte boundary.
|
||||
|
||||
- **Raw Texture Data:**
|
||||
The atlas data is written pixel by pixel as returned by the pixel packing function. The total size equals
|
||||
*(Atlas Width × Atlas Height)* The data is prepared for a DMA transfer to the VRAM.
|
||||
|
||||
---
|
||||
7
doc/splashbundle.md.meta
Normal file
7
doc/splashbundle.md.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1944ac962a00b23c2a880b5134cdc7ab
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user