[Solved] Deferred lighting changes when camera moves

I’m touching up my deferred rendering lighting, and I’m noticing strange behavior with the G-Buffer normals. I can’t tell if there’s something I did wrong, or if it’s common, expected behavior.

Look at the first image, where everything seems correct. I have a square room, with the camera pointing down and forward, and a directional light pointing down and to the right. The vertices at the two far corners of the room have normals pointing diagonally towards the center (as shown), with the center of wall having it’s proper normal (also shown) as interpolated by the shader.

Now look at the second image. When I move the camera forward, something with the multiplication by WorldViewIT causes the normals to shift (as shown) and the entire far wall becomes dark. This somewhat makes sense since the light direction is in the same plane as the wall, but regardless of the lighting produced, it seems like the normals shouldn’t be changing.

Is this expected behavior? I feel like I’ve seen lighting like this in games before, but it’s strange to see the lighting change so much when I just move across the room.

I can share the shader code if necessary.

EDIT: the normals are expected to move because they are in view-space, but the lighting is not expected to change

Those normals should be in world space, right? If so, than that’s certainly not expected behaviour. World space normals shouldn’t be influenced by camera movements.

Are you multiplying your normals by WorldViewIT? If those are indeed world space normals, then you can’t involve the view matrix, because that makes the resulting normals dependent on the camera.
To get world space normals from object space normals, you should multiply only by the rotation matrix of the object, so basically the world matrix without translation and scale.

This is actually in view space. T, B, and N are multiplied by WorldViewIT to form the TBN matrix. normal * TBN is what is then written to the G-Buffer and shown in the images. In the light shader, these normals are then transformed “into World space by multiplying by the inverse of the View matrix” (according to the guide I’m following), although that seems odd. Are they trying to say that n * (W * V)^-1T * V^-1 == n * W ?

I see, it’s view space. Then it of course makes sense for the normals to change. The lighting however should never change this much. Tiny fluctuations due to imprecisions maybe, but nothing this severe. There’s definitely a bug somewhere.

I think it makes sense. The inverse transpose is only used to avoid “stretching of normals” for non-uniformly scaled objects, and shouldn’t influence things otherwise. If you only use uniformly scaled objects, you should be able to simplyfy this to
n * (W * V) * V^-1 == n * W
which makes perfect sense again.

If you post the relevant code, maybe I can spot something. I can’t make any promises though.

I believe the only unusual thing I do is have the “UnlitFactor” stored in Albedo.w which is used at composition time to output a weighted average of the lit color (color * lighting + specular) and the unlit diffuse color.

Here is the GBuffer shader

// Vertex Shader Constants
float4x4 World;
float4x4 View;
float4x4 Projection;
float4x4 WorldViewIT;

// Color Texture
texture Texture;

// Normal Texture
texture NormalMap;

// Specular Texture
texture SpecularMap;

// Manual Stencil
float UnlitFactor;

// Albedo Sampler
sampler AlbedoSampler = sampler_state
{
	texture = <Texture>;
	MINFILTER = LINEAR;
	MAGFILTER = LINEAR;
	MIPFILTER = LINEAR;
	ADDRESSU = WRAP;
	ADDRESSV = WRAP;
};

// NormalMap Sampler
sampler NormalSampler = sampler_state
{
	texture = <NormalMap>;
	MINFILTER = LINEAR;
	MAGFILTER = LINEAR;
	MIPFILTER = LINEAR;
	ADDRESSU = WRAP;
	ADDRESSV = WRAP;
};

// SpecularMap Sampler
sampler SpecularSampler = sampler_state
{
	texture = <SpecularMap>;
	MINFILTER = LINEAR;
	MAGFILTER = LINEAR;
	MIPFILTER = LINEAR;
	ADDRESSU = WRAP;
	ADDRESSV = WRAP;
};

// Vertex Input Structure
struct VSI
{
	float4 Position : POSITION0;
	float3 Normal : NORMAL0;
	float2 UV : TEXCOORD0;
	float3 Tangent : TANGENT0;
	float3 BiTangent : BINORMAL0;
};

