Fast Look-Up Light Shading using HLSL & Ogre
Over the past 21 weeks at Uni I’ve been in the process of developing a completely functional 3D game, resembling a third-person, stealth-based ninja game. With three weeks of development left until the final hand-over there’s a fair bit left to do. A major issue that arose somewhat midway through the project was performance due to the complex outdoor environments. The main issue was the fixed-function pipeline lighting system which was causing batches in the scene to be redrawn, resulting in both huge batch counts (thousands) and huge triangle counts (over four million).
The way scene optimisation is set up is by performing static geometry batching on the level itself, an integrated feature of the Ogre3D 1.8 rendering library. This combines the thousands of components that the game’s level is built up of into one or more various regions – right now every 100 game units. Fixed function pipeline lights as well as most common shader-based lighting techniques require a a model to be redrawn for each light that affects it. As levels are built out of rather large regions formed by combining level assets, this results in very large redraws, seriously hampering performance when even half a dozen lights are introduced in the level.
To solve this issue, a rather experimental lighting technique was introduced. A texture is generated that covers the entire level at a very low resolution (in my scenario, 64×64 pixels). Each pixel represents a region that one light can be placed. The RGB value of each pixel represents the exact position of the light (R – X, G – Y, B – Z) in the 3D world. Each update light information is then applied over the entire world in a single pass with no redraws. This results in extremely fast lighting calculation with absolutely no performance hit for additional lights. What this means is that a scene, in theory, could have literally thousands of lights.
Another benefit to the lighting system is the fact that this is not a form of lightmapping but is infact real-time. This means that moving objects within the level have real-time lighting and even better, lights can actually be moved around as well by updating the generated texture each frame.
The shader was initially provided by an Ogre3D forum moderator, Kojack, providing a very simple version of the shader in what was definitely no more than 20 or 30 lines within the shader. I expanded this to include real-time normal mapping, texture mapping as well as allowing lights to have individual colour and brightness values to create the intended lighting effect within the scene.
Shader code:
sampler2D Texture1: register(s0);
sampler2D Texture2: register(s1);
sampler2D NormalMap: register(s2);
float3 lookup(float3 mappos, float3 offsetpos, float3 norm, float3 bump)
{
float3 c = float3(0, 0, 0);
float4 streetlightcell = tex2D( Texture1, offsetpos.xz+mappos.xz );
if(streetlightcell.a > 0)
{
float3 lightvec = streetlightcell.rgb – mappos*1000;
float dist = length(lightvec);
lightvec=normalize(lightvec);
c = streetlightcell.aaa * max((dot(lightvec,bump)/50) * (1 /(dist*0.005)),0);
}
return c;
}
void gridlight_vp(float4 position : POSITION,
float4 diffusein : COLOR0,
float2 texCoord0 : TEXCOORD0,
float3 normal : NORMAL,
float3 tangent : TANGENT,
float3 binormal : BINORMAL,
out float2 oTexCoord0 : TEXCOORD0,
out float4 oPosition : POSITION,
out float3 posAsColour : TEXCOORD1,
out float4 diffuseout : COLOR0,
out float3 oNormal : TEXCOORD2,
out float3 oTangent : TEXCOORD3,
out float3 oBinormal : TEXCOORD4,
uniform float4x4 worldViewProj,
uniform float4x4 world,
uniform float4x4 WorldInverseTranspose)
{
oPosition = mul(worldViewProj, position);
posAsColour = (mul(world, position).xyz)*(1.0/1000.0);
diffuseout = diffusein;
oTexCoord0 = texCoord0;
oNormal = normalize(mul(normal, WorldInverseTranspose));
oTangent = normalize(mul(tangent, WorldInverseTranspose));
oBinormal = normalize(mul(binormal, WorldInverseTranspose));
}
void gridlight_fp(
float2 texCoord0 : TEXCOORD0,
float4 diffuse : COLOR0,
float3 posAsColour : TEXCOORD1,
float3 norm : TEXCOORD2,
float3 tangent : TEXCOORD3,
float3 binormal : TEXCOORD4,
uniform float4 ambient,
out float4 oColour : COLOR)
{
// Calculate the normal, including the information in the bump map
float3 bump = 1.0 * (tex2D(NormalMap, texCoord0) – (0.5, 0.5, 0.5));
float3 bumpNormal = norm + (bump.x * tangent + bump.y * binormal);
bumpNormal = normalize(bumpNormal);
float gridstep = 1.0 / 64.0;
norm = normalize(norm);
float3 gridlights = (
lookup(posAsColour, float3(gridstep*-1,0,gridstep*-1),norm,bumpNormal)+
lookup(posAsColour, float3(gridstep*0, 0,gridstep*-1),norm,bumpNormal)+
lookup(posAsColour, float3(gridstep*1, 0,gridstep*-1),norm,bumpNormal)+
lookup(posAsColour, float3(gridstep*-1,0,gridstep*0 ),norm,bumpNormal)+
lookup(posAsColour, float3(gridstep*0, 0,gridstep*0 ),norm,bumpNormal)+
lookup(posAsColour, float3(gridstep*1, 0,gridstep*0 ),norm,bumpNormal)+
lookup(posAsColour, float3(gridstep*-1,0,gridstep*1 ),norm,bumpNormal)+
lookup(posAsColour, float3(gridstep*0, 0,gridstep*1 ),norm,bumpNormal)+
lookup(posAsColour, float3(gridstep*1, 0,gridstep*1 ),norm,bumpNormal));
//oColour.rgb = ambient.rgb + /*diffuse.rgb * */ gridlights;// + float3(ogrelight,ogrelight,ogrelight*0.7);
//oColour.a = 1;
float4 texcell = tex2D(Texture2, texCoord0);
gridlights.b = gridlights.b * 0.8;
oColour.rgb = (float3(0.3137254901960784, 0.3137254901960784, 0.3725490196078431) + gridlights) * texcell.rgb;
oColour.a = texcell.a;
//oColour.rgb = texcell.aaa;
//oColour.a = 1.0f;
}