PI or not to PI in game lighting equation
January 8, 2012 17 Comments
Version : 3.1 – Living blog – First version was 4 January 2012
With physically based rendering current trend of photo-realistic game, I feel the need to do my lighting equation more physically correct. A good start for this is to try to understand how our currently game lighting equation behave. For a long time, the presence or not in my game lighting equation of term or
have been more based on trial and error than physics. The origin of these terms in game lighting equations have already been discussed by others [1][3][7][12]. But as I found this subject confusing I dedicated this post only to that topic. This post is not about code, it is about understanding what we do under the hood and why this is correct. I care about this because more correct game lighting equations mean consistency under different lighting condition, so less artists time spend to tweak values. I would like to thank Stephen Hill for all the help he provide me around this topic.
This post is written as a memo for myself and as a help to anyone which was confusing as I was. Any feedback are welcomed.
I will begin this post by talking about Lambertian surface and the specificity of game light’s intensity then talk about diffuse shading and conclude by specular shading. I will not define common term found in lighting field like BRDF, Lambertian surface… See [1] for all these definitions and notation of this post.
Origin of
term confusion
The true origin of the confusing term come from the Lambertian BRDF which is the most used BRDF in computer graphics. The Lambertian BRDF is a constant define as :
The notation mean BRDF parametized by light vector
and view vector
. The view vector is not used in the case of Lambertian BRDF.
is what we commonly call diffuse color.
The first confusing term appear in this formula. It come from a constraint a BRDF should respect which is name conservation of energy. It mean that the outgoing energy cannot be greater than the incoming energy. Or in other word that you can’t create light. The derivation of the
can be found in [3].
As you may note, game Lambertian BRDF have not this term. Let’s see a light affecting a Lambertian surface in game:
FinalColor = c_diff * c_light * dot(n, l)
To understand where the disappeard, see how game light’s intensity is define. Games don’t use radiometric measure as the light’s intensity but use a more artist friendly measure [1] :
For artist convenience,
does not correspond to a direct radiometric measure of the light’s intensity; it is specified as the color a white Lambertian surface would have
when illuminated by the light from a direction parallel to the surface normal ()
Which mean, if you setup a light in a game with color and point it directly on a diffuse only quad mapped with a white diffuse texture you get the color of
.
Another way to see this definition is by taking the definition of a diffuse texture [2] :
How bright a surface is when lit by a 100% bright white light
Which mean, if you setup a white light in a game with brightness 1 and point it directly on a diffuse only quad mapped with a diffuse texture, you get the color of the diffuse texture.
This is very convenient for artists which don’t need to care about physical meaning of light’s intensity unit.
Theses definitions allows to define the punctual light equation use in game. A punctual light is an infinite small light like directional, point or spot light common in games.
.
The derivation and the notation of this equation is given in [1]. is the resulting exit radiance in the direction of the view vector
which is what you will use as color for your screen pixel.
is not used for Lambertian BRDF.
Using the punctual light equation with a Lambertian BRDF give us :
.
Which I will rewrite more simply by switching to a monochrome light ( is for RGB) :
This shading equation looks familiar except the term. In fact after simplification we get :
Which is our common game diffuse lighting equation.
This mean that for artists convenience, the value they enter as brightness in light’s settings is in fact the result of the light brightness multiply by (the energy conserving constant of Lambertian BRDF) . When artists put 1 in brightness, in reality they set a brightness of
. This is represented in the punctual lighting equation by
. In this post, I will define as game light’s unit the fact of multiplying the light brightness by
and as game Lambert BRDF the
term which is linked.
In the following I will describe the consequence of the game light’s unit on common diffuse lighting technic use in game then on common specular lighting technic.
Diffuse lighting in game
Lambert lighting
The most common diffuse lighting equation use in game is the classic Lambert lighting describe in the introduction which is the origin of the multiply by of the light intensity. So for this case we already describe the impact: it allow to remove the
from the Lambertian BRDF.
Diffuse irradiance environment map
Lot of games choose to approximate diffuse distant lighting with irradiance environment map. For simplicity, I will use cubemap as mapping but other mapping like sphere, dual paraboloïd… work as well.
To better understand the impact of game light’s unit intensity with diffuse irradiance cubemap lighting we need to understand how to calculate an irradiance cubemap and what is the relationship between radiance and irradiance.
I won’t go into the complex definition of these terms. Most of what I write here is from [7]. To stay simple, radiance is a measure of light in a single ray and irradiance is a measure of light incoming to a surface point from all directions. The irradiance under mathematic form is:
is the upper hemisphere of a surface so cosine is not clamped.
We will use a cubemap to represent the lighting environment (The function of the equation which take a direction in parameter and return radiance for this direction).
Texel of a cubemap represent radiance which are not in the game light’s unit we define in previous section :
– A “real” HDR light probe like the one provided by Paul Debevec [8] deal with real world unit.
– In-game engine generated HDR cubemap will contain only indirect lighting (in most common usage). The indirect lighting is the result of our game lighting bouncing on surface. Consequence, we don’t deal anymore with game light’s unit but with real world unit.
This result to :
is the solid angle subtended (cover) by cubemap texel designed by direction
.
This formula translates simply in pseudo code :
void GetIrradiance(n) { E = 0 foreach direction l if (dot(n,l) > 0) E += cubemap(l) * dot(n, l) * texelSolidAngle(l) }
cubemap(l) return the value of the texel in the direction l.
texelSolidAngle(l) return the solid angle for the texel in direction l
The number of direction l is defined by the cubemap resolution.
So if we want to generate an irradiance cubemap, we can use the pseudo code:
for each texel of the destination cubemap Get the direction n for this texel GetIrradiance(n) // Use the formula above
If you are interested the C++ code for these calculation can be found in source code of AMD Cubemapgen [10].
An irradiance cubemap store irradiance for all direction (limited by the cubemap resolution).
Now that we know how to calculate irradiance cubemap, let’s see how to use it.
In case of Lambertian surface the exit radiance is proportional to the irradiance:
.
is the Lambertian BRDF. A derivation of this equation can be found in [7].
The irradiance cubemap need to be sampled by the Lambertian surface normal to retrieve irradiance . Translated into code we get:
L_o = (c_diff / PI) * texCube(IrradianceCubemap, normal)
However for game we use for Lambertian BRDF due to our game light’s unit, so we would like to have:
L_o = c_diff * texCube(IrradianceCubemap, normal)
For this,we just have to turn irradiance into radiance at the irradiance cubemap generation time instead of inside the shader. Turning irradiance into radiance for Lambertian surface just mean dividing by the irradiance
.
void GetIrradiance(n) { E = 0 foreach direction l if (dot(n,l) > 0) E += cubemap(l) * dot(n, l) * texelSolidAngle(l) E = E / PI }
Diffuse irradiance environment map takeaway
The takeway here is to know what do your tool for generating irradiance cubemap. If the tools turn irradiance into radiance (divide irradiance cubemap by ), nothing special. If the tools don’t do it, you must apply the divide yourself when sampling the irradiance cubemap.
AMD Cubemapgen [10] and HDRShop [11] turn the irradiance to radiance when generating irradiance cubemap. To test it, you can input a grey (constant 0.5) HDR cubemap into the tool, generate the irradiance cubemap and check it is grey (constant 0.5) in output. I suppose that most tools do this to support LDR cubemap format like ARGB8 (In this case you can’t output a 0.5 * irradiance cubemap, it will be clamped to 1.0).
Added note:
To generate an irradiance cubemap with AMD Cubemapgen, select the cosine filter with an angle of 180.
The following code extracted from the cubemapgen source is actually what perform the divide by .
//divide through by weights if weight is non zero if(weightAccum != 0.0f) { for(k=0; k<m_NumChannels; k++) { a_DstVal[k] = (float32)(dstAccum[k] / weightAccum); } }
Where weightAccum is the sum of dot(n, l) * texelSolidAngle of each texel.
To understand why, let’s calculate weightAccum:
Derivation of this result can be found in [3].
Diffuse spherical harmonic lighting
Most game today used spherical harmonic (SH) as an approximation of the diffuse lighting environment. I will be lazy here and don’t introduce SH, I let reader refer to Stephen Hill’s post [4] for a quick sum up or Green talk for a large description [9] .
SH Irradiance map
The most know application of SH is the efficient evaluation of an irradiance environment map like the one of the previous section. Instead of creating an irradiance environment map you may keep it under the form of a more compact set of SH coefficients which I call SH irradiance map. The ShaderX2 article “Efficient Evaluation of Irradiance Environment Maps” of Peter-Pike Sloan [5] explain with source code how to generate and used these SH coefficients.
Remember the irradiance formula of the previous section
What we do here is convolving a lighting environment with the cosine lobe. This operation can be performed efficiently in a frequency space like allow SH with a simple dot product.
Then we use the relationship between irradiance and radiance for Lambertian surface as before to get our exit radiance:
Here is all the steps to do this with SH:
A. Generate SH irradiance map
– Project in SH the lighting environment (which I will represent by a cubemap and as said in the previous section a cubemap store radiance in real world unit)
– Project in SH the cosine lobe
– Convolve SH-projected lighting environment with SH-projected cosine lobe
B. Get exit radiance from irradiance
– Evaluate Irradiance for the normal direction in the irradiance cubemap represented by SH
– Turn irradiance to radiance for Lambertian surface by dividing by and get exit radiance
The nice thing with the SH-projected cosine lobe convolution is that it sum up to three scale band factor (in the case of second order SH) :
Let’s see the pseudo-code for all these steps :
// A. generate SH irradiance map // Project the lighting environment for each texel of the cubemap { float3 c_light = texel_radiance; float weight = texelSolidAngle; float3 n = texelDirection; float SHLightL[9]; SHLightL[0] = 0.282095f * c_light * weight; SHLightL[1] = -0.488603f * n.y * c_light * weight; SHLightL[2] = 0.488603f * n.z * c_light * weight; SHLightL[3] = -0.488603f * n.x * c_light * weight; SHLightL[4] = 1.092548f * n.x * n.y * c_light * weight; SHLightL[5] = -1.092548f * n.y * n.z * c_light * weight; SHLightL[6] = 0.315392f * (3.0f * n.z * n.z - 1.0f) * c_light * weight; SHLightL[7] = -1.092548f * n.x * n.z * c_light * weight; SHLightL[8] = 0.546274f * (n.x * n.x - n.y * n.y) * c_light * weight; } // Convolve with SH-projected cosinus lobe float ConvolveCosineLobeBandFactor[] = { PI, 2.0f * PI/3.0f, 2.0f * PI/3.0f, 2.0f * PI/3.0f, PI/4.0f, PI/4.0f, PI/4.0f, PI/4.0f, PI/4.0f } for (int i = 0; i < 9; ++i) SHLightL[i] *= ConvolveCosineLobeBandFactor[i]; // // B. Get exit radiance from irradiance // Evaluate irradiance at surface with normal n float SHLightResult[9]; SHLightResult[0] = 0.282095f * SHLightL[0]; SHLightResult[1] = -0.488603f * n.y * SHLightL[1]; SHLightResult[2] = 0.488603f * n.z * SHLightL[2]; SHLightResult[3] = -0.488603f * n.x * SHLightL[3]; SHLightResult[4] = 1.092548f * n.x * n.y * SHLightL[4]; SHLightResult[5] = -1.092548f * n.y * n.z * SHLightL[5]; SHLightResult[6] = 0.315392f * (3.0f * n.z * n.z - 1.0f) * SHLightL[6]; SHLightResult[7] = -1.092548f * n.x * n.z * SHLightL[7]; SHLightResult[8] = 0.546274f * (n.x * n.x - n.y * n.y) * SHLightL[8]; float result = 0.0f; for (int i = 0; i < 9; ++i) result += SHLightResult[i]; // Turn irradiance to radiance for Lambertian surface and get exit radiance L_o = result * c_diff / PI;
In order to have our game Lambertian BRDF at the end instead of
, we apply the same simplification as previous section here: we will turn our irradiance to radiance at the SH irradiance environment generation. Most code, like the one in ShaderX2 will include the divide by
inside the SH-projected cosine lobe term resulting in scale band factor
.
Compacted code is now:
// A. generate SH irradiance map // Project the lighting environment, convolve with cosine lobe, turn irradiance to radiance for each texel of the cubemap { (...) SHLightL[0] = 0.282095f * c_light * weight; SHLightL[1] = -0.488603f * n.y * 2.0f/3.0f * c_light * weight; SHLightL[2] = 0.488603f * n.z * 2.0f/3.0f * c_light * weight; SHLightL[3] = -0.488603f * n.x * 2.0f/3.0f * c_light * weight; SHLightL[4] = 1.092548f * n.x * n.y * 1.0f/4.0f * c_light * weight; SHLightL[5] = -1.092548f * n.y * n.z * 1.0f/4.0f * c_light * weight; (...) } // // B. Get exit radiance from irradiance // Evaluate irradiance (already turn to radiance) at surface with normal n float SHLightResult[9]; SHLightResult[0] = 0.282095f * SHLightL[0]; SHLightResult[1] = -0.488603f * n.y * SHLightL[1]; (...) for (int i = 0; i < 9; ++i) result += SHLightResult[i]; // Get exit radiance L_o = result * c_diff;
Full C++ source code can be found in Modified AMD Cubemapgen [10] see the post AMD Cubemapgen for physically based rendering.
We get exactly the same behavior than for irradiance environment map, good.
Added note:
The convolution is tied to the surface property as we will see later, so it is better to apply scale band factor at the moment we know the surface to affect, so when evaluating irradiance (which is no more irradiance from a semantic point of view). Thank to Stephen Hill to point this (see the comment).
All the steps I describe is for better understanding, in practice most constant are precomputed together for efficiency and applied at different time. The point here is about semantic, not code.
SH Punctual lights approximation
Another common application of SH is to approximate diffuse lighting of multiple punctual lights. The principe are very similar to a SH irradiance map. But be aware here that we deal with punctual light with game light’s unit. The punctual light’s intensity need to be multiplied by before to be projected in SH. This is the
part of the punctual lighting equation.
The steps are exactly the same that for SH irradiance map so I won’t repeat them. The difference is that the light environment is no more a cubemap in real world unit, but a set of punctual light in game light’s unit.
Compacted code is now:
// A. generate SH irradiance map // Project the lighting environment, convolve with cosine lobe, turn irradiance to radiance for each punctual light { float3 c_light = PI * c_punctual_light; float3 n = LightDirection; float SHLightL[9]; SHLightL[0] = 0.282095f * c_light; SHLightL[1] = -0.488603f * n.y * 2.0f/3.0f * c_light; SHLightL[2] = 0.488603f * n.z * 2.0f/3.0f * c_light; SHLightL[3] = -0.488603f * n.x * 2.0f/3.0f * c_light; SHLightL[4] = 1.092548f * n.x * n.y * 1.0f/4.0f * c_light; SHLightL[5] = -1.092548f * n.y * n.z * 1.0f/4.0f * c_light; (...) } // // B. Get exit radiance from irradiance // Evaluate irradiance (already turn to radiance) at surface with normal n float SHLightResult[9]; SHLightResult[0] = 0.282095f * SHLightL[0]; SHLightResult[1] = -0.488603f * n.y * SHLightL[1]; (...) for (int i = 0; i < 9; ++i) result += SHLightResult[i]; // Get exit radiance L_o = result * c_diff;
Added note:
As for SH irradiance map, you may want to apply the convolution at the surface affecting time. See later.
Diffuse SH lighting takeaway
To use our game Lambertian BRDF () with SH lighting:
– Think to turn irradiance into radiance by dividing the SH-Projected cosine lobe term by .
To deals with our game light’s unit:
– When projecting cubemap into SH, nothing special todo
– When projecting punctual lights into SH, scale the light’s intensity by PI
So take care when mixing both cubemap and punctual lights into the same set of SH coefficients.
Care must be taken when artists do hand painted HDR cubemap! They should paint with real world unit in mind, rather difficult…
Specular lighting in game
For Lambertian BRDF, all is fine. But as I said in my post about Adopting a physically based shading model care must be taken when using an energy conserving specular BRDF.
For sample, with the classical normalized Phong term the punctual light equation become:
Which simplify to
So when dealing with an energy conserving specular term, don’t forget to divide the constant factor by .
Wrap lighting in game
Wrap lighting is commonly use in game to fake subsurface scattering or area light. Wrap lighting change the cosine lobe formulation of Lambert law, it is not a physically based lighting model but it is helpful. This imply that we need to recalculate the energy conserving constant of our Lambertian BRDF because the factor was calculated from the cosine not the wrap lighting equation. Formula of wrap lighting and derivation of the new energy conservation term for Lambert BRDF can be found in [12] :
with between 0 and 1.
Energy conserving Lambertian BRDF with wrap lighting equation:
Translated into code, our new diffuse lighting equation is:
FinalColor = ( c_diff / (PI * (1 + w)
) ) * (dot(N, L) + w) / (1 + w);
Look at how it behave with our game light’s unit. As the change does not affect the original presence of the term in the Lambertian BRDF itself, all advices for punctual lights with game light’s unit and environment map still apply with wrap lighting. We can use as game diffuse wrap lighting equation:
FinalColor = ( c_diff / (1 + w)
) * (dot(N, L) + w) / (1 + w);
Or simply as in [12]
FinalColor = c_diff
* (dot(N, L) + w) / ((1 + w)
* (1 + w));
The thing Stephen Hill highlight in comment and in a recent exchange I have with him is about the SH diffuse wrap lighting. The SH convolution we perform in this case is no more done with but with
. See [4] for derivation of these values. Note that these values still include the divide by
which turn irradiance to radiance (which still valid with our new energy conserving constant). The important point here is that the convolution coefficient are tied to surface properties, and so should be evaluated in the shader. This will transform our steps for SH punctual lights as:
// A. generate SH irradiance map // Project the lighting environment for each punctual light { float3 c_light = PI * c_punctual_light; float3 n = LightDirection; float SHLightL[9]; SHLightL[0] = 0.282095f * c_light; SHLightL[1] = -0.488603f * n.y * c_light; SHLightL[2] = 0.488603f * n.z * c_light; SHLightL[3] = -0.488603f * n.x * c_light; SHLightL[4] = 1.092548f * n.x * n.y * c_light; SHLightL[5] = -1.092548f * n.y * n.z * c_light; (...) } // // B. Get exit radiance // Evaluate SH at surface with normal n, perform the convolution with the wrap argument, turn irradiance to radiance float SHLightResult[9]; SHLightResult[0] = 0.282095f * SHLightL[0]; SHLightResult[1] = -0.488603f * n.y * (2.0f-w)/3.0f * SHLightL[1]; SHLightResult[2] = 0.488603f * n.z * (2.0f-w)/3.0f * SHLightL[2]; SHLightResult[3] = -0.488603f * n.x * (2.0f-w)/3.0f * SHLightL[3]; SHLightResult[4] = 1.092548f * n.x * n.y * (1.0f-w) * (1.0f-w)/4.0f * SHLightL[4]; SHLightResult[5] = -1.092548f * n.y * n.z * (1.0f-w) * (1.0f-w)/4.0f * SHLightL[5]; (...) for (int i = 0; i < 9; ++i) result += SHLightResult[i]; // Get exit radiance L_o = result * c_diff;
This work nicely. So as you can see, we multiply by when we projecting the light environment in SH to take count of our game light’s unit and we perform all surface dependent thing into the shader.
Reference
[1] Hoffman, “Crafting Physically Motivated Shading Models for Game Development” and “Background: Physically-Based Shading” http://renderwonk.com/publications/s2010-shading-course/
[2] Epic game, “UDK documentation” http://udn.epicgames.com/Three/TexturingGuidelines.html
[3] “Energy conservation in game” http://www.rorydriscoll.com/2009/01/25/energy-conservation-in-games/
[4] Hill, “Righting wrap” http://blog.selfshadow.com/2011/12/31/righting-wrap-part-1/
[5] Sloan, “Efficient Evaluation of Irradiance Environment Maps” http://tog.acm.org/resources/shaderx/
[6] King, “Real-Time Computation of Dynamic Irradiance Environment Maps” http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter10.html
[7] Akenine-Möller, Haines, Hoffman, “Real-Time Rendering 3rd Edition” http://www.realtimerendering.com
[8] http://www.pauldebevec.com/
[9] Green, “Spherical Harmonic Lighting: The Gritty Details” http://www.research.scea.com/gdc2003/spherical-harmonic-lighting.pdf
[10] http://code.google.com/p/cubemapgen/
[11] http://www.hdrshop.com/
[12] McCauley, “Energy-Conserving Wrapped Diffuse” http://blog.stevemcauley.com/
“So you must be aware that when you convert a punctual light with game light’s unit in SH coefficients you must use scale band factor […]”
Here you’re really scaling the *punctual light* by pi, not changing the convolution, which is tied to the properties of the surface! Even if you roll SH light projection and convolution into one step in practice (because you’re doing this for diffuse surfaces only), I think it’s sensible to at least logically separate the two operations.
From memory, this is how D3DX separates things. For instance, I believe D3DXSHEvalDirectionalLight will scale by ~pi (it’s not quite this because it’ll rescale slightly to ensure that “the resulting exit radiance of a point directly under the light on a diffuse object with an albedo of 1 would be 1.0”):
http://msdn.microsoft.com/en-us/library/windows/desktop/bb205449%28v=vs.85%29.aspx
I went ahead and checked D3DX and this code underscores the point I was making:
[Seb: You may need to edit this comment, since I’m not sure I can use WP markup.]
Where’s your
now? It’s part of
D3DXSHEvalDirectionalLight
. The result of c is 1.0, as promised by the documentation.This separation allows you to project different lighting independently, sum the results and then apply whatever convolution is appropriate for your surfaces at the end.
“Evaluate exit radiance in direction of normal” is a poor choice of words, but you get the idea.
Thank for the detailed explanation Steve.
It’s been a long time since I use D3DX… 🙂
All this is semantic because the code is the same but this is exactly why I write this post. Thank you for all this clarification, I will update the post to be more precise.
I rewritte complety this post to be more understandable and with the hint give by Steve + added a Wrap lighting section. The sentence in the comment of Steve refer to version 2.0 but all is remarks are very helpful!
“It mean that the outgoing energy cannot be greater than the outgoing energy” that is easy to see! 😉
Aouch! Corrected, thank you 🙂
Pingback: Spherical Harmonics for Beginners | dickyjim
Pingback: Extracting dominant light from Spherical Harmonics | 25cafe
Hi Sebastien,
its a bit late, but you can also find a valuable discussion about PI in BRDF here;
http://www.thetenthplanet.de/archives/255
I originally only a short side note about PI, but then somebody asked, and it got really detailed!
Thanks for the additional information Christian.
Pingback: Light rendering on maps | mots d'un ingé
Pingback: 从0开始pbr « Babylon Garden
Pingback: Path Tracing – Getting Started With Diffuse and Emissive | The blog at the bottom of the sea
Pingback: Randomly generated stuff
Hi Sebastien,
Thanks for the blog post!
I have a question about specular IBL and SSR: I believe that local reflection probes (used for specular IBL) should be treated like diffuse irradiance maps as being in “real world units” that needs to be converted into “game’s light unit” by dividing by PI during sampling. But what about SSR? I think it should be considered as being in “real world unit” as well and thus divided by PI but I would like your input on this please :).
Hi. Ssr and reflection and irradiance probe can be processed the same way with split sum appoximation. Please see https://seblagarde.wordpress.com/2015/07/14/siggraph-2014-moving-frostbite-to-physically-based-rendering/