Upcoming Events
Southwest Gaming Expo
11/20 - 11/22 @ Dallas, TX

Workshop on Network and Systems Support for Games (NetGames 2009)
11/23 - 11/25 @ Paris, France

ICIDS 2009 Interactive Storytelling
12/9 - 12/11 @ Guimarães, Portugal

Global Game Jam
1/29 - 1/31  

More events...


Quick Stats
6427 people currently visiting GDNet.
2341 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!



Link to us

Link to us

  Intel sponsors gamedev.net search:   

Image Space Lighting


Rendering and Implementation

Now that we know the theory behind image space lighting, it is time to work out the actual rendering and implementation details. The steps to rendering image space lighting are as follows:
  1. Extract the position, normal and color information of the scene to calculate the lighting at any pixel. This can be done with deferred shading, where the scene is rendered once with a pixel shader that stores the position, normal and color information into the proper render targets. Any effects like normal mapping can be done here – just outputting the final normal to the normal texture.
  2. Render the scene to the screen normally (as it was rendered off screen previously). However, this does not require rendering the scene geometry again, as the per pixel attribute results were already generated in the deferred shading pass. Simply render a quad that fills the entire screen (this is easy to do with a vertex shader), and then retrieve the per pixel position, normal and color information from the deferred shading textures. During this pass, any primary light sources (like directional lights) and effects can be rendered (the contribution to the scene from the point lights will be added later). It is worth noting that the pixel shader only executes once per pixel in this pass, which can improve the speed of the rendering for complex pixel shaders.
  3. Render the point lights on top of the previously rendered scene as follows:
    1. Bind a shader that computes the lighting contribution from the light source whose proxy shape is being rendered on the point at the rasterized pixel position in the deferred shading textures. This shader can also discard the fragment if the point is not within the light’s area of effect to save computation costs (which happens because the proxy shape over-bounds the area of effect and because the depth test is disabled when rendering the proxy shapes (see below), so the point could be far away in the depth coordinate).
    2. Set the blending function to 1 * src + 1 * dst. This indicates that the computed lighting at each pixel is added to the previous value. As the lighting equation is additive (summation of the contribution of all light sources), this works as expected.
    3. Enable face culling, and cull front faces. If the back faces are culled, nothing will be rendered when the camera moves inside the proxy shape (as the front faces will be behind the camera). As long as the proxy shape is convex, there are only two rasterized fragments for each pixel on the shape (one on the front face, and one on the back face). If the front faces are culled, only one fragment is rasterized per pixel, which means that no light will contribute lighting twice for any pixel (which would be incorrect).
    4. Disable the depth test as pixels on the back face of the proxy shape could fail the depth test (thus contributing no light) even when the light source should contribute light to that pixel. However, since the pixel shader can quickly discard pixels outside of the light source’s area of effect, this does not affect performance too much.
    5. Render all of the proxy shapes. The proxy shapes can be stored in a tree structure to quickly render only the proxy shapes within the view frustum to improve performance.
    6. Finally, unbind the shader and textures, and then set blending, face culling and depth test back to their previous values.

Code Snippets

Below is an OpenGL code snippet from the game project Aero Empire [2], which renders the point lights following the above implementation.
Shader* cur_shader;

void renderLight(Light* light){
  //pass light world position to pixel shader
  cur_shader->setUniformVec3("lightWorldPos", light->getWorldPosition()); 
  
  //pass light color (magnitude of vector is power) to pixel shader
  cur_shader->setUniformVec3("lightColor", light->getColor()); 
  
  //pass the light's area of effect radius to pixel shader
  cur_shader->setUniformFloat("lightAoE", light->getAoERadius()); 
  light->render(); //render the proxy shape
}

