본문 바로가기
그래픽스

Compute Shader을 사용한 Blur 효과 (DirectX11,Opengl)

by greenherb 2021. 9. 14.

이전시간에 컴퓨트 셰이더를 사용하여 버퍼의 내용을 읽고 계산후 그결과를 다시 쓰는 작업을 보여드렸습니다.

이번에는 이미지의 내용을 읽고 다시 기록함으로서 가우시안 블러효과를 구현해보도록 하겠습니다.

이전에 컴퓨트 셰이더에서 버퍼의 읽기/쓰기를 수행하려면 어떤것이 필요했을까요?

DirectX11 에서는 StrucutredBuffer 즉 읽기만 수행하는경우 BufferStrutured 플래그가 지정된 버퍼가 필요하며 읽기/쓰기 둘다 진행하려면 따로 바인드 플래그에 D3D11_BIND_UNORDERED_ACCESS 를 지정한 버퍼가 필요했습니다.

OpenGL 에서는 SSBO(Shader Storage Buffer Object)  가 필요했습니다. 조사해보니 StructredBuffer의 특징이 SSBO 안에 포함되어 있더라구요. OpenGL의 경우 따로 다렉처럼 버퍼생성시 따로 나누는것이아니라 쉐이더 코드안에서 readonly,wirteonly 등 메모리 지시자와 함께 지정함으로 사용했습니다.

그렇다면 이미지를 컴퓨트셰이더에서 읽고 쓰기를 수행하려면 어떤것을 사용해야할까요? 그것은

DirectX11 에서는 텍스쳐를 생성할때 바인드 플래그에 D3D11_BIND_UNORDERED_ACCESS 를 붙여 생성하고 생성한 텍스쳐에 대한 순서없는접근뷰(Unodered Access View)를 생성하시면 됩니다. 다만 주의하실점은. UAV를 만드실때 구조체정보에 밉맵수준과, 배열의 경우 시작 지점부터 원하는 지점의 정보를 넘겨주셔야하며.  텍스쳐 생성시 바인드플래그는 Depth_Stencil 플래그와 함께 사용할 수 없음을 아셔야합니다.

OpenGL 에서는 Image store/Load 기능을 쉐이더에서 사용하시면됩니다. 따로 Dx처럼 텍스쳐 생성시 추가 작업이 있는것은 아니고 기존의 생성된 텍스쳐를 glBindImageTexture() 함수를 이용하여 슬롯에 바인딩하시면 바로 사용이 가능합니다. (상당히 간단하죠? 생성된 텍스쳐를 어떻게 사용할지에따라 해석이 달라집니다 물론.. 다렉처럼 자세하게 설정하는게 최적화면에서는 도움이 될 수 있을것같아요)

그렇다면 준비물은 위에서 설명한 텍스쳐에대한 뷰 혹은 텍스쳐를 바인딩하면되겠습니다. 이제 간단하게 가우시안 블러를 설명해볼까요?

이전글에서 컴퓨트셰이더는 그룹개수 x,y,z 방향으로 정하고, 그룹안의 쓰레드 개수를 정하여 각 쓰레드별로 쉐이더 내용을 실행 시킬수 있음을 압니다.  이전에는 단일배열의 데이터를 읽고 출력하기에 x,y,z 중 한가지만 필요했습니다. 하지만 이번에는 이미지의 내용을 읽고 출력을 해야하기 때문에 x,y 그룹을 사용해보도록하겠습니다(3d의경우는 z도 필요하겠죠?)

그럼 간단하게 160x80이미지를 컴퓨트셰이더에서 읽기작업을 수행한다고 해보겠습니다. 

그룹안의 쓰레드개수는 x - 16  ,y -16 총 256 개로 지정하겠습니다. 그렇담 이런경우에는 모두 읽으려면 몇개의 그룹이 필요할까요?

