Packing Normal and Specular into RGB10A2

Trying to improve my g-buffer layout (so I can fit it in the X360’s 10 MiB EDRAM)
Spherical Coordinates with Phi in 8 bits and Normal.Y in 8 bits looks terrible.
So, thanks to a suggestion by doesnotcompute on gamedev forums, I tried my hand at this packing:

Red = abs(Phi)
Green = abs(N.y)
Blue = Specular I & E combined (5 bits each)
Alpha = sign(Phi) & sign(N.y) combined

This gives 11 bits for Phi and N.y, which is a huge improvement.
Specular has suffered a bit though …

Here is the HLSL source code:

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
float PackIEto10bit(in float I, in float E) {
    return (floor(I * 31) * 32 + E * 31) / 1023.0;
}//function
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void UnpackIEfrom10bit(in float V, out float I, out float E) {

    float t1;
    float t2;
    
    t1  = V * 1023;
    t2  = floor(t1 / 32.0);
    I   = t2 / 31.0;
    E   = (t1 - t2 * 32) / 31.0;
    
}//function
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
float4 PackNIEtoRGB10A2(in float3 N, in float I, in float E) {

    float4  color;
    float   phi;

    // Map E
    E           = saturate(log10(E) - 1);

    phi         = atan2(N.x, N.z); // Note! This gives NaN when x = z = 0
    
    color.r     = abs(phi) / Pi;
    color.g     = abs(N.y);
    color.b     = PackIEto10bit(I, E);
    
    float a1    = saturate(sign(phi)) * 0.3333;
    float a2    = saturate(sign(N.y)) * 0.6666;
    
    color.a     = a1 + a2;
    
    return color;
    
}//function
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// R = abs(phi)
// G = abs(Y)
// B = IE
// A = sign(phi) + sign(Y)
float3 UnpackNIEfromRGB10A2(in float4 color, out float I, out float E) {

    UnpackIEfrom10bit(color.b, I, E);
    
    // Map E
    E = pow(10, E + 1);
    
    float signPhi   = sign(frac(color.a * 1.9) - 0.5);
    float signY     = sign(color.a - 0.5);
    float phi       = color.r * Pi * signPhi;
    float Y         = color.g * signY;
    
    float2 N;
    sincos(phi, N.x, N.y);
    
    const float threshold = 1021.0 / 1023.0;
    
    if (color.g > threshold) N = 0.0; // bodge up for singularity

    float L = sqrt(1.0 - Y * Y);
    
    return float3(N * L, Y).xzy;
    
}//function
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Ideas for improvements

Because there is only a limited range of view space normals with negative Z that are visible … I can increase normal precision by mapping phi to the visible range.

Also some gamma encoding on the specular intensity would also improve things.

(edit) Mapping the range of Phi by 1.0 / 0.64 before packing gives an extra 56% precision. Using a 45 degree FOV with 1.777 aspect means only normals facing away by up to 36.3 degrees are visible. As I am using LH coordinates that means I also need to flip Y, atan2(x, -y).

(edit) Simple gamma encoding for Specular I. For packing: I = sqrt(I), for unpacking I *= I;

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: