Monday, December 7, 2015

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

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

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

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

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

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

 DirectCompute에는 여러분이 사용할 두 종류의 데이터 구조가 있습니다. 텍스쳐와 버퍼입니다. 지난 튜토리얼에서는 텍스처에 대해 다루었고, 오늘은 버퍼에 대해 다룰 것입니다. 텍스처는 필터링이나 밉맵핑과 같은 색상 데이터를 다룰 때 좋고, 버퍼는 정점 위치나 법선과 같은 정보의 표현에 더 적합합니다. 버퍼는 GPU로 데이터를 보내거나, GPU에서 데이터를 받는 것을 가능하게 하고, 이는 Unity에서는 약간 부족한 기능입니다.(역주: DirectCompute를 사용하지 않을 경우)
 Structured, append, consume, counter, raw의 5종류 버퍼가 존재합니다. 오늘은 structured buffer만을 다룰 예정인데, 이 버퍼가 가장 흔히 사용되는 버퍼입니다. 나머지 종류는 다른 개별적인 튜토리얼에서 다룰 것인데요, 이러한 버퍼들은 좀 더 고급 기능을 가지고 있고, 그래서 기본부터 시작할 필요가 있습니다.
 C# 스크립트를 만들어서 시작해보죠. BufferExample이라는 스크립트를 만들고 아래 코드를 붙여 넣어보세요.

using UnityEngine;
using System.Collections;

public class BufferExample : MonoBehaviour
{
    public Material material;
    ComputeBuffer buffer;

    const int count = 1024;
    const float size = 5.0f;

    void Start ()
    {

        buffer = new ComputeBuffer(count, sizeof(float)*3, ComputeBufferType.Default);

        float[] points = new float[count*3];

        Random.seed = 0;
        for(int i = 0; i < count; i++)
        {
            points[i*3+0] = Random.Range(-size,size);
            points[i*3+1] = Random.Range(-size,size);
            points[i*3+2] = 0.0f;
        }

        buffer.SetData(points);
    }

    void OnPostRender()
    {
        material.SetPass(0);
        material.SetBuffer("buffer", buffer);
        Graphics.DrawProcedural(MeshTopology.Points, count, 1);
    }

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


 버퍼의 내용을 그릴 material이 필요하니 일반 Cg 셰이더를 만들고, 아래 코드를 붙여넣으신 뒤 이 셰이더를 사용해 meterial을 만드세요.
Shader "Custom/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;

            struct v2f
            {
                float4  pos : SV_POSITION;
            };

            v2f vert(uint id : SV_VertexID)
            {
                float4 pos = float4(buffer[id], 1);

                v2f OUT;
                OUT.pos = mul(UNITY_MATRIX_MVP, pos);
                return OUT;
            }

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

            ENDCG
        }
    }
}

 이제 씬을 실행하기 위해 메인 카메라에 스크립트를 붙이고, material 속성에 meterial을 할당하세요. 버퍼 데이터를 렌더링하기 위해 “OnPostRender”함수를 사용해야 하고, 이 함수는 카메라에 스크립트가 붙어있을 때만 호출되기 때문에 카메라에 스크립트를 붙였습니다(수정 – 스크립트를 카메라에 붙이고 싶지 않으면 “OnRenderObject”함수를 사용하면 됩니다). 씬을 실행하면 몇 개의 빨간색 점들이 무작위로 나타나는 것을 볼 수 있을 겁니다.
 아래 버퍼 생성 코드를 보세요.

buffer = new ComputeBuffer(count, sizeof(float)*3, ComputeBufferType.Default);

 생성자에 전달된 파라메터들은 count, stride와 type입니다. Count는 버퍼의 요소(element)개수입니다. 이 경우 1024개의 점이 되겠죠. Stride는 버퍼 내 각 요소의 크기를 바이트로 나타낸 것입니다. 버퍼 내 각 요소는 3차원 위치이고, 따라서 float 3개로 구성된 벡터가 필요합니다. Float은 4바이트이고 따라서 4*3크기의 stride가 필요합니다. 저는 개인적으로 명료한 코드를 위해서 sizeof 연산자를 사용하는 것을 선호합니다. 마지막 파라메터는 버퍼의 종류로, 선택적으로 사용할 수 있습니다. 아무 것도 입력하지 않았다면 기본 타입은 structured buffer입니다.
 이제 만들어둔 위치들을 넘겨주기 위해 아래와 같이 버퍼를 만들었습니다.

