Tuesday, December 8, 2015

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

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

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

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

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

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

오늘은 지난번에 했던 버퍼 주제를 확장해서 append 버퍼에 관해 다루겠습니다. 이 버퍼는 compute shader 또는 Cg 셰이더의 실행중에 동적으로 크기를 늘릴 수 있는 버퍼로 structured 버퍼에 굉장한 유연성을 더해줍니다.

수정-Unity에서의 append 버퍼가 예측불가능한, 버그성의 결과를 나타내는 경우가 있다는 것을 알려 드립니다. 버퍼에서부터 개수를 가져올 때, 뭔가 잘못 구현했다면 문제가 발생할 수 있고 이에 대해 다룰 것이긴 합니다. 몇몇 GPU에서는 이것이 동작하지 않는 경우도 보고된 바 있습니다. Unity의 버그인지 GPU 드라이버의 버그인지는 확실하지 않습니다.

이 버퍼의 좋은 점은, 이 버퍼가 여전히 structured 버퍼로 사용할 수 있고 따라서 그 내용을 렌더링 하기 쉽다는 것입니다. 새로운 셰이더를 만들고 이 코드를 붙여넣는 것부터 시작해 봅시다. 이는 우리가 버퍼에 추가한 점들을 화면에 그리기 위한 코드입니다. 버퍼가 structured 버퍼로 선언되었다는 것에 유의하세요.

Shader "Custom/AppendExample/BufferShader"
{
    SubShader
    {
        Pass
        {
            ZTest Always Cull Off ZWrite Off
            Fog { Mode off }

            CGPROGRAM
            #include "UnityCG.cginc"
            #pragma target 5.0
            #pragma vertex vert
            #pragma fragment frag

            uniform StructuredBuffer<float3> buffer;
            uniform float3 col;

            struct v2f
            {
               float4  pos : SV_POSITION;
            };

            v2f vert(uint id : SV_VertexID)
            {
                 v2f OUT;
                    OUT.pos = mul(UNITY_MATRIX_MVP, float4(buffer[id], 1));
                    return OUT;
            }

            float4 frag(v2f IN) : COLOR
            {
                return float4(col,1);
            }

            ENDCG
        }
    }
}


이제 버퍼를 생성할 스크립트를 만들어 봅시다. C# 스크립트를 만들고, 아래 코드를 붙인 뒤 새로운 씬의 카메라에 스크립트를 붙이세요.

using UnityEngine;
using System.Collections;

public class AppendBufferExample : 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();
    }
}


아래 코드를 보세요

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

이 부분이 append 버퍼의 생성 부분입니다. 당연히 “ComputeBufferType.Append” 타입을 선언하고 있습니다. 여전히 버퍼의 크기(width*width 파라메터)를 전달해 주는 것에 유의하세요. Append 버퍼는 셰이더에서 그 요소를 더해주어야 하지만 그래도 미리 설정된 크기는 필요합니다. 이것은 요소가 더해질 수 있도록 메모리 영역을 예약하는 것으로 생각하시면 됩니다. 또한 이에 의해 에러가 발생할 수 있는데 뒤쪽에서 다루도록 하겠습니다.
Append 버퍼는 빈(empty) 상태로 시작하고 셰이더에서 (역주: 값을) 더해주어야 합니다. 아래 부분을 보세요.

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

이 부분이 우리의 버퍼를 채울 compute shader를 실행하는 부분입니다. 새 compute shader를 만들고 아래 코드를 붙이세요.

#pragma kernel CSMain

AppendStructuredBuffer<float3> appendBuffer;

float size;
float width;

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

    //Normalize pos
    float3 pos = id / (width-1);

    //make pos range from -size to +size
    pos = (pos - 0.5) * 2.0 * size;
    //keep z pos at 0
    pos.z = 0.0;

    if(id.x % 2 == 0 && id.y % 2 == 0)
        appendBuffer.Append(  pos );
}

이에 의해 실행되는 각 쓰레드의 위치에 따라 버퍼가 채워지게 됩니다. 특별할 것 없죠. 아래 부분을 보세요.

if(id.x % 2 == 0 && id.y % 2 == 0)
    appendBuffer.Append(  pos );

“Append(pos)”가 실제로 버퍼에 위치(position)를 추가하는 부분입니다. 여기서는 dispatch 아이디의 x와 y가 짝수일 때만 위치를 더하고 있습니다. 이것이 Append 버퍼가 유용한 이유입니다. 셰이더에서 여러분이 원하는 조건일 때 값들을 추가할 수 있다는 것입니다.
하지만 렌더링 단계에서 이러한 append 버퍼의 동적인 내용물이 문제를 발생시킬 수도 있습니다. 지난 번 튜토리얼에서 아래 함수를 이용해 structured 버퍼의 내용을 렌더링 한 것을 기억하실 겁니다.

