522 lines
19 KiB
C#
522 lines
19 KiB
C#
namespace SM64Lib.N64Graphics;
|
|
|
|
public enum N64Codec { RGBA16, RGBA32, IA16, IA8, IA4, I8, I4, CI8, CI4, ONEBPP };
|
|
public enum N64IMode { AlphaCopyIntensity, AlphaBinary, AlphaOne };
|
|
|
|
public class N64Graphics
|
|
{
|
|
private static int SCALE_5_8(int val)
|
|
{
|
|
return (val * 0xFF) / 0x1F;
|
|
}
|
|
|
|
private static byte SCALE_8_5(byte val)
|
|
{
|
|
return (byte)((((val) + 4) * 0x1F) / 0xFF);
|
|
}
|
|
|
|
private static byte SCALE_8_4(byte val)
|
|
{
|
|
return (byte)(val / 0x11);
|
|
}
|
|
|
|
private static int SCALE_3_8(byte val)
|
|
{
|
|
return (val * 0xFF) / 0x7;
|
|
}
|
|
|
|
private static byte SCALE_8_3(byte val)
|
|
{
|
|
return (byte)(val / 0x24);
|
|
}
|
|
|
|
public static Color RGBA16Color(byte c0, byte c1)
|
|
{
|
|
int r = SCALE_5_8((c0 & 0xF8) >> 3);
|
|
int g = SCALE_5_8(((c0 & 0x07) << 2) | ((c1 & 0xC0) >> 6));
|
|
int b = SCALE_5_8((c1 & 0x3E) >> 1);
|
|
int a = ((c1 & 0x1) > 0) ? 255 : 0;
|
|
return Color.FromArgb(a, r, g, b);
|
|
}
|
|
|
|
public static void ColorRGBA16(Color col, out byte c0, out byte c1)
|
|
{
|
|
byte r, g, b;
|
|
r = SCALE_8_5(col.R);
|
|
g = SCALE_8_5(col.G);
|
|
b = SCALE_8_5(col.B);
|
|
c0 = (byte)((r << 3) | (g >> 2));
|
|
c1 = (byte)(((g & 0x3) << 6) | (b << 1) | ((col.A > 0) ? 1 : 0));
|
|
}
|
|
|
|
public static Color RGBA16Color(byte[] data, int pixOffset)
|
|
{
|
|
byte c0 = data[pixOffset];
|
|
byte c1 = data[pixOffset + 1];
|
|
|
|
return RGBA16Color(c0, c1);
|
|
}
|
|
|
|
public static Color RGBA32Color(byte[] data, int pixOffset)
|
|
{
|
|
int r, g, b, a;
|
|
r = data[pixOffset];
|
|
g = data[pixOffset + 1];
|
|
b = data[pixOffset + 2];
|
|
a = data[pixOffset + 3];
|
|
return Color.FromArgb(a, r, g, b);
|
|
}
|
|
|
|
public static Color IA16Color(byte[] data, int pixOffset)
|
|
{
|
|
int i = data[pixOffset];
|
|
int a = data[pixOffset + 1];
|
|
return Color.FromArgb(a, i, i, i);
|
|
}
|
|
|
|
public static Color IA8Color(byte[] data, int pixOffset)
|
|
{
|
|
int i, a;
|
|
byte c = data[pixOffset];
|
|
i = (c >> 4) * 0x11;
|
|
a = (c & 0xF) * 0x11;
|
|
return Color.FromArgb(a, i, i, i);
|
|
}
|
|
|
|
public static Color IA4Color(byte[] data, int pixOffset, int nibble)
|
|
{
|
|
int shift = (1 - nibble) * 4;
|
|
int i, a;
|
|
int val = (data[pixOffset] >> shift) & 0xF;
|
|
i = SCALE_3_8((byte)(val >> 1));
|
|
a = (val & 0x1) > 0 ? 0xFF : 0x00;
|
|
return Color.FromArgb(a, i, i, i);
|
|
}
|
|
|
|
public static Color I8Color(byte[] data, int pixOffset, N64IMode mode = N64IMode.AlphaCopyIntensity)
|
|
{
|
|
int i = data[pixOffset];
|
|
int a = i;
|
|
switch (mode)
|
|
{
|
|
case N64IMode.AlphaBinary: a = (i == 0) ? 0 : 0xFF; break;
|
|
case N64IMode.AlphaCopyIntensity: a = i; break;
|
|
case N64IMode.AlphaOne: a = 0xFF; break;
|
|
}
|
|
return Color.FromArgb(a, i, i, i);
|
|
}
|
|
|
|
public static Color I4Color(byte[] data, int pixOffset, int nibble, N64IMode mode = N64IMode.AlphaCopyIntensity)
|
|
{
|
|
int shift = (1 - nibble) * 4;
|
|
int i = (data[pixOffset] >> shift) & 0xF;
|
|
i *= 0x11;
|
|
int a = i;
|
|
switch (mode)
|
|
{
|
|
case N64IMode.AlphaBinary: a = (i == 0) ? 0 : 0xFF; break;
|
|
case N64IMode.AlphaCopyIntensity: a = i; break;
|
|
case N64IMode.AlphaOne: a = 0xFF; break;
|
|
}
|
|
return Color.FromArgb(a, i, i, i);
|
|
}
|
|
|
|
public static Color CI8Color(byte[] data, byte[] palette, int pixOffset)
|
|
{
|
|
byte c0, c1;
|
|
int palOffset = 2 * data[pixOffset];
|
|
c0 = palette[palOffset];
|
|
c1 = palette[palOffset + 1];
|
|
|
|
return RGBA16Color(c0, c1);
|
|
}
|
|
|
|
public static Color CI4Color(byte[] data, byte[] palette, int pixOffset, int nibble)
|
|
{
|
|
byte c0, c1;
|
|
int shift = (1 - nibble) * 4;
|
|
int palOffset = 2 * ((data[pixOffset] >> shift) & 0xF);
|
|
c0 = palette[palOffset];
|
|
c1 = palette[palOffset + 1];
|
|
|
|
return RGBA16Color(c0, c1);
|
|
}
|
|
|
|
public static Color BPPColor(byte[] data, int pixOffset, int bit)
|
|
{
|
|
int i, a;
|
|
int val = (data[pixOffset] >> (7 - bit)) & 0x1;
|
|
i = a = val == 0 ? 0x00 : 0xFF;
|
|
return Color.FromArgb(a, i, i, i);
|
|
}
|
|
|
|
// return number of bytes needed to encode numPixels using codec
|
|
public static int PixelsToBytes(N64Codec codec, int numPixels)
|
|
{
|
|
int numBytes = 0;
|
|
switch (codec)
|
|
{
|
|
case N64Codec.RGBA16: numBytes = numPixels * 2; break;
|
|
case N64Codec.RGBA32: numBytes = numPixels * 4; break;
|
|
case N64Codec.IA16: numBytes = numPixels * 2; break;
|
|
case N64Codec.IA8: numBytes = numPixels; break;
|
|
case N64Codec.IA4: numBytes = numPixels / 2; break;
|
|
case N64Codec.I8: numBytes = numPixels; break;
|
|
case N64Codec.I4: numBytes = numPixels / 2; break;
|
|
case N64Codec.CI8: numBytes = numPixels; break;
|
|
case N64Codec.CI4: numBytes = numPixels / 2; break;
|
|
case N64Codec.ONEBPP: numBytes = numPixels / 8; break;
|
|
}
|
|
return numBytes;
|
|
}
|
|
|
|
public static string CodecString(N64Codec codec)
|
|
{
|
|
switch (codec)
|
|
{
|
|
case N64Codec.RGBA16: return "rgba16";
|
|
case N64Codec.RGBA32: return "rgba32";
|
|
case N64Codec.IA16: return "ia16";
|
|
case N64Codec.IA8: return "ia8";
|
|
case N64Codec.IA4: return "ia4";
|
|
case N64Codec.I8: return "i8";
|
|
case N64Codec.I4: return "i4";
|
|
case N64Codec.CI8: return "ci8";
|
|
case N64Codec.CI4: return "ci4";
|
|
case N64Codec.ONEBPP: return "1bpp";
|
|
}
|
|
return "unk";
|
|
}
|
|
|
|
public static N64Codec StringCodec(string str)
|
|
{
|
|
switch (str.ToLower())
|
|
{
|
|
default:
|
|
case "rgba16": return N64Codec.RGBA16;
|
|
case "rgba32": return N64Codec.RGBA32;
|
|
case "ia16": return N64Codec.IA16;
|
|
case "ia8": return N64Codec.IA8;
|
|
case "ia4": return N64Codec.IA4;
|
|
case "i8": return N64Codec.I8;
|
|
case "i4": return N64Codec.I4;
|
|
case "ci8": return N64Codec.CI8;
|
|
case "ci4": return N64Codec.CI4;
|
|
case "1bpp": return N64Codec.ONEBPP;
|
|
}
|
|
}
|
|
|
|
public static Color MakeColor(byte[] data, byte[] palette, int offset, int select, N64Codec codec, N64IMode mode)
|
|
{
|
|
Color color;
|
|
switch (codec)
|
|
{
|
|
case N64Codec.RGBA16:
|
|
color = RGBA16Color(data, offset);
|
|
break;
|
|
case N64Codec.RGBA32:
|
|
color = RGBA32Color(data, offset);
|
|
break;
|
|
case N64Codec.IA16:
|
|
color = IA16Color(data, offset);
|
|
break;
|
|
case N64Codec.IA8:
|
|
color = IA8Color(data, offset);
|
|
break;
|
|
case N64Codec.IA4:
|
|
color = IA4Color(data, offset, select);
|
|
break;
|
|
case N64Codec.I8:
|
|
color = I8Color(data, offset, mode);
|
|
break;
|
|
case N64Codec.I4:
|
|
color = I4Color(data, offset, select, mode);
|
|
break;
|
|
case N64Codec.CI8:
|
|
color = CI8Color(data, palette, offset);
|
|
break;
|
|
case N64Codec.CI4:
|
|
color = CI4Color(data, palette, offset, select);
|
|
break;
|
|
case N64Codec.ONEBPP:
|
|
color = BPPColor(data, offset, select);
|
|
break;
|
|
default:
|
|
color = RGBA16Color(data, offset);
|
|
break;
|
|
}
|
|
return color;
|
|
}
|
|
|
|
public static void RenderTexture(Graphics g, byte[] data, byte[] palette, int offset, int width, int height, int scale, N64Codec codec, N64IMode mode)
|
|
{
|
|
Brush brush;
|
|
for (int h = 0; h < height; h++)
|
|
{
|
|
for (int w = 0; w < width; w++)
|
|
{
|
|
int pixOffset = (h * width + w);
|
|
int bytesPerPix = 1;
|
|
int select = 0;
|
|
switch (codec)
|
|
{
|
|
case N64Codec.RGBA16: bytesPerPix = 2; pixOffset *= bytesPerPix; break;
|
|
case N64Codec.RGBA32: bytesPerPix = 4; pixOffset *= bytesPerPix; break;
|
|
case N64Codec.IA16: bytesPerPix = 2; pixOffset *= bytesPerPix; break;
|
|
case N64Codec.IA8: break;
|
|
case N64Codec.IA4:
|
|
select = pixOffset & 0x1;
|
|
pixOffset /= 2;
|
|
break;
|
|
case N64Codec.I8: break;
|
|
case N64Codec.I4:
|
|
case N64Codec.CI4:
|
|
select = pixOffset & 0x1;
|
|
pixOffset /= 2;
|
|
break;
|
|
case N64Codec.CI8: break;
|
|
case N64Codec.ONEBPP:
|
|
select = pixOffset & 0x7;
|
|
pixOffset /= 8;
|
|
break;
|
|
}
|
|
pixOffset += offset;
|
|
if (data.Length > pixOffset + bytesPerPix - 1)
|
|
{
|
|
brush = new SolidBrush(MakeColor(data, palette, pixOffset, select, codec, mode));
|
|
g.FillRectangle(brush, w * scale, h * scale, scale, scale);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// return palette index of matching c0/c1 16-bit dataset, or -1 if not found
|
|
private static int paletteIndex(byte[] pal, int palCount, byte c0, byte c1)
|
|
{
|
|
for (int i = 0; i < palCount; i++)
|
|
{
|
|
if (pal[2 * i] == c0 && pal[2 * i + 1] == c1)
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
public static void Convert(ref byte[] imageData, ref byte[] paletteData, N64Codec codec, Bitmap bm)
|
|
{
|
|
int numPixels = bm.Width * bm.Height;
|
|
imageData = new byte[PixelsToBytes(codec, numPixels)];
|
|
int palCount = 0;
|
|
switch (codec)
|
|
{
|
|
case N64Codec.RGBA16:
|
|
for (int y = 0; y < bm.Height; y++)
|
|
{
|
|
for (int x = 0; x < bm.Width; x++)
|
|
{
|
|
Color col = bm.GetPixel(x, y);
|
|
byte c0;
|
|
byte c1;
|
|
ColorRGBA16(col, out c0, out c1);
|
|
int idx = 2 * (y * bm.Width + x);
|
|
imageData[idx + 0] = c0;
|
|
imageData[idx + 1] = c1;
|
|
}
|
|
}
|
|
break;
|
|
case N64Codec.RGBA32:
|
|
for (int y = 0; y < bm.Height; y++)
|
|
{
|
|
for (int x = 0; x < bm.Width; x++)
|
|
{
|
|
Color col = bm.GetPixel(x, y);
|
|
int idx = 4 * (y * bm.Width + x);
|
|
imageData[idx + 0] = col.R;
|
|
imageData[idx + 1] = col.G;
|
|
imageData[idx + 2] = col.B;
|
|
imageData[idx + 3] = col.A;
|
|
}
|
|
}
|
|
break;
|
|
case N64Codec.IA16:
|
|
for (int y = 0; y < bm.Height; y++)
|
|
{
|
|
for (int x = 0; x < bm.Width; x++)
|
|
{
|
|
Color col = bm.GetPixel(x, y);
|
|
int sum = col.R + col.G + col.B;
|
|
byte intensity = (byte)(sum / 3);
|
|
byte alpha = col.A;
|
|
int idx = 2 * (y * bm.Width + x);
|
|
imageData[idx + 0] = intensity;
|
|
imageData[idx + 1] = alpha;
|
|
}
|
|
}
|
|
break;
|
|
case N64Codec.IA8:
|
|
for (int y = 0; y < bm.Height; y++)
|
|
{
|
|
for (int x = 0; x < bm.Width; x++)
|
|
{
|
|
Color col = bm.GetPixel(x, y);
|
|
int sum = col.R + col.G + col.B;
|
|
byte intensity = SCALE_8_4((byte)(sum / 3));
|
|
byte alpha = SCALE_8_4(col.A);
|
|
int idx = y * bm.Width + x;
|
|
imageData[idx] = (byte)((intensity << 4) | alpha);
|
|
}
|
|
}
|
|
break;
|
|
case N64Codec.IA4:
|
|
for (int y = 0; y < bm.Height; y++)
|
|
{
|
|
for (int x = 0; x < bm.Width; x++)
|
|
{
|
|
Color col = bm.GetPixel(x, y);
|
|
int sum = col.R + col.G + col.B;
|
|
byte intensity = SCALE_8_3((byte)(sum / 3));
|
|
byte alpha = (byte)(col.A > 0 ? 1 : 0);
|
|
int idx = y * bm.Width + x;
|
|
byte old = imageData[idx / 2];
|
|
if ((idx % 2) > 0)
|
|
{
|
|
imageData[idx / 2] = (byte)((old & 0xF0) | (intensity << 1) | alpha);
|
|
}
|
|
else
|
|
{
|
|
imageData[idx / 2] = (byte)((old & 0x0F) | (((intensity << 1) | alpha) << 4));
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case N64Codec.I8:
|
|
for (int y = 0; y < bm.Height; y++)
|
|
{
|
|
for (int x = 0; x < bm.Width; x++)
|
|
{
|
|
Color col = bm.GetPixel(x, y);
|
|
int sum = col.R + col.G + col.B;
|
|
byte intensity = (byte)(sum / 3);
|
|
int idx = y * bm.Width + x;
|
|
imageData[idx] = intensity;
|
|
}
|
|
}
|
|
break;
|
|
case N64Codec.I4:
|
|
for (int y = 0; y < bm.Height; y++)
|
|
{
|
|
for (int x = 0; x < bm.Width; x++)
|
|
{
|
|
Color col = bm.GetPixel(x, y);
|
|
int sum = col.R + col.G + col.B;
|
|
byte intensity = SCALE_8_4((byte)(sum / 3));
|
|
int idx = y * bm.Width + x;
|
|
byte old = imageData[idx / 2];
|
|
if ((idx % 2) > 0)
|
|
{
|
|
imageData[idx / 2] = (byte)((old & 0xF0) | intensity);
|
|
}
|
|
else
|
|
{
|
|
imageData[idx / 2] = (byte)((old & 0x0F) | (intensity << 4));
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case N64Codec.CI4:
|
|
paletteData = new byte[16 * 2];
|
|
for (int y = 0; y < bm.Height; y++)
|
|
{
|
|
for (int x = 0; x < bm.Width; x++)
|
|
{
|
|
Color col = bm.GetPixel(x, y);
|
|
byte r, g, b;
|
|
r = SCALE_8_5(col.R);
|
|
g = SCALE_8_5(col.G);
|
|
b = SCALE_8_5(col.B);
|
|
byte c0 = (byte)((r << 3) | (g >> 2));
|
|
byte c1 = (byte)(((g & 0x3) << 6) | (b << 1) | ((col.A > 0) ? 1 : 0));
|
|
int idx = y * bm.Width + x;
|
|
int palIdx = paletteIndex(paletteData, palCount, c0, c1);
|
|
if (palIdx < 0)
|
|
{
|
|
if (palCount < paletteData.Length / 2)
|
|
{
|
|
palIdx = palCount;
|
|
paletteData[2 * palCount] = c0;
|
|
paletteData[2 * palCount + 1] = c1;
|
|
palCount++;
|
|
}
|
|
else
|
|
{
|
|
palIdx = 0;
|
|
// TODO: out of palette entries. error or pick closest?
|
|
}
|
|
}
|
|
byte old = imageData[idx / 2];
|
|
if ((idx % 2) > 0)
|
|
{
|
|
imageData[idx / 2] = (byte)((old & 0xF0) | (byte)palIdx);
|
|
}
|
|
else
|
|
{
|
|
imageData[idx / 2] = (byte)((old & 0x0F) | ((byte)palIdx << 4));
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case N64Codec.CI8:
|
|
paletteData = new byte[256 * 2];
|
|
for (int y = 0; y < bm.Height; y++)
|
|
{
|
|
for (int x = 0; x < bm.Width; x++)
|
|
{
|
|
Color col = bm.GetPixel(x, y);
|
|
byte r, g, b;
|
|
r = SCALE_8_5(col.R);
|
|
g = SCALE_8_5(col.G);
|
|
b = SCALE_8_5(col.B);
|
|
byte c0 = (byte)((r << 3) | (g >> 2));
|
|
byte c1 = (byte)(((g & 0x3) << 6) | (b << 1) | ((col.A > 0) ? 1 : 0));
|
|
int idx = y * bm.Width + x;
|
|
int palIdx = paletteIndex(paletteData, palCount, c0, c1);
|
|
if (palIdx < 0)
|
|
{
|
|
if (palCount < paletteData.Length / 2)
|
|
{
|
|
palIdx = palCount;
|
|
paletteData[2 * palCount] = c0;
|
|
paletteData[2 * palCount + 1] = c1;
|
|
palCount++;
|
|
}
|
|
else
|
|
{
|
|
palIdx = 0;
|
|
// TODO: out of palette entries. error or pick closest?
|
|
}
|
|
}
|
|
imageData[idx] = (byte)palIdx;
|
|
}
|
|
}
|
|
break;
|
|
case N64Codec.ONEBPP:
|
|
for (int y = 0; y < bm.Height; y++)
|
|
{
|
|
for (int x = 0; x < bm.Width; x++)
|
|
{
|
|
Color col = bm.GetPixel(x, y);
|
|
int sum = col.R + col.G + col.B;
|
|
byte intensity = (sum > 0) ? (byte)1 : (byte)0;
|
|
int idx = y * bm.Width + x;
|
|
byte old = imageData[idx / 8];
|
|
int bit = idx % 8;
|
|
int mask = ~(1 << bit);
|
|
imageData[idx / 8] = (byte)((old & mask) | (intensity << bit));
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|