buffer.SetData(points);

 이 부분이 GPU로 데이터를 넘기는 부분입니다. GPU로 데이터를 넘기거나, 받는 과정은 느리고, 일반적으로 데이터를 보내는 것이 받는 것보다는 빠르다는 것을 유념하세요.(역주: 생각보다 많이 느리더군요…위 예제에서 느낄 수 있는 정도는 아닙니다)
 이제 실제로 데이터를 그려야 합니다. 이 부분은 “OnPostRender”함수에서 실행되어야 하고, 이는 스크립트가 카메라에 붙어있을 때만 호출됩니다. Draw call은 “DrawProcedural”함수를 사용해 아래와 같이 실행할 수 있습니다.
material.SetPass(0);
material.SetBuffer("buffer", buffer);
Graphics.DrawProcedural(MeshTopology.Points, count, 1);


여기에 몇 가지 중요한 부분이 있습니다. 첫 번째는 material pass가 반드시 DrawProcedural 호출 전에 설정되어야 한다는 것입니다. 또한 buffer를 material에 할당해주어야 하고 이는 한번만 하면 됩니다. 제가 하는 것 처럼 매 프레임마다 할 필요는 없습니다. 이제 “DrawProcedural” 함수를 봅시다. 첫 번째 파라메터는 topology type이고 이 경우 그냥 점을 렌더링하는 것이지만 line, line strip, quad나 triangle도 렌더링 할 수도 있겠죠. 그럴 때는 topology에 맞게 데이터를 정렬해 주어야 합니다. 예를 들어, line을 렌더링 할 때에는 버퍼 내 두 점들마다 하나의 line segment가 생성되고, triangle의 경우 세 점들마나 하나의 triangle이 생성 될 것입니다. 다음 두 파라메터는 정점의 개수와 인스턴스 개수입니다. 정점 개수는 그리고자 하는 정점의 개수이고, 이 경우 버퍼에 있는 요소의 개수가 될 것입니다. 인스턴스 개수는 같은 데이터를 몇 번이나 그릴 것인지에 관한 것입니다. 이 경우 점들을 한 번만 렌더링하지만 각 인스턴스를 다른 위치에서 여러 번 렌더링 할 수도 있습니다.
 이제 material 부분입니다. 아래와 같이 버퍼를 uniform처럼 선언하면 됩니다.

uniform StructuredBuffer<float3> buffer;

 버퍼는 DirectCompute에만 존재하므로 셰이더를 SM5로 아래와 같이 설정해 주어야 합니다.

#pragma target 5.0

 정점 셰이더는 “uint id : SV_VertexID“인수를 가져야만 합니다. 그래야만 아래처럼 버퍼 내의 올바른 요소에 접근이 가능합니다.

float4 pos = float4(buffer[id], 1);

 버퍼는 일반적인 데이터 구조로, 이 예제와 같이 float일 필요는 없습니다. 대신 integer를 사용할 수도 있습니다. 아래와 같이 스크립트의 Start함수를 바꾸세요.
buffer = new ComputeBuffer(count, sizeof(int)*3, ComputeBufferType.Default);

int[] points = new int[count*3];

Random.seed = 0;
for(int i = 0; i < count; i++)
{
    points[i*3+0] = (int)Random.Range(-size,size);
    points[i*3+1] = (int)Random.Range(-size,size);
    points[i*3+2] = 0;
}

buffer.SetData(points);


그리고 셰이더의 uniform선언을 아래로 바꾸세요.

uniform StructuredBuffer<int3> buffer;

 Double도 사용할 수 있지만, 셰이더에서의 double precision 사용은 최근 그래픽 카드에서는 자주 지원되지만 아직 그렇게 범용적으로 지원하지는 않는다는 것을 유념하십시오.
