Search

[미리보는 C++ AMP-3] array와 array_view

DirectX 11 2011. 12. 21. 08:00 Posted by 알 수 없는 사용자

들어가기 앞서 지금까지 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 에서 가져왔습니다 )

vector<int> data(5);
for (int count = 0; count < 5; count++)
{
    data[count] = count;
}

array<int, 1> a(5, data);

parallel_for_each(
    a.grid,
    [=, &a](index<1> idx) restrict(direct3d)
    {
        a[idx] = a[idx] * 10;
    }
);

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 부터 텍스쳐 인터페이스는 꼭 이미지 데이터를 의미하지 않습니다.
대용량의 메모리 블럭의 의미에 더 가깝다는 것을 알아두시기 바랍니다.
텍스쳐의 개념을 사용하기 때문에 동시에 여러 데이터에 접근이 가능하고,
랜덤 액세스도 가능한 것입니다.^^



픽셀쉐이더
정점쉐이더에서와 마찬가지로 전체 소스코드부터 보여드립니다.

sampler2D DiffuseSampler;

struct PS_INPUT
{
   float2 mTexCoord : TEXCOORD0;
};

float4 ps_main( PS_INPUT Input ) : COLOR
{
   float4 albedo = tex2D(DiffuseSampler, Input.mTexCoord);
   return albedo.rgba;
}

 

픽셀쉐이더 입력데이터 및 전역변수
이제 픽셀쉐이더를 살펴보기로 하죠. 픽셀쉐이더에서 할 일은 텍스처 이미지에서 텍셀(texel)[각주:1]을 구해와 그 색을 화면에 출력하는 것이겠군요. 그렇다면 텍스처로 사용할 이미지와 현재 픽셀의 UV 좌표가 필요하겠죠? 텍스처 이미지는 픽셀마다 변하는 값이 아니므로 전역변수로, UV 좌표는 정점쉐이더로부터 보간기를 거쳐 들어온 입력데이터가 되겠네요. 우선 픽셀쉐이더 입력데이터의 구조체부터 만들겠습니다.

struct PS_INPUT
{
   float2 mTexCoord : TEXCOORD0;
};


어라? 별 다른 게 없네요? VS_OUTPUT 구조체를 가져와 mPosition을 지워버린 것 뿐이군요. 사실 픽셀쉐이더의 입력데이터는 정점쉐이더의 출력데이터와 일치할 수밖에 없습니다. 어차피 정점쉐이더에서 반환한 값을 가져오는 거니까요.

다음은 텍스처를 선언할 차례군요. 앞서 렌더몽키 프로젝트를 설정할 때 DiffuseSampler 라는 이름의 텍스처 개체를 만들었던 거 기억하시죠? 바로 이 개체가 텍셀을 구할 때 사용하는 텍스처 샘플러입니다. 따라서 HLSL 코드에서 사용하는 텍스처 샘플러의 이름도 DiffuseSampler 여야 합니다.

sampler2D DiffuseSampler;


sampler2D는 HLSL에서 지원하는 데이터형 중에 하나로 2D 텍스처에서 텍셀 하나를 구해오는데 사용합니다. 이 외에도sampler1D, sampler3D, samplerCUBE 등의 샘플러가 있습니다.

이제 픽셀쉐이더 함수를 작성해보죠.


픽셀쉐이더 함수
우선 헤더부터 보겠습니다.

float4 ps_main( PS_INPUT Input ) : COLOR
{


이전과 달라진 점이라면 PS_INPUT형의 Input 매개변수를 받는다는 것뿐이군요. 보간기가 계산해준 UV 좌표 값을 받아오기 위해서입니다. 자, 이제 UV 값과 텍스처 샘플러가 있으니 텍셀 값을 구하는 일만 남았군요. tex2D라는 HLSL 내장함수를 사용하시면 매우 쉽게 이런 일을 할 수 있습니다. tex2D는 첫 번째 매개변수로 텍스처 샘플러를,두 번째 매개변수로 UV 좌표를 받습니다. 

    float4 albedo = tex2D(DiffuseSampler, Input.mTexCoord);


위 코드는 DiffuseSampler에서 Input.mTexCoord 좌표에 있는 텍셀을 읽어옵니다. 그 값은 albedo라는 변수에 저장되겠네요. 이제 이 값을 가지고 무슨 일을 해야 할까요? 으음.... 텍스처를 그대로 보여주는 게 목적이니까 그냥 반환하면 되겠네요.

   return albedo.rgba;
}


이제 F5키를 눌러 정점쉐이더와 픽셀쉐이더를 각각 컴파일 한 뒤, 미리 보기 창을 보면...... 엉망이군요?!. 왜일까요? 그것은 정점 버퍼에서 올바른 UV  좌표 값을 불러오도록 설정을 하지 않았기 때문입니다. Workspace 패널 아래에서 Stream Mapping을 찾아 마우스를 더블클릭하세요. POSITION이라는 항목만 있는 거 보이시죠? 이제 Add 버튼을 눌러 새 항목을 추가한 뒤 Usage를 TEXCOORD로 바꿉니다. Index가 0이고 Data Type이 FLOAT2로 되어있는지도 확인하세요. Attribute Name은 굳이 손 안대셔도 됩니다. 이제 OK버튼을 누르면 다음 그림과 같이 제대로 된 지구본을 보실 수 있을 것입니다.


그림 3.6. 그럴듯해 보이는 지구본


 

근데 albedo란 변수를 반환할 때 return albedo;라고 하지 않고 return albedo.rgba;라고 한 것 보이시죠? 사실 return albedo;라고 해도 전혀 상관은 없지만 뭔가 새로운 것을 보여 드리기 위해 일부러 저렇게 썼습니다.

HLSL에서는 벡터형 변수 뒤에 xyzw나 rgba 등의 접미사를 붙이는 방법을 사용하여 벡터의 성분들에 매우 쉽게 접근할 수 있습니다. 예를 들어, float4를 4개의 요소를 가진 float 배열(즉, float[4])라고 본다면 x나 r은 첫 번째 요소를, y나 g는 두 번째 요소를, z나 b는 세 번째 요소를, w나 a는 네 번째 요소를 가리킵니다. 예를 들어서 위의 albedo에서 rgb값만을 가져오고 싶다면 

float3 rgb = albedo.rgb;


라고 하시면 됩니다. 하지만 이에 그치지 않습니다. 이들 접미사의 순서를 마음대로 뒤섞어 새로운 벡터를 만들 수도 있습니다.예를 들어 r, g, b채널의 순서를 뒤바꾸고 싶다면 

float4 newAlbedo = albedo.bgra;


이라고 하시면 됩니다. 심지어는 다음과 같이 r채널만 세 번 반복할 수도 있습니다.

float4 newAlbedo = albedo.rrra;


매우 멋지지 않나요? 이렇게 rgba나 xyzw를 이용해서 마음대로 순서를 바꿔가면서 벡터의 성분에 접근하는 것을 스위즐(swizzle)이라고 합니다.

자, 그러면 연습도 할 겸 스위즐을 이용해서 방금 만들었던 지구본의 빨강채널과 파랑채널을 뒤바꿔보는 것은 어떨까요? 누워서 떡 먹기죠? ^_^

선택사항: DirectX 프레임워크
이제 C++로 작성한 DirectX 프레임워크에서 쉐이더를 사용하시고자 하는 분들을 위한 선택적인 절입니다.

우선 '제2장: 진짜 쉬운 빨강쉐이더'에서 사용했었던 프레임워크의 사본을 만들어 새로운 폴더에 저장합니다. 그 다음, 렌더몽키에서 사용했던 쉐이더와 3D 모델을 DirectX 프레임워크에서 사용할 수 있도록 파일로 저장합니다. Sphere.x와 TextureMapping.fx라는 파일이름을 사용하도록 하겠습니다. 이제 렌더몽키에서 사용했던 earth.jpg라는 텍스처 파일도 복사해옵니다. 이 파일은 렌더몽키의 설치디렉터리를 보시면 \Examples\Media\Textures 폴더 안에 있습니다.

우선 전역변수들을 살펴봅시다. '제2장: 진짜 쉬운 빨강쉐이더' 사용했던 쉐이더 변수의 이름이 gpColorShader였었군요. 이것을 gpTextureMappingShader로 바꿉시다. 

// 쉐이더
LPD3DXEFFECT            gpTextureMappingShader = NULL;


지구 텍스처를 메모리에 저장할 때 사용할 텍스처 포인터도 하나 선언합니다.

// 텍스처
LPDIRECT3DTEXTURE9      gpEarthDM              = NULL;


이제 CleanUp() 함수로 가서 여기서 선언했던 D3D자원들을 해제하는 코드도 추가해야겠군요. 이래야 훌륭한 프로그래머이신 거 아시죠? gpColourShader의 이름을 변경하는 것도 잊지 맙시다.



     // 쉐이더를 release 한다.
     if ( gpTextureMappingShader )
     {
         gpTextureMappingShader->Release();
         gpTextureMappingShader = NULL;
     }

     // 텍스처를 release 한다.
     if ( gpEarthDM )
     {
         gpEarthDM->Release();
         gpEarthDM = NULL;
     }


이제 텍스처와 쉐이더를 로딩해 보겠습니다. 당연히 LoadAssets() 함수에서 로딩을 해야죠.

일단 쉐이더 변수의 이름과 쉐이더 파일의 이름을 각각 gpTextureMappingShader와 TextureMapping.fx로 변경합니다.

     // 쉐이더 로딩
     gpTextureMappingShader = LoadShader("TextureMapping.fx");
     if ( !gpTextureMappingShader )
     {
         return false;
     }


그리고 이전에 만들어 두었던 LoadTexture() 함수를 이용해서 earth.jpg 파일을 로딩합니다.

     // 텍스처 로딩
     gpEarthDM = LoadTexture("Earth.jpg");
     if ( !gpEarthDM )
     {
        return false;
     }


이제 렌더링을 담당하는 RenderScene() 함수를 살펴보도록 하죠. 일단 gpColorShader 변수명이 쓰이는 곳이 많네요. 이것을 모두 찾아 gpTextureMappingShader로 변경합시다.

텍스처매핑 쉐이더에서 새로 추가한 전역변수가 하나 있었죠? 바로 텍스처 샘플러입니다. 그런데 D3D 프레임워크에서 쉐이더에 텍스처를 대입해줄 때, 곧바로 샘플러에 대입해주는 게 아니라 텍스처 변수에 대입해줘야 합니다. 렌더몽키에서 DiffuseSampler말고 DiffuseMap이라고 불리던 텍스처가 있었죠? 이게 바로 텍스처 변수입니다. 그럼 DiffuseMap이란 이름의 쉐이더변수에 텍스처를 대입해주면 될 것 같죠? 사실 그래야 정상인데 렌더몽키가 자기 멋대로 텍스처 변수의 이름을 바꾸더군요. -_-;; TextureMapping.fx 파일을 메모장에서 열어보시면 아실 겁니다. 코드를 쭉 보다 보면 sampler2D DiffuseSampler바로 위에 texture 데이터형으로 선언된 변수가 하나 보일 겁니다? 자기 맘대로 _Tex 접미사를 붙여놨군요. 나쁜 원숭이 같으니라고...

texture DiffuseMap_Tex


뭐 불평해봐야 뭐 달라질게 있겠습니까? 그냥 이 변수명을 사용해서 텍스처를 대입해주도록 합시다. 쉐이더에 텍스처를 대입할 때는 SetTexture()함수를 사용합니다. 이 함수는 SetMatrix함수와 마찬가지로 쉐이더 내부의 변수명을 첫 번째 매개변수로 받습니다.

     gpTextureMappingShader->SetTexture("DiffuseMap_Tex", gpEarthDM);


자, 이제 프로그램을 컴파일 한 뒤 실행시켜 보세요. 렌더몽키에서 봤던 것과 동일한 결과를 보실 수 있죠? 근데, 이 지구가 천천히 회전하면 더 괜찮겠는걸요? 그럼 지구를 빙그르르 돌리는 코드를 추가해보죠.

일단, 현재 회전 값을 기억할 전역변수를 하나 추가합니다.

// 회전값
float gRotationY = 0.0f;


물체의 회전과 위치 등의 정보는 월드행렬의 일부가 됩니다. 따라서 RenderScene()함수로 다시 돌아와 월드행렬을 만드는 코드를 이렇게 바꾸겠습니다.

    // 프레임마다 0.4도씩 회전을 시킨다.
    gRotationY += 0.4f * PI / 180.0f;
    if ( gRotationY > 2 * PI )
    {
        gRotationY -= 2 * PI;
    }

     // 월드행렬을 만든다.
     D3DXMATRIXA16 matWorld;
     D3DXMatrixRotationY(&matWorld, gRotationY);


이 코드가 하는 일은 프레임마다 회전 각도를 0.02도씩 추가하고, 현재 회전 각도에 따라 회전행렬을 만들어서 그것을 월드행렬로 사용합니다. 현재 사용하시는 컴퓨터의 사양에 따라 이 회전 값이 너무 빠르거나 느릴 수도 있습니다. 본인의 컴퓨터에 맞게 적절히 값을 조정하세요.[각주:2]

자, 이제 다시 코드를 실행해보면 자전을 하는 지구의 모습을 볼 수가 있죠?

정리
다음은 이 장에서 배운 내용을 짧게 요약해 놓은 것입니다.
  • 텍스처매핑을 하려면 UV 좌표가 필요하다.
  • UV 좌표는 각 정점 상에 정의된 가변 값이다.
  • 픽셀쉐이더가 정점데이터를 이용하려면 정점쉐이더의 도움이 필요하다.
  • 정점쉐이더가 출력하는 값은 모두 보간기를 거친다.
  • tex2D함수를 이용하면 쉽게 텍스처를 샘플링할 수 있다.


고급쉐이더 기법 중에 텍스처매핑을 사용하지 않는 기법은 없다고 해도 과언이 아닐 정도로 텍스처매핑은 쉐이더 프로그래밍에 없어서는 안 될 존재입니다. 다행히도 HLSL에서 텍스처매핑이 그리 어렵진 않으니 잘 익혀두시기 바랍니다.

수고하셨습니다.


p.s. 여기는 댓글 달릴때마다 이메일이 오지 않아서 답변을 빨리빨리 드리기가 쉽지 않군요. 제 개인 블로그에 포스팅 된 버전에 댓글을 다시는게 제일 답변이 빠릅니다. 오탈자 및 잘못된 내용 수정도 제일 빠릅니다.


포프 올림


 
  1. 그림(picture)의 최소구성단위가 픽셀(pixel)인 것처럼 텍스처(texture)의 최소구성단위가 텍셀(texel)입니다. [본문으로]
  2. 실제 게임에서는 지난 프레임 이후 경과한 시간에 따라 회전량을 계산하는 게 옳은 방법입니다. 여기서 보여 드리는 코드는 쉐이더 데모를 위한 것이므로 그냥 이 정도로 놔두겠습니다. [본문으로]
샘플파일 받기: 


제3장 텍스처매핑

이 장에서 새로 배우는 HLSL
  • sampler2D - 텍스처에서 텍셀을 구해올 때 사용하는 샘플러 데이터형
  • tex2D() - 텍스처 샘플링에 사용하는 HLSL 함수
  • 스위즐(swizzle) - 벡터 성분의 순서를 마음대로 뒤섞을 수 있는 방법



저번 장에서 배운 내용 어떠셨나요? 너무 쉬었다고요? 실제 게임에서 별 쓸모가 없어 보인다고요? 네, 사실 그렇습니다. 저번 장의 주 목적은 실습을 통해 HLSL의 기초 문법을 배우는 것이었습니다. 보통 프로그래밍 책에서 헬로월드(hello world) 예제를 처음에 드는 것과 마찬가지 이치죠. 그럼 이번 장에서는 그보다 조금 더 쓸모가 있는 내용을 배워볼까요? 물체를 단색으로 출력하는 대신에 표면에 텍스처(texture)[각주:1]를 입혀보는 게 어떨까요? 이걸 보통 텍스처매핑(texture mapping)이라고 부른다는 것쯤은 다 아시죠?

텍스처매핑과 UV 좌표
3D 물체를 이루는 구성요소는 삼각형이라고 이전에 말씀드렸습니다. 정점 3개로 삼각형을 만들 수 있다는 것도요. 그렇다면 삼각형 위에 이미지를 입히려면 어떻게 해야 할까요? '이 삼각형의 왼쪽 꼭짓점에 저 이미지의 오른쪽 귀퉁이 픽셀을 출력할 것'과 같은 지시를 내릴 수 있어야겠죠?[각주:2] 삼각형은 이미 정점 3개로 이루어져 있으니 각 정점을 텍스처 위에 있는 한 픽셀에 대응시켜 주면 되겠군요. 그럼 텍스처 위에서 한 픽셀을 어떻게 가리킬까요? 텍스처란 결국 이미지 파일이니까 'x = 30, y = 101에 있는 픽셀'이라는 식으로 정의하면 될까요? 만약 이렇게 정의를 해버리면 나중에 이미지 파일의 크기를 2배로 늘리면 이것을 다시 x = 60, y = 202로 바꿔야겠네요. 별로 바람직하지 않죠?

저번 장에서 배운 내용을 떠올려보니 색상을 표현할 때도 비슷한 이야길 했던 것 같군요. 그 때, 채널의 비트 수에 상관없이 통일적으로 색상을 표현하려면 어떻게 해야 한다고 했죠? 모든 값을 백분율(0~1)로 표현한다고 했죠? 여기서도 똑같은 방법을 사용합니다.  x = 0이 텍스처의 제일 왼쪽 열을, x = 1은 제일 오른쪽 열을 나타낸다고 하면 되겠죠? 마찬가지로 y = 0은 텍스처의 제일 처음 행을, y = 1은 마지막 행을 나타냅니다. 참고로 텍스처매핑을 사용할 때는 XY대신에 UV를 사용하는 게 보통입니다. 특별한 이유는 없고 그냥 위치를 표현할 때 흔히 xy를 사용하니 그와 혼돈을 피하기 위해서 입니다. 이것을 그림으로 표현하면 다음과 같습니다.

 

그림 3.1. 텍스처의 UV 좌표






이제 다양한 UV 좌표를 대입하면 어떻게 결과가 달라지는지는 몇 가지 예를 들어보도록 하죠. 역시 그림으로 보면 이해가 쉽겠죠?

그림 3.2 다양한 텍스처매핑의 예


 
(a) 아직 텍스처를 입히지 않은 두 삼각형입니다. 정점 v0, v1, v2와 v0, v2, v3가 각각 삼각형을 하나씩 이루고 있군요.
(b) UV 좌표의 범위가 (0,0) ~ (1,1)입니다. 텍스처를 전부 다 보여줍니다.
(c) UV 좌표의 범위가 (0,0) ~ (0.5, 1)입니다. 따라서 텍스처의 왼쪽 절반만을 모여줍니다. 0.5가 백분율로는 50%니까 딱 중간인 거 맞죠?
(d) UV 좌표의 범위가 (0,0) ~ (0.5, 0.5) 이군요. 따라서 이미지의 왼쪽 절반과 위쪽 절반만을 보여줍니다.
(e) UV 좌표의 범위가 (0,0) ~ (1,2)니까 텍스처를 위아래로 두 번 반복을 해줘야겠네요.[각주:3] 
(f) UV 좌표의 범위가 (0,0) ~ (2,2)니까 텍스처를 위아래로 두 번, 그리고 좌우로 두 번 반복해줍니다.[각주:4]란 시맨틱이 바로 그것입니다. UV 좌표 데이터를 삽입한 뒤의 정점쉐이더 입력데이터는 아래와 같습니다.

struct VS_INPUT
{
    float4 mPosition : POSITION;
    float2 mTexCoord : TEXCOORD0;
};


TEXCOORD뒤에 0을 붙인 이유는 HLSL에서 지원하는 TEXCOORD 수가 여럿이기 때문입니다. 쉐이더에서 여러 개의 텍스처를 동시에 사용할 때, 둘 이상의 UV 좌표를 사용할 경우가 있는데 그럴 때에는 TEXCOORD0, TEXCOORD1등으로 시맨틱을 사용하시면 됩니다.

정점쉐이더 출력데이터
우선 '제2장: 진짜 쉬운 빨강쉐이더'에서 사용했던 정점쉐이더 출력데이터를 가져와 봅시다.

struct VS_OUTPUT 
{
    float4 mPosition : POSITION;
};


