using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using BCnEncoder.Encoder.Bc7; using BCnEncoder.Shared; namespace BCnEncoder.Encoder { public class EncoderInputOptions { /// /// If true, when encoding to a format that only includes a red channel, /// use the pixel luminance instead of just the red channel. Default is false. /// public bool luminanceAsRed = false; } public class EncoderOutputOptions { /// /// The compression format to use. Default is BC1. /// public CompressionFormat format = CompressionFormat.BC1; /// /// The quality of the compression. Use either fast or balanced for testing. /// Fast can be used for near real-time encoding for most algorithms. /// Use bestQuality when needed. Default is balanced. /// public CompressionQuality quality = CompressionQuality.Balanced; /// /// The output file format of the data. Either Ktx or Dds. /// Default is Ktx. /// public OutputFileFormat fileFormat = OutputFileFormat.Ktx; /// /// The DDS file format doesn't seem to have a standard for indicating whether a BC1 texture /// includes 1bit of alpha. This option will write DDPF_ALPHAPIXELS flag to the header /// to indicate the presence of an alpha channel. Some programs read and write this flag, /// but some programs don't like it and get confused. Your mileage may vary. /// Default is false. /// public bool ddsBc1WriteAlphaFlag = false; } public class EncoderOptions { /// /// Whether the blocks should be encoded in parallel. This can be much faster than single-threaded encoding, /// but is slow if multiple textures are being processed at the same time. /// When a debugger is attached, the encoder defaults to single-threaded operation to ease debugging. /// Default is true. /// public bool multiThreaded = true; } /// /// Handles all encoding of images into compressed or uncompressed formats. For decoding, /// public class BcEncoder { public EncoderInputOptions InputOptions { get; set; } = new EncoderInputOptions(); public EncoderOutputOptions OutputOptions { get; set; } = new EncoderOutputOptions(); public EncoderOptions Options { get; set; } = new EncoderOptions(); public BcEncoder() { } public BcEncoder(CompressionFormat format) { OutputOptions.format = format; } private IBcBlockEncoder GetEncoder(CompressionFormat format) { switch (format) { case CompressionFormat.BC1: return new Bc1BlockEncoder(); case CompressionFormat.BC1WithAlpha: return new Bc1AlphaBlockEncoder(); case CompressionFormat.BC2: return new Bc2BlockEncoder(); case CompressionFormat.BC3: return new Bc3BlockEncoder(); case CompressionFormat.BC4: return new Bc4BlockEncoder(InputOptions.luminanceAsRed); case CompressionFormat.BC5: return new Bc5BlockEncoder(); case CompressionFormat.BC7: return new Bc7Encoder(); default: return null; } } /// /// Encodes all mipmap levels into a ktx or a dds file and writes it to the output stream. /// public void Encode(byte[] inputImageRgba, int inputImageWidth, int inputImageHeight, Stream outputStream) { if (OutputOptions.fileFormat == OutputFileFormat.Ktx) { KtxFile output = EncodeToKtx(inputImageRgba, inputImageWidth, inputImageHeight); output.Write(outputStream); } else if (OutputOptions.fileFormat == OutputFileFormat.Dds) { DdsFile output = EncodeToDds(inputImageRgba, inputImageWidth, inputImageHeight); output.Write(outputStream); } } /// /// Encodes all mipmap levels into a Ktx file. /// public KtxFile EncodeToKtx(byte[] inputImageRgba, int inputImageWidth, int inputImageHeight) { KtxFile output; IBcBlockEncoder compressedEncoder = null; compressedEncoder = GetEncoder(OutputOptions.format); if (compressedEncoder == null) { throw new NotSupportedException($"This format is not supported: {OutputOptions.format}"); } output = new KtxFile( KtxHeader.InitializeCompressed(inputImageWidth, inputImageHeight, compressedEncoder.GetInternalFormat(), compressedEncoder.GetBaseInternalFormat())); uint numMipMaps = 1; byte[] encoded = null; if (OutputOptions.format.IsCompressedFormat()) { var blocks = ImageToBlocks.ImageTo4X4(inputImageRgba, inputImageWidth, inputImageHeight, out int blocksWidth, out int blocksHeight); encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.quality, !Debugger.IsAttached && Options.multiThreaded); } output.MipMaps.Add(new KtxMipmap((uint)encoded.Length, (uint)inputImageWidth, (uint)inputImageHeight, 1)); output.MipMaps[0].Faces[0] = new KtxMipFace(encoded, (uint)inputImageWidth, (uint)inputImageHeight); output.Header.NumberOfFaces = 1; output.Header.NumberOfMipmapLevels = numMipMaps; return output; } /// /// Encodes Rgba image into a Dds file. /// public DdsFile EncodeToDds(byte[] inputImageRgba, int inputImageWidth, int inputImageHeight) { DdsFile output; IBcBlockEncoder compressedEncoder = null; compressedEncoder = GetEncoder(OutputOptions.format); if (compressedEncoder == null) { throw new NotSupportedException($"This format is not supported: {OutputOptions.format}"); } DdsHeaderDxt10 dxt10Header; var ddsHeader = DdsHeader.InitializeCompressed(inputImageWidth, inputImageHeight, compressedEncoder.GetDxgiFormat(), out dxt10Header); output = new DdsFile(ddsHeader, dxt10Header); if (OutputOptions.ddsBc1WriteAlphaFlag && OutputOptions.format == CompressionFormat.BC1WithAlpha) { output.Header.ddsPixelFormat.dwFlags |= PixelFormatFlags.DDPF_ALPHAPIXELS; } uint numMipMaps = 1; var blocks = ImageToBlocks.ImageTo4X4(inputImageRgba, inputImageWidth, inputImageHeight, out int blocksWidth, out int blocksHeight); var encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.quality, !Debugger.IsAttached && Options.multiThreaded); output.Faces.Add(new DdsFace((uint)inputImageWidth, (uint)inputImageHeight, (uint)encoded.Length, (int)numMipMaps)); output.Faces[0].MipMaps[0] = new DdsMipMap(encoded, (uint)inputImageWidth, (uint)inputImageHeight); output.Header.dwMipMapCount = numMipMaps; if (numMipMaps > 1) { output.Header.dwCaps |= HeaderCaps.DDSCAPS_COMPLEX | HeaderCaps.DDSCAPS_MIPMAP; } return output; } /// /// Encodes an RGBA byte buffer to raw compressed bytes /// public byte[] EncodeToRawBytes(byte[] inputImageRgba, int inputImageWidth, int inputImageHeight, out int outputImageWidth, out int outputImageHeight) { IBcBlockEncoder compressedEncoder = GetEncoder(OutputOptions.format); if (compressedEncoder == null) { throw new NotSupportedException($"This format is not supported: {OutputOptions.format}"); } var blocks = ImageToBlocks.ImageTo4X4(inputImageRgba, inputImageWidth, inputImageHeight, out int blocksWidth, out int blocksHeight); var encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.quality, !Debugger.IsAttached && Options.multiThreaded); outputImageWidth = blocksWidth * 4; outputImageHeight = blocksHeight * 4; return encoded; } /// /// Encodes a single mip level of the input image to a byte buffer. /// //public byte[] EncodeToRawBytes(Image inputImage, int mipLevel, out int mipWidth, out int mipHeight) //{ // if (mipLevel < 0) // { // throw new ArgumentException($"{nameof(mipLevel)} cannot be less than zero."); // } // IBcBlockEncoder compressedEncoder = null; // IRawEncoder uncompressedEncoder = null; // if (OutputOptions.format.IsCompressedFormat()) // { // compressedEncoder = GetEncoder(OutputOptions.format); // if (compressedEncoder == null) // { // throw new NotSupportedException($"This format is not supported: {OutputOptions.format}"); // } // } // else // { // uncompressedEncoder = GetRawEncoder(OutputOptions.format); // } // uint numMipMaps = (uint)OutputOptions.maxMipMapLevel; // if (!OutputOptions.generateMipMaps) // { // numMipMaps = 1; // } // var mipChain = MipMapper.GenerateMipChain(inputImage, ref numMipMaps); // if (mipLevel > numMipMaps - 1) // { // foreach (var image in mipChain) // { // image.Dispose(); // } // throw new ArgumentException($"{nameof(mipLevel)} cannot be more than number of mipmaps"); // } // byte[] encoded = null; // if (OutputOptions.format.IsCompressedFormat()) // { // var blocks = ImageToBlocks.ImageTo4X4(mipChain[mipLevel].Frames[0], out int blocksWidth, out int blocksHeight); // encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.quality, // !Debugger.IsAttached && Options.multiThreaded); // } // else // { // if (!mipChain[mipLevel].TryGetSinglePixelSpan(out var mipPixels)) { // throw new Exception("Cannot get pixel span."); // } // encoded = uncompressedEncoder.Encode(mipPixels); // } // mipWidth = mipChain[mipLevel].Width; // mipHeight = mipChain[mipLevel].Height; // foreach (var image in mipChain) // { // image.Dispose(); // } // return encoded; //} /// /// Encodes all cubemap faces and mipmap levels into Ktx file and writes it to the output stream. /// Order is +X, -X, +Y, -Y, +Z, -Z /// //public void EncodeCubeMap(Image right, Image left, Image top, Image down, // Image back, Image front, Stream outputStream) //{ // if (OutputOptions.fileFormat == OutputFileFormat.Ktx) // { // KtxFile output = EncodeCubeMapToKtx(right, left, top, down, back, front); // output.Write(outputStream); // } // else if (OutputOptions.fileFormat == OutputFileFormat.Dds) // { // DdsFile output = EncodeCubeMapToDds(right, left, top, down, back, front); // output.Write(outputStream); // } //} /// /// Encodes all cubemap faces and mipmap levels into a Ktx file. /// Order is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. /// //public KtxFile EncodeCubeMapToKtx(Image right, Image left, Image top, Image down, // Image back, Image front) //{ // KtxFile output; // IBcBlockEncoder compressedEncoder = null; // IRawEncoder uncompressedEncoder = null; // if (right.Width != left.Width || right.Width != top.Width || right.Width != down.Width // || right.Width != back.Width || right.Width != front.Width || // right.Height != left.Height || right.Height != top.Height || right.Height != down.Height // || right.Height != back.Height || right.Height != front.Height) // { // throw new ArgumentException("All input images of a cubemap should be the same size."); // } // Image[] faces = new[] { right, left, top, down, back, front }; // if (OutputOptions.format.IsCompressedFormat()) // { // compressedEncoder = GetEncoder(OutputOptions.format); // if (compressedEncoder == null) // { // throw new NotSupportedException($"This format is not supported: {OutputOptions.format}"); // } // output = new KtxFile( // KtxHeader.InitializeCompressed(right.Width, right.Height, // compressedEncoder.GetInternalFormat(), // compressedEncoder.GetBaseInternalFormat())); // } // else // { // uncompressedEncoder = GetRawEncoder(OutputOptions.format); // output = new KtxFile( // KtxHeader.InitializeUncompressed(right.Width, right.Height, // uncompressedEncoder.GetGlType(), // uncompressedEncoder.GetGlFormat(), // uncompressedEncoder.GetGlTypeSize(), // uncompressedEncoder.GetInternalFormat(), // uncompressedEncoder.GetBaseInternalFormat())); // } // uint numMipMaps = (uint)OutputOptions.maxMipMapLevel; // if (!OutputOptions.generateMipMaps) // { // numMipMaps = 1; // } // uint mipLength = MipMapper.CalculateMipChainLength(right.Width, right.Height, numMipMaps); // for (uint i = 0; i < mipLength; i++) // { // output.MipMaps.Add(new KtxMipmap(0, 0, 0, (uint)faces.Length)); // } // for (int f = 0; f < faces.Length; f++) // { // var mipChain = MipMapper.GenerateMipChain(faces[f], ref numMipMaps); // for (int i = 0; i < numMipMaps; i++) // { // byte[] encoded = null; // if (OutputOptions.format.IsCompressedFormat()) // { // var blocks = ImageToBlocks.ImageTo4X4(mipChain[i].Frames[0], out int blocksWidth, out int blocksHeight); // encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.quality, // !Debugger.IsAttached && Options.multiThreaded); // } // else // { // if (!mipChain[i].TryGetSinglePixelSpan(out var mipPixels)) { // throw new Exception("Cannot get pixel span."); // } // encoded = uncompressedEncoder.Encode(mipPixels); // } // if (f == 0) // { // output.MipMaps[i] = new KtxMipmap((uint)encoded.Length, // (uint)mipChain[i].Width, // (uint)mipChain[i].Height, (uint)faces.Length); // } // output.MipMaps[i].Faces[f] = new KtxMipFace(encoded, // (uint)mipChain[i].Width, // (uint)mipChain[i].Height); // } // foreach (var image in mipChain) // { // image.Dispose(); // } // } // output.Header.NumberOfFaces = (uint)faces.Length; // output.Header.NumberOfMipmapLevels = mipLength; // return output; //} /// /// Encodes all cubemap faces and mipmap levels into a Dds file. /// Order is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. /// //public DdsFile EncodeCubeMapToDds(Image right, Image left, Image top, Image down, // Image back, Image front) //{ // DdsFile output; // IBcBlockEncoder compressedEncoder = null; // IRawEncoder uncompressedEncoder = null; // if (right.Width != left.Width || right.Width != top.Width || right.Width != down.Width // || right.Width != back.Width || right.Width != front.Width || // right.Height != left.Height || right.Height != top.Height || right.Height != down.Height // || right.Height != back.Height || right.Height != front.Height) // { // throw new ArgumentException("All input images of a cubemap should be the same size."); // } // Image[] faces = new[] { right, left, top, down, back, front }; // if (OutputOptions.format.IsCompressedFormat()) // { // compressedEncoder = GetEncoder(OutputOptions.format); // if (compressedEncoder == null) // { // throw new NotSupportedException($"This format is not supported: {OutputOptions.format}"); // } // var (ddsHeader, dxt10Header) = DdsHeader.InitializeCompressed(right.Width, right.Height, // compressedEncoder.GetDxgiFormat()); // output = new DdsFile(ddsHeader, dxt10Header); // if (OutputOptions.ddsBc1WriteAlphaFlag && // OutputOptions.format == CompressionFormat.BC1WithAlpha) // { // output.Header.ddsPixelFormat.dwFlags |= PixelFormatFlags.DDPF_ALPHAPIXELS; // } // } // else // { // uncompressedEncoder = GetRawEncoder(OutputOptions.format); // var ddsHeader = DdsHeader.InitializeUncompressed(right.Width, right.Height, // uncompressedEncoder.GetDxgiFormat()); // output = new DdsFile(ddsHeader); // } // uint numMipMaps = (uint)OutputOptions.maxMipMapLevel; // if (!OutputOptions.generateMipMaps) // { // numMipMaps = 1; // } // for (int f = 0; f < faces.Length; f++) // { // var mipChain = MipMapper.GenerateMipChain(faces[f], ref numMipMaps); // for (int mip = 0; mip < numMipMaps; mip++) // { // byte[] encoded = null; // if (OutputOptions.format.IsCompressedFormat()) // { // var blocks = ImageToBlocks.ImageTo4X4(mipChain[mip].Frames[0], out int blocksWidth, out int blocksHeight); // encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.quality, // !Debugger.IsAttached && Options.multiThreaded); // } // else // { // if (!mipChain[mip].TryGetSinglePixelSpan(out var mipPixels)) { // throw new Exception("Cannot get pixel span."); // } // encoded = uncompressedEncoder.Encode(mipPixels); // } // if (mip == 0) // { // output.Faces.Add(new DdsFace((uint)mipChain[mip].Width, (uint)mipChain[mip].Height, // (uint)encoded.Length, mipChain.Count)); // } // output.Faces[f].MipMaps[mip] = new DdsMipMap(encoded, // (uint)mipChain[mip].Width, // (uint)mipChain[mip].Height); // } // foreach (var image in mipChain) // { // image.Dispose(); // } // } // output.Header.dwCaps |= HeaderCaps.DDSCAPS_COMPLEX; // output.Header.dwMipMapCount = numMipMaps; // if (numMipMaps > 1) // { // output.Header.dwCaps |= HeaderCaps.DDSCAPS_MIPMAP; // } // output.Header.dwCaps2 |= HeaderCaps2.DDSCAPS2_CUBEMAP | // HeaderCaps2.DDSCAPS2_CUBEMAP_POSITIVEX | // HeaderCaps2.DDSCAPS2_CUBEMAP_NEGATIVEX | // HeaderCaps2.DDSCAPS2_CUBEMAP_POSITIVEY | // HeaderCaps2.DDSCAPS2_CUBEMAP_NEGATIVEY | // HeaderCaps2.DDSCAPS2_CUBEMAP_POSITIVEZ | // HeaderCaps2.DDSCAPS2_CUBEMAP_NEGATIVEZ; // return output; //} } }