Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Discussion options

Hi, I'm trying to implement my own format(s) as extensions using the ITextureFileFormat interface.
For now im stuck trying to write the Block Compressed data and the mipmaps yet i cant seem to find where the magick happens. I saw the MipMapper class yet it looks like it only outputs RgbaFloat image data

Big thanks for help in advance

You must be logged in to vote

Replies: 2 comments · 8 replies

Comment options

By the title I'm assuming you're working with the 3.0 branch.

Depending on what you're wanting to achieve, there are a couple of places you should take a look at:

Texture formats | ktx, ktx2, dds, etc.

If you want to add a new texture format, such as Valve Texture Format (.vtf), which acts as a container for different types of pixel data (rgb32, bc1, bc2 etc.), that can be achieved by creating your own class that implements the ITextureFileFormat interface. As an example:

// Valve texture format file
public class VtfFile : ITextureFileFormat<VtfFile>
{
	public bool SupportsLdr => true;
	public bool SupportsHdr => true;
	public bool SupportsCubeMap => true;
	public bool SupportsMipMaps => true;
	public bool SupportsArrays => true;
	
	
	// Pixel and metadata members go here
	private VtfMetadata metadata;
	private VtfPixeldata pixeldata;
	
	public bool IsSupportedFormat(CompressionFormat format)
	{
		// return whether the given CompressionFormat is valid for this file type
	}

	public void FromTextureData(BCnTextureData textureData)
	{
		// Take the raw pixel data from BCnTextureData class, and convert it to a Valve Texture Format specific container
	}

	public BCnTextureData ToTextureData()
	{
		// Inverse of the above operation. Take a look at KtxFile.cs for an example
	}

	public void ReadFromStream(Stream inputStream)
	{
		// Read raw bytes from a file stream and populate the metadata and pixeldata fields
	}

	public void WriteToStream(Stream outputStream)
	{
		// Write raw bytes to the file stream in the correct way for vtf files
	}
}

Then you can use the new format like so:

public void EncodeVtfFile()
{
	using Image<Rgba32> image = Image.Load<Rgba32>("example.png");

	BcEncoder encoder = new BcEncoder();

	encoder.OutputOptions.GenerateMipMaps = true;
	encoder.OutputOptions.Quality = CompressionQuality.Balanced;
	encoder.OutputOptions.Format = CompressionFormat.Bc1;

	// Open a file to write to
	using FileStream fs = File.OpenWrite("example.vtf");

	// Create a BCnTextureData with the compressed data
	BCnTextureData textureData = encoder.Encode(image);

	// Convert BCnTextureData to Vtf file
	VtfFile valveFile = new VtfFile();
	valveFile.FromTextureData(textureData);

	// Write to file
	valveFile.WriteToStream(fs);

	// Or, all of the above in one operation
	// encoder.EncodeToStream<VtfFile>(image, fs);
}

public void DecodeVtfFile()
{
	using FileStream fs = File.OpenRead("compressed_bc1.ktx");

	BcDecoder decoder = new BcDecoder();

	// Read vtf file from stream
	VtfFile valveFile = ITextureFileFormat<VtfFile>.Read(fs);

	// Convert to BCnTextureData
	BCnTextureData encodedData = valveFile.ToTextureData();

	// Decode to raw Rgba32 data
	BCnTextureData decodedData = decoder.Decode(encodedData, CompressionFormat.Rgba32);

	// Get raw pixel data as bytes
	byte[] rawByteData = decodedData.Mips[0][0,0].Data;

	// Save to png
	decodedData.AsImageRgba32().SaveAsPng("output.png");

	// Or easier like this
	// using Image<Rgba32> image = decoder.Decode<VtfFile>(fs).AsImageRgba32();
	// image.SaveAsPng("output.png");
}

Pixel formats | Rgba32, Bc1, ETC1, ASTC etc.

If you want to add one of these you need to first add a new enum for it in the CompressionFormat enum, then add an entry in the CompressionFormatInfo.Infos static dictionary.

