Advertisement

How to add shadow from directional light in deffered rendering

Started by December 06, 2024 08:01 PM
6 comments, last by Sekt4nt 2 weeks ago

I have shader for geometry pass and for light pass, I understand theory how it should works, but when I try to implement this, my brain explode. Could some one explain me how could I implement shadows from my directional light here ?

******************** GEOMETRY SHADER *************************

cbuffer ProjectionBuffer : register(b0)
{
matrix projectionMatrix;
};

cbuffer ViewBuffer : register(b1)
{
matrix viewMatrix;
};

cbuffer WorldBuffer : register(b2)
{
matrix worldMatrix;
};

struct VSInput
{
float3 Position : POSITION;
float2 TexCoord : TEXCOORD;
float3 Normal : NORMAL;
float3 Tangent : TANGENT;
};

struct VSOutput
{
float4 Position : SV_Position;
float2 TexCoord : TEXCOORD;
float3 WorldPos : WORLD_POS;
float3 Normal : NORMAL;
float3 Tangent : TANGENT;
};

VSOutput main(VSInput input)
{
VSOutput output;

float4 worldPos = mul(float4(input.Position, 1.0f), worldMatrix);
float4 viewPos = mul(worldPos, viewMatrix);
output.Position = mul(viewPos, projectionMatrix);

output.WorldPos = worldPos.xyz;
output.Normal = mul((float3x3)worldMatrix, input.Normal);
output.Tangent = mul((float3x3)worldMatrix, input.Tangent);
output.TexCoord = input.TexCoord;

return output;
}

*******************************************************

cbuffer AmbientLight : register(b1)
{
float3 ambientLight;
float padding;
};

Texture2D myTexture : register(t0);
SamplerState samplerState : register(s0);

struct PSInput
{
float4 Position : SV_Position;
float2 TexCoord : TEXCOORD;
float3 WorldPos : WORLD_POS;
float3 Normal : NORMAL;
float3 Tangent : TANGENT;
};

struct PSOutput
{
float4 Diffuse : SV_Target0;
float4 Normal : SV_Target1;
float4 Position : SV_Target2;
float4 Plane : SV_Target3;
};

PSOutput main(PSInput input)
{
PSOutput output;

output.Diffuse = myTexture.Sample(samplerState, input.TexCoord) * float4(ambientLight, 1.0f);

output.Normal = float4(normalize(input.Normal), 1.0);

output.Position = float4(input.WorldPos, 1.0);

return output;
}

***************** LIGHT SHADER **************

struct VSInput
{
float3 Position : POSITION;
float2 TexCoord : TEXCOORD;
};

struct VSOutput
{
float4 Position : SV_Position;
float2 TexCoord : TEXCOORD;
};

VSOutput main(VSInput input)
{
VSOutput output;
output.Position = float4(input.Position, 1.0);
output.TexCoord = input.TexCoord;
return output;
}

***********************************************

struct Light
{
int Type;
float3 Position;
float3 Direction;
float3 Color;
float Intensity;
float Range;
float SpotAngle;
};

struct DirectionalLight
{
float3 Direction;
float Intensity;
float3 Color;
float Padding;
};

StructuredBuffer<Light> Lights : register(t4);
cbuffer LightInfo : register(b0)
{
int LightCount;
int align[3];
};

cbuffer DirectionalLight : register(b2)
{
DirectionalLight directionalLight;
};

float3 CalculateDirectionalLight(DirectionalLight directionalLight, float3 normal)
{
float3 lightDir = normalize(-directionalLight.Direction);
float diff = max(dot(normal, lightDir), 0.0);
return directionalLight.Color * diff * directionalLight.Intensity;
}

float3 CalculatePointLight(Light light, float3 worldPos, float3 normal)
{
float3 lightDir = light.Position - worldPos;
float distance = length(lightDir);
lightDir = normalize(lightDir);

float diff = max(dot(normal, lightDir), 0.0);
float attenuation = 1.0 / (distance * distance + 1e-4);

if (distance < light.Range)
{
return light.Color * diff * attenuation * light.Intensity;
}
return float3(0, 0, 0);
}

float3 CalculateSpotLight(Light light, float3 worldPos, float3 normal)
{
float3 lightDir = light.Position - worldPos;
float distance = length(lightDir);
lightDir = normalize(lightDir);

float theta = dot(lightDir, normalize(-light.Direction));
float epsilon = cos(radians(light.SpotAngle) * 0.5);

if (theta > epsilon && distance < light.Range)
{
float diff = max(dot(normal, lightDir), 0.0);
float attenuation = 1.0 / (distance * distance + 1e-4);
float spotEffect = smoothstep(epsilon, 1.0, theta);

return light.Color * diff * attenuation * spotEffect * light.Intensity;
}
return float3(0, 0, 0);
}

Texture2D DiffuseTexture : register(t0);
Texture2D NormalTexture : register(t1);
Texture2D PositionTexture : register(t2);
SamplerState samplerState : register(s0);

