Lighting, specular maps and tangent-space normal mapping.

or, its a textured quad Jim, but not as we know it

Took some time out of my busy schedule (ha!) to tinker with some shader maths, it struck me that I'd not actually written something that actually does correct tangent space normal mapping, my terrain shader was just a hack really.

For that shader I just sample the normal map straight onto the object directly, without transforming the sampled normal to the surface - admittedly, it was only to provide a certain amount of texture to the terrain, which it does quite nicely and at a much lower cost than calculating the tangent basis properly.

This time round I'm implementing it properly - my 'vision' for this game requires a good quality lighting model, the geometry is not going to be massively complex and the lighting will make the difference.

This shot is just the ground plane of what will be the blacksmiths shop, its nothing complicated yet, but I'm definitely pleased with the lighting and the texture.


The shader code is pretty straight forward, there is some optimisation to do, but on the whole this is reasonably tidy. Inputs to the shader are your usual lighting parameters, with an extra vec2 v2LightIsPoint which allows the shader to render both directional and point lights; the diffuse, specular, and normal map textures; and interpolated position, tex-coord, surface normal, and view direction.

I think the view direction could actually be constant, rather than iterated without much of a visual change, at a slight reduction in acuity.

uniform sampler2D sDiffuseTex;
uniform sampler2D sNormalMap;
uniform sampler2D sSpeclTex;

// Material/Light colours
uniform vec4 v4Ambient;
uniform vec4 v4LightCol;
uniform vec4 v4LightCol2;

// 2 Lights - can be point or directional (if v2LightIsPoint==1.0 or 0.0)
uniform vec2 v2LightIsPoint;
uniform vec3 v3LightDir;
uniform vec3 v3LightDir2;
uniform float fShininess;

varying vec3 v3Position;
varying vec2 v2TexCoord;
varying vec3 v3Normal;
varying vec3 v3ViewDir;

void main(void)
    vec3 q0 = dFdx(;
    vec3 q1 = dFdy(;
    vec2 st0 = dFdx(;
    vec2 st1 = dFdy(;
    vec3 S = normalize( q0 * st1.t - q1 * st0.t);
    vec3 N = v3Normal;
    vec3 T = cross(N, S);
    //S = cross(N, T); // Not completely needed, but possibly produces a more accurate basis.

    vec4 v4Diffuse = texture2D(sDiffuseTex, v2TexCoord);
    vec3 v3NrmlMap = normalize(2.0 * texture2D(sNormalMap, v2TexCoord).rgb - 1.0);

    // Ensure normal map is normalised (might be redundant)
    v3NrmlMap = normalize(v3NrmlMap);

    // Transform by tangent basis to get surface normal in WS
    mat3 m3WSToTS;
    m3WSToTS[0] = S;
    m3WSToTS[1] = T;
    m3WSToTS[2] = N;
    v3NrmlMap = m3WSToTS * v3NrmlMap;

    // Lighting calculations
    float fSpecularIntensity = texture2D(sSpeclTex, v2TexCoord).r;
    v3ViewDir = normalize(v3ViewDir);

    // Light 1
    vec3 v3LightVec = normalize(v3LightDir - v3Position * v2LightIsPoint.x);
    float fNDotL = clamp(dot(v3NrmlMap, v3LightVec), 0.0, 1.0); 

    // Specular Calc 1
    vec3 r = normalize(2 * dot(v3LightVec, v3NrmlMap) * v3NrmlMap - v3LightVec);
    float fDotProduct = dot(r, v3ViewDir);
    vec4 v4Specular1 = fSpecularIntensity * v4LightCol * max(pow(fDotProduct , fShininess), 0);

    // Light 2
    v3LightVec = normalize(v3LightDir2 - v3Position * v2LightIsPoint.y);
    float fNDotL2 = clamp(dot(v3NrmlMap, v3LightVec), 0.0, 1.0);

    // Specular Calc 2
    r = normalize(2 * dot(v3LightVec, v3NrmlMap) * v3NrmlMap - v3LightVec);
    fDotProduct = dot(r, v3ViewDir);
    vec4 v4Specular2 = fSpecularIntensity * v4LightCol2 * max(pow(fDotProduct , fShininess), 0);

    gl_FragColor = v4Ambient
                 + fNDotL  * v4Diffuse * v4LightCol
                 + fNDotL2 * v4Diffuse * v4LightCol2
                 + v4Specular1
                 + v4Specular2;

I also noticed an interesting little ditty, the graphics card performed far better when using vec4's than vec3's (for colours). I had tried writing the shader on 3 component vectors and just doing vec4(v3Colour, 1.0) in the final accumulation step, thinking it might make better use of alu pipes. But it seems its probably better to let the compiler work it out itself! (I might investigate this further on some newer architectures as I noticed this coding against an old Ati HD4650 - the difference was 1290fps vs 1250fps, or around ~3%).

The next step on the 3D front is to start creating the room, which currently is largely going to be an extruded grid based affair, but first I'm going to work on a bit of game logic, the first prototypes of the sword crafting and smithing 'mini games'.

Posted By 1 at 12:36:12 on 2013-09-04. Comments (0)