Improved light baking, updated splashpack structure and documentation

This commit is contained in:
2025-03-30 21:31:02 +02:00
parent 9e0d1557ee
commit da12ade580
9 changed files with 523 additions and 227 deletions

View 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;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b707b7d499862621fb6c82aba4caa183

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Diagnostics;
@@ -27,6 +28,8 @@ namespace SplashEdit.RuntimeCode
public PSXVertex v0;
public PSXVertex v1;
public PSXVertex v2;
public PSXTexture2D Texture;
}
/// <summary>
@@ -37,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>
@@ -45,57 +82,99 @@ 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, float GTEScaling, Transform transform, bool isStatic, int textureWidth = 256, int textureHeight = 256)
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);
Vector3 v0, v1, v2;
// Get the material for this submesh.
Material material = materials[submeshIndex];
// Transform vertices to world space if a transform is provided.
// Get the corresponding texture for this material (assume mainTexture).
Texture2D texture = material.mainTexture as Texture2D;
PSXTexture2D psxTexture = null;
if (isStatic)
if (texture != null)
{
v0 = transform.TransformPoint(vertices[vid0]);
v1 = transform.TransformPoint(vertices[vid1]);
v2 = transform.TransformPoint(vertices[vid2]);
}
else
{
// Extract ONLY world scale
Vector3 worldScale = transform.lossyScale;
// Apply scale *before* transformation, ensuring rotation isnt affected
v0 = Vector3.Scale(vertices[vid0], worldScale);
v1 = Vector3.Scale(vertices[vid1], worldScale);
v2 = Vector3.Scale(vertices[vid2], worldScale);
// Find the corresponding PSX texture based on the Unity texture.
psxTexture = textures.FirstOrDefault(t => t.OriginalTexture == texture);
}
// Convert vertices to PSX format including fixed-point conversion and shading.
PSXVertex psxV0 = ConvertToPSXVertex(v0, GTEScaling, normals[vid0], uv[vid0], lightDir, lightColor, textureWidth, textureHeight);
PSXVertex psxV1 = ConvertToPSXVertex(v1, GTEScaling, normals[vid1], uv[vid1], lightDir, lightColor, textureWidth, textureHeight);
PSXVertex psxV2 = ConvertToPSXVertex(v2, GTEScaling, normals[vid2], uv[vid2], lightDir, lightColor, textureWidth, textureHeight);
if (psxTexture == null)
{
continue;
}
// Add the constructed triangle to the mesh.
psxMesh.Triangles.Add(new Tri { v0 = psxV0, v1 = psxV1, v2 = psxV2 });
// Get mesh data arrays.
Vector3[] vertices = mesh.vertices;
Vector3[] normals = mesh.normals;// Assuming this function recalculates normals
Vector3[] smoothNormals = RecalculateSmoothNormals(mesh);
Vector2[] uv = mesh.uv;
// Iterate through the triangles of the submesh.
for (int i = 0; i < submeshTriangles.Length; i += 3)
{
int vid0 = submeshTriangles[i];
int vid1 = submeshTriangles[i + 1];
int vid2 = submeshTriangles[i + 2];
Vector3 faceNormal = Vector3.Cross(vertices[vid1] - vertices[vid0], vertices[vid2] - vertices[vid0]).normalized;
if (Vector3.Dot(faceNormal, normals[vid0]) < 0)
{
(vid1, vid2) = (vid2, vid1);
}
// Scale the vertices based on world scale.
Vector3 v0 = Vector3.Scale(vertices[vid0], transform.lossyScale);
Vector3 v1 = Vector3.Scale(vertices[vid1], transform.lossyScale);
Vector3 v2 = Vector3.Scale(vertices[vid2], transform.lossyScale);
// Transform the vertices to world space.
Vector3 wv0 = transform.TransformPoint(vertices[vid0]);
Vector3 wv1 = transform.TransformPoint(vertices[vid1]);
Vector3 wv2 = transform.TransformPoint(vertices[vid2]);
// Transform the normals to world space.
Vector3 wn0 = transform.TransformDirection(smoothNormals[vid0]).normalized;
Vector3 wn1 = transform.TransformDirection(smoothNormals[vid1]).normalized;
Vector3 wn2 = transform.TransformDirection(smoothNormals[vid2]).normalized;
// Compute lighting for each vertex (this can be a custom function).
Color cv0 = PSXLightingBaker.ComputeLighting(wv0, wn0);
Color cv1 = PSXLightingBaker.ComputeLighting(wv1, wn1);
Color cv2 = PSXLightingBaker.ComputeLighting(wv2, wn2);
// Convert vertices to PSX format, including fixed-point conversion and shading.
PSXVertex psxV0 = ConvertToPSXVertex(v0, GTEScaling, normals[vid0], uv[vid0], psxTexture?.Width ?? 0, psxTexture?.Height ?? 0);
PSXVertex psxV1 = ConvertToPSXVertex(v1, GTEScaling, normals[vid1], uv[vid1], psxTexture?.Width ?? 0, psxTexture?.Height ?? 0);
PSXVertex psxV2 = ConvertToPSXVertex(v2, GTEScaling, normals[vid2], uv[vid2], psxTexture?.Width ?? 0, psxTexture?.Height ?? 0);
// Apply lighting to the colors.
psxV0.r = (byte)Mathf.Clamp(cv0.r * 255, 0, 255);
psxV0.g = (byte)Mathf.Clamp(cv0.g * 255, 0, 255);
psxV0.b = (byte)Mathf.Clamp(cv0.b * 255, 0, 255);
psxV1.r = (byte)Mathf.Clamp(cv1.r * 255, 0, 255);
psxV1.g = (byte)Mathf.Clamp(cv1.g * 255, 0, 255);
psxV1.b = (byte)Mathf.Clamp(cv1.b * 255, 0, 255);
psxV2.r = (byte)Mathf.Clamp(cv2.r * 255, 0, 255);
psxV2.g = (byte)Mathf.Clamp(cv2.g * 255, 0, 255);
psxV2.b = (byte)Mathf.Clamp(cv2.b * 255, 0, 255);
// Add the constructed triangle to the mesh.
psxMesh.Triangles.Add(new Tri { v0 = psxV0, v1 = psxV1, v2 = psxV2, Texture = psxTexture });
}
}
return psxMesh;
@@ -112,13 +191,9 @@ namespace SplashEdit.RuntimeCode
/// <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, float GTEScaling, Vector3 normal, Vector2 uv, Vector3 lightDir, Color lightColor, int textureWidth, int textureHeight)
private static PSXVertex ConvertToPSXVertex(Vector3 vertex, float GTEScaling, Vector3 normal, Vector2 uv, int textureWidth, int textureHeight)
{
// Calculate light intensity based on the angle between the normalized normal and light direction.
float lightIntensity = Mathf.Clamp01(Vector3.Dot(normal.normalized, lightDir));
// Compute the final shaded color by multiplying the light color by the intensity.
Color shadedColor = lightColor * lightIntensity;
PSXVertex psxVertex = new PSXVertex
{
@@ -137,9 +212,7 @@ namespace SplashEdit.RuntimeCode
v = (byte)Mathf.Clamp((1.0f - uv.y) * (textureHeight - 1), 0, 255),
// Convert the computed color to a byte range.
r = (byte)Mathf.Clamp(shadedColor.r * 255, 0, 255),
g = (byte)Mathf.Clamp(shadedColor.g * 255, 0, 255),
b = (byte)Mathf.Clamp(shadedColor.b * 255, 0, 255)
};
return psxVertex;

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using UnityEngine;
namespace SplashEdit.RuntimeCode
@@ -5,36 +6,111 @@ namespace SplashEdit.RuntimeCode
public class PSXObjectExporter : MonoBehaviour
{
public PSXBPP BitDepth = PSXBPP.TEX_8BIT; // Defines the bit depth of the texture (e.g., 4BPP, 8BPP)
public bool MeshIsStatic = true; // Determines if the mesh is static, affecting how it's processed. Non-static meshes don't export correctly as of now.
[HideInInspector]
public PSXTexture2D Texture; // Stores the converted PlayStation-style texture
public List<PSXTexture2D> Textures = new List<PSXTexture2D>(); // Stores the converted PlayStation-style texture
[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 bool PreviewNormals = false;
public float normalPreviewLength = 0.5f; // Length of the normal lines
private void OnDrawGizmos()
{
Renderer renderer = GetComponent<Renderer>();
if (renderer != null && renderer.sharedMaterial != null && renderer.sharedMaterial.mainTexture is Texture2D texture)
if (PreviewNormals)
{
Texture = PSXTexture2D.CreateFromTexture2D(texture, BitDepth);
Texture.OriginalTexture = texture; // Stores reference to the original texture
MeshFilter filter = GetComponent<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);
}
}
}
}
/// <summary>
/// Converts the object's material texture into a PlayStation-compatible texture.
/// </summary>
///
public void CreatePSXTextures2D()
{
Renderer renderer = GetComponent<Renderer>();
if (renderer != null)
{
Material[] materials = renderer.sharedMaterials;
Textures = new List<PSXTexture2D>(); // Ensure the list is initialized
foreach (Material mat in materials)
{
if (mat != null && mat.mainTexture != null)
{
Texture mainTexture = mat.mainTexture;
Texture2D tex2D = null;
// Check if it's already a Texture2D
if (mainTexture is Texture2D existingTex2D)
{
tex2D = existingTex2D;
}
else
{
// If not a Texture2D, try to convert
tex2D = ConvertToTexture2D(mainTexture);
}
if (tex2D != null)
{
PSXTexture2D tex = PSXTexture2D.CreateFromTexture2D(tex2D, BitDepth);
tex.OriginalTexture = tex2D; // Store reference to the original texture
Textures.Add(tex);
}
}
}
}
}
private Texture2D ConvertToTexture2D(Texture texture)
{
// Create a new Texture2D with the same dimensions and format
Texture2D texture2D = new Texture2D(texture.width, texture.height, TextureFormat.RGBA32, false);
// Read the texture pixels
RenderTexture currentActiveRT = RenderTexture.active;
RenderTexture.active = texture as RenderTexture;
texture2D.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0);
texture2D.Apply();
RenderTexture.active = currentActiveRT;
return texture2D;
}
/// <summary>
/// Converts the object's mesh into a PlayStation-compatible mesh.
/// </summary>
public void CreatePSXMesh(float GTEScaling)
{
MeshFilter meshFilter = gameObject.GetComponent<MeshFilter>();
if (meshFilter != null)
Renderer renderer = GetComponent<Renderer>();
if (renderer != null)
{
Mesh = PSXMesh.CreateFromUnityMesh(meshFilter.sharedMesh, GTEScaling, transform, MeshIsStatic, Texture.Width, Texture.Height);
Mesh = PSXMesh.CreateFromUnityRenderer(renderer, GTEScaling, transform, Textures);
}
}
}

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
@@ -33,7 +34,7 @@ namespace SplashEdit.RuntimeCode
_exporters = FindObjectsByType<PSXObjectExporter>(FindObjectsSortMode.None);
foreach (PSXObjectExporter exp in _exporters)
{
exp.CreatePSXTexture2D();
exp.CreatePSXTextures2D();
exp.CreatePSXMesh(GTEScaling);
}
PackTextures();
@@ -60,34 +61,6 @@ namespace SplashEdit.RuntimeCode
}
public static string PSXMatrixToStringMultiline(int[,] matrix)
{
return $@"
RT11={matrix[0, 0],6} RT12={matrix[0, 1],6} RT13={matrix[0, 2],6}
RT21={matrix[1, 0],6} RT22={matrix[1, 1],6} RT23={matrix[1, 2],6}
RT31={matrix[2, 0],6} RT32={matrix[2, 1],6} RT33={matrix[2, 2],6}";
}
public static Vector3 ConvertPSXMatrixToEulerAngles(int[,] psxMatrix)
{
// Convert PSX fixed-point (s3.12) to float
float r00 = psxMatrix[0, 0] / 4096.0f;
float r01 = psxMatrix[0, 1] / 4096.0f;
float r02 = psxMatrix[0, 2] / 4096.0f;
float r10 = psxMatrix[1, 0] / 4096.0f;
float r11 = psxMatrix[1, 1] / 4096.0f;
float r12 = psxMatrix[1, 2] / 4096.0f;
float r20 = psxMatrix[2, 0] / 4096.0f;
float r21 = psxMatrix[2, 1] / 4096.0f;
float r22 = psxMatrix[2, 2] / 4096.0f;
// Compute Euler angles (YXZ order for Unity)
float thetaX = Mathf.Asin(-r21) * Mathf.Rad2Deg; // X Rotation
float thetaY = Mathf.Atan2(r20, r22) * Mathf.Rad2Deg; // Y Rotation
float thetaZ = Mathf.Atan2(r01, r11) * Mathf.Rad2Deg; // Z Rotation
return new Vector3(thetaX, thetaY, thetaZ);
}
void ExportFile()
{
@@ -102,6 +75,19 @@ RT31={matrix[2, 0],6} RT32={matrix[2, 1],6} RT33={matrix[2, 2],6}";
List<long> atlasOffsetPlaceholderPositions = new List<long>();
List<long> atlasDataOffsets = new List<long>();
int clutCount = 0;
// Cluts
foreach (TextureAtlas atlas in _atlases)
{
foreach (var texture in atlas.ContainedTextures)
{
if (texture.ColorPalette != null)
{
clutCount++;
}
}
}
using (BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)))
{
@@ -111,6 +97,8 @@ RT31={matrix[2, 0],6} RT32={matrix[2, 1],6} RT33={matrix[2, 2],6}";
writer.Write((ushort)1);
writer.Write((ushort)_exporters.Length);
writer.Write((ushort)_atlases.Length);
writer.Write((ushort)clutCount);
writer.Write((ushort)0);
// Start of Metadata section
// GameObject section (exporters)
@@ -132,51 +120,12 @@ RT31={matrix[2, 0],6} RT32={matrix[2, 1],6} RT33={matrix[2, 2],6}";
writer.Write((int)rotationMatrix[2, 1]);
writer.Write((int)rotationMatrix[2, 2]);
writer.Write((ushort)exporter.Mesh.Triangles.Count);
// Set up texture page attributes
TPageAttr tpage = new TPageAttr();
tpage.SetPageX(exporter.Texture.TexpageX);
tpage.SetPageY(exporter.Texture.TexpageY);
switch (exporter.Texture.BitDepth)
{
case PSXBPP.TEX_4BIT:
tpage.Set(TPageAttr.ColorMode.Mode4Bit);
break;
case PSXBPP.TEX_8BIT:
tpage.Set(TPageAttr.ColorMode.Mode8Bit);
break;
case PSXBPP.TEX_16BIT:
tpage.Set(TPageAttr.ColorMode.Mode16Bit);
break;
}
tpage.SetDithering(true);
writer.Write((ushort)tpage.info);
writer.Write((ushort)exporter.Texture.ClutPackingX);
writer.Write((ushort)exporter.Texture.ClutPackingY);
if (exporter.Texture.BitDepth != PSXBPP.TEX_16BIT)
{
foreach (VRAMPixel color in exporter.Texture.ColorPalette)
{
writer.Write((ushort)color.Pack());
}
for (int i = exporter.Texture.ColorPalette.Count; i < 256; i++)
{
writer.Write((ushort)0);
}
}
else
{
for (int i = 0; i < 256; i++)
{
writer.Write((ushort)0);
}
}
// Write placeholder for mesh data offset and record its position.
offsetPlaceholderPositions.Add(writer.BaseStream.Position);
writer.Write((int)0); // 4-byte placeholder for mesh data offset.
writer.Write((int)exporter.Mesh.Triangles.Count);
}
// Atlas metadata section
@@ -192,20 +141,45 @@ RT31={matrix[2, 0],6} RT32={matrix[2, 1],6} RT33={matrix[2, 2],6}";
writer.Write((ushort)atlas.PositionY);
}
// Cluts
foreach (TextureAtlas atlas in _atlases)
{
foreach (var texture in atlas.ContainedTextures)
{
if (texture.ColorPalette != null)
{
foreach (VRAMPixel clutPixel in texture.ColorPalette)
{
writer.Write((ushort)clutPixel.Pack());
}
for (int i = texture.ColorPalette.Count; i < 256; i++)
{
writer.Write((ushort)0);
}
writer.Write((ushort)texture.ClutPackingX);
writer.Write((ushort)texture.ClutPackingY);
writer.Write((ushort)texture.ColorPalette.Count);
writer.Write((ushort)0);
}
}
}
// Start of data section
// Mesh data section: Write mesh data for each exporter.
foreach (PSXObjectExporter exporter in _exporters)
{
AlignToFourBytes(writer);
// Record the current offset for this exporter's mesh data.
long meshDataOffset = writer.BaseStream.Position;
meshDataOffsets.Add(meshDataOffset);
totalFaces += exporter.Mesh.Triangles.Count;
int expander = 16 / ((int)exporter.Texture.BitDepth);
foreach (Tri tri in exporter.Mesh.Triangles)
{
int expander = 16 / ((int)tri.Texture.BitDepth);
// Write vertices coordinates
writer.Write((short)tri.v0.vx);
writer.Write((short)tri.v0.vy);
@@ -224,18 +198,6 @@ RT31={matrix[2, 0],6} RT32={matrix[2, 1],6} RT33={matrix[2, 2],6}";
writer.Write((short)tri.v0.ny);
writer.Write((short)tri.v0.nz);
// Write UVs for each vertex, adjusting for texture packing
writer.Write((byte)(tri.v0.u + exporter.Texture.PackingX * expander));
writer.Write((byte)(tri.v0.v + exporter.Texture.PackingY));
writer.Write((byte)(tri.v1.u + exporter.Texture.PackingX * expander));
writer.Write((byte)(tri.v1.v + exporter.Texture.PackingY));
writer.Write((byte)(tri.v2.u + exporter.Texture.PackingX * expander));
writer.Write((byte)(tri.v2.v + exporter.Texture.PackingY));
writer.Write((ushort)0); // padding
// Write vertex colors with padding
writer.Write((byte)tri.v0.r);
writer.Write((byte)tri.v0.g);
@@ -251,6 +213,40 @@ RT31={matrix[2, 0],6} RT32={matrix[2, 1],6} RT33={matrix[2, 2],6}";
writer.Write((byte)tri.v2.g);
writer.Write((byte)tri.v2.b);
writer.Write((byte)0); // padding
// Write UVs for each vertex, adjusting for texture packing
writer.Write((byte)(tri.v0.u + tri.Texture.PackingX * expander));
writer.Write((byte)(tri.v0.v + tri.Texture.PackingY));
writer.Write((byte)(tri.v1.u + tri.Texture.PackingX * expander));
writer.Write((byte)(tri.v1.v + tri.Texture.PackingY));
writer.Write((byte)(tri.v2.u + tri.Texture.PackingX * expander));
writer.Write((byte)(tri.v2.v + tri.Texture.PackingY));
writer.Write((ushort)0); // padding
TPageAttr tpage = new TPageAttr();
tpage.SetPageX(tri.Texture.TexpageX);
tpage.SetPageY(tri.Texture.TexpageY);
switch (tri.Texture.BitDepth)
{
case PSXBPP.TEX_4BIT:
tpage.Set(TPageAttr.ColorMode.Mode4Bit);
break;
case PSXBPP.TEX_8BIT:
tpage.Set(TPageAttr.ColorMode.Mode8Bit);
break;
case PSXBPP.TEX_16BIT:
tpage.Set(TPageAttr.ColorMode.Mode16Bit);
break;
}
tpage.SetDithering(true);
writer.Write((ushort)tpage.info);
writer.Write((ushort)tri.Texture.ClutPackingX);
writer.Write((ushort)tri.Texture.ClutPackingY);
writer.Write((ushort)0);
}
}
@@ -309,5 +305,18 @@ RT31={matrix[2, 0],6} RT32={matrix[2, 1],6} RT33={matrix[2, 2],6}";
int padding = (int)(4 - (position % 4)) % 4; // Compute needed padding
writer.Write(new byte[padding]); // Write zero padding
}
void OnDrawGizmos()
{
Gizmos.DrawIcon(transform.position, "Packages/net.psxsplash.splashedit/Icons/PSXSceneExporter.png", true);
Vector3 sceneOrigin = new Vector3(0, 0, 0);
Vector3 cubeSize = new Vector3(8.0f * GTEScaling, 8.0f * GTEScaling, 8.0f * GTEScaling);
Gizmos.color = Color.red;
Gizmos.DrawWireCube(sceneOrigin, cubeSize);
}
}
}

