本文简述实现,具体步骤原理可查看闫大神高质量实时渲染。
一、PCSS简介
1.1 PCF
百分比渐近过滤(PCF)是一个简单,常见的进行阴影边缘反走的技术。它通过在片段周围进行采样,然后计算样本比片段的更接近光源的比例,使用这个比例对散射光和镜面光成分进行缩放,然后对片段进行着色。使用这一技术后,阴影边缘看上去进行了模糊一样。
但对于较大物体的内部的采样是一个较大的浪费,我们并无法只针对物体边界进行采样,所以在性能是具有很大的瓶颈。
1.2 PCSS
pcss是距离相关的, 它通过搜索阴影贴图上的附近区域来尝试找到所有可能的遮挡物。这些遮挡物与该位置的平均距离用于确定样本区域宽度。随着平均遮挡物越来越远离接收物并且更靠近光线,样本的表面区域的宽度增大。
如果找不到遮挡物,则该位置完全点亮,无需进一步处理。类似地,如果位置完全被遮挡,则处理可以结束。否则,继续对感兴趣的区域进行采样并计算光的近似贡献。为了节省处理成本,样本区域的宽度可用于改变采样的数量。 可以实现其他技术,例如,使用较低的采样率来获得不太重要的远距离软阴影。
具体实现步骤:
- 遮挡物平均深度计算;
- 半影区域计算;
- 适用性PCF采样;
其中需要注意的是:
- 半影区域计算如下图三角形比例关系:
- 遮挡物平均深度计算时的区域大小计算如下图(也可简化使用固定区域大小):
二、代码实现
本部分仅讲述Shader实现,C+代码具体参照之前阴影相关文章。
2.1 shadow map pass
简单取shadow map:
顶点着色器:
#version 330 core
in vec3 position;
uniform mat4 modelViewProjection;
void main()
{
gl_Position = modelViewProjection * vec4(position, 1);
}
片元着色器:
#version 330 core
out float outDepth;
void main()
{
outDepth = gl_FragCoord.z;
}
2.2 pcss pass
使用shadow map求PCSS
顶点着色器:
#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;
}
片元着色器中实现步骤基本与上述PCSS实现步骤一致,详见注释。
片元着色器:
#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;
//随机采样数据
vec2 RandomDirection(sampler1D distribution, float u)
{
return texture(distribution, u).xy * 2 - vec2(1);
}
//常规光照处理
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;
}
//坐标点转相机空间
vec3 ShadowCoords(mat4 shadowMapViewProjection)
{
vec4 projectedCoords = shadowMapViewProjection * vec4(vWorldPosition, 1);
vec3 shadowCoords = projectedCoords.xyz / projectedCoords.w;
shadowCoords = shadowCoords * 0.5 + 0.5;
return shadowCoords;
}
//遮挡范围计算
float SearchWidth(float uvLightSize, float receiverDistance)
{
return uvLightSize * (receiverDistance - NEAR) / eyePosition.z;
}
//遮挡物平均深度查询
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;
}
//软阴影计算
float PCSS_DirectionalLight(vec3 shadowCoords, sampler2D shadowMap, float uvLightSize)
{
// 遮挡物平均深度计算
float blockerDistance = FindBlockerDistance_DirectionalLight(shadowCoords, shadowMap, uvLightSize);
if (blockerDistance == -1)
return 1;
// 半影区域计算
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: //硬阴影
outColor = LightContribution(diffuseColor) * ShadowMapping_DirectionalLight(ShadowCoords(shadowMapViewProjection0), shadowMap0, lightSources[0].size / frustumSize);
break;
case SOFT_SHADOWS: //软阴影
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;
}
运行可见如下效果(32采样范围,面光源默认大小0.5):
对比原有shadow map效果:
再放一张低PCF效果(4采样范围,面光源默认大小0.5):
大家可以从帧率上明显看出PCFF的耗时,后续可有时间的话继续使用VSSM方法来优化PCSS第一步blocker search 和第三部pcf采样的耗时。