/// <summary> /// Determines DDS Surface Format given the header. /// </summary> /// <param name="ddspf">DDS PixelFormat structure.</param> /// <returns>Friendly format.</returns> public static ImageEngineFormat DetermineDDSSurfaceFormat(DDS_Header.DDS_PIXELFORMAT ddspf) { ImageEngineFormat format = ParseFourCC(ddspf.dwFourCC); if (format == ImageEngineFormat.Unknown) { // KFreon: Apparently all these flags mean it's a V8U8 image... if (ddspf.dwRGBBitCount == 16 && ddspf.dwRBitMask == 0x00FF && ddspf.dwGBitMask == 0xFF00 && ddspf.dwBBitMask == 0x00 && ddspf.dwABitMask == 0x00 && (ddspf.dwFlags & DDS_PFdwFlags.DDPF_SIGNED) == DDS_PFdwFlags.DDPF_SIGNED) format = ImageEngineFormat.DDS_V8U8; // KFreon: Test for L8/G8 else if (ddspf.dwABitMask == 0 && ddspf.dwBBitMask == 0 && ddspf.dwGBitMask == 0 && ddspf.dwRBitMask == 0xFF && ddspf.dwFlags == DDS_PFdwFlags.DDPF_LUMINANCE && ddspf.dwRGBBitCount == 8) format = ImageEngineFormat.DDS_G8_L8; // KFreon: A8L8. This can probably be something else as well, but it seems to work for now else if (ddspf.dwRGBBitCount == 16 && ddspf.dwFlags == (DDS_PFdwFlags.DDPF_ALPHAPIXELS | DDS_PFdwFlags.DDPF_LUMINANCE)) format = ImageEngineFormat.DDS_A8L8; // KFreon: RGB. RGB channels have something in them, but alpha doesn't. else if (((ddspf.dwFlags & DDS_PFdwFlags.DDPF_RGB) == DDS_PFdwFlags.DDPF_RGB && !((ddspf.dwFlags & DDS_PFdwFlags.DDPF_ALPHAPIXELS) == DDS_PFdwFlags.DDPF_ALPHAPIXELS)) || ddspf.dwABitMask == 0 && ddspf.dwBBitMask != 0 && ddspf.dwGBitMask != 0 && ddspf.dwRBitMask != 0) format = ImageEngineFormat.DDS_RGB; // KFreon: RGB and A channels are present. else if (((ddspf.dwFlags & (DDS_PFdwFlags.DDPF_RGB | DDS_PFdwFlags.DDPF_ALPHAPIXELS)) == (DDS_PFdwFlags.DDPF_RGB | DDS_PFdwFlags.DDPF_ALPHAPIXELS)) || ddspf.dwABitMask != 0 && ddspf.dwBBitMask != 0 && ddspf.dwGBitMask != 0 && ddspf.dwRBitMask != 0) format = ImageEngineFormat.DDS_ARGB; // KFreon: If nothing else fits, but there's data in one of the bitmasks, assume it can be read. else if (ddspf.dwABitMask != 0 || ddspf.dwRBitMask != 0 || ddspf.dwGBitMask != 0 || ddspf.dwBBitMask != 0) format = ImageEngineFormat.DDS_CUSTOM; else throw new FormatException("DDS Format is unknown."); } return format; }
internal static void ReadUncompressed(byte[] source, int sourceStart, byte[] destination, int pixelCount, DDS_Header.DDS_PIXELFORMAT ddspf) { int signedAdjustment = ((ddspf.dwFlags & DDS_Header.DDS_PFdwFlags.DDPF_SIGNED) == DDS_Header.DDS_PFdwFlags.DDPF_SIGNED) ? SignedAdjustment : 0; int sourceIncrement = ddspf.dwRGBBitCount / 8; // /8 for bits to bytes conversion bool oneChannel = (ddspf.dwFlags & DDS_Header.DDS_PFdwFlags.DDPF_LUMINANCE) == DDS_Header.DDS_PFdwFlags.DDPF_LUMINANCE; bool twoChannel = (ddspf.dwFlags & DDS_Header.DDS_PFdwFlags.DDPF_ALPHAPIXELS) == DDS_Header.DDS_PFdwFlags.DDPF_ALPHAPIXELS && oneChannel; uint AMask = ddspf.dwABitMask; uint RMask = ddspf.dwRBitMask; uint GMask = ddspf.dwGBitMask; uint BMask = ddspf.dwBBitMask; ///// Figure out channel existance and ordering. // Setup array that indicates channel offset from pixel start. // e.g. Alpha is usually first, and is given offset 0. // NOTE: Ordering array is in ARGB order, and the stored indices change depending on detected channel order. // A negative index indicates channel doesn't exist in data and sets channel to 0xFF. List<uint> maskOrder = new List<uint>(4) { AMask, RMask, GMask, BMask }; maskOrder.Sort(); maskOrder.RemoveAll(t => t == 0); // Required, otherwise indicies get all messed up when there's only two channels, but it's not indicated as such. int AIndex = 0; int RIndex = 0; int GIndex = 0; int BIndex = 0; if (twoChannel) // Note: V8U8 does not come under this one. { // Intensity is first byte, then the alpha. Set all RGB to intensity for grayscale. // Second mask is always RMask as determined by the DDS Spec. AIndex = AMask > RMask ? 1 : 0; RIndex = AMask > RMask ? 0 : 1; GIndex = AMask > RMask ? 0 : 1; BIndex = AMask > RMask ? 0 : 1; } else if (oneChannel) { // Decide whether it's alpha or not. AIndex = AMask == 0 ? -1 : 0; RIndex = AMask == 0 ? 0 : -1; GIndex = AMask == 0 ? 0 : -1; BIndex = AMask == 0 ? 0 : -1; } else { // Set default ordering AIndex = AMask == 0 ? -1 : maskOrder.IndexOf(AMask); RIndex = RMask == 0 ? -1 : maskOrder.IndexOf(RMask); GIndex = GMask == 0 ? -1 : maskOrder.IndexOf(GMask); BIndex = BMask == 0 ? -1 : maskOrder.IndexOf(BMask); } for (int i = 0, j = sourceStart; i < pixelCount * 4; i += 4, j += sourceIncrement) { destination[i] = BIndex == -1 ? (byte)0xFF : (byte)(source[j + BIndex] - signedAdjustment); destination[i + 1] = GIndex == -1 ? (byte)0xFF : (byte)(source[j + GIndex] - signedAdjustment); destination[i + 2] = RIndex == -1 ? (byte)0xFF : (byte)(source[j + RIndex] - signedAdjustment); destination[i + 3] = AIndex == -1 ? (byte)0xFF : (source[j + AIndex]); } }
static int WriteUncompressedMipMap(byte[] destination, int mipOffset, MipMap mipmap, ImageEngineFormat saveFormat, DDS_Header.DDS_PIXELFORMAT ddspf) { return DDS_Encoders.WriteUncompressed(mipmap.Pixels, destination, mipOffset, ddspf); }
private static MipMap ReadUncompressedMipMap(MemoryStream stream, int mipOffset, int mipWidth, int mipHeight, DDS_Header.DDS_PIXELFORMAT ddspf) { byte[] data = stream.GetBuffer(); byte[] mipmap = new byte[mipHeight * mipWidth * 4]; DDS_Decoders.ReadUncompressed(data, mipOffset, mipmap, mipWidth * mipHeight, ddspf); return new MipMap(mipmap, mipWidth, mipHeight); }
internal static byte[] Save(List<MipMap> mipMaps, ImageEngineFormat saveFormat, AlphaSettings alphaSetting, List<uint> customMasks = null) { // Set compressor for Block Compressed textures Action<byte[], int, int, byte[], int, AlphaSettings> compressor = null; bool needCheckSize = saveFormat.ToString().Contains("DXT") || saveFormat.ToString().Contains("ATI"); switch (saveFormat) { case ImageEngineFormat.DDS_ATI1: compressor = DDS_Encoders.CompressBC4Block; break; case ImageEngineFormat.DDS_ATI2_3Dc: compressor = DDS_Encoders.CompressBC5Block; break; case ImageEngineFormat.DDS_DX10: Debugger.Break(); break; // TODO: NOT SUPPORTED YET. DX10 case ImageEngineFormat.DDS_DXT1: compressor = DDS_Encoders.CompressBC1Block; break; case ImageEngineFormat.DDS_DXT2: case ImageEngineFormat.DDS_DXT3: compressor = DDS_Encoders.CompressBC2Block; break; case ImageEngineFormat.DDS_DXT4: case ImageEngineFormat.DDS_DXT5: compressor = DDS_Encoders.CompressBC3Block; break; } int height = mipMaps[0].Height; int width = mipMaps[0].Width; if (needCheckSize && !CheckSize_DXT(width, height)) throw new InvalidOperationException($"DXT compression formats require dimensions to be multiples of 4. Got: {width}x{height}."); int fullSize = GetCompressedSizeOfImage(mipMaps.Count, saveFormat, width, height); // +1 to get the full size, not just the offset of the last mip. //int fullSize = GetMipOffset(mipMaps.Count + 1, saveFormat, mipMaps[0].Width, mipMaps[0].Height); byte[] destination = new byte[fullSize]; // Create header and write to destination DDS_Header header = new DDS_Header(mipMaps.Count, height, width, saveFormat, customMasks); header.WriteToArray(destination, 0); int blockSize = ImageFormats.GetBlockSize(saveFormat); if (ImageFormats.IsBlockCompressed(saveFormat)) { int mipOffset = 128; foreach (MipMap mipmap in mipMaps) mipOffset = WriteCompressedMipMap(destination, mipOffset, mipmap, blockSize, compressor, alphaSetting); } else { // UNCOMPRESSED var action = new Action<int>(mipIndex => { if (alphaSetting == AlphaSettings.RemoveAlphaChannel) { // Remove alpha by setting AMask = 0 var ddspf = header.ddspf; ddspf.dwABitMask = 0; header.ddspf = ddspf; } // Get MipOffset int offset = GetMipOffset(mipIndex, saveFormat, width, height); WriteUncompressedMipMap(destination, offset, mipMaps[mipIndex], saveFormat, header.ddspf); }); if (ImageEngine.EnableThreading) Parallel.For(0, mipMaps.Count, new ParallelOptions { MaxDegreeOfParallelism = ImageEngine.NumThreads }, action); else for (int i = 0; i < mipMaps.Count; i++) action(i); } return destination; }
internal static List<MipMap> LoadDDS(MemoryStream compressed, DDS_Header header, int desiredMaxDimension) { MipMap[] MipMaps = null; int mipWidth = header.Width; int mipHeight = header.Height; ImageEngineFormat format = header.Format; int blockSize = ImageFormats.GetBlockSize(format); int estimatedMips = header.dwMipMapCount; int mipOffset = 128; // Includes header. // TODO: Incorrect mip offset for DX10 if (!EnsureMipInImage(compressed.Length, mipWidth, mipHeight, 4, format, out mipOffset)) // Update number of mips too estimatedMips = 1; if (estimatedMips == 0) estimatedMips = EstimateNumMipMaps(mipWidth, mipHeight); mipOffset = 128; // Needs resetting after checking there's mips in this image. // TESTUNIG if (estimatedMips == 0) estimatedMips = 1; int orig_estimatedMips = estimatedMips; // Store original count for later (uncompressed only I think) // KFreon: Decide which mip to start loading at - going to just load a few mipmaps if asked instead of loading all, then choosing later. That's slow. if (desiredMaxDimension != 0 && estimatedMips > 1) { if (!EnsureMipInImage(compressed.Length, mipWidth, mipHeight, desiredMaxDimension, format, out mipOffset)) // Update number of mips too throw new InvalidDataException($"Requested mipmap does not exist in this image. Top Image Size: {mipWidth}x{mipHeight}, requested mip max dimension: {desiredMaxDimension}."); // Not the first mipmap. if (mipOffset > 128) { double divisor = mipHeight > mipWidth ? mipHeight / desiredMaxDimension : mipWidth / desiredMaxDimension; mipHeight = (int)(mipHeight / divisor); mipWidth = (int)(mipWidth / divisor); if (mipWidth == 0 || mipHeight == 0) // Reset as a dimension is too small to resize { mipHeight = header.Height; mipWidth = header.Width; mipOffset = 128; } else { // Update estimated mips due to changing dimensions. estimatedMips = EstimateNumMipMaps(mipWidth, mipHeight); } } else // The first mipmap mipOffset = 128; } // Move to requested mipmap compressed.Position = mipOffset; // Block Compressed texture chooser. Action<byte[], int, byte[], int, int, bool> DecompressBCBlock = null; switch (format) { case ImageEngineFormat.DDS_DXT1: DecompressBCBlock = DDS_Decoders.DecompressBC1Block; break; case ImageEngineFormat.DDS_DXT2: case ImageEngineFormat.DDS_DXT3: DecompressBCBlock = DDS_Decoders.DecompressBC2Block; break; case ImageEngineFormat.DDS_DXT4: case ImageEngineFormat.DDS_DXT5: DecompressBCBlock = DDS_Decoders.DecompressBC3Block; break; case ImageEngineFormat.DDS_ATI1: DecompressBCBlock = DDS_Decoders.DecompressATI1; break; case ImageEngineFormat.DDS_ATI2_3Dc: DecompressBCBlock = DDS_Decoders.DecompressATI2Block; break; } MipMaps = new MipMap[estimatedMips]; // KFreon: Read mipmaps if (ImageFormats.IsBlockCompressed(format)) // Threading done in the decompression, not here. { for (int m = 0; m < estimatedMips; m++) { // KFreon: If mip is too small, skip out. This happens most often with non-square textures. I think it's because the last mipmap is square instead of the same aspect. if (mipWidth <= 0 || mipHeight <= 0) // Needed cos it doesn't throw when reading past the end for some reason. { break; } MipMap mipmap = ReadCompressedMipMap(compressed, mipWidth, mipHeight, blockSize, mipOffset, (format == ImageEngineFormat.DDS_DXT2 || format == ImageEngineFormat.DDS_DXT4), DecompressBCBlock); MipMaps[m] = mipmap; mipOffset += (int)(mipWidth * mipHeight * (blockSize / 16d)); // Also put the division in brackets cos if the mip dimensions are high enough, the multiplications can exceed int.MaxValue) mipWidth /= 2; mipHeight /= 2; } } else { int startMip = orig_estimatedMips - estimatedMips; // UNCOMPRESSED - Can't really do threading in "decompression" so do it for the mipmaps. var action = new Action<int>(mipIndex => { // Calculate mipOffset and dimensions int offset, width, height; offset = GetMipOffset(mipIndex, format, header.Width, header.Height); double divisor = mipIndex == 0 ? 1d : 2 << (mipIndex - 1); // Divisor represents 2^mipIndex - Math.Pow seems very slow. width = (int)(header.Width / divisor); height = (int)(header.Height / divisor); MipMap mipmap = null; try { mipmap = ReadUncompressedMipMap(compressed, offset, width, height, header.ddspf); } catch (Exception e) { Debug.WriteLine(e.ToString()); } MipMaps[mipIndex] = mipmap; }); if (ImageEngine.EnableThreading) Parallel.For(startMip, orig_estimatedMips, new ParallelOptions { MaxDegreeOfParallelism = ImageEngine.NumThreads }, action); else for (int i = startMip; i < orig_estimatedMips; i++) action(i); } List<MipMap> mips = new List<MipMap>(MipMaps.Where(t => t != null)); if (mips.Count == 0) throw new InvalidOperationException($"No mipmaps loaded. Estimated mips: {estimatedMips}, mip dimensions: {mipWidth}x{mipHeight}"); return mips; }
internal static int WriteUncompressed(byte[] source, byte[] destination, int destStart, DDS_Header.DDS_PIXELFORMAT ddspf) { int byteCount = ddspf.dwRGBBitCount / 8; byte signedAdjust = (ddspf.dwFlags & DDS_Header.DDS_PFdwFlags.DDPF_SIGNED) == DDS_Header.DDS_PFdwFlags.DDPF_SIGNED ? SignedAdjustment : (byte)0; bool oneChannel = (ddspf.dwFlags & DDS_Header.DDS_PFdwFlags.DDPF_LUMINANCE) == DDS_Header.DDS_PFdwFlags.DDPF_LUMINANCE; bool twoChannel = oneChannel && (ddspf.dwFlags & DDS_Header.DDS_PFdwFlags.DDPF_ALPHAPIXELS) == DDS_Header.DDS_PFdwFlags.DDPF_ALPHAPIXELS; uint AMask = ddspf.dwABitMask; uint RMask = ddspf.dwRBitMask; uint GMask = ddspf.dwGBitMask; uint BMask = ddspf.dwBBitMask; ///// Figure out channel existance and ordering. // Setup array that indicates channel offset from pixel start. // e.g. Alpha is usually first, and is given offset 0. // NOTE: Ordering array is in ARGB order, and the stored indices change depending on detected channel order. // A negative index indicates channel doesn't exist in data and sets channel to 0xFF. List<uint> maskOrder = new List<uint>(4) { AMask, RMask, GMask, BMask }; maskOrder.Sort(); maskOrder.RemoveAll(t => t == 0); // Required, otherwise indicies get all messed up when there's only two channels, but it's not indicated as such. int AIndex = 0; int RIndex = 0; int GIndex = 0; int BIndex = 0; // Set default ordering AIndex = AMask == 0 ? -1 : maskOrder.IndexOf(AMask); RIndex = RMask == 0 ? -1 : maskOrder.IndexOf(RMask); GIndex = GMask == 0 ? -1 : maskOrder.IndexOf(GMask); BIndex = BMask == 0 ? -1 : maskOrder.IndexOf(BMask); for (int i = 0; i < source.Length; i+=4, destStart += byteCount) { byte blue = (byte)(source[i] + signedAdjust); byte green = (byte)(source[i + 1] + signedAdjust); byte red = (byte)(source[i + 2] + signedAdjust); byte alpha = (byte)(source[i + 3]); if (twoChannel) { destination[destStart] = AMask > RMask ? red : alpha; destination[destStart + 1] = AMask > RMask ? alpha : red; } else if (oneChannel) destination[destStart] = (byte)(blue * 0.082 + green * 0.6094 + blue * 0.3086); // Weightings taken from ATI Compressonator. Dunno if this changes things much. else { if (AMask != 0) destination[destStart + AIndex] = alpha; if (RMask != 0) destination[destStart + RIndex] = red; if (GMask != 0) destination[destStart + GIndex] = green; if (BMask != 0) destination[destStart + BIndex] = blue; } } return destStart; }