DirectX11 With Windows SDK--30 計算着色器:高斯模糊、索貝爾算子

前言

到這裏計算着色器的主線學習基本結束,剩下的就是再補充兩個有關圖像處理方面的應用。這裏麪包含了龍書11的圖像模糊,以及龍書12額外提到的Sobel算子進行邊緣檢測。主要內容源自於龍書12,項目源碼也基於此進行調整。

學習目標:

  1. 熟悉圖像處理常用的卷積
  2. 熟悉高斯模糊、Sobel算子

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ羣: 727623616 可以一起探討DX11,以及有什麼問題也可以在這裏彙報。

圖像卷積

在圖像處理中,經常需要用到卷積,很多效果都能夠通過卷積的形式來實現。針對源圖像中的每一個像素\(P_{ij}\),計算以它爲中心的m×n矩陣的加權值。此加權值便是經過處理後圖像中第i行、第j列的顏色,如果寫成卷積的形式則爲:
\[ H_{ij}=\sum_{r=-a}^{a}\sum_{c=-b}^{b}W_{rc}P_{i-r,j-c} \]
其中,\(m=2a+1\)\(n=2b+1\),將m與n強制爲奇數,以此來保證m×n矩陣總是具有“中心”項。若a=b=r,則只需指定半徑r就可以確定矩陣的大小。\(W_{rc}\)爲m×n矩陣(又稱內核、算子)中的權值。爲了方便觀察計算及編碼,通常會將內核旋轉180°,這樣就得到了更加常用的計算公式:
\[ H_{ij}=\sum_{r=-a}^{a}\sum_{c=-b}^{b}W_{rc}P_{i+r,j+c} \]
若內核的所有權值的和爲1,則它可以用來做模糊處理;如果權值和大於0小於1,則處理後的圖像會隨着顏色的缺失而變暗;如果權值和大於1,則處理後的圖像會隨着顏色的增添而更加明亮。當然也會有權值和等於0甚至可能小於0的情況,比如索貝爾算子。

圖像模糊

在保證權值和爲1的前提下,我們就能用多種不同的方法來計算它。其中就有一種廣爲人知的模糊運算:高斯模糊(Gaussian blur)。該算法藉助高斯函數\(G(x)=exp(-\frac{x^2}{2\sigma^2})\)來獲取權值。下圖展示了取不同σ值時高斯函數的對應圖像:

可以看到,若σ越大,則曲線越趨於平緩,給鄰近點所賦予的權值也就越大。
\[ G(x)=exp(-\frac{x^2}{2\sigma^2})=e^{-\frac{x^2}{2\sigma^2}} \]
如果學過概率論的話應該知道它很像標準正態分佈的概率密度,只不過缺了一個係數\(\frac{1}{\sqrt{2\pi}\;\sigma}\)