여기에 다른 정보를 추가해야 할까요? '제2장: 진짜 쉬운 빨강쉐이더'에서 설명해 드리지 않았던 내용 중 하나가 정점쉐이더는 위치정보 외에도 다른 정보들을 반환할 수 있다는 것입니다. 정점쉐이더가 위치정보를 반환하는 이유는 래스터라이저가 픽셀들을 찾아낼 수 있도록 하기 위해서였습니다. 하지만, 위치 이외의 다른 정보를 반환하는 이유는 래스터라이저를 위해서가 아닙니다. 이는 오히려 픽셀쉐이더를 위해서입니다. 텍스처매핑에 필요한 UV 좌표가 그 좋은 예입니다. 

픽셀쉐이더는 정점 버퍼 데이터에 직접적으로 접근을 못 합니다. 따라서, 픽셀쉐이더에서 사용해야 할 정점데이터가 있다면(예, UV 좌표), 그 데이터는 정점쉐이더를 거쳐 픽셀쉐이더에 전달돼야 합니다. 좀 쓸데없는 제약 같다고요? 다음의 그림을 보시면 왜 이런 제약이 붙어 있는지를 알 수 있으실 것입니다.

그림 3.4. 과연 저 픽셀의 UV 좌표 값은 무엇일까?




UV 좌표가 정의된 장소는 각 정점입니다. 하지만 위 그림에서 볼 수 있듯이 픽셀의 UV 좌표는 정점의 UV 좌표와도 다른 것이 대부분입니다.[각주:5] 따라서 이 픽셀의 올바른 UV 값을 구하는 방법은 현재 위치에서 세 정점까지의 거리를 구한 뒤 그 거리의 비율에 따라 세 UV 값을 혼합하는 것이겠지요. 하지만 이런 혼합을 직접해줄 필요는 없습니다. 정점쉐이더에서 출력한 위치 정보를 래스터라이저가 알아서 처리해줬듯이 정점 이외의 기타 정보는 보간기(interpolator)라는 장치가 알아서 혼합해줍니다. 그럼 '제1장: 쉐이더란 무엇이죠?'에서 보여드렸던 GPU 파이프라인에 보간기를 추가해보죠. 그림 3.5가 되겠습니다.

그림 3.5. 보간기까지 추가했지만 여전히 너무 간략한 3D 파이프라인




참고로 보간기가 보간[각주:6]을 하는 것은 UV 좌표만이 아닙니다. 정점쉐이더가 반환하는 어떤 값이든 보간기는 보간을 해서 픽셀쉐이더에 전달해줍니다.

자, 그럼 이제 정점쉐이더에서 UV 좌표값도 반환해야 한다는 사실, 이해하시겠죠? 추가합시다.

struct VS_OUTPUT 
{
    float4 mPosition : POSITION;
    float2 mTexCoord : TEXCOORD0;
};



전역변수
'제2장: 진짜 쉬운 빨강쉐이더'에서 사용했던 것 이외에 별도로 필요한 전역변수는 없습니다. 따라서 별다른 설명 없이 코드만 보여 드리겠습니다.

float4x4 gWorldMatrix;
float4x4 gViewMatrix;
float4x4 gProjectionMatrix;


정점쉐이더 함수
누누이 말씀드리지만 정점쉐이더의 가장 중요한 임무는 정점의 위치를 투영공간으로 변환시키는 것입니다. 이 코드는 '제2장: 진짜 쉬운 빨강쉐이더'의 쉐이더에서 사용했던 것과 똑같습니다.

VS_OUTPUT vs_main( VS_INPUT Input )
{
   VS_OUTPUT Output;

   Output.mPosition = mul( Input.mPosition, gWorldMatrix );
   Output.mPosition = mul( Output.mPosition, gViewMatrix );
   Output.mPosition = mul( Output.mPosition, gProjectionMatrix );


이제 UV 좌표를 전달해 줄 차례군요. Output 구조체에 UV 좌표를 대입하기 전에 공간변환을 적용해야 할까요? 그렇지 않습니다. UV 좌표는 여태까지 다뤘던 3차원 공간에 존재하는 게 아니라 삼각형의 표면상에 존재하기 때문입니다. 따라서 아무 변환 없이 UV 좌표를 전달해 줍니다.

   Output.mTexCoord = Input.mTexCoord;


더는 처리할 데이터가 떠오르지 않는군요. 이제 Output을 반환하면서 이 함수를 마무리 짓겠습니다.

   return Output;
}



p.s. 여기는 댓글 달릴때마다 이메일이 오지 않아서 답변을 빨리빨리 드리기가 쉽지 않군요. 제 개인 블로그에 포스팅 된 버전에 댓글을 다시는게 제일 답변이 빠릅니다. 오탈자 및 잘못된 내용 수정도 제일 빠릅니다.


포프 올림



  1. 3D 그래픽에서는 이미지를 사용하여 표면의 색감 및 질감(texture)을 표현합니다. 여기서 사용하는 이미지들을 텍스처라고 부릅니다. [본문으로]
  2. 이렇게 다른 두 점을 서로 대응시키는 것을 영어로 매핑(mapping)이라고 합니다. [본문으로]
  3. UV 좌표가 0~1 범위 밖에 있을 때 이것을 처리하는 방법에는 여러 가지가 있습니다. 위에서 든 예는 랩(wrap, 반복) 모드를 사용할 때만 올바릅니다. 이 외에도 미러(mirror, 거울)라던가 클램프(clamp, 비반복) 모드도 있습니다. [본문으로]
  4. [/footnote]

이 외에도 UV 좌표의 범위를 (1,0) ~ (0,1)로 하면 텍스처의 좌우를 뒤집는 등의 효과도 줄 수 있습니다. 이 정도면 어떻게 UV 좌표를 지정해야 원하는 결과를 얻을 수 있는 지 대충 아시겠죠? 이 정도면 실제로 텍스처매핑 쉐이더를 작성할 준비가 된 것 같군요.

기초설정
  1. '제2장: 진짜 쉬운 빨강쉐이더'에서 했던 것과 마찬가지로 렌더몽키 안에서 새로운 DirectX 이펙트를 만든 뒤, 정점쉐이더와 픽셀쉐이더 코드를 삭제합니다.
  2. 이제 쉐이더의 이름을 TextureMapping으로 바꿉니다.
  3. 정점의 위치를 변환할 때 사용할 gWorldMatrix, gViewMatrix, gProjectionMatrix를 추가하는 것도 잊지 맙시다. 변수 시맨틱을 이용해서 실제 데이터를 전달해 주는 방법도 기억하시죠?
  4. 이제 텍스처로 사용할 이미지를 추가할 차례입니다. TextureMapping 쉐이더에 오른쪽 마우스 버튼을 누른 뒤, Add Texture > Add 2D Texture > 렌더몽키 설치폴더\examples\media\textures\earth.jpg 파일을 선택합니다. Earth라는 이름의 텍스처가 추가되었을 겁니다. 
  5. 이 텍스처의 이름을 DiffuseMap으로 변경합니다.
  6. 이제 Pass 0위에 마우스 오른쪽 버튼을 누른 뒤, Add Texture Object > DiffuseMap을 선택합니다. Texture0 이란 이름의 텍스처 개체가 추가되었죠?
  7. 이제 Texture0의 이름을 DiffuseSampler로 바꿉니다.
  8. 이 모든 설정을 마치셨다면 Workspace 패널이 다음 그림처럼 보일 겁니다.

그림 3.3. 기초설정을 마친 렌더몽키 프로젝트


 



정점쉐이더
일단 전체 소스코드부터 보여드린 뒤, 한 줄씩 차근차근 설명해드리겠습니다.

struct VS_INPUT
{
   float4 mPosition : POSITION;
   float2 mTexCoord : TEXCOORD0;
};

struct VS_OUTPUT
{
   float4 mPosition : POSITION;
   float2 mTexCoord : TEXCOORD0;
};

float4x4 gWorldMatrix;
float4x4 gViewMatrix;
float4x4 gProjectionMatrix;


VS_OUTPUT vs_main(VS_INPUT Input)
{
   VS_OUTPUT Output;
   
   Output.mPosition = mul(Input.mPosition, gWorldMatrix);
   Output.mPosition = mul(Output.mPosition, gViewMatrix);
   Output.mPosition = mul(Output.mPosition, gProjectionMatrix);
   
   Output.mTexCoord = Input.mTexCoord;
   
   return Output;
}


정점쉐이더를 살펴보기 전에 텍스처매핑을 하려면 어떤 데이터가 새로 필요한지 생각해봅시다. 일단 당연히 텍스처로 사용할 이미지 하나가 필요하겠죠? 그렇다면 텍스처를 입히는 작업을 어디에서 해야 할까요? 정점쉐이더일까요? 아니면 픽셀쉐이더일까요? 각 쉐이더가 실행되는 시점을 생각해보면 이에 대한 대답을 쉽게 구할 수 있습니다. 정점쉐이더는 각 정점마다 실행이 된다고 말씀드렸었죠? 근데 텍스처는 어디에 입히죠? 정점에 입히는 게 아니라 표면을 구성하는 모든 픽셀에 입혀야 하죠? 따라서 정점쉐이더에서 하기엔 뭔가 부족할 듯 싶군요. 정점쉐이더와는 달리 픽셀쉐이더는 각 픽셀마다 호출이 되니까 당연히 픽셀쉐이더에서 텍스처매핑을 해야겠군요. 자, 그럼 이미지는 텍스처로 사용할 테니 정점쉐이더에서 선언해줄 필요가 없네요.

그럼 이 외에 다른 정보가 필요할까요? 바로 위에서 말씀드렸었는데 말이죠. 그렇습니다. UV 좌표가 필요하지요. UV 좌표를 어디에 지정했었죠? 각 정점마다였죠? 따라서 UV 좌표는 전역변수가 아니라 정점데이터의 일부로 전달됩니다. 자~ 그럼 이 점을 염두에 두고 정점쉐이더의 입출력 데이터를 살펴보도록 합시다.

정점쉐이더 입력데이터
'제2장: 진짜 쉬운 빨강쉐이더'에서 사용했던 입력데이터의 구조체를 일단 가져와보도록 하지요.

struct VS_INPUT
{
    float4 mPosition : POSITION;
};



