Direct3D 기본 지식

WanJu Kim·2022년 12월 24일

Direct3D

목록 보기
3/29

이 시리즈는 Direct3D에 대해 다룬다.

Direct3D란?

Direct3D란 무엇인가? 그 정의는, '응용 프로그램이 3차원 그래픽 가속 기능을 이용해서 3차원 세계를 렌더링할 수 있게 하는 저수준 API(application programming interface)이다. 저수준은 무엇인가? 컴퓨터에 가까운 언어를 말한다. 그럼 반대말도 있겠지? 고수준은 인간의 언어다. 알다시피 컴파일링은 고수준 언어를 저수준 언어로 바꾸는 것이다. 그러니까 우리가 지금부터 배울 것은 컴퓨터 언어에 가까운 좀 어려운 단어 모음집이라는 소리다. 낮은 수준이라고 오해 하면 안된다.

Direct3D는 그래픽 하드웨어를 제어할 수 있는 소프트웨어를 인터페이스를 제공해준다. 예를 들어 화면을 깨끗히 지우라고 명령하고 싶다면, 특정 메서드를 호출하면 된다.

COM이란?

COM(Component Object Model)도 알아야한다. 이는 무엇인가? COM은 DirectX의 프로그래밍 언어 독립성과 하위 호환성을 가능하게 하는 기술이다. 무슨 말인가? 모르겠다. 그냥 클래스라고 생각하면 편하다. 우리가 사용할 것은 COM 객체의 메서드들이다. 특이한 점은, COM 객체는 new를 통해 생성하지 않는다. 또한 다 사용하고 나면, delete랑 비슷한, Release 메서드를 호출해주어야 한다. 이는 COM 객체들이 자신만의 고유한 방법으로 메모리를 관리하기 때문이다. 보통 COM 객체는 이름이 대문자 I로 시작한다. ex) ID3D11Texture2D

텍스처란?

게임하면 역시 이미지다. 이를 텍스처라 부른다. 이미지는 어떻게 관리하는가? 2차원 텍스처를 통해서 관리한다. 2차원 텍스처는 사실 2차원 배열인데, 각 원소에는 이미지 각각의 픽셀을 담당하고 있다.

하지만 그게 2차원 텍스처의 유일한 기능은 아니다. 예를 들어 나중에 법선 벡터를 배우는데, 그때는 원소에 색깔이 아니라 3차원 벡터를 담는다. 그러니 이미지만 넣는 게 아니라 범용적이라고 생각하는 게 좋다. 또한 텍스처는 특정 형식을 따르는 자료만 담을 수 있는 게 특징이다. 예로는 다음과 같은 게 있다.

DXGI_FORMAT_R32G32B32_FLOAT;	// 각 원소는 32비트 부동소수점 성분 세 개로 이루어진다.
DXGI_FORMAT_R16G16B16A16_UNORM;	// 각 원소는 [0,1] 구간의 16비트 성분 네 개로 이루어진다.
DXGI_FORMAT_R32G32_UINT;	// 각 원소는 32비트 부호 없는 정수 성분 두 개로 이루어진다.

예시는 더 있지만 이 3개만 보여주면 대충 감이 올 것이다. 여기서 RGBA는 각각 R : 빨강, G : 초록, B : 파랑, A : 알파를 뜻한다. 알파는 투명도를 말한다. 이렇게 말하면 당연히 색깔'만' 담긴다고 생각할 수 있는데 아니다. 예를 들어 DXGI_FORMAT_R32G32B32_FLOAT; 이것은, float 형식 성분 세 개로 구성되며, 따라서 3차원 벡터를 넣을 수 있다.

교환사슬