This is a bit more involved, so take a look at how the other formats are implemented, like BC1 for block compressed formats, and Rgba32 for raw pixel formats.

If it's a block compressed format, you need a encoder and decoder class for it. These should be added to the BcEncoder.GetEncoder and BcDecoder.GetDecoder methods respectively.

Raw pixel formats just work (TM) without encoder/decoder classes, if they're setup correctly.

For example below is the encoder and decoder classes for ATC (Adreno Texture Compression), which is just BC1 with extra steps.

[StructLayout(LayoutKind.Sequential)]
internal struct AtcBlock
{
	public ColorB5G5R5M1Packed color0;
	public ColorB5G6R5Packed color1;
	public uint colorIndices;

	public int this[int index]
	{
		readonly get => (int)(colorIndices >> (index * 2)) & 0b11;
		set
		{
			colorIndices = (uint)(colorIndices & ~(0b11 << (index * 2)));
			var val = value & 0b11;
			colorIndices = colorIndices | ((uint)val << (index * 2));
		}
	}

	public readonly RawBlock4X4RgbaFloat Decode()
	{
		var output = new RawBlock4X4RgbaFloat();
		var pixels = output.AsSpan;

		var color0 = this.color0.ToColorRgbaFloat();
		var color1 = this.color1.ToColorRgbaFloat();

		int mode = this.color0.Mode;

		Span<ColorRgbaFloat> colorsMode1 = stackalloc ColorRgbaFloat[] {
			new ColorRgbaFloat(0, 0, 0),
			color0.InterpolateFourthAtc(color1, 1),
			color0,
			color1
		};

		for (var i = 0; i < pixels.Length; i++)
		{
			var colorIndex = this[i];

			var color = mode == 0 ? color0.InterpolateThird(color1, colorIndex) : colorsMode1[colorIndex];

			pixels[i] = color;
		}
		return output;
	}
}

internal unsafe class AtcBlockEncoder : BaseBcBlockEncoder<AtcBlock, RgbEncodingContext>
{
	private static readonly Bc1BlockEncoder bc1BlockEncoder = new Bc1BlockEncoder(false, false);


	public AtcBlockEncoder()
	{
	}

	public override AtcBlock EncodeBlock(in RgbEncodingContext context)
	{
		var atcBlock = new AtcBlock();

		// EncodeBlock with BC1 first
		Bc1Block bc1Block = bc1BlockEncoder.EncodeBlock(context);

		// Atc specific modifications to BC1
		// According to http://www.guildsoftware.com/papers/2012.Converting.DXTC.to.ATC.pdf

		// Change color0 from rgb565 to rgb555 with method 0
		atcBlock.color0 = new ColorB5G5R5M1Packed(bc1Block.color0.R, bc1Block.color0.G, bc1Block.color0.B);
		atcBlock.color1 = bc1Block.color1;

		// Remap color indices from BC1 to ATC
		var remap = stackalloc byte[] { 0, 3, 1, 2 };
		for (var i = 0; i < 16; i++)
		{
			atcBlock[i] = remap[bc1Block[i]];
		}

		 return atcBlock;
	}
}

internal class AtcDecoder : BaseBcBlockDecoder<AtcBlock>
{
	protected override RawBlock4X4RgbaFloat DecodeBlock(AtcBlock block)
	{
		return block.Decode();
	}
}

For block-compressed formats, the encoder takes in a block of raw pixel values (4x4 only, for now) and outputs a struct that represents the internal data for that type of compressed block. These structs have the same memory layout as the block would have in file or gpu memory, so you can write these structs directly to the file or byte stream.

The decoder takes the compressed block and converts it to a 4x4 block of raw float rgb data.

Everything else is automagically handled by the rest of the code, like mipmaps, pixel preprocessing, alpha premultiplication etc.

Let me know if you need more help!

