Tuesday, December 22, 2015

3D Convex hull of a working excavator - 2. Refinement, Filtering idea


  • Since I found no better way to use GPU resource in Quickhull algorithm, implemented in Unity environment, I investigated other approach to achieve speedup.
  •  I ended up with application-specific method that filters out a bunch of input points that cannot be an extreme point in result convex hull.
  • So here is the comparison result of 3 approaches. The second one is explained in this post. The third one is new idea and I think I should clear up the assumption and theory behind it more on later post.
    1. Raw points : Raw vertices processing
    2. Extreme points : Using extreme vertices of convex hull of each components (swing, boom, arm and bucket)
    3. Filtered points : Using filtered vertices



  • In this post, brief performance evaluation and result analysis will be shown.


    • The first is comparison table. Difference is the number of points and as you can see the volume of result convex hull is same. Means same convex hull with different number of point input.
    • Points are shown below. You may see significant reduction of points from left to right.
    Raw points, extreme points and filtered points from left to right

    • The final implementation use MIConvexHull ported by again, Skrawk. Why I couldn't find it when I started this?...Anyway it is faster than Qhull in Meshlab. I think the Qhull module integrated in Meshlab is rather old one. Latest version of Qhull is much faster than Meshlab.
    • Also for the filtering method, I used GPU for speedup. It needs one time calculation for every vertices in each frame update without loop. So the performance improvement was significant. I'll explain it later in filtering approach.
    • Here are result clips show actual run-time calculation.

    • Raw points

    • Extreme points

    • Filtered points

    Thursday, December 10, 2015

    Unity DirectCompute performance test


    • I tested performance of DirectCompute feature in Unity where I planned to use it in 3D convex hull calculation.
    • In short, there seems to be a performance issue when reading data from GPU to CPU, in Unity's direct compute feature.
    • GPU is really faster when using it in well-aligned way with rendering pipeline. But I have to use it in different way in Convex hull. The point-plane test has to be done several times (300~500 in average for our excavator vertex set) on subset of points with newly generated planes in each loop repetition. The "Loop repetition" is a problem here. Since we need point-plane test result of previous loop in next loop calculation, we should send calculation result from GPU to CPU but it is extremely slow.
    • As I mentioned in previous post, I predicted the problem but the amount of slowdown is far more than I expected. I also wrote the related question in Unity answers forum but no "answers" yet.
    • The paper I read(CudaHull) also took similar approach with my plan. But there seems less slowdown in GPU to CPU data transfer. So I suspect that the problem is Unity-specific, or DirectCompute-specific one...

    • Here is the first test result on 100,000 vertices with tetrahedron. Tetrahedron is changing in each frame and all the vertices are tested if it can be seen in each face of tetrahedron. As you can see, GPU is lot faster than CPU when without a loop.


    • Test on 100,000 vertices.

    • Following two clips are 100,000 vertices with 100 and 300 loop. In each loop, subset of points are tested and results are sent to GPU with GetData() function. You can see how long it takes to send data from GPU to CPU.


    • I really hope to know how to solve this issue...

    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.

    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

    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

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

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


    Wednesday, December 2, 2015

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


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

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

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

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

    ---------------------------------------------------------------------------------------------------

    Unity를 위한 DirectCompute  튜토리얼 : 커널과 쓰레드 그룹

    - From ScrawkBlog (http://scrawkblog.com/)

    지난번 포스트는 약간의 개요였지만 여기서부터는 코드와 관련된 내용입니다. 오늘은 Unity에서 compute shader의 작성을 위한 핵심 개념을 다룰 것입니다. Compute shader의 핵심은 커널입니다. 이것은 셰이더로의 진입점이며, 다른 프로그래밍 언어에서의 메인 함수처럼 동작합니다. 또한 GPU의 쓰레드 tiling에 대해서도 다룰 것입니다. 타일들은 블록 또는 쓰레드 그룹이라고도 합니다.
    Unity에서 compute shader를 생성하기 위해서는 프로젝트 패널로 가서 create->compute shader를 클릭하기만 하면 되고, Monodevelop에서 수정하기 위해서는 더블 클릭해서 열면 됩니다. 아래 코드를 새로 생성된 compute shader에 붙여넣으세요.

    #pragma kernel CSMain1
    
    [numthreads(4,1,1)]
    
    void CSMain1()
    {
    
    }
    


    이는 compute shader의 최소한의 코드이고, 물론 아무 것도 수행하지 않지만 시작을 위한 코드로는 좋습니다. Compute shader는 Unity 스크립트에서 실행되어야 하고 따라서 스크립트도 필요합니다. 프로젝트 패널로 가서 Create->C# script를 클릭하세요. KernelExample로 이름을 설정하고 아래 코드를 붙여넣으세요.

    using UnityEngine;
    using System.Collections;
    
    public class KernelExample : MonoBehaviour
    {
    
        public ComputeShader shader;
    
        void Start ()
        {
            shader.Dispatch(0, 1, 1, 1);
        }
    
    }
    
    

    이제 스크립트를 아무 게임 오브젝트에 붙이시고 compute shader를 shader 속성에 붙여넣으세요. 이제 씬이 실행될 때, Start 함수에서 compute shader가 실행될 것입니다. 씬을 실행하기 전에 dx11을 활성화하기위해 Edit->Project Settings->Player로 가서 “Use Direct3D 11”의 체크박스를 설정하세요.(역주 : Unity 4.6 및 Unity 5.1버전에서 저 항목이 없습니다. 디폴트 설정으로 놔둬도 잘 실행되었습니다.) 이제 씬을 실행해 보십시오. 셰이더는 아무것도 하지 않겠지만 에러도 없어야 합니다.

    스크립트에서, “Dispatch” 함수가 호출되는 것을 보실 수 있을겁니다. 이 함수가 셰이더를 동작하게 하는 함수입니다. 첫 번째 변수가 0인 것을 주목하세요. 이것은 실행을 원하는 커널의 아이디입니다. 셰이더에서는 “#pragma kernel CSMain1”을 보실 수 있을겁니다. 이는 셰이더 내의 여러 함수(또는 여러 커널들도 있을 수 있습니다)들 중 어떤 함수가 커널인지 정의하는 것입니다. 셰이더 내에 CSMain1이라는 이름의 함수가 있어야만 하고 그렇지 않으면 셰이더는 컴파일되지 않을것입니다.

    이제 “[numthreads(4,1,1)]” 줄을 주목하세요. 이는 GPU에게 그룹당 몇 개의 쓰레드를 커널에 대해 실행할 것인지 알려주는 부분입니다. 세 개의 숫자는 각 차원에 대한 숫자입니다. 쓰레드 그룹은 3차원으로 구성될 수 있고, 이 예제에서는 4개 쓰레드의 너비를 가진 1차원의 그룹을 실행합니다. 그 말은, 총 4개의 쓰레드가 실행되며 각 쓰레드는 커널의 복사본을 실행할 것입니다. 이것이 GPU가 빠른 이유입니다. GPU는 동시에 몇 천 개의 쓰레드를 동시에 실행 가능합니다.

    이제 커널이 실제로 뭔가를 하게 해봅시다. 셰이더를 이렇게 바꿔보세요…

    #pragma kernel CSMain1
    
    RWStructuredBuffer<int> buffer1;
    
    [numthreads(4,1,1)]
    
    void CSMain1(int3 threadID : SV_GroupThreadID)
    {
        buffer1[threadID.x] = threadID.x;
    }
    
    

    그리고 스크립트의 Start 함수를 아래처럼 바꿔보세요…

    void Start ()
    {
        ComputeBuffer buffer = new ComputeBuffer(4, sizeof(int));
    
        shader.SetBuffer(0, "buffer1", buffer);
    
        shader.Dispatch(0, 1, 1, 1);
    
        int[] data = new int[4];
    
        buffer.GetData(data);
    
        for(int i = 0; i < 4; i++)
            Debug.Log(data[i]);
    
        buffer.Release();
    }
    
    

    이제 씬을 실행하면 숫자 0,1,2,3이 출력되는 것을 보실 수 있을겁니다. 버퍼(buffer)에 대해서는 아직은 너무 걱정하지 마세요. 버퍼에 대해서는 제가 나중에 자세히 다룰 것이고 그냥 버퍼는 데이터를 저장하는 장소이고, 처리가 끝나면 릴리즈(release) 함수를 호출해줘야 한다는 것만 알아두세요. CSMain1 함수의 인수(argument)로 추가된 “int3 threadID : SV_GroupThreadID”를 주목하세요. 이것은 커널이 실행될 때 GPU에 해당 커널의 쓰레드 아이디를 전달하도록 요청하는 것입니다. 그리고 우리는 버퍼에 쓰레드 아이디를 쓰는데, 앞서 말했듯이 GPU는 4개의 쓰레드를 실행하도록 했기 때문에 출력된 것처럼 0에서 3까지의 아이디를 갖는 쓰레드가 실행되는 것입니다.

    저 4개의 쓰레드가 쓰레드 그룹을 구성합니다. 이 예제에서는 4개의 쓰레드를 갖는 1개의 그룹을 실행하지만, 여러 그룹을 실행할 수도 있습니다. 이제 1개 말고 2개의 그룹을 실행 해봅시다. 셰이더 커널을 아래와 같이 바꾸세요.

    void CSMain1(int3 threadID : SV_GroupThreadID, int3 groupID : SV_GroupID)
    {
        buffer1[threadID.x + groupID.x*4] = threadID.x;
    }
    
    

    그리고 스크립트의 Start 함수를 아래와 같이 바꾸세요.

    void Start ()
    {
        ComputeBuffer buffer = new ComputeBuffer(4 * 2, sizeof(int));
    
        shader.SetBuffer(0, "buffer1", buffer);
    
        shader.Dispatch(0, 2, 1, 1);
    
        int[] data = new int[4 * 2];
    
        buffer.GetData(data);
    
        for(int i = 0; i < 4 * 2; i++)
            Debug.Log(data[i]);
    
        buffer.Release();
    }
    
    

    이제 씬을 실행하면 0-3이 두 번 출력되어야 합니다. dispatch함수의 변화에 주목하세요. 뒤쪽 세 개의 인수(2,1,1)는 실행하고 싶은 그룹의 숫자이고 쓰레드처럼 그룹도 3차원까지 설정이 가능하며, 이 예제에서는 1차원의 2개 그룹을 실행하고 있습니다. 또한 커널의 인수를 “int3 groupID : SV_GroupID”를 추가하여 수정하였습니다. 이것은 커널이 실행될 때 GPU에 그룹 아이디를 전달하도록 요청하는 것입니다. 이것이 필요한 이유는 우리가 각 4개 쓰레드를 갖는 2개 그룹에서 8개 값을 출력하려 하기 때문입니다. 버퍼에서 쓰레드의 위치가 필요하고 이에 대한 수식은 쓰레드 아이디에 그룹 아이디 곱하기 쓰레드 숫자입니다(threadID.x + groupID.x*4). 이렇게 쓰는 것은 좀 어색합니다. 당연히 GPU가 쓰레드 위치를 알아야 하지 않을까요? 맞습니다. 셰이더 커널을 아래와 같이 바꾸고 다시 씬을 실행해보세요.

    void CSMain1(int3 threadID : SV_GroupThreadID, int3 dispatchID : SV_DispatchThreadID)
    {
        buffer1[dispatchID.x] = threadID.x;
    }
    
    

    똑같이 0-3이 두 번 출력됩니다. 그룹 아이디 인수가 “int3 dispatchID : SV_DispatchThreadID”로 바뀐 것에 주목하세요. 이것은 위 수식과 같은 숫자를 넘겨주지만 GPU가 그 계산을 대신 해 주는 것입니다. 이것이 쓰레드 그룹들이 있을 때 쓰레드의 위치입니다.

    지금까지는 1차원만 다루었습니다. 이제 다음 단계인 2차원으로 가 봅시다. 이번엔 커널을 다시 작성하는 대신 다른 커널을 셰이더에 추가해봅시다. 같은 알고리즘을 수행하기 위해 각 차원에 따라 서로 다른 커널을 사용하는 것이 그리 특이한 것은 아닙니다. 먼저 아래 코드를 이전 코드 아래에 추가하면 이제 셰이더에는 두 개의 커널이 있게 됩니다.

    #pragma kernel CSMain2
    
    RWStructuredBuffer<int> buffer2;
    
    [numthreads(4,4,1)]
    
    void CSMain2( int3 dispatchID : SV_DispatchThreadID)
    {
        int id = dispatchID.x + dispatchID.y * 8;
    
        buffer2[id] = id;
    }
    
    

    그리고 스크립트는 아래와 같습니다.

    void Start ()
    {
    
        ComputeBuffer buffer = new ComputeBuffer (4 * 4 * 2 * 2, sizeof(int));
    
        int kernel = shader.FindKernel ("CSMain2");
    
        shader.SetBuffer (kernel, "buffer2", buffer);
    
        shader.Dispatch (kernel, 2, 2, 1);
    
        int[] data = new int[4 * 4 * 2 * 2];
    
        buffer.GetData (data);
    
        for(int i = 0; i < 8; i++)
        {
            string line = "";
            for(int j = 0; j < 8; j++)
            {
                line += " " + data[j+i*8];
            }
            Debug.Log (line);
        }
    
        buffer.Release ();
    }
    
    

    씬을 실행하면 0에서 7까지 출력되는 행과 8에서 15까지 출력되는 행, 계속해서 같은 방식으로 63까지 출력되는 것을 볼 수 있습니다. 왜 0에서 63까지일까요? 이제 4개의 2차원 쓰레드 그룹이 있고, 각 그룹은 4x4이니, 16개의 쓰레드 입니다. 결국 총 64개의 쓰레드가 되겠죠. 출력되는 값을 정의하는 “int id=dispatchID.x + dispatchID.y*8” 부분을 주목하세요. Dispatch 아이디는 그룹에서의 쓰레드의 위치입니다. 이재 2차원이니 버퍼에서는 쓰레드의 전역 위치가 필요하고, 이는 dispatch의 x아이디 더아기 dispatch의 y아이디 곱하기 1차원에서의 총 쓰레드 개수(4*2)입니다. Compute shader를 사용하기 위해서는 이 개념에 익숙해져야 합니다. 그 이유는 버퍼가 항상 1차원이기 때문이고, 높은 차원(역주 : 쓰레드과 그룹)에서 작업하기 위해서는 결과값이 버퍼의 어느 부분에 할당될지 인덱스를 계산해야 하기 때문입니다.

    같은 이론이 3차원에도 적용되지만, 복잡해지기 때문에 2차원까지의 예제만 보여드리겠습니다. 3차원에서 버퍼의 위치는 “int id = dispatchID.x + dispatchID.y * groupSizeX + dispatchID.z * groupSizeX * groupSizeY” 와 같이 계산된다는 것만 알고 계시면 됩니다. 그룹 사이즈는 그룹 개수 곱하기 해당 차원에서 쓰레드의 개수입니다.

    또한 시멘틱이 어떻게 동작하는지 알아야 합니다. 이 커널 인수의 예제를 보세요.

    int3 dispatchID : SV_DispatchThreadID

    SV_DispatchThreadID는 이 인수에 대해 GPU가 어떤 값을 전달해야 할지 알려줍니다. 인수의 이름은 상관 없습니다. 원하는 대로 붙이시면 됩니다. 예를 들어 아래 인수는 위와 동일하게 동작합니다.

    int3 id : SV_DispatchThreadID

    또한 변수 형(type)도 바뀔 수 있습니다. 예를 들어

    int dispatchID : SV_DispatchThreadID

    Int3가 int로 바뀐 것을 보십시오. 1차원에서 작업할 때는 이것도 상관 없습니다. 마찬가지로 2차원에서는 int2를 사용할 수 있고 원한다면 unsigned int(uint)도 사용 가능합니다.

    이제 셰이더에 두 개의 커널이 있으니 dispatch를 호출할 때, GPU에 어떤 커널을 실행하고 싶은지도 알려줘야 합니다. 각 커널은 작성된 순서대로 아이디가 부여됩니다. 우리의 첫 번째 커널은 아이디 0이 되겠고, 다음 것은 1이 됩니다. 셰이더에 커널이 많아지면, 헷갈리기 쉽고, 잘못된 아이디를 설정하기 쉽습니다. 이것은 커널 이름으로 아이디를 찾도록 셰이더에 요청함으로써 해결할 수 있습니다. “int kernel = shader.FindKernel (“CSMain2”);” 줄은 “CSMain2” 커널의 아이디를 얻어옵니다. 이후 아이디를 버퍼를 셋팅하고 dispatch 호출을 할 때 사용할 수 있습니다.

    지금은 쓰레드 그룹들에 대한 개념이 약간 헷갈리실 수 있습니다. 그냥 그룹을 하나만 사용하면 안될까요? 뭐 가능합니다만 GPU에서 쓰레드들이 그룹으로 정렬되는 이유가 있다는 것을 아셔야 합니다. 우선 쓰레드 그룹 내 쓰레드 개수에는 제한이 있습니다(셰이더에서 “[numthreads(x,y,z)]”로 정의되어 있는 부분). 이 제한은 현재 1024개 이고, 새로운 하드웨어에서는 다를 수 있습니다. 예를 들어 1차원이라면 최대 “numthreads(1024,1,1)”개, 2차원이라면 “numthreads(32,32,1)”인 식으로 최대치를 가질 수 있습니다. 하지만 쓰레드 그룹의 개수에는 제한이 없고, 백만개 이상의 데이터를 처리해야 할 때에는 쓰레드 그룹의 개념이 필수적입니다. 그룹 내 쓰레드들은 또한 메모리를 공유할 수 있으며, 이는 어떠한 알고리즘에서는 굉장한 성능 이득을 가져다 줍니다. 이에 대해서는 나중 포스트에서 다루도록 하겠습니다.(역주 : 아직 이에 대한 포스팅은 없는 것 같습니다. 이러한 자료 등을 참고하세요)

    커널과 쓰레드 그룹에 대해서는 어느정도 다룬 것 같습니다. 하나 더 다루고 싶은 부분이 있는데요. 셰이더에 uniform을 어떻게 전달하는지 입니다. Cg 셰이더와 같은 방식으로 동작하지만, uniform 키워드는 없습니다. 대부분 이 작업은 간단하지만 몇 가지 제가 파악한 부분이 있어 짚어드리고자 합니다.

    예를 들어, float을 셰이더에 전달하고 싶을 때는 셰이더에 아래 부분을 추가하고

    float myFloat;

    스크립트에 아래를 추가하면 됩니다.

    shader.SetFloat("myFloat", 1.0f);

    벡터를 전달하기 위해서는 아래를 셰이더에 추가하고

    float4 myVector;

    아래를 스크립트에 추가하면 됩니다.

    shader.SetVector("myVector", new Vector4(0,1,2,3));

    스크립트에서는 Vector4만 전달 가능하지만, uniform(역주: 셰이더에서의)은 float, float2, float3, float4가 될 수 있습니다. 적절한 값으로 채워지게 됩니다.

    이제, 좀 까다로워지는 부분입니다. 배열을 전달할 수 있습니다만, 첫 번째 예제는 작동하지 않는다는 것에 주의하십시오. 이유는 설명해 드리겠습니다. 셰이더에는 아래 코드를 추가하시고,

    float myFloats[4];

    스크립트에는 아래를 추가하세요.

    shader.SetFloats("myFloats", new float[]{0,1,2,3});

    이 코드는 동작하지 않습니다. 원래 설계가 이렇게 되었는지, 유니티에서의 버그인지 모르겠습니다. 가능하게 하려면 벡터를 uniform으로 사용해야 합니다. 셰이더에서는

    float4 myFloats;

    스크립트에서는

    shader.SetFloats("myFloats", new float[]{0,1,2,3});

    이러면 동작합니다. Float2나 float3도 사용 가능합니다. 단지 float 하나는 안됩니다. 마찬가지로 벡터의 배열은 사용 가능합니다. 셰이더에서는

    float4 myFloats[2];

    스크립트에서는

    shader.SetFloats("myFloats", new float[]{0,1,2,3,4,5,6,7});

    여기에선 float4 두개로 이루어진 비열이 있고, 스크립트에서는 8개 float으로 이루어진 배열로 할당됩니다. 행렬에 대해서도 같은 원리가 적용됩니다. 셰이더에서는

    float4x4 myFloats;

    스크립트에서는

    shader.SetFloats("myFloats", new float[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15});

    당연히 행렬의 배열도 사용 가능합니다. 셰이더에서는

    float4x4 myFloats[2];

    스크립트에서는

    shader.SetFloats("myFloats", new float[]{0,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});

    같은 논리는 float2x2나 float3x3에는 적용되지 않는 것 같습니다. 역시, 버그인지 설계가 그렇게 된 것인지는 모르겠습니다.

    오늘의 분량은 여기까지 인 것 같습니다. 다음 글에서는 compute shader에서 텍스처를 사용하는 방법에 대해 다루도록 하겠습니다. 커널 예제에 대한 프로젝트 파일을 다운로드 하실 수 있습니다. 기본적인 것이지만, 필요하다면 받으세요. 이후 튜토리얼들에서도 프로젝트 파일을 추가할 예정입니다.

    Project Files


    Tuesday, December 1, 2015

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

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

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

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

    아래 개요 부분에 잘 나와있지만, GPGPU 프로그래밍을 위해 여러 API가 개발되었고, 개발되고 있습니다만, 현재, 그리고 앞으로도 Unity에서 가장 쉽게 이를 활용하기 위한 방법은 DirectCompute를 통한 방법이 될 것 같습니다. 하지만 Documentation이 현재 매우 부족한 상태이고(블로그 글이 1년 전인데, 그 때도 그랬다고 합니다...), 저도 검색하다 위 블로그를 발견하여 많은 도움을 얻었습니다. 필요하신 분들에게 도움이 되었으면 좋겠습니다.

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

    UNITY를 위한 DirectCompute  튜토리얼: 개요

    - From ScrawkBlog (http://scrawkblog.com/)

    저는 유니티를 2년정도 사용해왔지만, 이전에는 C++로 직접 그래픽스 프로그램을 만들었었습니다. OpenGL을 사용했었고, 그러면서 GLSL 셰이더 프로그램 경험을 많이 쌓았죠. Unity를 사용하면서부터는 Cg 셰이더를 사용했습니다. GLSL과 Cg 셰이더는 크게 다르지 않아서 쉽게 바꿀 수 있었습니다(GLSL도 사실 Unity에서 지원하지만, 그렇게 많이 사용되지는 않습니다). OpenGL 사용 경험은 방대한 연구용 코드의 포팅, 주로 OpenGL 코드를 Unity로 변환하는 것을 가능하게 하는 데 많은 도움이 되었습니다(예들 들어, 최근의 Proland port).

     이제 Unity가 Microsoft DirectX 11을 지원하기 시작했고, DirectX 11에는 DirectCompute API가 있습니다. 이 API는 compute shader를 통한 GPU의 활용에 대한 완전히 새로운 방법을 가능하게 합니다. 다만 문제가 하나 있는데요. DirectCompute의 사용에 관한 튜토리얼이나 문서화가 부족하다는 것입니다. 구글 검색에서도 많이 나오지 않고, Microsoft의 문서도 적고 아마존에서도 몇 개의 책들만이 이 주제를 다루고 있습니다. 좋은 자료를 찾으면 코멘트 부분에 링크를 남겨주시면 좋을 것 같습니다.(역주 : 이 블로그의 모든 코멘트 창은 현재 닫혀있음 ㅜㅜ)

     지난 몇 년 동안, 이 강력한 API를 사용하는 방법에 대한 자료를 수집했고, 여전히 그에 관한 정보들은 부족하게 생각되어 Unity에서 DirectCompute를 사용하는 법에 대한 튜토리얼 시리즈를 작성하기로 결심했습니다. 왜 DirectCompute가 필요하고 기존 그래픽스 파이프라인과 어떻게 다른지, compute shader를 통해 커널을 어떻게 구현하는지, GPU 내부의 쓰레드 tiling이 어떻게 동작하는지, 텍스처에 어떻게 접근하는지, 여러 버퍼 타입을 어떻게 사용하는지, 쓰레드 동기화와 공유 메모리를 어떻게 사용해야 하는지와 최종적으로 어떻게 성능을 향상할 수 있는지에 대한 소개로 시작하고자 합니다.

    이 튜토리얼들은 Cg 셰이더에 대한 지식은 필요로 하지 않지만, compute shader를 쓰기 전에 보통의 Cg 셰이더에 대한 확실한 이해를 하기를 추천합니다. 보토 compute shader로 처리하는 데이터는 가시화 될 데이터들이고, 이러한 결과를 위해서는 보통의 Cg 셰이더가 필요하기 때문입니다. 하지만 이러한 내용도 어쨌든 다룰 예정입니다. Compute shader는 Microsoft의 HLSL로 쓰여집니다. 튜토리얼을 이해하기 위해 이 언어를 알아야 할 필요는 없지만, 적어도 하나의 셰이더 언어(GLSL, Cg 또는 HLSL)에 대한 경험을 해보길 추천합니다. 다행히 HLSL과 Cg는 매우 유사해서 차이점을 알아채기 어려울 정도입니다. 또한 Monodevelop이 기본적으로 제공하지 않기 때문에, compute shader를 위한 syntax highlighter를 세팅하기를 추천합니다.

    그래픽스 파이프라인


    DirectCompute가 왜 필요한지 이해하려면 기본적인 그래픽스 파이프라인과 그 역사에 대해 이해하는 것이 도움이 됩니다. 모든 것은 90년대 초반에 시작되었습니다. 3D 그래픽스가 대중적이 되었고, 컴퓨터에서 GPU가 기본 사양이 되기 시작한 시기죠. 필요한 것은 개발자가 이 강력한 장치를 제어할 수 있는 그래픽스 API였습니다. 1992년에 SGI에서 개발한 IRIS GL이라는 독점적인 시스템에 사용될 OpenGL 1.0이 릴리즈되었습니다. Microsoft는 SGI, Digital Equipment Corp, IBM, Intel과 함꼐 OpenGL표준 개발을 가이드하는 OpenGL 아키텍처 리뷰 위원회 중 하나였습니다. 제정 후 얼마 뒤에 Microsoft는 위원회를 나와 경쟁 시스템인 DirectX API의 한 부분인 Direct3D 개발을 시작했습니다. 위원회는 나중에 Khronos 그룹으로 발전하였고 현재 멤버는 3Dlabs, ATI, Discreet, Evans & Sutherland, Intel, NVIDIA, SGI와 Sun Microsystems입니다.

    개발자가 해야 할 일은 렌더링 될 지오메트리를 GPU로 전송하는 것이었고, OpenGL은 픽셀들이 화면에 디스플레이 되기까지의 파이프라인으로 그것들을 전달합니다. 이 파이프라인은 일부분을 켜거나(enable) 끄거나(disable) 일부 세팅을 변경할 수 있긴 하지만 고정적인 것이었고, 바꿀 수 있는 부분은 많이 없었습니다. 얼마 간은 그것이 필요한 전부였지만, 개발자들이 GPU를 더 활용하고, 발전된 기능들을 필요로 하게 되면서 이 파이프라인이 좀 더 유연성을 가져야 한다는 것이 명확해졌습니다. 해결책은 파이프라인의 특정 부분을 프로그램 가능하도록 하는 것이었죠. 이제 개발자들은 파이프라인의 특정 부분에 대해 프로그램을 작성할 수 있었습니다. 이 새로운 프로그램은 셰이더 (프로그램과 셰이더에 대한 기술적인 차이가 있긴 하지만, 그건 다른 이야기로 합시다)라 알려지게 되었습니다.

    이 프로그램 작성 가능한 파이프라인은 완전히 새로운 가능성을 열어주었고, 그것이 없었다면 최근 세대의 게임에서와 같은 품질의 그래픽은 불가능했을 것입니다. 한편, 파이프라인은 원래 그래픽을 생성하기 위해 개발되었지만 새로운 유연성은 이제 GPU가 여러 종류의 알고리즘을 처리하는 데 사용될 수 있다는 것을 의미합니다. 다양한 연구들이 재빨리 알고리즘을 GPU의 멀티쓰레드 환경에서 동작하도록 수정하기 시작했고, 물리, 금융, 수학, 의학과 다른 많은 분야에서 데이터 처리를 위해 GPU를 사용하기 시작했습니다. GPU의 성능은 거부하기 힘든 것이었고, GPGPU 또는 General Purpose Graphical Processing Unit 프로그래밍이 탄생하게 되었습니다.

    General Purpose Graphical Processing Unity 프로그래밍


     GPGPU는 빠르게 주류가 되었고 산업적인 용도로 사용되었습니다. 하지만 여전히 한 가지 문제가 있었는데, 그래픽스 API가 여전히 그래픽스 파이프라인에 묶여있다는 것이었습니다. 파이프라인의 어떤 단계에서 아무리 셰이더가 많은 유연성을 제공한다 하더라도, 파이프라인의 제약에 따라야 했습니다. 정점 셰이더(vertex shader)는 여전히 정점을 출력해야 했고, 픽셀 셰이더(fragment shader)는 여전히 픽셀을 출력해야 했습니다. 엄청난 진보와 창의적 작업물들이 개발되면서, GPGPU가 더 발전하려면 변화가 일어나야 한다는 것이 확실해졌습니다. 개발자를 그래픽스 파이프라인이라는 족쇄로부터 자유롭게 하고, 그래픽스와 관련이 없는 세팅에서도 GPU의 성능을 활용할 수 있도록 하는 환경을 제공하는 API가 필요하게 되었습니다. 

    그 후 몇 년간 GPGPU API가 나타났고, 현재 개발자들은 CUDA, OpenCL, DirectCompue를 선택할 수 있습니다. 이 새로운 API들은 GPU로 작업할 수 있는 새로운 방법을 제시하였으며, 전통적인 그래픽스 파이프라인에 더 이상 구속되지 않습니다. 여전히 약간의 제약이 있지만, 다른 어떤 방법보다도 멀티쓰레드 환경에서의 작업을 하기 적합합니다.

     GPGPU에 대한 필요성은 그래픽과 관련 없는 환경에서도 GPU를 활용한 작업 방법을 제시하는 것이었으나 당연하게도 게임 업계도 마찬가지로 이 새로운 기능을 활용하는 데 열정적입니다. 게임들이 점점 리얼해지면서, 실세계의 물리현상을 시뮬레이션하기 시작했습니다. 이러한 계산은 복잡하고, 많은 처리를 필요로하는 경우가 많습니다. GPGPU가 제공하는 성능과 유연성으로 훨씬 상세하고 광범위하게 이러한 계산이 가능합니다. 기존 그래픽 API가 3D 게임에 새로운 세상을 열었다는 것은 우리 모두가 받아들이는 바입니다. GPGPU API는 이제 같은 길을 걷고 있고, 게이밍에 대로운 시대를 열 것입니다. 현 세대의 게이밍은 이러한 API의 활용으로 가능해진 현실적인 그래픽과 물리 현상으로 가득해질 것입니다.

    이제 당신이 해야 할 일은 그것들을 사용하는 방법에 대해 배우는 것 뿐입니다. 커널을 세팅하고, 쓰레드 작업을 할당하는 법을 다음 파트에서 보여드리겠습니다.

    아래는 현재 dx11의 파이프라인 그림입니다.



    Wednesday, November 25, 2015

    3D Convex hull of a working excavator - 1.Problem definition and the first development

    • Problem Def
      • The reason why I've been investigating 3D, dynamic convex hull is to calculate and visualize it with working excavator, for safety control.
      • The concept is similar with the objective of proximity sensor mounted in vehicle, which allows safety stop of a vehicle by detecting obstacles before crashing.
      • So far, we're developing simulator of it focusing on excavator, where safety control of excavator is much complex due to it's kinematic characteristic.
      • Same problem in 2D space is simpler and we already have a result.

      • As you may notice, the area is predicted using angular velocity of the cabin so it is became wider along its rotation heading.
      • It stops when detecting an obstacle in "danger area".
      • The prediction of actual danger area is much complicated and emergency stop algorithm considers many variables. It will be studied by other research group. And is not included in video clip.
      • What I have done is calculate and visualize 2D danger area by 2D convex hull algorithm using series of offset points of cabin where the movement of it is simply predicted from linear extrapolation.
      • In this new series of posts, I will study the method to calculate and visualize 3D danger area in real-time rate. In 3D, all movement of its components(cabin, boom, arm and bucket) will be precisely reflected in danger area.

    • Preparation
      • First step I did for 3D is reducing the number of points for convex hull calculation. The target source model of mine has 26,805 points(vertices).
    Target excavator mesh. Cabin, boom, arm and bucket are separated, but shown as an assembly.

      • As you may seen in previous post regarding my implementation of 3D convex hull using QuickHull method, it is hard to achieve real-time rate(let our first, ambitious target number as 30Hz, around 30ms)with that much point input. It results 280ms to calculate 3D convex hull in MeshLab.
      • We don't need all these points in runtime. We only need the points that has a possibility to contribute in result convex hull.
      • As a preprocessing, convex hull of each component(cabin, boom, arm and bucket) mesh with 1 unit(=meter in application) offset has been made and recorded the information of point(actually, index of vertices).
      • In runtime, only those points are considered for convex hull calculation.
      • As a result, 1,750 point set is a minimum number of input point when excavator doesn't move.

    Example of arm mesh preprocessing. With 1 unit offset, convex hull was calculated with 233 out of 2,687 points, so we can filter out rest points in run time calculation.

    Convex hull of all components. 1,750 out of 26,805 points contributes to result convex hull.

    • First development
      • Unfortunately, we have to deal with up to 35,000 points in runtime.
      • Since danger area should be "predictive", number of points are larger than 1,750. For example, we set the prediction interval as 100 ms and prediction time up to 2000 ms. Therefore up to 1,750 x (2000/100) = 35,000 number of points are became input for each frame.
      • The first version of application is implemented with features below
        1. Simple excavator manipulation using keyboard
        2. Component movement prediction using linear extrapolation and several global-local coordinate conversion between orientation of components
        3. Some debug & logging modules for further performance test
        4. Of course, 3D convex hull calculation using points from 2.
      • To prevent convex hull calculation affects rendering-related executions, 4. was implemented as a separated thread.
      • Result video clip is here.

      • Without movement, calculation from 1,750 point input can be done around 80~90ms.
      • When there is a movement, so prediction starts, (you may see the predictive danger area covers ahead from current position of components) around 20,000 points are passed as an input so calculation takes around 250ms.
      • Therefore for 30ms calculation, about 10 times faster calculation is required.

    • Next
      • I'm considering to develop parallelized calculation for convex hull.
      • There is a PAPER studied Quickhull algorithm with CUDA(named CudaHull), but I'm not sure it may fit to our application. GPU-CPU data transfer takes time and if we follow the method of CudaHull, the transfer should happened in every single frame.

    Friday, November 6, 2015

    Convex Hull algorithm in Unity - 7. Tentative Final

    Quickhull Benchmark Test2

    • Version 2.2 Implementation
      • Optimize data structure. Cleaning up.
    • Benchmark test vs MeshLab Qhull
      • Same performance test as previous pose, was conducted after optimization.
      • Result shows good enough numbers. Result of test interval 3 shows still far faster calculation time compared with MeshLab Qhull, and difference of result numbers in interval 1 has been decreased.
    Full result table

    (Interval 1) Calculation time graph. My implementation is marked as blue

    (Interval 2) Calculation time graph

    (Interval 2) Calculation time graph

    • Next?
      • For the next step, real-time calculation combined with actual application (excavator simulation) will be investigated.

    Tuesday, November 3, 2015

    Convex Hull algorithm in Unity - 6. Performance Benchmark

    Quickhull Benchmark Test

    • Version 2.1 Implementation
      • Added additional property for face adjacency data. This data enables faster calculation in horizontal finding.

    • Benchmark test vs MeshLab
      • MeshLab also has convex hull calculation function. (Filters -> Remeshing, Simplification... -> Convex hull, based on the version 1.3.3)
      • Test set are prepared from my Unity3D application, that generates random 3D locations inside unit sphere with designated number of times.
      • Prepared points sets have 1,000~9,000 (1st interval), 10,000~90,000 (2nd interval) and 100,000~900,000 (3rd interval) number of points.
      • Processing time and number of triangles in result convex hulls are recorded.

    Full result table. As table shows, number of result triangle is identical in my implementation and MeshLab. Therefore I can argue that the result convex hulls from my implementation is valid.
    (Interval 1) When the number of input point is small, processing time of MeshLab module is faster than my implementation.

    (Interval 2) Around 60,000 number of input points, my implementation starts to show faster processing time than MeshLab module 

    (Interval 3) Above hundred thousands of points, my implementation shows way better result than MeshLab


      • I think there exist not optimized code in the early stage(ex, initial conflict list building) of process in my implementation, which I think, is convincing since mine finished slower in Interval 1(with smaller input points).
      • I'm not sure why my implementation is faster with larger input points. Maybe due to the newly implemented face adjacency data? As far as I know, the same procedure is done by half-edge traverse in MeshLab implementation.

    • Demo Clip
      • My implementation in Unity3D C# and MeshLab
      • The last point set (102,334 points) results 2,870 triangles both in my result and MeshLab