float이나 int같은 기본 데이터형만 사용할 수 있는 것도 아닙니다. Unity의 Vector도 아래와 같이 사용할 수 있습니다.
buffer = new ComputeBuffer (count, sizeof(float) * 3, ComputeBufferType.Default);

Vector3[] points = new Vector3[count];

Random.seed = 0;
for (int i = 0; i < count; i++)
{
    points[i] = new Vector3();
    points[i].x = Random.Range (-size, size);
    points[i].y = Random.Range (-size, size);
    points[i].z = 0;
}

buffer.SetData (points);


Uniform은 아래와 같습니다.
uniform StructuredBuffer<float3> buffer;

또한 구조체(struct)를 만들어 사용할 수도 있습니다. Start함수와 그 앞의 구조체 선언부를 아래와 같이 수정하세요.
struct Vert
{
    public Vector3 position;
    public Vector3 color;
}

void Start ()
{

    buffer = new ComputeBuffer (count, sizeof(float) * 6, ComputeBufferType.Default);

    Vert[] points = new Vert[count];

    Random.seed = 0;
    for (int i = 0; i < count; i++)
    {
        points[i] = new Vert();

        points[i].position = new Vector3();
        points[i].position.x = Random.Range (-size, size);
        points[i].position.y = Random.Range (-size, size);
        points[i].position.z = 0;

        points[i].color = new Vector3();
        points[i].color.x = Random.value > 0.5f ? 0.0f : 1.0f;
        points[i].color.y = Random.value > 0.5f ? 0.0f : 1.0f;
        points[i].color.z = Random.value > 0.5f ? 0.0f : 1.0f;
    }

    buffer.SetData (points);

}


그리고 셰이더는 아래와 같이 바꿔보세요.
Shader "Custom/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

            struct Vert
            {
                float3 position;
                float3 color;
            };

            uniform StructuredBuffer<Vert> buffer;

            struct v2f
            {
                float4  pos : SV_POSITION;
                float3 col : COLOR;
            };

            v2f vert(uint id : SV_VertexID)
            {
                Vert vert = buffer[id];

                v2f OUT;
                OUT.pos = mul(UNITY_MATRIX_MVP, float4(vert.position, 1));
                OUT.col = vert.color;
                return OUT;
            }

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

            ENDCG

        }
    }
}


전과 같이 점들을 그리지만 이제는 랜덤한 색상으로 나타날 것입니다. 클래스는 사용할 수 없고, 구조체를 사용해야 한 다는 것을 알고 계셔야 합니다.
 버퍼를 사용할 경우 종종 복사를 해야 할 경우가 있습니다. Compute shader를 통해 아래와 같이 간단히 할 수 있습니다.

#pragma kernel CSMain

StructuredBuffer<float> buffer1;
RWStructuredBuffer<float> buffer2;

[numthreads(8,1,1)]
void CSMain (uint id : SV_DispatchThreadID)
{
    buffer2[id] = buffer1[id];
}


 Buffer1이 buffer2로 복사되는 것을 볼 수 있습니다. 버퍼는 항상 1차원이기 때문에 1차원의 쓰레드 그룹에서 실행하는 것이 좋습니다.
 텍스처가 객체였던 것처럼 버퍼도 그로부터 호출할 수 있는 함수들이 있습니다. 버퍼 내 요소에 인덱스(역주: 원문은 subscript operator입니다만, 이해하기 쉽도록 약간 의역했습니다)를 사용하는 것과 비슷하게 접근하기 위해 load함수를 사용할 수 있습니다.

buffer2[id] = buffer1.Load(id);

버퍼는 “GetDimension” 함수도 있습니다. 이는 버퍼 내 요소의 개수와 stride를 리턴해줍니다.

uint count, stride;
buffer2.GetDimensions(count, stride);

버퍼에 대한 부분은 여기까지입니다. 다음 튜토리얼에서는 고급 버퍼 종류에 대해 알아볼 것입니다. Append와 consume 버퍼에 대한 내용과 “DrawProceduralIndirect”함수를 어떻게 사용하는지, 그리고 이 함수에 전달되어야 하는 argument buffer를 어떻게 사용하는 지에 대해 다룰 것입니다. 오늘 여기까지 하면 DirectCompute의 기본 기능에 대해서는 마쳤다고 볼 수 있습니다. 여기서부터는 대부분의 내용이 고급 기능이라고 생각하시면 되겠습니다.

