I'm looking to encode a 3-float normal to something smaller for passing to a compute shader, and "signed octahedron normal encoding" appears to be the standard (according to the addendum at the top of Aras P's article). Following the last link takes us to a Blogspot Article from 2017 which notes an adjustment to a previous octahedral encoding method which reportedly increases performance while doubling precision. Great! Exactly what we want.
Only, I'm not totally sure how to use it.
The code in question is:
float3 SignedOctEncode( float3 n )
{
float3 OutN;
n /= ( abs( n.x ) + abs( n.y ) + abs( n.z ) );
OutN.y = n.y * 0.5 + 0.5;
OutN.x = n.x * 0.5 + OutN.y;
OutN.y = n.x * -0.5 + OutN.y;
OutN.z = saturate(n.z*FLT_MAX);
return OutN;
}
float3 SignedOctDecode( float3 n )
{
float3 OutN;
OutN.x = (n.x - n.y);
OutN.y = (n.x + n.y) - 1.0;
OutN.z = n.z * 2.0 - 1.0;
OutN.z = OutN.z * ( 1.0 - abs(OutN.x) - abs(OutN.y));
OutN = normalize( OutN );
return OutN;
}
Octahedral encodings should produce two components, and this article from John White/Anonymous appears to suggest if we add an extra bit (or two) we can achieve better performance in decoding and gain increased precision at the same time. That seems to track since we're storing what looks like [0..1] ranges in the x and y components.
My questions then are:
What is the correct way to convert the x,y components to something which can be packed into an int? I want to say multiply by
2^n - 1
where n is the number of bits we're encoding per component. Then when unpacking in the shader, divide by that same value to get back to the normalised range.How does
OutN.z = saturate(n.z*FLT_MAX);
work? Or more precisely, why is multiplying by FLT_MAX necessary? Depending on if I multiply a test normal by -1 it results in z being 1 or 0.saturate
is equivalent toclamp(x, 0, 1)
so if x is negative you'll always get 0. Is it to push the value high enough that it'll clamp back to 1, even for small values of positive x?
Using the 10:10:10:2 format mentioned at the top of the article, would this be a correct solution?
float3 encoded = SignedOctEncode(normal);
uint32 x10 = uint32(encoded.x * 0x3ff) & 0x3ff;
uint32 y10 = uint32(encoded.y * 0x3ff) & 0x3ff;
uint32 z2 = uint32(encoded.z) & 3;
uint32 packed = (x10 << 12 | y10 << 2 | z2)
Which leaves 10 bits at the top for some other 10-bit component. I feel like I've probably answered my own questions in the process of writing this out, but a sanity check is appreciated.