3
\$\begingroup\$

I have been following a few tutorials to learn some graphics programming with OpenGL, and recently implemented Normal Mapping.

It works well for the most part, especially for objects with normal-mapped textures. However, I get some strange problems with some models when I give them a try - weird triangular artifacts show up. The lighting looks good for the most part, but these artifacts really ruin it.

One such example would be this - you can see the chassis of the car is quite affected. Other parts appear fine. BMW Model - Test 1 In other cases (I'm sure it's somehow related) the model appears split in half and one side is lit completely wrong. BMW Model - Rear

I'm confident that it's to do with how I've written my vertex shader but as far as I can see, everything is 'correct' with regards to the tutorials I've followed. I'm hoping someone can take a look and let me know if I'm missing something - maths is not my strong suit and if it's an issue with the matrix math or anything like that I'm unlikely to spot it :/

Strangely enough, if I change the line mat3 TBN = transpose(mat3(T, B, N)); to simply mat3 TBN = (mat3(T, B, N)); the strange artifacts go away however the lighting goes strangely smooth, like so: smooth beamer

Here's my vertex shader code:

#version 330

layout (location=0) in vec3 aPosition;
layout (location=1) in vec3 aNormal;
layout (location=2) in vec2 aTexCoord;
layout (location=3) in vec3 aTangent;
layout (location=4) in vec4 aJointWeights;
layout (location=5) in ivec4 aJointIndices;

const int MAX_WEIGHTS = 4;
const int MAX_JOINTS = 150;

out VS_OUT{
    vec2 texCoord;
    vec3 fragPos;
    vec3 tangentViewPos;
    vec3 tangentFragPos;
    vec3 vertexNormal;
    mat3 TBN;
} vs_out;

uniform vec3 viewPos;
uniform mat4 projectionMatrix;
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 jointMatrices[MAX_JOINTS];

void main()
{ 
    vec4 initPos = vec4(0);
    vec4 initNormal = vec4(0);
    vec4 initTangent = vec4(0);

    int count = 0;
    for (int i = 0; i < MAX_WEIGHTS; i++){
        float weight = aJointWeights[i];
        if (weight > 0){
            count++;
            int jointIndex = aJointIndices[i];

            vec4 tmpPos = jointMatrices[jointIndex] * vec4(aPosition, 1.0);
            initPos += weight * tmpPos;

            vec4 tmpNormal = jointMatrices[jointIndex] * vec4(aNormal, 0.0);
            initNormal += weight * tmpNormal;

            vec4 tmpTangent = jointMatrices[jointIndex] * vec4(aTangent, 0.0);
            initTangent += weight * tmpTangent;
        }
    }
    if (count == 0){
        initPos = vec4(aPosition, 1.0);
        initNormal = vec4(aNormal, 0.0);
        initTangent = vec4(aTangent, 0.0);
    }

    vs_out.fragPos = vec3(modelMatrix * vec4(initPos.xyz, 1.0));
    vs_out.texCoord = aTexCoord;

    mat3 normalMatrix = transpose(inverse(mat3(modelMatrix)));
    vec3 T = normalize(vec3(normalMatrix * initTangent.xyz));
    vec3 N = normalize(vec3(normalMatrix * initNormal.xyz));
    T = normalize(T - dot(T, N) * N);
    vec3 B = cross(N, T);
    if (dot(cross(N, T), B) < 0.0)
        T = T * -1.0;

    mat3 TBN = transpose(mat3(T, B, N));
    vs_out.tangentViewPos = TBN * viewPos;
    vs_out.tangentFragPos = TBN * vs_out.fragPos;

    vs_out.TBN = TBN;    
    vs_out.vertexNormal = vec3(normalMatrix * initNormal.xyz);

    gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(initPos.xyz, 1.0);
}

And here's the code I use for the fragment shader:

#version 330

in VS_OUT{
    vec2 texCoord;
    vec3 fragPos;
    vec3 tangentViewPos;
    vec3 tangentFragPos;
    vec3 vertexNormal;
    mat3 TBN;
} fs_in;

layout (location = 0) out vec4 fragColor;
layout (location = 1) out vec4 brightColor;

struct Material {
    vec4 ambient;
    vec4 diffuse;
    vec4 specular;
    float shininess;
    float alphaTest;

    float opacity;

    sampler2D diffuseMap;
    int hasDiffuseMap;            

    sampler2D normalMap;
    int hasNormalMap;

    sampler2D specularMap;
    int hasSpecularMap;

    sampler2D exponentMap;
    int hasExponentMap;

