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
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.
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
fun calcTangent(
vPos: FloatArray, vTexPos: FloatArray,
srcIndices: IntArray?,
vTangent: FloatArray, vBitangent: FloatArray?
) {
val posComponents = 3
val texPosComponets = 2
var offset: Int
val indices : IntArray = srcIndices ?: IntArray(vPos.size / posComponents)
if (srcIndices == null) {
for (i in indices.indices) {
indices[i] = i
}
}
// coordinates of points of triangle
val p0 = Vector3f()
val p1 = Vector3f()
val p2 = Vector3f()
// texture coordinates of points of triangle
val pTex0 = Vector2f()
val pTex1 = Vector2f()
val pTex2 = Vector2f()
// for storing calculated edges (p1,p0) and (p2,p0)
val deltaP10 = Vector3f()
val deltaP20 = Vector3f()
// for storing calculated edges
// in texture coordinates (pTex1, pTex0) and (pTex2,pTex0)
val deltaTex10 = Vector2f()
val deltaTex20 = Vector2f()
//
val tmp1 = Vector3f()
val tmp2 = Vector3f()
val tangent = Vector3f()
val bitangent = Vector3f()
var i = 0
while (i < indices.size) {
offset = indices[i] * posComponents
//p0[vPos[offset], vPos[offset + 1]] = vPos[offset + 2]
p0.set(vPos[offset], vPos[offset + 1], vPos[offset + 2]) // set x,y,z
offset = indices[i + 1] * posComponents
p1.set(vPos[offset], vPos[offset + 1], vPos[offset + 2]) // set x,y,z
offset = indices[i + 2] * posComponents
p2.set(vPos[offset], vPos[offset + 1], vPos[offset + 2]) // set x,y,z
offset = indices[i] * texPosComponets
pTex0.set(vTexPos[offset], vTexPos[offset + 1])
offset = indices[i + 1] * texPosComponets
pTex1.set(vTexPos[offset], vTexPos[offset + 1])
offset = indices[i + 2] * texPosComponets
pTex2.set(vTexPos[offset], vTexPos[offset + 1])
// calc edges
p1.sub(p0, deltaP10)
p2.sub(p0, deltaP20)
pTex1.sub(pTex0, deltaTex10)
pTex2.sub(pTex0, deltaTex20)
val r = 1.0f / (deltaTex10.x * deltaTex20.y
- deltaTex10.y * deltaTex20.x)
/* tangent = (deltaP10 * deltaTex20.y
- deltaP20 * deltaTex10.y)*r; */
tmp1.set(deltaP10).mul(deltaTex20.y)
.sub(tmp2.set(deltaP20).mul(deltaTex10.y), tangent)
tangent.mul(r)
// add same tangent for each points of triangle
// if point already had tangent we will
// average tangent from old value and new value
offset = indices[i] * posComponents
vTangent[offset] += tangent.x
vTangent[offset + 1] += tangent.y
vTangent[offset + 2] += tangent.z
offset = indices[i + 1] * posComponents
vTangent[offset] += tangent.x
vTangent[offset + 1] += tangent.y
vTangent[offset + 2] += tangent.z
offset = indices[i + 2] * posComponents
vTangent[offset] += tangent.x
vTangent[offset + 1] += tangent.y
vTangent[offset + 2] += tangent.z
vBitangent?.also {
/* bitangent = (deltaP20 * deltaTex10.x
- deltaP10 * deltaTex20.x)*r; */
tmp1.set(deltaP20).mul(deltaTex10.x)
.sub(tmp2.set(deltaP10).mul(deltaTex20.x), bitangent)
bitangent.mul(r)
// add same bitangent for each points of triangle
// if point already had bitangent we will
// average bitangent from old value and new value
offset = indices[i] * posComponents
it[offset] += bitangent.x
it[offset + 1] += bitangent.y
it[offset + 2] += bitangent.z
offset = indices[i + 1] * posComponents
it[offset] += bitangent.x
it[offset + 1] += bitangent.y
it[offset + 2] += bitangent.z
offset = indices[i + 2] * posComponents
it[offset] += bitangent.x
it[offset + 1] += bitangent.y
it[offset + 2] += bitangent.z
}
i += 3
}
normalize3(vTangent)
vBitangent?.apply {
normalize3(this)
}
}
fun normalize3(src: FloatArray) {
val v = Vector3f()
var i = 0
while (i < src.size) {
v.set(src[i], src[i + 1], src[i + 2])
v.normalize()
src[i] = v.x
src[i + 1] = v.y
src[i + 2] = v.z
i += 3
}
}
public static void calcTangent(float[] vPos,
float[] vTexPos,
int[] indices,
float[] vTangent, float[] vBitangent) {
int posComponents = 3;
int texPosComponets = 2;
int offset;
if (indices == null) {
indices = new int[vPos.length / posComponents];
for (int i = 0; i < indices.length; i++) {
indices[i] = i;
}
}
// coordinates of points of triangle
Vector3f p0 = new Vector3f();
Vector3f p1 = new Vector3f();
Vector3f p2 = new Vector3f();
// texture coordinates of points of triangle
Vector2f pTex0 = new Vector2f();
Vector2f pTex1 = new Vector2f();
Vector2f pTex2 = new Vector2f();
// for storing calculated edges (p1,p0) and (p2,p0)
Vector3f deltaP10 = new Vector3f();
Vector3f deltaP20 = new Vector3f();
// for storing calculated edges
// in texture coordinates (pTex1, pTex0) and (pTex2,pTex0)
Vector2f deltaTex10 = new Vector2f();
Vector2f deltaTex20 = new Vector2f();
//
Vector3f tmp1 = new Vector3f();
Vector3f tmp2 = new Vector3f();
final Vector3f tangent = new Vector3f();
final Vector3f bitangent = new Vector3f();
for (int i = 0; i < indices.length; i += 3) {
offset = indices[i] * posComponents;
p0.set(vPos[offset], vPos[offset + 1], vPos[offset + 2]);
offset = indices[i + 1] * posComponents;
p1.set(vPos[offset], vPos[offset + 1], vPos[offset + 2]);
offset = indices[i + 2] * posComponents;
p2.set(vPos[offset], vPos[offset + 1], vPos[offset + 2]);
offset = indices[i] * texPosComponets;
pTex0.set(vTexPos[offset], vTexPos[offset + 1]);
offset = indices[i + 1] * texPosComponets;
pTex1.set(vTexPos[offset], vTexPos[offset + 1]);
offset = indices[i + 2] * texPosComponets;
pTex2.set(vTexPos[offset], vTexPos[offset + 1]);
// calc edges
p1.sub(p0, deltaP10);
p2.sub(p0, deltaP20);
pTex1.sub(pTex0, deltaTex10);
pTex2.sub(pTex0, deltaTex20);
float r = 1.0f / (deltaTex10.x * deltaTex20.y
- deltaTex10.y * deltaTex20.x);
// tangent = (deltaP10 * deltaTex20.y - deltaP20 * deltaTex10.y)*r;
tmp1.set(deltaP10).mul(deltaTex20.y)
.sub(tmp2.set(deltaP20).mul(deltaTex10.y), tangent);
tangent.mul(r);
// add same tangent for each points of triangle
// if point already had tangent we will
// average tangent from old value and new value
offset = indices[i] * posComponents;
vTangent[offset] += tangent.x;
vTangent[offset + 1] += tangent.y;
vTangent[offset + 2] += tangent.z;
offset = indices[i + 1] * posComponents;
vTangent[offset] += tangent.x;
vTangent[offset + 1] += tangent.y;
vTangent[offset + 2] += tangent.z;
offset = indices[i + 2] * posComponents;
vTangent[offset] += tangent.x;
vTangent[offset + 1] += tangent.y;
vTangent[offset + 2] += tangent.z;
if (vBitangent != null) {
/* bitangent = (deltaP20 * deltaTex10.x
- deltaP10 * deltaTex20.x)*r; */
tmp1.set(deltaP20).mul(deltaTex10.x)
.sub(tmp2.set(deltaP10).mul(deltaTex20.x),
bitangent);
bitangent.mul(r);
// add same bitangent for each points of triangle
// if point already had bitangent we will
// average bitangent from old value and new value
offset = indices[i] * posComponents;
vBitangent[offset] += bitangent.x;
vBitangent[offset + 1] += bitangent.y;
vBitangent[offset + 2] += bitangent.z;
offset = indices[i + 1] * posComponents;
vBitangent[offset] += bitangent.x;
vBitangent[offset + 1] += bitangent.y;
vBitangent[offset + 2] += bitangent.z;
offset = indices[i + 2] * posComponents;
vBitangent[offset] += bitangent.x;
vBitangent[offset + 1] += bitangent.y;
vBitangent[offset + 2] += bitangent.z;
}
}
normalize3(vTangent);
if (vBitangent != null) {
normalize3(vBitangent);
}
}
public static void normalize3(float[] src) {
Vector3f v = new Vector3f();
for (int i = 0; i < src.length; i += 3) {
v.set(src[i], src[i + 1], src[i + 2]);
v.normalize();
src[i] = v.x;
src[i + 1] = v.y;
src[i + 2] = v.z;
}
}
Model.calcTangent = function(vPos, vTexPos, srcIndices, vTangent, vBitangent) {
if (!vPos || !vTexPos) {
throw "Wrong arguments to calculate calcTangent()";
}
const posComponents = 3;
const texPosComponets = 2;
var offset;
const indices = !srcIndices ? new Array(vPos.length / posComponents) : srcIndices;
vTangent.fill(0.0);
if (!!vBitangent) {
vBitangent.fill(0.0);
}
if (!srcIndices) {
for (var i = 0; i < indices.length; ++i) {
indices[i] = i;
}
}
// const { vec3, vec2 } = glMatrix;
const vec3 = glMatrix.vec3;
const vec2 = glMatrix.vec2;
// coordinates of points of triangle
const p0 = vec3.create();
const p1 = vec3.create();
const p2 = vec3.create();
// texture coordinates of points of triangle
const pTex0 = vec2.create();
const pTex1 = vec2.create();
const pTex2 = vec2.create();
// for storing calculated edges (p1,p0) and (p2,p0)
const deltaP10 = vec3.create();
const deltaP20 = vec3.create();
// for storing calculated edges
// in texture coordinates (pTex1, pTex0) and (pTex2,pTex0)
const deltaTex10 = vec2.create();
const deltaTex20 = vec2.create();
//
const tmp1 = vec3.create();
const tmp2 = vec3.create();
const tangent = vec3.create();
const bitangent = vec3.create();
var i = 0;
while (i < indices.length) {
offset = indices[i] * posComponents;
vec3.set(p0, vPos[offset], vPos[offset + 1], vPos[offset + 2]);
offset = indices[i + 1] * posComponents;
vec3.set(p1, vPos[offset], vPos[offset + 1], vPos[offset + 2]);
offset = indices[i + 2] * posComponents;
vec3.set(p2, vPos[offset], vPos[offset + 1], vPos[offset + 2]);
offset = indices[i] * texPosComponets;
vec2.set(pTex0, vTexPos[offset], vTexPos[offset + 1]);
offset = indices[i + 1] * texPosComponets;
vec2.set(pTex1, vTexPos[offset], vTexPos[offset + 1]);
offset = indices[i + 2] * texPosComponets;
vec2.set(pTex2, vTexPos[offset], vTexPos[offset + 1]);
// calc edges
vec3.sub(deltaP10, p1, p0);
vec3.sub(deltaP20, p2, p0);
vec2.sub(deltaTex10, pTex1, pTex0);
vec2.sub(deltaTex20, pTex2, pTex0);
const r = 1.0 / (deltaTex10[0] * deltaTex20[1]
- deltaTex10[1] * deltaTex20[0]);
/* tangent = (deltaP10 * deltaTex20.y
- deltaP20 * deltaTex10.y)*r;*/
vec3.scale(tmp1, deltaP10, deltaTex20[1]);
vec3.scale(tmp2, deltaP20, deltaTex10[1]);
vec3.sub(tangent, tmp1, tmp2);
vec3.scale(tangent, tangent, r);
// add same tangent for each points of triangle
// if point already had tangent we will
// average tangent from old value and new value
offset = indices[i] * posComponents;
vTangent[offset] += tangent[0]; // x
vTangent[offset + 1] += tangent[1]; // y
vTangent[offset + 2] += tangent[2]; // z
offset = indices[i + 1] * posComponents;
vTangent[offset] += tangent[0];
vTangent[offset + 1] += tangent[1];
vTangent[offset + 2] += tangent[2];
offset = indices[i + 2] * posComponents;
vTangent[offset] += tangent[0];
vTangent[offset + 1] += tangent[1];
vTangent[offset + 2] += tangent[2];
if (!!vBitangent) {
/* bitangent = (deltaP20 * deltaTex10.x
- deltaP10 * deltaTex20.x)*r; */
vec3.scale(tmp1, deltaP20, deltaTex10[0]);
vec3.scale(tmp2, deltaP10, deltaTex20[0]);
vec3.sub(bitangent, tmp1, tmp2);
vec3.scale(bitangent, bitangent, r);
// add same bitangent for each points of triangle
// if point already had bitangent we will
// average bitangent from old value and new value
offset = indices[i] * posComponents;
vBitangent[offset] += bitangent[0];
vBitangent[offset + 1] += bitangent[1];
vBitangent[offset + 2] += bitangent[2];
offset = indices[i + 1] * posComponents;
vBitangent[offset] += bitangent[0];
vBitangent[offset + 1] += bitangent[1];
vBitangent[offset + 2] += bitangent[2];
offset = indices[i + 2] * posComponents;
vBitangent[offset] += bitangent[0];
vBitangent[offset + 1] += bitangent[1];
vBitangent[offset + 2] += bitangent[2];
}
i += 3;
}
Model.normalize3(vTangent);
if (!!vBitangent) {
Model.normalize3(vBitangent);
}
}
Model.normalize3 = function(src) {
const v = glMatrix.vec3.create();
var i = 0;
while (i < src.length) {
glMatrix.vec3.set(v, src[i], src[i + 1], src[i + 2]);
glMatrix.vec3.normalize(v, v);
src[i] = v[0];
src[i + 1] = v[1];
src[i + 2] = v[2];
i += 3;
}
}
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: