在Piccolo引擎上实现PCF与PCSS


最近在Piccolo引擎上实现了PCF与PCSS, 虽然绘制结果中存在着大量的噪点, 但还是想着记录一下实现的流程~

Piccolo引擎实现阴影绘制的代码主要在deferred_lighting.frag中, 其引用了mesh_lighting.inl. 自己是把PCF与PCSS相关的接口都放在了一个新增的头文件utilities.h中, 由mesh_lighting.inl调用绘制阴影. 调用代码如下所示:


shadow = (closest_depth < current_depth) ? 1.0f : -1.0f;

if (shadow <= 0.0f)
{
    highp vec3 En = scene_directional_light.color * NoL;
    Lo += BRDF(L, V, N, F0, basecolor, metallic, roughness) * En;
}
 else
{
    highp vec4  coords     = vec4(uv.x, uv.y, position_clip.z, 1.0f);
    highp float visibility = PCSS(directional_light_shadow, coords);
    La *= visibility;
    Libl *= visibility;
}

1. PCF

入口函数为

highp float PCF(in sampler2D shadowMap, in highp vec4 coords) 
{
    highp vec2  uv        = coords.xy;
    highp float zReceiver = coords.z; // Assumed to be eye-space z in this code

    poissonDiskSamples(uv);
    return PCF_Filter(shadowMap, uv, zReceiver, 0.002);
}

$\cdot$ Step 1: Poisson圆盘采样

根据UV值进行Poisson圆盘采样, 得到一组用于ShadowMap采样的UV偏移量

void poissonDiskSamples(const in highp vec2 randomSeed)
{
    highp float ANGLES_STEP = PI2 * float(NUM_RINGS) / float(NUM_SAMPLES);
    highp float INV_NUM_SAMPLES = 1.0 / float(NUM_SAMPLES);

    highp float angle      = rand_2to1(randomSeed) * PI2;
    highp float radius     = INV_NUM_SAMPLES;
    highp float radiusStep = radius;

    for (int i = 0; i < NUM_SAMPLES; ++i)
    {
        poissonDisk[i] = vec2(cos(angle), sin(angle)) * pow(radius, 0.75);
        radius += radiusStep;
	angle += ANGLES_STEP;
    }
}

期间, 会将UV值作为随机数种子, 与一些Magic Number进行运算后得到一个特定的采样角度.


highp float rand_2to1(highp vec2 uv) 
{
    // 0 -1
    const highp float a = 12.9898, b = 78.233, c= 43758.5453;
    highp float       dt = dot(uv.xy, vec2(a, b));
    highp float sn = mod(dt, PI);
    return fract(sin(sn) * c);
}

$\cdot$ Step 2: PCF

利用Poisson圆盘采样得到的一组UV偏移量, 进行PCF.


highp float PCF_Filter(in sampler2D shadowMap, highp vec2 uv, highp float zReceiver, highp float filterRadius)
{ 
    highp float sum = 0.0;

    for (int i = 0; i < PCF_NUM_SAMPLES; ++i)
    {
        highp float depth = unpack(texture(shadowMap, uv + poissonDisk[i] * filterRadius));

        if (zReceiver <= depth)
	    {
		sum += 1.0;
	    }
	}

	for (int i = 0; i < PCF_NUM_SAMPLES; ++i)
	{
            highp float depth = unpack(texture(shadowMap, uv + -poissonDisk[i].yx * filterRadius));

	    if (zReceiver <= depth)
	    {
	        sum += 1.0;
	    }
	}

	return sum / (2.0 * float(PCF_NUM_SAMPLES));
}

2. PCSS

入口函数为


highp float PCSS(in sampler2D shadowMap, in highp vec4 coords)
{
    highp vec2 uv = coords.xy;
    highp float zReceiver = coords.z; // Assumed to be eye-space z in this code
    // STEP 1: blocker search
    poissonDiskSamples(uv);
    highp float avgBlockerDepth = findBlocker(shadowMap, uv, zReceiver);

    // There are no occluders so early out(this saves filtering)
    if (avgBlockerDepth == -1.0)
    {
	return 1.0;
    }

    // STEP 2: penumbra size
    highp float penumbraRatio = penumbraSize(zReceiver, avgBlockerDepth);
    highp float filterSize    = penumbraRatio * LIGHT_SIZE_UV * NEAR_PLANE / zReceiver;

    // STEP 3: filtering
    // return avgBlockerDepth;
    return PCF_Filter(shadowMap, coords.xy, zReceiver, filterSize);
}

$\cdot$ Step 1: 寻找遮挡物

通过findBlocker函数得到当前Shading Point的所有遮挡物的平均深度.


highp float findBlocker(in sampler2D shadowMap, highp vec2 uv, highp float zReceiver) 
{
    // This uses similar triangles to compute what
    // area of the shadow map we should search
    highp float searchRadius = LIGHT_SIZE_UV * (zReceiver - NEAR_PLANE) / zReceiver;
    highp float blockerDepthSum = 0.0;
    highp int numBlockers = 0;
    for (int i = 0; i < BLOCKER_SEARCH_NUM_SAMPLES; ++i)
    {
        highp float shadowMapDepth = unpack(texture(shadowMap, uv + poissonDisk[i] * searchRadius));

	if (shadowMapDepth < zReceiver)
	{
	    blockerDepthSum += shadowMapDepth;
	    ++numBlockers;
	}
    }

    if (numBlockers == 0)
    {
	return -1.0;
    }

    return blockerDepthSum / float(numBlockers);
}

$\cdot$ Step 2: 计算半影大小


highp float penumbraSize(highp float zReceiver, highp float zBlocker) 
{
    // Parallel plane estimation
    return (zReceiver - zBlocker) / zBlocker;
}

highp float penumbraRatio = penumbraSize(zReceiver, avgBlockerDepth);
highp float filterSize    = penumbraRatio * LIGHT_SIZE_UV * NEAR_PLANE / zReceiver;

原理可参考下图.

$\cdot$ Step 3: Filtering

将上述步骤得到的filterSize传递给PCF_Filter函数.

最终, 应用了PCSS的阴影绘制结果如下图所示. 可以看出, 阴影边缘处存在着大量的噪点, 后面如果有机会, 再加个降噪的Pass叭~

完整代码可参考: Piccolo with PCF and PCSS.

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注