본문 바로가기
그래픽스 API/DirectX 11

DirectX 11 학습에 필요한 개념 정리

by 니키티스 2023. 8. 9.

학습에 필요한 개념

기능 수준(Feature Level)

[DirectX 12] 기본지식 - 기능 수준(Feature Level) (tistory.com)

기능 수준들은 GPU가 지원하는 기능들의 엄격한 집합을 정의한다(각 기능들이 지원하는 구체적인 내용은 SDK를 참고). 예를들어 기능 수준 11을 지원하는 GPU는 반드시 Direct3D 11의 기능 집합 전체를 지원해야 한다.

이러한 기능 수준은 응용 프로그램 개발을 편하게 해주는 요인이다. 현재 GPU의 기능 수준을 파악하기만 하면, 구체적으로 어떤 기능을 사용할 수 있는지를 확실히 알 수 있기 때문이다.

사용자의 하드웨어가 특정 기능 수준을 지원하지 않는 경우 응용 프로그램이 실행을 아예 포기하는 대신 더 낮은 기능 수준으로 후퇴하는 전략을 사용할 수도 있다.

typedef 
enum D3D_FEATURE_LEVEL
    {
        D3D_FEATURE_LEVEL_1_0_CORE    = 0x1000,
        D3D_FEATURE_LEVEL_9_1    = 0x9100,
        D3D_FEATURE_LEVEL_9_2    = 0x9200,
        D3D_FEATURE_LEVEL_9_3    = 0x9300,
        D3D_FEATURE_LEVEL_10_0    = 0xa000,
        D3D_FEATURE_LEVEL_10_1    = 0xa100,
        D3D_FEATURE_LEVEL_11_0    = 0xb000,
        D3D_FEATURE_LEVEL_11_1    = 0xb100,
        D3D_FEATURE_LEVEL_12_0    = 0xc000,
        D3D_FEATURE_LEVEL_12_1    = 0xc100,
        D3D_FEATURE_LEVEL_12_2    = 0xc200
    }     D3D_FEATURE_LEVEL;

DXGI(DirectX Graphics Infrastructure)

DXGI 개요(DirectX Graphics Infrastructure Overview) (tistory.com)

DXGI - Win32 apps | Microsoft Docs

DXGI (Microsoft DirectX Graphics Infrastructure)는 그래픽 어댑터 열거, 디스플레이 모드 열거, 버퍼 형식 선택, 프로세스 간(예 : 응용 프로그램과 데스크탑 창 관리자(DWM) 간) 자원 공유, 렌더링된 프레임을 디스플레이하기 위한 창 또는 모니터에 표시하는 작업을 처리합니다.

DXGI는 Direct3D 10, 11, 그리고 12에서 사용됩니다.

대부분 그래픽 프로그래밍이 Direct3D에서 수행되지만, DXGI를 사용하여 최종 구성(eventual composition) 및 디스플레이를 위해 창, 모니터 또는 기타 그래픽 구성 요소에 프레임을 표시할 수 있습니다. 또한 DXGI를 사용하여 모니터의 콘텐츠를 읽을 수 있습니다.

-MSDN

DXGI는 여러 그래픽 API에 공통인 그래픽 관련 작업들을 묶은 것이다.

DXGI의 목적은 커널 모드 드라이버 및 시스템 하드웨어와 통신하는 것이다.

페이지 전환(스왑체인), 어댑터, 모니터, 디스플레이 모드 등의 그래픽 시스템 정보 등이 DXGI가 담당하는 영역이라 보면 되겠다.

DirectX 10부터 사용되는 DirectX API 구조

Direct3D 11은 그래픽스에 관한 기초 기능을 제공하는 DXGI 상에 구축되었다.

위 그림의 경우에는 D3D10이라고 되어 있는데, 이는 DirectX 10 버전부터 위와 같은 구조를 사용하기 시작해서 그렇다.
일반적으로 Direct3D 11을 사용할 경우 DXGI는 Direct3D 11을 경유하여 사용한다.

직접 사용하는 경우가 잘 없는 편인데, 감마 컬렉션, 디스플레이 선택이 필요할 때 사용한다고 한다.

스왑 체인

