Post Lists

2019년 1월 15일 화요일

DirectX Tutorial Lesson 5: Drawing a Triangle

http://www.directxtutorial.com/Lesson.aspx?lessonid=11-4-5

Lesson Overview
이곳이 우리가 실제로 어떤 것을 그리게 될 튜토리얼의 첫 번째 부분이다. 이 강의에서, 너는 스크린에서 삼각형을 그리는 법을 배울 것이다. 우리는 일련의 정점을 생성하고 하드웨어가 그것들을 스크린에 그리게 하여 이 삼각형을 구성할 것이다.

이것은 많은 코드가 필요하다. 나는 삼각형을 렌더링하는 것이 풀 스크린으로 가는 것 만큼 빠른척 하지 않을 것이지만, 그것은 끝에서 확실히 이해될 것이고, 상황은 우리가 진행함에 따라 더 쉬워질 것이다. 그 동안 들어가 보자.

삼각형을 렌더링 하는 것은 발생하는 많은 행동들을 요구한다. 이 강의는 길지만, 이러한 부분들로 분해된다:


  1. 우리는 GPU에게 우리의 geometry 렌더링을 어떻게 할 지를 말한다.
  2. 우리는 우리의 삼각형의 세 개의 정점들을 생성한다.
  3. 우리는 이러한 정점을 video memory에 저장한다.
  4. 우리는 GPU에게 이러한 정점들을 어떻게 읽을지를 말한다.
  5. 우리는 마지막으로 삼각형을 렌더링한다.
좋은 소식은 이러한 단계뜰 각각은 쉽다. 만약 우리가 한 번에 하나씩 한다면, 이 강의는 금방 끝날 것이다!

Using Shaders
첫 번째 단계를 이야기 하여 시작해보자. GPU에게 우리의 삼각형 렌더링을 어떻게 할 지에 대해 말하면서.
렌더링 프로세스는 렌더링 파이프라인에 의해 통제된다. 만약 너가 강의 1을 기억한다면, 너는 렌더링 파이프라인이 렌더링된 이미지를 만들어내는 연속의 단계들이라는 것을 상기할 것이다. 불행하게도, 그 파이프라인은 자동적으로 무엇을 할지 알지 못한다. 그것은 처음에 프로그래밍 되어야 하고, 그것은 쉐이더에 의해서 프로그래밍 된다.

쉐이더는 잘못 만들어진 용어인데, 쉐이더가 shade를 제공하지 않기 때문이다. 쉐이더는 실제로 파이프라인의 한 단계를 통제하는 mini-program이다.

몇 가지 다른 종류의 쉐이더들이 있고, 각각은 렌더링 동안 몇 번 작동된다. 예를들어, vertex shader는 그려질 각 정점에 대해 한 번씩 작동하고, 반면에 pixel shader는 그려질 각 pixel에 대해 작동하는 프로그램이다.

우리는 다음 강의에서 쉐이더 프로그래밍에 들어갈 것이다. 지금 당장, 우리는 삼각형을 렌더링할 것이고, 그것을 하기 위해서, 우리는 GPU에 어떤 쉐이더들을 불러와야만 한다.

쉐이더들을 불러오는 것은 몇 가지 단계들을 거친다. 여기에 우리가 해야할 것이 있다:
  1. .shader file로부터 두 개의 쉐이더들을 불러오고 컴파일 한다.
  2. 두 쉐이더들을 shader objects에 캡슐화한다.
  3. 두 쉐이더들을 active shaders로 설정한다.
이러한 단계들 각각은 매우 쉽다. 그것들을 빨리 해보자.

-------------------------------------------
Shader 컴파일 부분인데, 나는 d3dx 안쓰고 DirectXTK를 써서 하므로,
D3DCompileFormFile 함수를 쓴다. 그래서 msdn의 내용을 해석하고 공부하겠다.
함수에 대한 내용은 여기에

함수의 사용 코드 예제는 여기에

D3DCompileFromFile : Microsoft High Level Shader Language(HLSL) 코드를 주어진 타겟에 대해 bytecode로 컴파일한다.

Syntax


HRESULT D3DCompileFromFile(
  LPCWSTR                pFileName,
  const D3D_SHADER_MACRO *pDefines,
  ID3DInclude            *pInclude,
  LPCSTR                 pEntrypoint,
  LPCSTR                 pTarget,
  UINT                   Flags1,
  UINT                   Flags2,
  ID3DBlob               **ppCode,
  ID3DBlob               **ppErrorMsgs
);