// Vertex Output Structure
struct VSO
{
	float4 Position : POSITION0;
	float2 UV : TEXCOORD0;
	float3 Depth : TEXCOORD1;
	float3x3 TBN : TEXCOORD2;
};

// Vertex Shader
VSO VS(VSI input)
{
	// Initialize Output
	VSO output;

	// Transform Position
	float4 worldPosition = mul(input.Position, World);
	float4 viewPosition = mul(worldPosition, View);
	output.Position = mul(viewPosition, Projection);

	// Pass Depth
	output.Depth.x = output.Position.z;
	output.Depth.y = output.Position.w;
	output.Depth.z = viewPosition.z;

	// Build TBN Matrix
	output.TBN[0] = normalize(mul(float4(input.Tangent, 0), WorldViewIT).xyz);
	output.TBN[1] = normalize(mul(float4(input.BiTangent, 0), WorldViewIT).xyz);
	output.TBN[2] = normalize(mul(float4(input.Normal, 0), WorldViewIT));

	// Pass UV
	output.UV = input.UV;

	// Return Output
	return output;
}

// Pixel Output Structure
struct PSO
{
	float4 Albedo : COLOR0;
	float4 Normals : COLOR1;
	float4 Depth : COLOR2;
};

// Normal Encoding Function
half3 encode(half3 n)
{
	n = normalize(n);
	n.xyz = 0.5f * (n.xyz + 1.0f);
	return n;

	//half p = sqrt(n.z * 8 + 8);
	//return half4(n.xy / p + 0.5, 0, 0);
}

// Normal Decoding Function
half3 decode(half4 enc)
{
	return (2.0f * enc.xyz - 1.0f);

	//half2 fenc = enc * 4 - 2;
	//half f = dot(fenc, fenc);
	//half g = sqrt(1 - f / 4);
	//half3 n;
	//n.xy = fenc*g;
	//n.z = 1 - f / 2;
	//return n;
}

// Pixel Shader
PSO PS(VSO input)
{
	// Initialize Output
	PSO output;

	// Pass Albedo from Texture
	output.Albedo = tex2D(AlbedoSampler, input.UV);

	// Pass Extra - Can be whatever you want, in this case will be a Specular Value
	output.Albedo.w = UnlitFactor; // 0.5f;// tex2D(SpecularSampler, input.UV).w;

	// Read Normal From Texture
	half3 normal = tex2D(NormalSampler, input.UV).xyz * 2.0f - 1.0f;

	// Transform Normal to WorldViewSpace from TangentSpace
	normal = normalize(mul(normal, input.TBN));

	// Pass Encoded Normal
	output.Normals.xyz = encode(normal);

	// Pass Extra - Can be whatever you want, in this case will be a Specular Value
	output.Normals.w = 0.0f;// tex2D(SpecularSampler, input.UV).x;

	// Pass Depth(Screen Space, for lighting)
	output.Depth = input.Depth.x / input.Depth.y;

	// Pass Depth(View Space, for SSAO)
	output.Depth.g = input.Depth.z;

	// Return Output
	return output;
}

// Technique
technique Default
{
	pass p0
	{
		VertexShader = compile vs_3_0 VS();
		PixelShader = compile ps_3_0 PS();
	}
}

And the directional light shader

// Inverse View
float4x4 InverseView;

// Inverse View Projection
float4x4 InverseViewProjection;

// Camera Position
float3 CameraPosition;

// Light Vector
float3 L;

// Light Color
float4 LightColor;

// Light Intensity
float LightIntensity;

// GBuffer Texture Size
float2 GBufferTextureSize;

//// GBuffer Texture0
//sampler GBuffer0 : register(s0);
//// GBuffer Texture1
//sampler GBuffer1 : register(s1);
//// GBuffer Texture2
//sampler GBuffer2 : register(s2);

// GBuffer Texture0
texture GBufferTexture0;
sampler GBuffer0 = sampler_state
{
	texture = <GBufferTexture0>;
	MINFILTER = LINEAR;
	MAGFILTER = LINEAR;
	MIPFILTER = LINEAR;
	ADDRESSU = CLAMP;
	ADDRESSV = CLAMP;
};

