From 9ddafca9294b0e231fea390f23c2e6fc82c405db Mon Sep 17 00:00:00 2001 From: jracek Date: Sat, 25 Jan 2025 20:28:51 +0100 Subject: [PATCH] Rewrote quantizer --- Editor/PSXObjectExporterEditor.cs | 4 +- Editor/QuantizedPreviewWindow.cs | 188 ++--------------- Runtime/ImageProcessing.cs | 163 +++++++++++++++ ...ntizer.cs.meta => ImageProcessing.cs.meta} | 0 Runtime/ImageQuantizer.cs | 175 ---------------- Runtime/PSXObjectExporter.cs | 2 +- Runtime/PSXTexture.cs | 196 ------------------ Runtime/PSXTexture.cs.meta | 2 - Runtime/PSXTexture2D.cs | 173 ++++++++++++++++ Runtime/PSXTexture2D.cs.meta | 2 + 10 files changed, 357 insertions(+), 548 deletions(-) create mode 100644 Runtime/ImageProcessing.cs rename Runtime/{ImageQuantizer.cs.meta => ImageProcessing.cs.meta} (100%) delete mode 100644 Runtime/ImageQuantizer.cs delete mode 100644 Runtime/PSXTexture.cs delete mode 100644 Runtime/PSXTexture.cs.meta create mode 100644 Runtime/PSXTexture2D.cs create mode 100644 Runtime/PSXTexture2D.cs.meta diff --git a/Editor/PSXObjectExporterEditor.cs b/Editor/PSXObjectExporterEditor.cs index d541c5b..ccacea9 100644 --- a/Editor/PSXObjectExporterEditor.cs +++ b/Editor/PSXObjectExporterEditor.cs @@ -11,6 +11,7 @@ namespace PSXSplash.EditorCode { public override void OnInspectorGUI() { + /* PSXObjectExporter comp = (PSXObjectExporter)target; serializedObject.Update(); @@ -79,7 +80,8 @@ namespace PSXSplash.EditorCode EditorGUILayout.EndVertical(); serializedObject.ApplyModifiedProperties(); - + */ } + } } \ No newline at end of file diff --git a/Editor/QuantizedPreviewWindow.cs b/Editor/QuantizedPreviewWindow.cs index fc7589b..b9db5b2 100644 --- a/Editor/QuantizedPreviewWindow.cs +++ b/Editor/QuantizedPreviewWindow.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.IO; using PSXSplash.RuntimeCode; using UnityEditor; @@ -9,13 +10,9 @@ public class QuantizedPreviewWindow : EditorWindow private Texture2D originalTexture; private Texture2D quantizedTexture; private Texture2D vramTexture; // New VRAM Texture - private ushort[] clut; // Changed to 1D array + private List 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; - public bool dithering = true; - private int maxKMeans = 100; + private PSXBPP bpp; private readonly int previewSize = 256; [MenuItem("Window/Quantized Preview")] @@ -31,13 +28,9 @@ public class QuantizedPreviewWindow : EditorWindow originalTexture = (Texture2D)EditorGUILayout.ObjectField("Original Texture", originalTexture, typeof(Texture2D), false); - targetWidth = EditorGUILayout.IntField("Target Width", targetWidth); - targetHeight = EditorGUILayout.IntField("Target Height", targetHeight); - dithering = EditorGUILayout.Toggle("Dithering", dithering); + bpp = (PSXBPP)EditorGUILayout.EnumPopup("Bit Depth", bpp); - 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) { @@ -121,9 +114,9 @@ public class QuantizedPreviewWindow : EditorWindow using (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write)) using (BinaryWriter writer = new BinaryWriter(fileStream)) { - foreach (ushort value in clut) + foreach (VRAMPixel value in clut) { - writer.Write(value); + writer.Write(value.Pack()); } } } @@ -133,145 +126,25 @@ public class QuantizedPreviewWindow : EditorWindow private void GenerateQuantizedPreview() { - Texture2D resizedTexture = PSXTexture.ResizeTexture(originalTexture, targetWidth, targetHeight); - if(dithering) { - resizedTexture = PSXTexture.DitherTexture(resizedTexture); - } + PSXTexture2D psxTex = PSXTexture2D.CreateFromTexture2D(originalTexture, bpp, false); - if (bpp == 16) - { - quantizedTexture = null; - indexedPixelData = PSXTexture.ConvertTo16Bpp(resizedTexture); - clut = null; - vramTexture = ConvertTo16BppTexture2D(resizedTexture); - } - else - { - var (indexedPixels, generatedClut) = ImageQuantizer.Quantize(resizedTexture, bpp, maxKMeans); + quantizedTexture = psxTex.GeneratePreview(); + vramTexture = psxTex.GenerateVramPreview(); + clut = psxTex.ColorPalette; - 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++) - { - int index; - - 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] & 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++; - } - } - } - - quantizedTexture.SetPixels(quantizedColors); - quantizedTexture.Apply(); - - vramTexture = CreateVramTexture(resizedTexture.width, resizedTexture.height, indexedPixelData); - } } - 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 = (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); - } - - vramTexture.SetPixels(vramColors); - vramTexture.Apply(); - - return vramTexture; - } private void DrawTexturePreview(Texture2D texture, int size, bool flipY = true) { Rect rect = GUILayoutUtility.GetRect(size, size, GUILayout.ExpandWidth(false)); - - // Flip the texture on the Y-axis - Texture2D displayedTexture = flipY ? FlipTextureY(texture) : texture; - EditorGUI.DrawPreviewTexture(rect, displayedTexture, null, ScaleMode.ScaleToFit, 0, 0, ColorWriteMask.All); + EditorGUI.DrawPreviewTexture(rect, texture, 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() { @@ -282,7 +155,7 @@ public class QuantizedPreviewWindow : EditorWindow GUILayout.Space(10); - int totalColors = clut.Length; + int totalColors = clut.Count; int totalRows = Mathf.CeilToInt((float)totalColors / maxColorsPerRow); for (int row = 0; row < totalRows; row++) @@ -296,9 +169,9 @@ public class QuantizedPreviewWindow : EditorWindow int index = row * maxColorsPerRow + col; 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 + clut[index].R / 31.0f, // Red: bits 0–4 + clut[index].G / 31.0f, // Green: bits 5–9 + clut[index].B / 31.0f // Blue: bits 10–14 ); Rect rect = GUILayoutUtility.GetRect(swatchSize, swatchSize, GUILayout.ExpandWidth(false)); @@ -309,35 +182,4 @@ public class QuantizedPreviewWindow : EditorWindow } } - private Texture2D ConvertTo16BppTexture2D(Texture2D source) -{ - int width = source.width; - int height = source.height; - Texture2D convertedTexture = new Texture2D(width, height); - - Color[] originalPixels = source.GetPixels(); - Color[] convertedPixels = new Color[originalPixels.Length]; - - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - int flippedY = height - y - 1; - - Color pixel = originalPixels[flippedY * width + x]; - - 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[y * width + x] = new Color(r, g, b, pixel.a); - } - } - - convertedTexture.SetPixels(convertedPixels); - convertedTexture.Apply(); - - return convertedTexture; -} - } diff --git a/Runtime/ImageProcessing.cs b/Runtime/ImageProcessing.cs new file mode 100644 index 0000000..3880c20 --- /dev/null +++ b/Runtime/ImageProcessing.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using UnityEngine; + + + +namespace PSXSplash.RuntimeCode +{ + public class ImageQuantizer + { + private int _maxColors; + private Color[] _pixels; + private Color[] _centroids; + private int[] _assignments; + private List _uniqueColors; + + public Color[] Palette + { + get => _centroids; + } + + public int[] Pixels + { + get => _assignments; + } + + + public void Quantize(Texture2D texture2D, int maxColors) + { + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); + _pixels = texture2D.GetPixels(); + _maxColors = maxColors; + _centroids = new Color[_maxColors]; + _uniqueColors = new List(); + + + FillRandomCentroids(); + + bool hasChanged; + _assignments = new int[_pixels.Count()]; + + do + { + hasChanged = false; + Parallel.For(0, _pixels.Count(), i => + { + int newAssignment = GetNearestCentroid(_pixels[i]); + + if (_assignments[i] != newAssignment) + { + lock (_assignments) + { + _assignments[i] = newAssignment; + } + + lock (this) + { + hasChanged = true; + } + } + }); + + RecalculateCentroids(); + } while (hasChanged); + + stopwatch.Stop(); + + UnityEngine.Debug.Log($"Quantization completed in {stopwatch.ElapsedMilliseconds} ms"); + + } + + private void FillRandomCentroids() + { + foreach (Color pixel in _pixels) + { + if (!_uniqueColors.Contains(pixel)) + { + _uniqueColors.Add(pixel); + } + } + + for (int i = 0; i < _maxColors; i++) + { + _centroids[i] = _uniqueColors[UnityEngine.Random.Range(0, _uniqueColors.Count - 1)]; + } + + } + + private double CalculateColorDistance(Color color1, Color color2) + { + float rDiff = color1.r - color2.r; + float gDiff = color1.g - color2.g; + float bDiff = color1.b - color2.b; + + return Math.Sqrt(rDiff * rDiff + gDiff * gDiff + bDiff * bDiff); + } + + private int GetNearestCentroid(Color color) + { + double minDistance = double.MaxValue; + int closestCentroidIndex = 0; + + for (int i = 0; i < _maxColors; i++) + { + double distance = CalculateColorDistance(_centroids[i], color); + if (distance < minDistance) + { + minDistance = distance; + closestCentroidIndex = i; + } + } + + return closestCentroidIndex; + } + + private void RecalculateCentroids() + { + Color[] newCentroids = new Color[_maxColors]; + + + Parallel.For(0, _maxColors, i => + { + List clusterColors = new List(); + for (int j = 0; j < _pixels.Length; j++) + { + if (_assignments[j] == i) + { + clusterColors.Add(_pixels[j]); + } + } + + Color newCentroid; + + try + { + newCentroid = AverageColor(clusterColors); + } + catch (InvalidOperationException) + { + System.Random random = new System.Random(); + newCentroid = _uniqueColors[random.Next(0, _uniqueColors.Count - 1)]; + } + + newCentroids[i] = newCentroid; + }); + + _centroids = newCentroids; + } + + private Color AverageColor(List colors) + { + float r = colors.Average(c => c.r); + float g = colors.Average(c => c.g); + float b = colors.Average(c => c.b); + float a = colors.Average(c => c.a); + return new Color(r, g, b, a); + } + } +} diff --git a/Runtime/ImageQuantizer.cs.meta b/Runtime/ImageProcessing.cs.meta similarity index 100% rename from Runtime/ImageQuantizer.cs.meta rename to Runtime/ImageProcessing.cs.meta diff --git a/Runtime/ImageQuantizer.cs b/Runtime/ImageQuantizer.cs deleted file mode 100644 index 0abd26c..0000000 --- a/Runtime/ImageQuantizer.cs +++ /dev/null @@ -1,175 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEngine; -using Random = UnityEngine.Random; - -public class ImageQuantizer -{ - - public static (ushort[], ushort[]) 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(); - 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); - } - - ushort[] assignments = new ushort[pixelColors.Length]; - - // Perform k-means clustering - for (int iteration = 0; iteration < maxIterations; iteration++) - { - bool centroidsChanged = false; - - for (int i = 0; i < pixelColors.Length; i++) - { - ushort closestCentroid = (ushort)GetClosestCentroid(pixelColors[i], centroids); - if (assignments[i] != closestCentroid) - { - assignments[i] = closestCentroid; - centroidsChanged = true; - } - } - - 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); - } - - 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; - - // Loop through pixels and pack the data, flipping along the Y-axis - for (int y = height - 1; y >= 0; y--) - { - for (int x = 0; x < width; x++) - { - int pixelIndex = y * width + x; - ushort centroidIndex = assignments[pixelIndex]; - - // For 4bpp, we need to pack 4 indices into a single integer - if (bpp == 4) - { - 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++; - } - } - } - - int actualColors = centroids.Count; - ushort[] clut = new ushort[actualColors]; - for (int i = 0; i < actualColors; i++) - { - 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(); - 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/PSXObjectExporter.cs b/Runtime/PSXObjectExporter.cs index 3bc681d..181515b 100644 --- a/Runtime/PSXObjectExporter.cs +++ b/Runtime/PSXObjectExporter.cs @@ -6,7 +6,7 @@ namespace PSXSplash.RuntimeCode { public PSXMesh Mesh; - public PSXTexture Texture; + //ublic PSXTexture Texture; public void Export() { diff --git a/Runtime/PSXTexture.cs b/Runtime/PSXTexture.cs deleted file mode 100644 index cd99f14..0000000 --- a/Runtime/PSXTexture.cs +++ /dev/null @@ -1,196 +0,0 @@ -using System.IO; -using UnityEditor; -using UnityEngine; - -namespace PSXSplash.RuntimeCode -{ - public enum PSXTextureType - { - TEX_4BPP = 4, - - TEX_8BPP = 8, - - TEX16_BPP = 16 - } - - - - [System.Serializable] - public class PSXTexture - { - public PSXTextureType TextureType = PSXTextureType.TEX_8BPP; - public bool Dithering = true; - - [Range(1, 256)] - public int Width = 128; - - [Range(1, 256)] - public int Height = 128; - - public int MaxKMeans = 50; - - - - public ushort[] ExportTexture(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 = ResizeTexture(originalTexture, Width, Height); - if (Dithering) - { - newTexture = DitherTexture(newTexture); - } - if (TextureType == PSXTextureType.TEX16_BPP) - { - ushort[] converted = ConvertTo16Bpp(newTexture); - return converted; - } - else - { - var (indexedPixels, _) = ImageQuantizer.Quantize(newTexture, (int)TextureType, MaxKMeans); - return indexedPixels; - } - - } - } - return null; - } - - public ushort[] ExportClut(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 = ResizeTexture(originalTexture, Width, Height); - if (TextureType == PSXTextureType.TEX16_BPP) - { - return null; - } - else - { - var (_, generatedClut) = ImageQuantizer.Quantize(newTexture, (int)TextureType, MaxKMeans); - return generatedClut; - } - - } - } - return null; - } - - public static 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; - } - - public static ushort[] ConvertTo16Bpp(Texture2D source) - { - int width = source.width; - int height = source.height; - ushort[] packedData = new ushort[width * height]; - - Color[] originalPixels = source.GetPixels(); - - // Flip the image on the Y-axis - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - int flippedY = height - y - 1; - - int index = flippedY * width + x; - - // Retrieve the pixel color - Color pixel = originalPixels[index]; - - // Convert to 5-bit components - int r = Mathf.Clamp(Mathf.RoundToInt(pixel.r * 31), 0, 31); // 5 bits for red - int g = Mathf.Clamp(Mathf.RoundToInt(pixel.g * 31), 0, 31); // 5 bits for green - int b = Mathf.Clamp(Mathf.RoundToInt(pixel.b * 31), 0, 31); // 5 bits for blue - - // Pack into a ushort: R(0..4), G(5..9), B(10..14), Padding(15) - packedData[y * width + x] = (ushort)((b << 10) | (g << 5) | r); - } - } - - return packedData; - } - - public static Texture2D DitherTexture(Texture2D sourceTexture, float threshold = 0.2f, float errorDiffusionStrength = 0.1f) - { - int width = sourceTexture.width; - int height = sourceTexture.height; - Color[] pixels = sourceTexture.GetPixels(); - Color[] ditheredPixels = new Color[pixels.Length]; - - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - int index = y * width + x; - Color pixel = pixels[index]; - - // Convert the pixel to grayscale - float gray = pixel.grayscale; - - // Apply threshold to determine if it's black or white - int dithered = (gray > threshold) ? 1 : 0; - - // Calculate the error as the difference between the grayscale value and the dithered result - float error = gray - dithered; - - // Store the dithered pixel - ditheredPixels[index] = new Color(dithered, dithered, dithered); - - // Spread the error to neighboring pixels with customizable error diffusion strength - if (x + 1 < width) pixels[(y * width) + (x + 1)] += new Color(error * 7f / 16f * errorDiffusionStrength, error * 7f / 16f * errorDiffusionStrength, error * 7f / 16f * errorDiffusionStrength); - if (y + 1 < height) pixels[((y + 1) * width) + x] += new Color(error * 3f / 16f * errorDiffusionStrength, error * 3f / 16f * errorDiffusionStrength, error * 3f / 16f * errorDiffusionStrength); - if (x - 1 >= 0 && y + 1 < height) pixels[((y + 1) * width) + (x - 1)] += new Color(error * 5f / 16f * errorDiffusionStrength, error * 5f / 16f * errorDiffusionStrength, error * 5f / 16f * errorDiffusionStrength); - if (x + 1 < width && y + 1 < height) pixels[((y + 1) * width) + (x + 1)] += new Color(error * 1f / 16f * errorDiffusionStrength, error * 1f / 16f * errorDiffusionStrength, error * 1f / 16f * errorDiffusionStrength); - } - } - - // Clamp the final pixel values to ensure they are valid colors - for (int i = 0; i < pixels.Length; i++) - { - pixels[i].r = Mathf.Clamp01(pixels[i].r); - pixels[i].g = Mathf.Clamp01(pixels[i].g); - pixels[i].b = Mathf.Clamp01(pixels[i].b); - } - - // Create the resulting dithered texture - Texture2D ditheredTexture = new Texture2D(width, height); - ditheredTexture.SetPixels(pixels); - ditheredTexture.Apply(); - - return ditheredTexture; - } - - } -} diff --git a/Runtime/PSXTexture.cs.meta b/Runtime/PSXTexture.cs.meta deleted file mode 100644 index f5f3c69..0000000 --- a/Runtime/PSXTexture.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 6d64b6bb75da33720b928203b2780952 \ No newline at end of file diff --git a/Runtime/PSXTexture2D.cs b/Runtime/PSXTexture2D.cs new file mode 100644 index 0000000..c340f88 --- /dev/null +++ b/Runtime/PSXTexture2D.cs @@ -0,0 +1,173 @@ +using System.Collections.Generic; +using UnityEngine; + + +namespace PSXSplash.RuntimeCode +{ + + public enum PSXBPP + { + TEX_4BIT = 4, + TEX_8BIT = 8, + TEX_16BIT = 15 + } + + public struct VRAMPixel + { + private ushort r; // 0-4 bits + private ushort g; // 5-9 bits + private ushort b; // 10-14 bits + + public ushort R + { + get => r; + set => r = (ushort)(value & 0b11111); + } + + public ushort G + { + get => g; + set => g = (ushort)(value & 0b11111); + } + + public ushort B + { + get => b; + set => b = (ushort)(value & 0b11111); + } + + public bool SemiTransparent { get; set; } // 15th bit + + + public ushort Pack() + { + return (ushort)((r << 11) | (g << 6) | (b << 1) | (SemiTransparent ? 1 : 0)); + } + + public void Unpack(ushort packedValue) + { + r = (ushort)((packedValue >> 11) & 0b11111); + g = (ushort)((packedValue >> 6) & 0b11111); + b = (ushort)((packedValue >> 1) & 0b11111); + SemiTransparent = (packedValue & 0b1) != 0; + } + } + + + public class PSXTexture2D + { + public int Width { get; set; } + public int Height { get; set; } + public int[] Pixels { get; set; } + public List ColorPalette = new List(); + public PSXBPP BitDepth; + + private int _maxColors; + + public static PSXTexture2D CreateFromTexture2D(Texture2D inputTexture, PSXBPP bitDepth, bool dither) + { + PSXTexture2D psxTex = new PSXTexture2D(); + + psxTex.Width = inputTexture.width; + psxTex.Height = inputTexture.height; + psxTex.BitDepth = bitDepth; + + psxTex._maxColors = (int)Mathf.Pow((int)bitDepth, 2); + + ImageQuantizer quantizer = new ImageQuantizer(); + quantizer.Quantize(inputTexture, psxTex._maxColors); + + foreach (Color pixel in quantizer.Palette) + { + VRAMPixel vramPixel = new VRAMPixel { R = (ushort)(pixel.r * 31), G = (ushort)(pixel.g * 31), B = (ushort)(pixel.b * 31) }; + psxTex.ColorPalette.Add(vramPixel); + } + psxTex.Pixels = quantizer.Pixels; + + return psxTex; + } + + public Texture2D GeneratePreview() + { + Texture2D tex = new Texture2D(Width, Height); + + List colors = new List(); + for (int y = 0; y < Height; y++) + { + for (int x = 0; x < Width; x++) + { + int pixel = Pixels[y * Width + x]; + VRAMPixel color = ColorPalette[pixel]; + + float r = color.R / 31f; + float g = color.G / 31f; + float b = color.B / 31f; + + colors.Add(new Color(r, g, b)); + } + } + tex.SetPixels(colors.ToArray()); + tex.Apply(); + return tex; + } + + public Texture2D GenerateVramPreview() + { + + int adjustedWidth = Width; + + if (BitDepth == PSXBPP.TEX_4BIT) + { + adjustedWidth = Mathf.CeilToInt(Width / 4f); + } + else if (BitDepth == PSXBPP.TEX_8BIT) + { + adjustedWidth = Mathf.CeilToInt(Width / 2f); + } + + Texture2D vramTexture = new Texture2D(adjustedWidth, Height); + + List packedValues = new List(); + + if (BitDepth == PSXBPP.TEX_4BIT) + { + for (int i = 0; i < Pixels.Length; i += 4) + { + ushort packed = (ushort)((Pixels[i] << 12) | (Pixels[i + 1] << 8) | (Pixels[i + 2] << 4) | Pixels[i + 3]); + packedValues.Add(packed); + } + } + else if (BitDepth == PSXBPP.TEX_8BIT) + { + for (int i = 0; i < Pixels.Length; i += 2) + { + ushort packed = (ushort)((Pixels[i] << 8) | Pixels[i + 1]); + packedValues.Add(packed); + } + } + + + List colors = new List(); + for (int i = 0; i < packedValues.Count; i++) + { + int index = packedValues[i]; + + float r = (index & 31) / 31.0f; + float g = ((index >> 5) & 31) / 31.0f; + float b = ((index >> 10) & 31) / 31.0f; + + colors.Add(new Color(r, g, b)); + } + vramTexture.SetPixels(colors.ToArray()); + vramTexture.Apply(); + + return vramTexture; + + } + + + + + + } +} \ No newline at end of file diff --git a/Runtime/PSXTexture2D.cs.meta b/Runtime/PSXTexture2D.cs.meta new file mode 100644 index 0000000..0e3e359 --- /dev/null +++ b/Runtime/PSXTexture2D.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ac29cfb818d45b12dba84e61317c794b \ No newline at end of file