現在假設我們要進行規模爲1×5的高斯模糊(即在水平方向進行1D模糊),且設σ=1。分別對x=-2,-1,0,1,2求G(x)的值,可以得到:
\[ \begin{align}G(-2)&=exp(-\frac{(-2)^2}{2})=e^{-2} \\G(-1)&=exp(-\frac{(-1)^2}{2})=e^{-\frac{1}{2}} \\G(0)&=exp(0)=1 \\G(1)&=exp(-\frac{1^2}{2})=e^{-\frac{1}{2}} \\G(2)&=exp(-\frac{2^2}{2})=e^{-2}\end{align} \]
但是,這些數據還不是最終的權值,因爲它們的和不爲1:
\[ \begin{align} \sum_{x=-2}^{x=2}G(x)&=G(-2)+G(-1)+G(0)+G(1)+G(2)\\ &=1+2e^{-\frac{1}{2}}+2e^{-2}\\ &\approx 2.48373 \end{align} \]
如果將前面5個值都除以它們的和進行規格化處理,那麼我們便會基於高斯函數獲得總和爲1的各個權值:
\[ \begin{align} w_{-2}&=\frac{G(-2)}{\sum_{x=-2}^{x=2}G(x)}\approx 0.0545\\ w_{-1}&=\frac{G(-1)}{\sum_{x=-2}^{x=2}G(x)}\approx 0.2442\\ w_{0}&=\frac{G(0)}{\sum_{x=-2}^{x=2}G(x)}\approx 0.4026\\ w_{1}&=\frac{G(1)}{\sum_{x=-2}^{x=2}G(x)}\approx 0.2442\\ w_{2}&=\frac{G(2)}{\sum_{x=-2}^{x=2}G(x)}\approx 0.0545\\ \end{align} \]
對於二維的高斯函數,有
\[ \begin{align} G(x, y) &= G(x)\cdot G(y) \\ &=exp(-\frac{x^2}{2\sigma^2})\cdot exp(-\frac{y^2}{2\sigma^2}) \\ &=e^{-\frac{x^2+y^2}{2\sigma^2}} \end{align} \]
假如我們要進行3x3的高斯模糊,且設σ=1,則未經過歸一化的內核爲:
\[ \begin{bmatrix} G(-1)G(-1) & G(-1)G(0) & G(-1)G(1) \\ G(0)G(-1) & G(0)G(0) & G(0)G(1) \\ G(1)G(-1) & G(1)G(0) & G(1)G(1) \\ \end{bmatrix} = \begin{bmatrix} G(-1) \\ G(0) \\ G(1) \\ \end{bmatrix}\begin{bmatrix} G(-1) & G(0) & G(1) \\ \end{bmatrix} \]
由於上面的內核矩陣可以寫成一個列向量乘以一個行向量的形式,因此在做模糊的時候可以將一個2D模糊過程分爲兩個1D模糊過程。這也就說明該內核具有可分離性

  1. 通過1D橫向模糊將輸入的圖像I進行模糊處理:\(I_H=Blur_H(I)\)
  2. 對上一步輸出的結果再次進行1D縱向模糊處理:\(Blur(I)=Blur_V(I_H)\)

因此有:
\[ Blur(I)=Blur_V(Blur_H(I)) \]
假如模糊核爲一個9×9矩陣,我們就需要對總計81個樣本依次進行2D模糊運算。但通過將模糊過程分離爲兩個1D模糊階段,便僅需要處理9+9=18個樣本!我們常常要對紋理進行模糊處理,而對紋理採樣是代價高昂的操作。因此,通過分離模糊過程來減少紋理採樣操作是一種受用戶歡迎的優化手段。儘管有些模糊方法不具備可分離性,但只要保證最終圖像在視覺上足夠精準,我們往往還是能以優化性能爲目的而簡化其模糊過程。

實現原理

首先,假設所運用的模糊算法具有可分離性,據此將模糊操作分爲兩個1D模糊運算:一個橫向模糊運算,一個縱向模糊運算。假定用戶提供了一個紋理A作爲輸入(通常是作爲SRV形參),以及一個紋理B作爲輸出(通常是作爲UAV形參)。不過要考慮到有的用戶希望將直接修改紋理A,將紋理A的SRV和UAV都傳入。因此我們還是需要兩個存儲中間結果的紋理T0、T1,過程如下:

  1. 給紋理A綁定SRV作爲輸入,並且給紋理T0綁定UAV作爲輸出。
  2. 調度線程組進行橫向模糊操作。完成後,紋理T0存儲了橫向模糊的結果
  3. 解綁紋理T0的UAV,將它的SRV作爲輸入。
  4. 若用戶指定了UAV,並且模糊次數爲1,則將該UAV作爲輸出;否則由於後續還需要進行混合,則將紋理T1的UAV作爲輸出。
  5. 調度線程組進行縱向模糊操作。若當前爲最後一次模糊,且用戶指定了UAV,則該UAV的紋理將保存最終的結果;否則T1保存了當前模糊的結果。解綁UAV後,若仍有剩餘模糊次數,則將紋理T1綁定SRV作爲輸入,並給紋理T0綁定UAV作爲輸出,回到步驟2繼續;否則就再解綁SRV後結束。