View File

@@ -59,17 +59,31 @@ namespace SplashEdit.RuntimeCode
/// <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, TextureAtlas[] atlases, 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
@@ -80,33 +94,48 @@ namespace SplashEdit.RuntimeCode
_ => 256
};
// Create a new atlas for this group.
// Create an initial atlas for this group.
TextureAtlas atlas = new TextureAtlas { BitDepth = group.Key, Width = atlasWidth, PositionX = 0, PositionY = 0 };
_textureAtlases.Add(atlas);
// Process each texture in descending order of area (width * height).
foreach (var obj in group.OrderByDescending(obj => obj.Texture.QuantizedWidth * obj.Texture.Height))
// Process each texture in descending order of area.
foreach (var texture in group.OrderByDescending(tex => tex.QuantizedWidth * tex.Height))
{
// Remove duplicate textures
/*if (uniqueTextures.Any(tex => tex.OriginalTexture.GetInstanceID() == obj.Texture.OriginalTexture.GetInstanceID() && tex.BitDepth == obj.Texture.BitDepth))
// Check for duplicates in the entire set.
if (uniqueTextures.Any(tex => tex.OriginalTexture.GetInstanceID() == texture.OriginalTexture.GetInstanceID() &&
tex.BitDepth == texture.BitDepth))
{
obj.Texture = uniqueTextures.First(tex => tex.OriginalTexture.GetInstanceID() == obj.Texture.OriginalTexture.GetInstanceID());
// Skip packing this texture it will be replaced later.
continue;
}*/
}
// Try to place the texture in the current atlas.
if (!TryPlaceTextureInAtlas(atlas, obj.Texture))
if (!TryPlaceTextureInAtlas(atlas, texture))
{
// If failed, create a new atlas and try again.
// If failed, create a new atlas for this bit depth group and try again.
atlas = new TextureAtlas { BitDepth = group.Key, Width = atlasWidth, PositionX = 0, PositionY = 0 };
_textureAtlases.Add(atlas);
if (!TryPlaceTextureInAtlas(atlas, obj.Texture))
if (!TryPlaceTextureInAtlas(atlas, texture))
{
Debug.LogError($"Failed to pack texture {obj.Texture}. It might not fit.");
break;
Debug.LogError($"Failed to pack texture {texture}. It might not fit.");
continue;
}
}
uniqueTextures.Add(obj.Texture);
uniqueTextures.Add(texture);
}
}
// Now update every exporter so that duplicate textures reference the unique instance.
foreach (var obj in objects)
{
for (int i = 0; i < obj.Textures.Count; i++)
{
var unique = uniqueTextures.FirstOrDefault(tex => tex.OriginalTexture.GetInstanceID() == obj.Textures[i].OriginalTexture.GetInstanceID() &&
tex.BitDepth == obj.Textures[i].BitDepth);
if (unique != null)
{
obj.Textures[i] = unique;
}
}
}
@@ -114,7 +143,6 @@ namespace SplashEdit.RuntimeCode
ArrangeAtlasesInVRAM();
// Allocate color lookup tables (CLUTs) for textures that use palettes.
AllocateCLUTs();
// Build the final VRAM pixel array from placed textures and CLUTs.
BuildVram();
return (objects, _finalizedAtlases.ToArray(), _vramPixels);
@@ -227,7 +255,7 @@ namespace SplashEdit.RuntimeCode
if (IsPlacementValid(candidate))
{
_allocatedCLUTs.Add(candidate);
texture.ClutPackingX = (ushort)(x/16);
texture.ClutPackingX = (ushort)(x / 16);
texture.ClutPackingY = y;
placed = true;
break;
@@ -254,7 +282,6 @@ namespace SplashEdit.RuntimeCode
foreach (PSXTexture2D texture in atlas.ContainedTextures)
{
// Copy texture image data into VRAM using atlas and texture packing offsets.
for (int y = 0; y < texture.Height; y++)
{
@@ -298,6 +325,5 @@ namespace SplashEdit.RuntimeCode
return !(overlapsAtlas || overlapsReserved || overlapsCLUT);
}
}
}