    sampler2D parallaxMap;
    int hasParallaxMap;
    float parallaxHeightScale;

    vec2 uvMultiplier;
};

struct Light {
    vec3 position;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};

struct DirectionalLight{
    vec3 direction;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};

struct PointLight{
    vec3 position;

    float constant;
    float linear;
    float quadratic;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};

struct SpotLight{
    vec3 position;
    vec3 direction;

    float cutOff;
    float outerCutOff;

    float constant;
    float linear;
    float quadratic;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};

struct Environment{
    samplerCube cubeMap;
    samplerCube depthMap;
    float farPlane;
};

#define POINT_LIGHTS 
uniform PointLight pointLights[POINT_LIGHTS];
uniform int numPointLights;
uniform DirectionalLight dirLight;

uniform Material material;
uniform Environment environment;

uniform int blinn;
uniform int halfLambert;
uniform int fresnel;

uniform vec3 viewPos;

vec3 gridSamplingDisk[20] = vec3[]
(
   vec3(1, 1,  1), vec3( 1, -1,  1), vec3(-1, -1,  1), vec3(-1, 1,  1), 
   vec3(1, 1, -1), vec3( 1, -1, -1), vec3(-1, -1, -1), vec3(-1, 1, -1),
   vec3(1, 1,  0), vec3( 1, -1,  0), vec3(-1, -1,  0), vec3(-1, 1,  0),
   vec3(1, 0,  1), vec3(-1,  0,  1), vec3( 1,  0, -1), vec3(-1, 0, -1),
   vec3(0, 1,  1), vec3( 0, -1,  1), vec3( 0, -1, -1), vec3( 0, 1, -1)
);

float fZero = pow( (1.0f-(1.0f/1.31f))/ (1.0f+(1.0f/1.31f)), 2);

vec4 dMap;
vec3 sMap;
vec3 nMap;

float specularExponent;

float shadowCalculation(vec3 lightPos, vec3 fragPos){
    vec3 fragToLight = fragPos - lightPos;
    float currentDepth = length(fragToLight);

    float shadow = 0.0;
    float bias = 0.10;
    int samples = 20;
    float viewDistance = length(viewPos - fragPos);
    float diskRadius = (1.0 + (viewDistance / environment.farPlane)) / 35.0;
    for(int i = 0; i < samples; ++i)
    {
        float closestDepth = texture(environment.depthMap, fragToLight + gridSamplingDisk[i] * diskRadius).r;
        closestDepth *= environment.farPlane;
        if(currentDepth - bias > closestDepth)
            shadow += 1.0;
    }
    shadow /= float(samples);

    return shadow;
}

float calcFresnel(vec3 v, vec3 h, float f0){
    float base = 1-dot(v, h);
    float exp = pow(base, 5);
    return exp + f0 * (1.0 - exp);
}

vec4 calcDirLight(DirectionalLight light, vec3 normal, vec3 viewDir){
    vec3 lightDir = normalize((fs_in.TBN * -light.direction) - fs_in.tangentFragPos);    
    float diff = max(dot(lightDir, normal), 0.0);

    vec3 halfwayDir = normalize(lightDir + viewDir);
    vec3 reflectDir = reflect(-lightDir, normal);

    float spec = pow(max(dot(normal, halfwayDir), 0.0), specularExponent);

    vec3 ambient = light.ambient;
    vec3 diffuse = light.diffuse * diff;
    vec3 specular = light.specular * spec * sMap;    

    return vec4(ambient + diffuse + specular, 1.0) * dMap;
}

vec4 calcPointLight(PointLight light, vec3 normal, vec3 fPos, vec3 vPos){
    vec3 tangentLightPos = (fs_in.TBN * light.position);
    vec3 lightDir = normalize(tangentLightPos - fPos);

    float diff = max(dot(lightDir, normal), 0.0);

    vec3 viewDir = normalize(vPos - fPos);
    vec3 halfwayDir = normalize(lightDir + viewDir);
    float spec = pow(max(dot(normal, halfwayDir), 0.0), specularExponent);

    float distance = length(tangentLightPos - fPos);
    float attenuation = 1.0 / (light.constant + light.linear * distance + 
                        light.quadratic * (distance * distance));

    vec3 ambient = light.ambient;
    vec3 diffuse = diff * light.diffuse;
    vec3 specular = light.specular * spec * sMap; // * light.specular * sMap * frs;

    ambient *= attenuation;
    diffuse *= attenuation;
    specular *= attenuation;

    float shadow = shadowCalculation(light.position, fs_in.fragPos);
    vec3 lighting = (ambient + (1 - shadow) * (diffuse + specular)) * dMap.rgb;

    return vec4(lighting, dMap.a);
}