由於渲染到紋理種的場景於窗口工作區要保持着相同的分辨率,我們需要不時重新構建離屏紋理,而模糊算法用的臨時紋理T也是如此。在GameApp::OnResize的時候重新調整即可。

假如要處理的圖像寬度爲w、寬度爲h。對於1D縱向模糊而言,一個線程組用256個線程來處理水平方向上的線段,而且每個線程又負責圖像中一個像素的模糊操作。因此,爲了圖像中的每個像素都能得到模糊處理,我們需要在x方向上調度\(ceil(\frac{w}{256})\)個線程組(ceil爲上取整函數),且在y方向上調度h個線程組。如果w不能被256整除,則最後一次調度的線程組會存有多餘的線程(見下圖)。我們對於這種情況無能爲力,因爲線程組的大小固定。因此,我們只得把注意力放在着色器代碼中越界問題的鉗位檢測(clamping check)上。

1D縱向模糊於上述1D橫向模糊的情況相似。在縱向模糊過程中,線程組就像由256個線程構成的垂直線段,每個線程只負責圖像中一個像素的模糊運算。因此,爲了使圖像中的每個像素都能得到模糊處理,我們需要在y方向上調度\(ceil(\frac{h}{256})\)個線程組,並在x方向上調度w個線程組。

現在來考慮對一個28x14像素的紋理進行處理,我們所用的橫向、縱向線程組的規模分別爲8x1和1x8(採用X×Y的表示格式)。對於水平方向的處理過程來說,爲了處理所有的像素,我們需要在x方向上調度\(ceil(\frac{w}{8})=ceil(\frac{28}{8})=4\)個線程組,並在y方向上調度14個線程組。由於28並不能被8整除,所以最右側的線程組中會有\((4\times 8-28)\times 14=56\)個線程聲明都不做。對於垂直方向的處理過程而言,爲了處理所有的像素,我們需要在y方向上分派\(ceil(\frac{h}{8})=ceil(\frac{14}{8})=2\)個線程組,並在x方向上調度28個線程組。同理,由於14並不能被8整除,所以最下側的線程組中會有\((2\times 8 - 14)\times 28\)個閒置的線程。沿用同一思路就可以將線程組擴展爲256個線程的規模來處理更大的紋理。

BlurFilter::Execute不僅計算出了每個方向要調度的線程組數量,還開啓了計算着色器的模糊運算:

