C# Encode Bitmap to DDS (DXT1, DXT3, DXT5)

692 views Asked by At

I have a simple Bitmap in my app. I need to save it as DDS (Direct Draw Surface) not like a simple image with .dds extension, but need to encode pixel data and write it to .bin file. So, regarding to MSDN Documentation (Programming Guide for DDS,DDS_HEADER structure, DDS_PIXELFORMAT structure) DDS images consists from signature, header, pixel format and encoded pixels data (main surface and then other surfaces - isn't it?). Ok, so what I'm trying to do:

private const uint DDS_SIGNATURE = 0x20534444;
private const uint DDPF_FOURCC = 0x00000004;

private const int DDSD_CAPS = 0x00000001;
private const int DDSD_HEIGHT = 0x00000002;
private const int DDSD_WIDTH = 0x00000004;
private const int DDSD_PITCH = 0x00000008;
private const int DDSD_PIXELFORMAT = 0x00001000;
private const int DDSD_MIPMAPCOUNT = 0x00020000;
private const int DDSD_LINEARSIZE = 0x00080000;

private const int DDSCAPS_COMPLEX = 0x00000008;
private const int DDSCAPS_TEXTURE = 0x00001000;
private const int DDSCAPS_MIPMAP = 0x00400000;

private const uint FOURCC_DXT1 = 0x31545844;
private const uint FOURCC_DXT2 = 0x32545844;
private const uint FOURCC_DXT3 = 0x33545844;
private const uint FOURCC_DXT4 = 0x34545844;
private const uint FOURCC_DXT5 = 0x35545844;

private static byte[] GetDDSImageBytes(Bitmap image, EDDSCompressionMode format)
{
    if (image is null ||
        (format != EDDSCompressionMode.DXT1
            && format != EDDSCompressionMode.DXT2
            && format != EDDSCompressionMode.DXT3
            && format != EDDSCompressionMode.DXT4
            && format != EDDSCompressionMode.DXT5))
    {
        return null;
    }
    
    var capsMipMap = 0;
    var capsComplex = 0;
    var mipMapsCount = 0;
    var mipMaps = new List<Bitmap>();

    if (format == EDDSCompressionMode.DXT2 || format == EDDSCompressionMode.DXT3) // let's imagine I need MipMaps only for these formats
    {
        capsMipMap = DDSCAPS_MIPMAP;
        capsComplex = DDSCAPS_COMPLEX;
        mipMapsCount = DDSD_MIPMAPCOUNT;

        while (true)
        {
            var width = Math.Max(1, image.Width >> 1);
            var height = Math.Max(1, image.Height >> 1);

            if (width == 1 && height == 1)
            {
                break;
            }

            mipMaps.Add(CreateMipMap(image, width, height));
        }
    }

    var header = new DDSHeader
    {
        Size = 124,
        Flags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT | mipMapsCount | DDSD_LINEARSIZE,
        Height = image.Height,
        Width = image.Width,
        PitchOrLinearSize = image.Width * image.Height, // I'm not sure it's a proper calculation, please correct me if I'm wrong
        Depth = 0,
        MipMapCount = mipMaps.Count,
        Caps = DDSCAPS_TEXTURE | capsMipMap | capsComplex,
        Caps2 = 0,
        Caps3 = 0,
        Caps4 = 0,
        Reserved2 = 0,
    };

    var pixelFormat = new DDSPixelFormat
    {
        Size = 32,
        Flags = DDPF_FOURCC,
        FourCC = format switch
        {
            EDDSCompressionMode.DXT1 => FOURCC_DXT1,
            EDDSCompressionMode.DXT2 => FOURCC_DXT2,
            EDDSCompressionMode.DXT3 => FOURCC_DXT3,
            EDDSCompressionMode.DXT4 => FOURCC_DXT4,
            EDDSCompressionMode.DXT5 or _ => FOURCC_DXT5,
        },
        RGBBitCount = 0,
        RBitMask = 0,
        GBitMask = 0,
        BBitMask = 0,
        ABitMask = 0
    };

    var data = new List<byte>(CreateHeaderAndFormat(header, pixelFormat));

    //TODO: Implement writing binary data

    return data.ToArray();
}

private static Bitmap CreateMipMap(Bitmap image, int width, int height)
{
    var bmp = new Bitmap(width, height);

    using var blitter = Graphics.FromImage(bmp);

    blitter.InterpolationMode = InterpolationMode.HighQualityBicubic;

    using var wrapMode = new ImageAttributes();

    wrapMode.SetWrapMode(WrapMode.TileFlipXY);
    blitter.DrawImage(image, new Rectangle(0, 0, width, height), 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, wrapMode);

    return bmp;
}

private static byte[] CreateHeaderAndFormat(DDSHeader header, DDSPixelFormat pixelFormat)
{
    var data = new List<byte>(BitConverter.GetBytes(DDS_SIGNATURE));

    data.AddRange(BitConverter.GetBytes(header.Size));
    data.AddRange(BitConverter.GetBytes(header.Flags));
    data.AddRange(BitConverter.GetBytes(header.Height));
    data.AddRange(BitConverter.GetBytes(header.Width));
    data.AddRange(BitConverter.GetBytes(header.PitchOrLinearSize));
    data.AddRange(BitConverter.GetBytes(header.Depth));
    data.AddRange(BitConverter.GetBytes(header.MipMapCount));

    for (var i = 0; i < 11; i++)
    {
        data.AddRange(BitConverter.GetBytes((uint)0));
    }

    data.AddRange(BitConverter.GetBytes(pixelFormat.Size));
    data.AddRange(BitConverter.GetBytes(pixelFormat.Flags));
    data.AddRange(BitConverter.GetBytes(pixelFormat.FourCC));
    data.AddRange(BitConverter.GetBytes(pixelFormat.RGBBitCount));
    data.AddRange(BitConverter.GetBytes(pixelFormat.RBitMask));
    data.AddRange(BitConverter.GetBytes(pixelFormat.GBitMask));
    data.AddRange(BitConverter.GetBytes(pixelFormat.BBitMask));
    data.AddRange(BitConverter.GetBytes(pixelFormat.ABitMask));

    data.AddRange(BitConverter.GetBytes(header.Caps));
    data.AddRange(BitConverter.GetBytes(header.Caps2));
    data.AddRange(BitConverter.GetBytes(header.Caps3));
    data.AddRange(BitConverter.GetBytes(header.Caps4));
    data.AddRange(BitConverter.GetBytes(header.Reserved2));

    return data.ToArray();
}

Some additional info:

public enum EDDSCompressionMode
{
    Unknown = 0,
    DXT1 = 1,
    DXT2 = 2,
    DXT3 = 3,
    DXT4 = 4,
    DXT5 = 5
}

public struct DDSHeader
{
    public int Size;
    public int Flags;
    public int Height;
    public int Width;
    public int PitchOrLinearSize;
    public int Depth;
    public int MipMapCount;
    public int[] Reserved1;
    public int Caps;
    public int Caps2;
    public int Caps3;
    public int Caps4;
    public int Reserved2;
}

public struct DDSPixelFormat
{
    public uint Size;
    public uint Flags;
    public uint FourCC;
    public uint RGBBitCount;
    public uint RBitMask;
    public uint GBitMask;
    public uint BBitMask;
    public uint ABitMask;
}

So the problem is that I'm not an expert regarding DDS images and I have no idea how to create a proper binary data from my Bitmap. Looked for some external libraries, but seems like all of them directed to save .dds file, not to return encoded binary data of pixels.

Another thing which I found is implementation of reading the data from the file (and it's working properly!): EQZip. Seems like I can only rewrite (i.e. invert) DXT1, DXT3 and DXT5 methods from link above to get the encoded data, but again - I have no idea how to do it properly.

Any suggestions, ideas, advices, tips, etc.?

0

There are 0 answers