Posts: 41
Threads: 21
Joined: Mar 2018
04-13-2019, 04:22 AM
(This post was last modified: 04-21-2019, 09:42 AM by David.)
ExoQuant is a MIT licensed C library written by Dennis Ranke that can be used to reduce the number of colors in an image, which is useful for creating CI textures. I have also written ports to C#, Javascript, VB.NET, and Python that should give the same results as the original C version.
ExoQuant v0.7 (C/C++): https://github.com/exoticorn/exoquant
ExoQuantSharp (C#): https://github.com/DavidSM64/ExoQuantSharp
ExoQuantJS (Javascript): https://github.com/DavidSM64/ExoQuantJS
ExoQuantVB (Visual Basic .NET): https://github.com/DavidSM64/ExoQuantVB
ExoQuantPY (Python): https://github.com/DavidSM64/ExoQuantPY
Converting RGBA32 data to CI data + palette:
Code: /**
* width = Width of the Texture.
* height = Height of the Texture.
* ciBitDepth = Should either be 4 for CI4, or 8 for CI8
* numOfColors = Size of the color palette.
* rgba32Data = input RGBA32 data
* ciData = output CI data
* rgba16Palette = output palette for CI data
*/
void ConvertRGBA32ToCI(int width, int height, int ciBitDepth, int numOfColors, unsigned char *rgba32Data, unsigned char **ciData, unsigned char **rgba16Palette)
{
if (ciBitDepth == 8) // CI8
{
if (numOfColors > 256)
numOfColors = 256;
}
else // CI4
{
if (numOfColors > 16)
numOfColors = 16;
}
int ci_len = width * height;
unsigned char* ci8Data = malloc(ci_len); // Output CI8 data buffer.
unsigned char* rgba32Palette = malloc(numOfColors * 4); // palette buffer.
// Use ExoQuant to reduce the number of colors.
exq_data *pExq = exq_init();
exq_feed(pExq, rgba32Data, ci_len);
exq_quantize_hq(pExq, numOfColors);
exq_get_palette(pExq, rgba32Palette, numOfColors);
exq_map_image_ordered(pExq, width, height, rgba32Data, ci8Data);
exq_free(pExq);
*rgba16Palette = malloc(numOfColors * 2);
// Convert RGBA32 palette to a RGBA16 palette
for (int i = 0; i < numOfColors; i++)
{
unsigned char red = (rgba32Palette[i * 4 + 0] / 8) & 0x1F;
unsigned char green = (rgba32Palette[i * 4 + 1] / 8) & 0x1F;
unsigned char blue = (rgba32Palette[i * 4 + 2] / 8) & 0x1F;
unsigned char alpha = rgba32Palette[i * 4 + 3] > 0 ? 1 : 0; // 1 bit alpha
(*rgba16Palette)[i * 2 + 0] = (red << 3) | (green >> 2);
(*rgba16Palette)[i * 2 + 1] = ((green & 3) << 6) | (blue << 1) | alpha;
}
if (ciBitDepth == 4)
{
ci_len /= 2;
*ciData = malloc(ci_len);
for (int i = 0; i < ci_len; i++)
{
(*ciData)[i] = (ci8Data[i * 2 + 0] << 4) | ci8Data[i * 2 + 1];
}
}
else
{
*ciData = malloc(ci_len);
for(int i = 0; i < ci_len; i++)
{
(*ciData)[i] = ci8Data[i];
}
}
free(ci8Data);
free(rgba32Palette);
}
Code: /**
* width = Width of the Texture.
* height = Height of the Texture.
* ciBitDepth = Should either be 4 for CI4, or 8 for CI8
* numOfColors = Size of the color palette.
* rgba32Data = input RGBA32 data
* ciData = output CI data
* rgba16Palette = output palette for CI data
*/
void ConvertRGBA32ToCI(int width, int height, int ciBitDepth, int numOfColors, std::vector<unsigned char> &rgba32Data, std::vector<unsigned char> &ciData, std::vector<unsigned char> &rgba16Palette)
{
if (ciBitDepth != 4 && ciBitDepth != 8)
throw std::invalid_argument("Invalid CI type! ciBitDepth should either be 4 or 8.");
if (ciBitDepth == 8) // CI8
{
if (numOfColors > 256)
numOfColors = 256;
}
else // CI4
{
if (numOfColors > 16)
numOfColors = 16;
}
std::vector<unsigned char> ci8Data(rgba32Data.size() / 4); // Output CI8 data buffer.
std::vector<unsigned char> rgba32Palette(numOfColors * 4); // palette buffer.
// Use ExoQuant to reduce the number of colors.
exq_data *pExq = exq_init();
exq_feed(pExq, (unsigned char*)&rgba32Data[0], width * height);
exq_quantize_hq(pExq, numOfColors);
exq_get_palette(pExq, (unsigned char*)&rgba32Palette[0], numOfColors);
exq_map_image_ordered(pExq, width, height, (unsigned char*)&rgba32Data[0], (unsigned char*)&ci8Data[0]);
exq_free(pExq);
rgba16Palette.resize(numOfColors * 2);
// Convert RGBA32 palette to a RGBA16 palette
for (int i = 0; i < numOfColors; i++)
{
unsigned char red = (rgba32Palette[i * 4 + 0] / 8) & 0x1F;
unsigned char green = (rgba32Palette[i * 4 + 1] / 8) & 0x1F;
unsigned char blue = (rgba32Palette[i * 4 + 2] / 8) & 0x1F;
unsigned char alpha = rgba32Palette[i * 4 + 3] > 0 ? 1 : 0; // 1 bit alpha
rgba16Palette[i * 2 + 0] = (red << 3) | (green >> 2);
rgba16Palette[i * 2 + 1] = ((green & 3) << 6) | (blue << 1) | alpha;
}
if (ciBitDepth == 4)
{
// Convert CI8 image to CI4 image
ciData.resize(ci8Data.size() / 2);
for (int i = 0; i < ciData.size(); i++)
{
ciData[i] = (ci8Data[i * 2 + 0] << 4) | ci8Data[i * 2 + 1];
}
}
else
{
ciData.resize(ci8Data.size());
for(int i = 0; i < ci8Data.size(); i++)
{
ciData[i] = ci8Data[i];
}
}
}
Code: /**
* width = Width of the Texture.
* height = Height of the Texture.
* ciBitDepth = Should either be 4 for CI4, or 8 for CI8
* numOfColors = Size of the color palette.
* rgba32Data = input RGBA32 data
* out ciData = output CI data
* out rgba16Palette = output palette for CI data
*/
void ConvertRGBA32ToCI(int width, int height, int ciBitDepth, int numOfColors, byte[] rgba32Data, out byte[] ciData, out byte[] rgba16Palette)
{
if (ciBitDepth != 4 && ciBitDepth != 8)
throw new Exception("Invalid CI type: CI" + ciBitDepth);
if (ciBitDepth == 8) // CI8
{
if (numOfColors > 256)
numOfColors = 256;
}
else // CI4
{
if (numOfColors > 16)
numOfColors = 16;
}
// Use ExoQuant to reduce the number of colors.
ExoQuant exq = new ExoQuant();
exq.Feed(rgba32Data);
exq.QuantizeHq(numOfColors);
exq.GetPalette(out byte[] rgba32Palette, numOfColors);
exq.MapImageOrdered(width, height, rgba32Data, out byte[] ci8Data);
rgba16Palette = new byte[numOfColors * 2];
// Convert RGBA32 palette to a RGBA16 palette
for (int i = 0; i < numOfColors; i++)
{
byte red = (byte)((rgba32Palette[i * 4 + 0] / 8) & 0x1F);
byte green = (byte)((rgba32Palette[i * 4 + 1] / 8) & 0x1F);
byte blue = (byte)((rgba32Palette[i * 4 + 2] / 8) & 0x1F);
byte alpha = (byte)(rgba32Palette[i * 4 + 3] > 0 ? 1 : 0); // 1 bit alpha
rgba16Palette[i * 2 + 0] = (byte)((red << 3) | (green >> 2));
rgba16Palette[i * 2 + 1] = (byte)(((green & 3) << 6) | (blue << 1) | alpha);
}
if (ciBitDepth == 4)
{
// Convert CI8 image to CI4 image
ciData = new byte[rgba32Data.Length / 8];
for (int i = 0; i < ciData.Length; i++)
{
ciData[i] = (byte)((ci8Data[i * 2 + 0] << 4) | ci8Data[i * 2 + 1]);
}
}
else
ciData = ci8Data;
}
Comparing RGBA16 to CI4:
A benefit of using CI4 is that it uses up much less data in a ROM compared to RGBA16. Not all textures should be converted to CI4 though. If a texture relies on having lots of different hues, then it will not translate to CI4 well.
The level backgrounds in SM64 are pretty good CI4 candidates, since they generally theme around one or two colors.
However, the castle paintings are bad CI4 candidates since they use a lot of different colors.
|