综合技术

DirectX11 使用计算着色器实现高斯模糊

微信扫一扫,分享到朋友圈

DirectX11 使用计算着色器实现高斯模糊
0 0

高斯模糊,是一种使画面产生朦胧感的技术。在游戏中也可以可以经常看见使用高斯模糊的技术:

(《赛达尔传说:荒野之息》中对游戏背景使用高斯模糊,就将我们的主角们与背景分离出来,形成了前景、背景两个层面)

这节我们就来学习下如何使用DirectX11的计算着色器来实现高斯模糊。

一、计算着色器(Compute Shader)

如下图所示,计算着色器不属于渲染管线的一部分,但是计算着色器可以读写渲染管线。

计算着色器的计算能力和处理器数量有关,比如如果你的GPU有16个多处理器,那么就可以开启16个线程组。每个线程组,可以有n个线程。NVIDIA显卡基于SIMD32,n必须是一个warp(即32)的倍数。而ATI显卡是基于一个wavefront(即64)的倍数。一个线程组可以互相共享内存,而且等待每个线程同步,但是不同线程组是没有该能力的。

二、高斯模糊计算原理

可以这样通俗地描述,高斯模糊就是对每个像素点综合周围的像素点进行混合,最终产生模糊的像素点。

例如对于某个点p ij 会有如下图综合周围像素点的计算。而且需要一个权重矩阵(Weight Matrix),该矩阵对应着其周围像素的颜色贡献比例(而且所有权加起来必须等于1,大于1会过亮,小于1会过暗)。

对于高斯模糊的算法,我们可以用这样一个严谨的数学形式来描述它(说白了,其实就是我上面说的那段话,不需要害怕数学公式):

好了,有了这样一个基本的高斯模糊思路之后,后面的内容就是主要围绕着如何实现这个算法。

那周围的像素和自身的像素应该如何贡献呢?我们可以自己想想,应该自身的像素贡献最大(否则模糊后连基本的轮廓都认不出来),因此就需要引入一个高斯分布函数(其实就是高中学的正态分布函数)来计算权值:

(正态分布是一种钟形曲线,越接近中心,取值越大,越远离中心,取值越小。)

有没有发现上面的高斯分布函数是一维的?但是我们的图像是二维的,权重矩阵也是二维的,那么如何计算呢?其实,我们正是要减轻高斯模糊的计算量,将问题化简成行模糊和列模糊。如果原来要取9*9个像素点,需要81次取点计算。现在我们可以取中心点的一行与一列,分别进行一次模糊,只需要9+9=18次取点计算。

我们先渲染一张场景图到场景纹理,然后接着把场景纹理作为着色器资源视图,通过水平模糊计算后输出到另一张临时纹理。接着,我们再使用垂直模糊计算后输出到原来的场景纹理。现在的场景纹理就已经变成了高斯模糊后的场景了。我们只需要把场景纹理输出到后备缓冲区即可。

还有一个问题需要解决的是,如果计算的点是边界周围的点,它的周围像素点不足指定的像素点,又该怎么办呢?

这时候我们可以对计算着色器的共享内存进行一定的扩展,如果我们的高斯模糊半径是R,那么对每边扩展R个像素,并且填充上边界的像素颜色,就不会导致越界计算了。

//=============================================================================
// Blur.fx by Frank Luna (C) 2011 All Rights Reserved.
//
// Performs a separable blur with a blur radius of 5.  
//=============================================================================

cbuffer cbSettings
{ 
    //一行或一列的权重矩阵,半径是5
    float gWeights[11];
};

cbuffer cbFixed
{
    static const int gBlurRadius = 5;
};

Texture2D gInput;
RWTexture2D gOutput;

#define N 256
#define CacheSize (N + 2*gBlurRadius)
groupshared float4 gCache[CacheSize];

