diff --git a/Editor/PSXObjectExporterEditor.cs b/Editor/PSXObjectExporterEditor.cs index 8b5d47d..29ff5c1 100644 --- a/Editor/PSXObjectExporterEditor.cs +++ b/Editor/PSXObjectExporterEditor.cs @@ -17,7 +17,7 @@ namespace PSXSplash.EditorCode EditorGUILayout.PropertyField(serializedObject.FindProperty("Mesh")); if (GUILayout.Button("Export mesh")) { - comp.Mesh.Export(); + comp.Mesh.Export(comp.gameObject); } EditorGUILayout.EndVertical(); @@ -26,7 +26,7 @@ namespace PSXSplash.EditorCode EditorGUILayout.PropertyField(serializedObject.FindProperty("Texture")); if (GUILayout.Button("Export texture")) { - comp.Texture.Export(); + comp.Texture.Export(comp.gameObject); } EditorGUILayout.EndVertical(); diff --git a/Editor/PSXSceneExporterEditor.cs b/Editor/PSXSceneExporterEditor.cs index 459c60e..b4cc044 100644 --- a/Editor/PSXSceneExporterEditor.cs +++ b/Editor/PSXSceneExporterEditor.cs @@ -3,12 +3,15 @@ using UnityEditor; using PSXSplash.RuntimeCode; [CustomEditor(typeof(PSXSceneExporter))] -public class PSXSceneExporterEditor : Editor { - public override void OnInspectorGUI() { +public class PSXSceneExporterEditor : Editor +{ + public override void OnInspectorGUI() + { DrawDefaultInspector(); - + PSXSceneExporter comp = (PSXSceneExporter)target; - if(GUILayout.Button("Export")) { + if (GUILayout.Button("Export")) + { comp.Export(); } diff --git a/Editor/QuantizedPreviewWindow.cs b/Editor/QuantizedPreviewWindow.cs new file mode 100644 index 0000000..7e0bf23 --- /dev/null +++ b/Editor/QuantizedPreviewWindow.cs @@ -0,0 +1,146 @@ +using UnityEditor; +using UnityEngine; +using PSXSplash.RuntimeCode; +using UnityEngine.Rendering; + +public class QuantizedPreviewWindow : EditorWindow +{ + private Texture2D originalTexture; + private Texture2D resizedTexture; + private Texture2D quantizedTexture; + private Vector3[,] clut; + private int bpp = 4; + private int targetWidth = 128; + private int targetHeight = 128; + private int previewSize = 256; + + [MenuItem("Window/Quantized Preview")] + public static void ShowWindow() + { + GetWindow("Quantized Preview"); + } + + private void OnGUI() + { + GUILayout.Label("Quantized Preview", EditorStyles.boldLabel); + + originalTexture = (Texture2D)EditorGUILayout.ObjectField("Original Texture", originalTexture, typeof(Texture2D), false); + + targetWidth = EditorGUILayout.IntField("Target Width", targetWidth); + targetHeight = EditorGUILayout.IntField("Target Height", targetHeight); + + bpp = EditorGUILayout.IntPopup("Bits Per Pixel", bpp, new[] { "4 bpp", "8 bpp" }, new[] { 4, 8 }); + + if (GUILayout.Button("Generate Quantized Preview") && originalTexture != null) + { + GenerateQuantizedPreview(); + } + + if (originalTexture != null) + { + GUILayout.Label("Original Texture"); + DrawTexturePreview(originalTexture, previewSize); + } + + if (resizedTexture != null) + { + GUILayout.Label("Resized Texture"); + DrawTexturePreview(resizedTexture, previewSize); + } + + if (quantizedTexture != null) + { + GUILayout.Label("Quantized Texture"); + DrawTexturePreview(quantizedTexture, previewSize); + } + + if (clut != null) + { + GUILayout.Label("Color Lookup Table (CLUT)"); + DrawCLUT(); + } + } + + private void GenerateQuantizedPreview() + { + resizedTexture = ResizeTexture(originalTexture, targetWidth, targetHeight); + + int maxColors = (int)Mathf.Pow(2, bpp); + + var (quantizedPixels, generatedClut) = ImageQuantizer.Quantize(resizedTexture, maxColors); + + quantizedTexture = new Texture2D(resizedTexture.width, resizedTexture.height); + Color[] quantizedColors = new Color[resizedTexture.width * resizedTexture.height]; + + for (int y = 0; y < resizedTexture.height; y++) + { + for (int x = 0; x < resizedTexture.width; x++) + { + quantizedColors[y * resizedTexture.width + x] = new Color( + quantizedPixels[x, y, 0], + quantizedPixels[x, y, 1], + quantizedPixels[x, y, 2] + ); + } + } + + quantizedTexture.SetPixels(quantizedColors); + quantizedTexture.Apply(); + + clut = generatedClut; + } + + private void DrawTexturePreview(Texture2D texture, int size) + { + Rect rect = GUILayoutUtility.GetRect(size, size, GUILayout.ExpandWidth(false)); + EditorGUI.DrawPreviewTexture(rect, texture, null, ScaleMode.ScaleToFit, 0, 0, ColorWriteMask.All); + } + + private void DrawCLUT() + { + if (clut == null) return; + + int swatchSize = 20; + int maxColorsPerRow = 40; + + GUILayout.Space(10); + + int totalColors = clut.GetLength(0); + int totalRows = Mathf.CeilToInt((float)totalColors / maxColorsPerRow); + + for (int row = 0; row < totalRows; row++) + { + GUILayout.BeginHorizontal(); + + int colorsInRow = Mathf.Min(maxColorsPerRow, totalColors - row * maxColorsPerRow); + + for (int col = 0; col < colorsInRow; col++) + { + int index = row * maxColorsPerRow + col; + Vector3 color = clut[index, 0]; + Rect rect = GUILayoutUtility.GetRect(swatchSize, swatchSize, GUILayout.ExpandWidth(false)); + EditorGUI.DrawRect(rect, new Color(color.x, color.y, color.z)); + } + + GUILayout.EndHorizontal(); + } + } + + + private Texture2D ResizeTexture(Texture2D source, int newWidth, int newHeight) + { + RenderTexture rt = RenderTexture.GetTemporary(newWidth, newHeight); + rt.antiAliasing = 1; + Graphics.Blit(source, rt); + + Texture2D resizedTexture = new Texture2D(newWidth, newHeight); + RenderTexture.active = rt; + resizedTexture.ReadPixels(new Rect(0, 0, newWidth, newHeight), 0, 0); + resizedTexture.Apply(); + + RenderTexture.active = null; + RenderTexture.ReleaseTemporary(rt); + + return resizedTexture; + } +} diff --git a/Editor/QuantizedPreviewWindow.cs.meta b/Editor/QuantizedPreviewWindow.cs.meta new file mode 100644 index 0000000..5874c2a --- /dev/null +++ b/Editor/QuantizedPreviewWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b50b0d7a7033bac78b14fce6e89cc015 \ No newline at end of file diff --git a/Runtime/ImageQuantizer.cs b/Runtime/ImageQuantizer.cs new file mode 100644 index 0000000..5b5548d --- /dev/null +++ b/Runtime/ImageQuantizer.cs @@ -0,0 +1,137 @@ +using System.Collections.Generic; +using UnityEngine; + +public class ImageQuantizer +{ + /// + /// Quantizes a texture and outputs a 3D pixel array. + /// + /// The input texture. + /// The maximum number of colors in the quantized image. + /// The maximum number of iterations for the k-means algorithm. + /// A tuple containing a 3D pixel array and the color lookup table. + public static (float[,,], Vector3[,]) Quantize(Texture2D image, int maxColors, int maxIterations = 10) + { + int width = image.width; + int height = image.height; + + List centroids = InitializeCentroids(image, maxColors); + + Color[] pixels = image.GetPixels(); + Vector3[] pixelColors = new Vector3[pixels.Length]; + for (int i = 0; i < pixels.Length; i++) + { + pixelColors[i] = new Vector3(pixels[i].r, pixels[i].g, pixels[i].b); + } + + // Storage for pixel-to-centroid assignments + int[] assignments = new int[pixelColors.Length]; + + for (int iteration = 0; iteration < maxIterations; iteration++) + { + bool centroidsChanged = false; + + // Step 1: Assign each pixel to the closest centroid + for (int i = 0; i < pixelColors.Length; i++) + { + int closestCentroid = GetClosestCentroid(pixelColors[i], centroids); + if (assignments[i] != closestCentroid) + { + assignments[i] = closestCentroid; + centroidsChanged = true; + } + } + + // Step 2: Recalculate centroids + Vector3[] newCentroids = new Vector3[centroids.Count]; + int[] centroidCounts = new int[centroids.Count]; + + for (int i = 0; i < assignments.Length; i++) + { + int centroidIndex = assignments[i]; + newCentroids[centroidIndex] += pixelColors[i]; + centroidCounts[centroidIndex]++; + } + + for (int i = 0; i < centroids.Count; i++) + { + if (centroidCounts[i] > 0) + { + newCentroids[i] /= centroidCounts[i]; + } + else + { + newCentroids[i] = RandomizeCentroid(image); + } + } + + if (!centroidsChanged) break; + + centroids = new List(newCentroids); + } + + float[,,] pixelArray = new float[width, height, 3]; + for (int i = 0; i < pixelColors.Length; i++) + { + int x = i % width; + int y = i / width; + + Vector3 centroidColor = centroids[assignments[i]]; + pixelArray[x, y, 0] = centroidColor.x; // Red + pixelArray[x, y, 1] = centroidColor.y; // Green + pixelArray[x, y, 2] = centroidColor.z; // Blue + } + + Vector3[,] clut = new Vector3[maxColors, 1]; + for (int i = 0; i < centroids.Count; i++) + { + clut[i, 0] = centroids[i]; + } + + return (pixelArray, clut); + } + + private static List InitializeCentroids(Texture2D image, int maxColors) + { + List centroids = new List(); + Color[] pixels = image.GetPixels(); + HashSet uniqueColors = new HashSet(); + + foreach (Color pixel in pixels) + { + Vector3 color = new Vector3(pixel.r, pixel.g, pixel.b); + if (!uniqueColors.Contains(color)) + { + uniqueColors.Add(color); + centroids.Add(color); + if (centroids.Count >= maxColors) break; + } + } + + return centroids; + } + + private static Vector3 RandomizeCentroid(Texture2D image) + { + Color randomPixel = image.GetPixel(Random.Range(0, image.width), Random.Range(0, image.height)); + return new Vector3(randomPixel.r, randomPixel.g, randomPixel.b); + } + + private static int GetClosestCentroid(Vector3 color, List centroids) + { + int closestCentroid = 0; + float minDistanceSq = float.MaxValue; + + for (int i = 0; i < centroids.Count; i++) + { + float distanceSq = (color - centroids[i]).sqrMagnitude; + if (distanceSq < minDistanceSq) + { + minDistanceSq = distanceSq; + closestCentroid = i; + } + } + + return closestCentroid; + } +} diff --git a/Runtime/ImageQuantizer.cs.meta b/Runtime/ImageQuantizer.cs.meta new file mode 100644 index 0000000..df80c60 --- /dev/null +++ b/Runtime/ImageQuantizer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1291c85b333132b8392486949420d31a \ No newline at end of file diff --git a/Runtime/PSXMesh.cs b/Runtime/PSXMesh.cs index bd1c0fa..14e52e3 100644 --- a/Runtime/PSXMesh.cs +++ b/Runtime/PSXMesh.cs @@ -1,12 +1,15 @@ using UnityEngine; -namespace PSXSplash.RuntimeCode { +namespace PSXSplash.RuntimeCode +{ [System.Serializable] - public class PSXMesh { + public class PSXMesh + { public bool TriangulateMesh = true; - public void Export() { + public void Export(GameObject gameObject) + { Debug.Log($"Export: {this}"); } } diff --git a/Runtime/PSXSceneExporter.cs b/Runtime/PSXSceneExporter.cs index a2f8a46..f77e9cf 100644 --- a/Runtime/PSXSceneExporter.cs +++ b/Runtime/PSXSceneExporter.cs @@ -3,7 +3,7 @@ using UnityEngine.SceneManagement; namespace PSXSplash.RuntimeCode { - public class PSXSceneExporter : MonoBehaviour + public class PSXSceneExporter : MonoBehaviour { public void Export() { diff --git a/Runtime/PSXTexture.cs b/Runtime/PSXTexture.cs index ea80062..1ceea4a 100644 --- a/Runtime/PSXTexture.cs +++ b/Runtime/PSXTexture.cs @@ -1,22 +1,51 @@ using UnityEngine; -namespace PSXSplash.RuntimeCode { - public enum PSXTextureType { - TEX_4BPP, +namespace PSXSplash.RuntimeCode +{ + public enum PSXTextureType + { + TEX_4BPP = 4, - TEX_8BPP, + TEX_8BPP = 8, - TEX16_BPP + TEX16_BPP = 16 } + + [System.Serializable] public class PSXTexture { - public PSXTextureType TextureType; + public PSXTextureType TextureType = PSXTextureType.TEX_8BPP; public bool Dithering = true; - public void Export() { + [Range(1, 256)] + public int Width = 128; + + [Range(1, 256)] + public int Height = 128; + + public void Export(GameObject gameObject) + { Debug.Log($"Export: {this}"); + + MeshRenderer meshRenderer = gameObject.GetComponent(); + if (meshRenderer != null) + { + Texture texture = meshRenderer.material.mainTexture; + if (texture is Texture2D) + { + Texture2D originalTexture = (Texture2D)texture; + + Texture2D newTexture = new Texture2D(originalTexture.width, originalTexture.height, originalTexture.format, false); + newTexture.SetPixels(originalTexture.GetPixels()); + newTexture.Apply(); + Debug.Log((int)TextureType); + newTexture.Reinitialize(Width, Height, UnityEngine.Experimental.Rendering.GraphicsFormat.R8G8B8_UInt, false); + var (quantizedPixels, clut) = ImageQuantizer.Quantize(originalTexture, (int)TextureType); + + } + } } } }