I've been working on implementing parallax occlusion mapping in a custom engine. I have everything setup and working (kinda), however it only seems to be accurate when the normals of the mesh are facing the -z axis. You can see this behavior in the following video. I was hoping someone might be able to take a look and see if there's anything obvious that I've missed or have setup incorrectly.
Extremely basic vertex shader:
#version 450 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
out vec2 TexCoords;
out vec3 WorldPos;
out vec3 Normal;
uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
void main()
{
TexCoords = aTexCoords;
WorldPos = vec3(model * vec4(aPos, 1.0));
Normal = mat3(model) * aNormal;
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
Fragment shader:
#version 450 core
out vec4 FragColor;
in vec2 TexCoords;
in vec3 WorldPos;
in vec3 Normal;
// material parameters
uniform sampler2D albedoMap;
uniform sampler2D normalMap;
uniform sampler2D metallicMap;
uniform sampler2D roughnessMap;
uniform sampler2D aoMap;
uniform sampler2D heightMap;
// IBL
uniform samplerCube irradianceMap;
uniform samplerCube prefilterMap;
uniform sampler2D brdfLUT;
// lights - FOR TESTING
vec3 lightPositions[1] = vec3[1](vec3(4.0, 4.0, -4.0));
vec3 lightColors[1] = vec3[1](vec3(50.0, 100.0, 50.0));
uniform vec3 camPos;
uniform float heightScale;
const float PI = 3.14159265359;
vec2 ParallaxMapping(vec3 V, mat3 TBN, float hs, sampler2D tex, vec2 uv)
{
const float NUM_PARALLAX_OCCLUSION_STEPS = 32;
V = V / length(V); // ?
V = TBN * V;
vec2 uv_dx = dFdxCoarse(uv);
vec2 uv_dy = dFdyCoarse(uv);
float layerHeight = 1.0 / NUM_PARALLAX_OCCLUSION_STEPS;
float curLayerHeight = 0;
vec2 dtex = hs * V.xy / NUM_PARALLAX_OCCLUSION_STEPS;
vec2 currentTextureCoords = uv;
float heightFromTexture = 1 - textureGrad(tex, currentTextureCoords, uv_dx, uv_dy).r;
int iter = 0;
while(heightFromTexture > curLayerHeight && iter < NUM_PARALLAX_OCCLUSION_STEPS)
{
curLayerHeight += layerHeight;
currentTextureCoords -= dtex;
heightFromTexture = 1 - textureGrad(tex, currentTextureCoords, uv_dx, uv_dy).r;
iter++;
}
vec2 prevTCoords = currentTextureCoords + dtex;
float nextH = heightFromTexture - curLayerHeight;
float prevH = 1 - textureGrad(tex, prevTCoords, uv_dx, uv_dy).r - curLayerHeight + layerHeight;
float weight = nextH / (nextH - prevH);
vec2 finalTextureCoords = prevTCoords * weight + currentTextureCoords * (1.0 - weight);
return finalTextureCoords;
}
// ----------------------------------------------------------------------------
mat3 getTBN(vec3 N, vec3 P, vec2 UV)
{
// get edge vectors of the pixel triangle
vec3 dp1 = dFdx(P);
vec3 dp2 = dFdy(P);
vec2 duv1 = dFdx(UV);
vec2 duv2 = dFdy(UV);
// solve the linear system
vec3 dp2perp = cross(dp2, N);
vec3 dp1perp = cross(N, dp1);
vec3 T = dp2perp * duv1.x + dp1perp * duv2.x;
vec3 B = dp2perp * duv1.y + dp1perp * duv2.y;
// construct a scale-invariant frame
float invmax = inversesqrt(max(dot(T,T), dot(B,B)));
mat3 TBN = mat3(T * invmax, B * invmax, N);
return TBN;
}
// ----------------------------------------------------------------------------
vec3 getNormalFromMap(mat3 TBN, vec2 uv)
{
//vec3 map = texture(normalMap, fs_in.TexCoords).xyz * 2.0 - 1.0;
vec3 map = texture(normalMap, uv).xyz * 2.0 - 1.0;
return normalize(TBN * map);
}
// ----------------------------------------------------------------------------
float DistributionGGX(vec3 N, vec3 H, float roughness)
{
float a = roughness*roughness;
float a2 = a*a;
float NdotH = max(dot(N, H), 0.0);
float NdotH2 = NdotH*NdotH;
float nom = a2;
float denom = (NdotH2 * (a2 - 1.0) + 1.0);
denom = PI * denom * denom;
return nom / denom;
}
// ----------------------------------------------------------------------------
float GeometrySchlickGGX(float NdotV, float roughness)
{
float r = (roughness + 1.0);
float k = (r*r) / 8.0;
float nom = NdotV;
float denom = NdotV * (1.0 - k) + k;
return nom / denom;
}
// ----------------------------------------------------------------------------
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
{
float NdotV = max(dot(N, V), 0.0);
float NdotL = max(dot(N, L), 0.0);
float ggx2 = GeometrySchlickGGX(NdotV, roughness);
float ggx1 = GeometrySchlickGGX(NdotL, roughness);
return ggx1 * ggx2;
}
// ----------------------------------------------------------------------------
vec3 fresnelSchlick(float cosTheta, vec3 F0)
{
return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
// ----------------------------------------------------------------------------
vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness)
{
return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
// ----------------------------------------------------------------------------
void main()
{
vec3 V = normalize(camPos - WorldPos);
mat3 TBN = getTBN(Normal, -V, TexCoords); // should we pass V as negative here or not?
vec2 parTexCoord = ParallaxMapping(V, TBN, heightScale, heightMap, TexCoords); // should we pass V as negative here or not?
vec3 albedo = pow(texture(albedoMap, parTexCoord).rgb, vec3(2.2));
float alpha = texture(albedoMap, parTexCoord).a;
float metallic = texture(metallicMap, parTexCoord).r;
float roughness = texture(roughnessMap, parTexCoord).r;
float ao = texture(aoMap, parTexCoord).r;
vec3 N = getNormalFromMap(TBN, parTexCoord);
vec3 R = reflect(-V, N);
vec3 F0 = vec3(0.04);
F0 = mix(F0, albedo, metallic);
vec3 Lo = vec3(0.0);
for(int i = 0; i < 4; ++i)
{
vec3 L = normalize(lightPositions[i] - WorldPos);
vec3 H = normalize(V + L);
float distance = length(lightPositions[i] - WorldPos);
float attenuation = 1.0 / (distance * distance);
vec3 radiance = lightColors[i] * attenuation;
float NDF = DistributionGGX(N, H, roughness);
float G = GeometrySmith(N, V, L, roughness);
vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0);
vec3 numerator = NDF * G * F;
float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.0001; // + 0.0001 to prevent divide by zero
vec3 specular = numerator / denominator;
vec3 kS = F;
vec3 kD = vec3(1.0) - kS;
kD *= 1.0 - metallic;
float NdotL = max(dot(N, L), 0.0);
Lo += (kD * albedo / PI + specular) * radiance * NdotL;
}
vec3 F = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness);
vec3 kS = F;
vec3 kD = 1.0 - kS;
kD *= 1.0 - metallic;
vec3 irradiance = texture(irradianceMap, N).rgb;
vec3 diffuse = irradiance * albedo;
const float MAX_REFLECTION_LOD = 4.0;
vec3 prefilteredColor = textureLod(prefilterMap, R, roughness * MAX_REFLECTION_LOD).rgb;
vec2 brdf = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).rg;
vec3 specular = prefilteredColor * (F * brdf.x + brdf.y);
vec3 ambient = (kD * diffuse + specular) * ao;
float brightness = 0.1; // adding this static for test
//vec3 ambient = albedo * brightness * ao; // test
//vec3 color = ambient + Lo;
vec3 color = (ambient * brightness) + Lo;
//vec3 color = (ambient * brightness);
color = color / (color + vec3(1.0));
color = pow(color, vec3(1.0/2.2));
//FragColor = vec4(color, 1.0);
FragColor = vec4(color, alpha);
}