구성요소
물체를 그리기 위해서는 필요한 것은 vertices
, indices
, normals
라는 기본적인 3개의 구성요소이다.
해당 요소를 쉽게 정리하기 위해서 MeshData
, Mesh
라는 클래스를 정의하였다.
Mesh
해당 클래스의 경우에는 물체를 그리기 위해 필요한 buffer들을 담아두는 역할을 한다. 우리가 만드는 도형에다가 텍스쳐 이미지를 사용할 수 있으므로, 이에 대비해 texture2D와 SRV를 정의한다.
struct Mesh {
// Mesh Constant
// uint16_t Material Constant (materialCBV)
// PSO
ComPtr<ID3D11Buffer> vertexBuffer;
ComPtr<ID3D11Buffer> indexBuffer;
ComPtr<ID3D11Buffer> vertexConstBuffer;
ComPtr<ID3D11Buffer> pixelConstBuffer;
ComPtr<ID3D11Texture2D> albedoTexture;
ComPtr<ID3D11Texture2D> emissiveTexture; // add emissive texture (make a light)
ComPtr<ID3D11Texture2D> normalTexture;
ComPtr<ID3D11Texture2D> heightTexture;
ComPtr<ID3D11Texture2D> aoTexture;
ComPtr<ID3D11Texture2D> metallicRoughnessTexture;
ComPtr<ID3D11ShaderResourceView> albedoSRV;
ComPtr<ID3D11ShaderResourceView> emissiveSRV;
ComPtr<ID3D11ShaderResourceView> normalSRV;
ComPtr<ID3D11ShaderResourceView> heightSRV;
ComPtr<ID3D11ShaderResourceView> aoSRV;
ComPtr<ID3D11ShaderResourceView> metallicRoughnessSRV;
UINT indexCount = 0; // Number of indiecs = 3 * number of triangles
UINT vertexCount = 0;
UINT stride = 0;
UINT offset = 0;
};
MeshData
MeshData
가 실질적으로 정의하는데 있어서 중요한 역할을 하는 클래스이다. Mesh
클래스의 경우, CPU와 GPU 간 소통을 위한 클래스라고 볼 수 있다.
vertices
와 indices
에다가 사용자가 정의한 vertex와 index에 따라서 그리고자 하는 물체의 형태가 달라진다.
이외에 std::string
으로 정의된 변수들은 텍스쳐 파일의 주소를 입력받아 연결하는 역할을 한다.
struct MeshData {
std::vector<Vertex> vertices;
std::vector<uint32_t> indices;
std::string albedoTextureFilename;
std::string emissiveTextureFilename;
std::string normalTextureFilename;
std::string heightTextureFilename;
std::string aoTextureFilename; // Ambient Occlusion
std::string metallicTextureFilename;
std::string roughnessTextureFilename;
};
여기서 중요한 부분이 있다. Vertex
라는 클래스이다. 우리는 그림을 그리기 위해서는 그림을 그릴 때의 꼭짓점도 중요하지만, 그림의 색이 어느 방향을 향해있을지도 중요한 포인트이다. 이외에도 꼭짓점에 대해서 여러 정보를 담을 클래스가 필요한데, 그것이 Vertex
클래스이다.
Vertex
물체를 그리는 점의 기본적인 요소들이 모두 포함되어 있다.
using DirectX::SimpleMath::Vector2;
using DirectX::SimpleMath::Vector3;
struct Vertex {
Vector3 position;
Vector3 normalModel;
Vector2 texcoord;
Vector3 tangentModel;
};
GPU를 통해 그리기 위한 사전 작업
물체를 그리는 꼭짓점의 정보들(position, normal, texcoord, tagent)과 indices를 모두 입력했다고 가정하자.
그렇다면, 해당 정보들을 어떻게 저장하고 GPU와 연결해야 할까?
우리는 vertex buffer와 같은 buffer에다가 저장해서 이용해야한다. 자원은 GPU가 사용하고 조작하도록 마려된 메모리 블록
일 뿐이다.
각 면마다 Vertex
클래스를 만들어서 할당하였고 이를 이용해서 각 면마다 vertex buffer
와 index buffer
를 만들어 주어야 한다. 아까 설명한 클래스 중 Mesh
가 있다. Mesh
를 통해서 해당 버퍼들의 정보를 모두 담아야 한다.
for (const auto &meshData : meshes) {
auto newMesh = std::make_shared<Mesh>();
D3D11Utils::CreateVertexBuffer(device, meshData.vertices,
newMesh->vertexBuffer);
newMesh->indexCount = UINT(meshData.indices.size());
std::cout << newMesh->indexCount << std::endl;
newMesh->vertexCount = UINT(meshData.vertices.size());
newMesh->stride = UINT(sizeof(Vertex));
D3D11Utils::CreateIndexBuffer(device, meshData.indices,
newMesh->indexBuffer);
if (!meshData.albedoTextureFilename.empty()) {
D3D11Utils::CreateTexture(
device, context, meshData.albedoTextureFilename, true,
newMesh->albedoTexture, newMesh->albedoSRV);
m_materialConstsCPU.useAlbedoMap = true;
}
...
newMesh->vertexConstBuffer = m_meshConstsGPU;
newMesh->pixelConstBuffer = m_materialConstsGPU;
this->m_meshes.push_back(newMesh);
}
CreateVertexBuffer 및 CreateIndexBuffer 함수
// desc -> subresource_data -> create
template <typename T_VERTEX>
static void CreateVertexBuffer(ComPtr<ID3D11Device> &device,
const vector<T_VERTEX> &vertices,
ComPtr<ID3D11Buffer> &vertexBuffer) {
D3D11_BUFFER_DESC bufferDesc;
ZeroMemory(&bufferDesc, sizeof(bufferDesc));
bufferDesc.Usage = D3D11_USAGE_IMMUTABLE;
bufferDesc.ByteWidth = UINT(sizeof(T_VERTEX) * vertices.size());
bufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
bufferDesc.CPUAccessFlags = 0; // 0 is no CPU access is necessary.
bufferDesc.StructureByteStride = sizeof(T_VERTEX);
// initialize in MS Sample
D3D11_SUBRESOURCE_DATA vertexBufferData = {0};
vertexBufferData.pSysMem = vertices.data();
vertexBufferData.SysMemPitch = 0;
vertexBufferData.SysMemSlicePitch = 0;
ThrowIfFailed(device->CreateBuffer(&bufferDesc, &vertexBufferData,
vertexBuffer.GetAddressOf()));
}
void D3D11Utils::CreateIndexBuffer(ComPtr<ID3D11Device> &device,
const std::vector<uint32_t> &indices,
ComPtr<ID3D11Buffer> &indexBuffer) {
D3D11_BUFFER_DESC bufferDesc = {};
bufferDesc.Usage =
D3D11_USAGE_IMMUTABLE; // After init, never change. only GPU read this.
bufferDesc.ByteWidth = UINT(sizeof(uint32_t) * indices.size());
bufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER;
bufferDesc.CPUAccessFlags = 0; // 0 if no CPU access is necessary.
bufferDesc.StructureByteStride = sizeof(uint32_t);
// initialize in MS Sample
D3D11_SUBRESOURCE_DATA indexBufferData = {0};
indexBufferData.pSysMem = indices.data();
indexBufferData.SysMemPitch = 0;
indexBufferData.SysMemSlicePitch = 0;
device->CreateBuffer(&bufferDesc, &indexBufferData,
indexBuffer.GetAddressOf());
}
MeshData
클래스에 있는 여러 면들의 대한 정보들을 for-each문을 통해서 각 정보들을 도출해서 buffer들을 만드는 과정이다. vector를 통해서 받는 이유는 해당 내장 클래스의 여러 유용한 메서드를 활용하기 위해서이다.
최종적으로 vertex와 index buffer 모두, device->CreateBuffer(&bufferDesc, &vertexBufferData, vertexBuffer.GetAddressOf())
함수를 통해서 buffer를 만들고 ID3D11Buffer
클래스에 바인딩하는 것을 볼 수 있다.
그리고 마지막 줄 부근에 적혀 있는 ConstBuffer
가 있다. Mesh
클래스의 Constbuffer 변수의 주소에다가 사용자가 정의한 ConstBuffer를 연결하는 과정이다.
해당 ConstBuffer는 향후 쉐이더에서 물체를 그리기 위해 필요한 변수들을 정의한 것이다. CreateBuffer
함수를 통해서 만드는 것은 똑같지만, desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
와 같이 세부적인 desc가 다르다.
Constant Buffer
셰이더 프로그램에 Constnat 정보를 제공하는 데 쓰인다. 이러한 특징을 사용해 CPU 쪽 응용 프로그램에서 각 프로그램 가능 셰이더 단계들 각각에 자료를 공급하는 주된 수단이다.
상수 버퍼는 다른 버퍼들과 달리 배열이 아니라 구조체라고 할 수 있다.
셰이더 프로그램이 상수 버퍼를 읽기 전용으로만 사용
한다는 점을 이용해서도 상수 버퍼 갱신을 줄일 수 있다. 또한, 읽기 전용
으로 사용 시 파이프라인의 여러 지점들에 동시에 연결해도 메모리 접근 충돌이 생기지 않는다.
template <typename T_CONSTANT>
static void CreateConstBuffer(ComPtr<ID3D11Device> &device,
const T_CONSTANT &constantBufferData,
ComPtr<ID3D11Buffer> &constantBuffer) {
static_assert((sizeof(T_CONSTANT) % 16) == 0,
"Constant Buffer size must be 16-byte aligned");
D3D11_BUFFER_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.ByteWidth = sizeof(constantBufferData);
desc.Usage = D3D11_USAGE_DYNAMIC; //GPU read Only CPU write Only
desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
desc.MiscFlags = 0;
desc.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA initData;
ZeroMemory(&initData, sizeof(initData));
initData.pSysMem = &constantBufferData;
initData.SysMemPitch = 0;
initData.SysMemSlicePitch = 0;
ThrowIfFailed(device->CreateBuffer(&desc, &initData,
constantBuffer.GetAddressOf()));
}
동적인 상수 버퍼를 생성할 때에는 D3D11_USAGE_DYNAMIC
을 지정한다. 또한, 실행시점에서 CPU가 자원을 갱신할 수 있도록 CPU 접근 플래그 D3D11_CPU_ACCESS_WRITE
도 지정한다.
정적 상수 버퍼의 경우 D3D11_USAGE_IMMUTABLE
용도 플래그를 지정하고 CPU 접근은 전혀 없는 것(READ)
으로 설정한다.
예외적으로, GPU만이 상수 버퍼를 갱신하게 할 수도 있다. 추가/소비 버퍼의 원소 개수를ID3D11DeviceContext::CopyStructureCount()
메서드를 이용해서 상수 버퍼에 기록하는 등이 그러한 GPU 갱신의 예이다. 이러한 경우에는 DEFAULT
용도 플래그를 지정하고 CPU 접근은 전혀 없는 것(READ)
로 두면 된다.
- 상수 버퍼의 내용은 CPP 구조체를 정의해서 사용된다. 그러면 응용 프로그램은 주 메모리 안에서 그 구조체 인스턴스를 갱신하고, 그것을 그대로 버퍼에 복사 할 수 있다.
- 상수 버퍼의 크기(ByteWidth)는 반드시 16Byte의 배수이어야 한다. 오직 상수 버퍼에만 적용된다.
- 상수 버퍼 서술 구조체(
D3D11_BUFFER_DESC
)의 연결 플래그에는 오직D3D11_BIND_CONSTANT_BUFFER
만을 지정할 수 있다.
상수 버퍼는 프로그램 가능 셰이더 단계들에 연결하고 HLSL에 접근하는 버퍼이지만, 그 내용을 자원 뷰가 다르게 해석해야 할 여지는 없다. 따라서, 그대로 노출되는 상수버퍼는 자원 뷰를 사용할 필요가 없다.
HLSL 안에서 상수 버퍼는 cbuffer
라는 키워드를 이용해서 선언하는 구조체에 대응된다.
Render 과정
이렇게, vertex
, index
, const
buffer들을 모두 준비했다. vertex와 index의 할 일은 이제 없다. const buffer를 통해서 여러 값들을 최신화하면서 그리는 일만 남았다.
물체에 대한 정보에 대한 것들을 끝냈다면, 이제 남은 것은 '내 시점'으로 보는 것이다. 내 시점으로 보기 위해서는 내 시점과 똑같은 '카메라'가 필요하다.
Camera
class Camera {
public:
Camera() { UpdateViewDir(); }
Matrix GetViewRow();
Matrix GetProjRow();
Vector3 GetEyePos();
void UpdateViewDir();
void UpdateKeyboard(const float dt, bool const keyPressed[256]);
void UpdateMouse(float mouseNdcX, float mouseNdcY);
void MoveForward(float dt);
void MoveRight(float dt);
void MoveUp(float dt);
void SetAspectRatio(float aspect);
public:
bool m_useFirstPersonView = false;
private:
/*Vector3 m_position = Vector3(-1.3469f, 0.461257f, -0.408304f);*/
Vector3 m_position = Vector3(0.0f, 1.0f, -1.0f);
Vector3 m_viewDir = Vector3(0.0f, 0.0f, 1.0f);
Vector3 m_upDir = Vector3(0.0f, 1.0f, 0.0f); // 이번 예제에서는 고정
Vector3 m_rightDir = Vector3(1.0f, 0.0f, 0.0f);
// roll, pitch, yaw
// https://en.wikipedia.org/wiki/Aircraft_principal_axes
float m_yaw = 0.441786f, m_pitch = -0.449422f;
float m_speed = 5.0f; // 움직이는 속도
// 프로젝션 옵션도 카메라 클래스로 이동
float m_projFovAngleY = 90.0f;
float m_nearZ = 0.05f;
float m_farZ = 50.0f;
float m_aspect = 16.0f / 9.0f;
bool m_usePerspectiveProjection = true;
};
카메라 클래스를 만드는 이유는 시점에 대한 정보를 함수를 통해서 쉽게 얻기 위함이다. 물체를 그릴 때 '어떻게' 보여야 하는지 정의하기 위해서는 시점의 정보가 필요하기 때문이다.
카메라 클래스를 통해 view
, projection
시점에 대한 값들을 도출하고 해당 정보를 const buffer를 통해 쉐이더에 바인딩함으로써 물체를 그릴 수 있다.
각 면에 대한 정보를 Mesh
클래스가 들고 있고 면에 대한 배열을 vector
로 저장하였다. 이 모든 면들을 그려야하므로, for-each문을 통해서 각 면에 대한 정보를 쉐이더에 바인딩하면서 물체를 그리게 된다.
for (const auto &mesh : m_meshes) {
context->VSSetConstantBuffers(
0, 1, mesh->vertexConstBuffer.GetAddressOf());
context->PSSetConstantBuffers(
0, 1, mesh->pixelConstBuffer.GetAddressOf());
context->VSSetShaderResources(0, 1, mesh->heightSRV.GetAddressOf());
// 물체 렌더링할 때 여러가지 텍스춰 사용 (t0 부터시작)
vector<ID3D11ShaderResourceView *> resViews = {
mesh->albedoSRV.Get(), mesh->normalSRV.Get(), mesh->aoSRV.Get(),
mesh->metallicRoughnessSRV.Get(), mesh->emissiveSRV.Get()};
context->PSSetShaderResources(0, UINT(resViews.size()),
resViews.data());
context->IASetVertexBuffers(0, 1, mesh->vertexBuffer.GetAddressOf(),
&mesh->stride, &mesh->offset);
context->IASetIndexBuffer(mesh->indexBuffer.Get(),
DXGI_FORMAT_R32_UINT, 0);
context->DrawIndexed(mesh->indexCount, 0, 0);
// std::cout << mesh->indexCount << std::endl;
}
위 코드에는 안 적혀 있지만, 그리기 전에 OM(output merger)에 대한 RTV 초기화, DSV 초기화와 같은 기초 설정들을 끝마쳐줘야한다.
m_context->ClearDepthStencilView(m_depthStencilView.Get(),
D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL,
1.0f, 0);
m_context->OMSetRenderTargets(UINT(RTVs.size()), RTVs.data(),
m_depthStencilView.Get());
RTV를 설정하게 되는데, 기본적으로 RTV는 Texture2D
버퍼와 바인딩 되어있는 자원이다. backbuffer는 Texture2D형태를 가지고 있는 것이다.
backbuffer가 아닌 Texture2D 버퍼에다가 그린다. 그다음 backbufferRTV를 설정해주고 이전에 그린 렌더링 화면SRV로 PS에 바인딩 해주게 되면, 렌더링이 끝난 화면(2D형태)에다가 여러가지 효과들을 적용할 수 있다.
ComPtr<ID3D11Texture2D> backBuffer;
D3D11_TEXTURE2D_DESC desc;
backBuffer->GetDesc(&desc);
desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
desc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;
desc.Usage = D3D11_USAGE_DEFAULT; // It can copy from staging texture.
쉐이더에서의 변환 과정
물체를 쉐이더로 넘기는 것까지의 모든 과정이 끝났다. 그렇다면, 쉐이더에서 받은 물체의 정보를 Clip 행렬로써 만들어주어야 한다.
DirectX에서는 WVP
라고 하며, OpenGL에서는 MVP
라고 한다.
- World -> View -> Projection
순서로 변환을 진행한다. 해당 변환을 진행하기 위한 행렬들은 ConstBuffer를 통해서 전달받아서 사용한다.
내 코드는 World 좌표는 각 점에 따라서 변동되는 요소이므로, Mesh 클래스 내에 Constbuffer 변수를 통해 관리하였다.
이외에 시점을 통해 관리를 해야하는 view, proj 같은 경우에는 따로 ConstBuffer를 만들어서 관리해주었다.
// in Vertex Shader
float4 pos = float4(input.posModel, 1.0f);
pos = mul(pos, world);
pos = mul(pos, viewProj);
output.posProj = pos;
NDC 화면으로는 어떻게 변환되는가?
Clip -> NDC로 변환되는 과정은 DirectX 내부적으로 수행된다고 한다.
변환 자체도 Clip의 x, y, z 좌표에다가 w를 나누어주게 되면 NDC좌표가 도출된다.
'Graphics' 카테고리의 다른 글
6. Deferred Lighting (0) | 2024.07.01 |
---|---|
5. Normal Mapping (0) | 2024.06.27 |
4. Point Shadow Mapping (1) | 2024.06.24 |
3. Directional Shadow Mapping (0) | 2024.06.21 |
2. Depth Buffer와 안개 효과 (1) | 2024.06.14 |