Graphics.DrawProcedural(MeshTopology.Points, count, 1);

Unity의 “DrawPeocedural” 함수는 화면에 그려질 요소의 개수를 알아야만 합니다. Append 버퍼의 내용물이 동적이라면 얼마나 많은 요소가 그 안에 있는지는 어떻게 알 수 있을까요?
이를 위해 우리는 argument 버퍼를 사용해야 합니다. 스크립트의 아래 코드를 보세요.

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

이것은 “ComputeBufferType.DrawIndirect“ 타입이라는 것에 유의하시고, 4개의 integer로 된 요소를 갖고 있습니다. 이 버퍼는 Unity에게 “DrawProceduralIndirect“ 함수를 통해 append 버퍼의 내용을 어떻게 화면에 그릴 지 알려줄 때 사용됩니다.
4개의 요소는 정점의 개수, 인스턴스의 개수, 시작 정점과 시작 인스턴스를 표현합니다. 인스턴스의 개수는 단일 셰이더 패스(pass)에서 몇번이나 버퍼를 그릴지에 대한 것입니다. 이는 숲을 표현할 때와 같이 몇 개의 나무들을 여러 번 그려야 할 때 유용합니다. 시작 정점과 시작 인스턴스는 어디서부터 그리기 시작할 지 조정할 수 있게 해줍니다.
이 값들은 스크립트에서 정해집니다. 아래 코드를 보세요.

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

여기서는 Unity에 우리 버퍼 인스턴스 하나를 처음부터 그리라고 알려주는 겁니다. 하지만 중요한 것을 바로 첫 번째 숫자입니다. 이는 정점의 개수이고 0으로 정해놓은 것을 보실 수 있을겁니다. 이는 append 버퍼에 있는 요소의 실제 개수를 받아와야 하기 때문입니다. 이러한 과정은 아래 코드에서 수행하고 있습니다.

ComputeBuffer.CopyCount(buffer, argBuffer, 0);

이 코드가 append 버퍼의 요소 개수를 argument 버퍼에 복사합니다. 이 단계에서 모든 것이 제대로 동작하는지 확실히 하셔야 하고, argument 버퍼의 값들을 가져와 그 내용을 아래와 같이 출력할 수 있습니다.

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]);

셰이더와 material을 스크립트에 제대로 할당했는지 확인하시고 씬을 실행해 보세요. 정점의 개수가 256개 인 것을 볼 수 있을 겁니다. 이는 우리가 compute shader에서 1024개의 쓰레드를 실행했고, x와 y 아이디가 짝수일 때만 위치를 더했으므로, 256개의 점이 화면에서 보이는 것입니다.

제가 버퍼 크기와 관련해서 에러가 발생할 수 있다고 했던 것을 기억하시나요? 버퍼는 1024의 크기로 처음 선언되었고, 우리는 256개의 요소를 입력했습니다. 만약 1024개 이상의 요소를 넣으면 어떻게 될까요? 좋지 않습니다. 버퍼 크기 이상의 요소를 append하면 GPU 드라이버에서 에러가 납니다. 이게 모든 종류의 문제의 원인입니다. 좋아 봤자 GPU가 충돌(crash)하는 것이고, 최악의 경우에는 버퍼 내 요소 개수 계산에 영구적인 에러가 발생해서 Unity를 재시작하는 수 밖에 없습니다. 이는 특별한 이유도 없이 제대로 된 화면 출력이 되지 않는다는 것을 의미합니다. 더 안좋은 것은 이것이 드라이버상의 문제기 때문에 다른 컴퓨터에서는 다른 형태로 문제가 나타나고 이러한 상황에서의 문제 해결은 언제나 쉽지 않다는 것입니다.

그 이유로 버퍼의 요소 개수를 출력하게 한 것입니다. 항상 그 값이 예상되는 범위 내에 있는 것을 확인해야 합니다.

정점의 개수를 argument 버퍼에 복사했다면 아래와 같이 버퍼를 화면에 출력할 수 있습니다.

Graphics.DrawProceduralIndirect(MeshTopology.Points, argBuffer, 0)