TV에 나오는 만화는, 종이에 그림을 그려 마치 움직이는 듯한 모션을 취한다. 20분 만화에는 대략 5000장이 필요하다고 한다. 3D 그래픽도 마찬가지이지만, 5000장까지는 필요 없다. 2장만 있으면 된다. 앞면을 그리고 송출하고, 그 다음 장면을 뒷면에 그리고, 그 순서를 다시 바꿔서 뒷면을 송출하고, 앞면을 다시 그리는 식이다. 여기서 '장' 대신에 '버퍼'라는 단어를 써서, '전면 버퍼', '후면 버퍼'라고 부른다. 만약 하나의 버퍼로만 송출하면 어떻게 되는가? 화면이 무수히 껌뻑인다. 전면 버퍼와 후면 버퍼를 교환해서 화면에 표시하는 것을 '제시(presenting)'라고 부른다. 전면 버퍼와 후면 버퍼는 하나의 교환 사슬(swap chain)을 형성한다고 말한다. 두 개의 버퍼를 사용하는 것을 이중 버퍼링(double buffering)이라 부른다. 더 많은 버퍼를 사용하는 것도 가능하다. 예를 들어 삼중 버퍼링(triple buffering)은 버퍼 세 개를 사용하는 것이다. 그러나 일반적으로는 두 개면 충분하다.

깊이 버퍼링

깊이 버퍼(depth buffer)는 이미지 자료를 담지 않는 텍스처의 한 예이다. 깊이 버퍼는 각 픽셀의 깊이 정보를 담는다. 관찰자와 제일 가까운 픽셀이 0.0이고, 제일 먼 픽셀이 1.0이다. 깊이 버퍼의 원소들과 후면 버퍼의 원소들은 일대일 대응된다. 그러니까, 후면 버퍼의 ij번째 원소 깊이는, 깊이 버퍼의 ij번째 원소 깊이랑 같다. 따라서 후면 버퍼의 해상도가 1280 x 1024라면, 깊이 버퍼는 1280 x 1024개의 원소들로 구성된다.

그럼 만약 텍스처가 겹치면 어떻게 되는가? 프로그램은 그때 깊이 버퍼를 비교한다. 일단 제일 처음에 렌더링 하기 전에는, 기본값 1.0으로 깊이 버퍼를 지운다(Clear). 그 다음에 물체를 하나하나 그려나가고, 그때 특정 픽셀에 특정 깊이 버퍼가 새겨진다. 그러다가 이미 깊이 버퍼가 있는 곳을 렌더링할 때, 깊이 버퍼를 비교해서, 만약 깊이 버퍼가 더 작다면 깊이 버퍼를 최신화 하고, 아니면 최신화 하지 않는다. 왜? 깊이 버퍼가 작다는 게 관찰자와 가깝다는 것이고 관찰자의 눈에 보여야 하기 때문이다.

깊이 버퍼도 하나의 텍스처이므로, 특정한 자료 형식을 지정해서 생성해야 한다. 예시 하나만 보겠다.

DXGI_FORMAT_D32_FLOAT_S8X24_UINT;	// 각 텍셀은 32비트 부동소수점 깊이 값과 [0,255] 구간의 8비트 부호 없는 정수 스텐실 값(또 다른 버퍼인 스텐실 버퍼에 쓰이는 값), 그리고 용도 없이 채움(padding) 용으로만 쓰이는 24비트로 구성된다.

텍스처 자원 뷰

나중에 '렌더링 파이프라인'이 나오는데, 이는 3차원 공간을 2차원인 모니터에 옮겨담는 단계들의 모임을 뜻한다. 텍스처를 묶을 수 있는 단계들이 여럿 존재한다. 용도는 보통 Direct3D가 텍스처에 렌더링 한다거나, 셰이더 안에서 텍스처를 추출하는데 쓰인다. 그걸 표현하는 코드(플래그)는 다음과 같다.

D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;

각각 렌더 타겟, 셰이더 리소스에 묶다(bind)는 표현을 썼다. 근데 사실 이게 파이프라인 단계에 직접 묶이는 것은 아니고, 어떤 용도이든 Direct3D에서 텍스처(or 자원)를 사용하려면, 초기화 할때 그 텍스처의 자원 뷰(resource view)를 생성해야 한다. 왜? 효율성을 위해서라고만 알아두자. 그래서 그 자원 뷰는 각각 렌더 대상 뷰(D3D11RenderTargetView), 셰이더 자원 뷰(ID3D11ShaderResourceView)이다. 이름을 보면 어떤 자원 뷰가 어떤 용도인지 알 수 있다. 자원 뷰는 무슨 일을 하는가? 하나는 파이프라인의 어떤 단계에 묶일 것인지 알려주고, 또 다른 하나는 생성 지점에서 무형식을 지정한 자원 형식의 구체적인 형식을 결정하는 것이다. 그러니까 무형식의 자원의 경우 텍스처 원소를 한 파이프라인의 단계에서는 부동 소수점으로, 다른 단계에서는 정수로 사용할 수 있다는 말이다. (말이 어렵다.)