void BlurFilter::Execute(ID3D11DeviceContext* deviceContext, ID3D11ShaderResourceView* inputTex, ID3D11UnorderedAccessView* outputTex, UINT blurTimes)
{
    if (!deviceContext || !inputTex || !blurTimes)
        return;

    // 設置常量緩衝區
    D3D11_MAPPED_SUBRESOURCE mappedData;
    deviceContext->Map(m_pConstantBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData);
    memcpy_s(mappedData.pData, sizeof m_CBSettings, &m_CBSettings, sizeof m_CBSettings);
    deviceContext->Unmap(m_pConstantBuffer.Get(), 0);

    deviceContext->CSSetConstantBuffers(0, 1, m_pConstantBuffer.GetAddressOf());

    ID3D11UnorderedAccessView* nullUAV[1] = { nullptr };
    ID3D11ShaderResourceView* nullSRV[1] = { nullptr };
    // 第一次模糊
    // 橫向模糊
    deviceContext->CSSetShader(m_pBlurHorzCS.Get(), nullptr, 0);
    deviceContext->CSSetShaderResources(0, 1, &inputTex);
    deviceContext->CSSetUnorderedAccessViews(0, 1, m_pTempUAV0.GetAddressOf(), nullptr);

    deviceContext->Dispatch((UINT)ceilf(m_Width / 256.0f), m_Height, 1);
    deviceContext->CSSetUnorderedAccessViews(0, 1, nullUAV, nullptr);
    // 縱向模糊
    deviceContext->CSSetShader(m_pBlurVertCS.Get(), nullptr, 0);
    deviceContext->CSSetShaderResources(0, 1, m_pTempSRV0.GetAddressOf());
    if (blurTimes == 1 && outputTex)
        deviceContext->CSSetUnorderedAccessViews(0, 1, &outputTex, nullptr);
    else
        deviceContext->CSSetUnorderedAccessViews(0, 1, m_pTempUAV1.GetAddressOf(), nullptr);
    deviceContext->Dispatch(m_Width, (UINT)ceilf(m_Height / 256.0f), 1);
    deviceContext->CSSetUnorderedAccessViews(0, 1, nullUAV, nullptr);

    // 剩餘模糊次數
    while (--blurTimes)
    {
        // 橫向模糊
        deviceContext->CSSetShader(m_pBlurHorzCS.Get(), nullptr, 0);
        deviceContext->CSSetShaderResources(0, 1, m_pTempSRV1.GetAddressOf());
        deviceContext->CSSetUnorderedAccessViews(0, 1, m_pTempUAV0.GetAddressOf(), nullptr);

        deviceContext->Dispatch((UINT)ceilf(m_Width / 256.0f), m_Height, 1);
        deviceContext->CSSetUnorderedAccessViews(0, 1, nullUAV, nullptr);
        // 縱向模糊
        deviceContext->CSSetShader(m_pBlurVertCS.Get(), nullptr, 0);
        deviceContext->CSSetShaderResources(0, 1, m_pTempSRV0.GetAddressOf());
        if (blurTimes == 1 && outputTex)
            deviceContext->CSSetUnorderedAccessViews(0, 1, &outputTex, nullptr);
        else
            deviceContext->CSSetUnorderedAccessViews(0, 1, m_pTempUAV1.GetAddressOf(), nullptr);
        deviceContext->Dispatch(m_Width, (UINT)ceilf(m_Height / 256.0f), 1);

        
        deviceContext->CSSetUnorderedAccessViews(0, 1, nullUAV, nullptr);
    }
    // 解除剩餘綁定
    deviceContext->CSSetShaderResources(0, 1, nullSRV);

}

其餘C++端源碼則直接去項目源碼看即可。

HLSL代碼

由於水平模糊與垂直模糊的實現原理相仿,這裏我們只討論水平模糊。

在上面的代碼中,我們可以看到調度的線程組是由256個線程構成的水平“線段”,每個線程都負責圖像中一個像素的模糊操作。一種低效的實現方案是,每個線程都簡單地計算出以正在處理的像素爲中心的行矩陣(因爲我們現在正在進行的是1D橫向模糊處理,所以要針對行矩陣進行計算)的加權平均值。這種辦法的缺點是需要多次拾取同一紋素。

僅考慮輸入圖像中的這兩個相鄰像素,假設模糊核爲1×7。光是在對這兩個像素進行模糊的過程中,8個不同的像素中就已經有6個被採集了2次,而且要考慮到訪問設備內存的效率在GPU內存模型中是屬於比較慢的一種。

我們可以根據前面一節提到的模糊處理策略,利用共享內存來優化上述算法。這樣一來,每個線程就可以在共享內存中讀取或存儲所需的紋素數據。待所有線程都從共享內存讀取到它們所需的紋素後,就能夠執行模糊運算了。不得不說,從共享內存中讀取數據的速度飛快。除此之外,還有一件棘手的事情,就是利用具有n = 256個線程的線程組行模糊運算的時候,卻需要n + 2R個紋素數據,這裏的R就是模糊半徑:

由於模糊半徑的原因,在處理線程組邊界附近的像素時,可能會讀取線程組以外存在“越界”情況的像素。解決辦法其實也並不複雜。我們只需要分配出能容納n + 2R個元素的共享內存,並且有2R個線程要各獲取兩個紋素數據。唯一麻煩的地方就是在共享內存時要多花心思,因爲組內線程ID此時不能於共享內存中的元素一一對應了。下圖演示了當R=4時,從線程到共享內存的映射過程。