Parameters
- pFileName
쉐이더 코드를 포함하는 파일의 이름을 포함한 null로 끝나는 constant string에 대한 포인터.

- pDefines
shader macros를 정의하는 D3D_SHADER_MACRO의 부가적 배열. 각 macro definition은 한 이름과 NULL로 끝나는 정의를 포함한다. 만약 사용되지 않는다면 NULL로 설정.

- pInclude
Include 파일들을 처리하기 위해 컴파일러가 사용하는 ID3DInclude 인터페이스에 대한 optional pointer. 만약 너가 이 파라미터를 NULL로 설정하고, 그 shader가 #include를 포함한다면, compile 에러가 발생한다. 너는 D3D_COMPILE_STANDARD_FILE_INCLUDE 매크로를 넘길 수 있다. 이것은 default include handler에 대한 포인터이다. 이 default include handler는 현재 directory를 기준으로 handler includes files들을 포함한다.


- pEntryPoint
쉐이더 실행이 시작하는 shader entry point function의 이름을 포함하는 constant인 null 로 끝나는 스트링에 대한 포인터이다. 너가 한 effect를 컴파일 할 때, D3DCompileFromFile은 pEntrypoint를 무시한다; 우리는 너가 pEntrypoint를 NULL로 설정하기를 추천한다. 왜냐하면 만약 그 호출된 함수가 그것을 사용하지 않을 것이라면 그것은 pointer parameter를 NULL로 처리하는 것은 좋은 프로그래밍 연습이다.

- pTarget
컴파일 할 shader features의 집합 또는 쉐이더 타겟을 명시하는 constant인 null로 종료하는 string에 대한 포인터이다. 그 쉐이더 타겟은 shader model이 될 수 있다 (예를들어, shader model 2, shader model 3 4 , 5 and later). 그 타겟은 한 effect type이 될 수 있다 (예를들어, fx_4_1). 다양한 프로파일들을 지원하는 타겟에 대한 정보를 위해 Specifying Compiler Targets를 보아라.

- Flags1
bitwise OR 연산을 사용하여 결합되는 쉐이더 compile options의 조합. 그 결과 값은 컴파일러가 HLSL 코드를 어떻게 컴파일 할지를 명시한다.

- Flags2
bitwise OR 연산을 사용하여 결합되는 effect compile options의 조합. 그 최종 값은 그 컴파일러가 effect를 어떻게 컴파일 할 지를 명시한다. 너가 effect file이 아닌 shader를 컴파일 할 때, D3DCompileFromFile은 Flags2를 무시한다; 우리는 만약 그 호출된 함수가 파라미터를 사용하지 않는다면 nonpointer parameter를 zero로 설정하는 것은 좋은 연습이기 때문에, Flags2를 zero로 설정하는 것을 추천한다.

- ppCode
컴파일된 코드에 접근하기 위해 너가 사용할 수 있는 ID3DBlob interface에 대한 포인터를 받는 변수의 포인터

- ppErrorMsgs
너가 compiler error messages들을 접근하기 위해 사용할 수 있는 ID3DBlob interface에 대한 포인터를 받는 변수에 대한 부가적 포인터. 만약 에러가 없다면 NULL이다.
-------------------------------------------

2. Encapsulate both shaders into shader objects
각 쉐이더가 그것 자신의 COM object에 저장되었다.  우리는 vertex shader와 pixel shader를 만드는 것에 관심이 있다. 이러한 것에 대한 COM 오븢게트들은 ID3D11___SHhader라고 불려진다:

우리가 우리의 포인터들을 가지기만 한다면, 우리는 dev->Create___Shader()를 사용하여 그 오브젝트들을 만든다. 이렇게:

3. Set both shaders to be the active shaders
이 단계는 간단하다.

이러한 함수에 대해 첫 번째 함수는 설정한 쉐이더 오브젝트의 주소이고, 반면에 다른 두 오브젝트들은 고급이고 나중에 다뤄질 것이다.

~~

Vertex Buffers
만약 Lesson1에서 상세히 읽었다면, 너는 vertex의 정의를 상기할 것이다: 3D 공간에서 정확한 점의 location과 properteis. 그 위치는 간단히 세 개의 numerical values로 구성된다. 그리고 그것들은 그 정점의 좌표를 나타낸다. 그 vertex properties는 또한 numerical values를 사용하여 정의된다.

Direct3D는 input layout이라고 불려지는 것을 사용한다. input layout은 한 vertex의 location과 properties를 포함하는 데이터의 layut이다. 그것은 너가 수정할 수 있고, 너의 필요에 따라 설정할 수 있는 data의 한 format이다. 이것이 정확히 어떻게 작동하는지를 봐보자.

