Nominom/BCnEncoder.NET

Color interpolation a little bit off when decoding BC1-BC5

Nominom opened this issue · 10 comments

Colors are not interpolated correctly between the two endpoints compared to other decoders.

The colors are 1 or 2 255th off per channel because of floating point interpolation. This might be an issue.

Hi, I'm curious about how you measured this error.

Also, where did you learn the algorithms for BC1-7 encode?

Ah, I was comparing the output of Compressonator to my library's test images and found that there was a slight difference in some pixels. Upon further investigation I realized that the conversion of 5 to 8-bit and 6 to 8-bit colors was implemented differently in Compressonator, where as my library converts them using floating point math. The "correct" way is probably somewhere in the official spec too but I've just missed it. The output is so close that it wasn't really a huge issue anyways.

I learned most of it from the official Microsoft documentation and Khronos specs. Some of the specifics I had to dig out from various blog posts and documents online.

Here's some links if you're curious:
https://docs.microsoft.com/en-us/windows/win32/direct3d10/d3d10-graphics-programming-guide-resources-block-compression
https://docs.microsoft.com/en-us/windows/win32/direct3d11/bc7-format
https://www.khronos.org/registry/DataFormat/specs/1.1/dataformat.1.1.html

Thanks, I had seen those Microsoft docs, but the Khronos one is new to me.

The part I'm looking for is an explanation of how to pick the endpoint colors for a block during encode. I guess that's the part you learned from various resources online. I'll probably just have to search harder.

You should look into principal component analysis for finding the endpoints. Especially the eigenvectors. Maybe find some library that does PCA for you, since it's a bit complicated to implement from scratch.

I'm interested in this phenomenon.
Can you share example RGB565 to RGB888 value pairs? So what your library produces vs. what compressonator produces (and is seemingly more correct)?
I have my own conversion from 8-bit to n-bit and vice versa and would like to see if my code has the same problem. If not, maybe it can aid you into fixing the problem in your library?

I compared a test image decoded with both BCnEncoder and Compressonator and also attached the related source code from both programs. Seems like the endpoints are fine, but the interpolated colors are calculated differently.

test_decompress_bc1.dds:
block 12,7

Name		   =  c0               c2               c3              c1
BcnEncoder     = (222, 146, 140), (208, 121, 120), (194, 96, 100), (181, 73, 82)
Compressonator = (222, 146, 140), (208, 121, 120), (194, 97, 101), (181, 73, 82)

// Compressonator code
CGU_UINT32 r0; 
CGU_UINT32 g0; 
CGU_UINT32 b0; 
r0 = ((n565 & 0xf800) >> 8); 
g0 = ((n565 & 0x07e0) >> 3); 
b0 = ((n565 & 0x001f) << 3); 

// Apply the lower bit replication to give full dynamic range (5,6,5) 
r0 += (r0 >> 5); 
g0 += (g0 >> 6); 
b0 += (b0 >> 5);

CGU_UINT32 c0 = 0xff000000 | (r0 << 16) | (g0 << 8) | b0;
CGU_UINT32 c1 = 0xff000000 | (r1 << 16) | (g1 << 8) | b1;
CGU_UINT32 c2 = 0xff000000 | (((2 * r0 + r1) / 3) << 16) | (((2 * g0 + g1) / 3) << 8) | (((2 * b0 + b1) / 3)); 
CGU_UINT32 c3 = 0xff000000 | (((2 * r1 + r0) / 3) << 16) | (((2 * g1 + g0) / 3) << 8) | (((2 * b1 + b0) / 3));


// BcnEncoder.NET code
ColorRgb565.R => {
	int r5 = ((data & RedMask) >> RedShift);
	return (byte)((r5 << 3) | (r5 >> 2));
}
ColorRgb565.G => {
	int g6 = ((data & GreenMask) >> GreenShift);
	return (byte)((g6 << 2) | (g6 >> 4));
}
ColorRgb565.B => {
	int b5 = (data & BlueMask);
	return (byte)((b5 << 3) | (b5 >> 2));
}

color0 = new ColorRgb24(c0.R, c0.G, c0.B);
color1 = new ColorRgb24(c1.R, c1.G, c1.B);

ColorRgb24[] colors =
	new ColorRgb24[] {
		color0,
		color1,
		color0 * (2.0 / 3.0) + color1 * (1.0 / 3.0),
		color0 * (1.0 / 3.0) + color1 * (2.0 / 3.0)
	};
	
// Compressonator c3.g

c3.g = (73 * 2 + 146) / 3 = 97

// BcnEncoder.NET c3.g

c3.g = 146 * (1.0 / 3.0) + 73 * (2.0 / 3.0) = 
	   48                + 48               = 96

A relatively easy fix would be to add a function like ColorRgb24.InterpolateTwoThirds(ColorRgb24 c0, ColorRgb24 c1) that would have the same calculation as Compressonator for each of the color channels.

Ah, compressonator indeed does it like I do actually. I made it more generic, but overall it's the same approach.
Use the integer n of n/3 directly, and not calculate it by floating point operations. I can add that to my PR, if you wish?

private int Interpolate(int a, int b, int num, float den) =>
    (int)((num * a + (den - num) * b) / den);

Where den is the divisor (3), and num is the dividend (1, 2, 3) if we work with 4 color endpoints here.
a is the component of the lower endpoint. b is the component of the higher endpoint.

If you want to add it, go ahead.

Alright, I'm on it.