在此例中,R = 4。最左側的4個線程以及最右側的4個線程,每個都要讀取2個紋素數據,並將它們存於共享內存之中。而這8個線程之外的所有線程都只需要讀取1個像素,並將其存於共享內存之中。這樣一來,我們即可以得到以模糊半徑R對N個像素進行模糊處理所需的所有紋素數據。

現在要討論的是最後一種情況,即下圖中所示的最左側於最右側的線程組在索引輸入圖像時會發生越界的情形。

前面提到,從越界的索引處讀取數據並不是非法操作,而是返回0(對越界索引處進行寫入是不會執行任何操作的,即no-op)。然而,我們在讀取越界數據時並不希望得到數據0,因爲這意味着值爲0的顏色(即黑色)會影響到邊界處的模糊結果。我們此時期盼能實現出類似於鉗位(clamp)紋理尋址模式的效果,即在讀取越界的數據時,能夠獲得一個與邊界紋素相同的數據。這個方案可以通過對索引進行鉗位來加以實現,在下面完整的着色器代碼可以看到(這裏將模糊半徑調大了):

// Blur.hlsli
cbuffer CBSettings : register(b0)
{
    int g_BlurRadius;
    
    // 最多支持19個模糊權值
    float w0;
    float w1;
    float w2;
    float w3;
    float w4;
    float w5;
    float w6;
    float w7;
    float w8;
    float w9;
    float w10;
    float w11;
    float w12;
    float w13;
    float w14;
    float w15;
    float w16;
    float w17;
    float w18;
}

Texture2D g_Input : register(t0);
RWTexture2D<float4> g_Output : register(u0);

static const int g_MaxBlurRadius = 9;

#define N 256
#define CacheSize (N + 2 * g_MaxBlurRadius)
// Blur_Horz_CS.hlsl
#include "Blur.hlsli"

groupshared float4 g_Cache[CacheSize];

[numthreads(N, 1, 1)]
void CS(int3 GTid : SV_GroupThreadID,
    int3 DTid : SV_DispatchThreadID)
{
    // 放在數組中以便於索引
    float g_Weights[19] =
    {
        w0, w1, w2, w3, w4, w5, w6, w7, w8, w9,
        w10, w11, w12, w13, w14, w15, w16, w17, w18
    };

    // 通過填寫本地線程存儲區來減少帶寬的負載。若要對N個像素進行模糊處理,根據模糊半徑,
    // 我們需要加載N + 2 * BlurRadius個像素
    
    // 此線程組運行着N個線程。爲了獲取額外的2*BlurRadius個像素,就需要有2*BlurRadius個
    // 線程都多采集一個像素數據
    if (GTid.x < g_BlurRadius)
    {
        // 對於圖像左側邊界存在越界採樣的情況進行鉗位(Clamp)操作
        int x = max(DTid.x - g_BlurRadius, 0);
        g_Cache[GTid.x] = g_Input[int2(x, DTid.y)];
    }
    
    if (GTid.x >= N - g_BlurRadius)
    {
        // 對於圖像左側邊界存在越界採樣的情況進行鉗位(Clamp)操作
        // 震驚的是Texture2D居然能通過屬性Length訪問寬高
        int x = min(DTid.x + g_BlurRadius, g_Input.Length.x - 1);   
        g_Cache[GTid.x + 2 * g_BlurRadius] = g_Input[int2(x, DTid.y)];
    }
    
    // 將數據寫入Cache的對應位置
    // 針對圖形邊界處的越界採樣情況進行鉗位處理
    g_Cache[GTid.x + g_BlurRadius] = g_Input[min(DTid.xy, g_Input.Length.xy - 1)];
    
    // 等待所有線程完成任務
    GroupMemoryBarrierWithGroupSync();
    
    // 開始對每個像素進行混合
    float4 blurColor = float4(0.0f, 0.0f, 0.0f, 0.0f);
    for (int i = -g_BlurRadius; i <= g_BlurRadius; ++i)
    {
        int k = GTid.x + g_BlurRadius + i;
        
        blurColor += g_Weights[i + g_BlurRadius] * g_Cache[k];
    }
    
    g_Output[DTid.xy] = blurColor;
}

