diff --git a/Editor/QuantizedPreviewWindow.cs b/Editor/QuantizedPreviewWindow.cs index 69731eb..44e974f 100644 --- a/Editor/QuantizedPreviewWindow.cs +++ b/Editor/QuantizedPreviewWindow.cs @@ -1,22 +1,20 @@ using UnityEditor; using UnityEngine; -using PSXSplash.RuntimeCode; using UnityEngine.Rendering; -using System.Threading.Tasks; -using UnityEditor.PackageManager.UI; public class QuantizedPreviewWindow : EditorWindow { private Texture2D originalTexture; - private Texture2D resizedTexture; private Texture2D quantizedTexture; - private Vector3[,] clut; + private Texture2D vramTexture; // New VRAM Texture + private float[] clut; // Changed to 1D array + private ushort[] indexedPixelData; // New field for indexed pixel data private int bpp = 4; private int targetWidth = 128; private int targetHeight = 128; + private int maxKMeans = 100; private int previewSize = 256; - [MenuItem("Window/Quantized Preview")] public static void ShowWindow() { @@ -33,7 +31,8 @@ public class QuantizedPreviewWindow : EditorWindow targetWidth = EditorGUILayout.IntField("Target Width", targetWidth); targetHeight = EditorGUILayout.IntField("Target Height", targetHeight); - bpp = EditorGUILayout.IntPopup("Bits Per Pixel", bpp, new[] { "4 bpp", "8 bpp", "15 bpp" }, new[] { 4, 8, 15 }); + bpp = EditorGUILayout.IntPopup("Bits Per Pixel", bpp, new[] { "4 bpp", "8 bpp", "16 bpp" }, new[] { 4, 8, 16 }); + maxKMeans = EditorGUILayout.IntField("Max K-Means", maxKMeans); if (GUILayout.Button("Generate Quantized Preview") && originalTexture != null) { @@ -50,11 +49,11 @@ public class QuantizedPreviewWindow : EditorWindow GUILayout.EndVertical(); } - if (resizedTexture != null) + if (vramTexture != null) { GUILayout.BeginVertical(); - GUILayout.Label("Resized Texture"); - DrawTexturePreview(resizedTexture, previewSize); + GUILayout.Label("VRAM View (Indexed Data as 16bpp)"); + DrawTexturePreview(vramTexture, previewSize); GUILayout.EndVertical(); } @@ -65,7 +64,9 @@ public class QuantizedPreviewWindow : EditorWindow DrawTexturePreview(quantizedTexture, previewSize); GUILayout.EndVertical(); } + GUILayout.EndHorizontal(); + if (clut != null) { GUILayout.Label("Color Lookup Table (CLUT)"); @@ -75,42 +76,105 @@ public class QuantizedPreviewWindow : EditorWindow private void GenerateQuantizedPreview() { - resizedTexture = ResizeTexture(originalTexture, targetWidth, targetHeight); + Texture2D resizedTexture = ResizeTexture(originalTexture, targetWidth, targetHeight); - if (bpp == 15) // Handle 15bpp (R5G5B5) without CLUT + if (bpp == 16) { - quantizedTexture = ConvertTo15Bpp(resizedTexture); - clut = null; // No CLUT for 15bpp + quantizedTexture = ConvertTo16Bpp(resizedTexture); + clut = null; + vramTexture = resizedTexture; } else { - int maxColors = (int)Mathf.Pow(2, bpp); + var (indexedPixels, generatedClut) = ImageQuantizer.Quantize(resizedTexture, bpp, maxKMeans); - var (quantizedPixels, generatedClut) = ImageQuantizer.Quantize(resizedTexture, maxColors); + indexedPixelData = indexedPixels; + clut = generatedClut; + int pixelSize = bpp == 4 ? 4 : bpp == 8 ? 2 : 1; quantizedTexture = new Texture2D(resizedTexture.width, resizedTexture.height); Color[] quantizedColors = new Color[resizedTexture.width * resizedTexture.height]; + int pixelIndex = 0; 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] - ); + int index = 0; + + if (pixelSize == 4) + { + int packedValue = indexedPixelData[pixelIndex]; + index = (packedValue >> ((x % 4) * 4)) & 0xF; + } + else if (pixelSize == 2) + { + int packedValue = indexedPixelData[pixelIndex]; + index = (packedValue >> ((x % 2) * 8)) & 0xFF; + } + else + { + + index = indexedPixelData[pixelIndex]; + } + + + Vector3 color = new Vector3(clut[index * 3], clut[index * 3 + 1], clut[index * 3 + 2]); + quantizedColors[y * resizedTexture.width + x] = new Color(color.x, color.y, color.z); + + if ((x % pixelSize) == (pixelSize - 1)) + { + pixelIndex++; + } } } quantizedTexture.SetPixels(quantizedColors); quantizedTexture.Apply(); - clut = generatedClut; + vramTexture = CreateVramTexture(resizedTexture.width, resizedTexture.height, indexedPixelData); } } - private Texture2D ConvertTo15Bpp(Texture2D source) + + + + private Texture2D CreateVramTexture(int width, int height, ushort[] indexedData) + { + int adjustedWidth = width; + + if (bpp == 4) + { + adjustedWidth = Mathf.CeilToInt(width / 4f); + } + else if (bpp == 8) + { + adjustedWidth = Mathf.CeilToInt(width / 2f); + } + + Texture2D vramTexture = new Texture2D(adjustedWidth, height); + + Color[] vramColors = new Color[adjustedWidth * height]; + + for (int i = 0; i < indexedData.Length; i++) + { + int index = indexedData[i]; + + float r = Mathf.Floor((index >> 11) & 31) / 31.0f; + float g = Mathf.Floor((index >> 5) & 31) / 31.0f; + float b = Mathf.Floor(index & 31) / 31.0f; + + vramColors[i] = new Color(r, g, b); + } + + vramTexture.SetPixels(vramColors); + vramTexture.Apply(); + + return vramTexture; + } + + + private Texture2D ConvertTo16Bpp(Texture2D source) { int width = source.width; int height = source.height; @@ -128,7 +192,7 @@ public class QuantizedPreviewWindow : EditorWindow float g = Mathf.Floor(pixel.g * 31) / 31.0f; // 5 bits for green float b = Mathf.Floor(pixel.b * 31) / 31.0f; // 5 bits for blue - convertedPixels[i] = new Color(r, g, b, pixel.a); // Maintain alpha channel + convertedPixels[i] = new Color(r, g, b, pixel.a); } convertedTexture.SetPixels(convertedPixels); @@ -152,7 +216,7 @@ public class QuantizedPreviewWindow : EditorWindow GUILayout.Space(10); - int totalColors = clut.GetLength(0); + int totalColors = clut.Length / 3; int totalRows = Mathf.CeilToInt((float)totalColors / maxColorsPerRow); for (int row = 0; row < totalRows; row++) @@ -164,7 +228,7 @@ public class QuantizedPreviewWindow : EditorWindow for (int col = 0; col < colorsInRow; col++) { int index = row * maxColorsPerRow + col; - Vector3 color = clut[index, 0]; + Vector3 color = new Vector3(clut[index * 3], clut[index * 3 + 1], clut[index * 3 + 2]); Rect rect = GUILayoutUtility.GetRect(swatchSize, swatchSize, GUILayout.ExpandWidth(false)); EditorGUI.DrawRect(rect, new Color(color.x, color.y, color.z)); } @@ -173,7 +237,6 @@ public class QuantizedPreviewWindow : EditorWindow } } - private Texture2D ResizeTexture(Texture2D source, int newWidth, int newHeight) { RenderTexture rt = RenderTexture.GetTemporary(newWidth, newHeight); diff --git a/Runtime/ImageQuantizer.cs b/Runtime/ImageQuantizer.cs index 5971900..a84b8e1 100644 --- a/Runtime/ImageQuantizer.cs +++ b/Runtime/ImageQuantizer.cs @@ -1,20 +1,18 @@ +using System; using System.Collections.Generic; using UnityEngine; +using Random = UnityEngine.Random; 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) + + public static (ushort[], float[]) Quantize(Texture2D image, int bpp, int maxIterations = 10) { int width = image.width; int height = image.height; + int maxColors = (int)Math.Pow(bpp, 2); + List centroids = InitializeCentroids(image, maxColors); Color[] pixels = image.GetPixels(); @@ -24,17 +22,16 @@ public class ImageQuantizer 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]; + ushort[] assignments = new ushort[pixelColors.Length]; + // Perform k-means clustering 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); + ushort closestCentroid = (ushort)GetClosestCentroid(pixelColors[i], centroids); if (assignments[i] != closestCentroid) { assignments[i] = closestCentroid; @@ -42,7 +39,6 @@ public class ImageQuantizer } } - // Step 2: Recalculate centroids Vector3[] newCentroids = new Vector3[centroids.Count]; int[] centroidCounts = new int[centroids.Count]; @@ -70,23 +66,59 @@ public class ImageQuantizer centroids = new List(newCentroids); } - float[,,] pixelArray = new float[width, height, 3]; + int pixelSize = bpp == 4 ? 4 : bpp == 8 ? 2 : 1; + int adjustedWidth = width / pixelSize; + ushort[] pixelArray = new ushort[adjustedWidth * height]; + + ushort packIndex = 0; + int bitShift = 0; + for (int i = 0; i < pixelColors.Length; i++) { - int x = i % width; - int y = i / width; + ushort centroidIndex = assignments[i]; - 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 + // For 4bpp, we need to pack 4 indices into a single integer + if (bpp == 4) + { + pixelArray[packIndex] |= (ushort)(centroidIndex << (bitShift * 4)); // Shift by 4 bits for each index + bitShift++; + + // Every 4 indices, move to the next position in the pixelArray + if (bitShift == 4) + { + bitShift = 0; + packIndex++; + } + } + // For 8bpp, we need to pack 2 indices into a single integer + else if (bpp == 8) + { + pixelArray[packIndex] |= (ushort)(centroidIndex << (bitShift * 8)); // Shift by 8 bits for each index + bitShift++; + + // Every 2 indices, move to the next position in the pixelArray + if (bitShift == 2) + { + bitShift = 0; + packIndex++; + } + } + // For 15bpp, just place each index directly (no packing) + else + { + pixelArray[packIndex] = centroidIndex; + packIndex++; + } } + // Create the CLUT as a 1D array of RGB values int actualColors = centroids.Count; - Vector3[,] clut = new Vector3[actualColors, 1]; + float[] clut = new float[actualColors * 3]; for (int i = 0; i < actualColors; i++) { - clut[i, 0] = centroids[i]; + clut[i * 3 + 0] = centroids[i].x; // Red + clut[i * 3 + 1] = centroids[i].y; // Green + clut[i * 3 + 2] = centroids[i].z; // Blue } return (pixelArray, clut);