Improved light baking, updated splashpack structure and documentation
This commit is contained in:
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;
|
||||
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 isn’t 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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user