원문: http://www.rastertek.com/dx11tut04.html
이 튜토리얼은 DirectX 11의 정점(vertex)과 픽셀 셰이더에 대한 소개가 될 것입니다. 또한 DirectX 11에서의 이것들에 대한 사용법도 포함합니다. 이것들은 3D 그래픽스를 이해하고 응용하기 위해 필요한 가장 기본이 되는 개념입니다.
정점 버퍼(Vertex buffers)
처음으로 소개할 개념은 바로 정점 버퍼입니다. 이해를 돕기 위해 3D로 표현된 공의 모형을 보도록 하겠습니다.
사실 3D 공의 모형은 수백개의 삼각형으로 만든 것입니다.
공 모델에 있는 각 삼각형들은 세 꼭지점을 가지고 있는데, 이 꼭지점을 정점(vertex)라고 부릅니다. 따라서 공을 표현하기 위해서 우리는 공을 구성하고 있는 모든 정점들을 정점 버퍼(vertex buffer)라고 부르는 특별한 데이터 배열에 넣어주어야 합니다. 일단 모든 공의 모든 점들을 정점 버퍼에 넣었다면 이를 GPU에게 보내 공을 그리도록 하게 할 수 있습니다.
인덱스 버퍼(Index buffers)
인덱스 버퍼는 정점 버퍼와 관련이 있습니다. 이것은 정점 버퍼에 있는 각 정점들이 연결되는 순서를 기록하기 위한 것입니다. 그러면 GPU는 인덱스 버퍼를 이용해서 빠르게 정점 버퍼에서 필요한 몇몇 정점들을 찾아냅니다. 인덱스 버퍼는 여러분이 원하는 것을 빠르게 찾게 도와준다는 점에서 책의 목차와 같다고도 할 수 있습니다. DirectX SDK의 문서에서는 인덱스 버퍼의 사용이 비디오 메모리에서의 정점 데이터의 캐싱의 가능성을 높여준다고 합니다. 따라서 성능 측면에서도 인덱스 버퍼의 사용은 권장되는 바입니다.
정점 셰이더(Vertex shaders)
정점 셰이더는 주로 정점 버퍼의 정점들을 3D공간으로 변환시켜주는 작은 프로그램입니다. 이 외에도 각 정점의 법선을 계산한다던가 하는 다른 연산도 가능합니다. 정점 셰이더 프로그램은 GPU에서 계산이 필요하다고 판단될 때 호출됩니다. 예를 들어, 5,000개의 폴리곤을 가진 모델을 화면에 표시한다면 단지 저 하나의 모델을 그리기 위해 매 프레임마다 15000번의 정점 셰이더 프로그램이 실행됩니다. 따라서 프로그램이 60fps의 fps를 가진 그래픽 프로그램에서는 단지 5000개의 삼각형을 그리기 위해 매 초마다 900,000번의 정점 셰이더를 호출하게 됩니다. 따라서 효율적인 정점 셰이더를 작성하는 것이 중요합니다.
픽셀 셰이더(Pixel shaders)
픽셀 셰이더는 그리고자 하는 도형에 색상을 입힐 때를 위한 작은 프로그램입니다. 이것은 화면에 보여지는 모든 픽셀들에 대해 GPU에서 연산됩니다. 색상을 입히고(coloring), 텍스쳐를 입히고(texturing), 광원 효과를 주고(lighting), 그 외 다른 많은 도형 채색 효과를 주는 것이 바로 이 픽셀 셰이더 프로그램에서 제어됩니다. 픽셀 셰이더는 GPU에 의해 수없이 호출되기 때문에 반드시 효율적으로 작성되어야 합니다.
HLSL(High Level Shader Language)
HLSL은 앞서 설명한 DirectX 11에서 사용하는 작은 정점 및 픽셀 셰이더 프로그램을 작성할 때 사용하는 일종의 언어입니다. 구문은 미리 정의된 타입이 있다는 것을 빼면 C언어와 거의 동일합니다. HLSL 프로그램 파일은 전역 변수, 타입 정의, 정점 셰이더, 픽셀 셰이더, 그리고 기하 셰이더(geometry shader)로 구성되어 있습니다. 이 튜토리얼은 HLSL을 사용하는 첫번째이므로 여기서는 우선 DirectX 11을 이용하여 아주 간단한 HLSL 프로그램을 실행해 볼 것입니다.
프레임워크 업데이트
이 튜토리얼은 위해 프레임워크가 한층 업데이트되었습니다. GraphicsClass 안에 CameraClass, ModelClass, ColorShaderClass라고 하는 세 클래스를 추가하였습니다. CameraClass는 지난번에 이야기했던 뷰 행렬을 다룰 것입니다. 이것은 현재 월드에서의 카메라의 위치, 보는 방향을 제어하며 이 위치를 필요한 셰이더에 전달합니다. ModelClass는 3D 객체들의 기하학적인 부분을 다룰 것인데, 이 튜토리얼에서는 단순함을 유지하기 위해 3D 객체를 삼각형으로 할 것입니다. 그리고 마지막으로 ColorShaderClass는 직접 작성한 HLSL 셰이더를 호출하여 객체들을 그리는 일을 맡게 될 것입니다.
우선 HLSL 셰이더 프로그램부터 시작하도록 하겠습니다.
Color.vs
이것이 우리의 첫번째 셰이더 프로그램이 될 것입니다. 셰이더는 실제 모델의 렌더링을 수행하는 작은 프로그램입니다. 이 셰이더는 color.vs와 color.ps라는 소스파일에 HLSL로 작성되었습니다. 우선은 이 파일들을 엔진의 cpp파일과 h파일과 같은 위치에 놓았습니다. 이 셰이더의 목적은 우선 첫 HLSL 튜토리얼이기 때문에 가능한 한 단순함을 유지하면서 색상이 있는 삼각형을 그리고자 하는 것입니다. 우선 정점 셰이더부터 보도록 하겠습니다.
//////////////////////////////////////////////////////////////////////////////// // Filename: color.vs ////////////////////////////////////////////////////////////////////////////////
셰이더 프로그램은 우선 전역 변수로부터 시작합니다. 이 전역 변수들은 여러분의 C++코드에서 접근하여 수정할 수 있습니다. 여기서 int나 float와 같은 타입의 변수들을사용할 수 있고 셰이더 프로그램을 사용하기 전에 외부적으로 설정할 수 있습니다. 보통 대부분의 전역 변수들은 그것이 단지 하나뿐일지라도 'cbuffer'이라고 불리는 버퍼 객체 타입에 넣게 됩니다. 논리적으로 이 버퍼들을 조직하는 일은 셰이더의 효율적인 실행뿐만 아니라 그래픽카드가 어떻게 이 버퍼들을 저장하는지와도 관련되기 때문에 중요합니다. 이 예제에서는 세 개의 행렬을 사용하는데, 매 프레임마다 같이 업데이트하기 때문에 같은 버퍼에 넣도록 하였습니다.
///////////// // GLOBALS // ///////////// cbuffer MatrixBuffer { matrix worldMatrix; matrix viewMatrix; matrix projectionMatrix; };
사용자 정의 타입 역시 C와 비슷하게 사용할 수 있습니다. 여기서는 HLSL에서만 지원하는 float4와 같은 타입을 사용하여 셰이더 프로그래밍을 쉽고 가독성이 높게 만들었습니다. 이 예제에서는 x, y, z, w위치를 가지는 벡터와 빨강(red), 초록(green), 파랑(blue), 투명도(alpha)의 색상을 모두 가지는 타입을 만들었습니다. POSITION, COLOR, 그리고 SV_POSITION은 GPU에 이 변수들을 사용할 것이라고 전달합니다. 비록 구조는 같지만 정점 및 픽셀 셰이더의 의미가 다르기 때문에 이에 해당하는 두개의 다른 구조체를 만들었습니다. COLOR은 두 구조체 모두 공통으로 가지고 있지만 POSITION은 정점 셰이더에 정의되어 있으며 SV_POSITION은 픽셀 셰이더에서 동작합니다. 만약 같은 타입을 더 사용하고 싶다면 COLOR0이나 COLOR1처럼 변수 이름 끝에 숫자를 붙여주어야 합니다.
////////////// // TYPEDEFS // ////////////// struct VertexInputType { float4 position : POSITION; float4 color : COLOR; }; struct PixelInputType { float4 position : SV_POSITION; float4 color : COLOR; };
정점 셰이더는 정점 버퍼의 데이터를 처리하기 위해 GPU에 의해 호출됩니다. ColorVertexShader라고 이름지은 정점 셰이더는 정점 버퍼의 모든 각 정점에 대해 호출됩니다. 정점 셰이더의 입력으로 들어오는 것은 VertexInputType와 같이 셰이더의 소스에서 정의한 자료구조와 그 구조가 맞아 떨어져야 합니다. 정점 셰이더의 처리 결과는 픽셀 셰이더로 보내지는데, 이 소스에서는 앞서 언급했던 PixelInputType 형태로 그 출력이 나오게 됩니다.
주의깊이 보아야 할 것은 정점 셰이더가 출력으로 PixelInputType 타입을 만들어낸다는 것입니다. 입력된 정점들은 들어와서는 월드, 뷰, 그리고 사영 행렬과 곱셈 연산을 하게 됩니다. 이렇게 하여 정점을 3D 세계에서의 정점을 우리의 시야에 맞는 2D화면의 위치로 지정해 줍니다. 출력으로 나온 데이터들이 색상을 할당받은 뒤에는 픽셀 셰이더의 입력 데이터로 사용됩니다. 또한 여기서 위치 지정을 위해 XYZ좌표만을 사용하기 때문에 입력 변수 W의 값은 1.0으로 설정하였다는 걸 주의하십시오.
//////////////////////////////////////////////////////////////////////////////// // Vertex Shader //////////////////////////////////////////////////////////////////////////////// PixelInputType ColorVertexShader(VertexInputType input) { PixelInputType output; // 올바르게 행렬 연산을 하기 위하여 position 벡터를 w까지 있는 4성분이 있는 것으로 사용합니다. input.position.w = 1.0f; // 정점의 위치를 월드, 뷰, 사영의 순으로 계산합니다. output.position = mul(input.position, worldMatrix); output.position = mul(output.position, viewMatrix); output.position = mul(output.position, projectionMatrix); // 픽셀 셰이더에서 사용하기 위해 입력 색상을 저장합니다. output.color = input.color; return output; }
Color.ps
픽셀 셰이더는 화면에 그려지는 도형의 각 픽셀들을 실제로 그립니다. 이 픽셀 셰이더에서는 PixelInputType을 입력으로 사용하고 최종 픽셀의 색상이 저장된 float4 타입을 반환합니다. 여기서 소개하는 픽셀 셰이더는 단지 입력 색상을 바로 출력으로 내보내는 단순한 프로그램입니다. 기억해 둘 것은, 정점 셰이더의 결과물을 픽셀 셰이더에서 사용한다는 점입니다.
//////////////////////////////////////////////////////////////////////////////// // Filename: color.ps //////////////////////////////////////////////////////////////////////////////// ////////////// // TYPEDEFS // ////////////// struct PixelInputType { float4 position : SV_POSITION; float4 color : COLOR; }; //////////////////////////////////////////////////////////////////////////////// // Pixel Shader //////////////////////////////////////////////////////////////////////////////// float4 ColorPixelShader(PixelInputType input) : SV_TARGET { return input.color; }
Modelclass.h
앞서 언급했듯이, ModelClass는 3D 모델들의 복잡한 기하학들을 캡슐화하는 클래스입니다. 이 튜토리얼에서는 한 녹색 삼각형을 만들기 위한 데이터를 차근차근 만들어 볼 것입니다. 또한 이 삼각형이 화면에 그려지기 위해 필요한 정점 버퍼와 인덱스 버퍼도 역시 만들 것입니다.
//////////////////////////////////////////////////////////////////////////////// // Filename: modelclass.h //////////////////////////////////////////////////////////////////////////////// #ifndef _MODELCLASS_H_ #define _MODELCLASS_H_ ////////////// // INCLUDES // ////////////// #include <d3d11.h> #include <d3dx10math.h> //////////////////////////////////////////////////////////////////////////////// // Class name: ModelClass //////////////////////////////////////////////////////////////////////////////// class ModelClass { private:
여기서 이 ModelClass의 정점 버퍼에 사용할 정점의 구조체를 선언합니다. 이 typedef 문은 나중 튜토리얼에서 살펴볼 ColorShaderClass에서 사용할 것과 그 구조가 같아야 합니다.
struct VertexType { D3DXVECTOR3 position; D3DXVECTOR4 color; }; public: ModelClass(); ModelClass(const ModelClass&); ~ModelClass();
이 함수들은 3D 모델의 정점 버퍼와 인덱스 버퍼들의 초기화와 종료 과정을 제어합니다. Render 함수는 그래픽 카드에 모델들의 기하 정보를 넣고 컬러 셰이더로 그릴 준비를 합니다.
bool Initialize(ID3D11Device*); void Shutdown(); void Render(ID3D11DeviceContext*); int GetIndexCount(); private: bool InitializeBuffers(ID3D11Device*); void ShutdownBuffers(); void RenderBuffers(ID3D11DeviceContext*);
ModelClass의 private로 선언된 변수들은 정점 버퍼와 인덱스 버퍼, 그리고 각 버퍼의 크기 정보를 가지고 있는 변수입니다. DirectX 11의 버퍼들은 대개 일반적인 ID3D11Buffer 타입을 이용하며 생성할 때의 버퍼 description으로 구분합니다.
private: ID3D11Buffer *m_vertexBuffer, *m_indexBuffer; int m_vertexCount, m_indexCount; }; #endif
Modelclass.cpp
//////////////////////////////////////////////////////////////////////////////// // Filename: modelclass.cpp //////////////////////////////////////////////////////////////////////////////// #include "modelclass.h"
클래스 생성자에서는 정점 버퍼와 인덱스 버퍼의 포인터를 null로 설정합니다.
ModelClass::ModelClass() { m_vertexBuffer = 0; m_indexBuffer = 0; } ModelClass::ModelClass(const ModelClass& other) { } ModelClass::~ModelClass() { }
Initialize 함수는 정점 버퍼와 인덱스 버퍼의 초기화 함수를 호출합니다.
bool ModelClass::Initialize(ID3D11Device* device) { bool result; // 정점 버퍼와 인덱스 버퍼를 초기화합니다. result = InitializeBuffers(device); if(!result) { return false; } return true; }
Shutdown 함수는 정점 버퍼와 인덱스 버퍼를 정리하는 함수를 호출합니다.
void ModelClass::Shutdown() { // 정점 버퍼와 인덱스 버퍼를 해제합니다. ShutdownBuffers(); return; }
Render 함수는 GraphicsClass::Render 함수에서 호출됩니다. 이 함수에서는 RenderBuffers 함수를 호출하여 정점 버퍼와 인덱스 버퍼를 그래픽 파이프라인에 넣어 컬러 셰이더가 이들을 그릴 수 있도록 합니다.
void ModelClass::Render(ID3D11DeviceContext* deviceContext) { // 정점 버퍼와 인덱스 버퍼를 그래픽스 파이프라인에 넣어 화면에 그릴 준비를 합니다. RenderBuffers(deviceContext); return; }
GetIndexCount 함수는 해당 모델의 인덱스의 개수를 알려줍니다. 컬러 셰이더에서 모델을 그리기 위해서는 이 정보가 필요합니다.
int ModelClass::GetIndexCount() { return m_indexCount; }
InitializeBuffers 함수는 정점 버퍼와 인덱스 버퍼를 생성하는 작업을 제어합니다. 보통 이 부분에서는 데이터 파일로부터 모델의 정보를 읽어 와서 버퍼들을 만드는 일을 합니다. 이 튜토리얼에서는 삼각형 하나만을 다루기 때문에 간단히 정점 버퍼와 인덱스 버퍼에 점을 세팅하는 일만을 합니다.
bool ModelClass::InitializeBuffers(ID3D11Device* device) { VertexType* vertices; unsigned long* indices; D3D11_BUFFER_DESC vertexBufferDesc, indexBufferDesc; D3D11_SUBRESOURCE_DATA vertexData, indexData; HRESULT result;
우선, 정점과 인덱스 데이터를 담아둘 두 개의 임시 배열을 만들고 나중에 최종 버퍼를 생성할 때 사용하도록 합니다.
// 정점 배열의 길이를 설정합니다. m_vertexCount = 3; // 인덱스 배열의 길이를 설정합니다. m_indexCount = 3; // 정점 배열을 생성합니다. vertices = new VertexType[m_vertexCount]; if(!vertices) { return false; } // 인덱스 배열을 생성합니다. indices = new unsigned long[m_indexCount]; if(!indices) { return false; }
그리고 정점/인덱스 배열에 삼각형의 각 점과 그 순서를 채워넣습니다. 반드시 주의해야 할 점은, 이것을 그리기 위해서는 점들을 시계 방향으로 만들어야 한다는 것입니다. 만약 반시계 방향으로 만들게 되면 DirectX에서 이 삼각형은 반대편을 바라본다고 판단하며 backface culling에 의해 그려지지 않게 됩니다. 따라서 GPU에게 도형을 그리도록 할 때 이 순서를 기억하는 것이 중요합니다. 여기서 정점의 description을 작성하기 때문에 색상 역시 정해주게 됩니다. 여기서는 녹색으로 정하였습니다.
// 정점 배열에 값을 넣습니다. vertices[0].position = D3DXVECTOR3(-1.0f, -1.0f, 0.0f); // 왼쪽 아래 vertices[0].color = D3DXVECTOR4(0.0f, 1.0f, 0.0f, 1.0f); vertices[1].position = D3DXVECTOR3(0.0f, 1.0f, 0.0f); // 상단 가운데 vertices[1].color = D3DXVECTOR4(0.0f, 1.0f, 0.0f, 1.0f); vertices[2].position = D3DXVECTOR3(1.0f, -1.0f, 0.0f); // 오른쪽 아래 vertices[2].color = D3DXVECTOR4(0.0f, 1.0f, 0.0f, 1.0f); // 인덱스 배열에 값을 넣습니다. indices[0] = 0; // 왼쪽 아래 Bottom left. indices[1] = 1; // 상단 가운데 indices[2] = 2; // 오른쪽 아래
정점 배열과 인덱스 배열이 채워졌으므로 이를 이용하여 정점 버퍼와 인덱스 버퍼를 만듭니다. 두 버퍼를 만드는 일은 비슷한 과정을 거치게 됩니다. 우선 버퍼에 대한 description을 작성합니다. 이 description에는 ByteWidth(버퍼의 크기)와 BindFlags(버퍼의 타입)을 정확히 입력해야 합니다. 이를 채워넣은 이후에는 방금 만들었던 정점 배열과 인덱스 배열을 subresource 포인터에 연결합니다. 이 descrition과 subresource 포인터, 그리고 D3D 디바이스의 CreateBuffer 함수를 사용하여 새 버퍼의 포인터를 받아옵니다.
// 정점 버퍼의 description을 작성합니다. vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT; vertexBufferDesc.ByteWidth = sizeof(VertexType) * m_vertexCount; vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER; vertexBufferDesc.CPUAccessFlags = 0; vertexBufferDesc.MiscFlags = 0; vertexBufferDesc.StructureByteStride = 0; // 정점 데이터를 가리키는 보조 리소스 구조체를 작성합니다. vertexData.pSysMem = vertices; vertexData.SysMemPitch = 0; vertexData.SysMemSlicePitch = 0; // 정점 버퍼를 생성합니다. result = device->CreateBuffer(&vertexBufferDesc, &vertexData, &m_vertexBuffer); if(FAILED(result)) { return false; } // 인덱스 버퍼의 description을 작성합니다. indexBufferDesc.Usage = D3D11_USAGE_DEFAULT; indexBufferDesc.ByteWidth = sizeof(unsigned long) * m_indexCount; indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER; indexBufferDesc.CPUAccessFlags = 0; indexBufferDesc.MiscFlags = 0; indexBufferDesc.StructureByteStride = 0; // 인덱스 데이터를 가리키는 보조 리소스 구조체를 작성합니다. indexData.pSysMem = indices; indexData.SysMemPitch = 0; indexData.SysMemSlicePitch = 0; // 인덱스 버퍼를 생성합니다. result = device->CreateBuffer(&indexBufferDesc, &indexData, &m_indexBuffer); if(FAILED(result)) { return false; }
정점 버퍼와 인덱스 버퍼를 만든 후에는 이미 값이 복사되어 필요가 없어진 정점 배열과 인덱스 배열을 제거할 수 있습니다.
// 생성되고 값이 할당된 정점 버퍼와 인덱스 버퍼를 해제합니다. delete [] vertices; vertices = 0; delete [] indices; indices = 0; return true; }
ShutdownBuffers 함수는 단순히 InitializeBuffers 함수에서 만들었던 정점 버퍼와 인덱스 버퍼를 해제하는 일을 합니다.
void ModelClass::ShutdownBuffers() { // 인덱스 버퍼를 해제합니다. if(m_indexBuffer) { m_indexBuffer->Release(); m_indexBuffer = 0; } // 정점 버퍼를 해제합니다. if(m_vertexBuffer) { m_vertexBuffer->Release(); m_vertexBuffer = 0; } return; }
RenderBuffers 함수는 Render 함수에서 호출됩니다. 이 함수의 목적은 바로 정점 버퍼와 인덱스 버퍼를 GPU의 어셈블러의 버퍼로서 활성화시키는 것입니다. 일단 GPU가 활성화된 정점 버퍼를 가지게 되면 셰이더를 이용하여 버퍼의 내용을 그릴 수 있게 됩니다. 또한 이 함수에서는 이 정점을 삼각형이나 선분, 부채꼴 등 어떤 모양으로 그리게 될지 정의합니다. 이 튜토리얼에서는 어셈블러의 입력에 정점 버퍼와 인덱스 버퍼를 넣고 DirectX의 IASetPrimitiveTopology 함수를 사용하여 GPU에게 이 정점들을 삼각형으로 그리도록 주문합니다.
void ModelClass::RenderBuffers(ID3D11DeviceContext* deviceContext) { unsigned int stride; unsigned int offset; // 정점 버퍼의 단위와 오프셋을 설정합니다. stride = sizeof(VertexType); offset = 0; // input assembler에 정점 버퍼를 활성화하여 그려질 수 있게 합니다. deviceContext->IASetVertexBuffers(0, 1, &m_vertexBuffer, &stride, &offset); // input assembler에 인덱스 버퍼를 활성화하여 그려질 수 있게 합니다. deviceContext->IASetIndexBuffer(m_indexBuffer, DXGI_FORMAT_R32_UINT, 0); // 정점 버퍼로 그릴 기본형을 설정합니다. 여기서는 삼각형입니다. deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); return; }
Colorshaderclass.h
ColorShaderClass는 GPU상에 존재하는 3D 모델들을 그리는 데 사용하는 HLSL 셰이더를 호출하는 클래스입니다.
//////////////////////////////////////////////////////////////////////////////// // Filename: colorshaderclass.h //////////////////////////////////////////////////////////////////////////////// #ifndef _COLORSHADERCLASS_H_ #define _COLORSHADERCLASS_H_ ////////////// // INCLUDES // ////////////// #include <d3d11.h> #include <d3dx10math.h> #include <d3dx11async.h> #include <fstream> using namespace std; //////////////////////////////////////////////////////////////////////////////// // Class name: ColorShaderClass //////////////////////////////////////////////////////////////////////////////// class ColorShaderClass { private:
아래의 것은 정점 셰이더에 사용될 cBuffer 구조체의 정의입니다. 셰이더가 올바로 렌더링을 하기 위해서는 이 typedef문의 구조체가 반드시 정점 셰이더에 사용된 구조체와 동일해야 합니다.
struct MatrixBufferType { D3DXMATRIX world; D3DXMATRIX view; D3DXMATRIX projection; }; public: ColorShaderClass(); ColorShaderClass(const ColorShaderClass&); ~ColorShaderClass();
여기에 있는 함수들은 셰이더의 초기화와 마무리를 제어합니다. Render 함수는 셰이더에 사용되는 변수들을 설정하고 셰이더를 이용해 준비된 모델의 정점들을 그려냅니다.
bool Initialize(ID3D11Device*, HWND); void Shutdown(); bool Render(ID3D11DeviceContext*, int, D3DXMATRIX, D3DXMATRIX, D3DXMATRIX); private: bool InitializeShader(ID3D11Device*, HWND, WCHAR*, WCHAR*); void ShutdownShader(); void OutputShaderErrorMessage(ID3D10Blob*, HWND, WCHAR*); bool SetShaderParameters(ID3D11DeviceContext*, D3DXMATRIX, D3DXMATRIX, D3DXMATRIX); void RenderShader(ID3D11DeviceContext*, int); private: ID3D11VertexShader* m_vertexShader; ID3D11PixelShader* m_pixelShader; ID3D11InputLayout* m_layout; ID3D11Buffer* m_matrixBuffer; }; #endif
Colorshaderclass.cpp
//////////////////////////////////////////////////////////////////////////////// // Filename: colorshaderclass.cpp //////////////////////////////////////////////////////////////////////////////// #include "colorshaderclass.h"
다른 클래스들처럼 이 클래스의 생성자에서도 모든 private로 선언된 포인터들을 null로 초기화합니다.
ColorShaderClass::ColorShaderClass() { m_vertexShader = 0; m_pixelShader = 0; m_layout = 0; m_matrixBuffer = 0; } ColorShaderClass::ColorShaderClass(const ColorShaderClass& other) { } ColorShaderClass::~ColorShaderClass() { }
Initialize 함수는 셰이더의 초기화를 수행하는 함수를 호출합니다. 여기서는 아까 살펴본 color.vs, color.ps의 HLSL 셰이더 파일의 이름을 넘겨줍니다.
bool ColorShaderClass::Initialize(ID3D11Device* device, HWND hwnd) { bool result; // 정점 셰이더와 픽셀 셰이더를 초기화합니다. result = InitializeShader(device, hwnd, L"../Engine/color.vs", L"../Engine/color.ps"); if(!result) { return false; } return true; }
Shutdown함수는 셰이더를 정리하는 함수를 호출합니다.
void ColorShaderClass::Shutdown() { // 정점 셰이더와 픽실 셰이더 및 그와 관련된 것들을 반환합니다. ShutdownShader(); return; }
Render 함수에서는 우선 SetshaderParameters 함수를 사용하여 셰이더에서 사용될 인자들을 설정합니다. 인자들이 설정되면 RenderShader 함수를 호출하여 HLSL 셰이더를 이용한 녹색 삼각형 그리기를 수행합니다.
bool ColorShaderClass::Render(ID3D11DeviceContext* deviceContext, int indexCount, D3DXMATRIX worldMatrix, D3DXMATRIX viewMatrix, D3DXMATRIX projectionMatrix) { bool result; // 렌더링에 사용할 셰이더의 인자를 입력합니다. result = SetShaderParameters(deviceContext, worldMatrix, viewMatrix, projectionMatrix); if(!result) { return false; } // 셰이더를 이용하여 준비된 버퍼를 그립니다. RenderShader(deviceContext, indexCount); return true; }
이제 이 튜토리얼에서 가장 중요한 InitializeShader이라고 하는 함수를 보도록 하겠습니다. 이 함수에서 실제로 셰이더 파일을 불러오고 DirectX와 GPU에서 사용 가능하도록 하는 일을 합니다. 또한 레이아웃을 세팅하고 어떻게 정점 버퍼의 데이터가 GPU에서 사용되는지 볼 수 있습니다. 이 레이아웃은 modelclass.h뿐만 아니라 color.vs에 선언된 VertexType와 일치할 필요가 있습니다.
bool ColorShaderClass::InitializeShader(ID3D11Device* device, HWND hwnd, WCHAR* vsFilename, WCHAR* psFilename) { HRESULT result; ID3D10Blob* errorMessage; ID3D10Blob* vertexShaderBuffer; ID3D10Blob* pixelShaderBuffer; D3D11_INPUT_ELEMENT_DESC polygonLayout[2]; unsigned int numElements; D3D11_BUFFER_DESC matrixBufferDesc; // 이 함수에서 사용하는 포인터들을 null로 설정합니다. errorMessage = 0; vertexShaderBuffer = 0; pixelShaderBuffer = 0;
이제 여기서 셰이더 프로그램을 버퍼로 컴파일합니다. 셰이더 파일의 이름, 셰이더의 이름, 셰이더의 버전(DirectX 11에서는 5.0), 그리고 셰이더가 컴파일될 버퍼를 인자로 넘겨줍니다. 만약 컴파일 과정이 실패하면 에러 메세지를 errorMessage 문자열에 출력하고 이것은 다른 함수로 볼 수 있습니다. 만약 errorMessage 없이 실패한다면 셰이더 파일이 없다는 의미이므로 그 내용을 알려주는 다이얼로그를 생성하도록 합니다.
// 정점 셰이더를 컴파일합니다. result = D3DX11CompileFromFile(vsFilename, NULL, NULL, "ColorVertexShader", "vs_5_0", D3D10_SHADER_ENABLE_STRICTNESS, 0, NULL, &vertexShaderBuffer, &errorMessage, NULL); if(FAILED(result)) { // 셰이더가 컴파일에 실패하면 에러 메세지를 기록합니다. if(errorMessage) { OutputShaderErrorMessage(errorMessage, hwnd, vsFilename); } // 에러 메세지가 없다면 셰이더 파일을 찾지 못한 것입니다. else { MessageBox(hwnd, vsFilename, L"Missing Shader File", MB_OK); } return false; } // 픽셀 셰이더를 컴파일합니다. result = D3DX11CompileFromFile(psFilename, NULL, NULL, "ColorPixelShader", "ps_5_0", D3D10_SHADER_ENABLE_STRICTNESS, 0, NULL, &pixelShaderBuffer, &errorMessage, NULL);
if(FAILED(result)) { // 셰이더 컴파일이 실패하면 에러 메세지를 기록합니다. if(errorMessage) { OutputShaderErrorMessage(errorMessage, hwnd, psFilename); } // 에러 메세지가 없다면 단순히 셰이더 파일을 찾지 못한 것입니다. else { MessageBox(hwnd, psFilename, L"Missing Shader File", MB_OK); } return false; }
정점 셰이더와 픽셀 셰이더가 버퍼로 잘 컴파일되면 이를 이용하여 셰이더 객체를 만들 수 있습니다. 여기서 나온 포인터를 정점 셰이더와 픽셀 셰이더의 인터페이스로서 사용할 것입니다.
// 버퍼로부터 정점 셰이더를 생성합니다. result = device->CreateVertexShader(vertexShaderBuffer->GetBufferPointer(), vertexShaderBuffer->GetBufferSize(), NULL, &m_vertexShader); if(FAILED(result)) { return false; } // 버퍼로부터 픽셀 셰이더를 생성합니다. result = device->CreatePixelShader(pixelShaderBuffer->GetBufferPointer(), pixelShaderBuffer->GetBufferSize(), NULL, &m_pixelShader); if(FAILED(result)) { return false; }
다음 과정은 셰이더에서 사용할 정점 데이터의 레이아웃을 생성하는 것입니다. 이 셰이더에서는 위치 벡터와 색상 벡터를 사용하므로 레이아웃에 각각의 벡터의 크기를 포함하는 두 레이아웃을 만듭니다. SemanticName은 이 요소가 레이아웃에서 어떻게 사용되는지 알려주므로 레이아웃에서 가장 먼저 채워져야 할 항목입니다. 우선 두 다른 요소들 중에서 POSITION을 먼저, COLOR를 두번째로 처리합니다. 그 다음으로 레이아웃에서 중요한 부분은 Format입니다. 위치 벡터에 대해서는 DXGI_FORMAT_R32G32B32_FLOAT를 사용하고 색상 벡터에 대해서는 DXGI_FORMAT_R32G32B32A32_FLOAT를 사용합니다. 마지막으로 주의를 기울여야 할 것은 버퍼에 데이터가 어떻게 배열되는지 알려주는 AlignedByteOffset 입니다. 이 레이아웃에서는 처음 12 byte를 위치 벡터에 사용하고 다음 16 byte를 색상으로 사용할 것임을 알려줘야 하는데, AlignedByteOffset이 각 요소가 어디서 시작하는지 보여줍니다. 여기서 직접 값을 입력하기보다 D3D11_APPEND_ALIGNED_ELEMENT 를 지정하여 자동으로 알아내도록 합니다. 나머지는 이 튜토리얼에서 크게 중요하지 않기 때문에 기본값으로 두었습니다.
// 정점 입력 레이아웃 description을 작성합니다. // 이 설정은 ModelClass와 셰이더에 있는 VertexType와 일치해야 합니다. polygonLayout[0].SemanticName = "POSITION"; polygonLayout[0].SemanticIndex = 0; polygonLayout[0].Format = DXGI_FORMAT_R32G32B32_FLOAT; polygonLayout[0].InputSlot = 0; polygonLayout[0].AlignedByteOffset = 0; polygonLayout[0].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA; polygonLayout[0].InstanceDataStepRate = 0; polygonLayout[1].SemanticName = "COLOR"; polygonLayout[1].SemanticIndex = 0; polygonLayout[1].Format = DXGI_FORMAT_R32G32B32A32_FLOAT; polygonLayout[1].InputSlot = 0; polygonLayout[1].AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT; polygonLayout[1].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA; polygonLayout[1].InstanceDataStepRate = 0;
레이아웃의 description이 채워지면 이것의 크기를 가지고 D3D 디바이스를 사용하여 입력 레이아웃을 생성합니다. 레이아웃이 생성되면 정점/픽셀 셰이더 버퍼들은 더 이상 사용되지 않으므로 할당을 해제합니다.
// 레이아웃의 요소 갯수를 가져옵니다. numElements = sizeof(polygonLayout) / sizeof(polygonLayout[0]); // 정점 입력 레이아웃을 생성합니다. result = device->CreateInputLayout(polygonLayout, numElements, vertexShaderBuffer->GetBufferPointer(), vertexShaderBuffer->GetBufferSize(), &m_layout); if(FAILED(result)) { return false; } // 더 이상 사용되지 않는 정점 셰이더 퍼버와 픽셀 셰이더 버퍼를 해제합니다. vertexShaderBuffer->Release(); vertexShaderBuffer = 0; pixelShaderBuffer->Release(); pixelShaderBuffer = 0;
셰이더를 사용하기 위한 마지막 단계는 상수 버퍼(constant buffer)입니다. 정점 셰이더에서 보았던 것처럼 일단 지금은 단 하나의 상수 버퍼를 가지고 있기 때문에 여기서 그것을 세팅하여 셰이더에 대한 인터페이스를 사용할 수 있습니다. 매 프레임마다 상수 버퍼를 업데이트하기 때문에 버퍼의 사용은 동적이 될 필요가 있습니다. 바로 BindFlags로 상수 버퍼를 이 버퍼로 한다는 것을 설정합니다. CPUAccessFlags도 용도에 맞추어야 하기 때문에 D3D11_CPU_ACCESS_WRITE로 설정되어야 합니다. 이 description이 채워지면 상수 버퍼의 인터페이스를 만들고 이와 SetShaderParameters 함수를 이용하여 셰이더의 내부 변수들에 접근할 수 있도록 합니다.
// 정점 셰이더에 있는 행렬 상수 버퍼의 description을 작성합니다. matrixBufferDesc.Usage = D3D11_USAGE_DYNAMIC; matrixBufferDesc.ByteWidth = sizeof(MatrixBufferType); matrixBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; matrixBufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; matrixBufferDesc.MiscFlags = 0; matrixBufferDesc.StructureByteStride = 0; // 상수 버퍼 포인터를 만들어 이 클래스에서 정점 셰이더 상수 버퍼에 접근할 수 있게 합니다. result = device->CreateBuffer(&matrixBufferDesc, NULL, &m_matrixBuffer); if(FAILED(result)) { return false; } return true; }
ShutdownShader 함수에서는 InitializeShader 함수에서 생성했던 4개의 인터페이스들을 정리합니다.
void ColorShaderClass::ShutdownShader() { // 상수 버퍼를 해제합니다. if(m_matrixBuffer) { m_matrixBuffer->Release(); m_matrixBuffer = 0; } // 레이아웃을 해제합니다. if(m_layout) { m_layout->Release(); m_layout = 0; } // 픽셀 셰이더를 해제합니다. if(m_pixelShader) { m_pixelShader->Release(); m_pixelShader = 0; } // 정점 셰이더를 해제합니다. if(m_vertexShader) { m_vertexShader->Release(); m_vertexShader = 0; } return; }
OutputShaderErrorMessage 함수에서는 정점/픽셀 셰이더의 컴파일 도중 오류로 인해 만들어진 에러 메세지를 출력하는 일을 합니다.
void ColorShaderClass::OutputShaderErrorMessage(ID3D10Blob* errorMessage, HWND hwnd, WCHAR* shaderFilename) { char* compileErrors; unsigned long bufferSize, i; ofstream fout; // 에러 메세지를 담고 있는 문자열 버퍼의 포인터를 가져옵니다. compileErrors = (char*)(errorMessage->GetBufferPointer()); // 메세지의 길이를 가져옵니다. bufferSize = errorMessage->GetBufferSize(); // 파일을 열고 안에 메세지를 기록합니다. fout.open("shader-error.txt"); // 에러 메세지를 씁니다. for(i=0; i<bufferSize; i++) { fout << compileErrors[i]; } // 파일을 닫습니다. fout.close(); // 에러 메세지를 반환합니다. errorMessage->Release(); errorMessage = 0; // 컴파일 에러가 있음을 팝업 메세지로 알려줍니다. MessageBox(hwnd, L"Error compiling shader. Check shader-error.txt for message.", shaderFilename, MB_OK); return; }
SetShaderVariables 함수는 셰이더의 전역 변수를 쉽게 다룰 수 있도록 하기 위해 만들어졌습니다. 이 함수에 사용된 행렬들은 GraphicsClass에서 만들어진 것입니다.
bool ColorShaderClass::SetShaderParameters(ID3D11DeviceContext* deviceContext, D3DXMATRIX worldMatrix, D3DXMATRIX viewMatrix, D3DXMATRIX projectionMatrix) { HRESULT result; D3D11_MAPPED_SUBRESOURCE mappedResource; MatrixBufferType* dataPtr; unsigned int bufferNumber; // 행렬을 transpose하여 셰이더에서 사용할 수 있게 합니다. D3DXMatrixTranspose(&worldMatrix, &worldMatrix); D3DXMatrixTranspose(&viewMatrix, &viewMatrix); D3DXMatrixTranspose(&projectionMatrix, &projectionMatrix); // 상수 버퍼의 내용을 쓸 수 있도록 잠급니다. result = deviceContext->Map(m_matrixBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource); if(FAILED(result)) { return false; } // 상수 버퍼의 데이터에 대한 포인터를 가져옵니다. dataPtr = (MatrixBufferType*)mappedResource.pData; // 상수 버퍼에 행렬을 복사합니다. dataPtr->world = worldMatrix; dataPtr->view = viewMatrix; dataPtr->projection = projectionMatrix; // 상수 버퍼의 잠금을 풉니다. deviceContext->Unmap(m_matrixBuffer, 0); // 정점 셰이더에서의 상수 버퍼의 위치를 설정합니다. bufferNumber = 0; // 마지막으로 정점 셰이더의 상수 버퍼를 바뀐 값으로 바꿉니다. deviceContext->VSSetConstantBuffers(bufferNumber, 1, &m_matrixBuffer); return true; }
RenderShader 함수는 Render 함수에서 불리는 두번째 함수입니다. SetShaderParameters 이 함수보다 먼저 호출되어 셰이더의 인자들을 올바로 세팅하게 됩니다.
이 함수에서 가장 먼저 하는 것은 입력 레이아웃을 입력 어셈블러에 연결하는 것입니다. 이로써 GPU 정점 버퍼의 자료구조를 알게 됩니다. 두번째 단계는 정점 버퍼를 그리기 위한 정점 셰이더와 픽셀 셰이더를 설정하는 것입니다. 셰이더가 설정되면 D3D 디바이스 컨텍스트에서 DirectX 11의 DrawIndexed 함수를 사용하여 삼각형을 그려냅니다. 이 함수가 호출된 이후에 초록색 삼각형이 그려집니다.
void ColorShaderClass::RenderShader(ID3D11DeviceContext* deviceContext, int indexCount) { // 정점 입력 레이아웃을 설정합니다. deviceContext->IASetInputLayout(m_layout); // 삼각형을 그릴 정점 셰이더와 픽셀 셰이더를 설정합니다. deviceContext->VSSetShader(m_vertexShader, NULL, 0); deviceContext->PSSetShader(m_pixelShader, NULL, 0); // 삼각형을 그립니다. deviceContext->DrawIndexed(indexCount, 0, 0); return; }
Cameraclass.h
지금까지 우리는 HLSL의 프로그래밍과, 정점/인덱스 버퍼를 셋업하는 방법 그리고 ColorShaderClass를 사용하여 HLSL 셰이더를 호출하는 방법을 살펴보았습니다. 하지만 우리가 한가지 놓친 것이 있는데, 그것은 바로 월드에서 우리가 보는 시점입니다. 이를 구현하기 위해서는 어떻게 우리가 장면을 보는지에 대한 정보를 DirectX 11에게 전달하는 카메라 클래스가 필요합니다. 카메라 클래스는 카메라의 위치와 현재 회전 상태를 계속 가지고 있어야 합니다. 또한 이 정보를 이용하여 렌더링시에 HLSL 셰이더에서 사용할 뷰 행렬을 생성합니다.
//////////////////////////////////////////////////////////////////////////////// // Filename: cameraclass.h //////////////////////////////////////////////////////////////////////////////// #ifndef _CAMERACLASS_H_ #define _CAMERACLASS_H_ ////////////// // INCLUDES // ////////////// #include <d3dx10math.h> //////////////////////////////////////////////////////////////////////////////// // Class name: CameraClass //////////////////////////////////////////////////////////////////////////////// class CameraClass { public: CameraClass(); CameraClass(const CameraClass&); ~CameraClass(); void SetPosition(float, float, float); void SetRotation(float, float, float); D3DXVECTOR3 GetPosition(); D3DXVECTOR3 GetRotation(); void Render(); void GetViewMatrix(D3DXMATRIX&); private: float m_positionX, m_positionY, m_positionZ; float m_rotationX, m_rotationY, m_rotationZ; D3DXMATRIX m_viewMatrix; }; #endif
CameraClass의 헤더는 단지 4개의 함수만을 사용하는 간단한 구조입니다. SetPosition과 SetRotation 함수는 현재 카메라 객체의 위치와 회전 상태를 설정하는 데 사용될 것입니다. Render 함수는 카메라의 위치와 회전 상태에 기반한 뷰 행렬을 생성하는 데 사용됩니다. 그리고 마지막으로 GetViewMatrix 함수는 셰이더에서 렌더링에 사용할 수 있도록 카메라 객체의 뷰 행렬을 받아오는 데 사용합니다.
cameraclass.cpp
//////////////////////////////////////////////////////////////////////////////// // Filename: cameraclass.cpp //////////////////////////////////////////////////////////////////////////////// #include "cameraclass.h"
클래스 생성자에서는 카메라의 위치와 회전 상태를 원점으로 설정합니다.
CameraClass::CameraClass() { m_positionX = 0.0f; m_positionY = 0.0f; m_positionZ = 0.0f; m_rotationX = 0.0f; m_rotationY = 0.0f; m_rotationZ = 0.0f; } CameraClass::CameraClass(const CameraClass& other) { } CameraClass::~CameraClass() { }
SetPosition 함수와 SetRotation 함수는 카메라의 위치와 회전을 정하는 데 사용됩니다.
void CameraClass::SetPosition(float x, float y, float z) { m_positionX = x; m_positionY = y; m_positionZ = z; return; } void CameraClass::SetRotation(float x, float y, float z) { m_rotationX = x; m_rotationY = y; m_rotationZ = z; return; }
GetPosition 함수와 GetRotation 함수는 호출한 함수에게 카메라의 위치와 회전 정보를 돌려줍니다.
D3DXVECTOR3 CameraClass::GetPosition() { return D3DXVECTOR3(m_positionX, m_positionY, m_positionZ); } D3DXVECTOR3 CameraClass::GetRotation() { return D3DXVECTOR3(m_rotationX, m_rotationY, m_rotationZ); }
Render 함수는 카메라의 위치와 회전 정보를 이용하여 뷰 행렬을 갱신합니다. 우선 up 벡터와 위치, 회전 등의 변수를 초기화합니다. 화면의 원점에서 우선 카메라의 x, y, z축의 회전 정도로 카메라의 회전을 수행합니다. 회전이 잘 이루어지면 카메라를 3D 세계의 올바른 위치로 옮겨놓습니다. position과 lookAt 그리고 up의 값이 잘 설정되었다면 D3DXMatrixLookAtLH 함수를 사용하여 현재 카메라의 위치와 회전 정도를 표현하는 뷰 행렬을 만들어냅니다.
void CameraClass::Render() { D3DXVECTOR3 up, position, lookAt; float yaw, pitch, roll; D3DXMATRIX rotationMatrix; // Setup the vector that points upwards. up.x = 0.0f; up.y = 1.0f; up.z = 0.0f; // Setup the position of the camera in the world. position.x = m_positionX; position.y = m_positionY; position.z = m_positionZ; // Setup where the camera is looking by default. lookAt.x = 0.0f; lookAt.y = 0.0f; lookAt.z = 1.0f; // Set the yaw (Y axis), pitch (X axis), and roll (Z axis) rotations in radians. pitch = m_rotationX * 0.0174532925f; yaw = m_rotationY * 0.0174532925f; roll = m_rotationZ * 0.0174532925f; // Create the rotation matrix from the yaw, pitch, and roll values. D3DXMatrixRotationYawPitchRoll(&rotationMatrix, yaw, pitch, roll); // Transform the lookAt and up vector by the rotation matrix so the view is correctly rotated at the origin. D3DXVec3TransformCoord(&lookAt, &lookAt, &rotationMatrix); D3DXVec3TransformCoord(&up, &up, &rotationMatrix); // Translate the rotated camera position to the location of the viewer. lookAt = position + lookAt; // Finally create the view matrix from the three updated vectors. D3DXMatrixLookAtLH(&m_viewMatrix, &position, &lookAt, &up); return; }
뷰 행렬을 생성하는 Render 함수를 수행한 이후로는 GetViewMatrix 함수를 사용하여 뷰 행렬을 얻어올 수 있게 됩니다. 뷰 행렬은 HLSL 정점 셰이더에서 사용하는 중요한 세 행렬 중의 하나입니다.
void CameraClass::GetViewMatrix(D3DXMATRIX& viewMatrix) { viewMatrix = m_viewMatrix; return; }
Graphicsclass.h
GraphicsClass는 이제 세 개의 새로운 클래스가 추가되었습니다. 따라서 CameraClass, ModelClass, ColorShader 클래스의 헤더뿐만 아니라 멤버 변수가 추가됩니다. GraphicsClass가 이 프로젝트에서 사용되는 모든 그래픽 객체에 대한 호출을 담당한다는 점을 기억하시기 바랍니다.
//////////////////////////////////////////////////////////////////////////////// // Filename: graphicsclass.h //////////////////////////////////////////////////////////////////////////////// #ifndef _GRAPHICSCLASS_H_ #define _GRAPHICSCLASS_H_ /////////////////////// // MY CLASS INCLUDES // /////////////////////// #include "d3dclass.h" #include "cameraclass.h" #include "modelclass.h" #include "colorshaderclass.h" ///////////// // GLOBALS // ///////////// const bool FULL_SCREEN = true; const bool VSYNC_ENABLED = true; const float SCREEN_DEPTH = 1000.0f; const float SCREEN_NEAR = 0.1f; //////////////////////////////////////////////////////////////////////////////// // Class name: GraphicsClass //////////////////////////////////////////////////////////////////////////////// class GraphicsClass { public: GraphicsClass(); GraphicsClass(const GraphicsClass&); ~GraphicsClass(); bool Initialize(int, int, HWND); void Shutdown(); bool Frame(); private: bool Render(); private: D3DClass* m_D3D; CameraClass* m_Camera; ModelClass* m_Model; ColorShaderClass* m_ColorShader; }; #endif
Graphicsclass.cpp
//////////////////////////////////////////////////////////////////////////////// // Filename: graphicsclass.cpp //////////////////////////////////////////////////////////////////////////////// #include "graphicsclass.h"
GraphicsClass의 첫번째 변화는 우선 생성자에서 카메라, 모델, 컬러 셰이더 객체들을 null로 초기화하는 것입니다.
GraphicsClass::GraphicsClass() { m_D3D = 0; m_Camera = 0; m_Model = 0; m_ColorShader = 0; }
Initialize 함수 또한 새로운 객체를 초기화하는 부분이 추가되었습니다.
bool GraphicsClass::Initialize(int screenWidth, int screenHeight, HWND hwnd) { bool result; // Create the Direct3D object. m_D3D = new D3DClass; if(!m_D3D) { return false; } // Initialize the Direct3D object. result = m_D3D->Initialize(screenWidth, screenHeight, VSYNC_ENABLED, hwnd, FULL_SCREEN, SCREEN_DEPTH, SCREEN_NEAR); if(!result) { MessageBox(hwnd, L"Could not initialize Direct3D.", L"Error", MB_OK); return false; } // Create the camera object. m_Camera = new CameraClass; if(!m_Camera) { return false; } // Set the initial position of the camera. m_Camera->SetPosition(0.0f, 0.0f, -10.0f); // Create the model object. m_Model = new ModelClass; if(!m_Model) { return false; } // Initialize the model object. result = m_Model->Initialize(m_D3D->GetDevice()); if(!result) { MessageBox(hwnd, L"Could not initialize the model object.", L"Error", MB_OK); return false; } // Create the color shader object. m_ColorShader = new ColorShaderClass; if(!m_ColorShader) { return false; } // Initialize the color shader object. result = m_ColorShader->Initialize(m_D3D->GetDevice(), hwnd); if(!result) { MessageBox(hwnd, L"Could not initialize the color shader object.", L"Error", MB_OK); return false; } return true; }
Shutdown 역시 새로운 객체에 대한 정리 코드가 들어갑니다.
void GraphicsClass::Shutdown() { // Release the color shader object. if(m_ColorShader) { m_ColorShader->Shutdown(); delete m_ColorShader; m_ColorShader = 0; } // Release the model object. if(m_Model) { m_Model->Shutdown(); delete m_Model; m_Model = 0; } // Release the camera object. if(m_Camera) { delete m_Camera; m_Camera = 0; } // Release the D3D object. if(m_D3D) { m_D3D->Shutdown(); delete m_D3D; m_D3D = 0; } return; }
Frame 함수는 이전 튜토리얼과 동일합니다.
bool GraphicsClass::Frame() { bool result; // Render the graphics scene. result = Render(); if(!result) { return false; } return true; }
아마 예상하셨겠지만 Render 함수에 가장 많은 변화가 있었습니다. 검은색이라는 것만 빼면 여전히 화면을 초기화하는 코드로 시작합니다. 우선 Initialize 함수에서 지정한 카메라의 위치를 토대로 뷰 행렬을 만들기 위해 카메라의 Render 함수를 호출합니다. 뷰 행렬이 만들어지면 그것의 복사본을 가져올 수 있습니다. 또한 D3DClass 객체로부터 월드 행렬과 투영 행렬을 복사해 옵니다. 그리고 나서 ModelClass::Render 함수를 호출하여 그래픽 파이프라인에 삼각형 모델을 그리도록 합니다. 이미 준비한 정점들로 셰이더를 호출하여 셰이더는 모델 정보와 정점을 배치시키기 위한 세 행렬을 사용하여 정점들을 그려냅니다. 이제 삼각형이 백버퍼에 그려집니다. 씬 그리기가 완료되었다면 EndScene을 호출하여 화면에 표시하도록 합니다.
bool GraphicsClass::Render() { D3DXMATRIX worldMatrix, viewMatrix, projectionMatrix; bool result; // Clear the buffers to begin the scene. m_D3D->BeginScene(0.0f, 0.0f, 0.0f, 1.0f); // Generate the view matrix based on the camera's position. m_Camera->Render(); // Get the world, view, and projection matrices from the camera and d3d objects. m_Camera->GetViewMatrix(viewMatrix); m_D3D->GetWorldMatrix(worldMatrix); m_D3D->GetProjectionMatrix(projectionMatrix); // Put the model vertex and index buffers on the graphics pipeline to prepare them for drawing. m_Model->Render(m_D3D->GetDeviceContext()); // Render the model using the color shader. result = m_ColorShader->Render(m_D3D->GetDeviceContext(), m_Model->GetIndexCount(), worldMatrix, viewMatrix, projectionMatrix); if(!result) { return false; } // Present the rendered scene to the screen. m_D3D->EndScene(); return true; }
마치면서
여기까지 잘 따라오셨다면 정점 버퍼와 인덱스 버퍼가 어떻게 동작하는지에 대한 기초를 잘 이해할 수 있을 겁니다. 또한 정점/픽셀 셰이더의 기초와 HLSL을 이용하여 이것을 프로그래밍하는 법도 아시게 되었을 겁니다. 마지막으로 화면에 녹색 삼각형 하나를 그리기 위해 위 개념들을 어떻게 활용하는지에 대해서도 배웠습니다.
물론 삼각형 하나를 그리는 코드는 단순히 모든 기능을 main() 함수 하나에 때려박아 간단하게 만들 수도 있습니다. 하지만 이 프레임워크를 사용함으로 앞으로의 튜토리얼들에서 기존 코드에 조금의 수정만으로도 더 복잡한 그래픽 작업을 할 수 있도록 하였습니다.
연습문제
1. 튜토리얼을 컴파일하고 실행해 보십시오. 화면에 초록색 삼각형이 그려지는 것을 확인하십시오. 잘 그려진다면 Esc키를 눌러 종료하십시오.
2. 삼각형의 색깔을 빨간색으로 바꿔 보십시오.
3. 삼각형 대신 사각형이 그려지도록 해 보십시오.
4. 카메라를 10단위만큼 뒤로 물러나게 해 보십시오.
5, 픽셀 셰이더를 수정하여 출력되는 밝기가 절반이 되도록 해 보십시오.
(대박 힌트: ColorPixelShader의 무언가에 0.5f를 곱해 보세요)
소스 코드
Visual Studio 2010 Project: dx11tut04.zip
Source Only: dx11src04.zip
Executable Only: dx11exe04.zip
'강좌번역 > DirectX 11' 카테고리의 다른 글
DirectX11 Tutorial 6 - 조명 (1) | 2013.01.22 |
---|---|
DirectX11 Tutorial 5 - 텍스쳐 (14) | 2013.01.16 |
DirectX11 Tutorial 3 - DirectX 11의 초기화 (20) | 2013.01.05 |
DirectX11 Tutorial 2 - 프레임워크와 윈도우 만들기 (10) | 2013.01.05 |
DirectX11 Tutorial 1 - Visual Studio에서의 DirectX11 설정 (7) | 2013.01.05 |