I’m implementing shadows in my game but I also want my shader to be as general as possible.
My first implementation had 3 different functions:
layout(binding = 0) uniform sampler2DArrayShadow u_dirShadowMap;
layout(binding = 1) uniform sampler2DArrayShadow u_pointShadowMap;
layout(binding = 2) uniform sampler2DArrayShadow u_spotShadowMap;
#define CASCADE_COUNT 4
#define POINT_LIGHT_FACE_COUNT 6
float DirShadow(const uint lightIndex, const vec3 worldPosition, const vec3 geoNormal, const vec3 lightDir, const float NoL)
{
const uint textureID = (lightIndex * CASCADE_COUNT) + getShadowCascade(worldPosition);
const mat4 vp = u_shadowData.viewProjection[textureID];
const vec3 dirShadowMapCoords = getShadowUV(vp, worldPosition, geoNormal, lightDir, NoL, getDirTexelSizeWorldSpace(vp));
return PCF_Shadow(dirShadowMapCoords.xy, dirShadowMapCoords.z, u_dirShadowMap, textureID);
}
float PointShadow(const uint lightIndex, const vec3 worldPosition, const vec3 lightPosition, const vec3 geoNormal, const float NoL)
{
vec3 r = worldPosition - lightPosition;
const uint face = getPointLightFace(r);
const uint dirTextureCount = getDirLightCount() * CASCADE_COUNT;
const uint shadowIndex = face + (lightIndex * POINT_LIGHT_FACE_COUNT);
const uint matrixIndex = dirTextureCount + shadowIndex;
const vec3 dir = normalize(lightPosition - worldPosition);
const float texelSizeWorldSpace = getSpotTexelSizeWorldSpace(45.0f * DEG_TO_RAD);
const vec3 pointShadowMapCoords = getShadowUV(u_shadowData.viewProjection[matrixIndex], worldPosition, geoNormal, dir, NoL, texelSizeWorldSpace);
return PCF_Shadow(pointShadowMapCoords.xy, pointShadowMapCoords.z, u_pointShadowMap, shadowIndex);
}
float SpotShadow(const uint lightIndex, const vec3 worldPosition, const vec3 geoNormal, const vec3 lightDir, const float NoL, const float angle)
{
const uint dirTextureCount = getDirLightCount() * CASCADE_COUNT;
const uint pointTextureCount = getPointLightCount() * POINT_LIGHT_FACE_COUNT;
const uint matrixIndex = dirTextureCount + pointTextureCount + lightIndex;
const float texelSizeWorldSpace = getSpotTexelSizeWorldSpace(angle);
const vec3 spotShadowMapCoords = getShadowUV(u_shadowData.viewProjection[matrixIndex], worldPosition, geoNormal, lightDir, NoL, texelSizeWorldSpace);
return PCF_Shadow(spotShadowMapCoords.xy, spotShadowMapCoords.z, u_spotShadowMap, lightIndex);
}
but there’s a lot of similarities in these functions and I thought of having only one function
float Shadow(const ShadowParams shadowParams, const sampler2DArrayShadow shadowMap)
{
return PCF_Shadow(shadowParams.uv.xy, shadowParams.uv.z, shadowMap, shadowParams.shadowIndex);
}
and now there’s a problem. There are 3 types of lights: directional, point, and spot. Each one calculates their shadow map face, texel world size, and shadow matrix index differently.
I’m not sure how I should handle this.
I could do the if
statements to check for which light type this shadow function should work, but
if the scene has only directional lights, the shader would also do the point/spot light part due to branching. The same happens if only points or spotlights are in the scene.
It should not be a problem if there are all types of lights, but I want this shader to be as general as possible.
Thus I thought of calculating “differences” (shadow map face, texel world size, and shadow matrix index) as vec3s and then choosing the desired value by index like this:
#define SHADOW_TYPE_DIR 0
#define SHADOW_TYPE_POINT 1
#define SHADOW_TYPE_SPOT 2
#define CASCADE_COUNT 4
#define POINT_LIGHT_FACE_COUNT 6
#define SPOT_LIGHT_FACE_COUNT 1
float getDirTexelSizeWorldSpace(const mat4 viewProjection)
{
const vec3 p = vec3(0.5f, 0.5f, 0.0f);
const float n = viewProjection[0][0];
const float A = viewProjection[1][1];
const float B = viewProjection[3][1];
const float dz = A - p.y;
const float j = -B / (n * dz * dz);
const mat3 J = mat3(
j * dz, 0.0f, 0.0f,
j * p.x, j * n, 0.0f,
0.0f, j * p.z, j * dz
);
const float ures = getShadowTexelSize();
const float vres = getShadowTexelSize();
const vec3 Jx = J[0] * ures;
const vec3 Jy = J[1] * vres;
return max(length(Jx), length(Jy));
}
float getSpotTexelSizeWorldSpace(const float angle)
{
return 2.0f * tan(angle * DEG_TO_RAD) * getShadowTexelSize();
}
uint getShadowCascade(const vec3 worldPosition)
{
float z = MulMat4x4Float3(getView(), worldPosition).z;
uvec4 greaterZ = uvec4(greaterThan(getCascadeSplits(), vec4(z)));
return clamp(greaterZ.x + greaterZ.y + greaterZ.z + greaterZ.w, 0, getCascadeCount() - 1);
}
uint getPointLightFace(const vec3 r)
{
vec4 tc;
float rx = abs(r.x);
float ry = abs(r.y);
float rz = abs(r.z);
float d = max(rx, max(ry, rz));
if (d == rx) {
return (r.x >= 0.0 ? 0 : 1);
} else if (d == ry) {
return (r.y >= 0.0 ? 2 : 3);
} else {
return (r.z >= 0.0 ? 4 : 5);
}
}
uvec3 getMatrixOffset()
{
uvec3 v;
v.x = 0;
v.y = getDirLightCount() * CASCADE_COUNT;
v.z = v.y + getPointLightCount() * POINT_LIGHT_FACE_COUNT;
return v;
}
uvec3 getShadowFace(const vec3 worldPosition, const vec3 lightPosition)
{
uvec3 face;
face.x = getShadowCascade(worldPosition);
face.y = getPointLightFace(worldPosition - lightPosition);
face.z = 0;
return face;
}
vec3 getTexelSizeWorldSpace(const mat4 viewProjection, const float angle)
{
vec3 tsws;
tsws.x = getDirTexelSizeWorldSpace(viewProjection);
tsws.y = getSpotTexelSizeWorldSpace(45.0f);
tsws.z = getSpotTexelSizeWorldSpace(angle);
return tsws;
}
const uvec3 SHADOW_FACE_COUNT = uvec3(CASCADE_COUNT, POINT_LIGHT_FACE_COUNT, SPOT_LIGHT_FACE_COUNT);
struct ShadowParams
{
vec3 uv;
uint shadowIndex;
};
ShadowParams getShadowParams(const Light light, const ShadingParams shadingParams, const uint shadowType)
{
const uint face = getShadowFace(shadingParams.worldPosition, light.worldPosition)[shadowType];
const uint shadowIndex = face + (light.index * SHADOW_FACE_COUNT[shadowType]);
const uint matrixOffset = getMatrixOffset()[shadowType];
const mat4 shadowMatrix = u_shadowData.viewProjection[shadowIndex + matrixOffset];
const float texelSizeWorldSpace = getTexelSizeWorldSpace(shadowMatrix, light.angle)[shadowType];
ShadowParams params;
params.uv = getShadowUV(shadowMatrix, shadingParams, light, texelSizeWorldSpace);
params.shadowIndex = shadowIndex;
return params;
}
and in shading function I could do this (pseudocode):
vec3 Shading()
{
{
const ShadowParams shadowParams = getShadowParams(
light,
params,
SHADOW_TYPE_DIR
);
float visibility = Shadow(shadowParams, u_dirShadowMap);
....
}
{
const ShadowParams shadowParams = getShadowParams(
light,
params,
SHADOW_TYPE_POINT
);
float visibility = Shadow(shadowParams, u_pointShadowMap);
....
}
{
const ShadowParams shadowParams = getShadowParams(
light,
params,
SHADOW_TYPE_SPOT
);
float visibility = Shadow(shadowParams, u_spotShadowMap);
....
}
}
but now the shader calculates these differences everytime, for every light type, even though some of these values won’t be used.
I’m not sure which option might be better:
- if statements
- vec3s
What do you think about it? How would you handle this concern?
How would you rate the readability and the performance of these 2 options?
I’m interested more in hypothetical performance cases because for accurate results we’d have to test it.
Gerrard is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.