May 16, 2011

Oren-Nayar Lighting in Light Prepass Renderer

This is a conversation I had with another graphics programmer the other day:

  • A: "Using Oren-Nayar lighting is extreme hard with our rendering engine because IT is Light Pre-Pass renderer."
  • Me: "WTF? It's very easy."
  • A: "No. This blog says it's very hard."
  • Me: "Uh... but look at this.  I've already implemented it in our engine 2 years ago, and it was very trivial."
  • A: "OMG." -looks puzzled-
Okay. So I explained to him how I did it. And I'm gonna write the same thing here for the people who might be interested.  (I think the original blog post wanted to say supporting various lighting models is not easy in a deferred context, which is actually a valid point.)

First, if you don't know what Oren-Nayar is, look at this amazing free book. It even shows a way to optimize it with a texture lookup.  My own simple explanation of Oren-Nayar is a diffuse lighting model that additionally takes account of Roughness.  

Second, for those people who don't know what Light Pre-Pass renderer is, read this.

K, now real stuff.  To do Oren-Nayar, you only need one additional information. Yes, roughness.  Then how can we do Oren-Nayar in a Light Pre-pass renaderer?  Save roughness value on the G-Buffer, duh~.  There are multiple ways to save roughness on G-Buffer and probably this is where the confusion came from.

It looks like most light-prepas approaches use R16G16 for G-Buffer to store XY components of normals.  So to store additional information (e.g, roughness), you will need another render target = expensive = not good.

Another approach is to use 8 bit per channel to store normal map, but you will see some bending artifacts = bad lighting = bad bad. But, thanks to Crytek guys, you can actually store normals in three 8-bit channels without quality problem. It's called best-fit normal. So once you use this normal storage method, now you have an extra 8 bit channel that you can use for roughness.  Hooray! Problem solved.

But my actual implementation was a bit more than this because I needed to store specular power, too.  So I thought about it.  And found out we don't really need 8 bits for specular power(do you really need any specular power value over 127?  Or do you really use any specular power value less than 11?)  So I'm using 7 bit for specular power and 1 bit for roughness on/off flag.  Then roughness is just on and off? No. It shouldn't.  If you think a bit more, you will realize that roughness is just an inverse function of specular power  Think this way. Rougher surface will scatter lights more evenly, so specular power should be less for those surfaces and vice versa. 

With all these observations, and some hackery hack functions, this is what I really did at the end.

G-Buffer Storage
  • RGB: Normal
  • A: Roughness/Specular Power fusion
Super Simplified Lighting Pre-pass Shader Code

float4 gval = tex2D(Gbuffer, uv);

// decode normal using crytek's method texture
float3 normal = decodeNormal(gval.xyz);  


float specpower = gval.a * 255.0f;
float roughness = 0;
if (specpower > 127.0f)
{
    specpower -= 128.0f;
    roughness = someHackeryCurveFunction(127.0f - specpower);
}

// Now use this parameters to calculate correct lighting for the pixel.


Ta da.. not that hard, eh?  This approach was faster enough to ship a game on Xbox 360 and PS3 with some Oren-Nayar optimization through an approximation.

No comments:

Post a Comment