이미지의 크기가 쓰레드 개수에 정확히 딱 나눠떨어지게 나왔습니다. 이렇경우 쓰레드에서 160x80 이미지의 모든 텍셀들을 읽을 수 있겠죠. 다만 우리가 다뤄야할 이미지들은 이렇게 나눠 떨어지지 않는경우도 있을겁니다. 하지만 걱정하지마세요 우리에겐 GroupID,GlobalID가 있습니다. 이런 ID 값들을 통해 이미지 크기를 벗어나는 쓰레드의 경우 작업을 진행하지 않도록 하면됩니다.

예를들어 800x600이미지를 처리할경우 가로의경우 50개의 그룹으로 딱나눠떨어지지만 세로의경우 37.5  로 나눠지게 됩니다. 이럴경우 넉넉하게 y축으로 38개의 쓰레드 그룹을 잡아서 처리해주면 됩니다.

쓰기작업을할때도 위 방법을 고려하면서 적절한 쓰레드 ID값을 사용하시면됩니다! (읽기/쓰기방법은 실제 코드에서 보여드리도록 할게요)

그럼 아주 간단하게 가우시안 블러에대해서 설명해보도록 할게요 자세한 내용은 인터넷에 아주 많습니다!

이미지 출처:  https://iskim3068.tistory.com/41

위 이미지가 가우시안1차,2차 그래프를 나타냅니다.  보면 가운데는 솓아있고 가장자리로 갈수록 값이 낮아지는데요 이런 그래프를 이용하여 현재 픽셀의 가중치는 가장높은 가운데 그리고 주변픽셀은 점점 낮아지는 가중치를 가지게되어 현재 픽셀과 주변픽셀의 값으 더하여 색을 섞게됩니다.

그리고 가우시안 2차그래프는 2차원 평면상의 픽셀값의 가중치를 정해주죠. 2차원 가우시안 그래프를 사용하게되면 현재 픽셀의 값을 구하려면 주변 x,y 방향으로의 픽셀값을 읽고 가중치를 곱하여 최종 픽셀을 구하게 될겁니다. 예를들어 현재 픽셀 기준 앞뒤위아래로 -1~1 범위만큼 참고하여 픽셀값을 구하면 어떻게 될까요? 그렇게되면 연산 횟수는 3x3 범위의 픽셀들을 읽고 곱해줘야하므로 총 9번의 작업이 한픽셀의 색을 구하는데 처리될겁니다.

그렇다면 800x600x9 = 4,320,000 번 연산을 해야할까요 ? 다행이도 가우시안 블러는 분리가 가능합니다. 즉 1차원 그래프만으로 2차원 흐리기를 수행할 수 가 있어요!

위에는 가우시안 2차원 마스크입니다. 잘보시면 대각선을 기준으로 값들이 반대편에 있죠? (이런행렬을 뭐라고 부르죠..?)

이말인즉슨 1차원 마스크 x 1차원 마스크 = 2차원 마스크가 된다는것입니다. 9x9 마스크를 만들려면 9x9 가중치 행렬을 만들고 픽셀에 곱할것이아니라 9x1,1x9 행렬을 만들어 픽셀에 각각 곱한 결과를 더하면 2차원 마스크와 동일한 작업을 수행하게 됩니다. 총 18번의 작업만 수행되면 블러를 구현할 수 있겠네요.

즉 2차원을 1차원 두개로 나누었으니 가로방향(X 방향) 으로 수평흐리기 , 세로방향(Y방향) 으로 수직흐리기를 각각 한번씩 수행하면 가우시안 블러효과를 얻을 수 있습니다.

가중치값들을 구하는것은 총 마스크의 개수 / 마스크의 합 = 1 이되야합니다. 물론 이경우 직접 마스크를 지정할경우 고려해야할 부분이지만 간단하게 미리 총합이 1이되는 가중치값들을 사용합시다.

지름이 11인 수평흐리기를 수행할것이므로 (기준픽셀 상하좌우로 5씩 참고하는 즉 기준픽셀제외 10개의 픽셀에 가중치를 적용하고 더한다)