DirectX 11은 화면을 그리고 렌더링할 때 프론트 버퍼와 백 버퍼를 사용한다.

  • 프론트 버퍼: 디스플레이에 표시되는 화면 데이터를 갖는 버퍼

일단 백 버퍼에 먼저 기록하고, 렌더링이 끝났을 때 디스플레이에 표시되는 프론트 버퍼를 갱신한다(백 버퍼와 프론트 버퍼를 가리키는 포인터를 바꿔친다!(Swap)).

더블 버퍼링의 일종이다.

프론트 버퍼를 갱신하는 작업 중에도 백 버퍼에 렌더링할 수 있어서, 렌더링 퍼포먼스 저하를 막기 위해 3개 이상의 버퍼도 준비할 수 있다

정리하자면, 스왑 체인은 프론트버퍼, 백 버퍼를 포함한 복수의 버퍼의 집합과 이들 사이의 전환 방식의 집합이다.

DirectX 11에서 스왑 체인은 DXGI의 기능으로 제공된다.

스왑 체인은 거의 항상 디스플레이 하위시스템(대부분 그래픽 카드이지만, 마더보드에도 구현 가능)의 메모리에 만들어진다.

Direct3D에서 페이지를 전환하는 것, 즉 프론트 버퍼와 백 버퍼를 교환해서 화면 상에 백 버퍼를 그리는 행위를 프레젠팅이라 부른다.

Direct3D 11의 장치

(d3d11.h에 포함됨)

DX 11에서는 멀티 스레드 호환이 강화되어서 DX 10에서의 디바이스가 둘로 쪼개졌다.

즉, DX 10에서 디바이스에 있던 기능이 디바이스 + 디바이스 컨텍스트 둘로 나눠졌다.

장치

장치(디바이스; device)는 리소스 생성에 주로 사용한다. ID3D11Device 인터페이스로 표시된다.

렌더 타겟 뷰, 상태 변수의 생성 등등에 사용된다고 볼 수 있다. 리소스는 나중에 설명할 예정이다.

각 응용 프로그램에는 하나 이상의 장치가 필요하다. 대부분은 하나의 장치만 만든다.

D3D11CreateDevice로 디바이스를 생성하거나, D3D11CreateDeviceAndSwapChain으로 디바이스와 스왑 체인을 동시에 생성한다.

장치 컨텍스트

장치 컨텍스트: 디바이스의 설정을 담당한다. 장치를 사용하는 환경 및 설정을 포함하는데, 상태 변수 같은 것이 그 예시가 되겠다.

DepthStencilState 같은 상태 변수를 만들었다면 장치 컨텍스트에 적용시켜줘야 한다.

ID3D11DeviceContext 인터페이스로 표시된다.

장치에서 소유하는 리소스를 사용하여 파이프라인 상태를 설정하고, 렌더링 명령을 생성하는 데에 사용된다.

즉시 렌더링, 지연 렌더링이라는 두 유형이 있다.

  • 즉시 컨텍스트(Immediate Context): 디바이스에 직접 렌더링, 디바이스 1개만 존재
  • 지연 컨텍스트(Deferred Context): 메인 렌더링 이외에 워커 스레드에서 사용

앞에서 이야기했듯, ID3D11Device 인터페이스 메서드는 free-thread로, 멀티스레드가 한 번에 함수를 호출해도 안전하다.

DirectX 11을 사용할 수 없을 때

DirectX 11을 지원하는 그래픽 카드가 없다면 장치, 장치 컨텍스트 생성 함수가 실패한다.

즉, 디바이스와 스왑 체인을 생성할 때 아래와 같이 짜면 실패할 수도 있다.

// DirectX 11이 없을 경우 아래 코드는 실패할 수 있다.
HRESULT result = D3D11CreateDeviceAndSwapChain(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, 0, &featureLevel, 1,
        D3D11_SDK_VERSION, &swapChainDesc, &m_swapChain, &m_device, NULL, &m_deviceContext);

이때, D3D_DRIVER_TYPE_HARDWARED3D_DRIVER_TYPE_REFERENCE로 바꾸면 그래픽 카드가 아닌 CPU에서 렌더링을 처리하게 할 수 있다.