자원 뷰를 생성하기 전에 bind 플래그를 지정해야 한다. 예를 들어, 깊이와 스텐실 버퍼로 bind하는 자원 뷰(ID3D11DepthStencilView)를 생성하기 전에 D3D11_BIND_DEPTH_STENCIL(깊이 스텐실 플래그) 플래그를 지정해줘야 한다.

다중표본화 이론

모니터의 픽셀이 무한히 작지 않기 때문에, 모니터 화면의 임의의 선을 완벽하게 나타내는 것은 불가능하다. 이를 '계단 현상' 또는 앨리어싱(aliasing)이라 부른다. 이를 어떻게 방지하나? 모니터 해상도를 키워서 픽셀 크기를 줄이면 문제가 완화되겠지만, 모든 사람들이 그러긴 힘들 것 같다. 대신 앨리어싱 제거(antialiasing) 기법들이 존재한다.

첫 번째로, 초과표본화(supersampling)이다. 초과표본화에서는 후면 버퍼와 깊이 버퍼를 화면 해상도보다 4배(가로 세로 각각 2배)로 잡는다. 4배인 상태에서 후면 버퍼에 3D를 렌더링하고, 이미지를 화면에 제시할 때 다시 원래 크기로 환원(resolving) or 하향표준화(downsampling)한다. 둘 다 같은 말이다. 픽셀이 늘어났다가 사라졌으니, 없어져야 할 거 아닌가? 4배 더 작게 될 때, 픽셀 4개당 그 평균을 내서 색을 낸다. 이는 픽셀 처리량과 메모리 소비량이 기존의 4배라서 비용이 크다.

두 번째 방법은 다중표본화(multisampling)이다. 다중표본화와 초과표본화의 유일한 차이는, 다중표본화는 평균값을 계산할 때 초과표본화처럼 각 부분픽셀마다 계산하는 게 아니라, 픽셀 중심에서 한 번만 계산하고 그 부분픽셀들의 가시성과 포괄도를 고려해 최종 색상을 결정한다. 그래서 초과표본화보다 비용이 상당히 더 적게 일어난다.(사실 잘 모르겠음.) 당연히 기술적으로 더 정확한 건 초과표본화이다.

다중표본화를 위해서는 DXGI_SAMPLE_DESC라는 구조체를 채워야 한다.

struct DXGI_SAMPLE_DESC
{
	UINT count;		// 픽셀당 추출할 표본의 개수.
    UINT quality;	// 원하는 품질 수준.
};

당연히 표본 개수가 많을수록, 품질 수준이 높을 수록 비용이 더 크니, 절충선을 잘 잡아야 한다.

#define D3D11_MAX_MULTISAMPLE_SAMPLE_COUNT ( 32 )

보통은 표본을 4개나 8개로 추출한다. 다중표본화를 쓰고 싶지 않다면 count를 1, quality를 0으로 정하면 된다.

기능 수준

사용자의 하드웨어에 따라 가동할 수 있는 기능 수준이 다르다. 수준마다 차이점이 뭐냐고? 그건 여기를 참고하길 바란다. 열거자들의 숫자가 높을 수록 당연히 더 좋은 기능이다. 배열을 저런식으로 만든다면, 인덱스가 작은 숫자로 먼저 실행한다. 안되면? 그 다음 원소로 실행한다.

D3D_FEATURE_LEVEL featureLevels[4] = 
{
D3D_FEATURE_LEVEL_11_0,	// 제일 먼저 실행.
D3D_FEATURE_LEVEL_10_1,	// 위에가 안 되면 그 다음 실행.
D3D_FEATURE_LEVEL_10_0,	// 위에가 안 되면 그 다음 실행.
D3D_FEATURE_LEVEL_9_3	// 위에가 안 되면 그 다음 실행.
} 
profile
Question, Think, Select

0개의 댓글