Sunday, December 6, 2015

Unity DirectCompute 튜토리얼 시리즈 번역 - 3

개인 학습 및 정보 공유 차원에서 Unity에서의 DirectCompute(compute shader를 사용한) 튜토리얼 시리즈 번역을 할 예정입니다. 원작자에게 허가를 받은 사항입니다.

현재 6편이 포스팅되어 있으며 가능한 하루에 하나씩 번역해 올릴 생각입니다.

원문은 ScrawkBlog (http://scrawkblog.com/)에서 보실 수 있습니다. 좋은 자료가 많이 있으니 관심 있으신 분들은 한번 들어가 보세요.

(*제가 추가한 부분은 역주 표시가 되어있으며, 일부 한/영 번역이 일관성이 부족할 수 있습니다.)

---------------------------------------------------------------------------------------------------
Unity를 위한 DirectCompute 튜토리얼 : 텍스처
- From ScrawkBlog (http://scrawkblog.com/)

 오늘 튜토리얼에서는 텍스처에 집중할 것입니다. 텍스처는 아마도 DirectCompute를 사용할 때 가장 중요한 기능 중 하나일 겁니다. 아마 당신이 작성하는 셰이더는 적어도 하나의 텍스처는 사용할 가능성이 높습니다. 안타깝게도 렌더 텍스처(render texture)는 Unity Pro에서만 가능합니다. 좋은 소식은 오늘 튜토리얼 외에는 Unity Pro에서만 사용 가능한 기능은 없다는 겁니다. DirectCompute에서의 텍스처는 사용하기 간단하지만, 당신이 빠져들지도 모르는 몇 가지 함정이 있습니다. 간단한 것부터 시작해보죠. Compute shader를 만들고 아래 코드를 붙여 넣으세요.

#pragma kernel CSMain

RWTexture2D<float4> tex;

float w, h;

[numthreads(8,8,1)]
void CSMain (uint2 id : SV_DispatchThreadID)
{
    tex[id] = float4(id.x / w, id.y / h,  0.0, 1.0);
}


 그리고 TextureExample이라는 c# 스크립트를 만들고 아래 코드를 붙여 넣으세요.

using UnityEngine;
using System.Collections;

public class TextureExample : MonoBehaviour
{
    public ComputeShader shader;

    RenderTexture tex;

    void Start ()
    {

        tex = new RenderTexture(64, 64, 0);
        tex.enableRandomWrite = true;
        tex.Create();

        shader.SetFloat("w", tex.width);
        shader.SetFloat("h", tex.height);
        shader.SetTexture(0, "tex", tex);
        shader.Dispatch(0, tex.width/8, tex.height/8, 1);
    }

    void OnGUI()
    {
        int w = Screen.width/2;
        int h = Screen.height/2;
        int s = 512;

        GUI.DrawTexture(new Rect(w-s/2,h-s/2,s,s), tex);
    }

    void OnDestroy()
    {
        tex.Release();
    }
}


스크립트를 붙이고, 셰이더를 할당한 후 씬을 실행해 보세요. Uv가 색상으로 표현된 텍스처를 보실 수 있을겁니다. 이것이 compute shader를 통해 우리가 텍스처에 출력한 내용입니다.

지난번에 다루었던 커널 인수 “uint2 id : SV_DispatchThreadID”를 보십시오. 이것은 쓰레드 그룹들 내의 쓰레드의 포지션이고, 버퍼를 사용할 때처럼, texture에 결과값을 어느 곳에 할당할 것인지를 파악하는 데 필요합니다, “tex[id] = result” 부분처럼요. 하지만 이번에는 “평탄화된”(역주: 버퍼처럼 1차원으로 변환된) 인덱스는 필요 없습니다. Uint2를 직접 사용하면 됩니다. 이건 버퍼와는 달리 텍스처가 다차원이기 때문이죠. 우리는 2D 쓰레드 그룹과 2D 텍스처를 갖고있습니다.

이제 텍스처 변수의 선언부분인 “RWTexture2D tex;“를 보십시오. “RWTexture2D” 부분이 중요합니다. 당연히 이건 텍스처인데, RW는 뭘까요? 이것은 텍스처의 타입이 “unordered access view” (역주: 적당한 한글 번역을 찾지 못했습니다. 약어로 UAV라고 더 자주 불립니다) 라고 선언하는 것입니다. 이건 셰이더가 텍스처의 아무 부분에나 데이터를 쓸 수 있다는 뜻입니다. 텍스처로 값을 쓸 수는 있지만 읽을 수는 없습니다. RW를 지우면 보통 텍스처와 같이 되지만 값을 출력할 수 없습니다. 그냥 Texture2D에서는 읽기만 가능하고, RWTexture2D에는 쓰기만 가능하다는 것을 기억하시면 됩니다.

이제 스크립트 부분을 봅시다. 텍스처가 어떻게 생성되는지 보세요.

tex = new RenderTexture(64, 64, 0);
tex.enableRandomWrite = true;
tex.Create();


여기엔 중요한 것이 두 가지 있습니다. 첫 번째는  “enableRandomWrite“입니다. 텍스처에 값을 쓰려면 이것을 true값으로 지정해야 합니다. 이는 근본적으로 텍스처가 unordered access view를 갖는다는 이야기입니다. 이렇게 하지 않으면 실행해도 아무 일이 일어나지 않고, Unity는 에러를 뱉어내지도 않습니다. 겉보기에는 아무 이유 없이 그냥 실패하는 거지요. 두 번째는 “Create” 함수 호출입니다. 텍스처에 값을 쓰기 전에 생성을 먼저 해야 합니다. 마찬가지로, 이렇게 하지 않으면 에러도 나지 않고 아무 일도 일어나지 않습니다. Graphics blit의 텍스처에 값을 쓰려면 create를 호출할 필요는 없다는 것을 눈치채실 겁니다. 이건 Graphics blit이 텍스처가 생성되었는지 확인하고, 아니라면 알아서 생성하기 때문이지요. Dispatch 함수는 호출되었을 때 어떤 텍스처에 값이 쓰여지는지 알 수 없기 때문에 이를 대신할 수 없습니다.

또한, “OnDestroy” 함수에서 texture가 릴리즈(release)된 것을 보십시오. Render texture의 사용이 끝나면 릴리즈하는 것을 잊지 마세요.

이제 dispatch 호출을 봅시다.

shader.Dispatch(0, tex.width/8, tex.height/8, 1);


이 부분이 여러 그룹들을 실행하는 부분이라는 것을 지난 튜토리얼에서 다루었습니다. 자, 그럼 왜 실행할 그룹들의 수가 텍스처 크기를 8로 나눈 숫자 만큼일까요? 셰이더를 보십시오. “[numthreads(8,8,1)]” 줄에서 그룹당 8개의 쓰레드를 실행한 다는 것을 알 수 있습니다(역주: 64개지만, 한 차원당 8개라는 의미인 것으로 생각됩니다). 우리는 각 픽셀당 실행해야 할 쓰레드가 있습니다. 따라서 우리는 64픽셀 너비의 텍스처가 있고, 이를 그룹당 쓰레드의 숫자로 나누면, 필요한 그룹의 숫자를 얻을 수 있습니다. 결국 8개의 쓰레드를 갖는 8개의 그룹이고, x차원에서 총 64개의 쓰레드가 있습니다. 그리고 y차원도 마찬가지고요. 따라서 총 4096(64 * 64)개의 쓰레드가 돌아가고 이는 텍스처 내의 픽셀 개수와 같습니다.
이제 스크립트의 아래 부분을 봅시다.

shader.SetFloat("w", tex.width);
shader.SetFloat("h", tex.height);
shader.SetTexture(0, "tex", tex);


이 부분은 셰이더의 uniform을 설정하는 부분입니다. 우리가 작성할 텍스처를 설정해야 하고, 또한 dispatch 아이디에서 uv를 계산할 수 있도록 텍스처의 너비와 높이가 필요합니다. 이런 식으로 셰이더에 변수를 전달하는 것이 일반적이지만, 이 경우에는 너비와 높이를 전달할 필요가 없습니다. 셰이더 안에서 그 값을 얻을 수 있습니다. 셰이더를 아래와 같이 바꿔보세요.

#pragma kernel CSMain

RWTexture2D<float4> tex;

[numthreads(8,8,1)]
void CSMain (uint2 id : SV_DispatchThreadID)
{
    float w, h;
    tex.GetDimensions(w, h);

    float2 uv = float2(id.x/w, id.y/h);

    tex[id] = float4(uv, 0.0, 1.0);
}


그리고 아래 두 줄을 스크립트에서 지워보세요.

shader.SetFloat("w", tex.width);
shader.SetFloat("h", tex.height);


씬을 실행해 보십시오. 같은 결과가 나와야 합니다. “tex.GetDimensions(w, h);” 줄 부분을 보세요. 텍스처가 객체입니다. 이는 호출할 수 있는 함수가 있다는 것을 의미합니다. 이 경우 우리는 텍스처 크기를 요청하고 있습니다. 텍스처는 호출할 수 있는 여러 함수들과 그 오버로드(overload)들이 있습니다. 자주 사용하는 것들과 그것들을 어떻게 사용하는지 다룰 것이지만, 그 전에 씬을 조금 바꿔야 합니다. 지금 하려고 하는 것은 우리 텍스처에서 다른 텍스처로 내용을 복사하고 결과를 보여주는 것입니다.
먼저 새 compute shader를 만들고 아래 코드를 붙여넣으세요.

#pragma kernel CSMain

RWTexture2D<float4> texCopy;
Texture2D<float4> tex;

[numthreads(8,8,1)]
void CSMain (uint2 id : SV_DispatchThreadID)
{
    float4 t = tex[id];

    texCopy[id] = t;
}


스크립트는 아래와 같이 바꾸세요.

using UnityEngine;
using System.Collections;

public class TextureExample : MonoBehaviour
{
    public ComputeShader shader, shaderCopy;

    RenderTexture tex, texCopy;

    void Start ()
    {

        tex = new RenderTexture(64, 64, 0);
        tex.enableRandomWrite = true;
        tex.Create();

        texCopy = new RenderTexture(64, 64, 0);
        texCopy.enableRandomWrite = true;
        texCopy.Create();

        shader.SetTexture(0, "tex", tex);
        shader.Dispatch(0, tex.width/8, tex.height/8, 1);

        shaderCopy.SetTexture(0, "tex", tex);
        shaderCopy.SetTexture(0, "texCopy", texCopy);
        shaderCopy.Dispatch(0, texCopy.width/8, texCopy.height/8, 1);
    }

    void OnGUI()
    {
        int w = Screen.width/2;
        int h = Screen.height/2;
        int s = 512;

        GUI.DrawTexture(new Rect(w-s/2,h-s/2,s,s), texCopy);
    }

    void OnDestroy()
    {
        tex.Release();
        texCopy.Release();
    }
}


이제 “shaderCopy” 속성에 새로운 셰이더를 할당하고 씬을 실행해 보세요. 씬은 전과 동일하게 보여야 합니다. 여기서 우리가 하는 것은 전과 같이 첫 번째 텍스처의 uv에 따라 색상을 채우고 이를 다른 텍스처로 복사하는 것입니다. 이를 통해 텍스처에서 셰이더로 샘플링하는 여러 방법을 보여주고자 합니다. 복사 셰이더(역주: 새로 추가한 두 번째 셰이더)의 “float4 t = tex[id];” 부분을 보세요. 이는 텍스처를 샘플링하는 가장 간단한 방법입니다. Dispatch 아이디가 값을 쓰는 부분인 것처럼 이는 또한 값을 읽을 부분을 나타내기도 합니다. 텍스처가 배열처럼 값을 샘플링할 수 있다는 것을 볼 수 있습니다. 다른 방법들도 있습니다. 예를 들어,

 float4 t = tex.mips[0][id]; 

여기서는 텍스처의 밉맴(mipmap)에 접근합니다. 레벨 0이 첫 번째 밉맵이고, 텍스처와 같은 크기입니다. 밉맵 배열의 다음 차원은 dispatch 아이디를 사용하여 위치를 지정하는 부분입니다. 우리는 텍스처의 밉맵을 활성화 하지 않았기 때문에 레벨 0 이외에는 효과가 없다는 것을 알아 두십시오. 텍스처의 load함수를 통해서도 같은 작업을 할 수 있습니다.

 tex.Load(uint3(id,0)); 

이 경우 uint3의 x와 y값이 샘플링할 위치이고 z값(0)이 밉맵 레벨입니다. 두 방법 모두 똑같이 동작합니다.

텍스처의 기능 중 자주 사용하실 부분이 하나 있습니다. 텍스처의 필터와 랩(wrap) 기능입니다. 이를 위해선 샘플러 상태(sampler state)를 사용해야 합니다. HLSL를 Unity외의 다른 환경에서 사용 해 보신 적 있으시다면 이를 사용하기 위해선 샘플러 객체를 만들어야 한다는 것을 아실겁니다. Unity에서는 약간 다릅니다. 기본적으로 두 종류의 필터(선형(linear) 또는 점(point)와 두 종류의 랩(클램프(clamp) 또는 반복(repeat))을 선택할 수 있습니다. 해야 할 일은 셰이더 내에서 샘플러 상태를 Linear 또는 Point 단어 이름으로, Repeat 또는 Clamp 단어 이름으로 선언해 주기만 하면 됩니다. 예를 들어 myLinearClamp 또는 aPointRepeat같은 이름을 사용하실 수 있습니다. 저는 언더바를 선호합니다. 셰이더를 아래와 같이 바꾸세요.

#pragma kernel CSMain

RWTexture2D<float4> texCopy;
Texture2D<float4> tex;

SamplerState _LinearClamp;
SamplerState _LinearRepeat;
SamplerState _PointClamp;
SamplerState _PointRepeat;

[numthreads(8,8,1)]
void CSMain (uint2 id : SV_DispatchThreadID)
{
    float w, h;
    texCopy.GetDimensions(w, h);
    float2 uv = float2(id.x/w, id.y/h);

    float4 t = tex.SampleLevel(_LinearClamp, uv, 0);
    texCopy[id] = t;
}


씬을 실행하면 여전히 똑 같은 결과가 보여야 합니다. “float4 t = tex.SampleLevel(_LinearClamp, uv, 0);“ 코드를 보세요. 여기서는 텍스처의 SampleLevel 함수를 사용하고 있습니다. 이 함수는 샘플러 상태, uv와 밉맵 레벨을 인수로 받습니다. Uv는 0과 1사이 범위로 정규화(normalize)되어야 합니다. 위 쪽의 SamplerState 변수를 보세요. 샘플러 상태를 사용한다면 아마 bilinear 필터링을 사용하고 싶으실겁니다. 그런 경우라면, _LinearClamp 또는 _LinearRepeat 샘플러 상태를 사용하세요.

HLSL을 픽셀 셰이더로 사용했었다면(여기 compute shader와는 반대로요) 아래 함수를 필터링에 사용할 수 있다는 것을 아실겁니다.

 float4 t = tex.Sample(_LinearClamp, uv); 

SampleLevel이 아닌 Sample 코드를 호출한다는 것과, 밉맵 파라메터가 없어진 것을 보십시오. 만약 이 함수를 compute shader에서 사용하려고 한다면 이 함수가 존재하지 않기 때문에 에러가 날 것입니다. 그 이유는 생각외로 복잡하고 어떻게 GPU가 동작하는지에 대한 이해를 도울 수 있을겁니다. 씬 아래서 픽셀 셰이더(또는 어떤 셰이더라도)는 compute shader와 같은 GPU 아키텍처를 공유하여 돌아갑니다(역주: 오역주의 ㅜㅜ, 원문은 “Behind the scenes fragments shaders (or any shader) work in much the sample why as a compute shader as they share the same GPU architecture.”). 그들은 쓰레드를 실행하고 쓰레드는 쓰레드 그룹 안에 정렬되어 있습니다. 쓰레드 그룹 안의 쓰레드들이 메모리를 공유한다는 것을 기억하세요. 픽셀 셰이더는 적어도 2x2의 쓰레드로 이루어진 그룹에서 돌아갑니다. 텍스처를 샘플링 할 때에, 픽셀 셰이더는 이웃한 uv가 무엇인지 체크합니다. 이를 기반으로 uv의 미분값(derivatives)을 도출합니다. 미분값은 변화율이고, 변화율이 큰 부분에서는 높은 레벨의 밉맵이, 작은 부분에서는 낮은 레벨의 밉맵이 사용됩니다. 이것이 GPU가 앨리어싱(aliasing)문제를 해결하는 방법이고, 추가적으로 메모리 대역폭을 줄이는 효과도 있습니다(높은 레벨의 밉맵은 더 작습니다).
그래서 이것이 compute shader와 무슨 상관일까요? Compute shader는 보통의 GPU 파이프라인에서 동작하는 것이 아닙니다. 그래서 더 자유도가 높기도 하지만, 어떤 작업들은 스스로 해야 한다는 것을 의미하기도 합니다. Sample함수는 GPU가 자동으로 미분값을 계산해 주지 않기 때문에 지원되지 않는 것입니다. 하지만 그래서 못한다는 것은 아니구요, 비슷하게 SampleGrad 함수를 사용할 수 있습니다.

 float4 t = tex.SampleGrad(_LinearClamp, uv, dx, dy); 

하지만 미분값(dx와 dy)는 스스로 계산하셔야 합니다. 못할 이유는 없습니다. 쓰레드들은 쓰레드 그룹에서 돌아가고 그들은 메모리를 공유할 수 있다는 것을 기억하세요(메모리 공유에 대해서는 나중 포스트에서 다룰 것입니다(역주: 아직 안나온 것으로 압니다 ㅜㅜ)). 헷갈려도 걱정하지 마세요. 이것을 해야 할 경우는 많지 않고, 대부분의 경우 bilinear 필터링으로 충분합니다.

지금까지의 예제는 2차원이었지만 3차원에서도 같은 원리가 적용됩니다. 단지 텍스처를 약간 다른 방식으로 생성하시면 됩니다.

tex = new RenderTexture(64, 64, 0);
tex.volumeDepth = 64;
tex.isVolume = true;
tex.enableRandomWrite = true;
tex.Create();


이는 64*64*64크기의 텍스처를 생성합니다. “volumeDepth”이 64로, “isVolume” 이 true로 설정된 것을 보세요. 쓰레드와 그룹의 숫자도 잘 설정하는 것을 잊지 마세요. Dispatch 아이디 또한 uint3 또는 int3이어야 합니다.

 shader.Dispatch(0, tex.width/8, tex.height/8, tex.depth/8); 

그리고

[numthreads(8,8,8)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    tex[id] = anotherTex[id];
}


이제 텍스처에 대한 부분을 다뤄봤습니다. 다음 시간엔 버퍼에 대해 살펴보겠습니다. 기본 버퍼와,다른 버퍼 타입을 어떻게 사용하는지, 버퍼에 데이터를 어떻게 작성하는지와 버퍼 데이터를 입력/출력하는 방법에 대해 볼 것입니다.


No comments:

Post a Comment