You must be logged in to vote
4 replies
@wick3nd
Comment options

Thanks for sharing the workings of the encoder but my question regarding writing the encoded image data was unanswered, i understand you could not see the first message though lol.

I want to know what functions generate the mipmap images and what makes them turn into BC compressed data.

I saw that KtxFile uses its own functions for it but i cant figure out where the downscaling happens, the DdsFile on the other hand uses a MipMapper but it forces the mipmaps to be written in RgbaFloat

@Nominom
Comment options

Ah, sorry.

Not on the computer today so can't point to specific files or code, but the whole process of encoding goes something like this:

  • You have input pixel data either as raw byte array or inside a BcnTextureData class in any of the supported raw pixel formats, and call one of the BcEncoder.*Encode methods with it
  • The BcEncoder class inside one of the *Impl methods converts this data to RgbaFloats, which works as the intermediary format for the encoding
  • If MipMaps are enabled, the method calls the MipMapper class to genrate MipMaps from the raw float data
  • Each MipMap is then arranged into an array of 4x4 pixel blocks, still in raw float rgba format
  • The BcEncoder finds the right block encoder for the format, and feeds each block of each mipmap to the format specific encoder, which outputs the compressed data
  • The data is gathered to a new BcnTextureData object, which contains the block compressed data for each mipmap, cubemap face, array index etc.
  • The encoding process stops here, and outputs the finished BcnTextureData.
  • This object can then be tranformed into different file formats with the ITextureFileFormat interface
  • The chosen file format can then be written to a filestream or elsewhere as desired

Any tranformations inside KtxFile or DdsFile are purely arranging the data in the way that the file format desires. All MipMapping and encoding happens inside the BcEncoder class.

@wick3nd
Comment options

I need to ask if i understand this right
The BCnTextureData in FromTextureData already contains the compressed texture alongside its lower mipmap levels?
Im trying to get something working in OpenGL and the only thing i see is black, it really worries me as i wanted it to just be a short side side project 😅

@Nominom
Comment options

Yeah, the BcnTextureData has all the data needed for the texture, already compressed, along with any mipmaps, cubemap faces, array levels etc.

The way to access the different levels is via bcnTexData.Mips[n][0,0], where n is the mip level, and the 0s are cubemap face index and array index.

If you want I can take a look at the code you have, if it's up somewhere. Or you can paste it here?

Comment options

I really dont know what could be the issue, the BC1 data seems fine to me but OGL thinks its just black
Setx.zip - it contains the code and the compressed file

You must be logged in to vote
4 replies
@Nominom
Comment options

The code writing the file data seems fine to me. One thing that might be the issue is that the filtering is set to NearestMipmapNearest, while the included file.setx only has a single MipMap level inside, both in the header struct and the actual zstd compressed blob. OpenGL might not be able to render the texture if the filtering is set to use MipMaps while none are available.

By default, if you call BcEncoder.Encode without touching any MipMap options, the MipMaps should be present in the generated BCnTextureData. Did you call Encode with some other options, or is it not generating the MipMaps as it should?

@wick3nd
Comment options

Yes, it encodes as it should. i wanted to test if my importer did in fact extract the mip levels right

            var x = new BcEncoder();
            x.OutputOptions.MaxMipMapLevel = 1;
            x.EncodeToStream<SetxFile>(image, fs);
@Nominom
Comment options

Right, then I reckon the issue is something to do with how the texture is imported to OpenGL. Possibly the MinFiltering setting?

MinFilter = NumberOfMipLevels > 1 Texture_MinFiltering.NearestMipmapNearest : Texture_MinFiltering.Nearest,

The file otherwise looks fine, and the bc1 data decodes to a sensible texture.

@wick3nd
Comment options

Surprisingly, even though i tried changing the filter mode earlier, now it actually did work and render the image
OpenGL is truly mysterious sometimes
Thanks for help

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
2 participants
Morty Proxy This is a proxified and sanitized view of the page, visit original site.