void renderLights(const Shape* scene){
  //set blend function
  glBlendFunc(GL_ONE, GL_ONE); 
  
  //cull front faces
  glEnable(GL_CULL_FACE); 
  glCullFace(GL_FRONT);
  
  //disable depth testing
  glDisable(GL_DEPTH_TEST); 
  glDepthMask(false);
  
  //bind point light pixel shader
  Shader* shader = shaders[SHADER_LIGHT]; 
  shader->bind();

  //bind position, normal and color textures from deferred shading pass
  bindTexture(positionMap, POSITION_UNIT); 
  shader->setUniformInt("positionMap", POSITION_UNIT);
  bindTexture(normalMap, NORMAL_UNIT);
  shader->setUniformInt("normalMap", NORMAL_UNIT);
  bindTexture(colorMap, COLOR_UNIT);
  shader->setUniformInt("colorMap", COLOR_UNIT);
  bindTexture(attrMap, ATTR_UNIT);
  shader->setUniformInt("attrMap", ATTR_UNIT);

  //set pixel shader attribute "camPosition" to the camera pos 
  //(as it is needed for Phong shading)
  camera.loadCameraPosition(shader, "camPosition"); 
  shader->setUniformFloat("d_sx", 1.0/width);
  shader->setUniformFloat("d_sy", 1.0/height);
  cur_shader = shader;
  
  //run renderLight function on all light proxy shapes in scene
  getLights(scene, &renderLight); 
  
  //unbind and reset everything to desired values
  shader->unbind(); 
  unbindTexture(POSITION_UNIT);
  unbindTexture(NORMAL_UNIT);
  unbindTexture(COLOR_UNIT);
  unbindTexture(ATTR_UNIT);
  glDisable(GL_CULL_FACE);
  glEnable(GL_DEPTH_TEST);
  glDepthMask(true);
}
Below is the GLSL pixel shader bound above which computes the lighting (the vertex shader only sets gl_Position = ftransform() ).
//input parameters
uniform sampler2D positionMap, normalMap, colorMap, attrMap;
uniform vec3 camPosition, lightWorldPos, lightColor;
uniform float lightAoE, d_sx, d_sy;

void main(void)
{
  //calculate screen coord
	vec2 coord = vec2(gl_FragCoord.x*d_sx, gl_FragCoord.y*d_sy); 
	
	//get the position from deferred shading
	vec4 position = texture2D(positionMap, coord); 
	
	//vector between light and point
	vec3 VP = lightWorldPos-position.xyz; 
	
	//get the distance between the light and point
	float distance = length(VP); 
	
	//if outside of area of effect, discard pixel
	if(distance > lightAoE) discard; 
	
	//normalize vector between light and point (divide by distance)
	VP /= distance; 
	
	//get the normal from deferred shading
	vec4 normal = texture2D(normalMap, coord); 
	
	//get the color from deferred shading
	vec4 color = texture2D(colorMap, coord); 
	
	//get lighting attributes from deferred shading
	vec4 attributes = texture2D(attrMap, coord); 
	float diff_coefficient = attributes.r;
	float phong_coefficient = attributes.g;
	float two_sided = attributes.b;
	float cos_theda = dot(normal.xyz, VP);
	
	//calculate two sided lighting.
	cos_theda = (cos_theda < 0.0)?-two_sided*cos_theda:cos_theda; 
	
	//calculate diffuse shading
	float diffuse = diff_coefficient*cos_theda; 
	
	//calculate half vector
	vec3 H = normalize(VP+normalize(camPosition - position.xyz)); 
	
	//calculate Phong shading
	float phong = phong_coefficient*pow(max(dot(H, normal.xyz), 0.0), 100.0); 
	
	//calculate light contribution with attenuation
	vec3 C = lightColor*(color.rgb*diffuse+phong)/(distance*distance+0.8); 
	
	//all lights have constant quadratic attenuation of 1.0, with a constant attenuation of 0.8 to avoid dividing by small numbers
	gl_FragColor = vec4(C, 1.0); //output color
}
If you are planning to add lanterns or lamps, adding two-sided lighting allows for point lights inside a lampshade or lantern to contribute light to it. The above shader includes a very simple implementation of two-sided lighting.

Results


Figure 3: 2175 bright cube lanterns rendered on a flat plane in a night scene.


Figure 4: A daytime rendering of a blimp from Aero Empire [2] with lanterns during the day. Notice the Phong lighting on the envelope.


Figure 5: A nighttime rendering of the same blimp as above.


Figure 6: Ship hanger interior from Infinity [3]. This rendering includes both point lights and ambient lights (adding point lights that approximate indirect lighting).

As you can see, image space lighting creates accurate lighting effects that add to the scene whether daytime or nighttime, interior or exterior. The ability to efficiently render a large number of point light sources allows for much more diverse lighting environments which greatly improves the rendering of the scene, yet still runs in real time.



Performance & Conclusion


Contents
  Introduction & Overview
  Rendering & Implementation
  Performance & Conclusion

  Source code
  Printable version
  Discuss this article