struct PSInput
{
float4 Position : SV_Position;
float2 TexCoord : TEXCOORD;
};

float4 main(PSInput input) : SV_Target
{
float4 diffuse = DiffuseTexture.Sample(samplerState, input.TexCoord);
float3 normal = normalize(NormalTexture.Sample(samplerState, input.TexCoord).xyz);
float3 worldPos = PositionTexture.Sample(samplerState, input.TexCoord).xyz;

float3 finalColor = float3(1.0f, 1.0f, 1.0f);

finalColor += CalculateDirectionalLight(directionalLight, normal);

for (int i = 0; i < LightCount; ++i)
{
Light light = Lights[i];
if (light.Type == 0)
{
finalColor += CalculatePointLight(light, worldPos, normal);
}
else if (light.Type == 1)
{
finalColor += CalculateSpotLight(light, worldPos, normal);
}
}

return float4(finalColor * diffuse.rgb, diffuse.a);
}

None

You should not think about ‘adding shadow’. Think about ‘adding light’ instead.

Typically it's implenented like so: Whenever you calculate the contribution of some light source, you first calculate attenuation, spot light mask, etc. If either of these terms is zero, the light can't be visible from the shading point and you can skip it.
If they are not zero, the light is potentially visible, and then we need to calculate if the light is visible for real or not. The term ‘shadow’ thus means in practice: 1 - visibility. (I often see people confusing terms here. They often have a variable 'shadow', although it should be called ‘visibility’. This casues confusing code sampels and tutorials.)

In a deferred renderer you usually do all those calculations in one spot, either in a CS or PS which does the shading. It looks like so, for example:

vec3 accumLighting = 0.0;
for (int i=0; i<N; i++)
{
	Light &light = lights[i];
	// from your code:
	// ...
	float attenuation = 1.0 / (distance * distance + 1e-4);
	float spotEffect = smoothstep(epsilon, 1.0, theta);
	// ...
	float f = diff * attenuation * spotEffect * light.Intensity; 
	// (btw, there's another misleading variable name - diff associates to the difference of two 3D points, so we expect a 3D vector. But you use it for a dot product, which is a scalar value that has no dimensions. So your choice of variable name is wrong and causes confusion.)
	
	if (f > 0.0) // the light is potentially visible; now check if the light can see the shading point for real
	{
		float visibility = LookUpShadowMap(light, ...); // returns 0 if shadowed, 1 if visible, or something in between in penumubra or due to filtering. 
		f *= visibility;
	}
 	accumLighting += light.Color * f;
}

For unshadowed lights we just assume visibility to be 1 in any case.

Advertisement

@JoeJ How complete shader would looks like ? Because I dont have even shadow map implemented

None

Sekt4nt said:
@JoeJ How complete shader would looks like ? Because I dont have even shadow map implemented

Well, implementing a deferred renderer is the next point on my todo list, and i lack former experience myself, but i can give some example…

In a traditional deferred renderer, the pixel shader only writes out the G-buffer data, but it does no shading or lighting.

The shading happens in a second pass, where either a CS or a full screen quad PS reads spatial and material pixel data from the G-buffer, and iterates over a list of lights, summing up their contribution to shade the pixels. So the code would basically look like my example above, no matter if we use CS or PS.

However, CS has a potential advantage over PS: The many threads of a workgroup can use fast on chip memory (LDS) to process common data, allowing to implement parallel algorithms.

A related example where this is useful would be to build a screen space grid, so we can support a larger number of lights.
Say we have 1000 lights. It would be too expensive to iterate a list of 1000 lights for each pixel. So we make grid cells of 16x16 pixel, and bin the lights to the cells they overlap. A CS can do this very efficiently at negligible cost, and typical parallel algorithms like prefix sum are the building blocks.
After that we shade our pixels in 16x16 tiles, so all threads of our group read the same list of lights, iterating maybe 3 lights per tile on average instead of 1000.
If we use CS for the shading, we also gain control over which threads process which pixels. By using a workgroup size of 16x16 we can ensure hardware thread groups map exactly to one cell of our light grid, so we read only one list of lights and share them over all threads, saving bandwidth. This could be a reason to prefer CS also for the shading, for example.

@JoeJ Thats sound complicated, could you please check my new thread where I particulary added shadows, but have problem with showing them ?

Click here

None

Sekt4nt said:
Thats sound complicated,

It's not complicated. The idea is to have a list of lights per screenspace tile, so we don't need to process all lights per pixel.
It's actually simple and trivial.
But what makes it complicated is background on technical details about how GPUs work,
and mainly the need to figure out how to implement such simple ideas using complex gfx APIs.

Ofc. that's something you can eventually care about later. Initially it's easier to have a small number lights, so iterating all of them is no problem.

I can't really help on the SM problem. My first guess is that the projection matrix for the light is wrong, which might explain the weired geometry shown by the debugger.
You shoukld use the ‘insert code block’ button for the code you post. Otherwise it's very hard to read and people who could help might skip the post.

Advertisement

@undefined thanks

None

Advertisement