//numthreads(N, 1, 1),代表着线程的数量,可以是多维的
//groupThreadID,线程在线程组中的ID
//dispatchThreadID,在一次dispatch线程中的ID
//水平模糊
[numthreads(N, 1, 1)]
void HorzBlurCS(int3 groupThreadID : SV_GroupThreadID,
                int3 dispatchThreadID : SV_DispatchThreadID)
{
    //看看当前线程访问的像素点是否处于左边界范围,如果是,缓存取边界点像素颜色
    if(groupThreadID.x = N-gBlurRadius)
    {
        //防止超出纹理边界
        int x = min(dispatchThreadID.x + gBlurRadius, gInput.Length.x-1);
        gCache[groupThreadID.x+2*gBlurRadius] = gInput[int2(x, dispatchThreadID.y)];
    }

    //防止超出纹理边界
    gCache[groupThreadID.x+gBlurRadius] = gInput[min(dispatchThreadID.xy, gInput.Length.xy-1)];

    // 等待线程组的所有线程同步
    GroupMemoryBarrierWithGroupSync();

    //
    // 现在模糊每个像素
    //

    float4 blurColor = float4(0, 0, 0, 0);

    [unroll]
    for(int i = -gBlurRadius; i <= gBlurRadius; ++i)
    {
        int k = groupThreadID.x + gBlurRadius + i;

        blurColor += gWeights[i+gBlurRadius]*gCache[k];
    }

    gOutput[dispatchThreadID.xy] = blurColor;
}

//numthreads(N, 1, 1),代表着线程的数量,可以是多维的
//groupThreadID,线程在线程组中的ID
//dispatchThreadID,在一次dispatch线程中的ID
//垂直模糊
[numthreads(1, N, 1)]
void VertBlurCS(int3 groupThreadID : SV_GroupThreadID,
                int3 dispatchThreadID : SV_DispatchThreadID)
{

    //看看当前线程访问的像素点是否处于上边界范围,如果是,缓存取边界点像素颜色
    if(groupThreadID.y = N-gBlurRadius)
    {
        //防止超出纹理边界
        int y = min(dispatchThreadID.y + gBlurRadius, gInput.Length.y-1);
        gCache[groupThreadID.y+2*gBlurRadius] = gInput[int2(dispatchThreadID.x, y)];
    }

    //防止超出纹理边界
    gCache[groupThreadID.y+gBlurRadius] = gInput[min(dispatchThreadID.xy, gInput.Length.xy-1)];


    // 等待线程组的所有线程同步
    GroupMemoryBarrierWithGroupSync();

    //
    // 现在模糊每个像素
    //

    float4 blurColor = float4(0, 0, 0, 0);

    [unroll]
    for(int i = -gBlurRadius; i <= gBlurRadius; ++i)
    {
        int k = groupThreadID.y + gBlurRadius + i;

        blurColor += gWeights[i+gBlurRadius]*gCache[k];
    }

    gOutput[dispatchThreadID.xy] = blurColor;
}

technique11 HorzBlur
{
    pass P0
    {
        SetVertexShader( NULL );
        SetPixelShader( NULL );
        SetComputeShader( CompileShader( cs_5_0, HorzBlurCS() ) );
    }
}

technique11 VertBlur
{
    pass P0
    {
        SetVertexShader( NULL );
        SetPixelShader( NULL );
        SetComputeShader( CompileShader( cs_5_0, VertBlurCS() ) );
    }
}

下面也提供了在应用程序阶段如何计算高斯分布的权重矩阵的值:

void BlurFilter::SetGaussianWeights(float sigma)
{
    float d = 2.0f*sigma*sigma;

    float weights[11];
    float sum = 0.0f;
    for(int i = 0; i < 11; ++i)
    {
        float x = (float)i;
        weights[i] = 1.0/(sqrt(2* M_PI)*sigma)*expf(-pow((x-11/2),2)/d);

        sum += weights[i];
    }

    // 除以权重总和使得每个权重加起来总和为1
    for(int i = 0; i SetWeights(weights);
}

原场景:

先看一下高斯模糊渲染一趟的效果:

不是很明显,再高斯模糊渲染第2趟:

继续渲染第3趟:

第4趟:

经过模糊四趟之后就得到一个我们满意的模糊效果了。是不是有种朦胧的美感呢?

项目源代码:

https://github.com/ljcduo/Introduction-to-3D-Game-Programming-With-DirectX11/tree/master/Chapter%2012%20The%20Compute%20Shader/Blur

CSDN博客
感谢您的支持!

    A Type Inference Implementation Adventure

    上一篇

    数字颠覆时代更需无畏精神

    下一篇

    您也可能喜欢

    评论已经被关闭。

    插入图片