Advertisement Jump to content
Sign in to follow this  

Normal mapping in deferred and forward rendering

This topic is 744 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

I have implemented normal mapping in forward and deferred rendering paths but the results are slightly different (see the attached images and maybe download to compare them better). In forward rendering I'm doing the calculations in tangent space and in deferred rendering I'm doing the calculations in world space. I guess the difference comes from interpolating values between vertex and fragment shader. Let's see the most interesting parts of the shader code (I have narrowed the problem down to the value of "float diff"):



Vertex shader

N = normalize(mat3(M) * normal); // M = model to world space matrix
T = normalize(mat3(M) * tangent);
B = cross(T, N);
mat3 TBN = transpose(mat3(T, B, N)); // Transpose to get transform from world to tangent space
lightPos_Tan = TBN * lightPos_World; // Output to fragment shader
vertexPos_Tan = TBN * vertexPos_World; // Output to fragment shader

Fragment shader

vec3 normal_Tan = normalize(texture(normalTexture, texCoord).rgb * 2.0 - vec3(1.0));
vec3 lightDir_Tan = normalize(lightPos_Tan - vertexPos_Tan); // Inputs from vertex shader
float diff = clamp(dot(normal_Tan, lightDir_Tan), 0, 1);


GBuffer vertex shader

vec3 N = normalize(mat3(M) * normal); // M = model to world space matrix
vec3 T = normalize(mat3(M) * tangent);
vec3 B = cross(T, N);
TBN = mat3(T, B, N); // Output to fragment shader

GBuffer fragment shader

vec3 normal = normalize(texture(normalTexture, texCoord).rgb * 2.0 - vec3(1.0));
outNormal = normalize(TBN * normal); // TBN = Input from vertex shader

Deferred lighting fragment shader

vec3 normal = texture(normalTexture, texCoord).rgb; // Normal in world space written in gbuffer fragment shader
vec3 lightDir = normalize(lightPos - vertexPos); // Positions in world space
float diff = clamp(dot(normal, lightDir), 0, 1);

Do the shaders look OK? My guess is that the difference comes from interpolation between vertex and fragment shader: in forward rendering there are two vec3s and in deferred rendering there is a mat3. If in forward rendering the TBN matrix is created on fragment shader the results seem nearly identical (but this would be inefficient). Hence my questions are:


1. What could explain the small difference in the attached images?

2. How is mat3 interpolated between vertex and fragment shader?

3. What should be done to make results identical between forward and deferred rendering?

Share this post

Link to post
Share on other sites

What gbuffer format are you using? Have you tried verifying the gbuffer contents to ensure no precision/srgb/etc shenanigans are interfering?

Share this post

Link to post
Share on other sites
I would implement world-space shading in forward too, and then work to make your TS and WS forward shaders look the same. This will be simpler by removing the other differences of forward vs deferred.

The issue is likely that, while WS is well defined and consistent for the whole world, TS is defined per vertex, and we then interpolate it to get a new TS definition per pixel.

When interpolating a direction vector, you should normalize it in the pixel shader to avoid any unintended scaling. When interpolating three basis vectors, you should reorthonormalize them in the pixel shader (use cross to make sure they're all at right angles) to avoid unintended skewing.

Share this post

Link to post
Share on other sites
I treat the TBN matrix as 3 (2 + 1 extrapolated) orthonormal vectors that are interpolated by the fragment shader.

// attributes
attribute vec3 vtx_t;
attribute vec3 vtx_n;

varying vec3 t_interp, n_interp;

//vertex shader
t_interp = M * vec4( vtx_t, 0.0 );
n_interp = M * vec4( vtx_n, 0.0 );

//pixel shader
vec3 t, b, n;
t = normalize( t_interp );
b = normalize( cross( n_interp, t_interp ) );
n = cross( t, b );

vec3 norm = normalize( 2.0 * texture2D( tex, uv ).rgb - vec3(1.0) );

norm = norm.x * t + norm.y * b + norm.z * n;

The gotcha here is that like the vertex normal, the tangent and bitangent vectors are usually interpolated across the fragment shader, and unless they are the same across all 3 vertices, they will be skewed.

I tend to interpolate only two and extract the third during the re-orthonormalization process.

Share this post

Link to post
Share on other sites
Sign in to follow this  

  • Advertisement

Important Information

By using, you agree to our community Guidelines, Terms of Use, and Privacy Policy. is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!