// GBuffer Texture1
texture GBufferTexture1;
sampler GBuffer1 = sampler_state
{
	texture = <GBufferTexture1>;
	MINFILTER = LINEAR;
	MAGFILTER = LINEAR;
	MIPFILTER = LINEAR;
	ADDRESSU = CLAMP;
	ADDRESSV = CLAMP;
};

// GBuffer Texture2
texture GBufferTexture2;
sampler GBuffer2 = sampler_state
{
	texture = <GBufferTexture2>;
	MINFILTER = POINT;
	MAGFILTER = POINT;
	MIPFILTER = POINT;
	ADDRESSU = CLAMP;
	ADDRESSV = CLAMP;
};

// Vertex Input Structure
struct VSI
{
	float3 Position : POSITION0;
	float2 UV : TEXCOORD0;
};

// Vertex Output Structure
struct VSO
{
	float4 Position : POSITION0;
	float2 UV : TEXCOORD0;
};

// Vertex Shader
VSO VS(VSI input)
{
	// Initialize Output
	VSO output;

	// Just Straight Pass Position
	output.Position = float4(input.Position, 1);

	// Pass UV too
	output.UV = input.UV - float2(1.0f / GBufferTextureSize.xy);

	// Return
	return output;
}

// Manually Linear Sample
float4 manualSample(sampler Sampler, float2 UV, float2 textureSize)
{
	float2 texelpos = textureSize * UV;
	float2 lerps = frac(texelpos);
	float texelSize = 1.0 / textureSize.x;
	float4 sourcevals[4];
	sourcevals[0] = tex2D(Sampler, UV);
	sourcevals[1] = tex2D(Sampler, UV + float2(texelSize, 0));
	sourcevals[2] = tex2D(Sampler, UV + float2(0, texelSize));
	sourcevals[3] = tex2D(Sampler, UV + float2(texelSize, texelSize));

	float4 interpolated = lerp(lerp(sourcevals[0], sourcevals[1], lerps.x),
		lerp(sourcevals[2], sourcevals[3], lerps.x), lerps.y);

	return interpolated;
}

// Phong Shader with UnlitFactor as a manual stencil
float4 Phong(float3 Position, float3 N, float SpecularIntensity, float SpecularPower)
{
	// Calculate Reflection vector
	float3 R = normalize(reflect(L, N));

	// Calculate Eye vector
	float3 E = normalize(CameraPosition - Position.xyz);

	// Calculate N.L
	float NL = dot(N, -L);

	// Calculate Diffuse
	float3 Diffuse = NL * LightColor.rgb;

	// Calculate Specular
	float Specular = SpecularIntensity * pow(saturate(dot(R, E)), SpecularPower);

	// Calculate Final Product
	return LightIntensity * float4(Diffuse.rgb, Specular);
}

// Decoding of GBuffer Normals
float3 decode(float3 enc)
{
	return (2.0f * enc.xyz - 1.0f);

	//half2 fenc = enc * 4 - 2;
	//half f = dot(fenc, fenc);
	//half g = sqrt(1 - f / 4);
	//half3 n;
	//n.xy = fenc*g;
	//n.z = 1 - f / 2;
	//return n;
}

// Pixel Shader
float4 PS(VSO input) : COLOR0
{
	// Get All Data from Normal part of the GBuffer
	half4 encodedNormal = tex2D(GBuffer1, input.UV);

	// Decode Normal
	half3 Normal = mul(decode(encodedNormal.xyz), (float3x3)InverseView);

	// Get Specular Intensity from GBuffer
	float SpecularIntensity = encodedNormal.w;

	// Unlit Factor
	float UnlitFactor = tex2D(GBuffer0, input.UV).w;

	// Get Specular Power from GBuffer
	float SpecularPower = 128;// encodedNormal.w * 255;

	// Get Depth from GBuffer
	float Depth = manualSample(GBuffer2, input.UV, GBufferTextureSize).x;

	// Calculate Position in Homogenous Space
	float4 Position = 1.0f;
	Position.x = input.UV.x * 2.0f - 1.0f;
	Position.y = -(input.UV.x * 2.0f - 1.0f);
	Position.z = Depth;

	// Transform Position from Homogenous Space to World Space
	Position = mul(Position, InverseViewProjection);
	Position /= Position.w;

	// Return Phong Shaded Value
	float4 Light = Phong(Position.xyz, Normal, SpecularIntensity, SpecularPower);
	//Light = (1 - UnlitFactor) * Light + UnlitFactor * float4(1, 1, 1, 0);

	return Light;
}

