using System; using System.Drawing; using System.Drawing.Imaging; namespace NW4RTools { public class Texture { public struct TextureFormatInfo { public int TexelWidth, TexelHeight, NybblesPerPixel; public const int TexelByteSize = 32; private TextureFormatInfo(int tWidth, int tHeight, int npp) { TexelWidth = tWidth; TexelHeight = tHeight; NybblesPerPixel = npp; } static TextureFormatInfo() { I4 = new TextureFormatInfo(8, 8, 1); I8 = new TextureFormatInfo(8, 4, 2); IA4 = new TextureFormatInfo(8, 4, 2); IA8 = new TextureFormatInfo(4, 4, 4); RGB565 = new TextureFormatInfo(4, 4, 4); RGB5A3 = new TextureFormatInfo(4, 4, 4); RGBA8 = new TextureFormatInfo(4, 4, 8); C4 = new TextureFormatInfo(8, 8, 1); C8 = new TextureFormatInfo(8, 4, 2); C14X2 = new TextureFormatInfo(4, 4, 4); CMPR = new TextureFormatInfo(8, 8, 1); } public static TextureFormatInfo GetFormat(TextureFormat format) { switch (format) { case TextureFormat.I4: return I4; case TextureFormat.I8: return I8; case TextureFormat.IA4: return IA4; case TextureFormat.IA8: return IA8; case TextureFormat.RGB565: return RGB565; case TextureFormat.RGB5A3: return RGB5A3; case TextureFormat.RGBA8: return RGBA8; case TextureFormat.C4: return C4; case TextureFormat.C8: return C8; case TextureFormat.C14X2: return C14X2; case TextureFormat.CMPR: return CMPR; default: throw new ArgumentOutOfRangeException("unknown texture format"); } } public static readonly TextureFormatInfo I4, I8, IA4, IA8, RGB565, RGB5A3, RGBA8, C4, C8, C14X2, CMPR; } public Bitmap[] Images; public TextureFormat Format; public float MinLOD, MaxLOD; public Texture() { } public static int GetDataSize(int width, int height, TextureFormat format) { var info = TextureFormatInfo.GetFormat(format); // align width, height up width = Misc.AlignUp(width, info.TexelWidth); height = Misc.AlignUp(height, info.TexelHeight); return width * height * info.NybblesPerPixel / 2; } public int GetDataSize() { int size = 0; for (int i = 0; i < Images.Length; i++) { size += GetDataSize(i); } return size; } public int GetDataSize(int imageID) { return GetDataSize(Images[imageID].Width, Images[imageID].Height, Format); } unsafe public byte[] ExportData(int imageID) { var info = TextureFormatInfo.GetFormat(Format); int blkWidth = info.TexelWidth; int blkHeight = info.TexelHeight; int width = Images[imageID].Width; int height = Images[imageID].Height; var bits = Images[imageID].LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); var data = new OutputStream(ByteEndian.BigEndian); switch (Format) { case TextureFormat.RGB565: case TextureFormat.RGB5A3: case TextureFormat.I4: case TextureFormat.I8: case TextureFormat.IA4: case TextureFormat.IA8: // This won't be fun bool alreadyHaveI4Nybble = false; byte currentI4Byte = 0; for (int y = 0; y < height; y += blkHeight) { int yMax = y + blkHeight; if (yMax > height) yMax = height; for (int x = 0; x < width; x += blkWidth) { int xMax = x + blkWidth; if (xMax > width) xMax = width; for (int y1 = y; y1 < yMax; y1++) { byte* pRow = (byte*)bits.Scan0 + (y1 * bits.Stride); for (int x1 = x; x1 < xMax; x1++) { uint* pPixel = (uint*)pRow + x1; byte a = (byte)((*pPixel & 0xFF000000) >> 24); byte r = (byte)((*pPixel & 0x00FF0000) >> 16); byte g = (byte)((*pPixel & 0x0000FF00) >> 8); byte b = (byte)(*pPixel & 0x000000FF); if (Format == TextureFormat.RGB565) { ushort _r = (ushort)(r >> 3); ushort _g = (ushort)(g >> 2); ushort _b = (ushort)(b >> 3); data.WriteUInt16((ushort)((_r << 11) | (_g << 5) | _b)); } else if (Format == TextureFormat.RGB5A3) { if (a == 255) { ushort _r = (ushort)(r >> 3); ushort _g = (ushort)(g >> 3); ushort _b = (ushort)(b >> 3); data.WriteUInt16((ushort)(0x8000 | (_r << 10) | (_g << 5) | _b)); } else { ushort _a = (ushort)(a >> 5); ushort _r = (ushort)(r >> 4); ushort _g = (ushort)(g >> 4); ushort _b = (ushort)(b >> 4); data.WriteUInt16((ushort)((_a << 12) | (_r << 8) | (_g << 4) | _b)); } } else if (Format == TextureFormat.I4) { byte _i = (byte)((r & g & b) >> 4); if (!alreadyHaveI4Nybble) { currentI4Byte = (byte)(_i << 4); } else { data.WriteByte((byte)(currentI4Byte | _i)); currentI4Byte = 0; } alreadyHaveI4Nybble = !alreadyHaveI4Nybble; } else if (Format == TextureFormat.I8) { byte _i = (byte)(r & g & b); data.WriteByte(_i); } else if (Format == TextureFormat.IA4) { byte _i = (byte)((r & g & b) >> 4); byte _a = (byte)(a >> 4); data.WriteByte((byte)((_i << 4) | _a)); } else if (Format == TextureFormat.IA8) { byte _i = (byte)(r & g & b); data.WriteByte(_i); data.WriteByte(a); } } } } } break; case TextureFormat.RGBA8: // this one is a pain because it stores an AR chunk, then a GB chunk, then ... for (int y = 0; y < height; y += blkHeight) { int yMax = y + blkHeight; if (yMax > height) yMax = height; for (int x = 0; x < width; x += blkWidth) { int xMax = x + blkWidth; if (xMax > width) xMax = width; for (int blkType = 0; blkType < 2; blkType++) { for (int y1 = y; y1 < yMax; y1++) { byte* pRow = (byte*)bits.Scan0 + (y1 * bits.Stride); for (int x1 = x; x1 < xMax; x1++) { uint* pPixel = (uint*)pRow + x1; ushort piece; if (blkType == 0) { piece = (ushort)((*pPixel & 0xFFFF0000) >> 16); } else { piece = (ushort)(*pPixel & 0x0000FFFF); } data.WriteUInt16(piece); } } } } } break; case TextureFormat.CMPR: // the texture format from hell for (int y = 0; y < height; y += 8) { for (int x = 0; x < width; x += 8) { for (int iBlockY = 0; iBlockY < 2; iBlockY++) { for (int iBlockX = 0; iBlockX < 2; iBlockX++) { var block = Util.NVDXT.compressDXT1a((Util.NVDXT.ARGBPixel*)bits.Scan0, x + (iBlockX * 4), y + (iBlockY * 4), bits.Width, bits.Height); data.WriteUInt16(block.Color0); data.WriteUInt16(block.Color1); data.WriteUInt32(block.Lookup); } } } } break; } Images[imageID].UnlockBits(bits); return data.GetBuffer(); } unsafe public void ImportData(int imageID, byte[] imgdata, int width, int height, TextureFormat format) { var image = new Bitmap(width, height, PixelFormat.Format32bppArgb); var bits = image.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, image.PixelFormat); var info = TextureFormatInfo.GetFormat(format); int blkWidth = info.TexelWidth; int blkHeight = info.TexelHeight; // What I'm missing overall: all the palette ones // this thing probably breaks on textures which aren't multiples of the texel width/height, too // oh well var data = new InputStream(imgdata); switch (format) { case TextureFormat.RGB565: case TextureFormat.RGB5A3: case TextureFormat.I4: case TextureFormat.I8: case TextureFormat.IA4: case TextureFormat.IA8: // I4 is stupid and breaks my parser. I'll keep some state here bool alreadyHaveI4Nybble = false; byte currentI4Byte = 0; for (int y = 0; y < height; y += blkHeight) { int yMax = y + blkHeight; if (yMax > height) yMax = height; for (int x = 0; x < width; x += blkWidth) { int xMax = x + blkWidth; if (xMax > width) xMax = width; for (int y1 = y; y1 < yMax; y1++) { byte* pRow = (byte*)bits.Scan0 + (y1 * bits.Stride); for (int x1 = x; x1 < xMax; x1++) { uint* pPixel = (uint*)pRow + x1; if (format == TextureFormat.RGB565) { ushort val = data.ReadUInt16(); uint r = (uint)(val & 0xF800) >> 8; uint g = (uint)(val & 0x7e0) >> 3; uint b = (uint)(val & 0x1f) << 3; *pPixel = 0xFF000000 | (r << 16) | (g << 8) | b; } else if (format == TextureFormat.RGB5A3) { ushort val = data.ReadUInt16(); if ((val & 0x8000) == 0) { uint a = (uint)(val & 0x7000) >> 7; uint r = (uint)(val & 0xF00) >> 4; uint g = (uint)(val & 0xF0); uint b = (uint)(val & 0xF) << 4; *pPixel = (a << 24) | (r << 16) | (g << 8) | b; } else { uint r = (uint)(val & 0x7c00) >> 7; uint g = (uint)(val & 0x3e0) >> 2; uint b = (uint)(val & 0x1f) << 3; *pPixel = 0xFF000000 | (r << 16) | (g << 8) | b; } } else if (format == TextureFormat.I4) { if (!alreadyHaveI4Nybble) { currentI4Byte = data.ReadByte(); uint val = (uint)currentI4Byte & 0xF0; *pPixel = 0xFF000000 | (val << 16) | (val << 8) | val; } else { uint val = (uint)(currentI4Byte & 0xF) << 4; *pPixel = 0xFF000000 | (val << 16) | (val << 8) | val; } alreadyHaveI4Nybble = !alreadyHaveI4Nybble; } else if (format == TextureFormat.I8) { byte val = data.ReadByte(); *pPixel = 0xFF000000 | (uint)(val << 16) | (uint)(val << 8) | val; } else if (format == TextureFormat.IA4) { byte val = data.ReadByte(); uint i = (uint)(val >> 4); uint a = (uint)((val & 0xF0) << 4); *pPixel = (a << 24) | (i << 16) | (i << 8) | i; } else if (format == TextureFormat.IA8) { byte i = data.ReadByte(); byte a = data.ReadByte(); *pPixel = (uint)(a << 24) | (uint)(i << 16) | (uint)(i << 8) | i; } } } } } break; case TextureFormat.RGBA8: // this one is a pain because it stores an AR chunk, then a GB chunk, then ... for (int y = 0; y < height; y += blkHeight) { int yMax = y + blkHeight; if (yMax > height) yMax = height; for (int x = 0; x < width; x += blkWidth) { int xMax = x + blkWidth; if (xMax > width) xMax = width; for (int blkType = 0; blkType < 2; blkType++) { for (int y1 = y; y1 < yMax; y1++) { byte* pRow = (byte*)bits.Scan0 + (y1 * bits.Stride); for (int x1 = x; x1 < xMax; x1++) { uint* pPixel = (uint*)pRow + x1; uint val1 = data.ReadByte(); uint val2 = data.ReadByte(); if (blkType == 0) { // Alpha, Red *pPixel = ((*pPixel & 0x0000FFFF) | (val1 << 24) | (val2 << 16)); } else { // Green, Blue *pPixel = ((*pPixel & 0xFFFF0000) | (val1 << 8) | (val2 << 0)); } } } } } } break; case TextureFormat.CMPR: // this code is messy ushort[] rawClrArray = new ushort[4]; uint[] clrArray = new uint[4]; for (int y = 0; y < height; y += 8) { for (int x = 0; x < width; x += 8) { for (int iBlockY = 0; iBlockY < 2; iBlockY++) { for (int iBlockX = 0; iBlockX < 2; iBlockX++) { // decode a block rawClrArray[0] = data.ReadUInt16(); rawClrArray[1] = data.ReadUInt16(); bool hasAlpha = false; if (rawClrArray[0] > rawClrArray[1]) { rawClrArray[2] = CMPRAvgColor(2, 1, rawClrArray[0], rawClrArray[1]); rawClrArray[3] = CMPRAvgColor(1, 2, rawClrArray[0], rawClrArray[1]); } else { rawClrArray[2] = CMPRAvgColor(1, 1, rawClrArray[0], rawClrArray[1]); rawClrArray[3] = rawClrArray[1]; hasAlpha = true; } for (int i = 0; i < 4; i++) { byte r = (byte)(((rawClrArray[i] >> 11) & 0x1F) << 3); byte g = (byte)(((rawClrArray[i] >> 5) & 0x3F) << 2); byte b = (byte)((rawClrArray[i] & 0x1F) << 3); byte a = (byte)(((i == 3) && hasAlpha) ? 0 : 255); clrArray[i] = (uint)(a << 24) | (uint)(r << 16) | (uint)(g << 8) | b; } for (int inY = 0; inY < 4; inY++) { byte* pRow = (byte*)bits.Scan0 + ((y + (iBlockY * 4) + inY) * bits.Stride); byte val = data.ReadByte(); for (int inX = 0; inX < 4; inX++) { uint *pPixel = (uint*)pRow + x + (iBlockX * 4) + inX; *pPixel = clrArray[(val >> 6) & 3]; val <<= 2; } } } } } } break; default: Console.WriteLine("Unsupported texture format: {0}", format); break; } image.UnlockBits(bits); Images[imageID] = image; Format = format; } private static ushort CMPRAvgColor(ushort w0, ushort w1, ushort c0, ushort c1) { uint result; ushort a0 = (ushort)(c0 >> 11); ushort a1 = (ushort)(c1 >> 11); uint a = (uint)((w0*a0 + w1*a1) / (w0+w1)); result = (a << 11) & 0xffff; a0 = (ushort)((c0 >> 5) & 63); a1 = (ushort)((c1 >> 5) & 63); a = (uint)((w0 * a0 + w1 * a1) / (w0 + w1)); result |= ((a << 5) & 0xffff); a0 = (ushort)(c0 & 31); a1 = (ushort)(c1 & 31); a = (uint)((w0 * a0 + w1 * a1) / (w0 + w1)); result |= a; return (ushort)result; } } }