vertex는 구조체로 만들어지는데, 그것이 어떤 3D image를 위해서 만드는 지간에 적절한 데이터를 포함한다. 그 이미지를 보이기 위해서, 우리는 GPU에 모든 정보를 복사할 것이고, 그러고나서 Direct3D에게 그 데이터를 back buffer에 렌더링 하라고 명령한다. 그러나, 우리가 한 정점에 대해 가급적 원해질 수 있는 데이터를 보내려고 강요된다면 어떨까? 이것은 다음으로 발생한다.

물론, 너는 여기에서 문제가 무엇인지 모를 것이지만, 우리가 이러한 정보의 블럭 2개가 필요하다고 해보자. 우리는 그것을 GPU에게 더 빠르게 보낼 수 있다, 이렇게 해서:

이것이 우리가 input layout을 사용할 때 발생하는 것이다. 우리는 우리가 어떤 정보를 사용하고 싶은지를 고르고, 그것을 보낸다. 그리고 이것은 우리가 각 프레임 사이에 좀 더 많은 정점들을 보내도록 해준다.

~~~

Creating a Vertex Buffer
C++에서 구조체를 만들 때, 그 데이터는 system memory에 저장된다. 그러나 우리는 그것이 video memory에 있기를 원하고, 우리는 그것에 쉽게 접근하지 못한다.

우리가 video memory에 접근하기 위해서, Direct3D는 우리에게 우리가 두 시스템과 video memory에서 한 버퍼를 유지하도록 해주는 특정한 COM object를 제공한다.

어떻게 버퍼들은 두 시스템과 비디오 메모리에 존재하는가? 음, 초기에 그러한 버퍼에 있는 그 데이터는 시스템 메모리에 저장될 것이다. 그것에 대한 렌더링 호출이 발생할 때, Direct3D는 자동적으로 그것을 video memory로 너를 위해 복사할 것이다. 만약 그 비디오 카드가 메모리가 낮다면, Direct3D는 사용되지 않은 버퍼를 삭제할 것이고, 또는 더 새로운 자원을 위해 공간을 만들기 위해 "low priority"로 고려된다.

왜 우리는 Direct3D가 우리를 위해 이것을 하기를 필요하는가? 이것을 너 스스로 하는 것으 매우 어렵다, 비디오 메모리에 접근하는 것은 비디오 카드와 운영체제에 따라 다양하다. Direct3D를 갖는 것은 우리를 위해 이것을 관리하고 매우 편리하다.

이 COM object는 ID3D11 buffer라고 불려진다. 그것을 생성하기 위해, 우리는 CreateBuffer() 함수를 사용한다. 여기에 그 코드가 어떻게 생겼는지가 있다:

여기에 그것이 무엇을 의미하는지가 있다.

D3D11_BUFFER_DESC bd;
이것은 그 버퍼의 특징들을 포함하는 구조체이다.

ZeroMemory(&bd, sizeof(bd));
많은 Direct3D 구조체처럼, 우리는 그것을 zero로 만들어 초기화 할 필요가 있다.

bd.usage
버퍼들은 가능한 효율적으로 설정된다. 이것을 정확히 하기 위해서, Direct3D는 우리가 그것을 어떻게 접근하려는지 알 필요가 있다.

이 테이블은 여기에서 사용될 수 있는 다양한 플래그들을 보여준다. 우리는 이 튜토리얼을 위해 dynamic usage를 사용할 것이다.

bd.ByteWidth
이 값은 생성될 버퍼의 크기를 포함한다. 이것은 우리가 거기에 넣으려는 정점의 배열과 크기가 같다. 이 강의에서, 우리는 sizeof(VERTEX)* 3을 계산해서 이것을 얻는다.

bd.BindFlags
이 값은 Direct3D에게 어떤 종류의 버퍼를 만들지를 말한다. 우리가 할 수 있는 여러 종류의 버퍼들이 있지만, 우리가 만들고자 하는 종류는 vertex buffer이다. 이것을 위해서, 우리는 간단히 D3D11_BIND_VERTEX_BUFFER flag를 사용한다.

우리는 다른 플래그들을 여기에서 다루지 않을 것이다. 왜냐하면 그것들은 이 강의에 대해 조금 어렵기 때문이다. 그러나, 우리는 식나에 되면 그것들을 모두 다룰 것이다.