1/1000 정도로 느리지만 DirectX 11 기능을 사용할 수 있다. 그러나 개발용, 디버그용이기 때문에 실제 애플리케이션에서는 권장하지 않는다.

HARDWARE가 안된다면 일단 먼저, D3D_DRIVER_TYPE_WARP를 사용해보자. 현재 하드웨어에서 사용할 수 있는 Feature level로 사용되며, 다만 사용하는 함수의 조합에 따라 버전이 지원이 안 되는 것도 있으니 주의하자. 이것도 안 되면 D3D_DRIVER_TYPE_REFERENCE를 사용해보자.

속도는 D3D_DRIVER_TYPE_HARDWARE > D3D_DRIVER_TYPE_WARP > D3D_DRIVER_TYPE_REFERENCE 순이라고 한다!

깊이 테스트(Depth Test)

이후 사용하게 될 깊이-스텐실 뷰에서 이 깊이 값을 담는 버퍼를 사용해야 한다. 그래서 깊이 테스트를 알아놓으면 좋다.

깊이 테스트란?

깊이 버퍼는 각 프래그먼트(fragment)의 깊이 값(z값)을 가지고 있다.

깊이 테스트: 프래그먼트가 있을 때, 깊이 버퍼에 값을 업데이트할지를 결정하는 테스트

깊이 테스트(depth test)가 실행되면 프래그먼트별로 깊이 값을 비교 검사하여, 깊이 값이 가장 낮은 프래그먼트를 앞에 표시하는 식이다. 뒤에 있는 프래그먼트는 폐기된다(다만, 반투명한 오브젝트의 경우 폐기하는 게 아니라 알파 소팅 과정을 거침).

깊이 테스트를 통과하면 새로 깊이 값을 업데이트한다.

OpenGL에 관한 글이지만 DirectX에도 적용 가능하다.

Depth value precision

깊이 값은 0.0에서 1.0 사이의 값.

대부분 깊이 값은 24비트를 사용한다.

방정식이 비선형적이므로 깊이 버퍼의 0.5 값이 절두체의 중간 위치를 의미하지 않는다.

방정식이 1/z에 비례하므로 z값이 작을 때 정밀도가 크고, z값이 클수록 정밀도가 작다.

z 값과 깊이 값에 대한 그래프. 가로축이 z 값이고, 세로축이 깊이 값. z가 커질수록 깊이 값의 차이도 작아진다.

Z-fighting

두 평면 또는 삼각형들이 매우 근접하게 위치하여, 깊이 버퍼가 어떤 물체가 다른 물체의 앞에 있는지 판단하기 위한 충분한 정밀도를 가지지 못해 발생하는 시각적인 결함이다.

멀리 있는 물체일수록 정밀도가 낮아져서 더 많이 발생한다.

z 값이 높은 오브젝트가 여러 개 겹치면, 충분히 z 값이 차이가 나더라도 물체의 앞뒤가 막 이상하게 자꾸 바뀌는 현상이 일어난다.

해법: 두 물체가 오버랩될 정도로 절대로 가까이 두지 않게, 카메라 공간(뷰 공간)에서 near 평면을 가깝게 두어야 한다.

그래픽스 파이프라인

게임에서 그래픽을 그리는 과정은 쪼끔 복잡하다. 간단하게 말하면 매 프레임 입력을 받고, 물리 처리나 해야 할 처리를 한 다음 화면에 그림을 그려주면 된다.

하지만 하나의 스레드만 사용하면 충분한 성능이 나지 않아서, 여러 개의 스레드를 사용해서 각 작업을 동시다발적으로 진행하곤 한다.

말하자면 이런 느낌이다.

deltaTime은 언제 계산될까? (tistory.com)

게임 엔진(특히 Unity)에서 어떻게 파이프라인이 진행되는지 보려면 위 글을 추천한다.

마이크로소프트 측에서 제시한 바로는, Direct3D 11에서 진행되는 그래픽스 파이프라인은 다음과 같다.

DirectX 11 그래픽스 파이프라인

