
Constant Buffer, 즉 상수버퍼는 vertex Shader단계에서 변수를 사용하고 싶을 때 사용된다. 어떨 때 필요한지 생각해보자.
키보드입력에 따라 캐릭터의 위치를 바꿔야하는데 그러면 전시간에 만들었던 CreateGeometry단계에서 기하학적 모습을 담고있는 버텍스 버퍼에 담긴 벡터위치를 매 프레임 변경해야한다.
void Game::CreateGeometry()
{
// VertexData
{
_vertices.resize(4);
// 1 3
// 0 2
_vertices[0].position = Vec3(-0.5f, -0.5f, 0.f);
_vertices[0].uv = Vec2(0.f, 0.5f);
_vertices[1].position = Vec3(-0.5f, 0.5f, 0.f);
_vertices[1].uv = Vec2(0.f, 0.f);
_vertices[2].position = Vec3(0.5f, -0.5f, 0.f);
_vertices[2].uv = Vec2(0.5f, 0.5f);
_vertices[3].position = Vec3(0.5f, 0.5f, 0.f);
_vertices[3].uv = Vec2(0.5f, 0.f);
}
// VertexBuffer
{
D3D11_BUFFER_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.Usage = D3D11_USAGE_IMMUTABLE;
desc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
desc.ByteWidth = (uint32)(sizeof(Vertex) * _vertices.size());
D3D11_SUBRESOURCE_DATA data;
ZeroMemory(&data, sizeof(data));
data.pSysMem = _vertices.data();
HRESULT hr = _device->CreateBuffer(&desc, &data, _vertexBuffer.GetAddressOf());
CHECK(hr);
}
// Index
{
_indices = {0, 1, 2, 2, 1, 3};
}
// IndexBuffer
{
D3D11_BUFFER_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.Usage = D3D11_USAGE_IMMUTABLE;
desc.BindFlags = D3D11_BIND_INDEX_BUFFER;
desc.ByteWidth = (uint32)(sizeof(uint32) * _indices.size());
D3D11_SUBRESOURCE_DATA data;
ZeroMemory(&data, sizeof(data));
data.pSysMem = _indices.data();
HRESULT hr = _device->CreateBuffer(&desc, &data, _indexBuffer.GetAddressOf());
CHECK(hr);
}
}
위에서 버텍스 버퍼를 만들 때 구조체에 desc.Usage = D3D11_USAGE_IMMUTABLE; 이렇게 값을 바꾸지 않는다고 만들었다. 우리는 예쁜 3D모형을 만들어 도형에 대한 정보가 버텍스버퍼에 들어가고 GPU에 들어가게 된다. 이는 좌표에 대한 도형 전체는 고정한다는 뜻이다. 이동이나 스케일 등 변환이 있어도 이 전체 도형 값에다가 변환을 주는것이지 처음 만들어진 도형을 하나하나 바꾸는것은 위험해서 이렇게 만든다.
즉, 처음 GPU에 넘겨주는 값은 오브젝트의 최초의 기하학적인 도형이라고 이해하자. 그 자체는 변하지 않으므로 도형이 원래 무엇인지 알려주는 것이다.
변환은 진짜 버퍼안의 값을 하나하나 바꾸는게 아닌 추가적인 정보를 받아 전체에 적용하는 방식으로 이루어진다. 쉐이더에서 버퍼의 값을 가져와 아웃풋의 값에 연산을 한 값을 넣어 원하는 결과를 나타내다.
이런방식의 장점은 매번 위의 CreateGeometry를 호출해 기하학 좌표를 변화한 다음에 InputAssembler단계에 넣으면 속도가 매우 느리겠지만 버텍스 버퍼를 한 번만 만들고 추가적으로 버텍스 쉐이더 단계에서 세부적인 위치만 조잘하면 실제 바뀌는 것은 기하학적 도형이 아닌채로 버텍스 쉐이더에서 입력받은 정보만 조금 이동시켜 훨씬 빠르고 간편하게 연산을 할 수 있다.
// Default.hlsl
struct VS_INPUT
{
float4 position : POSITION;
//float4 color : COLOR;
float2 uv : TEXCOORD;
};
struct VS_OUTPUT
{
float4 position : SV_POSITION;
//float4 color : COLOR;
float2 uv : TEXCOORD;
};
cbuffer TransformData : register(b0)
{
float4 offset;
}
// IA - VS - RS - PS - OM
VS_OUTPUT VS(VS_INPUT input)
{
VS_OUTPUT output;
output.position = input.position + offset;
output.uv = input.uv;
return output;
}
위와같이 쉐이더를 작성했다고 하자.
VS_INPUT단계에서는 버텍스쉐이더에 담긴 기하학적인 정보이며 VS함수에서 필요한 연산을 해서 VS_OUTPUT에 실어 버텍스쉐이더 연산을 하는 것이다.
상수버퍼라는 의미의 cbuffer를 이동변환 데이터를 받을 TransformData이름으로 만들어 레지스터 b0에 저장하도록 하자. 여기에선 float4타입의 값을 받을 것이다. 여기에 CPU가 입력된 이동을 원하는 정도값을 넣어 넘겨줄 것이다. 그러면 버텍스쉐이더 단계에서 가지고있는 버텍스버퍼의 값에 입력받은 offset(원하는 이동정도)값을 더해서 output으로 넘겨줄 수 있다는 의미로 VS함수를 작성할 수 있다. 그리고 여기서 사용한 input.position, input.uv값은 기하학적인 도형자체의 값이기 때문에 변하는 것이 아니고 cbuffer 상수버퍼가 변하는 값으로 사용된다.
이 상수버퍼를 코드상에서 만들어 값을 전달할 수 있도록 해보자.
Game클래스 내에 CreateConstantBuffer함수로 상수버퍼를 만드는 함수를 만들어보자.
// Game.h
class Game
{
// ...
private:
// SRT
TransformData _transformData;
ComPtr<ID3D11Buffer> _constantBuffer;
}
// Game.cpp
void Game::Init(HWND hwnd)
{
_hwnd = hwnd;
_width = GWinSizeX;
_height = GWinSizeY;
CreateDeviceAndSwapChain();
CreateRenderTargetView();
SetViewport();
CreateGeometry();
CreateVS();
CreateInputLayout();
CreatePS();
CreateRasterizerState();
CreateSamplerState();
CreateBlendState();
CreateSRV(); // 지난시간에 만든 ShaderResourceView
CreateConstantBuffer(); // 잊지말고 Init함수에서도 호출 해주도록한다.
}
void Game::CreateConstantBuffer()
{
D3D11_BUFFER_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.Usage = D3D11_USAGE_DYNAMIC; // CPU_Write + GPU_Read
desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
desc.ByteWidth = sizeof(TransformData);
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
HRESULT hr = _device->CreateBuffer(&desc, nullptr, _constantBuffer.GetAddressOf());
CHECK(hr);
}
// Struct.h
struct TransformData
{
Vec3 offset;
float dummy;
};
위와같이 작성하였는데 인덱스버퍼나 버텍스버퍼를 만들때처럼 D3D11_BUFFER_DESC 타입구조체에 전달할 설명을 만들고 장치로부터 CreateBuffer를 해서 구조체에 담은 내용을, 선언한 ComPtr<ID3D11Buffer> _constantBuffer에 넣는다.
구조체에 담은 설명을 보면 Usage를 전에는 D3D11_USAGE_IMMUTABLE을 썼는데 이번엔 D3D11_USAGE_DYNAMIC으로 cpu에서 쓰고 gpu에서 읽는 방식으로 쓰게했다. BindFlags에는 어떤 용도로 쓸 것인지를 쓰는데 desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;라고 정해주었다. 참고로 인덱스버퍼에서는 D3D11_BIND_INDEX_BUFFER , 버텍스버퍼용을 만들때는 D3D11_BIND_VERTEX_BUFFER 이렇게 써주었다. ByteWidth값을 쓸 때는 얼만큼 쓸지를 지정하는데 위에서 쉐이더에서 쓴 TransformData구조체를 게임 내에서 쓸 수 있도록 만들고 이 TransformData구조체의 크기로 지정해 주었다. TransformData구조체는 버텍스 버퍼가 이동할 위치에 대해서 설명할 것이므로 Vec3타입의 offset을 넣고 Float타입의 dummy를 넣는 이유는 상수버퍼는 만들 때 16바이트 정렬을 해야되기 때문이다.(Vec3는 float3개이므로 12바이트에 더미 float 4 바이트추가) 버텍스버퍼와 인덱스버퍼는 (uint32)(sizeof(uint32) * _indices.size()) 와같은 값을 ByteWidth에 넣어줬는데 _indices와 _vertices는 각각 벡터였다. 마지막으로 상수버퍼는 CPUAccessFlags를 지정해주는데 CPU도 접근을 할 수 있도록 플래그를 설정해주었다.
그리고 중요한 것은 CreateBuffer를 할 때 두번째 인자이다. 두번째 인자는 만든 버퍼의 초기값을 어떻게 설정할지를 전달한다. 또 인덱스버퍼와 버텍스버퍼를 만들때와 비교하면
// 인덱스 버퍼에 넣을 초기값
D3D11_SUBRESOURCE_DATA data;
ZeroMemory(&data, sizeof(data));
data.pSysMem = _indices.data(); // _indices는 배열로 지정해준 값이다.
HRESULT hr = _device->CreateBuffer(&desc, &data, _indexBuffer.GetAddressOf());
// 버텍스버퍼에 넣을 초기값
D3D11_SUBRESOURCE_DATA data;
ZeroMemory(&data, sizeof(data));
data.pSysMem = _vertices.data(); // _vertices도 배열로 지정해준 값이다.
HRESULT hr = _device->CreateBuffer(&desc, &data, _vertexBuffer.GetAddressOf());
이렇게 data구조체에 담아서 각 버퍼들을 어떻게 초기화할 것인지 넘겨줬었는데 상수버퍼는 nullptr을 전달한다. 인덱스와 버텍스버퍼들은 한 번 초기화하고 항상 그 값을 쓰지만 상수버퍼는 업데이트를 할 때 매 프레임마다 다른 값이 들어가게 될것이라는걸 잊지말자..!
이렇게 _constantBuffer를 만들었는데 특이하게 CPU가 write할 수 있고 GPU가 read할 수 있는 버퍼로 어떻게 CPU에서 값을 쓸 것인지 보자. 그래도 바로 이 버퍼에 값을 쓸 수 있는것은 아니고 나름의 방식이 있는데 _constantBuffer를 만들 때 같이 만든 TransformData타입의 _transformData가 사실상 캐릭터의 회전값이나,스케일, 이동변환된 값을 월드기준의 위치를 넣어주게된다. (이는 후에 SRT변환에서 더 깊이 알아보고 현재는 offset이라는 값으로 사용하자. ) 현재는 _transformData에 들어간 값을 어떻게 상수버퍼에 복사할지를 살펴보면 우선 _deviceContext에는 map과 unmap이라는 함수가 있는데 이를 이용한다.
이제부터 Game클래스의 Update를 _tranformData에 들어간 변환된 값을 상수버퍼에 복사하는 방법을 이용해 만들어보자.
void Game::Update()
{
// Scale Rotation Translation
//_transformData.offset.x += 0.003f;
//_transformData.offset.y += 0.003f;
D3D11_MAPPED_SUBRESOURCE subResource;
ZeroMemory(&subResource, sizeof(subResource));
_deviceContext->Map(_constantBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &subResource);
::memcpy(subResource.pData, &_transformData, sizeof(_transformData));
_deviceContext->Unmap(_constantBuffer.Get(), 0);
}
_deviceContext->Map()함수로 _constantBuffer.Get()으로 주소를 넘겨줘 데이터를 넣어줄 준비를 하고 memcpy로 데이터를 넣고 Unmap으로 뚜껑을 덮어주는 방식(?) 으로 만들었다.
좀 더 자세히 살펴보면 Map함수의 인자에 subResources를 넣어줘야하므로 만든다음에 뚜껑을 열고 연결을 한 다음에 memcpy에서 subResources.pData를에 값을 넣어주고 Unmap을 하면 pData의 값이 연결된 채로 닫히게 되는 것이다. 즉, CPU에서 적은 값이 연결역할인 subResources를 통해 GPU의 _constantBuffer값에 들어가도록 해준 것이고 memcpy할 떄 &_transformData를 하면 subResources.pData에 _transformData에 있던 값이 복사가 된다.
그리고 ID3D11타입의 constantBuffer를 만들었으므로 Render()함수의 버텍스 쉐이더 단계에서 이 상수 버퍼 를 만든것을 등록해줘야하는데
void Game::Render()
{
RenderBegin();
{
uint32 stride = sizeof(Vertex);
uint32 offset = 0;
// IA
_deviceContext->IASetVertexBuffers(0, 1, _vertexBuffer.GetAddressOf(), &stride, &offset);
_deviceContext->IASetIndexBuffer(_indexBuffer.Get(), DXGI_FORMAT_R32_UINT, 0);
_deviceContext->IASetInputLayout(_inputLayout.Get());
_deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
// VS
_deviceContext->VSSetShader(_vertexShader.Get(), nullptr, 0);
_deviceContext->VSSetConstantBuffers(0, 1, _constantBuffer.GetAddressOf());
// RS
_deviceContext->RSSetState(_rasterizerState.Get());
// PS
_deviceContext->PSSetShader(_pixelShader.Get(), nullptr, 0);
_deviceContext->PSSetShaderResources(0, 1, _shaderResourveView.GetAddressOf());
_deviceContext->PSSetShaderResources(1, 1, _shaderResourveView2.GetAddressOf());
_deviceContext->PSSetSamplers(0, 1, _samplerState.GetAddressOf());
// OM
_deviceContext->OMSetBlendState(_blendState.Get(), nullptr, 0xFFFFFFFF);
//_deviceContext->Draw(_vertices.size(), 0);
_deviceContext->DrawIndexed(_indices.size(), 0, 0);
}
RenderEnd();
}
위와 같이 _deiviceContext->VSSetConstantBuffer를 이용해 0번 슬롯(b1)을 사용할 것이고 한 개반 등록한다고 알려주어 만든다. 정리를 하면 이 _constantBuffer는 GPU쪽에 있는 버퍼는 맞기는 하나 Update()에서 본듯이 CPU에서 GPU로 데이터를 밀어넣어주게 되고 다음에 VSSetConstantBuffer를 통해 렌더링 파이프라인에 묶어준 상태이기때문에 쉐이더에서도 사용할 수 있는 상태가 되는 것이다. 그리고 Update()에서 주석처리되어있는 _transformData의 offset값을 바꾸는 걸 활성화시키고 실행하면 캐릭터의 위치가 움직이는것을 확인하게 된다. 이런식으로 매 프레임마다 offset의 값을 조금씩 바꾸면 이동하는 것처럼 보인다.
다시 쉐이더의 소스를 보면 VS_INPUT의 float4타입의 position 은 기하학적인 오브젝트의 고유도형을 의미하는것으로 절대 직접 수정하면 안되고 놔 둔 상태에서 VS함수에서 추가적인 정보를 넣어 연상을 통해 물체를 이동시키는게 핵심이다. 그리고 그 사이에 상수버퍼는 이 버텍스쉐이더 단계에서 변수를 건네듯이 전달을 하는 역할이다. 원래 고유도형에 움직일 때마다 버텍스자체를 다시 계산한다고하면 모델링을 통해 이미 나온 30만개짜리 삼각형으로 이루어진 메쉬를 대상으로 다 다시 연산해서 GPU에 넘겨주려면 처음에 만들었던 버텍스 버퍼를 다시 만들어야하므로 엄청난 무리가 된다. 공룡도형은 동룡도형을 유지해야지 좀비도 될수는 없다.. 그래서 버텍스버퍼는 리드온리로 최초에 한번 CPU에서 GPU로 복사한 값을 재사용하는것이고, 그 후에는 상수버퍼에다가 바꾸고싶은 인자들만 넘기고 연산은 쉐이더에 함수를 바꿔서 이루어지는 것이다.
상용엔진 유니티나 언리얼에서도 머티리얼이라는 재질에 대해 옵션은 설정할 수 있다. 그런 값들이 상수버퍼에 들어간다. 이번시간에 배운 상수버퍼를 이용해 다음엔 offset값을 직접 넣는게 아니라 행렬값이나 수학공식을 이용해 이동 외에도 회전, 스케일 등을 넣어 변환하는 것을 살펴보자.
*현재까지 인덱스버퍼, 버텍스버퍼, 상수버퍼 그리고 렌더타겟 등을 살펴봤다.
'[DirectX11]' 카테고리의 다른 글
| Light #2 - Diffuse (0) | 2024.08.30 |
|---|---|
| Light #1 - Ambient (0) | 2024.08.27 |
| 3D - Geometry, Sampling (0) | 2024.08.07 |
| 3D - 프로젝트 설정 (0) | 2024.08.03 |
| 기본 프레임워크 만들기, 외부 라이브러리 추가 방법( feat. DirectXTex) (0) | 2023.11.05 |