Parallax occlusion mapping only accurate in one direction (with video and code)

Started by
5 comments, last by CDRZiltoid 1 year, 11 months ago

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);
}
Advertisement

whitwhoa said:
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.

When watching the video, i did not notice dependency on direction, but some overall warping effect, which feels strange.

It looks like by turning on the effect, the bricks move closer to the camera. I assume your pretty flat texture has an average ‘height’ different from zero to cause this movement.
So maybe, by fixing this, the true reasons for the bug would become more obvious. If you use a heightmap, this would be as easy as making sure the darkest sections in the image become black instead grey.

JoeJ said:

When watching the video, i did not notice dependency on direction, but some overall warping effect, which feels strange.

In the video, the first wall that the camera is looking at, which has a normal pointing in the -z direction, seems to produce the desired parallax occlusion affect. However, the second wall which the camera looks at has a normal pointing in the +x direction and produces the weird warping. Possibly unrelated I supposed, was just the only difference I could find between the faces where it seemed to work and the ones where it was warped.

JoeJ said:

I assume your pretty flat texture has an average ‘height’ different from zero to cause this movement.

That would be the `heightScale` uniform that is passed into the fragment shader. In the video I am adjusting that variable to demonstrate the effect.

JoeJ said:


So maybe, by fixing this, the true reasons for the bug would become more obvious. If you use a heightmap, this would be as easy as making sure the darkest sections in the image become black instead grey.

Afraid you lost me here. I am using a height map, but I'm not sure by what you mean when you say “by fixing this”?

whitwhoa said:
That would be the `heightScale` uniform that is passed into the fragment shader. In the video I am adjusting that variable to demonstrate the effect.

I see where you lost me.

Imagine we calculate distanceToTriangle = heightTexture * scale
And what i mean is that in the texture you may have values between 100 and 150. This causes the illusionary effect of the wall as a whole coming closer to the camera if you change the scale. Because of the 100 coming from the image.
If this 100 was a zero instead, the wall would not move, only the bricks would start to pop out. That's the expected relief effect, so maybe setting this to zero helps to limit some confusion.

But now i see myself the warping depends on global axis, which surely has nothing to do with that.
No idea. Maybe dtex needs to made from the inverse of TBN matrix, so it follows view direction in local texture space.

JoeJ said:
No idea. Maybe dtex needs to made from the inverse of TBN matrix, so it follows view direction in local texture space.

Thanks for the reply! I've been hacking around on it to see if something becomes apparent. Updated the following line:

//mat3 TBN = getTBN(Normal, -V, TexCoords);// from
mat3 TBN = getTBN(Normal, WorldPos, TexCoords);// to

Passing the world position of the vertex instead of the normalized difference between the camera position and world position seems to have made the warping better.

Now what I'm seeing is like the direction in which the parallax effect should be taking place is occurring on in the wrong axis for some of these wall meshes. Which I'm beginning to wonder if somehow my vertex normals are facing the wrong direction whenever they're loaded into the engine for the affected meshes. They're perfect in blender, but I'm exporting to .fbx, then importing via assimp, so it's possible that somewhere in between something's getting skewed. You can see in this updated video, whenever I clip to the opposite side of the mesh, the parallax seems to be working on the correct axis, just incorrect direction (for the second and third walls in video, the last wall appears to be fine like the first).

Might throw together a quick and dirty vertex normal shader to draw lines in the direction of the vertex normals in the scene…or just look at the data after it's loaded…that'd be quicker lol.

JoeJ said:
Maybe dtex needs to made from the inverse of TBN matrix, so it follows view direction in local texture space.

THIS!!! OMG THIS! You were correct indeed. Using the inverse of the TBN matrix in the parallax calculation appears to have corrected the issue. You are my hero ?

This topic is closed to new replies.

Advertisement