-
Drawinstanced Vs Merged Instancing
GDC 2014 : Vertex Sahder Tricks 슬라이드에 따르면 DrawInstanced 함수를 사용하여 인스턴싱을 하는것보다 vertexID 를 사용하여 인스턴싱을 하는것이 빠르다고 한다. vertexID 를 쓰는 방법은 굉장히 단순하다.
VSOutput VS(uint id : SV_VertexID) { VSOutput output; /* ... */ return output; }
SV_VertexID Semantic 을 사용하여 값을 접근하기만 하면 된다. vertexID 는 말그대로 버텍스별 인덱스를 뜻한다. SRV 나 UAV 와 함께 사용하여 Instancing 을 하면된다.
출처 : GDC 2014 : Vertex Sahder Tricks 그림을 보면 AMD GPU 에서 확실히 퍼포먼스 차이가 난것을 확인할 수 있다.
스피커가 AMD 소속이라는 게 포인트참조 자료
-
Hbao Plus Analysis 3
HBAO+ 3.1 버젼을 기준으로 글이 작성되었습니다.
이전 hbao plus analysis 2 글에서 Horizon based ambient occlusion 와 Cross Bilateral Filter 대해서 알아보았다. 이번 글에서는 부록의 느낌으로 HLSL 코드를 읽으면서 생소했던 기타 기법들에 대해서 써볼 것이다.
첫번째로 Full Screen Triangle 이라는 기법이다. 알고마면 굉장히 단순한 개념으로, 화면을 모두 덮는 한개의 삼각형을 그려서 모든 픽셀에 쉐이더를 돌릴 수 있게 해주는 기법이다. 아래 슬라이드를 보면 쉽게 이해가 갈것이다.
출처 : GDC 2014 : Vertex Sahder Tricks 단순하지만 처음 봤을 때는 조금 신박하게 느껴질 수도 있다. 두번째로는 모든 계산에 최대한 HLSL Intrisic 을 사용한다. 특히 벡터와 벡터사이의 거리를 계산할때 dot product 를 써서 하는게 정말 많았다. 어셈블리 레벨에서 달라지는것 같긴하나 정확한 이유는 알지 못했다. 추측해보면 GPU 에서 해당 명령어가 있지 않을까.. 라고 생각한다.
세번째도 위의 것과 비슷하다. 대부분의 데이터에 MAD 방식을 사용해서 계산한다. 하지만 이는 거의 공식적으로 정해진게 있다. MSDN : mad function 레퍼런스에서도 나오듯이 어떤 GPU 에서는 위에서 추측한대로 하드웨어에서 지원하는 명령어라고 한다.
… Shaders can then take advantage of potential performance improvements by using a native mad instruction (versus mul + add) on some hardware. …
또한 HBAO+ 소스에서 찾은 주석에는 GK104 부터 특정 구간에서 10% 퍼포먼스 이득이 있다고 쓰여져 있다.
네번째는 나누기를 절대 쓰지 않는다. 나머지 연산(mod, A % B)는 간혹 쓰이지만 나누기는 절대로 쓰이지 않았었다. 혹시라도 필요하다면 전부 Constant Buffer 에 CPU 에서 역수를 취해서 넘겨주는 방식으로 되어 있었다. 이도 역시 하드웨어에서 동작하는 부분을 알고 짠듯하다.
다섯번째는 HLSL 코드를 cpp 소스에 include 하여 Constant Buffer 값을 갱신하는 코드였다. 여태까지 예전의 DirectX 소스만 보거나 Unity 에서만 작업을 해서 그런지 이런 기능은 굉장히 낯설었다.
참조 자료
-
Hbao Plus Analysis 2
HBAO+ 3.1 버젼을 기준으로 글이 작성되었습니다.
이전 hbao plus analysis 1 글에서 HBAO+ 에서 Linearize Depth 와 Deinterleaved Texturing 에 대해서 알아보았다. 이번 글에서는 HBAO+ 의 핵심 알고리즘인 Horizon Based Ambient Occlusion 와 AO 블러에 사용되는 Cross Bilateral Filter 에 대해서 알아볼것이다.
-
Hbao Plus Analysis 1
HBAO+ 3.1 버젼을 기준으로 글이 작성되었습니다.
이전 hbao plus analysis 0 글에서 HBAO+ 을 알기위한 기본적인 개념들에 대해서 살펴보았다. 이번 글에서는 HBAO+ 의 구조와 Linearize Depth 와 Deinterleaved Texturing 에 대해서 알아보겠다.
-
Hbao Plus Analysis 0
게임에서 쓰이는 실시간 렌더링에서 빛과 물체들의 상호작용을 완벽하게 현실적으로 표현하는 거의 불가능하다. 하지만 이를 위해 수십년동안 많은 엔지니어와 연구자들이 노력하여 부분적이고 제한된 환경에서의 빛과 물체의 상호작용을 현실 세계와 비슷하게 따라잡고 있다. 이번 글에서 살펴볼 것은 Screen-Space Ambient Occlusion(SSAO) 기반의 HBAO+ 라는 라이브러리에 대해서 알아볼 것이다.
-
Shader Pipeline 4 Geometry Shader
“Fragemnt Shader” 에서 Fragment Shader 에 대해 알아보았다. 다음은 Geometry Shader 에 대해서 써보려 한다.
Geometry Shader 는 쉐이더 파이프라인에서 Rasterizer Stage 넘어가기 전의 Geometry Stage 의 마지막 단계로써 이전 쉐이더에서 넘긴 Primitive 데이터(point, line, triangle..)를 프로그래머가 원하는 복수의 Primitive 데이터로 변환할 수 있다. 삼각형을 삼각형의 중심을 나타내는 점으로 변환하는 쉐이더를 보자.
[maxvertexcount(1)] void geom(vertexOutput input[3], inout PointStream<geometryOutput> pointStream) { geometryShaderOutput o; o.vertex = (input[0].vertex + input[1].vertex + input[2].vertex) / 3; pointStream.Append(o); }
매우 간단한 코드다. 간략하게 설명하자면, 맨 윗줄의 maxvertexcount 는 해당 지오메트리 쉐이더에서 Stream 으로 넘길 정점별 데이터의 갯수를 뜻한다. Geometry Shader 한번당 Stream 으로 넘길 maxvertexcount 의 한계는 정해지지 않았지만 크기는 1024 바이트로 정해져 있기 때문에 적절하게 사용해야 겠다. 그 다음줄의 인자들에 대해서 설명하면, 첫번째 vertexOutput input[3] 은 정해진 프리미티브의 값들을 뜻한다. 여기서는 삼각형을 기준으로 만들었기 때문에 정점별 정보가 3개가 있다. _inout PointStream
pointStream_ 은 _Geometry Shader_ 의 최종 출력을 해주는 오브젝트다. _PointStream_ 은 점 프리미티브의 데이터를 받는 _Stream_ 으로써, 프리미티브가 다르면 각자 다른것을 사용할 수 있다.([MSDN : Getting Started with the Stream-Output Stage](https://msdn.microsoft.com/en-us/library/windows/desktop/bb205122.aspx)) 부등호 안에 있는 것은 일반적으로 알려진 제너릭이나 템플릿의 형태와 같으니 안에 출력으로 넘길 구조체를 넘겨주면 된다. 함수의 내용은 삼각형을 구성하는 각 정점의 위치의 평균을 구해 하나의 정점 정보만 _Stream_ 에 넘긴다. Stream 은 총 두가지의 역할을 한다. 하나는 Rasterizer 단계로 넘겨서 쉐이더에서 처리를 할 수 있게 하는 통로 역할을 하고, 다른 하나는 드라이버 레벨에서 데이터를 출력해주는 통로 역할을 한다. 두가지의 일을 하기 때문에 Stream 의 개념으로 추상화한 것인가 싶다. 그리고 하나의 Geometry Shader 에서 여러개의 Stream 으로 출력이 가능하긴 하다. 최대 4개의 Stream 을 사용할 수 있다. Stream 을 선택해서 데이터를 받아올 수도 있으며, Rasterizer 로 보낼수도 있다. 자세한 사항은 MSDN : How To: Index Multiple Output Streams 에서 확인하면 되겠다.
활용할 수 있는 다른 기능이 하나 더 있다. instance 기능이다. 아래 코드를 보자.
[instance(3)] [maxvertexcount(1)] void geom(vertexOutput input[3], uint InstanceID : SV_GSInstanceID, inout PointStream<geometryOutput> pointStream) { geometryShaderOutput o; o.vertex = input[InstanceID].vertex; pointStream.Append(o); }
해당 코드는 삼각형의 세개의 정점 위치를 넘기는 코드다. 달라진 것은 instance(3) 코드가 붙고, uint InstanceID : SV_GSInstanceID 파라미터가 생겨 코드 안에서 이를 활용한다. instance(x) 에 들어가는 x 는 반복하는 횟수를 뜻하고, InstanceID 파라미터는 반복하는 인덱스를 뜻한다. 같은 입력을 여러번 받아서 일정한 수만큼 반복하는 것이다. instance 속성에 들어가는 숫자의 한계는 32까지다.
Geometry Shader 는 Shader Model 4.0 에서 추가되었으며 뒤에 추가적으로 알아본 multiple stream 과 instance 키워드는 Shader Model 5.0 에서 확장된 기능들이다.
참조 자료
-
Shader Pipeline 3 Fragment Shader
이전 글 : “Rasterizer” 에서 Rasterizer 에 대해 알아보았다. 이번에는 Fragment Shader 을 알아보자.
쉐이더 파이프라인에서 Rasterizer 다음에 실행되는 것은 Programmable Shader 중에서 Fragment Shader 이다. Fragment Shader 은 Pixel Shader 라고도 불리는데, 이전에 Rasterizer 에서 조각낸 픽셀들을 단위로 실행되기 때문에 Pixel Shader 라고 불리기도 한다. 또한 각 픽셀은 조각난 단위이기 때문에 조각의 뜻을 가진 Fragment Shader 라고도 불린다. 이 글에서는 Fragment Shader 로 사용할 것이다.
Fragment Shader 의 역할은 굉장히 단순하다. Geometry Stage 에서 넘어와 Rasterizer 단계에서 정리된 파라미터를 받고, 해당 픽셀의 색을 반환하면 끝난다. 역할은 단순하지만 그만큼 중요한 것이 Fragment Shader 다. 마지막으로 픽셀 단위로 보여주는 색을 바꿀 수 있는 Programmable Shader 로써 반복하는 비용이 꽤나 많아 일반적으로 오래 걸린다고 평가되지만 색을 바꿀 수 있어 그만큼 잘쓰면 굉장한 효과를 낼 수 있는 Programmable Shader 다.
가장 단순한 형태의 Unity 에서 사용하는 CG/HLSL 쉐이더를 보자.
#pragma fragment frag struct v2f { float4 vertex : SV_POSITION; float4 tangent : TANGENT; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; } /* 다른 코드 들.. */ fixed4 frag(v2f i) { return float4(1,1,1,1); }
해당 쉐이더는 단순하게 흰색만 출력해주는 쉐이더다. 그만큼 매우 단순하고 쉽다. 하지만 많은 것들을 표현하려면 frag 함수의 코드는 점점 길어질 것이다.
또한 Fragment Shader 가 실행되는 시점에서 하드웨어, 드라이버 단계에서 지원하는 기능들도 있다. 일반적인 것들에 대해서 이야기 하자면 Depth Buffer 와 Stencil Buffer 가 있다. 두가지의 공통점은 각 픽셀 단위별로 데이터를 저장하는 버퍼들이다. Depth Buffer 는 Clip-Space 로 변환된 정점 값의 Z 값을 저장하는 용도로 쓰이는 버퍼로, 요즘 개발되거나 쓰이는 기술들은 Depth Buffer 를 엄청 많이 쓴다. 대표적으로 Depth Pre-Pass 가 있다. Stencil Buffer 는 픽셀별로 정수 데이터를 저장해서 사용하는 버퍼로써, 대부분 마스킹을 할 때 쓰인다.
참조 자료
-
Shader Pipeline 2 Rasterizer
이전 포스트 “Vertex Shader” 에서 Vertex Shader 에 대해 간단히 알아보았다. 이번 글에서는 Rasterizer 에 대해서 간단히 써보려 한다.
Rasterizer 는 쉐이더 파이프라인에 존재하는 고정 기능 단계이다. 간단하게 정의하면, Rasterizer 이전 단계를 거쳐 나온 Geometry 데이터들(vertex, mesh, …)을 정해진 해상도에 맞춰 픽셀별로 조각내어 주는 단계다. 아래 그림을 보자.
이는 일반적인 삼각형 폴리곤을 Rasterizer 단계에서 어떻게 픽셀로 변환하는지 한눈에 알게 해놓은 사진이다. 겹치는 정도에 따라 검은색에 가깝게 해놓은 것을 볼 수있다. MSDN : Rasterization Rules 에서 다른 프리미티브의 Rasterize 과정도 살펴볼 수 있다.
쉐이더 파이프라인에서 Rasterizer 가 가지는 의미도 조금 특별하다. 이는 3차원 메시 데이터를 2차원 이미지 데이터로 바꿔주는 과정이기 때문에 Geometry Stage 에서 Rasterizer Stage 로 넘어가는 관문이다.
또 하나 중요한 것은, Rasterizer 이전의 Geometry Stage 에서 각각 스테이지별로 넘어오던 데이터들을 Interpolator 라고 하는데, Interpolator 의 갯수들은 render target 의 Pixel 이 많으면 많을수록 큰 영향을 미친다. 왜냐하면 Rasterizer 는 단순히 Geometry 를 Pixel 로 변환하는 것만 있는게 아니라 Vertex 별로 가지고 있던 Interpolator 들을 Pixel 별로 보간하여 넣어주어야 하기 때문이다. 또한 VRAM 의 공간도 차지한다. 즉 갯수가 많으면 많을수록 데이터를 처리하는데 많은 비용이 든다는 것을 알 수 있다. 퍼포먼스를 생각한다면 최대한 갯수를 줄이는게 좋은 것 이다.
참조 자료
-
Shader Pipeline 1 Vertex Shader
이번 글에서 언급할 쉐이더는 Vertex Shader 다. 한글로는 정점 쉐이더 라고 보통 말한다. Vertex Shader 에서 할 수 있는 것은, 정점별로 들어온 정보들을 코딩을 해서 프로그래머가 원하는대로 바꾸어 다음 쉐이더에서 처리할 수 있도록 해주는 Shader 다.
Unity 에서의 CG/HLSL 일반적인 Vertex Shader 코드는 아래와 같다.
#pragma vertex vert #include "UnityCG.cginc" struct appdata_tan { float4 vertex : POSITION; float4 tangent : TANGENT; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; float4 tangent : TANGENT; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; } v2f vert(appdata_tan i) { v2f o; o.vertex = mul(UNITY_MATRIX_MVP, i.vertex); o.tangent = i.tangent; o.normal = i.normal; o.texcoord = i.texcoord; return o; } /* 기타 코드들.. */
위 코드에서 vert 함수에서 반환하는 값을 Varying 이라고 보통 말한다. 이는 바뀔수도 있는 값이라는 의미이며, GLSL 에서 사용하는 syntax 명명이나, HLSL 에서는 따로 이름이 없어 보통 shader 함수에서 넘어가는 데이터들을 Varying 이라고 통칭한다. 참고로 vert 의 입력 파라미터로 들어오는 것은 Input Assembly 단계에서 처리해준 값으로 이는 Varying 이라고 잘 부르지 않는다.
굉장히 단순한 Vertex Shader 코드다. 코드가 단순한 만큼 이 Shader 는 최소한의 역할만 하고 있다. model-space 에 있는 정점을 clipping-space 의 정점으로 변환 시켜 다음으로(fragment shader) 넘긴다. 위에서 위치 데이터를 바꿀 수 있다고 언급했는데, 이 변환은 정상적인 메커니즘을 통해 오브젝트를 출력하려면 Rasterizer Stage 로 넘어가기전에 반드시 정점값에 적용시켜주어야 하는 변환이다. 해당 변환에 대해서는 Model, View, Projection에 설명해 놓았으니 간단하게 참고하길 바란다.
위 코드에서 보여준 것들은 최소한의 것들이다. 코드를 짜는 것은 프로그래머의 역량이기 때문에 더 창의적인 것들을 할 수 있다. 쉬운 것들 중에서는 표면에서 웨이브를 주어 표면이 일렁이는 것처럼 보이게 할 수 있다. 이는 시간을 키값으로 두어 삼각함수를 이용해 할 수 있겠다.
float time; struct appdata { float4 vertex : POSITION; }; struct v2f { float4 vertex : SV_POSITION; } v2f vert(appdata i) { v2f o; i.vertex = i.vertex + i.normal * sin(time + i.vertex.x + i.vertex.z); o.vertex = mul(UNITY_MATRIX_MVP, i.vertex); return o; }
메쉬는 여러개의 정사각형 모양으로 잘라진 평평한 판의 형태의 메쉬를 준비하고, 간단하게 x 좌표와 z 좌표를 기준으로 오브젝트가 일렁이는 것을 만들어 보았다. 이렇게 Vertex Shader 를 응용해서 정점 데이터를 프로그래머가 원하는데로 움직일 수 있다. 정점 쉐이더는 사용하기에 크게 어려운점은 없기에 Shader 를 처음 다룰 때 가지고 놀만하다. 또한 Vertex Shader 가 가장 응용되기 쉬운 것은 바로 skinning 이다. skinning 자체가 정점 데이터들을 움직이고 움직임을 기반으로 바꾸는 것이기 때문에 Vertex Shader 의 형태가 skinning 을 적용하기 가장 알맞다.
-
Shader Pipeline 0 Opening
쉐이더 프로그래밍 환경
Programmable Shader 들을 정리하기 위해 각 쉐이더별로 한개씩 글을 써보기로 했다. 그 전에 미리 알아야될 것들에 대해 알아보려고 한다. 각각의 Shader 들은 코더의 입장에서 바라보았을 때는 단지 몇개의 파라미터를 받고 값을 반환하는 함수들이다. 하지만 일반적으로 알고있는 함수들과는 조금 다르게 실행된다. 첫번째로 일반적인 바이너리들은 CPU 에서 직렬로 실행된다. 멀티 스레드 기능을 따로 쓰지 않는한 말이다. 하지만 Shader 는 기본적으로 병렬로 실행된다. 아래 그림을 보자.
CPU 와 GPU 의 차이를 간단하게 보여주는 그림이다. 다만 위 그림이 전부는 아니니 간단하게 알고 넘어가도록 하자. 우리가 주목해야 할 것은 바로 core 갯수의 차이다. 요즘의 CPU 는 core 의 갯수가 많지 않다. 최근에 나온 i7-8700k 를 보면 코어의 갯수가 6개 인것을 확인할 수 있다. 다만 OS 스케줄링이 있어서 실질적으로 실행되는 것은 core 의 갯수에 엄격하게 제한되지는 않는다. 중요한 것은 개인용 PC 에 들어가는 CPU 는 아직은 core 의 갯수가 10개를 넘어가지 않는다는 것이다. 반면에 실제 GPU 의 코어의 갯수를 꽤나 많다. 그림에서는 hundreds of cores, 몇백개의 core 라고 하지만 요즘 개인용 PC 에 들어가는 GPU 코어는 몇천개나(gtx1080) 된다. GPU 의 코어가 많은 이유는 간단하다. CPU 에서 돌아가는 프로그램에 비해 간단한 프로그램 바이너리(쉐이더 혹은 GPGPU 프로그램)를 동시에 실행하는게 최근의 GPU 가 쓰이는 목적이기 때문이다.
일반적으로 CPU 에서 코딩하는 프로그램과 다르게 GPU 에서 실행되는 프로그램들은 이러한 병렬적인 실행 환경 때문에 특수한 사항들과 제약사항들이 존재한다. 퍼포먼스를 염두하고 프로그램을 코딩하다 보면 처음 경험하는 프로그래머는 조금 당황스러울 수도 있다.
쉐이더 파이프라인
GPU 의 여태까지의 주요한 역할은 기하학적(geometry) 성격을 띄고있는 데이터들을(mesh, vertex …) 이차원 이미지로 계산하여 보여주는 일이였다. 그렇게 3D 에셋 저작툴이나(3dsmax, maya, …) 게임에서 GPU 를 활용해 보다 많은 것들을 표현할 수 있게 해주었다. 우리가 이번에 살펴볼 것은 Shader Model 5.0 의 쉐이더가 실행되는 단계다. 이 단계는 위에서 언급한 기하학적 성격을 띄고 있는 데이터를 이차원 이미지로 계산하는 단계를 나타낸 것이다. 아래 그림을 보자.
이 그림에는 여러가지 항목들이 있다. Programmable Shader 를 제외하면 전부 고정된 기능을 가진 단계로써 프로그래머가 완전히 제어를 할 수 없는 단계다. 우리가 살펴볼 것은 이름 끝에 Shader 가 붙은 것들이다. 차례대로 Vertex Shader, Hull Shader, Domain Shader, Geometry Shader, Pixel Shader 가 있다. 위의 단계들은 두가지로 분류할 수 있다. Geometry Stage 와 Rasterizer Stage 다. Geometry Stage 는 일반적인 3D 상의 위치나 벡터를 가지고 있는 데이터를 처리하는 단계를 말한다. 위 그림에서는 Rasterizer 전 까지의 단계를 뜻한다. Rasterizer Stage 는 2D 이미지로 처리된 상태에서 데이터를 처리하는 단계를 말한다. Rasterizer 단계 부터 오른쪽 끝까지의 단계다. 각 단계에 대한 자세한 설명은 해당 글에서 하겠다.
쉐이더 파이프라인을 알고 있어야 여러 이론들을 구현할 수 있다. 쉐이더를 다루려면 이 쉐이더 파이프라인을 아는 것은 필수라고 할 수 있겠다.
각종 버퍼들의 활용
Shader Model 4.0 과 Shader Model 5.0 를 통해 여러 버퍼들을 사용하여 Shader 안에서 돌고도는 varying data (쉐이더들의 파라미터들) 와 함께 응용을 할 수 있게 되었다. 이는 함수 한개씩을 파라미터와 반환값만 바꾸면서 코딩하는 환경에서 참조할 전역 변수를 만들어주어 훨씬 더 많은 데이터들을 접근할 수 있게 해준것이다.