Blur_Vert_CS.hlsl與上面的代碼類似,就不再放出。

最右側的線程組可能存有一些多餘的線程,但輸出的紋理中並沒有與之對應的元素(意味着它們根本無需輸出任何數據,見上圖)。此時DTid.xy即爲輸出紋理之外的一個越界索引。但是我們無需爲此而擔心,因爲向越界處寫入數據的效果是不進行任何操作(no-op)。

索貝爾算子

索貝爾算子(Sobel Operator)用於圖像的邊緣檢測。它會針對每一個像素估算其梯度(gradient)的大小。梯度值較大的像素則表明它與周圍像素的顏色差異極大,因而此像素一定位於圖像的邊緣。相反,具有較小梯度的像素則意味着它與臨近像素的顏色趨同,即該像素並不處於圖像邊沿之上。需要注意的是,索貝爾算子返回的並非是像素是否位於圖像邊緣的二元結果,而是一個範圍在[0.0, 1.0]內表示邊緣“陡峭”程度的灰度值:值爲0表示非常平坦,與周圍像素並沒有顏色差異;值爲1表示非常陡峭,與周圍像素顏色差異很大。通常索貝爾逆圖像(1-c)往往會更加直觀有效,這時白色表示平坦且不位於圖像邊緣,而黑色則代表陡峭且處於圖像邊緣。

運用索貝爾算子後的結果:

索貝爾算子的逆圖像的結果:

如果將原始圖像與其經過索貝爾算子生成的逆圖像兩者間的對應顏色值相乘,我們將獲得類似於卡通畫或動漫書中那樣,其邊緣就像用黑色的筆勾描後的圖片效果。哪怕待處理的圖像首先經過模糊處理後已經隱去了部分細節,依舊可以恢復其相對粗獷的畫風,令其邊緣清晰起來。

索貝爾算子所採用的算法是先進行加權平均,然後進行近似求導運算,計算方法如下:
\[ G_x = \Delta_x f(x, y) = [f(x-1,y+1)+2f(x,y+1)+f(x+1,y+1)]-[f(x-1,y-1)+2f(x,y-1)+f(x+1,y-1)] \\ G_y = \Delta_y f(x, y) = [f(x-1,y-1)+2f(x-1,y)+f(x-1,y+1)]-[f(x+1,y-1)+2f(x+1,y)+f(x+1,y+1)] \]

因此我們就得到了梯度向量\((\Delta_x f(x, y), \Delta_y f(x, y))\),然後求出它的長度\(\parallel \sqrt{G_{x}^{2} + G_{y}^{2}}\parallel\)即爲變化方向最大處的變化率。

HLSL代碼

索貝爾算子的HLSL代碼實現如下:

// Sobel_CS.hlsl
Texture2D g_Input : register(t0);
RWTexture2D<float4> g_Output : register(u0);

// 將RGB色轉化爲灰色
float3 RGB2Gray(float3 color)
{
    return (float3) dot(color, float3(0.299f, 0.587f, 0.114f));
}

