Rewrote quantizer
This commit is contained in:
@@ -11,6 +11,7 @@ namespace PSXSplash.EditorCode
|
|||||||
{
|
{
|
||||||
public override void OnInspectorGUI()
|
public override void OnInspectorGUI()
|
||||||
{
|
{
|
||||||
|
/*
|
||||||
PSXObjectExporter comp = (PSXObjectExporter)target;
|
PSXObjectExporter comp = (PSXObjectExporter)target;
|
||||||
serializedObject.Update();
|
serializedObject.Update();
|
||||||
|
|
||||||
@@ -79,7 +80,8 @@ namespace PSXSplash.EditorCode
|
|||||||
EditorGUILayout.EndVertical();
|
EditorGUILayout.EndVertical();
|
||||||
|
|
||||||
serializedObject.ApplyModifiedProperties();
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using PSXSplash.RuntimeCode;
|
using PSXSplash.RuntimeCode;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
@@ -9,13 +10,9 @@ public class QuantizedPreviewWindow : EditorWindow
|
|||||||
private Texture2D originalTexture;
|
private Texture2D originalTexture;
|
||||||
private Texture2D quantizedTexture;
|
private Texture2D quantizedTexture;
|
||||||
private Texture2D vramTexture; // New VRAM Texture
|
private Texture2D vramTexture; // New VRAM Texture
|
||||||
private ushort[] clut; // Changed to 1D array
|
private List<VRAMPixel> clut; // Changed to 1D array
|
||||||
private ushort[] indexedPixelData; // New field for indexed pixel data
|
private ushort[] indexedPixelData; // New field for indexed pixel data
|
||||||
private int bpp = 4;
|
private PSXBPP bpp;
|
||||||
private int targetWidth = 128;
|
|
||||||
private int targetHeight = 128;
|
|
||||||
public bool dithering = true;
|
|
||||||
private int maxKMeans = 100;
|
|
||||||
private readonly int previewSize = 256;
|
private readonly int previewSize = 256;
|
||||||
|
|
||||||
[MenuItem("Window/Quantized Preview")]
|
[MenuItem("Window/Quantized Preview")]
|
||||||
@@ -31,13 +28,9 @@ public class QuantizedPreviewWindow : EditorWindow
|
|||||||
|
|
||||||
originalTexture = (Texture2D)EditorGUILayout.ObjectField("Original Texture", originalTexture, typeof(Texture2D), false);
|
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)
|
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 (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write))
|
||||||
using (BinaryWriter writer = new BinaryWriter(fileStream))
|
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()
|
private void GenerateQuantizedPreview()
|
||||||
{
|
{
|
||||||
Texture2D resizedTexture = PSXTexture.ResizeTexture(originalTexture, targetWidth, targetHeight);
|
|
||||||
|
|
||||||
if(dithering) {
|
PSXTexture2D psxTex = PSXTexture2D.CreateFromTexture2D(originalTexture, bpp, false);
|
||||||
resizedTexture = PSXTexture.DitherTexture(resizedTexture);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bpp == 16)
|
quantizedTexture = psxTex.GeneratePreview();
|
||||||
{
|
vramTexture = psxTex.GenerateVramPreview();
|
||||||
quantizedTexture = null;
|
clut = psxTex.ColorPalette;
|
||||||
indexedPixelData = PSXTexture.ConvertTo16Bpp(resizedTexture);
|
|
||||||
clut = null;
|
|
||||||
vramTexture = ConvertTo16BppTexture2D(resizedTexture);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var (indexedPixels, generatedClut) = ImageQuantizer.Quantize(resizedTexture, bpp, maxKMeans);
|
|
||||||
|
|
||||||
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)
|
private void DrawTexturePreview(Texture2D texture, int size, bool flipY = true)
|
||||||
{
|
{
|
||||||
Rect rect = GUILayoutUtility.GetRect(size, size, GUILayout.ExpandWidth(false));
|
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()
|
private void DrawCLUT()
|
||||||
{
|
{
|
||||||
@@ -282,7 +155,7 @@ public class QuantizedPreviewWindow : EditorWindow
|
|||||||
|
|
||||||
GUILayout.Space(10);
|
GUILayout.Space(10);
|
||||||
|
|
||||||
int totalColors = clut.Length;
|
int totalColors = clut.Count;
|
||||||
int totalRows = Mathf.CeilToInt((float)totalColors / maxColorsPerRow);
|
int totalRows = Mathf.CeilToInt((float)totalColors / maxColorsPerRow);
|
||||||
|
|
||||||
for (int row = 0; row < totalRows; row++)
|
for (int row = 0; row < totalRows; row++)
|
||||||
@@ -296,9 +169,9 @@ public class QuantizedPreviewWindow : EditorWindow
|
|||||||
int index = row * maxColorsPerRow + col;
|
int index = row * maxColorsPerRow + col;
|
||||||
|
|
||||||
Vector3 color = new Vector3(
|
Vector3 color = new Vector3(
|
||||||
(clut[index] & 31) / 31.0f, // Red: bits 0–4
|
clut[index].R / 31.0f, // Red: bits 0–4
|
||||||
((clut[index] >> 5) & 31) / 31.0f, // Green: bits 5–9
|
clut[index].G / 31.0f, // Green: bits 5–9
|
||||||
((clut[index] >> 10) & 31) / 31.0f // Blue: bits 10–14
|
clut[index].B / 31.0f // Blue: bits 10–14
|
||||||
);
|
);
|
||||||
|
|
||||||
Rect rect = GUILayoutUtility.GetRect(swatchSize, swatchSize, GUILayout.ExpandWidth(false));
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
163
Runtime/ImageProcessing.cs
Normal file
163
Runtime/ImageProcessing.cs
Normal file
@@ -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<Color> _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<Color>();
|
||||||
|
|
||||||
|
|
||||||
|
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<Color> clusterColors = new List<Color>();
|
||||||
|
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<Color> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Vector3> 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<Vector3>(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<Vector3> InitializeCentroids(Texture2D image, int maxColors)
|
|
||||||
{
|
|
||||||
List<Vector3> centroids = new List<Vector3>();
|
|
||||||
Color[] pixels = image.GetPixels();
|
|
||||||
HashSet<Vector3> uniqueColors = new HashSet<Vector3>();
|
|
||||||
|
|
||||||
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<Vector3> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ namespace PSXSplash.RuntimeCode
|
|||||||
{
|
{
|
||||||
|
|
||||||
public PSXMesh Mesh;
|
public PSXMesh Mesh;
|
||||||
public PSXTexture Texture;
|
//ublic PSXTexture Texture;
|
||||||
|
|
||||||
public void Export()
|
public void Export()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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<MeshRenderer>();
|
|
||||||
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<MeshRenderer>();
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 6d64b6bb75da33720b928203b2780952
|
|
||||||
173
Runtime/PSXTexture2D.cs
Normal file
173
Runtime/PSXTexture2D.cs
Normal file
@@ -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<VRAMPixel> ColorPalette = new List<VRAMPixel>();
|
||||||
|
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<Color> colors = new List<Color>();
|
||||||
|
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<ushort> packedValues = new List<ushort>();
|
||||||
|
|
||||||
|
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<Color> colors = new List<Color>();
|
||||||
|
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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Runtime/PSXTexture2D.cs.meta
Normal file
2
Runtime/PSXTexture2D.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ac29cfb818d45b12dba84e61317c794b
|
||||||
Reference in New Issue
Block a user