vec2 parallaxMapping(vec2 texCoords, vec3 viewDir){    
    const float minLayers = 8.0;
    const float maxLayers = 32.0;

    float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir)));

    float layerDepth = 1.0 / numLayers;
    float currentLayerDepth = 0.0;

    vec2 P = viewDir.xy * material.parallaxHeightScale;
    vec2 deltaTexCoords = P / numLayers;

    vec2 currentTexCoords = texCoords;
    float currentDepthValue = texture(material.parallaxMap, currentTexCoords).r;

    while (currentLayerDepth < currentDepthValue){
        currentTexCoords -= deltaTexCoords;
        currentDepthValue = texture(material.parallaxMap, currentTexCoords).r;
        currentLayerDepth += layerDepth;
    }

    vec2 prevTexCoords = currentTexCoords + deltaTexCoords;
    float afterDepth = currentDepthValue - currentLayerDepth;
    float beforeDepth = texture(material.parallaxMap, prevTexCoords).r - currentLayerDepth + layerDepth;

    float weight = afterDepth / (afterDepth - beforeDepth);
    vec2 finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight);

    return finalTexCoords;
}

void main()
{
    vec2 ourTexCoords;
    vec3 viewDir = normalize(fs_in.tangentViewPos - fs_in.tangentFragPos);

    if (material.hasParallaxMap){
        ourTexCoords = parallaxMapping(fs_in.texCoord, viewDir);
        if(ourTexCoords.x > 1.0 || ourTexCoords.y > 1.0 || ourTexCoords.x < 0.0 || ourTexCoords.y < 0.0)
            discard;
    }else{
        ourTexCoords = fs_in.texCoord * material.uvMultiplier;
    }

    float matOpacity;
    if (material.hasDiffuseMap){
        dMap = texture(material.diffuseMap, ourTexCoords);    
        matOpacity = dMap.a;
    }else{
        dMap = vec4(material.diffuse.rgb, 0.0);
        matOpacity = material.opacity;
    }

    if (material.hasSpecularMap){
        sMap = texture(material.specularMap, ourTexCoords).rgb;
    }else{
        sMap = material.specular.rgb;
    }

    vec3 normal;
    if (material.hasNormalMap){
        nMap = texture(material.normalMap, ourTexCoords).rgb;
        normal = normalize(nMap * 2.0 - 1.0);
    }else{
        normal = normalize(fs_in.vertexNormal);
    }

    if (material.hasExponentMap){
        specularExponent = 255 - (texture(material.exponentMap, ourTexCoords).r * 255);
    }else{
        specularExponent = material.shininess;
    }

    vec4 result = calcDirLight(dirLight, normal, viewDir);
    for (int i = 0; i < numPointLights; i++){
        result += calcPointLight(pointLights[i], normal, fs_in.tangentFragPos, fs_in.tangentViewPos);
    }

    fragColor = vec4(result.rgb, matOpacity);  

    float brightness = dot(fragColor.rgb, vec3(0.2126, 0.7152, 0.0722));
    if (brightness > 1){
        brightColor = vec4(fragColor.rgb, 1.0);
    }else{
        brightColor = vec4(0, 0, 0, 1);
    }
}

I appreciate any help in advance and hope that we can figure it out :)

\$\endgroup\$
  • \$\begingroup\$ Make sure that you actually have tangent and binormal data in the shader by outputting them as colour components. color = vec4((tangent.rgb+vec3(1.0))*vec3(2.0), 1.0) \$\endgroup\$ – Ocelot Aug 31 '19 at 19:32
  • \$\begingroup\$ Good suggestion - I've been using RenderDoc to visualize that and I can definitely see that those things are in there. \$\endgroup\$ – Dimitri Velovski Aug 31 '19 at 23:34
  • \$\begingroup\$ Do they look correct? \$\endgroup\$ – Ocelot Sep 2 '19 at 2:30
  • \$\begingroup\$ If you are using Assimp to load your model, ensure you are telling it on load to compute tangents. \$\endgroup\$ – Ian Young Sep 2 '19 at 10:32
  • \$\begingroup\$ Your tangent space might be incorrect - can you show us how you're calculating your tangent vectors as input to this shader? In particular, it looks like you might have a line of symmetry down the middle of the car, resulting in flipped tangents on one half - can you confirm this based on your UV unwrap? \$\endgroup\$ – DMGregory Sep 3 '19 at 2:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.