Compute 버퍼는 compute shader에서만 채울 수 있도록 제약되어 있지는 않습니다. Cg 셰이더에서도 값을 채울 수 있습니다. 만약 이미지에서 후처리 효과(post effect)를 위해 어떤 픽셀값들을 저장해야 하는 경우 이를 유용하게 사용할 수 있습니다. 알아두어야 할 점은 이러한 경우 평범한 그래픽스 파이프라인을 사용하는 것이라 그 규칙에 따라야 한다는 것입니다. 필요가 없다 하더라고 픽셀 값을 렌더 텍스처에 출력해야 합니다. As such if you find yourself in that situation it means that whatever your doing is probably better done from a compute shader. Other than that the process works much the same but like everything there are a few things to be careful of.(역주: 확실히 이해를 못하겠어서 그냥 남겨둡니다….ㅜㅜ compute shader가 더 쉬운 경우는 그걸 쓰고, 아니면 대부분 경우 일반 Cg 셰이더가 구현해야 하는 부분이 더 적다라는 뜻인 것으로 생각됩니다.)
새로운 셰이더를 만들고 아래 코드를 붙여 넣으세요.

Shader "Custom/AppendBufferCg"
{
    SubShader
    {
        Pass
        {
            ZTest Always Cull Off ZWrite Off
            Fog { Mode off }

            CGPROGRAM
            #include "UnityCG.cginc"
            #pragma target 5.0
            #pragma vertex vert
            #pragma fragment frag

            uniform AppendStructuredBuffer<float3> appendBuffer;
            uniform float size;
            uniform float width;

            struct v2f
            {
                float4  pos : SV_POSITION;
                float2  uv : TEXCOORD0;
             };

             v2f vert(appdata_base v)
             {
                 v2f OUT;
                 OUT.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                 OUT.uv = v.texcoord.xy;
                 return OUT;
             }

             float4 frag(v2f IN) : COLOR
             {
                 float3 pos = float3(IN.uv.xy,0);

                 //make pos range from -size to +size
                 pos = (pos - 0.5) * 2.0 * size;
                 //keep z pos at 0
                 pos.z = 0.0;

                 int2 id = IN.uv.xy * width;

                 if(id.x % 2 == 0 && id.y % 2 == 0)
                     appendBuffer.Append(  pos );

                 return float4(1,0,0,1);
             }
             ENDCG

        }
    }
}


픽셀 셰이더가 위에 사용했던 compute shader와 거의 유사한 것을 보십시오. X, y축의 Uv(아이디와 같이)가 짝수일 경우 그 픽셀의 위치를 버퍼에 더하고 있습니다. 에디터에서 이 셰이더로 meterial을 만들지는 마세요! 이유는 Unity가 프리뷰 이미지를 렌더링하려 시도하면서 바로 충돌이 일어날 수 있기 때문입니다. 셰이더는 스크립트로 전달해서 거기에서 meterial을 만들어야 합니다.

새로운 C# 스크립트를 만들고 아래 코드를 붙여넣으세요.

using UnityEngine;
using System.Collections;

public class AppendBufferExampleFromCg : MonoBehaviour
{
    public Material material;
    public Shader appendBufferShader;

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

    ComputeBuffer buffer;
    ComputeBuffer argBuffer;

    void Start()
    {

        Material mat = new Material(appendBufferShader);
        mat.hideFlags = HideFlags.HideAndDontSave;

        RenderTexture tex = new RenderTexture(width, width, 0);

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

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

        Graphics.SetRandomWriteTarget(1, buffer);
        Graphics.Blit (null, tex, mat, 0);
        Graphics.ClearRandomWriteTargets ();

        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.blue);
        Graphics.DrawProceduralIndirect(MeshTopology.Points, argBuffer, 0);
    }

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


Compute shader를 사용할 때와 거의 비슷한 것을 알 수 있을겁니다. 그래도 몇 가지 차이점이 있는데요.

Graphics.SetRandomWriteTarget(1, buffer);
Graphics.Blit (null, tex, mat, 0);
Graphics.ClearRandomWriteTargets ();

Compute shader의 dispatch 호출 대신 Graphics blit을 사용하고, 버퍼를 bind 및 unbind하야 합니다. 또한 Graphics blit을 위해 렌더 텍스처를 제공해 주어야 합니다. 하지만 이는 Pro 버전에서만 가능합니다.

이 스크립트를 새로운 씬의 메인 카메라에 붙이고 material과 셰이더를 할당한 뒤 실행해보세요. 이전 씬(점들의 그리드)과 같은 결과이지만 파란색으로 나타날 것입니다.

여기까지가 append 버퍼입니다. 셰이더를 통해 요소를 어떻게 버퍼에 추가하는지 이제 아셨을 꺼고, 그렇다면 제거는 어떻게 할까요? 이는 consume 버퍼가 필요하고 다음 튜토리얼에서 다룰 것입니다.

Project files

No comments:

Post a Comment