자, 이제 여기에 UV 좌표를 추가해야겠죠? UV 좌표는 u하고 v로 나뉘니까 데이터형은 float2가 되겠네요. 그렇다면 어떤 시맨틱을 사용해야 할까요? 위치정보가 POSITION이라는 시맨틱을 가졌듯이 UV 좌표도 자신만의 시맨틱을 가지겠죠? TEXCOORD[footnote]텍스처좌표(texture coordinate)의 줄임말입니다. [본문으로]
  • 픽셀의 위치가 정점과 일치하는 경우에는 UV 좌표가 같습니다. [본문으로]
  • 보간(interpolate)이란 단어가 잘 이해 안 되시는 분들은 그냥 위에서 설명해 드렸다시피 '인접한 세 정점까지의 거리에 비례하여 값을 혼합하는 것'이라고 이해하세요. [본문으로]
  • MSDN 두 번째 세미나 입니다.
    한번쯤 엔터프라이즈 시스템 프로젝트에 참여해 보신 분이라면 좋은 노하우를 많이 얻어가실 수 있을겁니다.

    엔터프라이즈 시스템의 개발부터 이어지는 소프트웨어 공학적인 접근과 이를 실제 구현하기 위한 애자일 개발 기법을 배우실 수 있습니다.

    관심있는 분들은 많이 신청해 주세요.
     
     




    픽셀쉐이더
    자, 이제 픽셀쉐이더를 작성해 볼 차례입니다. 정점쉐이더에서 했던 것과 마찬가지로 렌더몽키의 Workspace에서 Pixel Shader를 찾아 더블클릭합니다. 그리고 그 안에 있는 코드를 모두 지웁니다. 실제로 코드를 한 줄씩 쳐보셔야 실력이 늡니다. ^^ 꼭 코드를 다 지우세요.

    이제 정점쉐이더에서 그랬던 거와 마찬가지로 전체코드를 보여드린 뒤, 한 줄씩 살펴보기로 하죠.

    float4 ps_main() : COLOR
    {   
       return float4( 1.0f, 0.0f, 0.0f, 1.0f );
    }



    픽셀쉐이더의 가장 중요한 임무는 픽셀의 색을 반환하는 것입니다. 현재 저희가 만드는 쉐이더기 빨강쉐이더니까 그냥 빨간색을 반환하면 되겠죠? 그렇다면 빨간색을 RGB값으로 어떻게 표현할까요? RGB(255, 0, 0)이 제일 먼저 떠오르시나요? 흠... 그렇다면 픽셀쉐이더 코드를 작성하기 전에 다음 절을 먼저 보셔야겠습니다.

    색의 표현방법
    빨간색을 RGB로 표현하라고 하면 (255, 0, 0)을 먼저 떠올리시는 이유는 RGB의 각 채널을 8비트로 저장하는 경우가 대부분이기 때문입니다. 8비트를 정수로 표현하면 총 256개의 값(2∧8 = 256)을 표현할 수 있습니다. 이 값을 0부터 시작하면 0 ~ 255가 되므로 각 채널의 최대값이 255이 되는 거지요. 근데 8비트가 아니라 5비트로 각 채널을 표현하면 어떻게 될까요? 2∧5 = 32이므로 31이 최대 값이 되지요. 따라서 8비트 이미지에서 빨간색은 (255, 0, 0) 이지만 5비트 이미지에서의 빨간색은 (31, 0, 0)이라는 찹찹한 결과가 생기고 마네요?

    그러면 비트 수에 상관없이 통일적으로 색을 표현할 방법은 없을까요? 아마 포토샵에서 HDR 이미지를 다뤄보신 분들이라면 이미 그 답을 알고 계실 듯 하네요. 바로 백분율(%)을 사용하면 되지요. 백분율을 사용하면 비트 수에 상관없이 빨간색의 RGB값이 언제나 (100%, 0%, 0%)가 됩니다. 이게 바로 쉐이더에서 색상을 표현할 때 사용하는 방법입니다. 백분율을 그냥 유리수로 나타내면 0.0 ~ 1.0이 되니까 쉐이더에서 빨간색의 RGB값은 (1.0, 0.0, 0.0)이 됩니다.

    픽셀쉐이더 함수
    자, 그럼 이젠 어떤 RGB 값을 반환해야 할지도 알아봤으니 픽셀쉐이더 함수를 작성할 일만 남았군요. 픽셀쉐이더 함수의 헤더부터 살펴봅시다.

    float4 ps_main() : COLOR
    {


    이 헤더가 의미하는 바는 다음과 같습니다.
    • 이 함수의 이름은 ps_main이다.
    • 이 함수는 매개변수를 받지 않는다.
    • 이 함수의 반환형은 float4이다.
    • 이 함수의 반환 값을 백 버퍼의 색상(COLOR)값으로 처리할 것.

    여기서 딱히 추가로 설명해 드릴 부분은 반환 값의 데이터형으로 float3가 아니라 float4를 쓴다는 정도입니다. 4번째 값은 알파 채널로 보통 투명효과를 나타내는 용도로 쓰이곤 합니다.[각주:1]

    자, 그럼 이 함수 안에선 무슨 일을 해야 했었죠? 그렇죠. 빨간색을 반환해야죠. 이렇게 코드를 짜면 됩니다.

       return float4( 1.0f, 0.0f, 0.0f, 1.0f );
    }


    여기서 특별히 설명드릴 것은 float4(r, g, b, a)라는 형태로 float4 벡터를 새로 생성한다는 것과 알파 채널의 값이 1.0(100%)이므로 픽셀이 완전히 불투명 하다는 정도 입니다. 이제 쉐이더 편집기 안에서 F5키를 눌러 정점쉐이더와 픽셀쉐이더를 각각 컴파일 하면 미리 보기 창에서 다음과 같은 빨간색 공을 보실 수 있을 겁니다.

    팁: 렌더몽키에서 쉐이더를 컴파일 하는 법
    렌더몽키에서는 정점쉐이더와 픽셀쉐이더를 별도로 컴파일 해줘야 합니다. 편집기에서 각 쉐이더를 불러온 뒤 F5를 눌러주세요. 미리 보기 창이 열릴 때도 두 쉐이더가 모두 컴파일 됩니다.


    그림 2.7. 처음으로 만들어본 빨강쉐이더!



    정말 간단한 쉐이더였죠? 여기서 빨간색 대신 파란색을 보여주려면 어째야 할까요? float4(0.0, 0.0, 1.0, 1.0)을 반환하면 되겠죠? 노란색은요? 노란색은 연두색과 빨간색을 섞은 거니까.... 음.... 제가 굳이 답을 알려드리지 않아도 아시죠?

    이제 이 렌더몽키 프로젝트를 잘 저장해 두세요. 각 장이 끝날 때마다 렌더몽키 프로젝트를 저장해 두시기 바랍니다. 나중에 다른 장에서 다시 이용할 거거든요.

    선택사항: DirectX 프레임워크
    이제 C++로 작성한 DirectX 프레임워크에서 쉐이더를 사용하시고자 하는 분들을 위한 선택적인 절입니다.

    우선 '제1장: 쉐이더란 무엇이죠?'에서 만들었던 프레임워크의 사본을 만들어 새로운 폴더에 저장합니다. 각 장마다 프레임워크를 따로 저장하는 이유는 다른 장에서 이 프레임워크를 가져다가 코드를 추가할 예정이기 때문입니다.

    다음은 렌더몽키에서 사용했던 쉐이더와 3D 모델을 DirectX 프레임워크에서 사용할 수 있도록 파일로 저장할 차례입니다.

    1. Workspace 패널에서 ColorShader를 찾아 오른쪽 마우스 버튼을 누릅니다.
    2. 팝업메뉴에서 Export > FX Exporter를 선택합니다.
    3. 위에서 DirectX 프레임워크를 저장했던 폴더를 찾아 그 안에 ColorShader.fx란 이름으로 파일을 저장합니다.
    4. 이제 Workspace 패널에서 Model을 찾아 오른쪽 마우스 버튼을 누릅니다.
    5. 팝업메뉴에서 Save > Geometry Saver를 선택합니다.
    6. 역시 DirectX 프레임워크가 있는 폴더 안에 Sphere.x란 이름으로 파일을 저장합니다.

    이제 비주얼 C++ 에서 프레임워크의 솔루션 파일을 연 뒤, 다음의 코드들을 차례대로 추가해보도록 하죠. ShaderFramework.cpp 파일을 열겠습니다.

    우선, 투영행렬을 만들 때 필요한 상수들을 #define으로 정의하겠습니다.

    #define PI           3.14159265f
    #define FOV          (PI/4.0f)                     // 시야각
    #define ASPECT_RATIO (WIN_WIDTH/(float)WIN_HEIGHT) // 화면의 종횡비
    #define NEAR_PLANE   1                             // 근접 평면
    #define FAR_PLANE    10000                         // 원거리 평면


    이제 Sphere.x하고 ColorShader.fx 파일을 로딩해서 메모리에 저장해둘 때 사용할 포인터 2개를 선언합니다.

    // 모델
    LPD3DXMESH        gpSphere        = NULL;

    // 쉐이더
    LPD3DXEFFECT      gpColorShader   = NULL;


    이제 모델과 쉐이더 파일을 로딩해야겠죠? '제1장: 쉐이더란 무엇이죠?'에서 속을 비워두었던 LoadAssets()함수 안에 다음의 코드를 추가할 때로군요.

        // 쉐이더 로딩
        gpColorShader = LoadShader("ColorShader.fx");
        if ( !gpColorShader )
        {
            return false;
        }

        // 모델 로딩
        gpSphere = LoadModel("sphere.x");
        if ( !gpSphere )
        {
            return false;
        }


    위 코드는 '제1장: 쉐이더란 무엇이죠?'에서 미리 구현해 두었던 LoadShader() 함수와 LoadModel() 함수를 호출해서 파일들을 로딩한 뒤, 그 중에 하나라도 NULL 포인터이면 로딩에 실패했다는 의미로 false를 반환합니다. 이렇게 로딩에 실패한 경우 비주얼 C++의 출력 창에 에러메시지가 있을 테니 살펴보시기 바랍니다.

    새로운 D3D 자원을 로딩할 때 마다 이를 해제하는 코드를 추가하는 습관을 기르도록 합시다. GPU 상의 메모리 누수를 막기 위해서입니다. CleanUp() 함수에서 D3D를 해제하기 바로 전에 다음의 코드를 삽입하겠습니다.

        // 모델을 release 한다.
        if ( gpSphere )
        {
            gpSphere->Release();
            gpSphere = NULL;
        }

        // 쉐이더를 release 한다.
        if ( gpColorShader )
        {
            gpColorShader->Release();
            gpColorShader = NULL;
        }



    이제 사전작업은 모두 끝났으니 마지막으로 쉐이더를 이용해서 물체를 그리기만 하면 됩니다. 3D 물체를 그리는 코드는 RenderScene()에 넣기로 했었죠? RenderScene() 함수로 갑니다.

    // 3D 물체 등을 그린다.
    void RenderScene()
    {


    쉐이더 안에서 전역변수들을 사용했던 것 기억하시나요? 렌더몽키에서는 변수 시맨틱을 통해 이 값들을 대입해줬지만 여기서는 직접 이 값들을 만들어서 쉐이더에 전달해 줘야 합니다. 우선 뷰행렬부터 만들어 볼까요?

        // 뷰 행렬을 만든다.
        D3DXMATRIXA16 matView;
        D3DXVECTOR3 vEyePt(    0.0f, 0.0f, -200.0f ); 
        D3DXVECTOR3 vLookatPt( 0.0f, 0.0f,  0.0f );
        D3DXVECTOR3 vUpVec(    0.0f, 1.0f,  0.0f );
        D3DXMatrixLookAtLH( &matView, &vEyePt, &vLookatPt, &vUpVec );


    위에서 볼 수 있듯이 카메라의 현재 위치와 카메라가 바라보는 곳의 위치, 그리고 카메라의 위쪽을 가리키는 벡터만 있으면 D3DXMatrixLookAtLH() 함수를 호출하여 뷰행렬을 만들 수 있습니다. 여기서는 카메라가 현재 (0, 0, -200)에 위치해 있고 (0, 0, 0)을 바라보고 있다고 가정합니다. 실제 게임에서는 카메라 클래스로부터 이 정보를 가져와서 뷰행렬을 만드는 것이 정석입니다.

    다음은 투영행렬을 만들 차례입니다. 투영행렬은 원근투시법(perspective projection)을 사용하느냐 직교투시법(orthogonal projection)을 사용하느냐에 따라 사용할 함수와 매개변수들이 달라집니다. 여기서는 원근투시법을 사용하므로 D3DXMatrixPerspectiveFOVLH() 함수를 사용하겠습니다. 

        // 투영행렬을 만든다.
        D3DXMATRIXA16 matProjection;
        D3DXMatrixPerspectiveFovLH( &matProjection, FOV, ASPECT_RATIO, NEAR_PLANE,
            FAR_PLANE );


    이제 월드행렬을 만들어 보겠습니다. 사실 월드행렬은 한 물체의 위치와 방위, 그리고 확장/축소 변환을 합친 것입니다. 따라서 뷰행렬 및 투영행렬과 달리 각 물체마다 월드행렬을 만들어줘야 합니다. 본 예제에서는 월드의 원점(0, 0, 0)에 물체를 놓아둔다고 가정하므로 월드행렬을 그냥 단위행렬(identity matrix)로 놔두겠습니다.

        // 월드행렬을 만든다.
        D3DXMATRIXA16 matWorld;
        D3DXMatrixIdentity(&matWorld);


    쉐이더에서 사용할 전역변수 3개를 전부 다 만들었으니 이제 이 값들을 쉐이더에 전달해줘야 겠군요. 이 때 쉐이더의 SetMatrix함수를 이용하면 이런 일을 쉽게 할 수 있습니다. SetMatrix의 첫 번째 인수는 쉐이더 안에서 사용하는 변수의 이름이고, 두 번째 변수는 위에서 정의한 D3DXMATRIXA16형의 변수입니다.

        // 쉐이더 전역변수들을 설정
        gpColorShader->SetMatrix("gWorldMatrix", &matWorld);
        gpColorShader->SetMatrix("gViewMatrix",  &matView);
        gpColorShader->SetMatrix("gProjectionMatrix",  &matProjection);


    쉐이더에 필요한 변수들의 값을 모두 전달해줬다면 이제 GPU에게 명령을 내릴 차례입니다. '앞으로 그릴 모든 물체들에 이 쉐이더들을 적용할 것'이라는 명령을 말입니다. 이런 명령은 쉐이더의 Begin() / BeginPass()와 EndPass() / End() 함수호출로 내립니다. BeginPass()와 EndPass()가 구성하는 블럭 안에 물체를 그리는 함수를 넣으면 물체가 그려질 때 이 쉐이더가 사용되죠. 우선 아래의 코드를 보시죠.


        // 쉐이더를 시작한다.
        UINT numPasses = 0;
        gpColorShader->Begin(&numPasses, NULL);
        {
            for (UINT i = 0; i < numPasses; ++i )
            {
                gpColorShader->BeginPass(i);
                {
                    // 구체를 그린다.
                    gpSphere->DrawSubset(0);
                }
                gpColorShader->EndPass();
            }
        }
        gpColorShader->End();
    }



    DrawSubset() 호출이 BeginPass() / EndPass() 안에 있고, 이는 다시 Begin() / End() 호출 안에 있는 거 보이시죠? 이렇게 하면 GPU가 gpColorShader 쉐이더를 이용해서 gpSphere 물체를 그릴 것입니다.

    위의 코드를 보시면 쉐이더에서 Begin() 함수를 호출 하고 난 뒤에 다시 BeginPass()를 호출하는 거 보이시죠? 가끔 패스(pass)를 보고 '아니, 쉐이더는 알겠는데 그 안에 들어있는 패스는 또 뭐여?'라고 혼돈스러워하는 학생들을 본 적이 있는데 크게 신경 쓰지 않으셔도 됩니다. 패스는 다양한 쉐이더를 이용하여 동일한 물체를 여러 번 그릴 때 유용하지만 실무에서 둘 이상의 패스를 쓰는 경우가 별로 없으니 그냥 무시하세요. 그냥 Begin() 함수를 호출할 때, numPasses 변수의 주소를 전달하여 쉐이더 안에 들어있는 패스의 수(대부분의 경우 1)를 구해온다는 정도만 아시면 됩니다. 만약 2개 이상의 패스가 존재한다면 정점/픽셀쉐이더 쌍도 둘 이상이 존재한다는 거니까 그 수만큼 BeginPass()/EndPass()를 호출하면서 여러 번 물체를 그려주면 되는 거죠.

    이제 코드를 컴파일 한 뒤 프로그램을 실행하면 아까 렌더몽키에서 봤던 것과 동일한 결과를 보실 수 있습니다.
     
    정리
    다음은 이 장에서 배운 내용을 짧게 요약해 놓은 것입니다.
    • 각 정점마다 변하는 값은 정점데이터의 멤버변수로 받는다.
    • 모든 정점에 공통적으로 사용되는 값은 전역변수로 받는다.
    • HLSL은 벡터연산에 간편히 사용할 수 있는 float4, float4x4 등의 데이터형을 제공한다.
    • 정점의 공간을 변환할 때는 행렬 곱을 사용한다. HLSL에서 제공하는 내장함수 mul()을 사용하면 손쉽게 행렬과 벡터를 곱할 수 있다.
    • HLSL에서 색상을 표현할 때는 0 ~1 사이로 정규화한 값을 사용한다.


    이 장에서 배운 내용은 정말 기초 중의 기초입니다. 이렇게 간단한 쉐이더를 혼자서도 뚝딱 작성하실 정도로 쉐이더 문법의 기본이 되어야 나중에 다른 복잡한 쉐이더도 쉽게 작성하실 수 있습니다. 제가 강의를 할 때, 이 빨갱이 쉐이더가 너무 쉽다고 눈으로만 대충 훑어보고 넘어간 일부 학생들이 나중에 다른 쉐이더에서 고생하는 경우를 종종 봤습니다. 쉐이더 자체가 어려워서가 아니라 아주 기초적인 HLSL 문법조차도 제대로 숙지하지 않았기 때문이었습니다. 다음 장으로 가시기 전에 반드시 빨강쉐이더 정도는 직접 작성하실 수 있을 정도로 한두 번 연습을 해두시기 바랍니다.



    p.s. 여기는 댓글 달릴때마다 이메일이 오지 않아서 답변을 빨리빨리 드리기가 쉽지 않군요. 제 개인 블로그에 포스팅 된 버전에 댓글을 다시는게 제일 답변이 빠릅니다. 오탈자 및 잘못된 내용 수정도 제일 빠릅니다.

    포프 올림




     
    1. 이 값이 1이면 완전 불투명, 0이면 완전 투명입니다. [본문으로]
    샘플파일

    제2장 진짜 쉬운 빨강쉐이더

    이 장에서 새로 배우는 HLSL
    • float4: 4개의 성분을 가지는 벡터 데이터형
    • float4x4: 4 X 4 행렬 데이터형
    • mul(): 곱하기 함수. 거의 모든 데이터형을 변수로 받음.
    • POSITION: 정점위치 시맨틱. 정점데이터 중 위치정보만을 불러옴.



    이 장에서 새로 사용하는 수학
    • 3D 공간변환 - 행렬 곱을 이용함.




    '제1장: 쉐이더란 무엇이죠?'에서 쉐이더란 픽셀의 위치와 색을 계산하는 함수라고 말씀드렸습니다. 그렇다면 이번 장에서는 실제로 픽셀의 위치와 색을 계산하는 쉐이더를 만들어봐야겠죠? 처음 쉐이더를 짜보시는 분들도 쉽게 이해하실 수 있게끔 매우 간단한 쉐이더 프로그램을 만들어 보겠습니다. 우선 렌더몽키에서 빨간색 공을 그리는 쉐이더를 작성해보면서 HLSL 문법을 처음으로 접해보는 게 좋겠군요![각주:1]로부터 위치정보를 구해올 수 있는 이유는 POSITION이라는 시맨틱(semantic)[각주:2]때문입니다. 정점버퍼에는 정점의 위치, UV좌표, 법선 등을 비롯한 다양한 정보가 담겨 있을 수 있는데 이 중에서 필요한 정보만을 쏙쏙 빼오는 것을 시맨틱이라고 해두죠. 

    따라서 float4 mPosition : POSITION; 이라는 코드는 '정점데이터에서 위치(POSITION) 정보를 가져와서 mPosition에 대입해라!'라는 명령입니다.

    아차! 그렇다면 float4는 뭘까요? 이건 변수의 데이터형입니다. float4는 HLSL자체에서 지원하는 데이터형의 하나로 4개의 성분(x ,y, z, w)을 가지는 벡터입니다. 각 성분은 부동소수점(floating-point)형 입니다. HLSL은 float4외에도 float, float2, float3 등의 데이터형을 지원합니다.[각주:3]

    정점쉐이더 출력데이터
    정점쉐이더의 입력데이터를 선언해봤으니 이제 출력데이터를 살펴봐야겠죠? '제1장: 쉐이더란 무엇이죠?'에서 보여드렸던 초 간단 GPU 파이프라인의 그림을 기억하시나요? 각 픽셀의 위치를 찾아내려면 정점쉐이더가 위치변환 결과를 래스터라이저에 전달해줘야만 했습니다. 따라서 정점쉐이더는 반드시 위치변환 결과를 반환해야 합니다. 자, 그렇다면 정점쉐이더 출력데이터 구조체를 VS_OUTPUT이란 이름으로 선언해보지요!

    struct VS_OUTPUT 
    {
        float4 mPosition : POSITION;
    };


    float4형으로 위치데이터를 반환하면서 '이것은 위치(POSITION)요!'라는 시맨틱을 붙여준 거 보이시죠?

    전역변수
    정점쉐이더에서 공간 변환을 할 때, 사용해야 할 전역변수들이 몇 있는데 그 전에 공간 변환이 무언지부터 설명해 드려야 할 듯 싶군요.

    3D 공간변환
    3D물체를 모니터에 그리려면 정점들의 위치를 공간 변환해야 한다고 말씀드렸습니다. 그렇다면 과연 어떤 공간들을 거쳐야 3D물체를 모니터에 보여줄 수 있을까요? 사과를 예로 들어보죠.

    물체공간
    자, 일단 사과를 손에 쥐어봅시다. 사과의 중앙을 원점으로 삼고 그 점을 시작으로 오른쪽(+x), 위쪽(+y), 앞쪽(+z)으로 3개의 축을 만들어 볼까요? 이제 원점으로부터 사과의 표면까지의 거리를 이리저리 재보면 각 점들을 (x, y, z) 좌표로 표현할 수 있겠죠? 그리고 이 정점들을 3개씩 묶어 삼각형들을 만들면 폴리곤으로 사과모델을 만들 수 있겠네요.

    이제 사과를 손에 쥔 채 팔을 이리저리 움직여봅시다. 사과를 어디로 움직이던 간에 원점으로부터 각 정점까지의 거리는 변하지 않죠? 이것이 바로 물체공간(object space) 또는 지역공간(local space)입니다. 물체공간에서는 각 물체(3D 모델)가 자신만의 좌표계를 가지므로 다수의 물체를 통일적으로 처리하기 어렵습니다.

    그림 2.2. 물체공간의 예


     

    월드공간
    이제 사과를 모니터 옆에 놓아볼까요? 모니터도 물체니까 자신만의 물체공간을 가지고 있겠군요. 이 둘을 통일적으로 처리하고 싶은데 그러려면 어떻게 해야 할까요? 이 두 물체를 같은 공간으로 옮겨오면 될 거 같은데요? 그러면 새로운 공간을 하나 만들어야겠군요. 현재 계신 방의 입구를 원점으로 삼고 오른쪽, 위쪽, 앞쪽으로 +x, +y, +z인 3개의 축을 만들어보죠. 이제 그 원점에서부터 모니터를 구성하는 정점들까지의 거리를 재면 새로운 (x, y, z) 좌표로 정점들을 표현할 수 있겠죠? 사과도 똑같은 방법으로 표현할 수 있겠네요. 이 새로운 공간을 월드공간(world space) 또는 세계공간이라고 합니다.

    그림 2.3. 월드공간의 예


     

    뷰공간
    자, 그렇다면 이제 카메라를 가져다가 사진을 좀 찍어볼까요? 일단 위 두 물체들이 모두 사진 속에 들어오도록 사진을 찍고, 다음에는 이들이 전혀 보이지 않도록 전혀 엉뚱한 곳을 찍어봅시다. 이 두 사진은 확연히 다르죠? 처음 사진에서는 두 물체를 볼 수 있는데, 다른 사진에서는 흔적도 찾아볼 수 없군요. 그렇다면 이 두 사진 간에 뭔가 위치 변화가 있어야 한단 이야긴데 월드공간에서는 그 두 물체들의 위치가 전혀 변하지 않았는걸요? 아하! 그렇다면 이 카메라가 다른 공간을 사용하는 거로군요! 이렇게 카메라가 사용하는 공간을 뷰공간(view space)이라고 부릅니다. 뷰공간의 원점은 카메라 렌즈의 정 중앙이고 역시 그로부터 오른쪽, 위쪽, 앞쪽으로 3개의 축을 만들 수 있습니다.

    그림 2.4 뷰공간의 예. 물체들이 카메라 안에 있음.

     

    그림 2.5 뷰공간의 예. 물체들이 카메라 밖에 있음.




    투영공간
    일반 카메라로 사진을 찍으면 인간의 눈을 통해 보는 것과 마찬가지로 멀리 있는 물체는 조그맣게 보입니다. 근데 왜 우리 눈이 이렇게 작동하는지 아세요? 이건 인간의 시야가 좌우로 각각 100도 정도, 상하로 각각 75도 정도 되어서 그렇습니다. 따라서 멀리를 바라볼 수록 눈에 들어오는 범위가 넓어지는데 이 늘어난 범위를 일정한 크기의 망막에 담으려다 보니 멀리 있는 물체가 작게 보이는 거지요. 일반 카메라도 사람의 눈을 흉내 내는데 이와는 달리 직교카메라란 것도 있습니다. 직교카메라는 상하좌우로 퍼지는 시야를 가지지 않습니다. 무조건 앞쪽만 바라보지요. 따라서 직교카메라를 사용하면 거리에 상관없이 물체의 크기가 변하지 않습니다.

    그러면 결국 카메라로 사진을 찍는 과정을 두 단계로 나눌 수 있는 것 같네요. 첫째는 월드공간에 있는 물체들을 카메라 공간으로 이동, 회전, 확대/축소시키는 단계고요, 둘째는 이렇게 새로운 공간에 위치된 물체들을 2D 이미지 위에 투영하는 것입니다. 이러면 첫 번째 단계를 뷰공간, 두 번째 단계를 투영공간이라고 확실히 구분할 수 있겠죠? 이제 직각투시법(orthogonal projection)을 사용하던 원근투시법을 사용하던 간에 뷰공간은 아무 영향을 받지 않겠네요. 그 대신 투영공간에서 이 투시법을 적용하겠죠.

    이렇게 투영까지 마친 결과가 바로 화면에 보여지는 최종 이미지입니다.

    정리
    3D 그래픽에서 정점위치의 공간을 변환할 때 흔히 사용하는 방법이 정점의 위치벡터에 공간행렬을 곱하는 것입니다. 물체를 지역공간에서 화면공간까지 옮겨올 때 거치는 공간이 총 셋(월드공간, 뷰공간, 투영공간)이므로 행렬도 3개를 구해야 합니다. 참고로 각 공간의 원점과 세 축을 알면 그 공간을 나타내는 행렬을 쉽게 만들 수 있습니다.[각주:4]

    자, 그럼 여태까지 논한 모든 공간변환들을 정리해서 보여드리면 다음과 같습니다.

    물체공간 ----------> 월드공간 --------> 뷰공간 ---------> 투영공간
                  ⅹ월드행렬                   ⅹ뷰행렬                ⅹ투영행렬


    위의 모든 행렬들은 각 정점마다 값이 변하지 않으니 전역변수로 선언하기에 적합하군요.

    전역변수 선언
    그럼, 이제 어떤 전역변수들이 필요한지 아시겠죠? 그렇습니다. 공간변환을 할 때 사용할 월드행렬, 뷰행렬, 투영행렬이 필요합니다. 정점쉐이더 코드에 다음의 세 라인을 삽입합시다.

    float4x4 gWorldMatrix;
    float4x4 gViewMatrix;
    float4x4 gProjectionMatrix;


    float4x4라는 새로운 데이터형이 나왔군요? 이것도 역시 HLSL에서 지원하는 데이터형 중에 하나입니다. 4 X 4 행렬이라는 거 쉽게 아시겠죠? 이 외에도 float2x2, float3x3 등의 데이터형이 있습니다.

    자, 그럼 이제 행렬들도 선언했는데 과연 누가 이 변수들에 값을 전달해줄까요? 보통 게임에서는 그래픽 엔진에서 전역변수들의 값을 대입해주는 코드가 있습니다. 렌더몽키에서는 변수시맨틱(variable semantic)을 통해 변수 값을 대입해줍니다. 그럼 변수시맨틱을 사용해보죠.

    1. Workspace 패널 안에서 ColorShader를 찾아 마우스 오른쪽 버튼을 누릅니다.
    2. 팝업메뉴에서 Add Variable > Matrix > Float(4x4)를 선택합니다. f4x4Matrix란 이름의 새로운 변수가 추가될 겁니다.
    3. gWorldMatrix로 변수의 이름을 변경합니다.
    4. 이제 gWorldMatrix 위에 마우스 오른쪽 버튼을 눌러, Variable Semantic > World를 선택합니다. 이게 바로 렌더몽키에서 변수 시맨틱을 통해 변수 값을 전달하는 방법입니다.
    5. 이제 위의 과정을 반복하여 뷰 행렬과 투영행렬을 만듭니다. 변수 명을 각각 gViewMatrix와 gProjectionMatrix를 만들고 View와 Projection 변수시맨틱을 대입합니다.
    6. 마지막으로 matViewProjection이란 변수를 지웁니다. 처음 이펙트를 만들 때 같이 딸려온 변수인데 저희는 이 대신 gViewMatrix와 gProjectionMatrix를 씁니다.

    이 과정을 마치셨다면 Workspace 패널이 아래 그림처럼 보일 겁니다.

    그림 2.6. 변수시맨틱을 대입한 뒤의 Workspace 패널


     

    정점쉐이더 함수
    이제 모든 준비작업이 끝났습니다. 드디어 정점쉐이더 함수를 작성할 때가 왔군요. 우선 함수헤더부터 볼까요?

    VS_OUTPUT vs_main( VS_INPUT Input )
    {


    이 함수헤더가 의미하는 바는 이와 같습니다.
    • 이 함수의 이름은 vs_main이다.
    • 이 함수의 인수는 VS_INPUT 데이터형의 Input이다.
    • 이 함수의 반환값은 VS_OUPUT 데이터형이다.

    C에서 함수를 정의하는 것과 별 차이가 없죠? HLSL은 C와 비슷한 문법을 사용한다고 전에 말씀드렸습니다. 자, 그럼 다음 라인을 보죠.

       VS_OUTPUT Output;


    이건 그냥 함수의 끝에서 반환할 구조체를 선언한 것 뿐입니다. 함수헤더에서 선언했다시피 데이터형이 VS_OUTPUT인 거 보이시죠? VS_OUTPUT의 멤버로는 무엇이 있었죠? 투영공간으로 변환된 mPosition이 있었죠? 그럼 이제 공간변환을 해볼 차례군요! 우선, Input.mPosition에 담긴 모델공간 위치를 월드공간으로 변환합시다. 공간변환을 어떻게 한다고 했었죠? 정점위치에 행렬을 곱하는 거였네요. 그러면 float4형의 위치벡터와 float4x4 행렬을 곱해야겠네요? 행렬과 벡터를 곱하는 법을 찾기 위해 수학책을 뒤지실 필요는 없습니다. HLSL은 이미 여러 데이터형 간의 곱셈을 처리해주는 내장함수 mul()을 가지고 있습니다. 이 함수를 사용하면 공간변환이 이렇게 간단해집니다.

       Output.mPosition = mul( Input.mPosition, gWorldMatrix );


    위의 코드는 모델공간에 존재하는 정점위치(Input.mPosition)에 월드행렬(gWorldMatrix)를 곱해서 그 결과(월드공간에서의 위치)를 Output.mPosition에 대입합니다. 이제 똑같은 방식으로 뷰공간과 투영공간으로 변환하면 됩니다.

       Output.mPosition = mul( Output.mPosition, gViewMatrix );
       Output.mPosition = mul( Output.mPosition, gProjectionMatrix );


    복잡한 거 하나도 없죠? 자, 이제 무슨 일이 남았을까요? 정점쉐이더의 가장 중요한 임무는 모델공간에 있는 정점의 위치를 투영공간까지 변환하는 것이었으니까... 음.... 그 중요한 임무를 방금 막 마친 듯 한데요? 자, 그럼 이 결과를 반환하는 걸로 정점쉐이더를 마치겠습니다.

       return Output;
    }


    이제 F5를 눌러서 정점쉐이더를 한 번 컴파일 해보면 여전히 빨간 공이 보이죠? 그럼 일단 정점쉐이더는 잘 마무리가 된 듯 하네요. 혹시라도 컴파일 에러가 보이면 뭔가 잘못했단 이야기니 한번 코드를 다시 검토해보세요.

    팁: 쉐이더 컴파일에 실패한 경우
    오타나 문법적 오류 때문에 쉐이더 컴파일에 실패한 경우, 미리 보기 창에 컴파일에 실패했다는 오류메시지가 등장할 것입니다. 이 때, 정확히 어떤 코드에 문제가 있는지 알고 싶으시다면 렌더몽키의 젤 아래쪽에 위치한 출력(output)창을 보세요. 자세한 오류메시지와 더불어 문제가 있는 코드의 행과 열 번호까지도 보여줍니다.



    p.s. 여기는 댓글 달릴때마다 이메일이 오지 않아서 답변을 빨리빨리 드리기가 쉽지 않군요. 제 개인 블로그에 포스팅 된 버전에 댓글을 다시는게 제일 답변이 빠릅니다. 오탈자 및 잘못된 내용 수정도 제일 빠릅니다.


    포프 올림




    1. [/footnote]  렌더몽키에서 쉐이더를 작성하면 그 결과를 .fx 파일로 익스포트(export)해서 이걸 DirectX 프레임워크에 그대로 가져다 쓸 수도 있습니다.

    기초설정
    다음의 단계를 따라서 기초적인 설정을 마무리합시다.

    1. 렌더몽키를 시작합니다. 무서운(?) 원숭이 얼굴이 잠시 스쳐 지나간 뒤에 빈 작업공간(workspace)가 등장할 것입니다. 
    2. Workspace 패널 안에서 Effect Workspace위에 마우스 오른쪽 버튼을 누릅니다. 팝업 메뉴가 등장할 겁니다.
    3. 팝업메뉴에서 Add Default Effect > DirectX > DirectX를 선택합니다. 이제 미리 보기(preview) 창에 빨간색 공 하나가 보이죠?
    4. Workspace패널에 Deafult_DirectX_Effect라는 새로운 쉐이더도 추가되었을 것입니다. 쉐이더의 이름을 ColorShader로 바꿉니다.
    5. 이제 화면이 아래와 같을 것입니다.

    그림 2.1. 기초설정을 마친 렌더몽키 프로젝트


     

    정점쉐이더
    이제 ColorShader옆에 있는 더하기(+) 표시를 누릅니다. 제일 아래쪽에 Pass 0이 보이시죠? 그 옆에 있는 더하기 표시를 다시 누르세요. 이제 Vertex Shader를 더블클릭하시면 오른쪽 쉐이더 편집기 안에 Vertex Shader코드가 등장할 겁니다. 사실 여기에 들어있는 코드가 이미 빨간 공을 그리고 있지만 저희는 한 줄씩 연습을 해봐야 하니 이 속에 있는 코드를 모두 지우겠습니다. 

    코드를 다 지우셨나요? 그렇다면 이제 본격적으로 시작해보죠! 우선 한 눈에 보실 수 있게끔 정점쉐이더 코드를 전부 보여드린 뒤 한 줄씩 설명해 나가도록 하겠습니다.

    struct VS_INPUT 
    {
       float4 mPosition : POSITION;
    };

    struct VS_OUTPUT 
    {
       float4 mPosition : POSITION;
    };

    float4x4 gWorldMatrix;         
    float4x4 gViewMatrix;          
    float4x4 gProjectionMatrix;    


    VS_OUTPUT vs_main( VS_INPUT Input )
    {
       VS_OUTPUT Output;

       Output.mPosition = mul( Input.mPosition, gWorldMatrix );
       Output.mPosition = mul( Output.mPosition, gViewMatrix );
       Output.mPosition = mul( Output.mPosition, gProjectionMatrix );
       
       return Output;
    }




    전역변수 vs 정점데이터
    쉐이더에서 사용할 수 있는 입력 값으로는 전역변수와 정점데이터가 있습니다. 이 둘을 구분 짓는 기준은 한 물체를 구성하는 모든 정점이 동일한 값을 사용하느냐의 여부입니다. 만약 동일한 값을 사용한다면 이것은 전역변수가 될 수 있지만 각 정점마다 다른 값을 사용한다면 당연히 전역변수는 안되겠지요. 그 대신 정점 버퍼, 즉 정점 데이터의 일부로 이 값을 받아들여야 합니다.

    전역변수의 예로는 월드행렬, 카메라의 위치 등이 있고, 정점데이터 변수의 예로는 정점의 위치, UV좌표 등이 있습니다.

    정점쉐이더 입력데이터
    우선 정점쉐이더에서 입력으로 받을 데이터들을 VS_INPUT이라는 구조체로 선언해보겠습니다.

    struct VS_INPUT
    {
        float4 mPosition : POSITION;
    };


    '제1장: 쉐이더란 무엇이죠?'에서 정점쉐이더의 가장 중요한 임무는 각 정점의 위치를 공간 변환하는 것이라고 했던 거 기억하시나요? 그러기 위해서는 정점의 위치를 입력으로 받아야 하죠? 그게 바로 위 구조체가 멤버변수 mPosition을 통해 정점의 위치를 얻어오는 이유입니다. 이 변수가 DirectX의 정점버퍼[footnote]버텍스 버퍼(vertex buffer)라고도 합니다. [본문으로]
  • 태그(tag) 정도로 이해하시는 게 편할 겁니다. [본문으로]
  • 참고로 GPU는 부동소수점 벡터를 처리하는데 최적화된 장치입니다. 따라서 쉐이더에서 사용하는 기본적인 데이터형은 정수가 아닌 부동소수점입니다. 정수는 오히려 쉐이더의 성능을 저하시키는 요인입니다. [본문으로]
  • 이 행렬을 직접 만드는 방법은 3D 수학책을 참조하시기 바랍니다. 이 책에서는 Direct3D에서 제공하는 함수를 사용해서 이 행렬들을 구성합니다. [본문으로]
  • 샘플파일

    쉐이더 프로그래밍
    자, 그럼 쉐이더가 무엇인지는 대충 알아보았는데 쉐이더를 코드를 짠다는 것은 무슨 뜻일까요? 일단 그림 1.1을 다시 한번 살펴보죠. 그림 1.1을 보면 사각형으로 표현한 파이프라인 단계도 있고 둥글게 표현한 것도 있죠? 원형으로 표현한 단계들은 GPU(graphics processing unit, 그래픽 처리장치)가 알아서 처리해주는 -- 즉 프로그래머가 따로 제어할 수 없는 -- 단계들입니다. 그와 반대로 사각형으로 표현한 단계들은 프로그래머가 마음대로 제어할 수 있는 단계들이죠. 이 단계에서 사용할 함수를 작성하는 것이 바로 쉐이더 프로그래밍입니다. 그림 1.1에서 사각형으로 표현된 단계들은 정점쉐이더와 픽셀쉐이더 뿐인 거 보이시죠? 따라서 정점쉐이더와 픽셀쉐이더에 사용할 함수를 하나씩 만드는 것이 쉐이더 프로그래밍입니다.[각주:1]

    시중에 나와 있는 여러 쉐이더 언어 중에 이 책에서 사용할 언어는 DirectX에서 지원하는 HLSL입니다. HLSL(High Level Shader Language, 고수준 쉐이더언어)은 C와 매우 비슷한 문법을 사용하는 언어로 GLSL[각주:2]이나 CgFX[각주:3] 등의 기타 쉐이더 언어와 매우 흡사합니다. 따라서 HLSL을 배우시면 다른 쉐이더 언어를 익히시는데도 큰 무리가 없을 것입니다.

    한 언어를 배우는 최선의 방법은 직접 코딩을 하면서 배우는 것입니다. 이 언어의 철학은 이러네, 이 언어의 문법은 저러네 하면서 백날 떠들어봐야 입문자들은 하품만 하고 무슨 이야긴지 알아듣지도 못합니다. 일단 재미있게 코드를 짜봐야 프로그래밍에 애착도 생기고, 애착이 생기면 보다 나은 프로그래머가 되기 위해 노력을 하지요. 따라서 이 책에서는 쓸데없이 HLSL 문법을 나열하면서 독자분들의 짜증을 부추기는 대신 무조건 아주 쉬운 쉐이더부터 짜보는 방법으로 HLSL을 배우도록 하겠습니다. 정 문법이 궁금하신 분들은 부록을 참고하시길 바랍니다.

    하지만 HLSL 코드를 곧바로 짜기 전에 준비해야 할 것들이 좀 있군요. 이건 좀 지루하시더라도 꾹 참고 따라 해주시기 바랍니다.

    쉐이더 프로그래밍을 위한 기본준비
    서문에서도 말씀드렸듯이 이 책의 초점은 쉐이더 프로그래밍입니다. 이 책에서 DirectX에 대한 내용을 자세히 다루지 않기로 결정한 이유는 이미 훌륭한 DirectX 입문 책들이 시중에 나와있는데 굳이 DirectX를 다시 처음부터 소개하면서 쓸데없이 지면을 낭비하고 싶지 않았기 때문입니다.[각주:4] 또한 프로그래머 분들 외에 테크니컬 아티스트 분들도 이 책을 읽으실 수 있도록 하기 위해서입니다.

    마찬가지 이유로 이 책에서 쉐이더를 만드는 과정도 둘로 나눴습니다. 첫 번째 단계는 쉐이더 작성만을 하는 단계로 AMD(전 ATI) 사의 렌더몽키(render monkey)라는 프로그램을 사용합니다. 이 단계는 프로그래머와 아티스트 분들을 모두 대상으로 하므로 반드시 따라 해 주시기 바랍니다.

    두 번째 단계는 렌더몽키에서 만든 쉐이더를 C++/DirectX 프레임워크에서 불러와 사용하는 것으로 프로그래머 분들을 위한 단계입니다. 프로그래머이시더라도 C++/DirectX 프레임워크에 관심이 없으신 분들은 이 단계를 건너 뛰셔도 됩니다. 실제로 쉐이더 코드를 작성하는 곳은 첫 번째 단계입니다.

    자, 그러면 위 두 단계에서 쉐이더를 배우는 데 필요한 것들을 준비해보죠.

    렌더몽키
    렌더몽키는 AMD사에서 제공하는 쉐이더 작성도구로 프로토타이핑에 유용합니다. 부록 디스크에서 /RenderMonkey/ RenderMonkey.2008-12-17-v1.82.322.msi를 찾아 설치해 주세요. 그냥 기본(default) 옵션으로 설치하시면 되겠습니다.

    선택사항: 간단한 DirectX 프레임워크
    C++/DirectX 프레임워크에서 쉐이더를 실행해보고 싶으신 분들만 이 절을 따라 해주세요.

    우선 비주얼 C++ 2008과 DirectX SDK를 설치하시기 바랍니다. 비주얼 C++을 소장하고 계시지 않으신 분들은 마이크로소프트사의 웹 페이지에서 공짜 버전인 익스프레스 버전을 다운받으실 수 있습니다. DirectX SDK는 부록 CD의 DXSDK 폴더에 포함되어 있습니다.

    위 두 프로그램의 설치를 마치셨다면 비주얼 C++ 2008에서 부록 CD에 있는 samples/01_DxFramework/BasicFramework.sln 파일을 여시기 바랍니다. 별다른 수정 없이 이 프로그램을 실행하면 다음과 같은 파란 화면을 보실 수 있을 것입니다.

    그림 1.2. 별볼일 없는 초 간단 프레임워크



    이 프레임워크는 다음과 같은 기능들을 구현합니다.

    • 창의 생성 및 메시지 루프 등의 기본적인 윈도우 기능
    • Direct 3D 장치 생성
    • 텍스처, 모델, 쉐이더 등의 자원 로딩
    • 간단한 게임루프
    • 간단한 키보드 입력처리

    참고로 말씀드리는데 이 프로그램은 쉐이더 코드를 재빨리 실행할 수 있도록 매우 간단하게 만든 프레임워크입니다. 그 결과, 모든 함수들이 .cpp 파일 하나 안에 들어있고, 클래스나 개체도 사용하지 않지요. 따라서 모든 함수들은 C스타일로 작성되어 있고, 모든 변수들도 전역적으로 선언되어 있습니다. 실제 게임을 만드실 때, 이렇게 프레임워크를 만드시면 절대 안됩니다. 다시 한 번 말씀드리는데 이 프레임워크는 쉐이더 데모를 실행할 수 있도록 만든 프로그램일 뿐입니다.

    자, 그럼 적당히 주의도 드렸으니 이제 프레임워크를 살펴보도록 합시다. 우선 BasicFramework.h를 엽니다.

    //**********************************************************************
    //
    // ShaderFramework.h
    //
    // 쉐이더 데모를 위한 C스타일의 초간단 프레임워크입니다.
    // (실제 게임을 코딩하실 때는 절대 이렇게 프레임워크를
    // 작성하시면 안됩니다. -_-)
    //
    // Author: Pope Kim
    //
    //**********************************************************************


    #pragma once

    #include <d3d9.h>
    #include <d3dx9.h>

    // ---------- 선언 ------------------------------------
    #define WIN_WIDTH  800
    #define WIN_HEIGHT  600

    // ---------------- 함수 프로토타입 ------------------------

    // 메시지 처리기 관련
    LRESULT WINAPI MsgProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam );
    void ProcessInput(HWND hWnd, WPARAM keyPress);

    // 초기화 과련
    bool InitEverything(HWND hWnd);
    bool InitD3D(HWND hWnd);
    bool LoadAssets();
    LPD3DXEFFECT LoadShader( const char * filename );
    LPDIRECT3DTEXTURE9 LoadTexture(const char * filename);
    LPD3DXMESH LoadModel(const char * filename);

    // 게임루프 관련
    void PlayDemo();
    void Update();

    // 렌더링 관련
    void RenderFrame();
    void RenderScene();
    void RenderInfo();

    // 뒷정리 관련
    void Cleanup();



    이 헤더파일에서 눈 여겨 볼만한 것은 WIN_WIDTH와 WIN_HEIGHT밖에 없습니다. 이 두 #define문은 데모 프로그램의 창 크기를 정의합니다. 나머지 코드들은 단순히 함수선언들일 뿐입니다. 실제 함수들의 구현은 ShaderFramework.cpp 파일에 들어 있으니 ShaderFramework.cpp 파일을 열어보도록 할까요?

    이 파일의 제일 위에는 다음과 같은 전역변수들이 정의되어 있습니다.

    //----------------------------------------------------------------------
    // 전역변수
    //----------------------------------------------------------------------

    // D3D 관련
    LPDIRECT3D9       gpD3D          = NULL;        // D3D
    LPDIRECT3DDEVICE9 gpD3DDevice    = NULL;        // D3D 장치

    // 폰트
    ID3DXFont*        gpFont         = NULL;

    // 모델

    // 쉐이더

    // 텍스처

    // 프로그램 이름
    const char*       gAppName        = "초 간단 쉐이더 데모 프레임워크";



    이제 프로그램의 창을 생성할 차례입니다.


    //-----------------------------------------------------------------------
    // 프로그램 진입점/메시지 루프
    //-----------------------------------------------------------------------

    // 진입점
    INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR, INT )
    {


    프로그램의 창을 생성하려면 우선 윈도우 클래스를 등록해야 합니다.


        // 윈도우 클래스를 등록한다.
        WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_CLASSDC, MsgProc, 0L, 0L,
                          GetModuleHandle(NULL), NULL, NULL, NULL, NULL,
                          gAppName, NULL };
        RegisterClassEx( &wc );



    이제 CreateWindow 함수를 사용해서 위에서 등록한 윈도우 클래스의 인스턴스를 만듭니다. 이 때, 앞서 정의했던 WIN_WIDTH와 WIN_HEIGHT를 창의 크기로 지정합니다.


        // 프로그램 창을 생성한다.
        DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;
        HWND hWnd = CreateWindow( gAppName, gAppName,
                                  style, CW_USEDEFAULT, 0, WIN_WIDTH, WIN_HEIGHT,
                                  GetDesktopWindow(), NULL, wc.hInstance, NULL );


    창의 크기를 WIN_WIDTH와 WIN_HEIGHT로 만들면 실제 렌더링을 할 수 있는 공간이 이보다 작습니다. 창의 크기에 타이틀 바 및 경계선이 포함되기 때문이라죠. 따라서 실제 렌더링이 가능한 공간(client rect)이 WIN_WIDTH와 WIN_HEIGHT와 같도록 창의 크기를 재조정해야 겠네요.

        // Client Rect 크기가 WIN_WIDTH, WIN_HEIGHT와 같도록 크기를 조정한다.
        POINT ptDiff;
        RECT rcClient, rcWindow;
     
        GetClientRect(hWnd, &rcClient);
        GetWindowRect(hWnd, &rcWindow);
        ptDiff.x = (rcWindow.right - rcWindow.left) - rcClient.right;
        ptDiff.y = (rcWindow.bottom - rcWindow.top) - rcClient.bottom;
        MoveWindow(hWnd,rcWindow.left, rcWindow.top, WIN_WIDTH + ptDiff.x, WIN_HEIGHT + ptDiff.y, TRUE);


    이제 창의 크기도 적절히 조정했으니 창을 보여줄 차례입니다.

        ShowWindow( hWnd, SW_SHOWDEFAULT );
        UpdateWindow( hWnd );



    다음은 Direct3D를 초기화하고 모든 D3D 자원들(텍스처, 쉐이더, 메쉬 등)을 로딩합니다. 이 모든 기능들은 InitEverything() 함수 안에 포함되어 있습니다. 만약 Direct3D 및 기타 초기화에 실패하면 데모를 보여주는 게 불가능하므로 프로그램을 종료합니다.
     

        // D3D를 비롯한 모든 것을 초기화한다.
        if( !InitEverything(hWnd) )
        {
            PostQuitMessage(1);
        }


    D3D 및 기타 초기화를 마쳤다면 남은 일은 WM_QUIT 메시지를 받을 때까지 데모를 실행하는 것이 전부입니다. WM_QUIT은 데모를 종료하라는 윈도우 메시지입니다.


        // 메시지 루프
        MSG msg;
        ZeroMemory(&msg, sizeof(msg));
        while(msg.message!=WM_QUIT)
        {
            if( PeekMessage( &msg, NULL, 0U, 0U, PM_REMOVE ) )
            {
                TranslateMessage( &msg );
                DispatchMessage( &msg );
            }
            else // 메시지가 없으면 게임을 업데이트하고 장면을 그린다
            {
                PlayDemo();
            }
        }


    데모를 종료할 때가 되면 윈도우 클래스의 등록을 해제하고 프로그램을 끝마칩니다.


        UnregisterClass( gAppName, wc.hInstance );
        return 0;
    }


    다음은 윈도우 메시지를 처리하는 함수입니다.


    // 메시지 처리기
    LRESULT WINAPI MsgProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam )
    {
        switch( msg )
        {


    키보드 입력은 ProcessInput이라는 함수에서 처리할 것입니다.

        case WM_KEYDOWN:
            ProcessInput(hWnd, wParam);
            break;



    창이 닫힐 때는 초기화 도중에 생성했던 D3D 자원들을 해제하고 프로그램을 종료하라는 메시지를 보냅니다.


        case WM_DESTROY:
            Cleanup();
            PostQuitMessage(0);
            return 0;
        }


    이 데모에서 처리하지 않는 윈도우 메시지들은 기본(default) 메시지 처리기가 처리하도록 합니다.

        return DefWindowProc( hWnd, msg, wParam, lParam );
    }


    이 프레임워크가 현재 처리하는 키보드 입력은 ESC 키가 전부입니다. ESC키가 눌리면 프로그램의 실행을 마칩니다.

    // 키보드 입력처리
    void ProcessInput( HWND hWnd, WPARAM keyPress)
    {
     switch(keyPress)
     {
     // ESC 키가 눌리면 프로그램을 종료한다.
     case VK_ESCAPE:
      PostMessage(hWnd, WM_DESTROY, 0L, 0L);
      break;
     }
    }



    이제 초기화 코드를 살펴볼까요?


    //------------------------------------------------------------
    // 초기화 코드
    //------------------------------------------------------------
    bool InitEverything(HWND hWnd)
    {


    우선 InitD3D함수를 호출하여 D3D를 초기화합니다. D3D 초기화에 실패하지 않았다면 LoadAssets() 함수를 통해 모델, 쉐이더, 텍스처 등의 D3D 자원들을 로딩합니다.

        // D3D를 초기화
        if( !InitD3D(hWnd) )
        {
            return false;
        }

        // 모델, 쉐이더, 텍스처 등을 로딩
        if( !LoadAssets() )
        {
            return false;
        }


    그 다음은 폰트를 로딩할 차례입니다. 이 폰트를 사용하여 화면에 디버그 정보 등을 보여줄 것입니다.


        // 폰트를 로딩
        if(FAILED(D3DXCreateFont( gpD3DDevice, 20, 10, FW_BOLD, 1, FALSE,
            DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY,
            (DEFAULT_PITCH | FF_DONTCARE), "Arial", &gpFont )))
        {
     return false;
        }

        return true;
    }



    위의 D3DXCreateFont()에서 사용했던 각 매개변수의 의미는 순서대로 다음과 같습니다.

    • gpD3DDevice: D3D 장치
    • 20: 폰트의 높이
    • 10: 폰트의 너비
    • FW_BOLD: 볼드체(두꺼운 폰트)를 이용함
    • 1: 밉맵레벨
    • FALSE: 이탤릭체를 쓰지 않음
    • DEFAULT_CHARSET: 기본 문자셋을 사용
    • OUT_DEFAULT_PRECIS: 실제 화면에 출력되는 폰트가 여기서 지정한 속성과 어느 정도 비슷해야 하는지를 설정
    • DEFAULT_QUALITY: 여기서 지정하는 폰트와 실제 폰트의 품질이 얼마나 비슷해야 하는지 설정
    • DEFAULT_PITCH | FF_DONTCARE: 기본 피치를 사용하고 폰트군은 상관 없음
    • "Arial": 사용할 폰트이름
    • gpFont: 새로 만든 폰트를 저장할 포인터


    이제 D3D 객체와 D3D 장치를 생성하는 InitD3D 함수를 살펴봅시다. D3D를 이용해서 자원을 로딩하거나 렌더링을 하려면 반드시 D3D장치를 생성해야 합니다.


    // D3D 객체 및 장치 초기화
    bool InitD3D(HWND hWnd)
    {

    우선 Direct3D 객체를 구합니다.

        // D3D 객체
        gpD3D = Direct3DCreate9( D3D_SDK_VERSION ); 
        if ( !gpD3D )
        {
            return false;
        }


    그 다음에는 D3D 장치를 생성할 때 필요한 구조체에 정보를 채워 넣겠습니다.

        // D3D장치를 생성하는데 필요한 구조체를 채워 넣는다.
        D3DPRESENT_PARAMETERS d3dpp;
        ZeroMemory( &d3dpp, sizeof(d3dpp) );

        d3dpp.BackBufferWidth            = WIN_WIDTH;
        d3dpp.BackBufferHeight           = WIN_HEIGHT;
        d3dpp.BackBufferFormat           = D3DFMT_X8R8G8B8;
        d3dpp.BackBufferCount            = 1;
        d3dpp.MultiSampleType            = D3DMULTISAMPLE_NONE;
        d3dpp.MultiSampleQuality         = 0;
        d3dpp.SwapEffect                 = D3DSWAPEFFECT_DISCARD;
        d3dpp.hDeviceWindow              = hWnd;
        d3dpp.Windowed                   = TRUE;
        d3dpp.EnableAutoDepthStencil     = TRUE;
        d3dpp.AutoDepthStencilFormat     = D3DFMT_D24X8;
        d3dpp.Flags                      = D3DPRESENTFLAG_DISCARD_DEPTHSTENCIL;
        d3dpp.FullScreen_RefreshRateInHz = 0;
        d3dpp.PresentationInterval       = D3DPRESENT_INTERVAL_ONE;



    위에서 주목할만한 정보들은 다음과 같습니다.

    • BackBufferWidth: 백버퍼(렌더링 영역)의 너비
    • BackBuferHeight: 백버퍼의 높이
    • BackBufferFormat: 백버퍼의 포맷
    • AutoDepthStencilFormat: 깊이/스텐실 버퍼의 포맷
    • SwapEffect: 백버퍼를 스왑(swap)할 때의 효과. 성능 상 D3DSWAPEFFECT_DISCARD를 사용하는 것이 좋습니다.
    • PresentationInterval: 모니터 주사율과 백버퍼를 스왑하는 빈도간의 관계. D3DPRESENT_INTERVAL_ONE은 모니터가 수직동기될 때마다 백버퍼를 스왑해 줍니다. 게임에서는 성능상 모니터의 수직동기를 기다리지 않고 곧바로 스왑해 주는 경우가 많습니다.(D3DPRESENT IMMEDIATE). 단, 이럴 땐 전 프레임과 현재 프레임이 서로 찢겨 보이는 부작용이 있습니다.

    이제 위에서 채워 넣은 정보들을 이용해서 D3D장치를 생성합니다.

        // D3D장치를 생성한다.
        if( FAILED( gpD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
            D3DCREATE_HARDWARE_VERTEXPROCESSING,
            &d3dpp, &gpD3DDevice ) ) )
        {
            return false;
        }

        return true;
    }


    D3D 자원을 읽어오는 LoadAssets() 함수 안에는 현재 아무 내용도 들어있지 않습니다. 이 책을 진행하면서 텍스처, 쉐이더, 모델 등을 읽어올 일이 있을 때마다 이 함수 안에서 LoadShader(), LoadTexture(), LoadModel() 함수들을 호출하겠습니다.

    bool LoadAssets()
    {
        // 텍스처 로딩

        // 쉐이더 로딩

        // 모델 로딩

        return true;
    }


    다음은 .fx 포맷으로 저장된 쉐이더 파일을 불러오는 LoadShader() 함수입니다. .fx파일은 정점쉐이더와 픽셀쉐이더를 모두 포함하고 있는 텍스트 파일로 D3DXCreateEffectFromFile() 함수를 통해 로딩 및 컴파일합니다. 따라서 HLSL코드에 문법적인 오류가 있다면 이 함수를 호출하는 도중에 쉐이더 컴파일 에러가 나겠죠? 그럴 때는 D3DXCreateEffectFromFile()의 마지막 매개변수를 통해 에러메시지를 구해올 수 있습니다. 이 에러메시지를 비주얼 C++의 출력 창에 보여주도록 하겠습니다.


    // 쉐이더 로딩
    LPD3DXEFFECT LoadShader(const char * filename )
    {
        LPD3DXEFFECT ret = NULL;
        LPD3DXBUFFER pError = NULL;
        DWORD dwShaderFlags = 0;

    #if _DEBUG
        dwShaderFlags |= D3DXSHADER_DEBUG;
    #endif

        D3DXCreateEffectFromFile(gpD3DDevice, filename,
            NULL, NULL, dwShaderFlags, NULL, &ret, &pError);

        // 쉐이더 로딩에 실패한 경우 output창에 쉐이더
        // 컴파일 에러를 출력한다.
        if(!ret && pError)
        {
            int size  = pError->GetBufferSize();
            void *ack = pError->GetBufferPointer();

            if(ack)
            {
                char* str = new char[size];
                sprintf(str, (const char*)ack, size);
                OutputDebugString(str);
                delete [] str;
            }
        }

        return ret;
    }


    위에서 D3DXCreateEffectFromFile() 함수를 호출할 때 사용한 인자들의 의미는 순서대로 아래와 같습니다.

    • gpD3DDevice: D3D 장치
    • filename: 읽어올 쉐이더 파일의 이름
    • NULL: 쉐이더를 컴파일 할 때 추가로 사용할 #define 정의
    • NULL: #include 지시문을 처리할 때 사용할 인터페이스 포인터
    • dwShaderFlags: 쉐이더를 컴파일 할 때 사용할 플래그
    • NULL: 매개변수 공유에 사용할 이펙트 풀
    • ret: 로딩된 이펙트를 저장할 포인터
    • pError: 컴파일러 에러 메시지를 가리킬 포인터

    다음은 .x 포맷으로 저장된 모델을 로딩해오는 코드입니다. .x 파일은 DirectX에서 자체적으로 지원하는 메쉬 포맷입니다.

    // 모델 로딩
    LPD3DXMESH LoadModel(const char * filename)
    {
        LPD3DXMESH ret = NULL;
        if ( FAILED(D3DXLoadMeshFromX(filename,D3DXMESH_SYSTEMMEM, gpD3DDevice,
            NULL,NULL,NULL,NULL, &ret)) )
        {
            OutputDebugString("모델 로딩 실패: ");
            OutputDebugString(filename);
            OutputDebugString("\n");
        };

        return ret;
    }


    위에서 D3DXLoadMeshFromX() 호출에 사용했던 매개변수의 의미는 순서대로 다음과 같습니다.

    • filename: 로딩해 올 메쉬의 파일명
    • D3DXMESH_SYSTEMMEM: 시스템 메모리에 메쉬를 로딩할 것
    • gpD3DDevice: D3D 장치
    • NULL: 인접 삼각형 데이터를 따로 구해오지 않음.
    • NULL: 머테리얼(material) 정보를 따로 구해오지 않음
    • NULL: 이펙트 인스턴스를 따로 구해오지 않음
    • NULL: 머테리얼 수를 따로 구해오지 않음
    • ret: 로딩해온 메쉬를 저장할 포인터

    아래는 다양한 포맷으로 저장된 이미지들을 텍스처로 로딩해 오는 LoadTexture() 함수입니다.

    // 텍스처 로딩
    LPDIRECT3DTEXTURE9 LoadTexture(const char * filename)
    {
        LPDIRECT3DTEXTURE9 ret = NULL;
        if ( FAILED(D3DXCreateTextureFromFile(gpD3DDevice, filename, &ret)) )
        {
            OutputDebugString("텍스처 로딩 실패: ");
            OutputDebugString(filename);
            OutputDebugString("\n");
        }

        return ret;
    }



    다음은 게임루프 함수인 PlayDemo()입니다. 이 함수는 처리할 윈도우 메시지가 없을 때마다 호출됩니다. 실제 게임에서는 지난 프레임으로부터 경과한 시간을 구해서 업데이트 및 렌더링에 사용할 테지만 이 프레임워크에서는 간략함을 위해 그 부분을 생략했습니다.


    //------------------------------------------------------------
    // 게임루프
    //------------------------------------------------------------
    void PlayDemo()
    {
        Update();
        RenderFrame();
    }


    현재 Update() 함수에는 아무 내용도 없습니다. 나중에 필요하다면 코드를 채워 넣도록 하지요.


    // 게임로직 업데이트
    void Update()
    {
    }


    다음은 정작 무언가를 그리는 함수인 RenderFrame()입니다.

    //------------------------------------------------------------
    // 렌더링
    //------------------------------------------------------------

    void RenderFrame()
    {


    우선 화면을 파란색으로 지웁니다.

        D3DCOLOR bgColour = 0xFF0000FF; // 배경색상 - 파랑

        gpD3DDevice->Clear( 0, NULL, (D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER),
            bgColour, 1.0f, 0 );


    그 후, 장면(scene)과 디버그 정보를 그립니다.

        gpD3DDevice->BeginScene();
        {
            RenderScene();  // 3D 물체등을 그린다.
            RenderInfo();  // 디버그 정보 등을 출력한다.
        }
        gpD3DDevice->EndScene();



    모든 그리기를 마쳤다면 백 버퍼에 저장되어 있는 렌더링 결과를 화면에 뿌려줍니다.

        gpD3DDevice->Present( NULL, NULL, NULL, NULL );
    }



    현재 3D 물체 등을 그리는 RenderScene() 함수에는 아무 코드도 들어있지 않습니다. 다음 장에서 3D 물체를 그릴 때 여기에 코드를 채워 넣도록 하지요.

    // 3D 물체 등을 그린다.
    void RenderScene()
    {
    }


    RenderInfo() 함수는 간단한 키 매핑 정보를 화면에 보여줍니다.

    // 디버그 정보 등을 출력.
    void RenderInfo()
    {
        // 텍스트 색상
        D3DCOLOR fontColor = D3DCOLOR_ARGB(255,255,255,255);  

        // 텍스트를 출력할 위치
        RECT rct;
        rct.left=5;
        rct.right=WIN_WIDTH / 3;
        rct.top=5;
        rct.bottom = WIN_HEIGHT / 3;
     
        // 키 입력 정보를 출력
        gpFont->DrawText(NULL, "데모 프레임워크\n\nESC: 데모종료", -1, &rct,
            0, fontColor );
    }


    프로그램을 종료할 때, GPU상의 메모리 누수를 방지하려면 D3D를 통해 생성했던 자원들을 모두 해제(release)해줘야 합니다. 자원들을 모두 해제한 뒤에는 D3D 장치와 D3D 객체들도 해제해 줍니다.

    //------------------------------------------------------------
    // 뒷정리 코드.
    //------------------------------------------------------------

    void Cleanup()
    {
        // 폰트를 release 한다.
        if(gpFont)
        {
            gpFont->Release();
            gpFont = NULL;
        }

        // 모델을 release 한다.

        // 쉐이더를 release 한다.

        // 텍스처를 release 한다.

        // D3D를 release 한다.
        if(gpD3DDevice)
        {
            gpD3DDevice->Release();
            gpD3DDevice = NULL;
        }

        if(gpD3D)
        {
            gpD3D->Release();
            gpD3D = NULL;
        }
    }



    자, 이것으로 아주 간단한 쉐이더 프레임워크의 작성을 마쳤습니다. 위의 코드가 잘 이해가 안 되시는 분들이 계실지도 모르겠는데, 이 책에서 HLSL 프로그래밍을 익히는 데는 크게 문제가 안됩니다. 단, 그래픽 프로그래머가 되는 것이 꿈이신 분들은 이 책을 마친 뒤에라도 반드시 DirectX를 제대로 배우시라고 권해드리고 싶습니다.

    여기까지 다소 지루한 준비과정을 견뎌내시느라 수고하셨습니다. 후딱 정리를 마친 뒤에 다음 장부터 실제로 재미있게 쉐이더를 작성해 보기로 하죠!

    정리
    다음은 이 장에서 배운 내용을 짧게 요약해 놓은 것입니다.

    • 쉐이더는 어느 픽셀을 어떤 색으로 칠할지를 계산하는 함수이다.
    • 쉐이더를 화가가 그림을 그리는 것에 비유하면 정점쉐이더는 투시를 픽셀쉐이더는 명암을 담당한다.
    • 쉐이더 프로그래밍이란 정점쉐이더와 픽셀쉐이더에서 실행시킬 함수를 작성하는 것이다.
    • AMD사의 렌더몽키를 사용하면 재빨리 쉐이더를 프로토타입할 수 있다.

    p.s. 여기는 댓글 달릴때마다 이메일이 오지 않아서 답변을 빨리빨리 드리기가 쉽지 않군요. 제 개인 블로그에 포스팅 된 버전에 댓글을 다시는게 제일 답변이 빠릅니다. 오탈자 및 잘못된 내용 수정도 제일 빠릅니다.


    포프 올림




    1. DirectX 10과 11에서 새로운 쉐이더들이 추가되었습니다. 하지만 아직 실무에서 널리 사용되지 않아서 실용적인 접근이 어렵고, 입문자에게 적당하지 않은 내용이라 이 책에서 다루지 않습니다. [본문으로]
    2. OpenGL Shader Language의 약자로 OpenGL에서 지원하는 쉐이더 언어입니다. HLSL과 문법 정도가 조금 다릅니다. [본문으로]
    3. 엔비디아에서 지원하는 쉐이더 언어입니다. HLSL과 한두 개 빼고는 완전히 똑같습니다. [본문으로]
    4. 지면이 늘어나면 쓸데없이 책 값도 오릅니다. [본문으로]
    KGC 2011에서 발표했던 자료입니다. 

     

    [미리보는 C++ AMP-2] C++ AMP 맛 보기

    DirectX 11 2011. 12. 1. 08:00 Posted by 알 수 없는 사용자


    백문이 불여일견이라고들 하죠?
    글로써 언급하는 것보다,
    프로그래머들은 코드로 볼 때 더 직관적인 이해를 할 수 있는 경우가 많습니다.

    간단하게 두 배열의 합을 구하는 코드를 통해서,
    이를 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에서 사용된 예제들입니다.
    제가 구현한 것들이 아님을 알려드립니다.^^


    제1장 쉐이더란 무엇이죠?

    쉐이더의 정의
    제가 학생들을 가르치면서 제일 처음 듣는 질문 중 하나가 '도대체 쉐이더가 뭐에요?'였습니다. 사실 뒤돌아보면 제가 쉐이더를 처음 접할 때도 스스로 이런 질문을 던지곤 했었는데 그 누구도 저에게 이해하기 쉽도록 '쉐이더란 바로 이런 것이다!' 라고 설명을 해준 적이 없더군요. 또한, 기존의 자료들도 찾아봤는데 학생들이 쉽게 이해할만한 정의를 내려주는 자료를 찾을 수 없었습니다. 그래서 제 맘대로(?) 아주 쉽게 정의를 내렸습니다. 쉐이더란 화면에 출력할 픽셀의 위치와 색상을 계산하는 함수입니다. 어휘적/구조적인 측면에서 쉐이더를 살펴보면 이를 자세히 이해할 수 있을 겁니다.

    어휘적 접근
    사실 영어만 잘해도 거저 주워 먹는 것이 많은 분야가 컴퓨터 프로그래밍입니다. 쉐이더만 해도 크게 예외는 아닌데요. 쉐이더(shader)란 '색의 농담, 색조, 명암 효과를 주다.'라는 뜻을 가진 shade란 동사와 행동의 주체를 나타내는 접미사 '-er'을 혼합한 단어입니다. 즉, 색의 농담, 색조, 명암 등의 효과를 주는 주체가 쉐이더란 뜻이지요. 컴퓨터 그래픽에서 색이라 하면 당연히 화면에 등장하는 픽셀의 색상이므로 이를 다시 정리하면 다음과 같습니다.

    쉐이더는 픽셀의 농담, 색조, 명암을 결정한다.

    여기서 농담, 색조, 명암이라고 하니 '아니, 그렇다면 쉐이더가 출력하는 결과가 3개나 된다는 말인가요?'라고 하시는 분들이 계실 듯한데 그런 것은 아닙니다. 쉐이더의 최종결과는 농담, 색조, 명암 효과를 전부 짬뽕해서 나온 RGBA색상 값 하나입니다.[각주:1] 미술시간에 수채화 그려봤던 것 기억하시죠? 일단 밑그림을 완성하면 물감의 색을 고르고, 여기에 물을 혼합시키는 양을 바꿔가면서 다양한 명암효과를 냅니다. 하지만, 일단 그림이 완성되면 캔버스에 있는 결과는 결국 최종색상뿐이죠? 쉐이더도 이와 마찬가지입니다. 온갖 기법들을 이리저리 섞어서 픽셀들의 최종 색상 값을 구하는 것이 바로 쉐이더입니다.

    구조적 접근
    저희가 이 책에서 다루는 쉐이더는 정점쉐이더(vertex shader)와 픽셀쉐이더(pixel shader)인데 위의 어휘적 접근에서 살펴봤던 쉐이더의 정의는 이 중 하나에만 적용됩니다. 어떤 것일까요? 네, 그렇습니다. 픽셀쉐이더 입니다. 그렇다면 정점쉐이더란 무엇일까요? 이걸 이해하려면 3D 그래픽파이프라인의 구조를 살펴봐야겠군요.

    3D 파이프라인이 존재하는 이유 중 하나는 3차원 공간에 존재하는 물체를 컴퓨터 모니터라는 2차원 평면 위에 보여주기 위해서입니다. 우선 3D 그래픽파이프라인을 극단적으로 간략화시킨 그림 1.1을 살펴봅시다.[각주:2]

    그림 1.1 극단적으로 간략화시킨 3D 파이프라인




    그림 1.1에서 정점쉐이더가 입력 값으로 받는 것은 3D 모델 자체입니다. 3D 모델은 폴리곤(polygon, 다각형)으로 구성하는 것이 업계표준인데, 폴리곤이란 결국 삼각형들의 집합에 지나지 않습니다. 삼각형은 3개의 정점(vertex)[각주:3]으로 이뤄져 있죠? 그러니 정점데이터가 정점쉐이더의 입력 값이라고 해도 전혀 틀린 게 아니겠네요.

    정점쉐이더가 수행하는 가장 중요한 임무는 3D 물체를 구성하는 정점들의 위치를 화면좌표로 변환하는 것입니다. 이를 화가에 비유한다면 투시원근법을 사용하여 실제세계에 있는 물체들을 캔버스 위에 옮겨 그리는 과정이라 할까요? 이렇게 물체의 위치를 다른 공간으로 옮기는 과정을 공간변환(space transformation)이라고 부르는데 이에 대한 자세한 설명은 다음 장에서 드리도록 하겠습니다. 조금 전에 3D 모델은 결국 정점들의 집합이라고 말씀드렸었죠? 따라서 모든 정점을 하나씩 공간 변환하면 3D 물체 자체를 공간 변환하는 것과 똑같은 결과를 얻을 수 있습니다. 이게 바로 정점쉐이더가 하는 일이지요. 그렇다면 정점쉐이더 함수는 몇 번이나 호출될까요? 다음 문장을 보시면 답을 아실 수 있겠네요.

    정점쉐이더의 주된 임무는 각 정점의 공간을 변환하는 것이다.

    네, 그렇습니다. 정점쉐이더는 3D 물체를 구성하는 정점의 수만큼 실행됩니다.

    정점쉐이더가 반드시 출력하는 결과 값은 화면공간 안에 존재하는 정점의 위치[각주:4]입니다. 이 위치를 3개씩 그룹 지으면 화면에 출력할 삼각형을 만들 수 있지요.

    자, 그렇다면 이 삼각형 안에 픽셀이 몇 개나 들어갈까요? 화면을 구성하는 단위는 픽셀이니까 화면에 뭔가 그림을 그리려면 픽셀을 어디에 몇 개나 그려야 하는지를 알아야겠죠? 이게 바로 래스터라이저(rasterizer)란 장치가 하는 일입니다. 래스터라이저는 정점쉐이더가 출력하는 정점의 위치를 차례대로 3개씩 모아 삼각형을 만든 뒤, 그 안에 들어갈 픽셀들을 찾아냅니다. 자, 그러면 픽셀쉐이더 함수는 몇 번이나 호출될까요? 래스터라이저가 찾아내는 픽셀 수 만큼이겠죠?

    그렇다면 위에서 보여드렸던 초 간략 파이프라인의 마지막 단계인 픽셀쉐이더가 하는 일은 무엇일까요? 이미 위에서 살펴본 것 같지만 그래도 다시 한번 반복해 드리지요.

    픽셀쉐이더의 주된 임무는 화면에 출력할 최종색상을 계산하는 것이다.

    이제 정점쉐이더와 픽셀쉐이더의 임무를 합치면 아까 제 맘대로 내렸던 쉐이더의 정의가 나오죠?

    쉐이더란 화면에 존재하는 각 픽셀의 위치와 색상을 계산하는 함수이다.

    솔직히 이 정도 말씀을 드려도 쉐이더를 처음 접하시는 분들은 아직도 감이 안 잡히실 겁니다. 사실 쉐이더를 짜 보지 않으면 이해가 어렵습니다. 3D 물체를 화면에 그릴 때, 그 물체를 구성하는 픽셀들의 위치와 색을 프로그래머 맘대로 조작하는 거라고 하면 이해가 좀 더 되실까요? 아직도 이해가 안되시더라도 크게 걱정은 마세요 이 책을 읽으시다 보면 '아~ 이런 거였구나~'하고 갑자기 이해가 되실 겁니다. ^^


    p.s. 여기는 댓글 달릴때마다 이메일이 오지 않아서 답변을 빨리빨리 드리기가 쉽지 않군요. 제 개인 블로그에 포스팅 된 버전에 댓글을 다시는게 제일 답변이 빠릅니다. 오탈자 및 잘못된 내용 수정도 제일 빠릅니다.



    포프 올림



    1. 쉐이더가 반드시 한가지 색상만을 출력해야 하는 것은 아닙니다. 고급 쉐이더 기법들에서는 다수의 결과를 동시에 출력하는 경우가 있습니다. [본문으로]
    2. 이 그림은 정점쉐이더 및 픽셀쉐이더의 역할을 이해하기 위해서 극단적으로 간략화시킨 버전입니다. 실제 그래픽 파이프라인은 이 그림에 나와 있는 것보다 훨씬 복잡합니다. [본문으로]
    3. 꼭짓점이라고도 합니다. [본문으로]
    4. 이외에도 다양한 정보를 정점쉐이더의 결과 값에 담을 수 있습니다. 자세한 내용은 이 책의 뒷부분에서 살펴볼 것입니다. [본문으로]

    [StartD2D-10] 디바이스 로스트( Device Lost ) 처리하기

    DirectX 11 2011. 11. 23. 08:00 Posted by 알 수 없는 사용자

    앞서 작성했던 예제 샘플들은 사실 완전한 상태의 코드가 아닙니다. ( 죄송...^^ )
    바로 이 디바이스-로스트와 관련한 상황이 있어야, 안정성을 향상시킬 수 있습니다.

    디바이스-로스트의 처리는 다음과 같은 절차를 진행해야 합니다.

    1. 디바이스-로스트가 발생함을 체크
    2. 디바이스 의존적 리소스를 모두 제거
    3. 디바이스 의존적 리소스를 재할당


    이 세가지를 처리하는 것이 디바이스-로스트 상황에 대처하는 것입니다.
    Direct2D 의 리소스와 관련한 내용은
    http://vsts2010.net/593 글에서 제가 언급했었습니다.^^

    그러면 하나씩 살펴보겠습니다.
    우리가 진행했던 일반적인 렌더링 작업은 아래와 같습니다.


    여기서 우리의 첫번째 단계를 처리합니다.
    바로 hr = ::g_ipRT->EndDraw(); 부분입니다.
    EndDraw()는 렌더링 작업의 결과를 리턴합니다.
    리턴 값이 D2DERR_RECREATE_TARGET 이면, 바로 디바이스-로스트 상황입니다.
    이름에서 유추할 수 있듯이 "에러가 났으니, 다시 생성하라" 입니다.

    이번에 제가 사용할 샘플은 바로 이전 시간에 했던 알파이미지를 렌더링하는 샘플입니다.
    이 샘플에서 디바이스 의존적인 리소스는 렌더 타겟과 비트맵입니다.
    이 샘플에서 디바이스-로스트 상황이 발생한다면,
    렌더타겟과 비트맵 리소스는 메모리에서 제거했다가 다시 생성해 주어야 합니다.

    그래서 이번 샘플에서는 이들 리소스를 한번에 생성/삭제 하는 함수를 만들었습니다.


    그리고 또 하나 고려해야 하는 부분이 있습니다.
    바로 윈도우 사이즈의 변경입니다.
    이 경우는 디바이스-로스트가 발생하지는 않습니다만,
    순간적으로 화면이 깜빡이는 현상을 보이게 됩니다.
    이 곳에도 역시 적절한(?) 처리를 해야 합니다.


    렌더타겟의 리사이즈 작업이 실패하면,
    역시 디바이스 의존적 리소스들을 모두 제거해 버립니다.

    이제 WM_PAINT 이벤트를 위와 관련된 작업들과 연계해서 수정해야 합니다.


    렌더타겟이 없는 경우는 디바이스-로스트 상황이거나 초기화 상태로 인식하고,
    관련된 리소스를 생성합니다.
    그리고 렌더타겟의 CheckWindowState()를 통해서 해당 윈도우가 가려져 있는지를 체크하고,
    가려져 있지 않다면 렌더링 작업을 수행합니다.

    렌더링 작업의 마지막에는 디바이시 로스트 상황을 체크해서
    디바이스 의존적 리소스를 제거하고 있습니다. ( 앞서 언급했었죠..^^ )

    이제 샘플이 약간은 안정성이 향상되었습니다.^^
    이상으로 디바이스-로스트와 관련한 작업을 마치겠습니다.^^


    [미리보는 C++ AMP-1] C++ AMP를 구상해 보다!

    DirectX 11 2011. 11. 22. 08:00 Posted by 알 수 없는 사용자

    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를 직접 제어하는 것보다는 쉽습니다.

    다음 시간부터 C++ AMP 로 프로그래밍 하는 개념에 대해서 살펴보겠습니다.^^

    [포프의 쉐이더 입문강좌] 00. 들어가며

    Shader 2011. 11. 21. 09:30 Posted by 김포프

    (원래 출판을 목적으로 썼던 책인데 출판사 사정으로 책 출판이 어려워져 인터넷에 그대로 공개합니다.)

    들어가며

    안녕하세요. 캐나다 렐릭 엔터테인먼트에서 시니어 그래픽 프로그래머로 일하고 있는 포프입니다. 이 쉐이더 입문 책은 제가 2007년 1월부터 2009년 12월까지 3년간 캐나다의 The Art Institute of Vancouver 대학에서 쉐이더 프로그래밍 강의를 하면서 축적한 자료와 지식을 글로 옮겨 놓은 것입니다.

    책을 쓰게 된 배경
    2007년에 제가 강의를 시작할 때, 수업시간에 사용할 교과서를 찾으려고 참 많은 노력을 했습니다. 하지만 시중에 나와있는 책들 중, 쉐이더 입문과목에 적합한 놈이 없더군요. (몇 년이 지난 지금에도 마찬가지인 것 같습니다.) 시중에 나와있는 쉐이더 책들은 대부분 이미 쉐이더 코드를 짤 줄 아는 중고급 프로그래머를 위한 것이었습니다. 따라서 쉐이더에 입문하는 학생들이 보면 뭔 소린지 몰라서 그냥 포기할 게 뻔했죠. 그나마 쉐이더 입문 내용이 DirectX 책에 담겨있는 경우가 있었지만 그 중에서도 마땅한 책이 없다고 생각했던 이유가

    • 쉐이더는 구색 맞추기 식으로 넣어놓아서 너무 수박 겉 핥기 식이다.
    • 학계에 계신 분들이 쓴 책은 너무 이론이나 문법에만 치우쳐져 있다.
    • 실무에 그다지 쓸모가 없는 내용들을 너무 많이 담고 있다.
    • 지면수만 많아 책값이 너무 비싸다.
    등 이었습니다. 그래서 결국엔 교과서 없이 강의를 시작했죠. 이론이나 수학에 치우치기 보다는 실무에 곧바로 쓸 수 있거나 실무에서 쓸 수 있는 기법의 기초가 되는 내용들만을 가르치는데 주력했습니다. 강의를 하면서 좋았던 점은 저는 그리 어렵지 않다고 생각해왔는데 학생들이 이해하지 못하는 부분들을 알아낼 수 있다는 거였죠. 그래서 그걸 다시 쉽게 이해시킬 수 있도록 강의자료를 다듬고 다듬은 결과가 바로 이 책입니다. 강의를 하는 3년 내내 게임프로그래밍 학과 학생들이 이 과목을 AI 대학의 가장 훌륭한 수업으로 꼽을 정도였으니 (좀 부끄럽지만) 이 책을 자신있게 권해드릴 수 있을 것 같습니다. 그리고 제 과목에서 만든 데모 프로그램을 포트폴리오로 삼아서 Ubisoft 및 EA같은 세계 유수의 회사에 취직한 학생들도 몇 됩니다. 가슴 뿌듯한 일이죠. ^^

    현재는 게임개발에 좀 더 집중해 보려고 대학강의를 중단한 상황이지만 이 내용을 그냥 썩혀두기엔 아깝다고 생각되어 책을 내기로 결정을 했습니다. 이 책이 쉐이더를 배우시려는 분들에게 도움이 될 수 있었으면 좋겠습니다.

    이 책의 기본원칙
    강의에서도 그랬듯이 이 책을 쓸 때 다음의 원칙을 기초로 삼았습니다.

    • 실습 위주: 물론 쉐이더를 짤 때 수학이나 이론을 전혀 무시할 수는 없습니다. 하지만, 이론을 먼저 배우고 그걸 코드로 옮기는 것보단 일단 코드를 좀 짜본 뒤에 뭔가 막히면 이론을 찾아보는 것이 훨씬 훌륭한 학습방법입니다. 이렇게 문제를 해결하기 위해 찾아본 이론은 기억에 오래 남습니다. 따라서 이 책은 실습위주로 구성되어있습니다. 책의 내용을 한 줄씩 따라 하면서 코드를 짜다 보면 어느덧 배경 이론까지 적당히 이해하시게 될 겁니다.
    • 쉬운 설명: 제 수업에 청강을 하러 오는 게임아트 학과 학생들도 꽤 있었습니다. 따라서 아티스트들도 이해할 수 있도록 쉽게 설명을 하는 것이 제 목표 중 하나였습니다. 그러려면 무언가를 설명할 때, 수학공식을 보여주기 보다는 실제 생활에서 일어나는 현상을 예로 드는 것이 낫더군요. 이 책을 쓸 때도 마찬가지 원칙을 따랐습니다. 책을 읽으시다 보면 100% 이론적으로 옳지 않은 설명들도 가끔 보실 겁니다. 이것은 말 그대로 설명을 쉽게 하기 위해 제가 이론들을 적당히 무시하였거나 아니면 저 조차 이론을 100% 제대로 이해하지 못하는 경우입니다. 게임 그래픽은 어차피 눈에 보이는 결과가 맞으면 그게 정답인 분야이므로 이론적으로 약간 틀려도 결과만 맞으면 전 크게 신경 쓰지 않습니다.
    • 입문자만을 위한 책: 이 책은 순수하게 입문자를 위한 책입니다. 이미 고급기법을 다루는 훌륭한 책이 많이 나와있는 상황에서 굳이 그 책들과 경쟁할 필요를 못 느끼고, 중복되는 내용을 다루면서 지면수를 늘리고 싶지도 않기 때문입니다. 이 책을 보신 후에 쉐이더에 재미가 붙으신 분들이 다른 고급기법들을 즐겁게 찾아 보실 수 있다면 전 행복합니다. 그리고 정말 괜찮은 새 기법을 찾으시면 저에게 살짝 귀뜸이라도 해주시면 더 좋겠지요. ^^
    • 순서대로 배우는 내용: 강의를 할 때 좋았던 점은 쉬운 내용부터 어려운 내용까지 순서대로 가르칠 수 있었다는 것입니다. 이 책도 그런 식으로 진행이 됩니다. 처음 장부터 시작해서 천천히 지식을 축적해간다고 할까요? 따라서 뒷장으로 가면 갈 수록 기본적인 내용은 다시 설명을 하지 않습니다. 예를 들면, 법선매핑을 배우기 전에 이미 조명기법들을 배워보므로 법선매핑에서는 조명기법에 대해 다시 설명하지 않는거죠. 따라서 이 책을 읽으실 때는 대학강의를 들으시듯이 처음부터 순서대로 읽으셔야 합니다. 이 책이 다른 쉐이더 책들처럼 여러 논문을 한군데 모아놓은 게 아니니 그 정도는 이해해주시면 좋겠습니다. 그리고 지면수도 그리 많지 않으니 무리한 요구는 아닐 거라고 믿습니다.

    이 책에서 다루는 내용
    이 책에서 다루는 내용은 정점쉐이더와 픽셀쉐이더를 이용한 쉐이더 기법들입니다. 이 책은 크게 세 부분으로 나뉘어져 있습니다.
    • 제1부: 쉐이더의 정의를 알아 본 뒤, 모든 쉐이더 기법의 기초가 되는 색상, 텍스처 매핑, 조명 쉐이더를 만들어 봅니다.
    • 제2부: 1부에서 배운 내용에 살을 붙여 게임에서 널리 사용하는 스페큘러매핑, 법선매핑, 그림자 매핑 등의 기법들을 구현합니다.
    • 제3부: 요즘 게임에서 점점 중요해져 가는 2D 이미지 처리 기법을 배워봅니다.


    이 책에서 DirectX 10과 11에서 새로 추가된 지오메트리(geometry), 헐(hull), 연산(compute) 쉐이더들을 다루지 않는 이유는 초급자에겐 좀 어려운 내용일 뿐만 아니라 아직 실무에서 널리 이용되지 않기 때문입니다. 따라서 실용적인 내용을 알려드리기가 좀 어렵죠. DirectX 10이 처음 소개될 때만 해도 홍보자료에서는 엄청 대단한 것처럼 광고를 해댔지만 실제 실무에서 제대로 이용한 경우가 없으니까요. 일단 이 책에서 기초를 다잡으시면 몇 년 뒤에  이 내용을 배우셔도 크게 문제가 없을 겁니다.

    대상독자
    프로그래머
    제가 가르쳤던 학생들은 게임 프로그래밍 학과 2학년 학생들이었습니다. 제 과목을 듣기 전에 C++, 3D 수학, DirectX 등을 이미 마친 학생들이었지요. 게임개발자 분들이 쉐이더 프로그래밍에 입문하는 과정도 이와 다르지 않다고 생각합니다. 최소한 DirectX는 마치신 뒤에 쉐이더를 살펴보시는 게 보통이니까요. 이 책의 대상독자도 마찬가지로 하겠습니다. 이 책을 보시려면 최소한 C++과 DirectX 정도는 공부하셨어야 합니다. 3D 수학까지 아시면 더 도움이 되겠습니다.

    테크니컬 아티스트
    요즘 들어 프로그래머와 아티스트 사이를 조율해주는 테크니컬 아티스트 분들의 입지가 높아지고 있습니다. 그리고 이제 테크니컬 아티스트들이 쉐이더 프로토타입을 만드는 경우도 허다합니다. 강의를 하는 도중에 일반 아티스트(청강생)들도 어느 정도 이해를 했던 내용들이니 테크니컬 아티스트 정도 되시면 아무 문제가 없으시겠지요? 테크니컬 아티스트들은 굳이 DirectX를 직접 다루지 않아도 되니 별다른 준비사항 없이 이 책을 보셔도 될 것 같습니다. (보시다 이해가 안 되는 수학 같은 게 있으시면 정석 책을 열어보시거나 인터넷 검색을 좀 하셔야 할지도 모르지만요. ^^) 각 장의 마지막에 DirectX 프레임워크를 다루는 부분이 있는데 그 부분만 건너 뛰시면 됩니다.

    온라인 커뮤니티
    이 책을 보시다가 궁금하신 것이 있으시면 제 블로그로 오시기 바랍니다. 토론장을 열어두겠습니다. 그 외에 정오표나 기타 업데이트들도 이 사이트를 통해 공개할 예정입니다.


    (사실 이미 제 블로그에 이 글을 올리는 마당에 정오표나 기타 업데이트들을 굳이 갈렉산드리아에 올릴 필요가 있나 모르겠습니다. -_-)

    감사의 말씀
    이 책이 나오기까지 많은 분들이 도움을 주셨습니다. 이 자리를 빌어 감사의 뜻을 표현하는 것이 최소한의 도리라고 생각합니다.

    우선 이 책을 쓸 수 있는 계기를 마련해주신 조진현님께 감사의 말씀을 드리고 싶습니다.

    강의실에서 학생들과 직접 얼굴을 맞대면서 가르친 내용을 책으로 옮기는 건 사실 쉬운 일이 아니었습니다. 강의실 환경과는 달리 책은 일방적인 의사소통 수단이어서 과연 제가 말하고자 하는 바가 독자분들께 잘 전달이 될런지 매우 걱정이 되더군요. 이 때, 이 책의 내용과 샘플코드들을 꼼꼼히 테스트 해주신 개발자 분이 두 분 계십니다. 두 분 다 제 대상독자층에 속한 분이셨죠. 한 분은 이미 게임개발업계에 꽤 계셨지만 쉐이더 프로그래밍은 안 하셨던 분이고, 다른 분은 일반 프로그래머 일을 시작한 지 얼마 안 되시는 분입니다. 이 분들이 책을 처음부터 끝까지 꼼꼼히 읽어주시고, 코드를 한 줄 씩 직접 테스트해 주신 덕에 잘못된 내용을 최소한으로 줄일 수 있었습니다. 또한 이 분들이 보내주신 피드백에 따라 부족한 내용을 보완한 덕에 더욱 튼실한 책을 만들 수 있었죠. 유스하이텍의 이경배님과 네오플의 송진영님, 정말 많은 도움이 되었어요. 고맙습니다.

    마지막으로 이 책의 준비 단계부터 블로그와 트위터를 통해 많은 관심을 가져주시고 응원해주신 전직/현직/미래 게임개발자 분들과 일반인(?) 분들께 감사의 말씀을 드리고 싶습니다. 강다니엘, 고경석, 김동환, 김성완, 김영민, 김정현, 김혁, 김호용, 박경희, 박수경, 손기호, 신성일, 안진우, 유영운, 이경민, 이상대, 최재규 님, 책 나왔어요~~~


    p.s. 여기는 댓글 달릴때마다 이메일이 오지 않아서 답변을 빨리빨리 드리기가 쉽지 않군요. 제 개인 블로그에 포스팅 된 버전에 댓글을 다시는게 제일 답변이 빠릅니다. 오탈자 및 잘못된 내용 수정도 제일 빠릅니다.


    2011년 3월 캐나다 밴쿠버에서
    포프 올림


    코딩된 UI 테스트 에 커스텀 어설션 추가하기.

    Visual Studio 2010 2011. 11. 15. 09:00 Posted by 알 수 없는 사용자
    코딩된 UI 테스트를 하려고 하다 보면, '코딩된 UI 테스트 빌더'를 사용하게 됩니다.


    그런데 코딩된 UI 테스트 빌더를 사용하다 보면, 어설션을 추가할 수 있는 속성의 수가 매우 제한 적인 것을 알 수 있습니다. 예를 들어서, WPF의 리스트 박스에 항목이 몇 개 있는지를 나타내는 Count와 같은 속성에 어설션을 추가하고 싶은데, 그런 속성을 코딩된 UI 테스트 빌더를 사용하면 찾을 수가 없는 것이죠.


    이럴 때는 코드를 통해서 어설션을 추가해야 합니다. 그래서 다음과 같이 UIMap.Designer.cs에 어설션을 추가합니다.


    그리고 테스트를 실행하면, 다음과 같이 테스트가 통과하게 되죠.


    하지만, 코딩된 UI 테스트 빌더를 이용해서 새 테스트를 추가하거나, 편집을 수행하는 순간 UIMap.Designer.cs에 추가한 코드는 날아가 버립니다. 이 때문에 UIMap클래스는 partial 클래스로 선언이 되어 있습니다. 디자이너가 생성하는 코드는 UIMap.Designer.cs에, 그리고 코딩된 UI 테스트 빌더를 사용해서 추가하기 힘든 코드는 UIMap.cs에 직접 추가하게 되는 것이죠. 그렇다면 위 테스트는 UIMap.cs에 다음과 같이 코드를 추가하면 간단하게 해결 됩니다.

    public partial class UIMap
    {
        /// <summary>
        /// 리스트 박스의 항목 개수를 테스트하는 메서드
        /// </summary>
        public void CheckListItemCount()
        {
            int expectedCount = 3;
            int actualCount = this.UIMainWindowWindow.UIMyListBoxList.Items.Count;
            Assert.AreEqual(expectedCount, actualCount);
        }
    }

    그리고 테스트 메서드에서 CheckListItemCount를 호출하도록 코드를 추가해주면 되는 것이죠.

    [TestMethod]
    public void CodedUITestMethod1()
    {
        // 이 테스트의 코드를 생성하려면 바로 가기 메뉴에서 "코딩된 UI 테스트에 대한 코드 생성"을 선택한 다음 메뉴 항목 중 하나를 선택하십시오.
        // 생성된 코드에 대한 자세한 내용은 http://go.microsoft.com/fwlink/?LinkId=179463을 참조하십시오.
     
        this.UIMap.SelectionTest();
     
        this.UIMap.CheckListItemCount();
    }

    그러면, 의도 했던 테스트가 제대로 추가 되는 것을 볼 수 있습니다.

    정말 오랜만에 썼는데, 쓰고 나니 별 내용이 없는 포스트 네요 ㅠ_ㅠ;; 다음에 또 찾아 뵙겠습니다.

    ps.
    예제 코드를 첨부해드립니다. 예제는 Visual Studio 2010에서 작성했습니다.

    [KGC 2011] 발표 자료

    DirectX 11 2011. 11. 14. 08:00 Posted by 알 수 없는 사용자


    안녕하세요~ 조진현입니다.
    얼마 전 대구에서 KGC 2011 행사가 있었습니다.
    저는 그 곳에서 DirectX11 과 관련한 발표를 진행하고 왔습니다.
    그래서 발표 슬라이드를 공개해 드립니다.

    특히나 이번 발표 때, C++ AMP 에 대한 언급이 있었습니다.
    AMP는 아래 링크에 흥배님이 자세히 설명해 주셨습니다.
    http://vsts2010.net/591

    빠른 시간 내에 발표 때 언급했던 C++ AMP 와 관련한 내용을
    팀 블로그에 게재하도록 하겠습니다..^^

    매뉴얼 테스트와 테스트 자동화. 정말 그 범위를 정하기도 힘들고, 잘 하기도 힘든 가장 기초적인 테스트 방법입니다. 아마도 대다수의 사람들이 알고 있는 테스트의 기본이기도 합니다.

    메뉴얼 테스트(Manual Test)는 수동 테스트라는 의미로 테스터에 의해 직접 수행하여 테스트 결과를 기록하는 방식이며,
    테스트 자동화(Automated Test)는 자동 테스트라는 의미로 프로그래밍이나 스크립트에 의해 자동으로 테스트를 수행하여 결과를 기록하는 방식입니다.

    개발자라면 테스트 자동화를 최우선으로 여기지만, 사실은 매뉴얼 테스트의 영역이 갖는 테스트의 의미는 매우 비중이 높습니다. 많은 사람이 오해하는 것 중에 하나가 자동화 테스트가 정교하고 정확하다고 생각한다는 것입니다. 최근 추세도 자동화 테스트에 매우 큰 비중을 갖고 시스템을 구축하고 테스트 인력을 양산하고 있는 것도 사실입니다. 필자가 전제를 말씀을 드리자면 자동화 테스트는 그 신뢰도가 그리 높지 않습니다.

    반면, 매뉴얼 테스트는 매번 인력이 투입되는 테스트이기 때문에 테스트 수행 비용이 매우 비싼 편이며, 테스트 수행 속도가 느리지만 그 신뢰도가 매우 높은 편입니다.

    아마도 독자 여러분들은 매뉴얼 테스트가 왜 테스트 결과의 신뢰도가 높은지 의아해 할 수 있을지도 모릅니다. 최근 테스트 자동화를 구축하기 위해서 모든 테스트 케이스의 90%, 많게는 95%이 이르기 까지 테스트를 자동화하기 위해 노력하고 있습니다. 즉, 메뉴얼 테스트:자동화 테스트=95:5 라는 비율을 갖게 되는데, 이것의 비율에 대한 신뢰 퍼센테이지는 매뉴얼 테스트가 훨씬 더 신뢰도가 높다는 의미입니다. 바꾸어 말하면, 매뉴얼 테스트로 수행하는 테스트 케이스가 몇 되지 않기 때문에 그 만큼 테스트의 신뢰할 수 있는 확률이 높다는 의미입니다.

       

       

    하지만 이런 꿈 같은 이야기는 안타깝게도 단지 수치적이고 해외의 사례입니다. 우리나라의 테스트는 전혀 이런 이상적인 환경을 따라가지 못하고 있습니다. 그 이유 또한 이전의 포스트 중 "[ALM-Test] 8. 소프트웨어 테스트 후진국 "대한민국"에서 언급하였듯이 테스트를 대하는 자세, 테스트에 대응하는 자세가 아직 성숙되지 않았기 때문입니다.

       

       

    자동화 테스트부터 이야기 해 봅시다.

    실제로 소프트웨어 세계 1위 기업인 Microsoft 는 처음 기업을 만들면서 납품하던 여러 가지 DOS(Disk Operation System) 제품부터 테스트의 중요성을 깨닫고(깨닫기 보다 그들이 이미 소프트웨어계 최고의 인물이기 때문에…) 이 DOS 제품에 대한 테스트를 적극적으로 투자하였습니다. DOS 제품을 만들던 당시 여러 나라와 다양한 컴퓨터의 요구 사항에 맞는 DOS 제품을 거듭 납품하기 전부터 하나 둘 씩 테스트 인력을 확보하기 시작했습니다.

    운영체제로 동작하는 DOS 제품은 시스템 동작에 가장 기본적으로 제공하는 인터페이스를 제공하는 매우 중요한 제품이기 때문에 테스트에 대해 명확한 철학을 가지고 있었습니다. DOS 제품 자체는 내부적으로 인터럽트(Interrupt) 라고 하는 이벤트 기반으로 시스템과 디바이스를 제어하고 이것을 이용하여 동작하는 소프트웨어가 상당하기 많이 보유했기 때문에 그 제품 자체가 매우 정교할 수 밖에 없습니다. (최종적으로 작은 기업이었던 Microsoft 는 MS-DOS 를 IBM PC에 납품하면서 점차적으로 거대 기업으로 자랄 수 있는 기반이 되었습니다)

    이 이후 Windows 95 가 출시되면서 굉장히 획기적인 기능이 추가되었습니다. 그것이 바로 플러그앤플레이(PnP-Plug in Play) 입니다. 이것은 독자들도 알다시피 새로운 하드웨어나 장치가 추가되면 자동으로 OS가 그 장치를 인식할 수 있는 기능이었습니다. 가령, 그래픽 카드를 사서 꼽았는데 Windows OS가 알아서 새로운 장치가 추가됨을 알려주고 드라이버를 설치하라고 알려주거나 자동으로 환경을 구성할 수 있는 기능을 일컫습니다. 데스크탑 컴퓨터 환경에서는 매우 획기적인 OS의 기능이었습니다.

    일단 다시 처음으로 돌아가서, 왜 자동화 테스트를 해야 하는가… 필자는 "하지 않아도 됩니다." 라고 말하고 싶군요. 애자일 개발을 수십 가지 지침 중의 하나가 바로 TDD(Test Driven Development-테스트 주도 개발) 입니다. 오해 중의 오해라고 하면 이 지침이 마치 필수 조건으로 인식되는 경우가 있습니다. 필자가 테스트를 하지 않다고 된다는 의미는 테스트를 할 만큼 규모가 없음에도 불구하고 테스트를 의무적으로 강조할 필요는 없다는 의미입니다.

       

    그럼 테스트 자동화를 위한 개발 환경이 다음에 적용된다면 자동화 테스트를 적극적으로 추천합니다. 각 항목이 참(True)라면 반드시 고려해보기 바랍니다.

    • A = ( 10명 이상의 개발자가 하나의 프로젝트에 투입되었다. )
    • B = A and ( 3명 이상의 개발자가 다른 개발자의 소스 코드나 컴포넌트에 연관되어 있다. )
    • ( A or B ) and ( 두 개 이상의 연계 시스템, 산하 시스템 또한 레거시 시스템이 존재한다. )
    • C = ( 소프트웨어 특성상 매우 복잡하거나 여러 가지 알고리즘을 사용한다 )
    • A = A or ( 5명 이상의 개발자와 2개 이상의 팀이나 파트가 협업을 해야 한다 )

       

    즉, A or B or C = True 라면 반드시 테스트 자동화를 고려하기 바랍니다.

    필자가 언젠가 세미나에서 언급한 적이 있습니다. 테스트는 개발자간의 신뢰라고요.

    "나는 너를 믿지만 너가 만들 코드는 믿지 않는다. 테스트 코드가 없다면…"

    여럿이 함께 개발해야 하는 환경에서 타인의 코드에 의한 오류인 경우 내가 낸 오류보다 더 화가 나는 것도 그 때문입니다. 신뢰~!

       

    왜 자동화 테스트의 신뢰도가 낮은가

    일반적으로 테스트를 자동화하면 할 수록 그에 대한 노력은 두 배가 됩니다. 일반적으로 자동화 테스트를 완벽하게 수행하는 경우 제품 코드보다 테스트 코드가 1.5~2개 가량 많습니다. 즉, 100,000 라인에 달하는 코드로 만든 제품은 최대 200,000 라인의 별도의 테스트 코드가 필요하다는 의미입니다. 왜 이렇게 제품 코드보다 테스트 코드가 많은지 궁금할 수 있습니다.

    그 이유는 다음과 같습니다. 테스트 대상이 어떤 것인지에 따라 테스트 기법은 매우 다양합니다. 그리고 다국적 제품인 경우 나라 별로 법적인 대응 코드, 화폐나 날짜 체계, 그리고 문화적 다양성 체계 등이 상당한 분량의 테스트 코드를 양산할 수 밖에 없습니다.

    • 테스트 계획 (Planning)
    • 기능 테스트
      • 동등 클래스 분할 기법
      • 경계 값 분석 기법
      • 조합 분석 기법
    • 구조적 테스트
      • 블록 테스팅
      • 결정 테스팅
      • 조건 테스팅
      • 기본 경로 테스팅
    • 모델링 기반 테스트
    • 다국적 지원을 위한 로케일(Locale) 테스트
    • 비기능 테스트
    • 테스트 이력 관리 및 버그 추적, 관리
    • 전반적인 테스트 품질 관리 활동

       

       

    그럼 다시 질문하자면, 왜 자동화 테스트의 신뢰도가 낮은가 입니다. 의외로 답은 매우 간단합니다. 개발 코드는 언제나 버그나 결함을 내포하고 있을 가능성이 있는데, 테스트 코드라고 다를 바가 없습니다. 테스트 자동화의 테스트 신뢰도는 테스트 코드 자체에 의존해야 하기 때문에 테스트 코드 자체가 잘못된 검증은 한다면 올바른 테스트가 아닙니다. 하물며, 개발자에게 엄격한 NullReferenceException 이나 Null Point 가 빈번하게 발생하는 버그라면, 테스트 코드 또한 개발 코드에 의해 동작하므로 이러한 잘못된 동작이 테스트 결과에 미치는 영향은 상당히 높을 수 밖에 없습니다. 쉽게 말해, 테스트 코드도 사람이 만드니까 실수를 한다는 것이죠.

    개발자에게도 버그나 결함으로 지속적으로 코드를 관리해야 하지만, 테스터에게도 테스트 코드를 꾸준히 관리해야 하는 의무가 있는 것입니다. Microsoft 의 자동 업데이트 기능으로 Windows OS 가 꾸준히 패치되고 있긴 하지만, 어떤 패치는 네트워크 기능을 마비시키거나 그래픽 드라이버가 마비되는 애교 만점 패치도 상당 수 존재합니다.

    이렇게 거대한 최대 소프트웨어 기업도 간지러운 애교에 많은 사용자가 분노하고 비방하고 욕설을 퍼붓고 운영체제의 안정성을 논하며 펌하 하는데, 왜 개발자들은 자신이 만드는 버그에 대해 그렇게 관대한지 더욱 사랑스럽기도(?) 합니다.

       

    다시 이전에 얘기하던 Windows OS의 플러그앤플레이(Pnp-Plug in Play) 에 대한 내용은 잠시 보류하고, 매뉴얼 테스트를 다루어 본 이후에 다시 원점에서 다시 논의해보도록 하겠습니다.

    최근 여러 달 동안 블로그를 관리하지 않아 이렇게 다시 글을 올리는 것이 약간은 부담이 되네요. 거의 1년이나 방치해둔 제 블로그에 '어떤 글을 첫 번째 글로 올려야 하나?' 라는 기본적인 것부터 시작해서 앞으로 어떤 주제를 가지고 지속적으로 블로그를 해야 하는지도 말입니다. 이미 마음속으로는 "테스트"라는 소프트웨어 공학적인 주제를 가지고 첫 번째 글을 쓸 것이라고 마음은 먹었지만, 그 동안 느낀 많은 것들 또한 천천히 블로그로 포스팅 하리라 약속을 드립니다.

       

    소프트웨어 테스트 후진국 "대한민국"

    소프트웨어 개발 중 테스트는 소프트웨어 개발 라이프사이클의 중요한 과정 중으로 하나임을 전산과를 전공하였다면 이미 알고 있을 것입니다. 그 만큼 소프트웨어 개발 과정 중 "테스트" 단계는 일련의 모든 이전 단계, 즉 분석, 설계, 개발이라는 범위의 기나긴 여정의 과정을 검증하는 단계이고, 그리고 이 과정에서 상당한 분량을 지식을 얻을 수 있는 과정이 테스트 과정이기도 합니다.

       

    필자가 현재 회사로 이직을 하고 약 1여 년간 소프트웨어 테스트라는 Sub Job을 맞게 되면서, 초기에 가볍게 시작한 테스트 활동이 이제는 어느 정도 정착된 소프트웨어 테스트 프로세스를 수립하게 되었고, 이것을 올바르게 판단할 수 있도록 데이터를 시각화하기 위한 다양한 시도를 해 보았습니다. 그리고 테스트를 대하는 우리들의 자세에 대해서도 매우 비판적인 요소들이 많지만, 즉 이것은 아무도 올바른 테스트를 시도하지 않았고 경험해 보지 못한 것이라는 반증이기도 합니다.

       

       

    사실 우리나라에서는 "테스트"라는 용어가 피부에 와 닿게 된 시점이 바로 "애자일" 개발 프로세스(방법론이라고 칭하지 않음) 덕분에 여러 사람들의 입방아에 가장 자주 오른 용어이기도 합니다. "애자일 프렉티스", "애자일 소프트웨어", "애자일 개발" 이라는 일파만파적인 용어와 수식어 들이 생겨나면서 우리나라의 현실적인 소프트웨어 개발 생태계에서 "테스트"가 재조명 받는 시기가 바로 "애자일"의 영향이 매우 컸음을 의미합니다.

    물론, 자사의 솔루션이나 소프트웨어를 전문적으로 개발하는 진정한 소프트웨어 개발 업체에서는 정직하고 잘 동작하는 소프트웨어를 출시하기 위해 테스트에 상당한 노력을 기울이고 있습니다. 높은 단가로 팔리는 소프트웨어에 결함이 있다는 것은 고객에게 매우 치명적이기 때문에 이들의 업체들은 소프트웨어 테스트 전문 인력을 확보하고 꾸준히 품질 유지를 위해 노력하고 있으니까요.

    특히 20세기 후반부터 큰 규모의 소프트웨어 개발을 일컬어 "엔터프라이즈 급"의 제품이 개발되고 이것은 즉, 혼자서 잘 동작하는 소프트웨어가 아닌, 다른 시스템, 소프트웨어, 컴포넌트와 잘 동작해야 하는 이식성 때문에 압도적으로 비중 있게 다루는 것 또한 테스트 입니다.

       

    엔터프라이즈 급의 소프트웨어는 상하수직 관계와 팀간의 범위 등이 잘 조직화가 잘 된 우리나라에서 특히 가장 높은 퍼포먼스를 보여주기도 합니다. (높은 퍼포먼스가 그에 걸 맞는 품질을 보장하는 것이 아님을 주의해 주세요) 그리고 테스트는 1단계에서 2,3 단계까지 체계적으로 이루어지기도 합니다. "단위 테스트", "기능 테스트", "통합 테스트"라는 폴더를 만들고 수십 가지 문서를 양산해내면 테스트 단계는 통과하게 되는 것이지요^^. 테스트 이후 단계에서 문제가 발생하면 유지보수 계약에서 추가적으로 지원하면 되니까요..;; (물론, 그 과정이 단순한 과정이 아닌 것도 잘 아는 바이지만…)

    흔히 모든 사람들이 문제라는 것을 인지하고, 테스트가 중요하다는 공감대가 형성이 되어야 비로서 저와 함께 논의가 될 수 있으리라 생각합니다.

       

       

    개발자가 아닌 돌팔이를 주도적으로 양산

    일단 개발자의 정식 명식은 "소프트웨어 기술자" 입니다. 일반적으로 우리나라에서는 개발자의 등급을 분류하기를 다음과 같이 분류하는 것을 좋아합니다.

    자세한 개발자 등급 기준은 다음의 링크를 참고 하세요 (http://career.sw.or.kr/hrdict/front/guide/renew/sub1-4_popup.jsp )

    • 초급 기능사 : 기능사 자격증을 취득한 자
    • 중급 기능사 : 기능사 자격 취득 3년 경과 및 산업기사 취득한 자
    • 고급 기능사 : 기사, 산업기사 취득하고 기사 취득 후 7년 경과한 자
    • 초급 기술사 : 기사 취득, 산업 기사 이상 취득, 지식경제부장관이 고시한 공인민간자격을 취득한 자
    • 중급 기술사 : 기사 취득 및 그 후 3년 업계 종사 …
    • 고급 기술사 : 중급 기술사 취득 후 3년 이상 업계 종사
    • 특급 기술사 : 고급 기술사 취득 후 3년 이상 업계 종사
    • 기술사 : 기술사

       

    참 문제가 많은 등급 분류 제도이지요? 대충 밑바닥부터 특급 기술사가 되려면 어림잡아 20년은 넉넉히 업계에 종사해야 하고, 빨리 빨리 자격을 취득하는 것이 상책인 제도인 것 같습니다. 이것도 저것도 싫다면 몇 가지 제한이 있긴 하지만, 양반으로 승진하는 방법은 "기술사"가 유일한 방법이네요. 석사, 박사 과정을 이수하고 좀 놀다가 기술사를 따면 초고속으로 "기술사"로 승진할 수 도 있지요. 이런 등급 제도를 싸잡아 대략 4등급으로 나누면 "초급", "중급", "고급", "특급", 이렇게 네 부류의 개발자(사)로 나뉘어 지는데, Microsoft 제품과 Borland 제품을 가지고 논지 20년이 훌쩍 넘는 필자 또한 아직은 초급을 벗어날 수 없는 운명에 처해있네요.

    저렇게 등급화한 것이 문제가 아니라, 닷컴 열풍으로 소프트웨어 개발자 양산을 주도한 정부로써 진정한 엔지니어를 양산하려는 시도가 아닌 "초급 기능사"를 너무 많이 양산한 것이 문제이고, 일단 현업으로 뛰어든 초급 기능사는 현실적으로 다음 등급을 넘기 위해 다시 학교로 돌아가는 것이 가장 빠른 방법인 것이 더욱 문제이고, 더욱 문제는 막 현업에 뛰어든 기능사 개발자들이 더 높은 고지로 갈 수 있는 문턱은 당시 이미 닫혀버렸다는 것이 가장 큰 문제입니다. 덕분에 수 많은 일자리를 창출하고 당시 청년 실업률이 낮은 편이었으며, 누구에게나 개발자의 길이 열려있었다는 것이 지금 현재의 개발자 구인난의 대표적인 정책 실패라고 봅니다. 정책만이 실패한 것이 아닙니다. 닷컴 열풍 이후 지금의 소프트웨어 생태계는 이미 파괴되어버렸고, 지속적인 정책이 뒷받침 되지 않아 많은 수의 개발자들이 소프트웨어 업계를 떠났습니다.

    소프트웨어를 공부하려고 뛰어는 사람들이 아닌, 단지 특정 플랫폼과 개발 언어를 가르치면서, 그 시장이 원하는 플랫폼과 개발 언어의 문턱을 넘지 못하면 그대로 낙오될 수 밖에 없으니, 학원 문턱을 통해 현업에 종사할 수는 있어도, 잘 할 수 없는 반복적으로 순환되는 구조적인 문제점을 아직도 그 때의 문제들을 우리 현업 종사자들이 짊어지고 가고 있습니다.

       

    3년 후, 5년 후, 10년 후, 20년 후 개발자의 미래

    어떤 개발자가 좋은 개발자인가부터 시작해 봅시다. 10점 만점 중에 각 개발자에게 점수를 주어보았습니다.

    1. 개발자는 코딩을 잘 하면 좋은 개발자입니다. (1점 드립니다)
    2. 개발자가 코딩을 잘하는데 커뮤니케이션 스킬이 좋으면 좋은 개발자 입니다.(4점 드립니다)
    3. 개발자가 신기술에 거부반응 없이 잘 소화하면 좋은 개발자 입니다. (5점 드립니다)
    4. 문서화, 시각화 역량이 좋으면 더 없이 써 먹을 데가 많은 개발자 입니다 (SI에서 쓰는 문서 제외) (8점 드립니다)
    5. 부하직원을 잘 부리고 성격도 좋으면 금상 첨화 입니다 (10점 드립니다)

       

    이것이 무언인가 잘 생각해 보시면, 신입 사원이 팀장까지 가기 위한 스킬북이라고 보셔도 좋습니다. 전형적으로 우리나라에서 요구하는 개발자 구인 스킬이지요. 결국 코딩 하다가 커뮤니케이션 스킬을 인정받으면 5~10년 후 팀장 명함은 따놓은 당상입니다. 즉, 이것이 대다수의 개발자의 커리어가 될 수 도 있다는 의미입니다.

    • 이렇게 궁극의 개발자는 바로 관리자인 것인가…?
    • 개발도 제대로 못해본 사람들이 어떻게 좋은 소프트웨어를 만들 것인가…?

       

       

    코드+코드+코드…+컴파일=소프트웨어

    개발자+개발자+개발자…+야근=소프트웨어

       

    아마도 필자가 수 년 전에 사용했던 이력카드의 스킬 인벤토리는 여러 개발자들도 낯설지만은 않을 것입니다.

     

    우리나라 소프트웨어 산업의 80%인 SI(System Integration) 프로젝트가 모두 이런 형식의 이력카드를 사용하고, 고객사들도 원하는 것이 이런 형태의 이력카드이며, 여기에 근무기간을 합산하여 개발자 등급을 분류하여 페이(월급)을 지급합니다. 내가 저 프로젝트에서 뭘 하든 개발자를 바라보는 자세가 한낮 스타크래프트의 SCV, 그 이상이 아니다는 의미이기도 합니다.

       

    저 또한 저러한 이력카드를 채우지 않은지 오래되었지만, 최근에는 제 스킬 인벤토리를 페이스북에 올려놓고 꾸준히 업데이트를 하고자 노력하기도 합니다. (http://www.facebook.com/profile.php?id=100000339676463&sk=info ) 좋은 예가 될지는 모르겠지만, 제 스킬 인벤토리는 최대한 특정 플랫폼에 종속적이지 않도록 작성했고, 제가 수행한 업무를 잘 소개하려고 노력 하였습니다. (물론 제 진짜 이력서가 아닌 제 간략한 정보임을 인지하고 봐주세요) 그리고 제가 개발자로써 어떤 활동을 하며, 어떤 에반젤리즘을 하는지도http://powerumc.codeplex.com/ 를 통해 게시하고 있습니다.

    이러한 필자의 발버둥은 SCV 이상이 되고 싶음을 갈망하는 것일지도 모릅니다. 하지만 개발자는 개발자다워야 합니다. 개발자는 개발이 기본이 되어야겠지요.

    애자일 개발 프로세스에서는 함께 하는 팀간에 비즈니스 모델이나 비전을 제시하고 함께 공감대를 형성하여 최상의 퍼포먼트와 팀워크를 구축하는 것이 첫 번째 단계입니다. 그래야 반복이고 머고 잘 됩니다. 애자일 개발 프로세스는 몰라도 됩니다. 팀원이 "이것이야 말로 애자일 프로세스로 돌아가는 프로젝트구나!!!" 라는 것을 느끼게 할 필요도 없습니다. 첫 단추를 잘 꿰면 굳이 애자일 프로세스가 아니더라도 마치 자연의 생태계의 흐름처럼 애자일리티할 수 밖에 없기 때문입니다.

    즉, 필자가 이 섹션에서 하려는 말은 개발자는 개발이 기본이 되어야지, 회사나 프로젝트의 비즈니스는 그 다음 단계라는 것입니다. 회사나 프로젝트의 이익을 창출하는 비즈니스의 모델과 프로세스를 잘 이해하면 좋지만 개발자의 기본적인 소양을 갖추지 못한 사람이 그 비즈니스를 구현하게 되면 그것처럼 서포트하기 힘든 것도 없습니다.

       

    결국은 좋은 테스트를 하기 부적격한 개발자들

    이야기가 삼천포로 갔습니다만, 필자는 앞으로 테스트에 대한 연제는 계속될 것입니다. 그런데 다시 한번 우리나라 소프트웨어 생태계를 구성하는 많은 사람들을 보고 느낄 때는, 테스트가 문제가 아니라 개발자 한 명 한 명이 갖는 커리어와 목표의식이 없는 것이 더 큰 문제입니다. 이러한 우리나라 소프트웨어 개발에 대한 입방아는 책 한 권으로도 모자랄지도 모릅니다. 그리고 이를 공감하는 분들도 제 주변에 꽤 많은 편이고요.

    결국 마치 필자가 우리나라 소프트웨어 생태계를 전반적으로 싸잡아 몰아가는 것처럼 보이지만, 꼭 그렇지는 않습니다. 왜냐하면 한 동안 테스트를 Sub Job으로 수행하면서 이러한 개발자의 근본적인 자세, 테스트를 대하는 자세, 테스트에 대한 편견, 테스트 결과가 개발자 자신에게 미치는 영향 등 여러 가지 경험을 하면서 결국 근본적인 문제에 대한 생각을 한 것 뿐입니다. 즉, 테스트라는 일련의 소프트웨어 개발 단계에 들어가기도 전에 많은 수행 착오를 거치면서 테스트의 기술적인 문제가 아니라 그 이전에 테스트를 바라보고 이해하는 사람들의 문제라고도 바꾸어 이야기 할 수 있습니다.

    다시 말해,

    • 대다수의 개발자는 테스트는 필요하지만 내가 알 바는 아니라고 생각하고 있습니다.
    • 그리고 자신에게 부정적인 영향을 주는 테스트에 대한 적대적인 반감을 드러내는 것은 개발자로써가 아닌 감정적으로 대응하는 개발자들의 문제점들이 내포하고 있습니다.
    • 더 문제인 것은 테스트를 단 한번도 생각해 본적 없는 사람들이 테스트에 대해 펌하하고 논하는 것도 참 안타깝습니다.
    • 이보다 더 큰 문제는 테스트를 수행하는 사람 또한 소프트웨어/테스트 공학 1장도 읽어 보지 않은 사람들이 대부분이라는 것도 테스트를 수행하는데 큰 걸림돌입니다.
    • 종합해보면 테스트라는 일련의 단계에 대한 최소한의 이해가 부족하고, 스스로 부족한 것을 알고 받아들일 줄 아는 이해심도 부족합니다.

       

    소프트웨어 개발 사이클에서 초기 분석과 설계를 잘 하는 것이 중요하고 그것을 잘하는 사람이 있음으로써 개발에까지도 영향을 미치는 것은 "애자일 개발 프로세스"든 "전통적인 개발 프로세스"든 마찬가지 입니다. 테스트도 마찬가지 입니다.

    라이브 서비스를 하는 소프트웨어 서비스 업체들도 성능, 버그, 결함, 오류, 해킹 등에 많은 투자를 하고 심도 있게 다루는 부분입니다. 테스트는 분명 매우 민감하고 기본적이며 중요한 단계입니다.

    테스트는 개발을 염두하고 테스트를 하지만, 개발자는 테스트를 염두하고 개발하지 않습니다. 그렇기 때문에 테스트는 개발자에게 언제나 불리할 수 밖에 없습니다. 아무런 이음매가 없는 개발 단계에서 테스트 단계까지 자연스러운 이음매를 맺기 위해서 기술적인 것은 기본이 되어야 할 것이며, 그 근본적인 공학적인 지식도 앞으로 꾸준히 다루어 보고자 합니다.


    디바이스 로스트( 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 에서 디바이스-로스트는 잘 발생되지는 않습니다.^^

    이와 관련한 실제 처리는 다음 시간에 계속하겠습니다.^^

    SharePoint 2010 PowerShell- 개발자

    SharePoint 2010 2011. 10. 4. 08:30 Posted by 알 수 없는 사용자

    SharePoint 2010 PowerShell- 개발자

    제 생각에는 SharePoint 2010 관리 셀이라고 관리자만 사용한다고 생각하시면 안될 것 같습니다.

    아래의 SharePoint 2010 관리 셀 명령 프롬프트를 이용해서 개발자에게도 상당한 이점을 제공해주는 것 들 중 일부만 정리해보겠습니다.

    추가로 필요한 설명이나 예제를 보고 싶다면 위의 그림처럼 Get-Help 를 사용하시면 됩니다. 예제를 보고 싶다면 –examples 를 추가하시면 됩니다.

    자 그럼 첫 번째 내용입니다.

    l 개발자 대시보드

    $svc=[Microsoft.SharePoint.Administration.SPWebService]::ContentService

    $ddsetting=$svc.DeveloperDashboardSettings

    $ddsetting.DisplayLevel=[Microsoft.SharePoint.Administration.SPDeveloperDashboardLevel]::OnDemand

    $ddsetting.Update()

    $svc=[Microsoft.SharePoint.Administration.SPWebService]::ContentService

    $ddsetting=$svc.DeveloperDashboardSettings

    $ddsetting.DisplayLevel=[Microsoft.SharePoint.Administration.SPDeveloperDashboardLevel]::Off

    $ddsetting.Update()

    개발자 대시보드를 활성화하면 상당히 유용한 정보를 손쉽게 얻을 수 있으며 병목 탐지가 원활해지게 됩니다. SQL 구문까지도 알 수 있습니다. 개발자 대시보드의 모드는 ON, OnDemand, Off 3가지가 있습니다. 저희가 평상시 보는 모드가 Off 이며 On 시키면 모든 사이트에서 On 됩니다. 특정사이트만 On 시킬 수는 없습니다.

    위의 명령어를 여러 줄 복사해서 관리 셀에서 붙여 넣기 하면 됩니다.

    l 사이트 생성

    필요에 따라 동적으로 여러 사이트를 구성해야 할 필요가 있을 수 있습니다. 그리고 해당 사이트 모음은 해당 데이터베이스로 지정되게 해야 할 경우 아래 구문으로 실행할 수 있습니다.

    New-SPContentDatabase -Name WSS_Content_Intranet_Sales -WebApplication http://sp.webtime.co.kr

    $spsite = New-SPSite -Url "http://sp.webtime.co.kr/sites/Sales" -ContentDatabase WSS_Content_Intranet_Sales -OwnerAlias CONTOSO\hongju -Template "STS#0“

    New-SPContentDatabase 를 통해 Content 데이터베이스를 생성하고 New-SPSite 를 통해 사이트 모음을 만들면서 생성한 데이터베이스와 연결하고 있습니다. 중앙관리에서는 할 수 없는 상황이죠.

    l 목록 데이터 액세스

    필요에 따라 테스트 데이터를 생성해야 할 경우 개체 모델을 통해서 하는 것보다 PowerShell이 더 편할 수 있습니다. 개발자 환경에서 테스트하고 통합 개발 서버에 데이터를 생성해서 테스트 할 수 있습니다. 목록에 기본 데이터를 생성할 경우에도 사용할 수 있습니다.

    $site = Get-SPSite "http://sp.webtime.co.kr/sites/Sales"

    $web = $site.rootweb

    $list = $web.Lists["공지 사항"]

    $i = 1

    do {

    #add item

    $newitem = $list.items.Add()

    $newitem["Title"] = "Title -" + $i.ToString().PadLeft(4, "0");

    $newitem["Body"] = "Body-" + $i.ToString().PadLeft(4, "0");

    $newitem.Update()

    $i++

    }

    while ($i -le 100)

    $web.dispose()

    $site.dispose()

    l WSP 배포

    필요에 따라 WSP를 중앙 관리를 열어서 배포하지 않고 PowerShell로 배포할 수 있습니다.

    Add-SPSolution D:\SP10_DEV\VisualProduct.wsp

    Install-SPSolution -Identity VisualProduct.wsp –GACDeployment -Force –AllWebApplications

    Uninstall-SPSolution -Identity VisualProduct.wsp -AllWebApplications

    Remove-SPSolution -Identity VisualProduct.wsp

    위에서 일부 내용을 살펴보았는데 정말 많이 있습니다. 다 확인할 수는 없는 상황이고 필요하다면 Get-Help 를 통해 살펴보시고 액세스하시면 될 것 같습니다.


    SQL Azure Q2 2011 Service Release

    Cloud 2011. 9. 29. 08:30 Posted by 알 수 없는 사용자

    SQL Azure Q2 2011 Service Release

    SQL Azure 에 대한 Service Release에 대한 내용을 정리합니다. 여러 가지 업데이트 된 것이 많이 있으니 참고하시기 바랍니다.

    l SQL Azure @@version

    SQL Azure 로 연결하여 @@version 을 확인해 보시면 11점대로 변경된 것을 확인 가능합니다. 제가 사용하는 SQL Server 2008 R2 는 버전이 10.50.1617.0 입니다만 SQL Azure의 버전을 실행한 결과는 아래와 같습니다.

    l SQL Azure Import/Export Hosted Service CTP

    Windows Azure Management Portal에 로그온 하면 아래와 같은 리본 메뉴를 보실 수 있습니다.

    또한 관련 내용은 아래 링크를 참조하실 수 있습니다.

    http://sqldacexamples.codeplex.com/wikipage?title=Import Export Service Client

    이후 블로깅에서 보다 더 자세한 내용을 다루도록 하겠습니다.

    l SQL Server Data Tools “Juneau”

    새로운 SQL Server Data Tools 인 코드명 “Juneau” CTP 7월에 릴리즈 되었습니다. Juneau 에서도 SQL Azure 에 대한 내용을 지원해주고 있습니다. 데이터베이스 디자인, 개체 생성과 편집 등을 SQL Azure 에도 진행할 수 있습니다.

    또한 다음 블로깅에서 구체적으로 SQL Azure를 대상으로 알아보도록 하겠습니다.

    l 관리도구(SSMS) 업데이트 필요할 수 있음

    데이터센터의 업그레이드로 기존 관리도구(SSMS)의 업데이트가 필요할 수 있습니다. 혹시 연결에서 오류가 발생한다면 아래 주소를 참조해서 업데이트하시면 됩니다.

    http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=26727

    l SQL Azure Management Portal

    Database Manager 도구는 사라질 예정이며 SQL Azure Management Portal 에서 새로운 작업을 진행할 수 있습니다. 아래 두 내용에 대해서 좀 더 구체적으로 다루도록 하겠습니다.

    - 관리: Database Life Cycle

    - 디자인: Database Schema and Data


    l Spatial 데이터 형식

    데이터 형식 지원이 더 강화되었습니다. 지원되는 데이터 형식과 spatial 데이터 형식에 대한 내용은 아래 링크를 참조하시기 바랍니다.

    http://msdn.microsoft.com/ko-kr/library/windowsazure/ee336233.aspx

    l 공동 작업자 관리

    여러 명의 작업자를 지정할 수 있습니다.