[numthreads(16, 16, 1)]
void CS(int3 DTid : SV_DispatchThreadID)
{
    // 採集當前待處理像素及相鄰的八個像素
    float4 colors[3][3];
    for (int i = 0; i < 3; ++i)
    {
        for (int j = 0; j < 3; ++j)
        {
            int2 xy = DTid.xy + int2(-1 + j, -1 + i);
            colors[i][j] = g_Input[xy];
        }
    }
    
    // 針對每個顏色通道,利用索貝爾算子估算出關於x的偏導數近似值
    float4 Gx = -1.0f * colors[0][0] - 2.0f * colors[1][0] - 1.0f * colors[2][0] +
        1.0f * colors[0][2] + 2.0f * colors[1][2] + 1.0f * colors[2][2];
    
    // 針對每個顏色通道,利用索貝爾算子估算出關於y的偏導數的近似值
    float4 Gy = -1.0f * colors[2][0] - 2.0f * colors[2][1] - 1.0f * colors[2][2] +
        1.0f * colors[0][0] + 2.0f * colors[0][1] + 1.0f * colors[0][2];
    
    // 梯度向量即爲(Gx, Gy)。針對每個顏色通道,計算出梯度大小(即梯度的模擬)
    // 以找到最大的變化率
    float4 mag = sqrt(Gx * Gx + Gy * Gy);
    
    // 將梯度陡峭的邊緣處繪製爲黑色,梯度平坦的非邊緣處繪製爲白色
    mag = 1.0f - float4(saturate(RGB2Gray(mag.xyz)), 0.0f);
    
    g_Output[DTid.xy] = mag;

}
// VS使用Basic_VS_2D
// Composite_PS.hlsl
Texture2D g_BaseMap : register(t0); // 原紋理
Texture2D g_EdgeMap : register(t1); // 邊緣紋理
SamplerState g_SamLinearWrap : register(s0); // 線性過濾+Wrap採樣器
SamplerState g_SamPointClamp : register(s1); // 點過濾+Clamp採樣器

float4 PS(float4 posH : SV_Position, float2 tex : TEXCOORD) : SV_Target
{
    float4 c = g_BaseMap.SampleLevel(g_SamPointClamp, tex, 0.0f);
    float4 e = g_EdgeMap.SampleLevel(g_SamPointClamp, tex, 0.0f);
    // 將原始圖片與邊緣圖相乘
    return c * e;
}

在C++端的代碼可以直接去源碼中尋找SobelFilter

演示

本樣例爲高斯模糊提供了調整模糊半徑、Sigma和次數的功能。模糊半徑越大,模糊次數越大,幀數會越低。如果你的電腦配置承受不住,建議關掉OIT來觀察模糊效果會更好一些。至於Sobel算子則無法調整。

整個計算着色器的內容就到此結束了。

調試問題

該項目無法圖形調試,就和DirectX SDK Samples中OIT樣例一樣,遇到了未知問題。如果要調試,需要把OIT相關的代碼撤走才能調試。

此外,龍書12中的Composite.hlsl頂點着色器用到了SV_VertexID,一旦用了該系統值作爲輸入,就無法在最終結果選擇像素觀察運行過程了。因此本項目並沒有使用內置於着色器的頂點數據。

練習題

  1. 對圖像進行模糊處理是一種昂貴的操作,它所花費的時間於待處理的圖像大小息息相關。一般情況下,在把場景渲染到離屏紋理的時候,我們通常會將離屏紋理的大小設爲後備緩衝區尺寸的1/4.也就是說,假如後備緩衝區的大小爲800x600,則離屏紋理的尺寸將爲400x300.這樣一來不僅能加快離屏紋理的繪製速度(即減少了需要填充的像素數量),而且能同時提升模糊圖像的處理速度(需要模糊的像素也就更少)。另外,當紋理從1/4的屏幕分辨率拉伸爲完整大屏幕分辨率時,紋理放大過濾器也會執行一些額外的模糊操作。
    現在嘗試修改項目,讓BlurFilter的分辨率爲400x300,實現上述內容。
    提示:TextureRender開啓mipmaps,並將mip等級爲1的紋理子資源作爲SRV。

  2. 嘗試添加Composite_VS.hlsl,將繪製整個屏幕的6個頂點直接放在頂點着色器中,然後只使用SV_VertexID作爲頂點着色器的形參來繪製。
  3. 研究雙邊模糊(雙邊濾波器,bilateral blur)計數,並用計算着色器加以實現。

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ羣: 727623616 可以一起探討DX11,以及有什麼問題也可以在這裏彙報。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章