// Technique
technique Default
{
	pass p0
	{
		VertexShader = compile vs_3_0 VS();
		PixelShader = compile ps_3_0 PS();
	}
}

Everything looks correct and the light doesn’t move if I use world-space normals: multiply T, B, and N by World, and then no more need to transform back later. Is there a reason to use one over the other? I believe I read that world-space is better if you use lots of post-processing, but view-space is better otherwise. However, if this is the only change I need to make, then world-space is clearly more efficient.

On a first glance I can’t see a problem with the shader code.
My guess is that when you build the WorldViewIT matrix in C#, the translations in your world/view matrices are causing the problem. I’m pretty sure the “inverse transpose trick” only works when there is only rotation and scale. So to build that matrix properly, you have to get rid of the camera and object translation first.

If your object has uniform scale, then just removing the inverse transpose completely should fix it. Can you try that as a quick test? Just make sure the object has a uniform scale and pass the regular WorldView matrix to your WorldViewIT shader parameter, without inversing and transposing. In that case you don’t need to get rid of the translations, as they shouldn’t affect you shader anyway.

Of course I could be completely wrong.

You’re right, when I pass just world * view it works correctly. How can I ensure this will work with non-uniform scaling too though?

Also for reference, this SO thread mentions the following on world-space vs view-space normals:

  • View-space: “all normals in the g-buffer are pointing towards the camera (assuming you are not rendering back-faces). That makes compressing the normals into the G-Buffer much easier”
  • World-space: “because it makes my SSAO implementation (including the computation of bent normals) quicker. There are many other post-processing effects that work more efficiently in world-space, which I suspect is why Unreal Engine 4 and CryEngine 3 also use world-space lighting”

When I made my deferred renderer in the past I used world space, just because it was the most straight forward solution that came to mind. I’m not really a deferred renderer expert, it was just a little hobby project. There might be good reasons to use view space instead, but I wouldn’t know what those are.
What you mentioned about better normal compression makes sense though. In view space you should be able to store the normals in 2 floats instead of 3. If that’s the only reason, I don’t think I would go for it though. I like the simplicity of world space, and I have the feeling that, using view space, I would just end up transforming to world space a lot. Like you mentioned, post processing and stuff.

When you calculate WorldViewIT just set WorldView.Translation = Vector3.Zero before you invert and transpose it.

Thanks, that fixed the view-space calculations.

So I suppose in the absence of other post processing, it’s a choice of 1 extra float per pixel vs. an extra CPU invert and matrix multiplication per vertex buffer. It seems like world-space is still the way to go anyway

Unless there are other advantages of view space, that I don’t know of, then I would also go with world space. You also save the extra transformation to world space in your lighting shader.

Since we are talking optimization, another tip: If you don’t need the world and view position in the vertex shader, like in your GBuffer shader, you can calculate the combined WorldViewProjection matrix on the CPU and then just do output.Position = mul(input.Position, WorldViewProjection) in the vertex shader. Unless your vertex count is really low, that should be faster.

EDIT: oh, I just noticed you’re actually using the view position. In that case you can only combine world and view.

Thanks, good tip! I think the viewPosition.z is used for view-space SSAO later. I’m not sure if there’s another way it can be reconstructed, but for now I’ll take the savings of precomputing world * view.

EDIT: although I forgot, since I’m putting normals into world-space now, I do still need to pass world, but it’s better to do the multiplication once on CPU than per vertex on GPU.