복잡하니 간단하게만 정리하면,

  1. 입력-어셈블리 스테이지(IA): 정점 쉐이더에 필요한 데이터를 전달한다.
  2. 정점 쉐이더(Vertex shader): 정점을 가지고 월드 변환과 뷰 변환 등 필요한 처리를 한다.
  3. Hull Shader ~ Geomery Shader: 처음엔 복잡하니, 나중에 따로 공부해보기. 정점 쉐이더만으로는 부족한 디테일을 살릴 때 사용된다.
  4. 래스터라이저(Rasterizer): 각 정점을 보간하여 픽셀 쉐이더에서 사용할 프래그먼트(픽셀별 정보)를 생성한다.
  5. 픽셀 쉐이더(Pixel-Shader): 픽셀별로(여기서는 프래그먼트) 색깔을 입힌다.
  6. 출력 병합기(Output-Merger; OM): 픽셀 쉐이더에서 들어온 색상을 가지고, 렌더 타겟과 깊이 테스트와 스텐실 테스트 등을 고려하여 어떤 걸 표시하고 표시하지 않을지를 최종적으로 결정짓는다. 이 단계에서 계산된 픽셀 색상이 백 버퍼에 그려져서 화면에 보이게 될 것이다.

리소스 뷰/자원 뷰

리소스

GPU가 어떤 일을 하기 위해선 셰이더 프로그램이 필요한데, 셰이더에 넘겨주는 데이터를 리소스(Resource)라고 한다.

씬을 구성하는 기하 도형, 텍스처, 쉐이더 데이터가 모두 리소스이다.

모든 Direct3D의 리소스는 버퍼 또는 텍스처이다.

  • 버퍼 리소스는 단순한 1차원 배열(정점 버퍼, 인덱스 버퍼 등)
  • 텍스처 리소스는 텍스처를 보존하기 위해 구조화된 컬렉션이다. 샘플러에 의해 필터 처리할 수 있고, 밉맵 레벨도 가질 수 있다.
  • 수명 주기: 생성 -> 바인딩 -> 해지
  1. 디바이스에 의해 생성됨
  2. 디바이스 컨텍스트에 의해 파이프라인에 바인딩됨
  3. 다 쓰고 나면 Release 함수로 리소스 할당을 취소함

3번에서 Release 함수는 free 같은 일반적인 할당 해제 함수가 아니라, std::shared_ptr처럼 참조 카운터를 통해 관리하는 식으로 되어 있다.

리소스 뷰

파이프라인이 여러 단계가 있는데, 그 단계들이 접근하려는 리소스는 단 하나이다.

파이프라인 단계마다 리소스를 할당하고 복사할 수는 없으니, 리소스에 접근하기 위해 리소스 뷰를 사용한다.

리소스 뷰: 리소스를 렌더링 파이프라인에 바인딩해줄 수 있는 객체.

요약하면, 하나 이상의 단계에서 같은 리소스에 접근할 때 리소스 뷰로 접근한다.

리소스 뷰 없이 파이프라인에 바인딩할 수 있는 리소스는 정점 버퍼, 인덱스 버퍼, 상수 버퍼, 스트림 출력 버퍼이다.

리소스 뷰는 네 가지가 존재한다. 일단 앞의 두 개만 정리해둠.

  • 렌더 타겟 뷰(RenderTargetView)
  • 깊이 스텐실 뷰(DepthStencilView)
  • ShaderResourceView
  • unordered access view (DirectX 11에서 추가)

렌더 타겟 뷰(Render Target View)

렌더 타겟은 렌더링 대상을 가리킨다. 텍스처에다가 그릴 거면 텍스처가, 화면에 그릴 거면 백 버퍼가 렌더 타겟이 되겠다.

리소스 뷰의 일종으로 디바이스의 백 버퍼에 바인딩되면서 생성된다.

백버퍼에 장면을 출력하려고 할 때 백버퍼를 파이프라인에 직접 바인딩할 수 없고, 이 렌더 타겟 뷰를 파이프라인의 마지막 단계(출력 병합기(OM) 단계)에 바인딩해야 한다.

쉽게 말하면 물체가 주어지면 파이프라인이 정보를 던져주는데... 파이프라인에서 주어지는 정보가 백버퍼로 바로 연결될 수 없다는 것이다.

  • 파이프라인 → 렌더 타겟 뷰 → 백버퍼 → 그래픽 장치 의 순서로 전달된다.

