using System.Collections.Generic; using System.Linq; using UnityEngine; namespace SplashEdit.RuntimeCode { /// /// Represents a vertex formatted for the PSX (PlayStation) style rendering. /// public struct PSXVertex { // Position components in fixed-point format. public short vx, vy, vz; // Normal vector components in fixed-point format. public short nx, ny, nz; // Texture coordinates. public byte u, v; // Vertex color components. public byte r, g, b; } /// /// Represents a triangle defined by three PSX vertices. /// public struct Tri { public PSXVertex v0; public PSXVertex v1; public PSXVertex v2; /// /// Index into the texture list for this triangle's material. /// -1 means untextured (vertex-color only, rendered as POLY_G3). /// public int TextureIndex; /// /// Whether this triangle is untextured (vertex-color only). /// Untextured triangles are rendered as GouraudTriangle (POLY_G3) on PS1. /// public bool IsUntextured => TextureIndex == -1; } /// /// A mesh structure that holds a list of triangles converted from a Unity mesh into the PSX format. /// [System.Serializable] public class PSXMesh { public List Triangles; private static Vector3[] RecalculateSmoothNormals(Mesh mesh) { Vector3[] normals = new Vector3[mesh.vertexCount]; Dictionary> vertexMap = new Dictionary>(); for (int i = 0; i < mesh.vertexCount; i++) { Vector3 vertex = mesh.vertices[i]; if (!vertexMap.ContainsKey(vertex)) { vertexMap[vertex] = new List(); } 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; } /// /// Creates a PSXMesh from a Unity Renderer by extracting its mesh and materials. /// public static PSXMesh CreateFromUnityRenderer(Renderer renderer, float GTEScaling, Transform transform, List textures) { Mesh mesh = renderer.GetComponent().sharedMesh; return BuildFromMesh(mesh, renderer, GTEScaling, transform, textures); } /// /// Creates a PSXMesh from a supplied Unity Mesh with the renderer's materials. /// public static PSXMesh CreateFromUnityMesh(Mesh mesh, Renderer renderer, float GTEScaling, Transform transform, List textures) { return BuildFromMesh(mesh, renderer, GTEScaling, transform, textures); } private static PSXMesh BuildFromMesh(Mesh mesh, Renderer renderer, float GTEScaling, Transform transform, List textures) { PSXMesh psxMesh = new PSXMesh { Triangles = new List() }; Material[] materials = renderer.sharedMaterials; // Guard: only recalculate normals if missing if (mesh.normals == null || mesh.normals.Length == 0) mesh.RecalculateNormals(); if (mesh.uv == null || mesh.uv.Length == 0) mesh.uv = new Vector2[mesh.vertices.Length]; Vector3[] smoothNormals = RecalculateSmoothNormals(mesh); // Cache lights once for the entire mesh Light[] sceneLights = Object.FindObjectsByType(FindObjectsSortMode.None) .Where(l => l.enabled).ToArray(); // Precompute world positions and normals for all vertices Vector3[] worldVertices = new Vector3[mesh.vertices.Length]; Vector3[] worldNormals = new Vector3[mesh.normals.Length]; for (int i = 0; i < mesh.vertices.Length; i++) { worldVertices[i] = transform.TransformPoint(mesh.vertices[i]); worldNormals[i] = transform.TransformDirection(smoothNormals[i]).normalized; } for (int submeshIndex = 0; submeshIndex < mesh.subMeshCount; submeshIndex++) { int materialIndex = Mathf.Min(submeshIndex, materials.Length - 1); Material material = materials[materialIndex]; Texture2D texture = material != null ? material.mainTexture as Texture2D : null; int textureIndex = -1; if (texture != null) { for (int i = 0; i < textures.Count; i++) { if (textures[i].OriginalTexture == texture) { textureIndex = i; break; } } } int[] submeshTriangles = mesh.GetTriangles(submeshIndex); Vector3[] vertices = mesh.vertices; Vector3[] normals = mesh.normals; Vector2[] uv = mesh.uv; PSXVertex convertData(int index) { Vector3 v = Vector3.Scale(vertices[index], transform.lossyScale); Vector3 wv = worldVertices[index]; Vector3 wn = worldNormals[index]; Color c = PSXLightingBaker.ComputeLighting(wv, wn, sceneLights); if (textureIndex == -1) { Color matColor = material != null && material.HasProperty("_Color") ? material.color : Color.white; c = new Color(c.r * matColor.r, c.g * matColor.g, c.b * matColor.b); return ConvertToPSXVertex(v, GTEScaling, normals[index], Vector2.zero, null, null, c); } return ConvertToPSXVertex(v, GTEScaling, normals[index], uv[index], textures[textureIndex]?.Width, textures[textureIndex]?.Height, c); } 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); } psxMesh.Triangles.Add(new Tri { v0 = convertData(vid0), v1 = convertData(vid1), v2 = convertData(vid2), TextureIndex = textureIndex }); } } return psxMesh; } /// /// Converts a Unity vertex into a PSXVertex by applying fixed-point conversion, shading, and UV mapping. /// /// The position of the vertex. /// World-to-GTE scaling factor. /// The normal vector at the vertex. /// Texture coordinates for the vertex. /// Width of the texture for UV scaling. /// Height of the texture for UV scaling. /// Pre-computed vertex color from lighting. /// A PSXVertex with converted coordinates, normals, UVs, and color. private static PSXVertex ConvertToPSXVertex(Vector3 vertex, float GTEScaling, Vector3 normal, Vector2 uv, int? textureWidth, int? textureHeight, Color color) { int width = textureWidth ?? 0; int height = textureHeight ?? 0; PSXVertex psxVertex = new PSXVertex { // Convert position to fixed-point, clamping values to a defined range. vx = PSXTrig.ConvertCoordinateToPSX(vertex.x, GTEScaling), vy = PSXTrig.ConvertCoordinateToPSX(-vertex.y, GTEScaling), vz = PSXTrig.ConvertCoordinateToPSX(vertex.z, GTEScaling), // Convert normals to fixed-point. 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 * (width - 1), 0, 255), v = (byte)Mathf.Clamp((1.0f - uv.y) * (height - 1), 0, 255), // Apply lighting to the colors. r = Utils.ColorUnityToPSX(color.r), g = Utils.ColorUnityToPSX(color.g), b = Utils.ColorUnityToPSX(color.b), }; return psxVertex; } } }