weights[11] = { 0.05f,0.05f,0.1f,0.1f,0.1f,0.2f,0.1f,0.1f,0.1f,0.05f,0.05f };

이 가중치값들을 사용해봅시다. 가운데 0.2f 가 현재 픽셀의 가중치이며 옆으로 갈수록 점점 가중치가 작아지는것을 확인할수있고 가중치값의 합은 1을 나타냅니다. (1보다 크거나 작아질경우 블러시 밝기가 변하게됩니다)

먼저 수평흐리기후 해당결과를 가지고 다시 수직흐리기를 수행하도록 하겠습니다. 그렇다면 어떻게 진행해야할까요?

실제로 핑퐁쉐이딩이라고 한 쉐이더의 결과를 다른 쉐이더의 입력으로 넣어서 쉐이딩하는 기법이있습니다. 실제로 블러에 적용할수 있고요. 우리도 컴퓨트세이더를 사용하여 블러를 진행할때 한 쉐이더의 결과를 다른쉐이더의 입력으로 넣고 또다시 그결과를 다른쉐이더의 입력으로넣어 작업해보도록 하겠습니다.

첫번째는 수평흐리기부터 진행하겠습니다.

필요한 텍스쳐는 현재 장면을 가지고있는 입력텍스쳐 한개와 해당 텍스쳐를 읽고 수평흐리기를 진행한 결과를 저장할 출력텍스쳐  총 2개가 필요합니다.

glsl 코드를 보도록하겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#version 450 core
 
layout (local_size_x = 256, local_size_y = 1, local_size_z = 1) in;
 
layout (binding = 0) uniform sampler2D inputImg;
layout (binding=0, rgba8)  uniform  writeonly image2D outputImg;
 