다만 렌더 타겟은 백버퍼만 될 수 있는 게 아니라 텍스처도 될 수 있다.

DirectX에서 렌더 타겟을 설정할 때에는 우선 렌더 타겟 뷰를 만들어주고, 디바이스 컨텍스트에서 렌더 타겟을 설정하게 된다.

// 백버퍼의 포인터로 렌더타겟 뷰를 생성합니다.
result = m_device->CreateRenderTargetView(backBufferPtr, NULL, &m_renderTargetView);

// 장치 컨텍스트(m_deviceContext)를 설정
result = D3D11CreateDeviceAndSwapChain(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, 0, &featureLevel, 1,
        D3D11_SDK_VERSION, &swapChainDesc, &m_swapChain, &m_device, NULL, &m_deviceContext);

// 장치 컨텍스트에 렌더 타겟을 설정함
m_deviceContext->OMSetRenderTargets(1, &m_renderTargetView, m_depthStencilView);

OMSetRenderTargets 함수를 통해 렌더 타겟 뷰와 깊이-스텐실 뷰를 동시에 출력 렌더링 파이프라인에 바인딩시킨다.

파이프라인을 이용한 렌더링이 수행될 때, 렌더 타겟 뷰를 거쳐서 백버퍼에 장면이 그려진다.

  • 이후 백버퍼에 그려진 것을 프론트 버퍼와 바꿔치기하여 유저의 모니터에 출력한다(Swap chain).

다만, OMSetRenderTargets는 렌더링 대상을 동시에 최대 8개까지 설정할 수 있다!

깊이-스텐실 뷰(DepthStencilView)

RenderTargetView와 마찬가지로 렌더링 파이프라인의 출력을 받을 수 있다.

그러나 렌더 타겟 뷰가 색상 값들을 담는 버퍼를 위한 것이라면, 깊이-스텐실 뷰는 깊이와 스텐실 값을 담는다.

출력-병합 과정에서 쓰이며, 깊이와 스텐실은 둘 모두 출력 테스트에 쓰일 것이기 때문에 깊이-스텐실 뷰를 하나의 뷰로 묶는다고 한다.

스텐실 값은 내가 이해한 바로는, 두 오브젝트 이상이 겹쳤을 때 뭘 표시할지를 나타낸다. 다만 깊이 테스트와는 달리, 앞에 있는지 뒤에 있는지로 결정짓는 것이 아니라 스텐실 값이 큰지 작은지를 가지고 이런저런 상황을 따져서 앞뒤를 표시한다. 포탈 같은 경우가 대표적인 예시다!

// 렌더 타겟 뷰와 깊이 스텐실 버퍼를 출력 렌더링 파이프라인에 바인딩한다.
m_deviceContext->OMSetRenderTargets(1, &m_renderTargetView, m_depthStencilView);

기본적으로 필요한 행렬

  • 투영 행렬(projection matrix): 3D 화면을 2D 뷰포트 공간으로 변환시킨다. 여기서는 원근 투영 행렬을 가리킨다.
  • 월드 행렬: 오브젝트들의 좌표를 3D 세계의 좌표로 변환하는 데에 사용된다. 또한 3차원 공간에서의 회전/이동/크기 변환에도 사용된다.
  • 뷰 행렬: 현재 장면에서 어느 위치에서 어느 방향을 보고 있는가를 계산하는 데 이용된다. 3D 세계를 카메라라 한다면 카메라에 대한 행렬이라고 볼 수 있다.
  • 직교 투영 행렬: 3D 객체가 아니라 UI와 같은 2D의 요소를 그리기 위해 사용된다. 위에서 말한 투영 행렬은 원근 투영 행렬이지만, 여기서는 직교 투영 행렬을 사용한다.

참고 자료

DirectX 11 기초 (Tutorial)-.. : 네이버블로그 (naver.com) (상당부분 풍풍풍님의 글을 참고함)

[DirectX 12] 기본지식 - 기능 수준(Feature Level) (tistory.com)

DXGI 개요(DirectX Graphics Infrastructure Overview) (tistory.com)

DXGI - Win32 apps | Microsoft Docs

Graphics pipeline - Win32 apps | Microsoft Learn

댓글