View File

@@ -100,18 +100,34 @@ namespace SplashEdit.RuntimeCode
/// <returns>A 3x3 matrix representing the PSX-compatible rotation.</returns>
public static int[,] ConvertRotationToPSXMatrix(Quaternion rotation)
{
// Convert the quaternion to a Unity rotation matrix.
Matrix4x4 unityMatrix = Matrix4x4.Rotate(rotation);
// Standard quaternion-to-matrix conversion.
float x = rotation.x, y = rotation.y, z = rotation.z, w = rotation.w;
// Flip the Y-axis to match PSX's Y-down convention.
float m00 = 1f - 2f * (y * y + z * z);
float m01 = 2f * (x * y - z * w);
float m02 = 2f * (x * z + y * w);
float m10 = 2f * (x * y + z * w);
float m11 = 1f - 2f * (x * x + z * z);
float m12 = 2f * (y * z - x * w);
float m20 = 2f * (x * z - y * w);
float m21 = 2f * (y * z + x * w);
float m22 = 1f - 2f * (x * x + y * y);
// Apply Y-axis flip to match the PSX's Y-down convention.
// This replicates the behavior of:
// { m00, -m01, m02 },
// { -m10, m11, -m12 },
// { m20, -m21, m22 }
float[,] fixedMatrix = new float[3, 3]
{
{ unityMatrix.m00, -unityMatrix.m01, unityMatrix.m02 }, // Flip Y
{ -unityMatrix.m10, unityMatrix.m11, -unityMatrix.m12 }, // Flip Y
{ unityMatrix.m20, -unityMatrix.m21, unityMatrix.m22 } // Flip Y
{ m00, -m01, m02 },
{ -m10, m11, -m12 },
{ m20, -m21, m22 }
};
// Convert the Unity matrix to PSX fixed-point format.
// Convert to PSX fixed-point format.
int[,] psxMatrix = new int[3, 3];
for (int i = 0; i < 3; i++)
{