Athena: Cheap(er) Per-Pixel Directional Lighting

Per-Pixel Directional Lighting through Post ProcessAs mentioned in Athena: More Uses for Framebuffer Objects, I have thought up a way of doing lighting using colour buffers, at the moment it is only directional lighting however.

But the great thing is, depending on how many objects are in your scene, and how much overdraw can happen, this method is faster (I think, I don’t know the cost of writing to a second colour attachment).

Colour Shader

The colour shader is just a basic shader I’ve setup to show. First of all the vertex shader…

//
// Varying Variables
varying vec3 Normal;
 
//
// Vertex Shader entry point
void main()
{
	Normal = normalize( gl_NormalMatrix * gl_Normal );
	gl_Position = ftransform();
}

 

As you can see this is just a basic vertex shader, transforming a vertex position into screen space and passing the normal onto the fragment shader through a varying variable type.

Now the fragment shader…

//
// Varying Variables
varying vec3 Normal;
 
//
// Fragment Shader entry point
void main()
{
	vec3 N = normalize( Normal );
	float kDrawLit = 1.0;
	gl_FragData[0] = vec4( 1.0, 0.0, 0.0, 1.0 );
	gl_FragData[1] = vec4( N, kDrawLit );
}

 

This is a typical fragment shader as well, using draw buffer zero for the unlit texel colour. However, I’ve normalised the interpolated normal that was calculated for this texel and I’m outputting it to draw buffer one, along with another value ‘kDrawLit‘.

When the w component of draw buffer 1 for this texel is set to 0.0, the texel will not have any lighting calculations performed on it in the post process. I came up with this when I started thinking about rendering a HUD before the post processing.

I’ve chose just to output red as a colour, anything is fine, such as a texture that is mapped to the object in question.

Per-Pixel Lighting Shader

Again, another simple vertex shader, passing a transformed vertex and texture coordinate to the fragment shader…

//
// Varying Variables
varying vec3 v_lightDir;
varying vec3 v_halfVec;
 
//
// Vertex Shader entry point
void main()
{
	//
	v_lightDir = normalize( gl_LightSource[0].position.xyz );
	v_halfVec = normalize( gl_LightSource[0].halfVector.xyz );
 
	//
	gl_TexCoord[0].xy = gl_MultiTexCoord0.xy;
	gl_Position = gl_Vertex;
}

 

Now for where the magic happens, the fragment shader, it uses two textures. The ‘baseImage‘ sampler is for the colour buffer, whilst the ‘nrmImage‘ is used for the normal texture we wrote…

//
// Uniform Variables
uniform sampler2D baseImage;
uniform sampler2D nrmImage;
 
//
// Varying Variables
varying vec3 v_lightDir;
varying vec3 v_halfVec;
 
//
// Fragment Shader entry point
void main()
{
	// Get texel colour and normal data
	vec4 kTexelColour = texture2D( baseImage, gl_TexCoord[0].xy );
	vec4 kNormalColour = texture2D( nrmImage, gl_TexCoord[0].xy );
 
	// Don't Perform Lighting on a non-lit texel
	if( kNormalColour.w == 0.0 )
	{
		gl_FragData[0] = kTexelColour;
		return;
	}
 
	// Calculate Ambient
	vec4 kAmbient = gl_LightSource[0].ambient * kTexelColour;
 
	// Process Light Direction and Normals
	float NdotL = max( dot( kNormalColour.xyz, v_lightDir ), 0.0 );
	vec4 kColour = kAmbient;
 
	// Add Diffuse and Specular is NdotL is greater than zero
	if( NdotL > 0.0 )
	{
		// Calculate Diffuse
		vec4 kDiffuse = ( 1.0 - kAmbient ) * ( gl_LightSource[0].diffuse * kTexelColour );
		vec4 kSpecular = gl_LightSource[0].specular;
		float kShininess = 60.0;
 
		float NdotHV = max( dot( kNormalColour.xyz, v_halfVec ), 0.0 );
		kColour += kDiffuse * NdotL;
		kColour += kSpecular * pow( NdotHV, kShininess );
	}
 
	// Write Pixel Colour
	gl_FragData[0] = kColour;
}

 

As you can see, if the w component of the normal texture is 0.0 then the colour value is outputted as it is. If not, using the normal and the light currently setup, the colour is modified to be lit before being outputted.

I’ve just used a simple Blinn shader for calculating the lighting.

The Application

Now for the application side of things, below is just some quick sample code for what I used for my example. As you can see I’ve used a unsigned byte to store the normal, a floating point texture may have better accuracy at describing it though.

//
//
namespace CommonApp
{
	enum TextureTypes
	{
		kColour,
		kNormal,
		kDepthStencil,
		kNumTextureTypes,
	};
 