bd.CPUAccessFlags
이 멤버는 usage flags에 clarification을 더하는데, Direct3D에게 어떻게 우리가 CPU에 접근할 지를 말해서 한다. 여기에서 유일한 flags들은 D3D11_CPU_ACCESS_WRITE와 D3D11_CPU_ACCESS_READ이고, 그것들은 Table 2.1과 동일하게 사용되어질 수 있다.

~

Filling the Vertex Buffer
그래서 우리는 이제 삼각형에 대한 정점들을 가지고 있고, 우리는 그것들을 배치할 vertex buffer를 가지고 있다. 이제 우리가 할 필요가 있는 것은 그 정점들을 buffer에 복사하는 것이다.

그러나 Direct3D는 background에 있는 버퍼와 작동하기 때문에, 그것은 너에게 그곳에 대한 직접적은 CPU access를 주지 않는다. 그것에 접근하기 위해, 그 버퍼는 mapped 되어야만 한다. 이것은 Direct3D가 그 버퍼가 끝나도록 어떤 것이 처리되도록 하고, GPU가 그것이 unmapped 될 때 까지 버퍼로 작업하지 않게 막는 것을 의미한다.

그래서 vertex buffer를 채우기 위해 우리는:
  1. 그 vertex buffer를 Map한다 (그리고 그것으로 ㅇ니해 buffer의 location을 얻는다).
  2. 그 data를 buffer에 복사한다 (memcpy()를 사용해서).
  3. buffer를 unmap 한다.
DED11_MAPPED_SUBVRESOURCE ms;
이것은 우리가 mapped하고나서 그 버퍼에 대한 정보로 채워질 구조체이다. 이 정보는 그 버퍼의 location에 대한 포인터를 포함한다. 그리고 우리는 ms.Pdata를 사용해서 이 포인터에 접근할 수 있다.

devcon->Map()
그 다음 line은 그 버퍼를 maps하고, 우리가 그것에 접근하게 허용한다. 그 파라미터는 꽤 쉽다.

그 첫 번째 것은 버퍼 오브젝트의 주소이다. 우리의 버퍼 포인터는 pVBuffer라고 불려지고, 그래서 그것을 사용한다.

그 두 번째 것은 고급이다. 우리는 그것을 나중에 할 것이다. 운 좋게도 우리는 지금 그것을 NULL로 설정할 수 있다.

그 네 번째 파라미터는 그것이 mapped되는 동안 버퍼에 CPU 접근을 통제하게 해주는 플래그들의 집합이다. 우리는 DED11_MAP_WRITE_DISCARD를 쓸 것이지만, 다른 것들은 이 테이블에서 발견될 수 있다.

~

그 네 번째 파라미터는 또 다른 FLAG이고, 그것은 NULL or D3D11_MAPFLAG_DO_NOT_WAIT이 될 수 있다. 이 플래그는 프로그램이 계속하게 강요한다, 비록 GPU가 여전히 그 버퍼와 작업중일지라도.

그 마지막 파라미터는 D3D11_MAPPED_SUBRESOURCE 구조체의 주소이다. 그 함수는 우리가 여기에서 지정한 구조체를 채운다. 이것은 우리에게 필요한 정보를 준다.

~~

Verifying the Input Layout
지금까지 이 강의에서 우리는

A) 쉐이더들을 불러오고 파이프라인을 제어하도록 설정했고
B) 정점을 사용하여 shape를 만들고, 그것들을 GPU가 사용하도록 읽어들였다.

너는 GPU가 어떻게 우리의 정점들을 읽을 수 있는지 궁금할지도 모른다. 우리가 그것들을 우리 자신의 self-created struct로 배치했을 때. 어떻게 그것은 우리가 color 앞에 location을 배치했는지를 알 수 있는가? 어떻게 그것은 우리가 어떤 다른 것을 의미하지 않았느지 알 수 있는가?

그 답은 input layout이다.

이전에 언급했듯이, 우리는 렌더링 속도를 개선하기 위해 각 정점과 함께 무슨 저옵가 저장되었는지를 고를 수 있다. 그 input layout은 그 정점 구조체의 layout을 포함하는 오브젝트 이다. 그리고 이것은 GPU가 그 데이터를 적절하고 효율적으로 구성하도록 한다.

ID3D11InputLayout object는 우리의 VERTEX 구조체의 layout을 저장한다. 이 오브젝트를 만들기 위해 우리는 CreateInputLayout() 함수를 호출한다.

이것에 대해 두 부분이 있다. 처음에, 우리는 그 정점의 대한 각 원소를 정의할 필요가 있다. 두 번째, 우리는 그 input layout object를 만들 필요가 있다.

~~~
























댓글 없음:

댓글 쓰기