From 956a55ecebdba6006605c566146935ec7c23c340 Mon Sep 17 00:00:00 2001 From: jracek Date: Tue, 14 Jan 2025 19:24:23 +0100 Subject: [PATCH] Added texture export options --- Editor/QuantizedPreviewWindow.cs | 134 ++++++++++++++++++++++++++----- Runtime/ImageQuantizer.cs | 85 +++++++++++--------- 2 files changed, 158 insertions(+), 61 deletions(-) diff --git a/Editor/QuantizedPreviewWindow.cs b/Editor/QuantizedPreviewWindow.cs index 44e974f..283d890 100644 --- a/Editor/QuantizedPreviewWindow.cs +++ b/Editor/QuantizedPreviewWindow.cs @@ -1,3 +1,4 @@ +using System.IO; using UnityEditor; using UnityEngine; using UnityEngine.Rendering; @@ -7,7 +8,7 @@ public class QuantizedPreviewWindow : EditorWindow private Texture2D originalTexture; private Texture2D quantizedTexture; private Texture2D vramTexture; // New VRAM Texture - private float[] clut; // Changed to 1D array + private ushort[] clut; // Changed to 1D array private ushort[] indexedPixelData; // New field for indexed pixel data private int bpp = 4; private int targetWidth = 128; @@ -45,7 +46,7 @@ public class QuantizedPreviewWindow : EditorWindow { GUILayout.BeginVertical(); GUILayout.Label("Original Texture"); - DrawTexturePreview(originalTexture, previewSize); + DrawTexturePreview(originalTexture, previewSize, false); GUILayout.EndVertical(); } @@ -72,26 +73,78 @@ public class QuantizedPreviewWindow : EditorWindow GUILayout.Label("Color Lookup Table (CLUT)"); DrawCLUT(); } + + GUILayout.Space(10); + + if (indexedPixelData != null) + { + if (GUILayout.Button("Export pixel data")) + { + string path = EditorUtility.SaveFilePanel( + "Save pixel data", + "", + "pixel_data", + "bin" + ); + + if (!string.IsNullOrEmpty(path)) + { + using (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write)) + using (BinaryWriter writer = new BinaryWriter(fileStream)) + { + foreach (ushort value in indexedPixelData) + { + writer.Write(value); + } + } + } + } + } + + if (clut != null) + { + if (GUILayout.Button("Export clut data")) + { + string path = EditorUtility.SaveFilePanel( + "Save clut data", + "", + "clut_data", + "bin" + ); + + if (!string.IsNullOrEmpty(path)) + { + using (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write)) + using (BinaryWriter writer = new BinaryWriter(fileStream)) + { + foreach (ushort value in clut) + { + writer.Write(value); + } + } + } + } + } } private void GenerateQuantizedPreview() { Texture2D resizedTexture = ResizeTexture(originalTexture, targetWidth, targetHeight); - if (bpp == 16) + if (bpp == 16) { quantizedTexture = ConvertTo16Bpp(resizedTexture); - clut = null; + clut = null; vramTexture = resizedTexture; } else { var (indexedPixels, generatedClut) = ImageQuantizer.Quantize(resizedTexture, bpp, maxKMeans); - indexedPixelData = indexedPixels; + indexedPixelData = indexedPixels; clut = generatedClut; - int pixelSize = bpp == 4 ? 4 : bpp == 8 ? 2 : 1; + int pixelSize = bpp == 4 ? 4 : bpp == 8 ? 2 : 1; quantizedTexture = new Texture2D(resizedTexture.width, resizedTexture.height); Color[] quantizedColors = new Color[resizedTexture.width * resizedTexture.height]; @@ -110,18 +163,23 @@ public class QuantizedPreviewWindow : EditorWindow else if (pixelSize == 2) { int packedValue = indexedPixelData[pixelIndex]; - index = (packedValue >> ((x % 2) * 8)) & 0xFF; + 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]); + + Vector3 color = new Vector3( + (clut[index] & 31) / 31.0f, // Red: bits 0–4 + ((clut[index] >> 5) & 31) / 31.0f, // Green: bits 5–9 + ((clut[index] >> 10) & 31) / 31.0f // Blue: bits 10–14 + ); quantizedColors[y * resizedTexture.width + x] = new Color(color.x, color.y, color.z); + if ((x % pixelSize) == (pixelSize - 1)) { pixelIndex++; @@ -145,11 +203,11 @@ public class QuantizedPreviewWindow : EditorWindow if (bpp == 4) { - adjustedWidth = Mathf.CeilToInt(width / 4f); + adjustedWidth = Mathf.CeilToInt(width / 4f); } else if (bpp == 8) { - adjustedWidth = Mathf.CeilToInt(width / 2f); + adjustedWidth = Mathf.CeilToInt(width / 2f); } Texture2D vramTexture = new Texture2D(adjustedWidth, height); @@ -160,9 +218,9 @@ public class QuantizedPreviewWindow : EditorWindow { 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; + float r = (index & 31) / 31.0f; // Red: bits 0–4 + float g = ((index >> 5) & 31) / 31.0f; // Green: bits 5–9 + float b = ((index >> 10) & 31) / 31.0f; // Blue: bits 10–14 vramColors[i] = new Color(r, g, b); } @@ -174,6 +232,7 @@ public class QuantizedPreviewWindow : EditorWindow } + private Texture2D ConvertTo16Bpp(Texture2D source) { int width = source.width; @@ -187,12 +246,11 @@ public class QuantizedPreviewWindow : EditorWindow { Color pixel = originalPixels[i]; - // Convert to 5 bits per channel (R5G5B5) float r = Mathf.Floor(pixel.r * 31) / 31.0f; // 5 bits for red 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); + convertedPixels[i] = new Color(r, g, b, pixel.a); } convertedTexture.SetPixels(convertedPixels); @@ -201,10 +259,37 @@ public class QuantizedPreviewWindow : EditorWindow return convertedTexture; } - private void DrawTexturePreview(Texture2D texture, int size) + private void DrawTexturePreview(Texture2D texture, int size, bool flipY = true) { Rect rect = GUILayoutUtility.GetRect(size, size, GUILayout.ExpandWidth(false)); - EditorGUI.DrawPreviewTexture(rect, texture, null, ScaleMode.ScaleToFit, 0, 0, ColorWriteMask.All); + + // Flip the texture on the Y-axis + Texture2D displayedTexture = flipY ? FlipTextureY(texture) : texture; + EditorGUI.DrawPreviewTexture(rect, displayedTexture, null, ScaleMode.ScaleToFit, 0, 0, ColorWriteMask.All); + } + + private Texture2D FlipTextureY(Texture2D texture) + { + Color[] originalPixels = texture.GetPixels(); + Color[] flippedPixels = new Color[originalPixels.Length]; + + int width = texture.width; + int height = texture.height; + + // Flip the pixels on the Y-axis + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + flippedPixels[(height - y - 1) * width + x] = originalPixels[y * width + x]; + } + } + + Texture2D flippedTexture = new Texture2D(width, height); + flippedTexture.SetPixels(flippedPixels); + flippedTexture.Apply(); + + return flippedTexture; } private void DrawCLUT() @@ -216,7 +301,7 @@ public class QuantizedPreviewWindow : EditorWindow GUILayout.Space(10); - int totalColors = clut.Length / 3; + int totalColors = clut.Length; int totalRows = Mathf.CeilToInt((float)totalColors / maxColorsPerRow); for (int row = 0; row < totalRows; row++) @@ -228,7 +313,13 @@ public class QuantizedPreviewWindow : EditorWindow for (int col = 0; col < colorsInRow; col++) { int index = row * maxColorsPerRow + col; - Vector3 color = new Vector3(clut[index * 3], clut[index * 3 + 1], clut[index * 3 + 2]); + + Vector3 color = new Vector3( + (clut[index] & 31) / 31.0f, // Red: bits 0–4 + ((clut[index] >> 5) & 31) / 31.0f, // Green: bits 5–9 + ((clut[index] >> 10) & 31) / 31.0f // Blue: bits 10–14 + ); + Rect rect = GUILayoutUtility.GetRect(swatchSize, swatchSize, GUILayout.ExpandWidth(false)); EditorGUI.DrawRect(rect, new Color(color.x, color.y, color.z)); } @@ -237,6 +328,7 @@ 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 a84b8e1..0abd26c 100644 --- a/Runtime/ImageQuantizer.cs +++ b/Runtime/ImageQuantizer.cs @@ -6,7 +6,7 @@ using Random = UnityEngine.Random; public class ImageQuantizer { - public static (ushort[], float[]) Quantize(Texture2D image, int bpp, int maxIterations = 10) + public static (ushort[], ushort[]) Quantize(Texture2D image, int bpp, int maxIterations = 10) { int width = image.width; int height = image.height; @@ -66,64 +66,69 @@ public class ImageQuantizer centroids = new List(newCentroids); } - int pixelSize = bpp == 4 ? 4 : bpp == 8 ? 2 : 1; - int adjustedWidth = width / pixelSize; - ushort[] pixelArray = new ushort[adjustedWidth * height]; + 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; + ushort packIndex = 0; + int bitShift = 0; - for (int i = 0; i < pixelColors.Length; i++) + // Loop through pixels and pack the data, flipping along the Y-axis + for (int y = height - 1; y >= 0; y--) { - ushort centroidIndex = assignments[i]; - - // For 4bpp, we need to pack 4 indices into a single integer - if (bpp == 4) + for (int x = 0; x < width; x++) { - pixelArray[packIndex] |= (ushort)(centroidIndex << (bitShift * 4)); // Shift by 4 bits for each index - bitShift++; + int pixelIndex = y * width + x; + ushort centroidIndex = assignments[pixelIndex]; - // Every 4 indices, move to the next position in the pixelArray - if (bitShift == 4) + // For 4bpp, we need to pack 4 indices into a single integer + if (bpp == 4) { - bitShift = 0; + pixelArray[packIndex] |= (ushort)(centroidIndex << (bitShift * 4)); + bitShift++; + + 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)); + bitShift++; + + if (bitShift == 2) + { + bitShift = 0; + packIndex++; + } + } + // For 15bpp, just place each index directly (no packing) + else + { + pixelArray[packIndex] = centroidIndex; 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; - float[] clut = new float[actualColors * 3]; + ushort[] clut = new ushort[actualColors]; for (int i = 0; i < actualColors; 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 + int red = Mathf.Clamp(Mathf.RoundToInt(centroids[i].x * 31), 0, 31); // 5 bits + int green = Mathf.Clamp(Mathf.RoundToInt(centroids[i].y * 31), 0, 31); // 5 bits + int blue = Mathf.Clamp(Mathf.RoundToInt(centroids[i].z * 31), 0, 31); // 5 bits + + clut[i] = (ushort)((blue << 10) | (green << 5) | red); } return (pixelArray, clut); } + private static List InitializeCentroids(Texture2D image, int maxColors) { List centroids = new List();