Project Files

5 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. 안녕하세요. 포스팅 잘 보고 있습니다.

    본 예제처럼 C#에서 vertex값을 지정해주는 것이 아닌 shader에서 만들어준 vertex들의 위치값을 받아오려고 합니다.(StandardAsset에 있는 Water4의 vertex위치를 통해 부력 구현을 하기 위해서 입니다.)

    위와 동일한 방식으로 start에서 water4 메터리얼(셰이더를 포함한)로 c#에서 생성된 buffer를 전달하고, 셰이더의 vertex함수에서 생성된 position을 버퍼에 넣어주며, OnRenderObject에서 getData를 통해 받아오는 식으로 코드를 제작했습니다. 물론 값을 입력받아야하니 셰이더쪽에서는 RWStructedBuffer를 사용했구요.

    그런데 'DestroyBuffer can only be called from the main thread.'를 제외한 에러는 나오지 않지만 버퍼에 (정확히는 버퍼를 통해 set한 Vector4 배열에) 아무 값도 입력되지 않네요 ㅠ 조언 부탁드립니다.

    ReplyDelete
    Replies
    1. 일단 간단히 위 예제에서 테스트를 해 보았는데요, buffershader.shader에서 RWStructuredBuffer로 바꾸고, OnPostRender에서 GetData를 호출하면 값이 정상적으로 잘 읽어집니다. FXWater4Simple 셰이더도 잠깐 살펴봤는데 테스트를 하려면 수정을 많이 해야 될 것 같아서 돌려보진 않았는데요, 혹시 Shader Model(#pragma target)을 5.0으로 설정 하셨는지요? 아마 Compute Shader는 5.0 버전부터 지원할껍니다.

      Delete
    2. 빠른 답변 감사합니다:)

      다행히 에러체크를 통해 Shader Model은 5.0으로 설정한 상태입니다. 위의 예제처럼 해당 vertex를 넣는 경우에는 buffer를 RW로 바꾸지 않고, GetData를 한경우에도 정상적으로 값을 받아옵니다.(랜덤으로 생성하여 넣어준 값) 다만 RW로 바꾸는 경우 기존의 vertex를 어떻게 조회하는지를 모르겠네요.
      (해당 예제에서 변경하기만 하면 RWBuffer를 통해 어떤 값이 들어오게 되나요?)

      셰이더를 처음으로 만지고 있는 단계라서, #pragma vertex vert로 선언한 경우 vert 구문이 모든 vertex를 순회하면서 진행되는 거라고 인지하고 있는데,

      v2f vert(appdata_full v, uint id : SV_VertexID)
      {
      v2f o; (<- v.vertex 에서 값을 받아서 pos값을 갖게 됩니다.)
      ...중략..
      buffer[id] = o.pos.xyz;
      }
      이런식으로 처리하면 제대로 값이 들어오질 않습니다.
      사실 vertex/fragment shader의 기본적인 개념정립이 되지 않아서 건드리가 어렵네요..;;

      Delete
    3. 그러네요 제가 테스트를 잘못했군요 ㅜㅜ 안돼는 것이 맞는 것 같습니다. 일단 RWStructuredBuffer선언 자체가 Cg Shader에서는 안돼고(에러가 나는 것은 아닌데 셰이더가 정상 작동하지 않음), 물론 값을 읽어오는 것이 안돼네요...

      아래 링크 글을 보니, Compute shader가 아닌 일반 cg shader에서 값을 읽어오려면 transform feedback 방법이 있는데 유니티에서는 구현이 안 되어 있는 것 같고요(최신 버전에서도 그런지는 모르겠네요), 아니면 RenderTexture로 값을 써서 읽어오는 방법이 있는 것 같습니다. 개인적으로는 vertex shader에서 하는 연산을 compute shader에서 대신 수행하도록 수정하는 것이 어떨까 합니다...
      (http://forum.unity3d.com/threads/gpgpu-with-shaders.184535/)

      Delete