Bump mapping

If we will use only one normal for the surface to calculate lighting, it will look flat. It is good for the polished surfaces. But for other surfaces, like a brick wall, it won't look realistic.

Bump mapping is technique to provide normals for each points of surface from special texture - bump or normal texture.

The normal texture has a general blue tone because overall, the normal is towards the "outside of the surface".

with bump mapping

bump texture example

The normal extracted from texture has coordinates in surface space, but the coordinates of the position of the light source and light direction are passed in view or world space. As you know, all calculations must be in the same space, so we need a matrix to transform coordinates from one space to another.

In math a surface space is called local tangent space.

TBN matrix allows transform from tangent space to the world or view space. There are two ways to use TBN matrix:

  • transform a bump normal extracted from texture to world space, then calc lighting. Transformation can be done only in fragment shader.
  • invert TBN matrix, transform lighting variables to the tangent space, then calc lighting. Inverse of matrix and transformations may be done in vertex shader, and result passed to the fragment shader.

As you can see, second way faster, because all transformations are performed in the vertex shader, i.e. for each vertex. First way slower, because transformation performed in fragment shader, i.e. for each pixel between the vertices.

tangent space

Let's consider an arbitrary triangle of the model. Triangle will have 3 vertices P0, P1, P2. Assume texture coordinates (s,t) are assigned to each vertex: P0(s0, t0), P1(s1, t1), P2(s2, t2). Then we can construct a coordinate system with following axis:

  • T-axis - parallel to the direction of increasing s (or t) texture coordinate
  • N-axis - the normal vector, is perpendicular to the local surface
  • B-axis - perpendicular to both N and T, like T, also lies on the surface

This coordinate system is called tangent space.

We can express vectors of triangle edges E1 and E2 in tangent space via (s,t) coordinates.

E1 = (s1-s0)T + (t1-t0)B = ds1T + dt1B
E2 = (s2-s0)T + (t2-t0)B = ds2T + dt2B

Also we can express vectors of triangle edges E1 and E2 in model space via vectors P0, P1, P2.

E1 = P1 - P0 = (x1-x0, y1-y0, z1-z0) 
E2 = P2 - P0 = (x2-x0, y2-y0, z2-z0)

Now join all together in matrix form:

|Ex1 Ey1 Ez1|
|Ex2 Ey2 Ez2|
=
|ds1  dt1|
|ds2  dt2|
|Tx Ty Tz|
|Bx By Bz|

We need T and B values.

|Tx Ty Tz|
|Bx By Bz|
=
|ds1  dt1|-1
|ds2  dt2|
|Ex1 Ey1 Ez1|
|Ex2 Ey2 Ez2|

Replace inverse matrix:

|Tx Ty Tz|
|Bx By Bz|
=
______1______
ds1dt2 - ds2dt1
|dt2  -dt1|
|-ds2  ds1|
|Ex1 Ey1 Ez1|
|Ex2 Ey2 Ez2|

Vector N usually known and is passed to the shader to calculate light.

Now we have three vectors T, B, N. This allows to us to construct transformation matrix from tangent space to the model space.

       |Tx  Bx  Nx|
TBN =  |Ty  By  Ny| 
       |Tz  Bz  Nz|
Calculate tangents and bitangents

Note 1. Tangents are calculated for each triangle. If you draw model by the vertex indices then one vertex of a triangle can be included in other neighboring triangles. So, final tangent value can be calculated as average of all calculated tangents for a given vertex. But this lead that TBN matrix loose orthogonality. It can be fixed by the Gram-Schmidt process in the shader.

//tangent and normal passed in the world space
vec3 normal = normalize(normal);
vec3 tangent = normalize(tangent);
tangent = normalize(tangent - dot(tangent, normal) * normal);
vec3 bitangent = cross(normal, tangent);

Note 2. You can calculate bitangent directly in vertex shader as cross product of the tangent and normal vector: vec3 bitangent = cross(normal, tangent);.

extracting bump normal

The normal is stored as a color so its components are in the range [0-1]. We transform it back to its original format using the function 'f(x) = 2 * x - 1'. This function maps 0 to -1 and 1 to 1 and is simply the reverse of what happened when the normal map was generated:

vec3 bumpNormal = texture(normalSampler, exTexPos).xyz;
bumpNormal = 2.0 * bumpNormal - vec3(1.0, 1.0, 1.0);

shader example

Shaders code
#version 330 core
precision mediump float;

struct Light {
 vec3 ambient; 
 vec3 diffuse; 
 vec3 direction; 
};

layout(location=0) in vec4 vPos;
layout(location=2) in vec3 vNormal;
layout(location=3) in vec2 vTexPos ;
layout(location=4) in vec3 vTangent;

uniform mat4 m;
uniform mat4 mNormal;
uniform Light light;

out vec2 exTexPos;
out Light exLight;

void prepareLight(){
  vec3 normal = normalize(m*vec4(vNormal,0)).xyz;
  vec3 tangent = normalize(m*vec4(vTangent,0)).xyz;
  tangent = normalize(tangent - dot(tangent, normal) * normal);
  vec3 bitangent = cross(normal, tangent);
  mat3 miTBN = transpose(mat3(tangent, bitangent, normal));
  exLight = Light(light.ambient,
      light.diffuse,  
      normalize(miTBN*(light.direction)));
}

void main() {
    gl_Position = m * vPos;
    exTexPos = vTexPos;
    prepareLight();
}
#version 330 core
precision mediump float;

struct Light {
    vec3 ambient; 
    vec3 diffuse; 
    vec3 direction; 
};

uniform sampler2D sampler;
uniform mat4 mNormal;
uniform sampler2D normalSampler;

out vec4 fragColor;
in vec2 exTexPos;
in Light exLight;

vec3 calcLight(){
    vec3 bumpNormal = texture(normalSampler, exTexPos).xyz;
    bumpNormal = 2.0 * bumpNormal - vec3(1.0, 1.0, 1.0);
    float f = max(0.0, dot(bumpNormal.xyz, exLight.direction));
    return  clamp(exLight.ambient + (exLight.diffuse * f), 0.0,1.0);
}

void main() {
    fragColor = vec4(1,1,1,1);
    fragColor = fragColor*texture(sampler, exTexPos);
    vec3 exLighting = calcLight();
    fragColor = vec4(fragColor.rgb * exLighting, fragColor.a);
}

You can download bump mapping example on GitHub.