# Tangent space normal mapping visual problem along UV islands (using Vulkan)

I'm learning Vulkan and computer graphics. First time implementing normal mapping. I've been struggling with this for days now and I can't seem to find the problem - so that's the question. What is wrong here and how to fix it?

First, here is a short description of how I handle things. On the CPU I calculate MVP and inverse M matrices, with the inverse M I transform the eye position and light positions from world to model space. Then I pass the MVP and model space positions to the vertex shader via a uniform buffer. The vertex shader calculates light vectors (vector from vertex position to light position) and the eye vector and transforms them to tangent space with the inverse TBN matrix (the shader gets the vectors T, B and N from the CPU via input attributes). This is then passed through the rasterizer to the fragment shader, from which point things are straight forward.

As one might imagine, the differently illuminated areas are UV islands.

The lighting still changes based on the position of the model.

Here's a picture of tangent space bases and light vectors in model space for each vertex. The bases are different for the double vertices along the seams, but that is expected due to different UV coordinates. The light vectors should still be valid in each tangent space (after being transformed by the inverse TBN matrix).

This is a color representation of one of the light vectors. There is a clear correlation between these vectors and the lighting problem. But as far as I can tell the change in color (in this image) along the seams is expected because the tangent spaces are orientated differently. One thing to note is the front right leg of the turtle - depending on the orientation of the turtle, some parts of some islands will be black for whatever reason.

Now, here is the relevant code:

#version 450

#define lightCount 2

layout(location = 0) in vec3 inPos;
layout(location = 1) in vec2 inUV;
layout(location = 2) in vec3 inNormal;
layout(location = 3) in vec3 inTangent;
layout(location = 4) in vec3 inBitangent;

layout(set = 0, binding = 0) uniform VSUniformBufferObject {
mat4 MVP;
vec4 eyePos;
vec4 lightPos[lightCount];
} ubo;

layout(location = 0) out vec2 outUV;
layout(location = 1) out vec3 outEyeVec;
layout(location = 2) out vec3 outLightVec[lightCount];

out gl_PerVertex {
vec4 gl_Position;
};

void main() {
gl_Position = ubo.MVP * vec4(inPos,  1.0);

outUV = inUV;

mat3 invTBN = transpose(mat3(inTangent, inBitangent, inNormal));

outEyeVec = invTBN * (ubo.eyePos.xyz - inPos);
for (int i = 0; i < lightCount; i++) {
outLightVec[i] = invTBN * (ubo.lightPos[i].xyz - inPos);
}
}


all the input attributes are in model space

#version 450

#define lightCount 2

layout(location = 0) in vec2 inUV;
layout(location = 1) in vec3 inEyeVec;
layout(location = 2) in vec3 inLightVec[lightCount];

layout(set = 2, binding = 0) uniform sampler2D samplers[2];
layout(set = 1, binding = 0) uniform FSUniformBufferObject {
vec4 lightColors[lightCount];
} ubo;

layout(push_constant) uniform PushConstants {
int mode;
} push;

layout(location = 0) out vec4 outColor;

const vec4 ambientComponent = vec4(0.05, 0.05, 0.05, 1.0);
const float maxDistance = 5.0;

void main() {
vec4 texColor = texture(samplers[0], inUV);
vec3 texNormal = texture(samplers[1], inUV).xyz;

vec3 N = normalize(texNormal);
float distance = length(inEyeVec);
vec3 E = inEyeVec / distance;

vec3 diffuseIncoming = vec3(0.0, 0.0, 0.0);
vec3 specularComponent = vec3(0.0, 0.0, 0.0);
for (int i = 0; i < lightCount; i++) {
float D = length(inLightVec[i]);
vec3 L = inLightVec[i] / D;
D = (distance + D)*(distance + D);

float cosTheta = max(dot(N, L), 0.0);
diffuseIncoming += ubo.lightColors[i].rgb * cosTheta * ubo.lightColors[i].a / D;

if(push.mode > 0) {
vec3 R = reflect(-L, N);
float cosAlpha = max(dot(E,R), 0.0);
specularComponent += ubo.lightColors[i].rgb * pow(cosAlpha, push.mode) * ubo.lightColors[i].a / D;
}
}

//outColor = texColor * (vec4(diffuseIncoming, 1.0) + ambientComponent) + vec4(specularComponent, 1.0);
outColor = vec4(normalize(inLightVec[0]), 1.0);
}


push.mode is zero so the specular lighting is off

drawing commands

