LogLuv Encoding For HDR

Started by
14 comments, last by MJP 15 years, 9 months ago
For my recent project (targeting the 360 and PC through XNA) I've been doing some experimenting with alternative formats for HDR. If I were just doing PC I'd probably just use fp16 and be done with it, but for the 360 I've been trying out some alternatives since fp16 has some disadvantages on that platfrom (and since the special fp10 frame buffer format isn't available through XNA [flaming]) Anyway my most recent adventure has been with LogLuv encoding (Heavenly Sword's famous NAO32 technique). I've managed to come up with a working implmentation based on Christer Ericson's optimized Cg code, however I've been getting artifacts in the final result: I'm guessing that I'm running into discontinuites somewhere...however I can't seem to figure out where. If anyone has any experience with this and could point in the right direction, I'd be eternally grateful. This is the shader code I'm using:

// M matrix, for encoding
const static float3x3 M = float3x3(
    0.2209, 0.3390, 0.4184,
    0.1138, 0.6780, 0.7319,
    0.0102, 0.1130, 0.2969);

// Inverse M matrix, for decoding
const static float3x3 InverseM = float3x3(
	6.0013,	-2.700,	-1.7995,
	-1.332,	3.1029,	-5.7720,
	.3007,	-1.088,	5.6268);	

float4 LogLuvEncode(in float3 vRGB) 
{		 
    float4 vResult; 
    float3 Xp_Y_XYZp = mul(vRGB, M);
    Xp_Y_XYZp = max(Xp_Y_XYZp, float3(1e-6, 1e-6, 1e-6));
    vResult.xy = Xp_Y_XYZp.xy / Xp_Y_XYZp.z;
    float Le = 2 * log2(Xp_Y_XYZp.y) + 128;
    vResult.z = Le / 256;
    vResult.w = frac(Le);
    return vResult;
}

float3 LogLuvDecode(in float4 vLogLuv)
{	
	float Le = vLogLuv.z * 256 + vLogLuv.w;
	float3 Xp_Y_XYZp;
	Xp_Y_XYZp.y = exp2((Le - 128) / 2);
	Xp_Y_XYZp.z = Xp_Y_XYZp.y / vLogLuv.y;
	Xp_Y_XYZp.x = vLogLuv.x * Xp_Y_XYZp.z;
	float3 vRGB = mul(Xp_Y_XYZp, InverseM);
	return max(vRGB, 0);
}

I've tried playing around with clamping the values of Xp_Y_XYZp to 1e-6 and also the value of vLogLuv.y before division, but it hasn't gotten rid of the artifacts. I know Marco (Heavenly Sword dev) mentioned you had be careful to avoid carry problems if you're linearly filtering, however I'm getting these artifacts even when I just do a straight decode of the whole render target without bloom or tone-mapping. So yeah...I'm basically stumped. Any mathematical geniuses out there who can help out a poor soul? [smile] By the way, this is the original paper describing the LogLuv format.
Advertisement

(Sorry for the awkward read, this post kinda evolved as I tinkered along :)

I haven't worked with this myself yet, but from those resources you posted I couldn't see anything wrong with your implementation. I fixed up a little app to test your LogLuv encoding/decoding function on a plain image (just a pixel shader doing RGB->LogLuv->RGB) and this was the result:



According to the original LogLuv paper, RGB->LogLuv->RGB should yield an almost identical bitmap (p4, 4.4), so I'd guess there's indeed something wrong with either the encoding/decoding or my copy-paste skills. I'm not sure if my results are even remotely conclusive though (does it make sense to render decoded RGB imagery without tonemapping?).

Anyway, in the comments section of Christers blog NAO posted his approach and I noticed he went about packing the MSBs of Le differently than Christer. Makes sense too, since Christers implementation seems to depend on precission limitations to clamp the MSB. NAO on the other hand explicitly clamps this part of Le. Long story short, changing these lines in your encoding

vResult.z = Le / 256;vResult.w = frac(Le);


to this

vResult.w = frac(Le);vResult.z = (Le - (floor(vResult.w*256.0f))/256.0f)/256.0f;


stops the color bleeding in my test app and might also solve your artifacts.
Rim van Wersch [ MDXInfo ] [ XNAInfo ] [ YouTube ] - Do yourself a favor and bookmark this excellent free online D3D/shader book!
looks like skyroads? sorry to be offtopic, but i really love skyroads, so the picture (which looks great btw) got me interested
If that's not the help you're after then you're going to have to explain the problem better than what you have. - joanusdmentia

My Page davepermen.net | My Music on Bandcamp and on Soundcloud

Thanks for taking some time to investigate Remigius, you're a prince. [smile] I'll try that out when I get home, see what results I get. Even if it just eliminates some of that banding it'll be some good progress.

Quote:Original post by davepermen
looks like skyroads? sorry to be offtopic, but i really love skyroads, so the picture (which looks great btw) got me interested


Nice catch there! The game I'm making is actually going to be something of a Skyroads clone.


NP, I came across LogLuv on Wolfs blog and didn't quite know what to make of it, so I'm glad I had a chance to tinker with it. For future reference, using NAO's approach completely removes all banding and there is no longer any visible difference between the original RGB image and the RGB -> LogLuv -> RGB one. Maybe Christers trick (Le / 256) only works on PS3 hardware?

Now let's hope it solves you artifacts as well [smile]
Rim van Wersch [ MDXInfo ] [ XNAInfo ] [ YouTube ] - Do yourself a favor and bookmark this excellent free online D3D/shader book!
Hmm...using that method seems to work alright when rendering to a fp16 surface, but I still get the same artifacts and banding with A8R8G8B8. I suppose it's a precision problem then...but I'm not sure why I'm having one. Oh well, more investigation is needed...

Thank you anyway, remigius.

Ugh, I was thinking it should work with ARGB surfaces since that's what my app is using, but obviously there aren't any precission issues when doing the RGB -> LogLuv -> RGB test in an single pixelshader. I'll try messing around with the test app later today using an intermediate render target, hopefully I can find out something useful. I'm not sure it's a precission issue though, since I did a quick check with this:

    float4 lle = LogLuvEncode(color.rgb);        lle *= 256;    lle = floor(lle);    lle /= 256;        float3 lld = LogLuvDecode(lle);


And that does work (only slightly less saturated, but no banding or other artifacts), so it would seem the approach is sound. That at least narrows down the problem to either the conversion/rendering to an RGB surface or some weird filtering issue. Have you tried using point filtering to test how that works out?


Anyway... [smile]

I had a quick go at it and there was indeed some banding popping up again when doing RGB -> LogLuv -> (backbuffer resolve) -> LogLuv -> RGB. I had been wondering if the 128 & 256 numbers were valid for the 0-255 color range, so I decided to change those to 127 & 255 and that actually removes the banding even when using the intermediate buffer. Guess it was more of a silly range issue than precission. This code seems to do the trick:

// M matrix, for encodingconst static float3x3 M = float3x3(    0.2209, 0.3390, 0.4184,    0.1138, 0.6780, 0.7319,    0.0102, 0.1130, 0.2969);// Inverse M matrix, for decodingconst static float3x3 InverseM = float3x3(	6.0013,	-2.700,	-1.7995,	-1.332,	3.1029,	-5.7720,	.3007,	-1.088,	5.6268);	float4 LogLuvEncode(in float3 vRGB) {		     float4 vResult;     float3 Xp_Y_XYZp = mul(vRGB, M);    Xp_Y_XYZp = max(Xp_Y_XYZp, float3(1e-6, 1e-6, 1e-6));    vResult.xy = Xp_Y_XYZp.xy / Xp_Y_XYZp.z;    float Le = 2 * log2(Xp_Y_XYZp.y) + 127;    vResult.w = frac(Le);    vResult.z = (Le - (floor(vResult.w*255.0f))/255.0f)/255.0f;    return vResult;}float3 LogLuvDecode(in float4 vLogLuv){		float Le = vLogLuv.z * 255 + vLogLuv.w;	float3 Xp_Y_XYZp;	Xp_Y_XYZp.y = exp2((Le - 127) / 2);	Xp_Y_XYZp.z = Xp_Y_XYZp.y / vLogLuv.y;	Xp_Y_XYZp.x = vLogLuv.x * Xp_Y_XYZp.z;	float3 vRGB = mul(Xp_Y_XYZp, InverseM);	return max(vRGB, 0);}



Ps. for giggles I tried to see if Christers shortcut (Le / 255) worked this way, but that's still producing massive amounts of banding. I'm really curious to know if no one else ran into this or if it's simply no problem on PS3 hardware.


[Edited by - remigius on July 8, 2008 1:20:45 AM]
Rim van Wersch [ MDXInfo ] [ XNAInfo ] [ YouTube ] - Do yourself a favor and bookmark this excellent free online D3D/shader book!
Okay this is really really really embarrassing...

It turns out I had alpha-testing enabled and a few scattered pixels were getting killed when the fractional component came out to zero. After turning that off and using the code remigius came up with it all comes out very nice with no banding. Thanks again for the help, rim.

Good, problem solved! I guess I will need to write a post on my blog about NAO32/LogLuv encoding sooner or later..

I actually skipped the log part and stored the luminance value in two 8-bit channels ... this is much much faster and looks quite cool.

This topic is closed to new replies.

Advertisement