shared vec4 gCache[266];
float weights[11= {0.05f,0.05f,0.1f,0.1f,0.1f,0.2f,0.1f,0.1f,0.1f,0.05f,0.05f};
void main()
{
    vec2 imgSize = textureSize(inputImg,0);
    ivec2 globalindex = ivec2(gl_GlobalInvocationID.xy);
    ivec2 groupindex = ivec2(gl_LocalInvocationID.xy);
    vec2 index = min(vec2(globalindex),imgSize);
    vec2 coord = index / imgSize;
 
    gCache[groupindex.x + 5= textureLod(inputImg,coord,0);
    if(groupindex.x < 5)
    {
        int x = max(globalindex.x - 5 , 0);
        coord.x = (x/imgSize.x);
        gCache[groupindex.x] = textureLod(inputImg,coord,0);
    }
    if(groupindex.x >= 251)
    {
        int x= min(globalindex.x + 5,int(imgSize.x));
        coord.x = (x/imgSize.x);
        gCache[groupindex.x + 10= textureLod(inputImg,coord,0);
    }
    
    barrier();
 
    vec4 blurColor = vec4(0.0,0.0,0.0,0.0);
 
    for(int i=-5;i<=5;++i)
    {
        int k = groupindex.x + 5 + i;
        blurColor += gCache[k] * weights[i+5];
    }
 
    imageStore(outputImg,globalindex,blurColor);
}
cs

수평흐리기 glsl 코드입니다 처음으로 Y축줄마다 흐리기를 수행해야하므로 X축으로만 256개의 쓰레드를 할당합니다.일단 처음 입력으로 받는 sampler2D 코드부터 보겠습니다. 사실 이변수도 readonly 를 붙여서 image2D로 선언해도되지만 구글링하면서 본결과 sampler2D로 바인딩하는것이 그래픽메모리 캐시 방면에서 읽기속도가 더 빠르다고해서 입니다. 둘다 읽기/쓰기가 가능할 필요는 없겠지요?

그리고 두번째 출력용 텍스쳐부분입니다. sampler 와 image 객체는 서로 다른 슬롯을 쓰고있기 때문에 바인딩 포인트는 0번입니다. 그리고 rgba8은 해당 텍스쳐의 포맷을 얘기해줍니다. 실제로 서로 호환가능한 포맷일경우 rgba8 말고도 다른 포맷으로 지정할 수 있습니다.

그다음에는 한 그룹내에서 쓰레드간 공유할 수 있는 공유메모리인데요  shared 키워드를 사용하며 265개의 vec4 배열을 선언했습니다. 이것은 텍셀값을 저장할 공간입니다 왜 256이 아닐까요? 그것은 나중에 설명하도록 하겠습니다.

그다음 float 11개를 저장하는 배열은 반지름이 5인 커널을 나타내는 가중치 배열입니다. 해당 가중치를 가지고 픽셀값을 구할겁니다.이제 메인함수를 하나씩 설명해보겠습니다!.원래는! 만약 image 키워드로 선언한 텍스쳐의 경우 읽기/쓰기가 가능하다는점 알고 계실겁니다. 다만 이때 텍스쳐의 내용을 읽기위해서는 imageLoad 함수를 사용해야합니다. 위함수는 이미지 유닛과 두번째 파라매터로 정수인덱스를 받는데요 우리가 일반적으로 텍스쳐샘플링에 사용되는 [0-1] 사이의 uv 좌표값을 사용하지 않습니다. 말그대로 800x600 텍스쳐라면 해당 텍스쳐를 [0][0],[799][599] 처럼 배열 접근하듯이 텍셀에 접근합니다. 이경우 텍스쳐 필터링은 불가능 하겠죠?  (정확하진 않으나 hlsl 에서는 일반적인 텍스쳐를 컴퓨트 쉐이더에서 읽을때 [] 인덱스 기반으로 텍셀에서 읽기도 가능하고 따로 SamplerState를 사용하여 필터링도 가능합니다 다만 이때 SampleLevel 함수만 사용이 가능하다고 합니다.컴퓨트 셰이더는 추출할 텍스쳐의 밉맵수준을 자동으로 선택하는 논리가 없어서 직접 밉맵수준을 정해주는 SampleLevel 함수를 통해서 샘플링이 가능하다고합니다. 이때는 텍스쳐 가로,세로에 따른 좌표를 넘겨줘야합니다.)그렇다면 sampler2D를 위한 텍스쳐좌표를 얻기위한 코드가 나옵니다. 처음에 텍스쳐의 사이즈를 구하고 그후에는 각각 그룹ID와 그룹전체에서의 ID값을 구합니다. 그룹 전체에서의 ID값이 실제 텍스쳐의 텍셀의 위치에 해당하는 값이니 해당값을 이미지사이즈와 비교하여 작은쪽을 인덱스값으로 사용합니다 (위에서 설명한것처럼 쓰레드가 이미지 사이즈를 넘어갈경우 범위밖의 접근을 막기위함임)그리고 vec2 즉 float 형식의 텍스쳐좌표를 구하기위해 이미지사이즈로 인덱스값을 나눕니다. 그후 gCache[현재 그룹ID +5] 위치에 텍스쳐값을 저장하는데요. 왜 +5 일까요? 그것은 텍스쳐의 가장 왼쪽과 가장 오른쪽 가장자리에서의 가중치값을 곱해야하는 문제 덕분입니다. 공용메모리를 사용하는 주된 목적중하나는 매 컴퓨트쉐이더 쓰레드 실행마다 새롭게 인접픽셀의 값을 구하기위해 샘플링을 하기보다 각 쓰레드마다 미리 자기의 위치에 해당하는 텍셀값을 공용메모리에 저장하고 후에 가중치를 적용할때 바로바로 읽어오게 하기위함입니다. 실제로 샘플함수는 느린작업에 속하므로 매 쓰레드 실행마다 샘플링을 진행한다면 성능상에 지장을 줄 수도 있구요. 그렇다면 가장자리 텍셀에대한 가중치를 주기위해서는 가장자리너머에 임의의 공간을 두어 해당 값을 접근하여 가중치작업을 진행 할 수 있도록 해야합니다.
죄송합니다 그림을 못그려서..

가로사이즈가 256인 이미지를 생각해보겠습니다. 세로사이즈는 고려하지않습니다. 이 가로로만 긴 이미지를 흐릴려면 어떻게해야할까요? 당연히 인접텍셀과 현재 텍셀에 가중치값을 적용하여 모두 더하면되겠죠? 하지만 만약 첫번째 텍셀을 흐릴때 왼쪽으로는 인접텍셀이 없는데 어떻게 해야할까요? 일반적으로 정수색인으로 텍스쳐값에 접근하여 범위밖의 텍셀에 접근하게되면 0을 반환하도록 되어있습니다. 이럴경우.. 검은색에 가중치가 적용되어 현재 텍셀의값이 눈에띄게 변하게 될것입니다. 고로! 텍스쳐의 가장자리 텍셀에 접근할경우 특정 범위내의 텍셀의경우 2번값을 기록하도록 해야합니다.

위에서 처음 0번텍셀에 초록색선과 빨간색선이 보이실텐데요. 0번텍셀의 값을 읽으면 텍셀값은 5번에 저장하고 0번인덱스에도 저장합니다. 그다음 1번텍셀에 접근하면 1번텍셀의 값은 6번인덱스에 그리고 가장자리인 0번텍셀의 값을 1번인덱스에 저장합니다. 이렇게 연속적으로 처리하여 0~5까지의 인덱스는 0번텍셀의 값을 가지도록 처리합니다.

이럴경우 첫번째 텍셀을 구하기위해 가중치를 적용할경우 미리 준비해둔 0~4번인덱스 값에 0번텍셀값이 들어있기때문에 자연스럽게 가장자리 처리가 된다는점입니다. 오른쪽 가장자리도 동일합니다. 251번 텍셀에접근하면 미리 이전에 왼쪽에서 5만큼 인덱스를 사용했으므로 256인덱스에 저장하고 범위를 넘어가는 부분인 261 인덱스에는 251번 텍셀값을 입력하는것이죠

251번 텍셀에 가중치를 적용할때는 공유메모리에 접근하여 251~261까지의 인덱스에 접근하여 가중치를 적용하게됩니다.

이경우는 가장자리에만 속하는것이므로 그룹과 그룹이 이어지는부분에서도 생각해봅시다. 만약 0번인덱스는 첫번째 그룹 이후에오는 다음그룹의 첫번째 쓰레드가 담당하게될 텍셀이라고 해봅시다. 그렇다면 해당 0번인덱스에 들어갈 텍셀값은 텍스쳐의 범위를 넘어가지는 않지만 이전그룹에서의 범위와 겹치는 구간이 될것입니다.

int x = max(globalindex.x - 5 , 0);

int x= min(globalindex.x + 5,int(imgSize.x));

이 코드에서 max를 사용하고 그룹전체ID를 사용하는 이유이기도합니다. 그룹ID는 0~256이므로 이전그룹에서 그룹ID로 접근가능한 텍셀에 접근할 수 없습니다. 고로 그룹전체ID에서 -5 만큼 빼주게되면 이전그룹의 영역에 접근할 수 있겠죠

예를들어  0~255 까지의 텍셀이 처리되었고 256번째 텍셀을 처리한다고 가정해봅시다. 256 번째 텍셀은 현재 그룹의 첫번째 쓰레드가 처리할 텍셀입니다. 첫번째 텍셀이므로 가중치 적용을할때 왼족의 5개의 값이 필요한데 이5개의 값을 구하기위해 현재 그룹 ID에서 -5를 해줍니다. 256의경우 251텍셀에 접근하여 공유메모리의 0번째 인덱스에 저장할 것이고 차례대로 257-252 , 258-253 ... 260-255 연속적인 텍셀값이 저장될것입니다.  이것은 오른쪽 연결부분에서도 동일합니다. 현재 그룹에서 마지막 쓰레드 ID는 511이 될것입니다. 그렇다면 511을 접근하면 미리 공유메모리에 저장된 텍셀값에 접근하여 가중치를 적용할것인데. 502 텍셀 처리시 해당 텍셀은 공유메모리의 256 에 자신의 텍셀을 저장하고 해당텍셀의 값은 512가 되겠죠. 이렇게 오른쪽으로도 연속적인 텍셀접근이 가능합니다.

만약 첫번째 그룹과 마지막그룹에서는 max,min 함수는 범위를 넘어서서 접근하는것을 막기위해 0과 이미지사이즈 값으로 대체되겠죠.

자 그렇다면 이렇게 한그룹내의 공유메모리를 다 저장했으면 그밑의 Barrier 함수가 보일것입니다. 이함수는 모든쓰레드가 작업을 완료할때까지 먼저 완료한 쓰레드는 기다리는 함수입니다. 만약 모든 쓰레드가 작업을 끝냈다면 공유메모리에는 이미 샘플링한 텍셀값이 들어있습니다.

vec4 blurColor = vec4(0.0,0.0,0.0,0.0);

for(int i=-5;i<=5;++i) {

int k = groupindex.x + 5 + i;

blurColor += gCache[k] * weights[i+5];

}

imageStore(outputImg,globalindex,blurColor);

이제는 마지막으로 가중치배열에 접근하여 텍셀들에 곱한후 더한값을 imageStore을 통해 그룹전체ID를 사용하여 텍스쳐에 해당하는 위치에 저장하는 코드로 완료되게됩니다. 이과정까지 지나오게되면 이미지가 수평으로 흐려지게됩니다.

수직흐리기코드는 여기에서 별반 차이가 없기때문에 코드를 올리지 않겠습니다 하지만 바꿔야할 부분을 알려드릴게요

바꿔야할 부분은 쓰레드 사이즈를 정하는 곳을 x=1로 y는 256으로 바꾸시고 그룹전체ID와 그룹ID등 x로 접근했던것을 y에 맞추어 바꿔주시면됩니다. 

수평흐리기를 통해 나온 출력이미지를 다시 수직흐리기 쉐이더에 입력으로 넣어주시고 수직흐리기를 수행하시면 블러가 적용됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Sampler_Desc smpDesc;
smpDesc.addressU = WrapingMode::CLAMP;
smpDesc.addressV = WrapingMode::CLAMP;
Ref<Texture> m_ColorBuffer2;
m_ColorBuffer2 = Texture2D::Create(TextureFormat::RGBA8, smpDesc, desc.Width, desc.Height);
Ref<SamplerState> samp = SamplerState::Create(smpDesc);
 
RenderCommand::SetViewport(00, desc.Width, desc.Height);
        
Ref<TextureShaderView> readView1 = TextureShaderView::Create(TextureType::Texture2D, m_ColorBuffer, TextureFormat::RGBA8, 0101);
Ref<TextureShaderView> readView2 = TextureShaderView::Create(TextureType::Texture2D, m_ColorBuffer2, TextureFormat::RGBA8, 0101);
Ref<ReadWriteView> writeView1 = ReadWriteView::Create(TextureType::Texture2D, m_ColorBuffer, TextureFormat::RGBA8, 001);
Ref<ReadWriteView> writeView2 = ReadWriteView::Create(TextureType::Texture2D, m_ColorBuffer2, TextureFormat::RGBA8, 001);
        
 
//Blur
for (int i = 0; i < 1++i)
{
    m_HorizontalBlurShader->Bind();
    samp->Bind(0, ShaderType::CS);
    readView1->Bind(0, ShaderType::CS);
    writeView2->Bind(0);
 
    int xGroup = ceilf((float)desc.Width / 256.0f);
    RenderCommand::DispatchCompute(xGroup, desc.Height, 1);
    readView1->UnBind(0, ShaderType::CS);
    writeView2->UnBind(0);
 
    m_HorizontalBlurShader->UnBind();
 
    m_VerticalBlurShader->Bind();
    readView2->Bind(0, ShaderType::CS);
    writeView1->Bind(0);
 
    int yGroup = ceilf((float)desc.Height / 256.0f);
    RenderCommand::DispatchCompute(desc.Width, yGroup, 1);
 
    readView2->UnBind(0, ShaderType::CS);
    writeView1->UnBind(0);
    samp->UnBind(0, ShaderType::CS);
    m_VerticalBlurShader->UnBind();
}
cs

이부분은 핑퐁 쉐이딩을 위한 코드부분입니다.

처음부분은 SamplerDesc 즉 샘플러 스테이트 객체를 만들기위한 부분이며 다음 부분에서 서로 결과를 주고받을 현재 장면과 동일한 텍스쳐를 하나 생성합니다. 그후 하나는 읽기용 텍스쳐뷰,하나는 읽기/쓰기용 텍스쳐뷰 각각 2개씩 만듭니다.

(DX는 각각 SRV,UAV이며 GL의경우 각각 TextureView,TextureView 입니다만 ReadWrite를위해서 glBindImageTexture 를 사용한다는것이 다릅니다.)

그 후 반복문의 처음 부분에서 첫번째로 현재장면을 가르키는 텍스쳐뷰를 컴퓨트쉐이더의 0번슬롯에 바인딩합니다. 그다음 수평흐리기 결과를 저장할 결과이미지를 0번째 슬롯에 저장합니다. 그후 몇개의 X축으로 그룹을 사용할것인지 정해야하는데 이경우 현재 가로의 길이를 쓰레드 개수만큼 나눈것을 반올림하여 구합니다. 가로가 800이라고하면 3.125 정도나오니 4개의 그룹을 운용하게됩니다.

그후 Dispacth를 통해 컴퓨트쉐이더를 호출하게됩니다 여기서 세로의 그룹갯수는 세로사이즈만큼 정해줍니다.( 물론 그룹개수와 쓰레드 개수의 제한에 맞춰서 설정하시면 됩니다)

그다음 동일한 방법으로 이전에 출력으로 사용했던 텍스쳐의 입력용 텍스쳐뷰를 입력으로 바인딩하고 이전에 입력으로 사용했던 텍스쳐의 출력용 텍스쳐뷰를 바인딩합니다. 동일한 방법으로 Y 그룹의 개수를 정한다음 컴퓨트 쉐이더를 호출합니다.

위 코드의 경우 단 1번만 핑퐁했습니다 결과는 아래와 같습니다

 

블러가 아주 잘 되었죠? 솔직히말하면 블러효과는 작은 화면에다 하는것이 더 성능상 이점을 챙길수 있습니다. 예를들어 현재 이미지가 800x600 이라면 더작은 400x300 사이즈의 이미지에 기록을하여 블러된 작은 이미지를 다시 현재 화면 크기와 동일한 사각형쿼드에 그리게되면 확대하면서 자연적으로 블러가 적용되기 때문입니다.

Hlsl 코드도 보여드리도록하겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
Texture2D inputImage : register(t0);
SamplerState splr : register(s0);
RWTexture2D<float4> outputImage: register(u0);
 
groupshared float4 gCache[266];
const static float weights[11= { 0.05f,0.05f,0.1f,0.1f,0.1f,0.2f,0.1f,0.1f,0.1f,0.05f,0.05f };
 
[numthreads(25611)]
void CSMain(int3 groupindex :SV_GroupThreadID, int3 globalindex : SV_DispatchThreadID)
{
    float width, height;
    inputImage.GetDimensions(width, height);
 
    float2 imgSize = float2(width, height);
    float2 index = min(globalindex.xy, imgSize);
    float2 coord = index / imgSize;
 
    gCache[groupindex.x + 5= inputImage.SampleLevel(splr, coord, 0);
 
    if (groupindex.x < 5)
    {
        int x = max(globalindex.x - 50);
        coord.x = (x / imgSize.x);
        gCache[groupindex.x] = inputImage.SampleLevel(splr, coord, 0);
    }
    if (groupindex.x >= 251)
    {
        int x = min(globalindex.x + 5, imgSize.x);
        coord.x = (x / imgSize.x);
        gCache[groupindex.x + 10= inputImage.SampleLevel(splr, coord, 0);
    }
 
    GroupMemoryBarrierWithGroupSync();
 
    float4 blurColor = float4(0.0f, 0.0f, 0.0f, 0.0f);
    [unroll]
    for (int i = -5; i <= 5++i)
    {
        int k = groupindex.x + 5 +i;
        blurColor += gCache[k] * weights[i + 5];
    }
    outputImage[globalindex.xy] = blurColor;
 
}
 
cs

다른점은 image 객체대신 RWTexture2D<float4> 를 사용했다는것입니다. 해당 키워드가 읽기/쓰기 텍스쳐를 의미하며 u0~u9의 레지스터를 사용합니다. 여기서 중요한점은 glsl에서의 rgba8 처럼 텍스쳐의 포맷을 호환가능한 포맷에 한해 다른것으로 바꿔서 해석할 수 있다는것입니다.

두번째로는 shared가 groupshared 키워드로 사용되는것입니다. 그룹ID,전체ID의 경우 이전글에서 설명했으니 넘어가겠습니다.

glsl의 barrier() 함수는 hlsl 에서 GroupMemoryBarrierWithGroupSnyc()함수로 대체됩니다.

glsl 에서는 값을 저장할때 imageStore함수를 사용해야하지만 hlsl의경우 정수인덱스접근을 통해 바로 할당할 수 있습니다.

마지막으로 만약 핑퐁을 4번 주고받으면서 블러를 수행하면 어떻게 될까요?

위에 이미지보다 더 흐려보이죠?!

 

============!여기서부터 주의사항!===============================

제 쉐이더코드에서는 범위밖을 접근했을경우를 대비해 공유메모리가있는데 이상하게 가장자리에서 문제가 일어나는 경우가 있었습니다.

하단에 위에보이는 픽셀들이 조금보인다.

아마 제가 잘못 코드를짜서 그럴수도 있을겁니다 보통 이런문제들은 텍스쳐필터링이 Clamp 즉 가장자리너머의 텍스쳐값에 접근했을때 가장자리값을 반환하지않고 Mirror나 Repeat 등으로 다른 위치의 텍셀값을 가져오는경우 발생하게됩니다. 저는 이것을 방지하기위해 범위를 지정해줬는데도 생기는걸보면 코드가 잘못된것일수도 있다고 생각합니다.

고로! glsl이나 hlsl이나 샘플러 스테이트를 꼭! Clamp 로 지정하시길 바랍니다.!

 

2번째로 정수인덱스에서 텍스쳐좌표를 구할때 문제입니다! 정수인덱스는 int이며 텍스쳐좌표는 float이기에 적절하게 계산해주지 않으면 아래와같은 이미지가 나올수 도 있습니다.

이것은 텍스쳐좌표가 잘못지정된것이 100% 이므로 잘 계산되었는지 확인해보셔야합니다.

 

==================================================================

이제 컴퓨트 셰이더를 이용하여 이미지에 읽기/쓰기 수행을 해보았습니다.

우리의 목표인 애니메이션을위해 정점버퍼의 내용을 컴퓨트 셰이더를 이용하여 바꾸는 작업을 다음에 해보도록하겠습니다.

가장 기본적인 파티클시스템에서 정점들을 컴퓨트 셰이더로 구현해보도록 하겠습니다.