glm::mat4 VP = P * V;
glm::mat4 invM;
Rendering::VSUniformBufferObject vsubo;
for (auto obj : objs) {
ModelInfo &m = modelInfos[obj.modelId];

vsubo.MVP = VP * obj.M;
invM = glm::inverse(obj.M);
for (int i = 0; i < 2; i++) {
vsubo.lightPos[i] = invM * lightPos[i];
}
vsubo.eyePos = invM * V[3];
vsUniformBuffer_m.fill(0, sizeof(Rendering::VSUniformBufferObject), static_cast<const void *>(&vsubo));

vkCmdBindDescriptorSets(b, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelines_m.pipelineLayoutPtrs[0], 2, 1, descriptorPool_m.descriptorSets.data() + obj.textureId + 3, 0, nullptr);

vkCmdDrawIndexed(b, m.size, 1, m.start, 0, 0);
}


EDIT 1:

I've rendered the dot product of N (normal vector) and L (light vector). The normal map itself looks fine - just like any other normal map, even when rendered onto the model. The light vector is correctly transformed into model space. As far as I can tell this means the only thing left that can be wrong is the calculation of the TBN matrix. I used blender to bake my normal map from multires. I'll append the code for T, B and N calculations.

std::unordered_map<Rendering::Vertex, uint32_t> uniqueVertices;

for (const auto& shape : shapes) {
int i = 0;
for (const auto& index : shape.mesh.indices) {
Rendering::Vertex vertex = {};

vertex.pos = {
attrib.vertices[3 * index.vertex_index + 0],
attrib.vertices[3 * index.vertex_index + 1],
attrib.vertices[3 * index.vertex_index + 2]
};

vertex.uv = {
attrib.texcoords[2 * index.texcoord_index + 0],
1.0f - attrib.texcoords[2 * index.texcoord_index + 1]
};

vertex.normal = {
attrib.normals[3 * index.normal_index + 0],
attrib.normals[3 * index.normal_index + 1],
attrib.normals[3 * index.normal_index + 2]
};

vertex.tangent = {};
vertex.bitangent = {};

if (uniqueVertices.count(vertex) == 0) {
uniqueVertices[vertex] = static_cast<uint32_t>(vertices.size());
vertices.push_back(vertex);
}

indices.push_back(uniqueVertices[vertex]);

i++;
if (!(i % 3)) {
size_t indicesSize = indices.size();
uint32_t i0 = indices[indicesSize - 3];
uint32_t i1 = indices[indicesSize - 2];
uint32_t i2 = indices[indicesSize - 1];
Rendering::Vertex &v0 = vertices[i0];
Rendering::Vertex &v1 = vertices[i1];
Rendering::Vertex &v2 = vertices[i2];

glm::vec2 duv1 = v1.uv - v0.uv;
glm::vec2 duv2 = v2.uv - v0.uv;
float k = 1 / (duv1.x*duv2.y - duv2.x*duv1.y);
glm::mat2x2 UV(duv2.y, -duv1.y, -duv2.x, duv1.x);
glm::mat2x3 E(v1.pos - v0.pos, v2.pos - v0.pos);
glm::mat2x3 TB = k*E*UV;

v0.tangent += TB[0];
v0.bitangent += TB[1];
v1.tangent += TB[0];
v1.bitangent += TB[1];
v2.tangent += TB[0];
v2.bitangent += TB[1];
}
}
}

for (int i = 0; i < vertices.size(); i++) {
vertices[i].tangent = glm::normalize(vertices[i].tangent);
vertices[i].bitangent = glm::normalize(vertices[i].bitangent);
glm::vec3 &T = vertices[i].tangent;
glm::vec3 &B = vertices[i].bitangent;
glm::vec3 &N = vertices[i].normal;
T = glm::normalize(T - glm::dot(T, N) * N);
B = glm::cross(N, T);
}


The comparison operator for the Vertex structure only compares position, normal and uv coordinates.

Turns out the problem was the way I interpreted the normal map. I was too focused on the transformations and it completely slipped my mind.

This:

vec3 texNormal = texture(samplers[1], inUV).xyz;


should have been this:

vec3 texNormal = 2.0 * texture(samplers[1], inUV).rgb - 1.0;


because (128, 128, 255) from the normal map maps to (0.0, 0.0, 1.0) in tangent space. So (128, 128, 128) would map to the origin - this would of course be an invalid normal, that's just how the coordinates from one space map to the other.

More details can be found here http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-13-normal-mapping/