들어가기 앞서 지금까지 AMP가 GPU를 활용하는 프로그래밍 기법이라고, 제가 지속적으로 언급해 왔었습니다. 사실 이 말은 적절하지 않는 표현이였습니다.
얼마 전까지만 해도, 개발자에게 주어지는 프로세싱 유닛은 CPU와 GPU 뿐이였습니다. CPU는 개발자의 활용 영역에 있었지만, GPU는 제한적으로 사용할 수 있었습니다. 왜냐하면 GPU를 사용하기 위해서는 DirectX API 사용이 필수였기 때문입니다. 그 DirectX 의 영역을 일반적인 개발자 영역으로 확장하는 것이 C++ AMP 입니다. 그런데 최근에 CPU와 GPU를 통합한 APU 라는 것이 등장했습니다. 앞으로 또 다른 프로세싱 유닛이 등장할지도 모르는 일입니다. 그래서 이런 프로세싱 유닛들을 통합한 용어가 필요하게 되었고, C++ AMP에서는 이를 accelerator 라고 합니다. 즉, CPU와 GPU 그리고 APU 가 이 accelerator 에 속한다고 할 수 있습니다. accelerator 는 C++ AMP 코드가 실행될 수 있는 이런 타겟을 표현합니다. 그래서 C++ AMP는 이 accelerator를 활용하는 프로그래밍 기법이라고 해석하는 것이 더 적절한 표현입니다. 앞으로 이 accelerator 라는 표현을 많이 사용할 것이니 확실히 알아두시기 바랍니다.
앞서 간단하게 작성했던 샘플을 다시 한번 보겠습니다.
void AddArrays(int n, int * pA, int * pB, int * pC)
{
array_view<int,1> a(n, pA);
array_view<int,1> b(n, pB);
array_view<int,1> sum(n, pC);
parallel_for_each(
sum.grid,
[=](index<1> i) restrict(direct3d)
{
sum[i] = a[i] + b[i];
}
);
}
array_view 라는 것이 먼저 눈에 보입니다. C++ AMP 에서는 대규모 메모리를 의미하는 클래스로 array 와 array_view 라는 것이 있습니다. 기본적으로 이 두 클래스의 목적은 accelerator 상으로 데이터를 옮기기 위함 입니다.
array 의 경우에는 실제 데이터 배열입니다. STL 의 컨테이너와 유사합니다. 반면 array_view 는 데이터 배열의 일종의 래퍼( wrapper ) 입니다. 그래서 array_view 는 STL의 이터레이터( iterator ) 와 유사한 동작을 합니다. array_view는 한 번에 여러 데이터의 동시에 접근할 수 있으며, 랜덤 액세스( random-access ) 가 가능합니다.
array 에 의해서 정의되는 배열 데이터는 accelerator 상에 메모리를 가지게 됩니다. 이것은 개발자가 직접 정의해서 할당할 수도 있고, 런타임( runtime ) 에 의해서 자동적으로 생성될 수도 있습니다. 그렇기 때문에 실제 데이터가 생성되어질 때 깊은 복사( deep-copy )를 하게 됩니다. 우리가 일반적으로 오브젝트를 메모리에 생성했을 때와 같다고 생각하시면 됩니다.
array 는 다음과 같이 사용할 수 있습니다.( 샘플은 msdn 에서 가져왔습니다 )
data = a;
for (int i = 0; i < 5; i++)
{
cout << data[i] << "\n";
}
반면에 array_view는 이름에서 유추할 수 있듯이, 실제 데이터들은 다른 accelerator 상에 있고, 이를 연산을 위해서 복사를 하는 개념입니다. 즉, 커널 함수가 실행될 때, 데이터가 복사됩니다. ( 커널 함수는 AMP 내의 람다 함수 부분을 의미합니다. )
이 array_view 개념은 DirectX11 에서 보셨던 분들은 쉽게 이해할 수 있는 개념입니다. 바로 ComputeShader 를 위해서 데이터들을 연결하는 바로 그 개념이기 때문입니다. 아래의 그림은 ComputeShader 의 동작 방식을 보여주는데, SRV( shader resource view )와 UAV( unordered access view ) 라는 것이 결국 view 의 역할을 하는 것입니다.
DirectX11 과 연계해서 생각한다면, array 라는 메모리 배열도 결국 텍스쳐 메모리라는 것을 눈치챌 수 있을 것입니다. DirectX10 부터 텍스쳐 인터페이스는 꼭 이미지 데이터를 의미하지 않습니다. 대용량의 메모리 블럭의 의미에 더 가깝다는 것을 알아두시기 바랍니다. 텍스쳐의 개념을 사용하기 때문에 동시에 여러 데이터에 접근이 가능하고, 랜덤 액세스도 가능한 것입니다.^^
백문이 불여일견이라고들 하죠? 글로써 언급하는 것보다, 프로그래머들은 코드로 볼 때 더 직관적인 이해를 할 수 있는 경우가 많습니다.
간단하게 두 배열의 합을 구하는 코드를 통해서, 이를 AMP 적으로 어떻게 작성하는지를 보겠습니다.
아래는 우리가 일반적으로 생각할 수 있는 CPU를 활용해서 합을 구하는 코드입니다.
void AddArrays(int n, int * pA, int * pB, int * pC)
{
for (int i=0; i<n; i++)
{
pC[i] = pA[i] + pB[i];
}
}
자세한 설명은 생략해도 될 것이라 생각합니다.^^ 아래는 C++ AMP로 작성된 합을 구하는 코드입니다.
#include <amp.h>
using namespace concurrency;
void AddArrays(int n, int * pA, int * pB, int * pC)
{
array_view<int,1> a(n, pA);
array_view<int,1> b(n, pB);
array_view<int,1> sum(n, pC);
parallel_for_each(sum.grid,
[=](index<1> i) restrict(direct3d)
{
sum[i] = a[i] + b[i];
} );
}
위의 AMP 구현 부분에서 색상이 들어간 부분이 CPU를 활용한 부분과 다른 부분입니다. 코드량이 증가해버린 단순한 사실을 우리는 확인할 수 있습니다. 코드가 증가한 가장 기본적인 이유는 메모리 문제입니다. 우리가 지금까지 C++ 에서 사용하는 메모리는 CPU 가 접근할 수 있는 시스템 메모리입니다. 이 메모리를 GPU 로 처리하기 위해서는 GPU가 직접적으로 접근 가능해야 합니다. 그런데 C++ 에서 할당한 메모리는 GPU가 접근할 수가 없습니다. 그래서 비디오-메모리에 시스템-메모리의 데이터를 복사하는 과정이 필요합니다. 그 과정이 바로 코드의 증가를 불러오는 것입니다.
( 복사라고 보기는 조금 모호합니다만, 지금은 그냥 넘어가겠습니다. ) 이 증가한 코드들에 대해서 지금부터 살펴보겠습니다.
#include <amp.h>
using namespace concurrency;
AMP를 사용하기 위한 헤더의 선언입니다.
기본적으로 AMP를 사용하기 위해서는 람다식과 concurrency 에 대한 이해가 있어야 합니다.
array_view<int,1> a(n, pA);
array_view<int,1> b(n, pB);
array_view<int,1> sum(n, pC);
이 부분은 앞서 언급했던 GPU가 접근할 수 있는 메모리 영역으로 데이터를 만드는 부분입니다. 이 데이터를 만들 수 있는 메모리 영역이
array 와 array_view 라는 것으로 구분됩니다.
이 둘의 차이는 이후에 다루어 드릴테니,
지금은 GPU가 접근할 수 있는 메모리 영역으로 생각해 주셨으면 합니다.^^
parallel_for_each(... ) restrict( direct3d )
c++ 에 main(...) 이 있다면, AMP 에는 parallel_for_each( ... ) restrict( direct3d ) 가 있습니다. 이 부분은 GPU가 연산을 시작하는 진입점( EntryPoint ) 입니다. parallel_for_each를 잘 모르시는 분들은 아래의 링크를 참고하시 바랍니다. http://vsts2010.net/123 더 자세한 사항은 이 블로그의 VC++ 10 Concurrency Runtime 카테고리를 참고하시기 바랍니다.
제가 단순하게 정리해 드리면, 기존에 VC++ 10 에서 사용되는 parallel_for_each 는 CPU를 활용해서 병렬적으로 처리하는 것이지만,
뒤에 restrict( direct3d )를 명시함으로써 이를 GPU에서 병렬적으로 처리하도록 합니다. 이 진입 함수는 parallel_for_each( 람다식 ) 형태를 가지게 됩니다. 이는 GPU의 많은 스레드들에게 '이 람다식을 각각 실행해 주세요' 라고 명령을 내리는 것입니다. 역시 람다( Lambda ) 에 대해서 잘 모르시는 분은 옆의 카테고리에서 c++0x 를 보시기 바랍니다. 람다의 첫번째 설명 링크는 아래와 같습니다. http://vsts2010.net/73
그러면 얼마나 많은 스레드들이 람다식을 실행해야 하는지에 대한 명시가 있어야 합니다. 그것이 바로 paralle_for_each( ... ) 의 첫번째 인자인 sum.grid 입니다. grid 에 대한 설명은 뒷부분에서 자세히 다루겠으니, 지금은 스레드 갯수에 대한 정의로 보시면 충분합니다.
람다식의 인자로 index<1> idx 가 보이실 것입니다. 이 인자는 람다식에 전달되는 스레드들의 ID들입니다. 이 ID들을 통해서 스레들을 식별할 수 있습니다. 이 스레드들의 ID를 통해서 배열 형태의 데이터를 캡쳐해서 값을 저장하는 것입니다.
간단한 프로그램이지만, 사실 이런 형태가 C++ AMP의 전부입니다.^^
물론 이렇게 간단히 끝나면 무척 행복하겠지만, 난이도는 역시 알면 알수록 높아집니다.^^
본 글에서 사용된 예제들은 MS에서 사용된 예제들입니다.
제가 구현한 것들이 아님을 알려드립니다.^^
이 세가지를 처리하는 것이 디바이스-로스트 상황에 대처하는 것입니다. Direct2D 의 리소스와 관련한 내용은 http://vsts2010.net/593 글에서 제가 언급했었습니다.^^
그러면 하나씩 살펴보겠습니다. 우리가 진행했던 일반적인 렌더링 작업은 아래와 같습니다.
여기서 우리의 첫번째 단계를 처리합니다. 바로 hr = ::g_ipRT->EndDraw(); 부분입니다. EndDraw()는 렌더링 작업의 결과를 리턴합니다. 이 리턴 값이 D2DERR_RECREATE_TARGET 이면, 바로 디바이스-로스트 상황입니다. 이름에서 유추할 수 있듯이 "에러가 났으니, 다시 생성하라" 입니다.
이번에 제가 사용할 샘플은 바로 이전 시간에 했던 알파이미지를 렌더링하는 샘플입니다. 이 샘플에서 디바이스 의존적인 리소스는 렌더 타겟과 비트맵입니다. 즉 이 샘플에서 디바이스-로스트 상황이 발생한다면, 렌더타겟과 비트맵 리소스는 메모리에서 제거했다가 다시 생성해 주어야 합니다.
그래서 이번 샘플에서는 이들 리소스를 한번에 생성/삭제 하는 함수를 만들었습니다.
그리고 또 하나 고려해야 하는 부분이 있습니다. 바로 윈도우 사이즈의 변경입니다. 이 경우는 디바이스-로스트가 발생하지는 않습니다만, 순간적으로 화면이 깜빡이는 현상을 보이게 됩니다. 이 곳에도 역시 적절한(?) 처리를 해야 합니다.
렌더타겟의 리사이즈 작업이 실패하면, 역시 디바이스 의존적 리소스들을 모두 제거해 버립니다.
이제 WM_PAINT 이벤트를 위와 관련된 작업들과 연계해서 수정해야 합니다.
렌더타겟이 없는 경우는 디바이스-로스트 상황이거나 초기화 상태로 인식하고, 관련된 리소스를 생성합니다. 그리고 렌더타겟의 CheckWindowState()를 통해서 해당 윈도우가 가려져 있는지를 체크하고, 가려져 있지 않다면 렌더링 작업을 수행합니다.
렌더링 작업의 마지막에는 디바이시 로스트 상황을 체크해서 디바이스 의존적 리소스를 제거하고 있습니다. ( 앞서 언급했었죠..^^ )
이제 샘플이 약간은 안정성이 향상되었습니다.^^ 이상으로 디바이스-로스트와 관련한 작업을 마치겠습니다.^^
GPU를 활용하는 일은 모든 개발자에게 열려있는 길이여야 합니다. 하지만 DirectX를 직접적으로 활용해야만 하는 MS의 GPGPU 플랫폼인 DirectCompute는 그렇지가 않습니다.
그래픽카드라는게 원래 특수한 목적성을 가지고 등장한 장치이기 때문에, 이를 활용하는 사람들 또한 특정 영역에 국한되어 있는게 현실입니다. '이제부터 GPGPU 를 적극 활용합시다!' 라고 생각을 하더라도, 실제로 그것을 활용하기 위한 진입 장벽은 굉장히 높을 수 밖에 없습니다.
그러면 어떻게 해야만 이 장벽을 조금이라도 낮출 수 있을까요? 엔비디아의 CUDA 를 보면, 힌트가 있습니다. 하지만 몰라도 상관없습니다.^^ C++ 파일 내에서 컴파일러에 의해서 자동적으로 처리가 될 수 있으면 가장 좋지 않을까요? 순수 C++ 의 기능만 사용해서 컴파일러가 자동적으로 처리해 준다면, 개발자는 DirectX와 ComputeShader 에서 해방될 수 있을 것입니다. 그것이 바로 C++ AMP 가 등장하는 배경입니다. C++ AMP는 다음 버전의 VisualStudio 에 탑재 되어져서 등장할 예정이라고 합니다.
어떤 함수가 아래와 같이 있습니다. void Func( ... ) { 코드 } 위의 함수는 결국 컴파일러에 의해서 CPU 와 관련한 명령어를 생성하게 됩니다. 이를 AMP 적으로 확장하면 정확히 아래와 같이 구성됩니다. void Func( ... ) restrict( cpu ) { 코드 }
restrict 이라는 키워드를 함수에 적용함으로써 간단히 이를 구현합니다. 눈치가 좀 빠르신 분들이라면 '저 cpu를 gpu 로만 변경하면, gpu 로 컴파일 되어지는 것인가?' 라고 생각이 드실 겁니다. 네. 맞습니다. 그것이 바로 C++ AMP 가 DirectCompute 를 구현하는 방법입니다. 정확히는 아래와 같습니다. void Func( ... ) restrict( direct3d ) { 코드 } 'direct3d' 가 바로 'gpu' 를 의미합니다. 현재 이 옵션용 예약어는 확정적인 것은 아닙니다. 'direct3d' 가 확정될 수도 있고, 그렇지 않을 수도 있습니다. 아직 C++ AMP가 출시되지 않아서 유동적인 부분이 있습니다. 그 점 주의해서 읽어주시기 바랍니다.^^
다음 버전의 Visual C++ 부터는
함수마다 저렇게 restrict 한정자에 컴파일 옵션을 지정해주어야 합니다.
물론 지정을 하지 않았을 때는, 디폴트로 restrict( cpu ) 로 자동 처리할 것입니다.
그러면 한 함수 내에서 CPU와 GPU를 활용해야 하는 경우는 어떻게 해야할까요? void Func( ... ) restrict( direct3d, cpu ) { GPU를 사용하는 코드 CPU를 사용하는 코드 }
위와 같이 혼합해서 사용하는 것도 가능합니다.
또한 오버로드와 관련한 이슈도 문제 없이 처리될 것입니다. void Func( ... ); void Func( ... ) restrict( direct3d );
간단히 위와 같이 restrict 만으로 GPU를 사용하는 것이 완전히 된다면 얼마나 좋겠습니까만, restrict( direct3d ) 로 정의되어지는 함수들은 그에 상응하는 규칙으로 코딩 작업을
해야만 합니다.
이것이 사실 그렇게 쉬운 개념만으로 이해할 수 있는 것은 아닙니다.
하지만 DirectCompute를 직접 제어하는 것보다는 쉽습니다.
디바이스 로스트( Device Lost ) 라는 용어에 대해서 친숙한 분도 계실 것이고, 그렇지 않은 분도 계실 것입니다. 아마 Direct3D 를 다루어 보신 분들은 이 용어에 무척이나 친숙할 것입니다.
디바이스 로스트라는 것은 특정 상황을 얘기하는 것입니다. 이 상황은 지금까지 확보하고 있던 메모리 같은 리소스들이 모두 사라져서, 아무 작업도 할 수 없는 상황을 얘기합니다. 즉, 시스템 리소스가 모두 무효화 되어버린 상황이라 할 수 있습니다.
GDI 를 사용해서 렌더링 한다면, 갑자기 HDC 가 비활성화 되어버린 것이라 생각할 수 있습니다. 게임 개발에서 주로 사용하는 Direct3D를 사용하는 경우에는 Alt + Tab 문제라던지, 실시간으로 화면 사이즈를 조절하게 되었을 때 주로 나타납니다. Direct3D로 개발하는 경우에는 예전부터 이 문제에 친숙했기 때문에, 이 상황에 대해서 리소스들을 복구하는 코드를 개발자들이 직접 작성해 주었습니다.
Direct2D도 DirectX의 하나이기 때문에 바로 이 디바이스 로스트 상황이 발생을 합니다. 그래서 개발자들이 이 상황에 대해서 직접 적절한 처리를 해주어야 합니다.
그런데 중요한 것은 현재의 디바이스 로스트 상황이 예전과는 다르다는 것입니다. 이 차이를 설명하려면 디스플레이 드라이버 모델까지 언급을 해야 합니다.
Windows XP 시대와 현재의 Windows 7 시대는 디스플레이 드라이버 모델이 다릅니다. XP 시대는 XPDM 이라는 드라이버 모델을 사용하고 있으며, 윈도우 7 시대에는 WDDM( Windows Display Driver Model ) 라는 모델을 사용하고 있습니다. ( 모니터에 그리기까지의 과정이 다르다고 생각하시면 좋을 것 같습니다.^^ ) 이 모델의 차이가 디바이스 로스트에서도 차이를 만들어 냅니다.
XPDM 은 XP때까지 Windows OS가 발전시켜온 드라이버 모델입니다. 즉, 옛 것을 꾸준히 업데이트 한 결과라 할 수 있습니다. 아무래도 너무 오래되다보니 복잡하고 난해한 구조가 되었고, 그 복잡성 때문에 버그들이 드라이버 내부에 있었다고 합니다. 또한 하드웨어의 발전에도 빠르게 대응하는 것에 굉장한 한계가 있었다고 합니다.
그래서 Windows Vista 때 이 문제를 극복하기 위해서 도입된 새로운 디스플레이 드라이버 모델이 바로 WDDM 입니다. WDDM 은 기존의 복잡성을 최대한 버려서 심플한 구성을 가지도록 설계되었습니다. 기존의 XPDM 이 커널모드에서 대부분의 처리를 수행했던 것을 커널모드와 사용자 모드로 분리를 시켜서 이 디바이스 로스트에 대한 상황에 대한 대응을 용이하게 했습니다.
XPDM의 경우에는 커널에서 문제가 발생하면, 이 때문에 시스템 전체를 다시 시작해야 하는 불상사(?)가 꽤 있었습니다. MS 통계에 따르면 우리가 흔히 보던 블루스크린 상황의 20% 정도가 바로 이 디스플레이 드라이버가 원인이였다고 합니다. 그렇기 때문에 간략화 된 커널 모드의 드라이버를 제공하고, 계산이 복잡한 대부분의 기능은 사용자 모드 드라이버로 이동시킴으로써 문제를 해당 애플리케이션 하나로 국한 시킬 수 있습니다. 이는 블루스크린 상황을 감소시킬 수 있는 하나의 방법이기도 합니다.
XP OS의 디스플레이 드라이버 모델 교체가 어렵기 때문에 WDDM 은 Vista 이상의 OS에서만 지원합니다. 결국 이는 최신의 DirectX 들이 XP OS 에서 정상적으로 작동하지 못하는 가장 근본적인 이유입니다.
WDDM과 XPDM 의 차이는 바로 GPU 활용에 있습니다. XPDM 의 경우에는 GPU 관련 처리를 할 수 없었습니다. 왜냐하면 옛것을 꾸준히 계승시켜 발전시킨 모델이였기 때문에, GPU 처리와 같은 큰 패러다임의 전환은 이 모델 자체를 변경하는 일이기 때문입니다. 현재 Vista 이후의 Windows OS에서는 대부분의 그래픽 작업과 윈도우 관리에 바로 이 GPU가 활용되고 있습니다.
XP 시대에서 DirectX를 사용하는 것은 GPU를 독점적으로 사용하는 작업이였습니다. 만약에 GPU 작업을 실행하는 중에 다른 애플리케이션에서 GPU를 사용하려 한다면, GPU의 제어 권한을 빼앗기게 됩니다. XP 시대에서는 GPU를 여러 애플리케이션에서 동시에 공유할 수가 없었습니다.
하지만 현 세대의 Windows OS에서는 GPU 메모리 관리자가 비디오 메모리를 관리하고 있으며, GPU 스케줄러에 의해서 스케줄을 조정합니다. 우리가 '빠르다' 라고 하는 작업의 뒤에 바로 이런 작업들이 이루어지고 있습니다.
이러한 GPU 활용의 적극적인 도입은 결국 퀄리티의 향상까지 연결됩니다. XP 시대의 XPDM 의 경우에는 GDI로 렌더링 작업을 할 때, 화면에 직접 그리는 개념으로 작업을 했습니다. 그렇기 때문에 윈도우를 이동하거나 사이즈를 조절하게 되면, 화면이 깜빡이는 것을 확인 할 수 있었습니다. ( 티어링과 같은 현상도 확인할 수 있었습니다.^^ ) 또한 이 때 대량의 WM_PAINT 메시지가 발생해서 시스템에 상당한 부하를 주기도 했었습니다. 이는 XPDM 의 경우 모니터 화면에 직접 렌더링 작업을 했었기 때문입니다.
비디오 메모리가 풍족해진 현 세대에서는 오프-스크린 버퍼를 두어서, 화면이 아닌, 다른 버퍼에 렌더링 작업을 수행합니다. ( 흔히들 얘기하시는 더블-버퍼링 기법입니다.^^ )
이러한 것을 담당하는 것이 DWM( Desktop Window Manager ) 입니다. 이는 OS 에서 자동적으로 실행하기 때문에 쉽게 확인할 수 있습니다.
DWM 은 간단히 말해서 DirectX 애플리케이션입니다. DWM 은 비디오 메모리를 만들어서 각 애플리케이션 화면을 모아서 우리에게 화면을 보여주는 역할을 합니다. 그렇기 때문에 DWM 은 오프-스크린 버퍼를 관리하는 기능을 가지고 있습니다.
WDDM 에 대해서 논할 내용도 상당히 많이 있습니다. Vista 시절에는 WDDM 1.0 이였고, Windows 7 에서는 WDDM 1.1 이 사용되고 있습니다. 역시 버전이 업데이트 되면서, 더 좋아진 부분이 있습니다. 1.1 에 관한 개선 사항은 아래의 링크로 대신합니다.^^ http://jacking.tistory.com/442
GPU를 활용한 많은 기능들이 현세대의 Windows OS에 기본적으로 탑재가 되어 있습니다. 그렇기 때문에 디바이스-로스트 같은 GPU 관련한 예외 상황들에 대한 처리를 개발자들이 해주어야 합니다. XPDM의 경우와 개념도 다르고, 발생되는 상황도 다릅니다. Direct2D의 경우 디바이스-로스트란, 그래픽카드가 일정시간 동안 응답을 하지 않을 때를 의미합니다.
여기서 일정 시간이란 기본적으로 2초로 설정되어 있다고 합니다. Direct2D 에서 디바이스-로스트는 잘 발생되지는 않습니다.^^
이번 시간에 다룰 것은 투명 이미지 입니다. 얼마 전 댓글로 문의하신 내용인데 답변을 달기가 조금 부족한 듯 싶어서, 이렇게 별도로 아티클(?)로 남깁니다.
예전에 Win32 API 로 알파가 있는 이미지를 표현하는 작업은 무척 번거로운 작업이였습니다. 이미지 색상에 알파가 고려되어지면, 각 색상 성분마다 알파 연산을 해주어야 합니다. 또한 RGB 각 성분이 8비트씩 사용하는데 A성분이 추가되어지면서, 다시 8비트의 추가 데이터들이 각 색상값들에 필요하게 됩니다. 용량이 커지면 성능에 문제가 생기게 되는 것은 당연한 일입니다.
그래서 이를 흉내내기 위한 대안으로 마련된 것이 ColorKey 라고 불리는 기법입니다. 이 기법은 이미지 내의 특정 색상을 표현하지 않음으로써 구현됩니다. 아래의 그림을 예로 들어보겠습니다.
이미지 내에서 배경이 모두 붉은 색으로 되어있습니다. 이런 경우에 붉은 색을 ColorKey로 지정해서 데이터를 읽지 않는 것입니다. 그러면, 캐릭터 관련 색상만 메모리에 기록되게 됩니다. 이 방법을 사용하면 24비트 비트맵만으로 캐릭터를 표현할 수 있습니다. 주의해야 할 점은, ColorKey 에 해당하는 색상 값을 아티스트들에게 사용하지 말 것에 대한 사전 협의가 있어야 겠지요.
아쉽게도(?) Direct2D에서 이 ColorKey 사용에 대한 API를 찾지 못했습니다. ( Direct3D 에는 있습니다.^^ )
사실 ColorKey 방식이 널리 이용되긴 하지만, 근본적으로 알파 처리를 이용하며 관련 효과를 모두 구현할 수 있습니다. 그렇기 때문에, 굳이 ColorKey 를 염두할 필요성은 없습니다.
이번에 샘플에 사용한 이미지는 알파가 있습니다.
우측의 흑백 이미지가 알파 성분만 표현한 이미지 입니다. 당연히 검은 부분은 알파 성분이 0 이기 때문에 해당 영역은 화면에 표현되지 않을 것입니다.
이번에 살펴볼 샘플은 아래와 같습니다.
똑같은 이미지 파일에서 데이터를 읽었지만, 위의 그림은 알파 처리가 되어서 동물 부분만 출력이 되었습니다. 하지만 아래 부분은 알파처리가 이루어지지 않아서 이미지 영역이 모두 출력되었습니다.
이번 결과의 차이는 WIC를 이용한 것입니다. 우리가 지금껏 무심코(?) 지나쳤던 WIC의 컨버터를 기억하십니까?
제가 이번 샘플을 위해서 약간 개량을 했습니다. 두 사용법에 별 차이는 없지만, GUID_WICPixelFormat... 부분이 보일 것입니다. 이 인자가 바로 두 이미지의 차이를 만들어낸 부분입니다. 즉, 컨버터에서 데이터를 어떤 포맷으로 읽어들일지를 설정하는 부분입니다.
첫번째는 알파처리를 수행하는 방법으로 데이터를 읽어들입니다. GUID_WICPixelFormat32bppPBGRA는 4개의 색상 채널을 가지고 채널당 8개의 비트를 가지고 있으며, 픽셀 당 32비트를 표현하며 UINT로 각 색상이 저장되어 있는 포맷을 의미합니다.
두번째 알파처리를 하지 않는 GUID_WICPixelFormat32bppBGR은 3개의 색상 채널과 8비트로 각 채널을 표현하는 32비트 픽셀 포맷을 의미합니다. 이 포맷도 UINT로 각 색상 성분을 표현합니다. 이 경우에는 알파 채널이 존재하지만, 실제로 읽어들이지 않습니다.
즉, 알파 채널을 무시하는 것입니다.
관련 포맷이 매우 방대하기 때문에, 여기서 더 이상 자세히 다루지 않습니다. 중요한 것은 이 컨버터 덕분에 별다른 수고 없이 알파 처리를 쉽게 수행할 수 있습니다.
Direct2D 가 사실 많은 부분을 우리가 모르게 자동적으로 처리하는 부분이 많이 있습니다. Direct2D에서는 프로퍼티를 생성하는 부분들이 많이 있습니다. 예를 들면 다음과 같은 것들입니다.
사실 이 프로퍼티 정보들을 별도로 설정하지 않으면, 자동적으로 Direct2D에서 처리를 해버립니다. 혹은 디폴트 생성자들이 모두 들어있습니다. 자동적으로 처리되는 부분들 이외의 기능이 필요하다면, 이들을 잘 제어해야만 하겠지요..^^
Direct2D의 변환과 관련된 API는 모두 변환을 수행할 중심점을 함수 인자로 요구를 합니다. 그렇다면, 이 중심점은 스크린 기준에서 정의되어지는 것일까요?
다음과 같은 다람쥐 그림이 있다고 가정해 보겠습니다.
이 그림은 ( 100, 100 ) 의 크기로 그려지기를 원한다고 가정해 보겠습니다.
그리고 우리의 모니터에 ( 300, 200 ) 위치에 그려지기 원하도록 설정하겠습니다. 그러면, 우리에게 변환을 위한 중점을 설정하기 위한 기준 좌표는 어떻게 설정되어야 할까요? 그림의 좌측 상단을 변환의 중점으로 원한다면 ( 300, 200 )으로 설정하면 될까요? 그림의 좌측 상단이 변환의 중점이 된다는 말의 의미를 잘 되새겨 보시기 바랍니다.
그림의 중심을 기준으로 중점을 설정해 두는 것이 훨씬 쉬운 개념으로 변환할 수 있습니다. 즉, 위의 그림에서 변환의 중심이 되는 좌측 상단은 좌표는 ( 0, 0 ) 이 되는 것입니다. 만약, 우측 하단이 변환이 중심이 되길 원한다면 ( 100, 100 )을 설정하면 됩니다.
앞서 살펴 보았던 샘플에는 이 의미를 그냥 넘겼지만 이번에 제공되는 샘플에서는 이를 고려해서 모두 작성했으니, 유심히 살펴보시기 바랍니다.^^
우리는 이 개념을 다음과 같이 지금까지 사용하고 있었습니다.
D2D1_RECT_F 정의의 변수가 보이시나요? 바로 이 dxArea 를 기준으로 변환이 되는 중점을 설정하는 것이 이해하기가 편리합니다.^^
이번 샘플은 지난 번 샘플의 연장에 있습니다. 지난 번 샘플에서 이번 내용과 관련된 부분을 추가 했습니다.
< Scale( 확대/축소 ) >
확대/축소 작업은 1.0을 기준으로 이루어집니다. 1.0 보다 작으면 축소가, 1.0보다 크다면 확대가 이루어 집니다. 이 작업은 D2D1::Matrix3x2F::Scale() API를 통해서 이루어 지는데, 역시나 변환의 중점을 요구합니다.^^
만약 좌측 상단을 기준으로 확대를 하면 다음과 같은 개념입니다.
< Skew( 찌그러뜨리기 ) >
마지막 변환은 찌그러뜨리기 작업입니다. 이는 Matrix3x2F::Skew() API를 통해서 설정할 수 있습니다. 함수 인자로 받는 것은 X축과 Y축의 찌그러질 각도와 역시나 기준이 되는 중점입니다.
역시나 변환이 되는 중점에 따라 결과가 변합니다. 아래의 그림은 30도씩 찌끄러뜨린 결과입니다. 첫번째는 X축만 적용한 것이고, 두번째는 Y축을, 세번째는 X와 Y축 모두를 30도씩 찌그러뜨린 결과입니다.
우리는 항상 렌더링 작업을 하기 전에, 이 작업을 해주었습니다. 이것은 과연 무슨 의미를 가지고 있는 것일까요?
불행하게도(?) Direct2D에서 각종 변환 작업을 위해서는 행렬( Matrix )을 사용합니다. 저는 고등학교 2학년 학기 초에 배웠던 기억이 있습니다. 그 때는 주로 연립 방정식의 해를 구하기 위해서 풀이 방법을 익혔습니다. 아마 대부분 저와 비슷하실 것이라 생각이 듭니다.
왜 뜬금없이 행렬이 등장했는지를 언급하고자 하면, 상당히 고통스럽습니다.^^ 저는 이 행렬에 대한 깊은 이해를 원하지 않습니다. 제가 원하는 것은 이 행렬 관련 API를 이용해서 화면 상에 나오는 오브젝트들의
이동이나 회전 등과 같은 변환을 표현 할 수 있다는 것만 아셨으면 좋겠습니다.^^
Direct2D 에서는 3ⅹ2 행렬을 사용합니다. 행렬의 각 성분들은 아래와 같습니다.
M11
Default : 1.0
M12
Default : 0.0
M21
Default : 0.0
M22
Default : 1.0
M31
OffsetX : 0.0
M32
OffsetY : 0.0
이 행렬은 D2D1_MATRIX_3X2 라는 구조체로 표현됩니다. Direct2D에서는 이 행렬을 손쉽게 다루기 위해서 Matrix3x2F 라는 유틸리티 기능을 지원합니다. 우리는 주로 이 Matrix3x2F 의 기능을 이용해서 변환 정보를 설정할 것입니다. 행렬의 M11, M12, M21, M22 성분은 회전과 확대/축소와 관련된 수치가 입력됩니다. M31과 M32에는 이동과 관련된 수치가 입력됩니다. 행렬의 수치를 통해서 우리가 원하는 변형을 하고자 할 때, 이들 수치를 직접 입력해서 행렬을 제어하기는 무척 어렵습니다.
그래서 Direct2D 에서 제공하는 개발자 편의를 위한 여러 클래스들을 사용해서 이들 수치를 제어합니다.
스크린 좌표계
변환의 이해를 위해서는 좌표계에 대한 이해가 필수적입니다. 우리가 가장 많이 사용하는 좌표계는 당연히 2차원 좌표계입니다. 바로 모니터가 2차원이기 때문입니다. 하지만 이전에도 언급드린 적이 있지만,
컴퓨터 모니터의 좌표계는 우리가 일반적으로 알고 있는 2차원 좌표계가 아닙니다.
좌측이 우리가 흔히 학창시절에 배우는 2차원 데카르트 좌표계( 2D Cartesian coordinate system ) 입니다. 우측이컴퓨터모니터가사용하는스크린좌표계입니다. 이점잘숙지하시고, 다음내용을보시기바랍니다.^^
이번에 제공되는 샘플의 결과는 다음과 같습니다.
이동( Translate )
먼저 살펴 볼 것은 이동( Translate ) 입니다. 가장 위에 있는 이미지가 바로 이동만 적용된 형태입니다. 샘플에서는 단순히 ( X,Y ) 축으로 +20씩만 움직였습니다
D2D1::Matrix3x2F::Translation() 를 통해서 우리가 원하는 이동을 수행하고 있습니다. 쉽죠? 이런 식으로 하면 행렬에 대한 깊은 이해도 필요하지 않을 것이라 생각합니다.^^
회전( Rotate )
두 번째는 회전입니다. 샘플에서는 두 번째 그림과 세 번째 그림이 바로 회전 변환에 대한 결과물입니다. 사실 회전은 행렬의 성분들을 굉장히 복잡한 수치로 변경시킵니다. 왜냐하면 이들 회전 수치들은 삼각함수 값들이기 때문입니다.
하지만, 두려워하지 마시기 바랍니다. Direct2D 에서는 이를 편리하게 수행하기 위해 ::D2D1::Matrix3x2F::Rotation() 를 제공하고 있습니다. 회전 시킬 때, API 에 입력되는 값이 회전량과 회전 중점입니다. 회전량의 경우에는 라디안 값이 아닌, 각도 값이 입력됨을 주의하시기 바랍니다.
즉, Degree 값입니다. 그리고 이 회전량의 경우 음의 값과 양의 값을 모두 입력이 가능합니다. 음의 값이 입력이 되면 시계 반대 방향으로 회전을 수행하게 됩니다. ( 양수면 시계방향으로 회전 합니다. )
또한 회전 중점의 경우도 유의해야 하는데, 아래의 그림을 보시기 바랍니다.
좌측 그림의 경우에는 물체의 중점 위치를 기준으로 회전을 수행한 것입니다. 반면에, 우측 그림의 경우에는 물체의 좌측 상단을 기준으로 회전을 수행한 것입니다. 결과물이 다르죠? 이처럼 회전 변환을 할 때는 이 두 가지를 잘 고려해서 수행해야 합니다.
지금까지 이동과 회전을 각각 수행해 보았습니다. 만약에 이동 작업과 회전 작업이 모두 한 오브젝트에 필요하다면 어떻게 해야 할까요? 그런 경우가 필요하다면 바로 행렬의 곱셈 작업을 적용합니다. 그러면 이 두 가지 작업을 모두 표현 할 수 있습니다. 그런데 이 때 주의 할 부분이 있습니다. 행렬은 교환 법칙이 성립하지 않습니다. 즉 행렬 A, B 가 있을 때, A * B != B * A 라는 것입니다.
이 코드가 우리의 샘플에서 두 번째와 세 번째 그림의 차이를 보여주는 것입니다. 두 번째 그림은 이동을 한 후에 회전 작업을 수행한 것이고, 세 번째 그림은 회전 작업을 수행한 후에 이동 작업을 수행한 것입니다.
실제로 우리가 수행하는 모든 변환은 좌표계 변환입니다. 위에서 우리가 했던 변환은 모두 실제로 오브젝트들의 좌표계를 변환시켜서 얻은 결과물들입니다. 이 개념들은 분명히 이해하기 어려운 부분들입니다. 굳이 이렇게 어려운 부분까지 이해하실 필요는 없습니다.
샘플을 올려드리니, 수치를 바꿔보면서 여러 결과들을 눈으로 확인해 보시기 바랍니다.^^
안녕하세요..^^
DirectX의 컬러키 개념을 얘기하시는 거 같은데,
Direct2D에서는 비슷한 개념으로 간편히 수행할 수 있는 API를 제가 찾지는 못했습니다.
다만, 지금 현재 올라가 있는 샘플들은 자동적으로 알파 색상 처리를 수행합니다.
즉, 알파처리가 된 이미지로 샘플을 실행하시면 원하시는 기능을 확인하실 수 있습니다.
이와 관련한 사항을 조만간 정리해서 올려두겠습니다.
(사실 조금 모호한 부분이 있어서 올리지 못하고 있습니다. )
지금 D2D1을 공부중입니다.
혹시나 해서 질문을 올려보는데요.
RanderTarge->DrawBitmap();//해주고
RanderTarge->SetTransform();//을 해주면 비트맵이 변화가 되서 나옵니다. 그런데 이때 궁금 한점이 여러장의 비트맵을 생성했을 경우 DrawBitmap에는 각각의 비트맵을 선택해서 생성을 할수가 있는데 SetTransform은 일괄적으로 바껴버리네요 이걸 어떻게 해야하는건가요???
너무 멋져요을 개봉된! 나는 필자 전에 이런 걸 배우는 가정 없다. 그래서이 주제에 대한 몇 가지 참신한 아이디어가있는 모든 사람을 찾을 수 좋네요. 정말이 일을 시작 주셔서 감사합니다. 이 웹 사이트는 약간 독창성과 웹, 누군가에 원한의 한 가지입니다. 웹에 새로운 것을 가져다 유용 직업!
이번 시간에는 지난 시간들까지 언급한 내용을 기반으로 해서,
간단한 테셀레이션 작업을 구현해 보려 합니다.
당연한 얘기이겠지만,
하드웨어 기반의 테셀레이션은 하드웨어의 지원이 없으면 매우 느립니다.
즉 DirectX11 이상을 지원하는 그래픽 카드가 아니면,
효과를 눈으로 확인하는 것조차 무척 고통스럽습니다.
그래서 이번 시간에 만들 테셀레이션은 간단히 삼각형 하나를 이용합니다.
우리는 이 삼각형 하나를 가지고 테셀레이션 작업을 수행할 것이며,
DirectX11 을 지원하지 않는 그래픽카드라면
강제적으로 REF 모드로 테셀레이션 작업을 수행하도록 합니다.
먼저 결과 샘플을 보면 아래와 같습니다.
이제 우리가 만들려는 그림이 그려졌으니, 직접 코딩 작업을 시작하겠습니다.
이 글에서는 DirectX11 의 기본 셋팅과 관련한 사항은 생략합니다..^^
자세한 API 적인 설명은 생략을 하니 DirectX 2010 6월 버전의 SDK 의 튜토리얼을 참고하시거나,
'알코코더의 DirectX11' 을 참고하시기 바랍니다.^^
우리가 이번 샘플에서 사용할 버텍스 데이터의 형식은 위치 정보만 있으면 됩니다.
이번 샘플에서는 최대한 간단하게 작성하는 것을 목적으로 했기 때문에,
많은 정보를 필요로 하지는 않습니다..^^
그래서 아래와 같이 간단한 버텍스 형식을 정의했습니다..^^
생소한 데이터 타입이 보입니다. 바로 XMFLOAT3 입니다. DirectX11 부터는 D3DX 계열의 수학 데이터 타입들은 더 이상 업데이트 되지 않습니다.
지금부터는 XNA Math 라는 수학 라이브러리를 사용합니다.
그렇다고 더 이상 D3DX 계열의 수학 데이터 타입들을 사용할 수 없는 것은 아니니, 안심하시기 바랍니다.
이들에 대해서는 향후 언급할 기회가 있으니,
지금은 D3DX 계열의 수학 클래스 대신에 XNA Math 라는
새로운 수학 클래스를 사용한다는 정도로만 인식하고 넘어가겠습니다.^^
아래는 우리가 애플리케이션 전역으로 사용할 변수들의 선언입니다.
그 동안의 DirectX11을 언급하면서 꾸준히 언급되던 내용이기에 자세한 설명은 생략하겠습니다.
특이할 만한 것이라면, 래스터라이져 스테이트 오브젝트를 2개 만드는 것입니다.
이는 우리의 샘플이 솔리드( Solid ) 한 렌더링과 와이어프레임( Wire-Frame ) 기반의 렌더링으로
전환이 가능하기 때문입니다.
다음은 상수버퍼( ConstantBuffer ) 에 관한 전역 선언들 입니다.
우리는 월드 좌표계의 정점을 버퍼에 입력할 것입니다.
그래서 View-Projection 행렬만 변환을 위해서 필요합니다.
그리고 얼마나 테셀레이션 작업을 세밀하게 할지를 결정하는 상수를 하나 추가합니다.
쉐이더를 컴파일 해주는 보조 함수를 다음과 같이 하나 만듭니다.
이제 본격적으로 시작을 합니다.
InitD3D() 에 각종 초기화 작업을 수행합니다.
앞서 잠깐 언급드렸듯이,
DirectX11을 지원하는 하드웨어가 아니면, 강제로 REF 모드로 동작하도록 합니다.
또한 이 함수에서는 각 쉐이더 스테이지에 대응되는 HLSL 코드를 컴파일 해줍니다.
그리고 이들에 대한 각 오브젝트를 만듭니다.
초기화 작업은 주로 반복적인 작업이 많기 때문에, 설명은 생략합니다.
InitD3D() 에 버텍스버퍼의 데이터를 설정해 줘야 합니다.
이번 샘플에서는 월드 좌표로 정의된 삼각형을 사용할 것입니다.
또한 카메라 공간에 대한 설정도 같이 해 줍니다.
이들에 대한 코드는 아래와 같습니다.
이 정도로 초기화와 관련된 작업을 마무리 합니다.
이제는 프레임 관련한 처리를 작성합니다.( Render() )
이 Render() 부분에서는 상수버퍼에 설정할 데이터들을 다음과 같이 업데이트 합니다.
우리는 와이어프레임 모드와 솔리드 모드의 렌더링 방식 둘 다를 표현할 것이기에,
이들에 대한 설정도 아래와 같이 고려해 주어야 합니다.
그리고 마지막으로 입력되는 버텍스 형식을 알려주고 버텍스 버퍼를 연결한 후에,
그리기 작업을 수행합니다.^^
이제 키보드 이벤트에 따라 약간의 변화를 주는 작업을 합니다.
현재는 'w' 키로 렌더링 모드를 Wire 와 Solid 간의 토글이 되도록 설정합니다.
그리고 위/아래 방향키로 테셀레이션의 분할 정도를 증감합니다.
이번 작업은 여기까지 입니다.
지금까지 DX11을 살펴보면서, 언급된 내용들이 대부분이라 전체적으로 설명드리지는 않습니다.
( HLSL 코드도 최대한 간결하게 작성했습니다..^^ )
샘플을 같이 첨부드리니, 직접 작성하시면서 익혀보시기 바랍니다.^^
렌더타겟의 EndDraw() 함수가 D2DERR_RECREATE_TARGET 를 리턴하는 경우가 언제인가요? 제가 윈도우 사이즈도 바꿔보고, 화면 해상도도 바꿔봤는데 발생하지 않는 것 같습니다. 풀스크린 / 윈도우 모드를 전환하는 방법이나, D3D9의 디바이스 소실과 같은 경우의 처리도 궁금해요. (이런게 있는지 없는지도 궁금하지만, 그나마 D2DERR_RECREATE_TARGET 에러의 케이스가 디바이스 소실과 가장 비슷한 예외상황인거 같네요.)
중급자 이상의 내용들도 공유해 주세요 ~
제가 단순한 fillrect와 drawline 그리고 drawtext를 GDI를 통해서 그리고 있는데 이 draw에서 프로그램 병목현상이 심하게 생겨서 고심하던중..
이 블로그에서 GPU, Directx의 글들을 보고 용기를 내어 Directx적용(후엔 TTB적용 예정)해서 속도를 개선해보고자 하였으나 오히려 엄청 더 느려지더군요.
SetText를 하기 위해서 begindraw와 enddraw이 두 함수에서 엄청 엄청 무지하게 느리더라구요.
단순 2D Draw시에는 아무 효과가 없나요?
너무 멋져요을 개봉된! 나는 필자 전에 이런 걸 배우는 가정 없다. 그래서이 주제에 대한 몇 가지 참신한 아이디어가있는 모든 사람을 찾을 수 좋네요. 정말이 일을 시작 주셔서 감사합니다. 이 웹 사이트는 약간 독창성과 웹, 누군가에 원한의 한 가지입니다. 웹에 새로운 것을 가져다 유용 직업!
이제 테셀레이션 작업의 마지막 단계입니다. 테셀레이터를 통한 결과와 Hull Shader 단계에서의 결과인 패치 데이터가
Domain Shader 의 입력으로 전달이 되게 됩니다.
Domain Shader 에서는
우리가 DX9 세대에서 주로 수행했던 Vertex 변환 작업을 수행하게 됩니다.
즉, 실제적으로 Projection 변환까지 Domain Shader 단계에서 이루어지게 됩니다.
테셀레이션 작업만으로는 폴리곤의 퀄리티를 향상시키는데에 효과적이지 못합니다.
그래서 실제적으로 높이 정보를 포함하고 있는 텍스쳐를 사용하게 되는데,
이 텍스쳐를 사용하는 것을 'Displacement mapping' 이라고 합니다.
아래의 그림을 살펴보도록 하겠습니다.
첫번째 모델은 로우 폴리곤으로 제작된 것입니다.
두번째가 바로 테셀레이션 작업이 종료된 폴리곤 모델입니다.
( 아주 미끄러운 캐러멜 같은 느낌이지요? ^^ )
세번째는 테셀레이션 작업을 마치고, Displacement mapping 까지 마친 모델입니다.
Displacement mapping 의 특징은 실제 Vertex 데이터를 조작하는 것에 있습니다.
위의 작업을 단순하게 표현해 보면 아래의 그림과 같습니다.
수년 전부터 가장 많이 쓰이고 있는 Bump mapping 기법은 우리의 눈을 속이기 위한 방법이라면,
Displacement mapping 은 눈속임이 아니라, 실제로 데이터를 조작하는 텍스쳐 기법입니다.
아래의 그림은 이들에 대한 차이점을 질감적으로 잘 표현해 주고 있습니다. ^^
Bump mapping의 경우에는 실제로 평면이지만,
텍스쳐링 효과를 이용해서 우리에게 질감의 느낌을 전달해 주고 있습니다.
반면에 Displacement mapping은 실제 Vertex 데이터를 조작해서 질감의 느낌을 전달합니다.
Displacement mapping 의 설명에 제가 열을 올리는 이유는
바로 이 작업 Domain Shader 단계에서 계산되어서 적용되기 때문입니다.
앞서 설명했듯이, Displacement mapping 은 실제 Vertex 를 조작하는 기법이기 때문에
최종 Vertex 를 생성하는 Domain Shader 에서 적용되는 것이 당연한 일입니다.^^
이 작업 자체가 어려운 일은 당연히 아닙니다.
하지만, Displacement mapping을 고려하게 됨으로써,
Vertex 변환 단계가 조금 복잡해 진 것은 사실입니다.
텍스쳐링의 단계를 수행하고, 그 텍셀의 수치만큼 실제 Vetex를 조정해 주어야 하기 때문입니다.
거기다, 쉐이더 단계에서 LOD 레벨을 고려해야하기 때문에
쉐이더의 Texture 멤버함수로 SampleLevel() 를 이용하는 상황이 생기기도 할 것입니다.
또 하나의 더 큰 효과를 위한 하나의 방법이 늘었다고 생각하시면 더 좋을 것 같습니다.^^
너무 멋져요을 개봉된! 나는 필자 전에 이런 걸 배우는 가정 없다. 그래서이 주제에 대한 몇 가지 참신한 아이디어가있는 모든 사람을 찾을 수 좋네요. 정말이 일을 시작 주셔서 감사합니다. 이 웹 사이트는 약간 독창성과 웹, 누군가에 원한의 한 가지입니다. 웹에 새로운 것을 가져다 유용 직업!
DirectX나 OpenGL의 경우 3D기반 렌더링을 기반으로 하다보니 선처리나, 텍스쳐 처리에서 제약사항이 발생하는 경우도 많더군요.
또한 시스템 그래픽 리소스 부분에서 관리 난이도도 조금 올라가는 부분도 없지 않아 있는것 같습니다.
결국 프로그래머 입장에서는 난이도가 약간이나마 올라가는 부작용도 있는 것 같습니다. GDI가 내부적으로 GPU를 통해서 빠르게 동작할 수 있으면 좋겠지만, 성향의 차이로 인해 쉽지는 않겠지요...
갑자기 궁금해 져서 질문 하나 드립니다. ^^
쿼드코어 CPU와 448 코어를 가지고 있다는 NVIDIA 의 Tesla에 대하여 아래와 같이 말해도 문제가 없는건지요?
쿼드코어 CPU는 4개의 ALU를 가지고 있고 Tesla는 448개의 ALU를 가지고 있다...
이렇게 말하면 문제가 있는지요?
테셀레이터는 Hull Shader 의 결과를 입력으로 받아서 작업을 합니다. 이 스테이지는 프로그래머가 제어할 수 없는 영역입니다.( 정말 다행이죠? ^^ )
앞선 Hull Shader 스테이지에서 정의된 폴리곤 분할 방법과 분할 수치에 따라서
실제로 Vertex 데이터들을 생성할 수 있는 정보를 주게 됩니다.
즉, 우리는 큰 덩어리 형태의 Vertex 데이터만 HullShader 를 통해서 전달할 뿐입니다. 테셀레이터의 정해진 연산에 의해서,
도메인 쉐이더( DomainShader )에 무게 중심 좌표( BarycentricCoordinates )들을 전달하게 됩니다.
< 무게 중심 좌표( BarycentricCoordinates ) >
무게 중심 좌표를 언급하기 전에, 벡터의 외적의 성질에 대해서 언급할 사항이 있습니다.
우리가 이미 알고 있듯이, 두 벡터의 외적 연산으로 두 벡터에 수직인 벡터를 구할 수 있습니다.
지금부터 여기에 주목할 것은 이렇게 외적 연산을 통해서 얻어진 벡터의 길이입니다.
이렇게 구해진 벡터의 길이는 기하학적으로 두 벡터를 평행사변형을 만들었을 때, 넓이를 의미합니다.
아래의 그림이 이해에 도움이 되었으면 좋겠습니다.^^
꽤 재미있는 성질이지 않습니까? ^^
( 이미 다들 알고 계셨을 것이라 생각하지만, 처음 접했을때, 저는 꽤 재미있는 성질이라고 생각했습니다..^^ )
두 벡터의 외적으로 나온 결과 벡터의 길이가 평행사변형의 넓이라는 사실을 인지한다면,
우리는 이제 무게 중심 좌표에 대해서 얘기할 수 있습니다.
힌트를 드리면, 무게 중심 좌표는 다른 말로 면적 좌표로도 불리기도 합니다.
삼각형 내부의 임의의 점 P는 점 A,B,C를 구성하는 삼각형들의 비율로 표현할 수 있습니다.
위의 그림에서 나오는 공식과 그림은 바로 이를 보여드리고 있습니다.
w들은 가중치 상수를 의미합니다.
각 가중치들의 합은 반드시 1.0 이여야 합니다.
만약 C의 가중치인 w3 의 경우에는 삼각형 APB 의 넓이 / 삼각형 ABC의 넓이 가 되는 것입니다.
이런 식으로 서로 대응되는 각 가중치들을 삼각형을 구성하는 각각의 정점 위치에 대응시키면,
우리가 원하는 P의 위치를 구할 수 있습니다.
벡터 외적의 기하학적 특징을 이용해서 가중치를 구하는 코드는 아래와 같습니다.
이 코드에서는 삼각형 넓이를 구할 때 수행하는 2를 나누는 작업이 생략되어 있습니다.
이유는 어차피 이 코드의 결과는 비율에 대한 가중치이기 때문에, 2를 나누는 작업은 의미가 없기 때문입니다.
이처럼 무게중심좌표를 구하는 일이 DirectX11의 테셀레이터의 임무 중 하나입니다. 삼각형을 구성하는 세 정점이 주어졌을 때 세 정점의 가중치를 구할 수 있다면,
임의의 점 P를 구할 수 있습니다. 바로 이 역활을 수행하는 것이 테셀레이터의 기능 중 하나입니다.
앞선 언급했듯이 테셀레이터의 기능은 우리가 조작 할 수 있지 않습니다.
즉, 고정 기능입니다.
우리는 Hull Shader를 통해서 Patch를 정의하고,
이렇게 정의된 패치 데이터는 이후에 가공되지 않고, 바로 Domain Shader 에서도 사용됩니다.
( 테셀레이터에서도 이 데이터를 사용해서 연산을 합니다. ) 테셀레이터 단계에서는 이 패치 데이터에 대응되는 가중치들을 구성해서,
바로 다음 단계인 Domain Shader 로 전달하게 되는 것입니다.
물론 내부적으로는 더욱 복잡한 과정을 거치겠지만,
우리가 코딩관점에서 관심을 가질 수 있는 변수 정보는 이들 뿐입니다.
질문 있습니다.
vecResult.z를 구하는 코드를 보면 ::D3DXVec3Cross( &vecCross, &a2b, &a2c); 이렇게 되어 있는데 ::D3DXVec3Cross( &vecCross, &a2p, &a2c); 이게 맞지 않나요?
a2b 와 a2c 외적을 구해서 길이를 구하게되면 rTotalArea 과 vecResult.z 값이 서로 같아지는것 같은데요.
너무 멋져요을 개봉된! 나는 필자 전에 이런 걸 배우는 가정 없다. 그래서이 주제에 대한 몇 가지 참신한 아이디어가있는 모든 사람을 찾을 수 좋네요. 정말이 일을 시작 주셔서 감사합니다. 이 웹 사이트는 약간 독창성과 웹, 누군가에 원한의 한 가지입니다. 웹에 새로운 것을 가져다 유용 직업!
우리가 그래픽스에서 사용하는 폴리곤은 굉장히 복잡한 방식으로 처리가 됩니다.
많은 스테이지를 통해서 결국 우리는 화면 픽셀로 변환된 최종 결과를 확인하게 되는 것입니다.
그 과정 속에서 Direct3D 9에서는 Vertex와 Pixel 을 조작할 수 있도록 변화되어 왔습니다.
Direct3D 10 은 여기에 Geometry 까지 조작할 수 있도록 프로그래머들에게 개방되었습니다.
Direct3D 11 은 무려 3개의 스테이지가 추가되었습니다.
Hull Shader, Tessellator, Domain Shader 가 바로 그것들입니다.
이 중에 프로그래머가 제어하는 부분은 Hull / Domain Shader 이며,
Tessellator 의 경우에는 하드웨어가 직접 처리하게 됩니다.
테셀레이션을 언급하면서 가장 많이 나오는 주제는 현재 LOD( Level of Detail ) 처리 이지만,
정확하게 테셀레이션이 필요한 이유는 글인 http://vsts2010.net/331 을 통해서 확인할 수 있습니다.
현재 그래픽 파이프라인에서 테셀레이션 작업은 현재 옵션으로 설정되어 있습니다.
여러분이 이 기능을 사용하기 원하지 않는다면, 이들을 활성화 시키지 않으시면 됩니다.
그렇게 된다면, 기존의 파이프라인과 동일한 방식으로 Vertex 데이터를 처리하게 됩니다.
< Hull Shader >
Hull Shader 는 테셀레이션 작업의 시작입니다.
하지만, 실제로 프로그래머의 시작은 Vertex Shader 입니다.
DirectX9에서 VertexShader 는 World-View-Projection 변환을 수행하는 것이 가장 큰 목적이였습니다. DirectX11에서 VertexShader 의 목적은 Hull Shader 로의 데이터를 전달하는 것입니다.
즉, 테셀레이션이 목적인 경우에는 DirectX11에서 VertexShader 스테이지에서 World-View-Projection 을 수행해서는 안됩니다.
테셀레이션 작업시 VertexShader 에서 처리되는 Vertex는 실제 우리가 사용하는 데이터가 아닙니다.
우리는 VertexShader 의 입력으로 들어오는 데이터를 모아서,
많은 수의 Vertex를 새롭게 생성시켜야 합니다.
그래서 테셀레이션 작업시 VertexShader 스테이지에서는 Vertex를 월드 변환까지만 수행합니다.
Hull Shader 에서는 '폴리곤을 어떻게 분할할 것인가?' 와 '폴리곤을 얼마나 분할할 것인가?' 를 결정합니다.
가장 단순한 형태로 이 Hul Shader의 기능을 표현하면 다음과 같습니다.
위의 그림은 MSDN 의 그림입니다.
Hull Shader 는 두 가지의 작업을 동시에 수행합니다.
그것은 제어점( Control Point ) 를 생성하는 작업과 Patch Constant Data 를 계산하는 작업입니다.
이들 작업은 병렬적으로 수행되게 됩니다.
HLSL 코드는 실제로 드라이버 수준의 하드웨어 명령어를 생성하게 되는데,
이 때, 병렬처리가 가능한 형태로 변환되게 됩니다.
이는 Hull Shader 가 빠르게 동작할 수 있는 중요한 이유이기도 합니다.
Hull Shader 의 입력으로 들어오는 제어점( Control Point )들은
낮은 차수의 면을 표현하는 정점들입니다.
이를 높은 차수의 면을 표현하는 제어점들로 만들어 내게 됩니다.
이 때 생성된 제어점들은 Tessellator 스테이지에서 사용되는 것이 아니라,
그 다음 스테이지인 Domain Shader 에서 사용됩니다.
위의 그림은 베지어(Bezier) 제어점들을 이용해서 베지어 곡면을 표현한 것입니다.
근본적으로 테셀레이션은 평면을 곡면으로 생성시키는 개념과 매우 비슷합니다.
( 굳이 평면을 많은 갯수의 폴리곤으로 표현할 필요는 없기 때문이겠죠. )
그렇기 때문에, 분할 방법으로 사용되는 알고리즘들은 베지어처럼 게임 프로그래머들에게 친숙한
개념들이 사용됩니다.
Hull Shader 의 또 하나의 중요한 역활은 불필요한 연산을 줄이기 위해
테셀레이션 단계를 스킵할지를 결정할 수 있다는 것입니다.
즉, Hull Shader 에서 Tessellation Factor 가 0 이하인 경우에
이 패치는 컬링되어 버린 것으로 간주됩니다.
( Tessellation Factor 는 얼마나 분할할지를 나타내는 수치적 비율입니다. )
이로인해 더 이상 파이프라인 처리가 이루어지지 않음으로써,
성능 향상을 도모할 수 있습니다.
( 폴리곤을 처리하지 않는 것이 가장 큰 성능의 이득이겠죠..^^ )
그러면 과연 Hull Shader 에서의 '폴리곤을 어떻게 분할할 것인가?' 와 '폴리곤을 얼마나 분할할 것인가?'
프로그램 코드에서는 어떻게 표현해야 할까요?
현재 MSDN 에 나와있는 Hull Shader 의 가장 단순한 형태는 다음과 같습니다.
( 물론 실제로 구현되고 동작되는 내용들의 예들은 DirectX11 샘플에 있습니다. )
// Insert code to compute Output here.
return Output;
}
위의 Hull Shader 는 동작 방식을 설정합니다.
몇몇 정의된 값들을 셋팅해 주면, 이는 테셀레이션 작업을 하는 동안에 사용되게 됩니다.
즉, 위의 셋팅들은 '폴리곤을 어떻게 분할할것인가?' 에 준하는 프로그램 코드라 할 수 있습니다.
이제 남은 것은 '폴리곤을 얼마나 분할할 것인가?' 입니다.
이는 PatchConstantFunc 을 통해서 병렬적으로 처리된다고 앞서 설명을 했습니다.
이곳에서는 Tessellation Factor 를 계산하게 되는데, 그 결과에 따라서 컬링 작업이 실행됩니다.
( 이 값이 0 이하의 경우에는 더 이상 처리가 필요하지 않습니다. )
이 작업을 하는 함수를 우리는 직접 작성해서,
위의 [patchconstantfunc("SubDToBezierConstantsHS")] 처럼 설정해 주면 자동적으로 동작합니다.
MSDN 에 나와있는 PatchConstantFunc의 기본적인 형태는 다음과 같습니다.
지금까지 Hull Shader의 기본적인 개념과 역활에 대해서 언급해 드렸습니다.
이렇게 얻어진 결과는 테셀레이터로 전달되게 됩니다.
세부적인 Hull Shader 의 작성은 이후의 시간들을 통해서 살펴볼 예정입니다.
( 현재 본 글들은, 개념 위주의 설명에 포커스를 두고 있습니다. ^^ )
렌더타겟에 렌더링을 하기위해서는 뷰포트의 설정도 필요합니다. 뷰포트라는 것은 렌더타겟의 렌더링 영역에 관한 설정입니다. 렌더타겟에 넓이와 높이, 그리고 깊이값으로 렌더링 영역을 설정할 수 있습니다. 그렇기 때문에 뷰포트는 각 렌더타겟별로 설정합니다. 즉 8개의 렌더타겟이 있으면, 뷰포트 설정 역시 8개를 설정해야 합니다. 뷰포트는 특별한 인터페이스가 있는 것이 아니라, 값을 설정하는 것이기 때문에 뷰포트 구조체에 값을 채우고, 렌더타겟에 세팅해주기만 하면 됩니다.
Diagram of the viewport rectangle
뷰포트는 'ID3D11DeviceContext::RSSetViewports 메소드'로 설정합니다. RS는 래스터라이저(RS) 스테이지를 의미합니다. 즉 파이프라인에서 레스터라이저 스테이지의 설정입니다. 뷰포트의 설정에는 'D3D11_VIEWPORT 구조체'를 사용합니다. 이전의 DX10과는 달리 값들을 실수값으로 지정합니다.
스크린좌표는 왼쪽위가 (0,0)이고, Y축이 아래쪽 방향인 좌표계입니다. 쉽게 이야기해서 우리가 일반적으로 모니터 화면에서 보는 좌표와 방향이 같습니다. 다만 해상도로 계산하는 것이 아니라 0~1사이의 값으로 높이와 넓이를 계산하는 것입니다. 그래서 백버퍼의 왼쪽위가 (0,0)이 되고, 오른쪽 하단이 (1,1)이 되는 좌표계가 됩니다.
뷰포트 변환의 공식은 아래와 같습니다.
X = (x+1) * Viewport.Width * 0.5 + Viewport.TopLeftX
Y = (1-y) * Viewport.Height * 0.5 + Viewport.TopLeftY
Z = Viewport.MinDepth + z * (Viewport.MaxDepth – Viewport.MinDepth)
레스터라이저는 렌더타겟의 뷰포트의 내부에 있는 프리미티브(3D모델)로부터 픽셀을 생성합니다. 깊이버퍼의 값은 뷰포트의 깊이값의 범위로 스케일링됩니다.
예를 들어, 처음에 깊이값의 범위를 0.0~0.5로 렌더링하고, 다음에 0.5~1.0로 렌더링하게되면, 처음에 렌더링했던 내용이 뒤에 렌더링했던 내용보다도 화면 앞에 렌더링 됩니다. (깊이 버퍼가 유효할 때)
뷰포트의 깊이값은 일반적으로 보통 0.0~1.0사이로 설정합니다.
파이프라인에 렌더타겟이 1개가 설정되어 있을 때, 뷰포트를 렌더타겟의 (0,0) ~ (640,480)의 영역으로 설정하는 코드는 아래처럼 됩니다.
void RSSetViewports(
[in] UINT NumViewports,
[in] const D3D11_VIEWPORT *pViewports
);
NumViewports
설정할 뷰포트의 개수
pViewports
뷰포틀 설정할 뷰포트 구조체의 배열
D3D11_VIEWPORT 구조체
typedef struct D3D11_VIEWPORT {
FLOAT TopLeftX;
FLOAT TopLeftY;
FLOAT Width;
FLOAT Height;
FLOAT MinDepth;
FLOAT MaxDepth;
} D3D11_VIEWPORT;
TopLeftX
뷰포트의 좌상단 X좌표.
범위는 D3D11_VIEWPORT_BOUNDS_MIN ~ D3D11_VIEWPORT_BOUNDS_MAX
TopLeftY
뷰포트의 좌상단 Y좌표.
범위는 D3D11_VIEWPORT_BOUNDS_MIN ~ D3D11_VIEWPORT_BOUNDS_MAX
댓글을 달아 주세요
용량의 메모리 블럭의 의미에 더 가깝다는 것을 알