This paper briefly describes the implementation. The specific steps and principles can be viewed in Yan Dashen's high-quality real-time rendering.
1, Introduction to PCSS
1.1 PCF
Percentage asymptotic filtering (PCF) is a simple and common technique for shadow edge backtracking. It samples around the clip, then calculates the proportion of the sample closer to the light source than the clip, uses this proportion to scale the scattered light and specular light components, and then colors the clip. Using this technique, the shadow edges look blurred.
However, the internal sampling of large objects is a big waste. We can't only sample the object boundary, so there is a big bottleneck in performance.
1.2 PCSS
pcss is distance dependent. It tries to find all possible occlusions by searching the nearby area on the shadow map. The average distance of these obstructions from this location is used to determine the width of the sample area. As the average occlusion moves farther away from the receiver and closer to the light, the width of the surface area of the sample increases.
If no shelter can be found, the position is fully lit without further treatment. Similarly, if the position is completely obscured, the processing can end. Otherwise, continue to sample the region of interest and calculate the approximate contribution of light. In order to save processing costs, the width of the sample area can be used to change the number of samples. Other techniques can be implemented, such as using a lower sampling rate to obtain less important long-distance soft shadows.
Specific implementation steps:
- Calculation of average depth of shelter;
- Penumbra area calculation;
- Applicability PCF sampling;
It should be noted that:
- The calculation of penumbra area is shown in the triangle scale relationship below:
- The calculation of the area size in the calculation of the average depth of the occlusion is shown in the figure below (the fixed area size can also be simplified):
2, Code implementation
This section only describes the implementation of Shader. For C + code, please refer to the previous shadow related articles.
2.1 shadow map pass
Simply take the shadow map:
Vertex shader:
#version 330 core in vec3 position; uniform mat4 modelViewProjection; void main() { gl_Position = modelViewProjection * vec4(position, 1); }
Slice shader:
#version 330 core out float outDepth; void main() { outDepth = gl_FragCoord.z; }
2.2 pcss pass
Using shadow map to calculate PCSS
Vertex shader:
#version 330 core in vec3 position; in vec3 normal; in vec2 texcoords; out vec2 vTexcoords; out vec3 vNormal; out vec3 vViewDir; out vec3 vWorldPosition; out vec3 vCameraPosition; uniform mat4 model; uniform mat4 view; uniform mat4 projection; uniform vec3 eyePosition; void main() { vTexcoords = texcoords; vNormal = (model * vec4(normal, 0)).xyz; vec4 worldPosition = model * vec4(position, 1.0f); vWorldPosition = worldPosition.xyz; vec4 cameraPosition = view * model * vec4(position, 1.0f); vCameraPosition = cameraPosition.xyz; vViewDir = normalize(eyePosition - vWorldPosition); gl_Position = projection * cameraPosition; }
The implementation steps in the chip shader are basically the same as the above PCSS implementation steps. See the notes for details.
Slice shader:
#version 330 core #define NEAR 0.1 #define HARD_SHADOWS 0 #define SOFT_SHADOWS 1 in vec2 vTexcoords; in vec3 vNormal; in vec3 vViewDir; in vec3 vWorldPosition; in vec3 vCameraPosition; struct LightSource { vec3 diffuseColor; float diffusePower; vec3 specularColor; float specularPower; vec3 position; int type; float size; }; layout (std140) uniform LightSources { LightSource lightSources[2]; }; uniform sampler2D shadowMap0; uniform samplerCube shadowCubeMap0; uniform mat4 shadowMapViewProjection0; uniform mat4 invView; uniform mat4 lightProjection; uniform vec3 eyePosition; uniform vec3 ambientColor = vec3(0.8,0.8,0.8); uniform vec3 specularColor = vec3(1,1,1); uniform float specularity = 0; uniform float frustumSize = 1; uniform sampler2D tex0; uniform sampler1D distribution0; uniform sampler1D distribution1; uniform int numBlockerSearchSamples = 16; uniform int numPCFSamples = 16; uniform int displayMode = 0; uniform int selectedLightSource = -1; out vec3 outColor; //Random sampling data vec2 RandomDirection(sampler1D distribution, float u) { return texture(distribution, u).xy * 2 - vec2(1); } //Conventional light treatment vec3 LightContribution(vec3 diffuseColor) { float NdotL=max(0, dot(vNormal, normalize(vViewDir - lightSources[0].position))); return diffuseColor * lightSources[0].diffuseColor * lightSources[0].diffusePower * NdotL + pow(NdotL, specularity) * specularColor * lightSources[0].specularColor * lightSources[0].specularPower; } //Coordinate point to camera space vec3 ShadowCoords(mat4 shadowMapViewProjection) { vec4 projectedCoords = shadowMapViewProjection * vec4(vWorldPosition, 1); vec3 shadowCoords = projectedCoords.xyz / projectedCoords.w; shadowCoords = shadowCoords * 0.5 + 0.5; return shadowCoords; } //Occlusion range calculation float SearchWidth(float uvLightSize, float receiverDistance) { return uvLightSize * (receiverDistance - NEAR) / eyePosition.z; } //Average depth of occlusion query float FindBlockerDistance_DirectionalLight(vec3 shadowCoords, sampler2D shadowMap, float uvLightSize) { int blockers = 0; float avgBlockerDistance = 0; float searchWidth = SearchWidth(uvLightSize, shadowCoords.z); for (int i = 0; i < numBlockerSearchSamples; i++) { float z = texture(shadowMap, shadowCoords.xy + RandomDirection(distribution0, i / float(numBlockerSearchSamples)) * searchWidth).r; if (z < (shadowCoords.z - 0.002f)) { blockers++; avgBlockerDistance += z; } } if (blockers > 0) return avgBlockerDistance / blockers; else return -1; } //PCF float PCF_DirectionalLight(vec3 shadowCoords, sampler2D shadowMap, float uvRadius) { float sum = 0; for (int i = 0; i < numPCFSamples; i++) { float z = texture(shadowMap, shadowCoords.xy + RandomDirection(distribution0, i / float(numPCFSamples)) * uvRadius).r; sum += (z < (shadowCoords.z - 0.002f)) ? 1 : 0; } return sum / numPCFSamples; } //ShadowMap float ShadowMapping_DirectionalLight(vec3 shadowCoords, sampler2D shadowMap, float uvLightSize) { float z = texture(shadowMap, shadowCoords.xy).x; return (z < (shadowCoords.z - 0.002f)) ? 0 : 1; } //Soft shadow calculation float PCSS_DirectionalLight(vec3 shadowCoords, sampler2D shadowMap, float uvLightSize) { // Calculation of average depth of shelter float blockerDistance = FindBlockerDistance_DirectionalLight(shadowCoords, shadowMap, uvLightSize); if (blockerDistance == -1) return 1; // Penumbra area calculation float penumbraWidth = ((shadowCoords.z - blockerDistance) / blockerDistance) * uvLightSize; // PCF float uvRadius = penumbraWidth * NEAR / shadowCoords.z; return 1 - PCF_DirectionalLight(shadowCoords, shadowMap, uvRadius); } void main() { vec3 diffuseColor = texture(tex0, vTexcoords).rgb; switch (displayMode) { case HARD_SHADOWS: //Hard shadow outColor = LightContribution(diffuseColor) * ShadowMapping_DirectionalLight(ShadowCoords(shadowMapViewProjection0), shadowMap0, lightSources[0].size / frustumSize); break; case SOFT_SHADOWS: //Soft shadow outColor = LightContribution(diffuseColor) * PCSS_DirectionalLight(ShadowCoords(shadowMapViewProjection0), shadowMap0, lightSources[0].size / frustumSize); break; default: outColor = vec3(0.3,0.3,0.3); } outColor += ambientColor*diffuseColor; }
The following effects can be seen when running (32 sampling range, the default size of area light source is 0.5):
Compare the original shadow map effect:
Put another low PCF effect (4 sampling range, area light default size 0.5):
You can clearly see the time-consuming of PCFF from the frame rate. If you have time in the future, continue to use VSSM method to optimize the time-consuming of PCSS in the first step blocker search and the third pcf sampling.