Wednesday, December 9, 2015

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

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

현재 6편이 포스팅되어 있으며 이번이 마지막 부분입니다. 혹시 나중에 추가되는 경우 더 올리도록 할 생각입니다.

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

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

---------------------------------------------------------------------------------------------------
Unity를 위한 DirectCompute 튜토리얼 : Consume 버퍼
- From ScrawkBlog (http://scrawkblog.com/)

 이번 튜토리얼에서는 Direct Compute에서 consume 버퍼를 어떻게 사용하는지에 대해 다룰 것입니다. Append 버퍼와 consume 버퍼는 비슷한 종류이기 때문에 원래는 append 버퍼 튜토리얼에서 함께 다루려 했습니다. 하지만 너무 길어질 것 같아서 분리하기로 결정했습니다. 지난 튜토리얼에서는 “수정” 부분을 추가했는데요, Unity에서의 append 버퍼에 몇 가지 문제점이 있는 것으로 밝혀졌기 때문이었습니다. 어떤 그래픽 카드에서는 동작하지 않는 것 같습니다. 보고된 버그 리포트들이 좀 있고, 앞으로 고쳐졌으면 하는 바람입니다.

 Consume 버퍼를 사용하기 위해서는 append 버퍼를 사용해야 하고, 따라서 같은 문제가 이번 튜토리얼에도 발생합니다. 지난 번 튜토리얼의 코드들이 여러분의 그래픽카드에서 동작하지 않는다면, 이번 것도 마찬가지일겁니다.

 또한 append와 consume 버퍼를 만들 때 실수를 했다면, 코드를 고치고 재 실행을 한다고 하더라도 예측 불가능한 결과가 발생할 수 있다는 것을 다시 알려드립니다. 이러한 문제가 발생한다면, 특히 에러로 인해 GPU 충돌이 발생했다면 GPU 컨텍스트(context)를 제거하기 위해 Unity를 재시작하는 것이 좋습니다.

 Unity에서의 consume 버퍼에 대해 들어본 적 없는 분들이 많을 겁니다. 흔히 사용되는 기능도 아니고 사실은 그렇게 유용하지도 않습니다. 이는 consume 버퍼가 동작하는 방식이 GPU 동작 방식과 그렇게 잘 맵핑되지 않기 때문입니다. 그래도 알아두면 좋은 것이, GPU에서 일반적으로 수행되지 않는 기능을 사용할 수 있게 하고, 또한 append 버퍼의 동작 방식에 대한 고찰을 할 수 있게 해줍니다.

 Append 버퍼의 데이터만 consume이 가능하기 때문에 , 시작하기에 앞서 append 버퍼에 데이터를 넣어주어야 합니다. C# 스크립트를 만들고 아래 코드를 붙이세요.


public class ConsumeBufferExample : MonoBehaviour
{
    public Material material;
    public ComputeShader appendBufferShader;

    const int width = 32;
    const float size = 5.0f;

    ComputeBuffer buffer;
    ComputeBuffer argBuffer;

    void Start()
    {

        buffer = new ComputeBuffer(width * width, sizeof(float) * 3, ComputeBufferType.Append);

        appendBufferShader.SetBuffer(0, "appendBuffer", buffer);
        appendBufferShader.SetFloat("size", size);
        appendBufferShader.SetFloat("width", width);

        appendBufferShader.Dispatch(0, width/8, width/8, 1);

        argBuffer = new ComputeBuffer(4, sizeof(int), ComputeBufferType.DrawIndirect);

        int[] args = new int[]{ 0, 1, 0, 0 };
        argBuffer.SetData(args);

        ComputeBuffer.CopyCount(buffer, argBuffer, 0);
        argBuffer.GetData(args);

        Debug.Log("vertex count " + args[0]);
        Debug.Log("instance count " + args[1]);
        Debug.Log("start vertex " + args[2]);
        Debug.Log("start instance " + args[3]);

    }

    void OnPostRender ()
    {
        material.SetPass(0);
        material.SetBuffer ("buffer", buffer);
        material.SetColor("col", Color.red);
        Graphics.DrawProceduralIndirect(MeshTopology.Points, argBuffer, 0);
}

    void OnDestroy ()
    {
        buffer.Release();
        argBuffer.Release();
    }

}


 여기서는 append 버퍼를 만들고 “appendBufferShader”의 각 실행되는 쓰레드마다 위치를 append 버퍼에 추가해주고 있습니다.

 결과를 렌더링하기 위한 셰이더도 필요합니다. 지난번 튜토리얼에 포스팅했던 “Custom/AppendExample/BufferShader”가 사용될 수 있으며 여기 다시 올리지 않겠습니다. Append 버퍼 튜토리얼에서 찾아보시거나 프로젝트 파일을 다운로드하세요(이 튜토리얼 마지막에 링크되어 있습니다).

 이제 스크립트를 카메라에 붙이시고, material과 compute shader를 할당하고 씬을 실행하세요. 빨간 점들로 이루어진 그리드를 볼 수 있을 겁니다.

 우리는 몇몇 점들을 append했고, 이제 consume을 할 것입니다. 아래 변수를 스크립트에 추가하세요.


public ComputeShader consumeBufferShader;


Append 셰이더의 Dispatch 호출 다음에 아래 두 줄을 추가하세요.


consumeBufferShader.SetBuffer(0, "consumeBuffer", buffer);

consumeBufferShader.Dispatch(0, width /8, width /8, 1);


 이 부분이 append 버퍼의 데이터를 consume하는 compute shader를 실행할 것입니다. 새 compute shader를 만들고 아래 코드를 넣으세요.


#pragma kernel CSMain

ConsumeStructuredBuffer<float3> consumeBuffer;

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{

    consumeBuffer.Consume();

}


 이 셰이더를 스크립트에 할당하고 씬을 실행해보세요. 아무것도 보이지 않을 겁니다. 콘솔창에 정점 개수가 0개인 것을 보실 수 있습니다. 데이터에 무슨 일이 일어난걸까요?

 여기 이 줄이 무언가를 한것입니다.


consumeBuffer.Consume();


 이것은 호출될 때마다 append 버퍼의 요소 하나를 제거합니다. Append 버퍼의 요소 개수만큼 쓰레드를 실행했기 때문에 결국엔 모든 것이 제거된 것입니다. 꽤 간단한 작업이지만 몇 가지 중요한 과정이 있습니다.

 버퍼는 아래와 같이 consume 버퍼로 선언되어야 합니다.


ConsumeStructuredBuffer<float3> consumeBuffer;


 하지만 스크립트에서 설정한 버퍼는 consume 타입이 아닌 것을 보세요. Append 버퍼였습니다. 생성되는 부분에서 알 수 있습니다.


buffer = new ComputeBuffer(width * width, sizeof(float) * 3, ComputeBufferType.Append);


 consume이라는 타입은 없고, append만 있습니다. 버퍼가 어떻게 사용될지는 compute shader내에서 어떻게 선언되었는지에 달려있습니다. 데이터를 append하기 위해서는 “AppendStructuredBuffer”로 선언하고, consume하기 위해서는 “ConsumeStructuredBuffer”로 선언하십시오.

 버퍼에서 데이터를 consume하는 것에는 리스크가 있습니다. 지난 번 튜토리얼에서 버퍼 크기보다 많은 요소를 append하면 GPU 충돌이 발생한다는 것을 언급한 적 있습니다. 버퍼에 있는 요소보다 더 많은 것을 consume하면 어떻게 될까요? 쉽게 예상하실 수 있겠죠. GPU 충돌이 발생합니다. 테스트 중에는 항상 버퍼 내 요소의 개수를 출력해서 예상하는 대로 코드가 동작하고 있는지 확인하세요.

 버퍼의 모든 요소를 제거하는 것은 append 버퍼를 초기화하는 좋은 방법이지만 (또한 재 생성하지 않고 버퍼를 clear하는 유일한 방법입니다) 몇몇 요소들만 제거하는 경우 어떻게 될까요?

 Consume 셰이더의 dispatch 호출을 아래와 같이 바꿔보세요.


consumeBufferShader.Dispatch(0, width/2 /8, width/2 /8, 1);


 여기서는 버퍼 내 요소의 1/4에 대해서만 셰이더를 실행하고 있습니다. 문제는 어떤 요소가 제거되는 것일까 하는거죠. 씬을 실행 해 보세요. 몇몇 점이 사라진 화면이 출력되는 것을 보실 수 있을겁니다. 콘솔창을 보시면 768개의 요소가 버퍼에 있는 걸을 알 수 있습니다. 1024에서 1/4(256)개가 제거되었으니 768개가 남습니다. 하지만 문제가 있습니다. 제거된 요소는 무작위적으로 선택되는 것 같아보이고 (대부분의 경우) 씬을 실행할 때마다 달라질 것입니다.

 이러한 특성이 consume 버퍼의 사용성에 제약을 주지만, 그래서 쓸모 없다는 것은 아닙니다. LIFO(역주: Last In First Out, 후입선출)는 GPU에서 불가능한 것이었고, 요소의 정확한 순서가 중요하지 않은 한 이러한 과거에는 불가능했던 알고리즘의 동작을 가능하게 합니다. Direct Compute는 또한 쓰레드 동기화(thread synchronization)를 통해 쓰레드의 동작 방식에 대해 어느 정도 제어가 가능하도록 기능을 추가하였고, 이에 대해서는 나중 튜토리얼에서 다루겠습니다.

 Consume 버퍼는 여기까지이고, 다음 튜토리얼에서는 마지막 버퍼 종류인 counter 버퍼에 대해 다뤄보겠습니다.

Project Files.

No comments:

Post a Comment