	//
	const int kWidth = 800;
	const int kHeight = 600;
 
	//
	GLuint l_frameBuffer;
	GLuint l_textures[kNumTextureTypes];
 
	//
	GLuint l_colourProgram;
	GLuint l_lightingProgram;
}
 
//
// Use alternate INTERNAL_FORMAT if floating point textures are not supported
//#define INTERNAL_FORMAT GL_RGBA
#define INTERNAL_FORMAT GL_RGBA32F_ARB
 
//
//
void CommonApp::Init()
{
	// Code to load colouring shader program
	// ...
 
	// Code to load lighting shader program and setup uniform variables
	// ...
 
	// Generate Textures
	glGenTextures( kNumTextureTypes, l_textures );
 
	// Create Colour Texture
	glBindTexture( GL_TEXTURE_2D, l_textures[kColour] );
	glTexImage2D( GL_TEXTURE_2D, 0, INTERNAL_FORMAT, kWidth, kHeight, 0, GL_RGBA, GL_FLOAT, 0 );
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
 
	// Create Normal Texture
	glBindTexture( GL_TEXTURE_2D, l_textures[kNormal] );
	glTexImage2D( GL_TEXTURE_2D, 0, INTERNAL_FORMAT, kWidth, kHeight, 0, GL_RGBA, GL_FLOAT, 0 );
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
 
	// Create Depth Buffer
	glBindTexture( GL_TEXTURE_2D, l_textures[kDepthStencil] );
	glTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8_EXT, kWidth, kHeight, 0, GL_DEPTH_STENCIL_EXT, GL_UNSIGNED_INT_24_8_EXT, 0 );
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
 
	// Generate Framebuffer and attach textures
	glGenFramebuffersEXT( 1, &l_frameBuffer );
	glBindFramebufferEXT( GL_FRAMEBUFFER_EXT, l_frameBuffer );
	glFramebufferTexture2DEXT( GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, l_textures[kColour], 0 );
	glFramebufferTexture2DEXT( GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT1_EXT, GL_TEXTURE_2D, l_textures[kNormal], 0 );
	glFramebufferTexture2DEXT( GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_TEXTURE_2D, l_textures[kDepthStencil], 0 );
	glFramebufferTexture2DEXT( GL_FRAMEBUFFER_EXT, GL_STENCIL_ATTACHMENT_EXT, GL_TEXTURE_2D, l_textures[kDepthStencil], 0 );
}
 
//
//
void CommonApp::DeInit()
{
	glDeleteFramebuffersEXT( 1, &l_frameBuffer );
	glDeleteTextures( kNumTextureTypes, l_textures );
}

 

Just the usual code as you can see for setting up a frame buffer. Now for rendering the scene…

//
//
void CommonApp::Render()
{
	// Use Frame Buffer
	glBindFramebufferEXT( GL_FRAMEBUFFER_EXT, l_frameBuffer );
 
	// Set Draw Buffers
	const GLenum kBuffers[] = { GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT };
	glDrawBuffers( 2, kBuffers );
 
	// Use Shading Program
	glUseProgram( l_colourProgram );
 
	// Perform Rendering
	// ...
 
	// Reset Draw Buffer
	glDrawBuffer( GL_COLOR_ATTACHMENT0_EXT );
 
	// Reset Frame Buffer
	glBindFramebufferEXT( GL_FRAMEBUFFER_EXT, 0 );
}

 

I’ve set it up so that drawing to draw buffer zero writes to the colour texture, whilst drawing to draw buffer one writes to the normal texture, to reflect what is being done in the shader.

You don’t need to use the colour shader I described up above, as long as it writes out the normal and an unlit colour, any shader can be used for this process, but for demonstration I’ve made it in this example that all geometry is drawn with this shader program.

Finally with our scene rendered, we can moved onto the final post processing stage…

//
//
void CommonApp::PostRender()
{
	glActiveTexture( GL_TEXTURE0 );
	glBindTexture( GL_TEXTURE_2D, l_textures[kColour] );
 
	glActiveTexture( GL_TEXTURE1 );
	glBindTexture( GL_TEXTURE_2D, l_textures[kNormal] );
 
	glUseProgram( l_lightingProgram );
 
	// Render Fullscreen Quad
	// ...
}

 

More Per-Pixel Directional Lighting through Post ProcessAttaching the colour and normal texture, using the per pixel lighting shader program and drawing a quad. The images within this blog are the final result (showing a comparison would be useless since they look the same).

One downside to this is that all texels have to use the same shininess value, however the alpha value of the colour buffer could be used to control this.

It is only a start, I need to performance check it at some point to see if it gives better or worse performance and under what conditions.

Per Pixel Lighting (MacOSX) (428 downloads)