GPU 입장에서 보면, '상수 버퍼'란 특정 셰이더 단계에서 읽기 전용(Read Only)으로 사용되는 데이터를 담고 있는 버퍼 이다. 즉, 셰이더가 실행되는 동안에는 그 값이 변하지 않는다는 의미에서 상수(const) 라고 부르는 것이다.
CPU 입장에서 상수버퍼는 프레임마다 갱신할 수도 있고 특정 상황에서만 갱신할 수도 있다. CPU 는 언제든지 상수 버퍼 데이터를 수정할 수 있지만, 중요한 점은 GPU 가 접근하는 동안에는 변경하면 안된다는 것이다. 즉, "프레임마다 갱신하는 상수버퍼" 는 CPU 입장에서 보면 그냥 일반적인 변수처럼 변하지만, GPU 가 해당 데이터를 사용할 때는 그 순간 변하지 않으므로 "상수" 라고 취급된다고 이해할 수 있다.
즉, "셰이더가 참조하는 동안 변하지 않는 데이터" 라는 의미이며, 이것이 c++ 에서 말하는 const 키워드처럼 절대 변하지 않는다 는 의미는 아니다.
상수 버퍼(constant buffer) 는 셰이더 프로그램에서 참조하는 자료를 담는 GPU 자원(ID3D12Resource) 의 예이다. 앞에서 말했듯이 텍스처나 기타 버퍼 자원 역시 셰이더 프로그램에서 참조 할 수 있다. 셰이더코드에 다음과 같은 코드가 있다면
cbuffer cbPerObject : register(b0)
{
float4x4 gWorldViewProj;
}
이 코드는 cbPerObject 라는 cbuffer 객체(상수 버퍼) 를 참조한다. 이 예에서 상수버퍼는 gWorldViewProj 라는 4 × 4 행렬 하나만 저장한다. 이 행렬은 한 점을 LocalSpace 에서 Homogeneous clipping Space(동차절단공간) 으로 변환하는데 쓰이는 세계행렬, 시야행렬, 투영행렬을 하나로 결합한 것이다. HLSL 에서 4 × 4 행렬은 내장 형식 float4x4 로 대표된다. 그 외에도 여러 행렬 형식이 있는데, 예를 들어 3 × 4 행렬은 float3x4 2 × 2 행렬은 float2x2 등이 있다. 정점 버퍼나 색인 버퍼와는 달리 상수 버퍼는 CPU 가 프레임당 한번 갱신하는 것이 일반적이다. 예를 들어 카메라가 매 프레임 이동한다면, 프레임마다 상수 버퍼를 새 시야 행렬로 갱신해야 할 것이다. 따라서 상수 버퍼는 기본힙이 아니라 업로드 힙에 만들어야 한다 그래야 CPU 가 버퍼의 내용을 갱신할 수 있다. 다른 버퍼들은 GPU 가 관리할 수 있도록 Default Heap 에 만든다는 점 VB, IB 는 Default Heap RTB, DSB 도 Default Heap 또한, 상수 버퍼에는 특별한 하드웨어 요구조건이 있다. 바로, 크기가 반드시 최소 하드웨어 할당 크기(256byte)의 배수 이어야 한다는 것이다. 같은 종류의 상수 버퍼를 여러 개 사용하는 경우가 많다. 예를 들어 위의 상수버퍼 cbPerObjecct 는 물체마다 달라지는 상수들을 담으므로, 만일 장면의 물체가 n 개이면 이 종류의 상수버퍼가 n 개 필요하다. 다음 코드는 NumElements 개의 상수 버퍼 객체를 담는 하나의 버퍼를 생성하는 방법을 보여준다.
struct ObjectConstants
{
DirectX::XMFLOAT4x4 WorldViewProj = MathHelper::Identity4x4(); // 4차원 단위행렬
}
/* 버퍼의 크기(byte 개수) 를 최소 하드웨어 할당 크기(256byte) 의 배수가 되게 하는 연산 */
UINT CalcConstantBufferByteSize(UINT byteSize)
{
/*
상수 버퍼의 크기는 반드시 최소 하드웨어 할당 크기 (흔히 256 byte) 의 배수이어야 한다.
이 메서드는 주어진 크기에 가장 가까운
256 의 배수를 구해서 돌려준다.
이를 위해 이 메서드는 크기에 255 를 더하고
비트마스크를 이용해서 하위 2 바이트, 즉 256 보다 작은 모든 비트를 0으로 만든다.
예 : byteSize = 300 이라 할 때
(300 + 255) & ~255
555 & ~255
0x022B & ~ 0x00ff
0x022B & 0xff00
0x0200
512
*/
return (byteSize + 255 ) & ~ 255;
}
UINT elementByteSize = CalcConstantBufferByteSize(sizeof(ObjectConstants));
ComPtr<ID3D12Resource> mUploadCBuffer;
devide->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(mElementByteSize*NumElements),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&mUploadCBuffer)
);
mUploadCBuffer 를, ObjectConstants 형식의 상수 버퍼들의 배열을 담는 (256byte 의 배수가 되게 하는 채움 byte 들과 함께) 버퍼라고 간주할 수 있다. 어떤 물체를 그릴 때가 되면, 이 버퍼에서 해당 물체를 위한 상수들이 있는 부분 영역을 서술하는 상수 버퍼 뷰를 파이프 라인에 묶는다.
상수 자료를 256 배수 크기로 할당하지만, HLSL 구조체에서 해당 상수 자료에 여분의 바이트들을 명시적으로 채울 필요는 없다. 채움은 암묵적으로 일어난다. 애초에 모든 상수 버퍼 구조체를 256 byte 의 배수가 되도록 정의하면 (적절한 채움 필드들을 이용해서) 상수 버퍼 원소들을 일일이 256 byte 의 배수로 만드는 번거로움을 피할 수 있다.
// 256 byte 경계에 맞게 바이트들이 암묵적으로 채워진다.
cbuffer cbPerObject : register(b0)
{
float4x4 gWorldViewProj;
};
// 256 바이트 경계에 맞게 명시적으로 바이트들을 채운다.
cbuffer cbPerObject : register(b0)
{
float4x4 gWorldViewProj;
float4x4 Pad0;
float4x4 Pad1;
float4x4 Pad1;
}
Direct3D 12 는 셰이더 모형(shader model) 5.1 을 도입했다. 셰이더 모형 5.1 은 상수 버퍼를 정의하는 또 다른 HLSL 문법을 지원한다. 다음이 그러한 예이다.
struct ObjectConstants
{
flaot4x4 gWorldViewProj;
uint matIndex;
};
ConstantBuffer<ObjectConstant> gObjConstants : register(b0);
uint index = gObjConstants.matIndex;
이 방식에서는 상수 버퍼에 담을 자료의 자료의 형식을 개별적인 구조체로 정의하고, 그 구조체를 이용해서 상수 버퍼를 정의한다. 또한 구조체와 같이 자료 멤버 구문을 이용해서 상수 버퍼의 필드들에 접근한다.
앞에서 상수 버퍼를 업로드 힙, 즉 D3D12_HEAP_TYPE_UPLOAD 형식의 힙에 생성했으므로, CPU 에서 상수 버퍼 자원에 자료를 올릴 수 있다. 자료를 올릴려면 먼저 자원 자료를 가리키는 포인터를 얻어야 하는데, 그러러면, 다음과 같이 Map 메서드를 호출해야 한다.
ComPtr<ID3D12Resource> mUploadBuffer;
BYTE* mMappedData = nullptr;
mUploadBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mMappedData));
이 메서드의 첫 매개변수는 CPU 에 대응(mapping; 사상) 시키려는 부분 자원(Subresouce)의 색인이다. 버퍼의 경우에는 버퍼 자체가 유일한 부분 자원이므로 그냥 0 을 지정하면된다. 둘째 매개변수는 대응 시킬 메모리의 범위를 서술하는 D3D12_RANGE 구조체의 포인터인데, 자원 전체를 대응시키려면 지금처럼 nullptr 을 지정하면 된다. 출력 매개변수인 셋째 매개변수에는 대응된 자료를 가리키는 포인터가 설정된다. 시스템 메모리에 있는 자료 상수 버퍼에 복사하려면 다음처럼 memcpy 를 이용한다.
memcpy(mMappedData, &data, dataSizeInBytes);
상수 버퍼에 자료를 다 복사했으면, 해당 메모리를 해제하기 전에 Unmap 을 호출해주어야 한다.
if(mUploadBuffer != nullptr)
{
mUploadBuffer->Unmap(0,nullptr);
}
mMappedData = nullptr;
Unmap 의 첫 매개변수는 대응을 해제할 부분 자원의 색인인데, 버퍼의 경우에는 0 이다. Unmap 의 둘째 매개변수는 대응을 해제할 메모리 범위를 서술하는 D3D12_RANGE 구조체의 포인터인데, 자원 전체의 대응을 해제하려면 nullptr 을 지정하면 된다.
상수 버퍼의 생성, 업로드 까지 정리
// 상수버퍼 생성
struct ObjectConstants
{
XMFLOAT4x4 WorldViewProj = MathHelper::Identity4x4(); // 4차원 단위행렬
}
// 버퍼의 크기를 최소 하드웨어 할당크기 (256byte 의 배수) 가 되게 하는 연산
UINT CalcConstantBufferByteSize(UINT byteSize)
{
return (byteSize + 255) & ~255;
}
UINT elementByteSize = CalcConstantBufferByteSize(sizeof(ObjectConstants));
ComPtr<ID3D12Resource> mUploadBuffer;
device->CreateCommittedResource(
CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
CD3DX12_RESOURCE_DESC::Buffer(elementByteSize*NumElements),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(mUploadbuffer.GetAddressOf())
);
// 상수 버퍼의 갱신
/* mMappedData -> mUploadbuffer */
/* mMappedData 를 mUploadbuffer 에 사상(대응; 연결) */
BYTE* mMappedData = nullptr; //
mUploadbuffer->Map(0, nullptr, reinterpret_cast<void**>(&mMappedData));
// mMappedData 에 데이터 복사 ( 흘려보내기 ). 어떤 데이터 data 가 있다고 할때
memcpy(mMappedData, &data, dataSizeInBytes);
// mUploadBuffer 닫기
if(mUploadBuffer != nullptr)
{
mUploadBuffer->Unmap(0,nullptr);
}
mMappedData = nullptr;
업로드 버퍼 관련 기능을 담은 가벼운 클래스가 있으면 편할 것이다. 여기서는 업로드 버퍼를 손쉽게 다룰 수 있는 UploadBuffer 클래스를 정의한다. 이 클래스는 업로드 버퍼 자원의 생성 및 파괴와 자원의 메모리 대응 및 해제를 처리해 준다. 또한 버퍼의 특정 항목을 갱신하는 CopyData 메서드도 제공한다. CPU 에서 버퍼의 내용을 변경해야 할 때 (이를테면 시야 행렬이 변햇을 때) 이 CopyData 메서드를 사용할 수 있다. 이 클래스를 상수 버퍼뿐만 아니라 그 어떤 업로드 버퍼에도 사용할 수 있음을 기억하기 바란다. 그런데 이 클래스를 상수 버퍼에 사용할 때에는 반드시 생성자의 isConstantBuffer 매개변수에 true 를 지정해야 한다. 그러면 이 클래스는 각 상수 버퍼가 256 byte 의 배수가 되도록 적절히 byte 들을 채운다.
UINT CalcConstantBufferByteSize(UINT ByteSize){
return ByteSize+255 & ~255;
}
template<typename T>
class UploadBuffer
{
public:
UploadBuffer(ID3D12Device* device, UINT elementCount, bool isConstantBuffer)
:mIsConstantBuffer(isConstantBuffer)
{
mElementByteSize = sizeof(T);
/*
상수 버퍼 원소의 크기는 반드시 256byte 의 배수이어야 한다.
이는 하드웨어가 m*256 byte 오프셋에서 시작하는 n*256byte 길이의
상수 자료만 볼 수 있기 때문이다.
typedef struct D3D12_CONSTANT_BUFFER_VIEW_DESC{
UINT64 OffsetInBytes; // 256 의 배수
UINT SizeInBytes; // 256 의 배수
} D3D12_CONSTANT_BUFFER_VIEW_DESC;
*/
if(isConstantBuffer)
{
mElementByteSize = CalcConstantBufferByteSize(sizeof(T));
}
ThrowIfFailed(
device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(mElementByteSize*elementCount),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&mUploadBuffer)
)
);
ThrowIfFailed(
mUploadBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mMappedData));
)
/*
자원을 다 사용하기 전에는 대응을 해제할 필요가 없다.
그러나 자원을 GPU 가 사용하는 중에는
CPU 에서 자원을 갱신하지 않아야 한다.
(따라서 반드시 동기화 기법을 사용해야 한다.)
*/
}
UploadBuffer(const UploadBuffer& rhs) = delete;
UploadBuffer& operator=(const UploadBuffer& rhs) delete;
~UploadBuffer()
{
if(mUploadBuffer != nullptr)
{
mUploadBuffer->Unmap(0,nullptr);
mMappedData = nullptr;
}
}
ID3D12Resource* Resource()const
{
return mUploadBuffer.Get();
}
void CopyData(int elementIndex, const T& data){
memcpy(&mMappedData[elementIndex*mElementByteSize], &data, sizeof(T));
}
private:
ComPtr<ID3D12Resource> mUploadBuffer;
BYTE* mMappedData = nullptr;
UINT mElementByteSize = 0;
bool mIsConstantBuffer = false;
}
일반적으로 물체의 세계 행렬은 장면 안에서 물체가 이동하거나, 회전하거나, 크기가 변하면 바뀌고, 물체의 시야 행렬은 카메라가 이동하거나, 회전하면 바뀐다. 그리고 투영 행렬은 창의 크기가 변하면 바뀐다. 이번장의 예제 프로그램에서 사용자는 마우스를 이용해서 카메라를 이동∙회전 할 수 있다. 이를 위해, 매 프레임이 호출되는 Update 함수에서 세계 - 시야 - 투영 행렬 (세계 행렬, 시야 행렬, 투영 행렬을 결합한 행렬) 응 새 시야 행렬로 갱신한다.
이는 따로 사용자 정의한 UploadBuffer 를 사용하는 예시로, 유저 가 마우스 이용에 따라 카메라를 이동∙회전 시키면서 매 프레임마다 갱신된 세계행렬 - 시야행렬 - 투영행렬을 결합한 행렬의 값을 구해서, 업로드 버퍼에 업데이트 하는 예시이다.
void BoxApp::OnMouseMove(WPARM btnState, int x, int y)
{
if((btnState & MK_LBUTTON) != 0)
{
// 마우스 한 픽셀 이동을 4분의 1도에 대응시킨다.
flaot dx = XMConvertToRadians(0.25*static_cast<float>(x - mLastMousePos.x));
float dy = XMConvertToRadians(0.25*static_cast<float>(y - mLastMousePos.y));
// 입력에 기초해 각도를 갱신해서 카메라가 상자를 중심으로 공전하게 한다.
mTheta += dx;
mPhi += dy;
// mPhi 각도를 제한한다.
mphi = MathHelper::Clamp(mPhi, 0.1f, MathHelper::Pi - 0.1f);
}
else if((btnState & MK_RBUTTON) != 0)
{
// 마우스 한 픽셀 이동을 장면의 0.005 단위에 대응시킨다.
float dx = 0.005f*static_cast<float>(x - mLastMousePos.x);
float dy = 0.005f*static_cast<float>(y - mLastMousePos.y);
// 입력에 기초해서 카메라 반지름을 갱신한다.
mRadius += dx - dy;
// 반지름을 제한한다.
mRadius = MathHelper::Clamp(mRadius, 3.0f, 15.0f);
}
mLastMousePos.x = x;
mLastMousePose.y = y;
}
void BoxApp::Update(const GameTimer& gt)
{
// 구면 좌표를 데카르트 좌표(직교 좌표)로 변환한다.
float x = mRadius*sinf(mPhi)*cosf(mTheta);
float z = mRadius*sinf(mPhi)*sinf(mTheta);
float y = mRadius*cosf(mPhi);
// 시야 행렬을 구축한다.
XMVECTOR pos = XMVectorSet(x, y, z, 1.0f);
XMVECTOR target = XMVectorZero();
XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
XMMatrix view = XMMatrixLookAtLH(pos, target, up); // 시야행렬
XMStoreFloat4x4(&mView, view);
XMMATRIX WORLD = XMLoaddFloat4x4(&mWorld);
XMMATRIX Proj = XMLoadFloat4x4(&mProj);
XMMATRIX worldViewPrj = World*View*Prj;
/*
struct ObjectConstants{
// 월드행렬×시야행렬×원근투영행렬
XMFLOAT4x4 WorldViewProj = MathHelper::Identity4x4();
}
*/
// 최신의 worldViewProj 행렬로 상수 버퍼를 갱신한다.
ObjectConstants objConstants;
XMStoreFloat4x4(&objConstants.WorldViewProj);
mObjectCB->CopyData(0, objConstants);
}
자원을 렌더링 파이프라인에 묶으려면 서술자 객체가 필요하다. 상수 버퍼를 파이프라인에 묶을 때에도 역시 서술자가 필요하다. 상수 버퍼 서술자는 D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV 형식의 서술자 힙에 담긴다. 이 힙은 상수버퍼(CBV), 셰이더 자원뷰(SRV), 순서 없는 접근뷰(UAV) 서술자들을 섞어서 담을 수 있다. 그런 새로운 형식의 서술자들을 저장하기 위해서는 이 형식의 서술자 힙을 생성해야 한다.
D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc;
cbvHeapDesc.NumDescriptors = 1;
cbvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
cbvHeapDesc.Flag = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
cbvHeapDesc.NodeMask = 0;
ComPtr<ID3D12DescriptorHeap> mCbvHeap = nullptr;
md3dDevice->CreateDescriptorHeap(&cbvHeapDesc, IID_PPV_ARGS(mCbvHeap.GetAddressOf()));
이 코드는 Render Target 이나 Depth∙Stencil 버퍼 서술자 힙을 생성하는 코드와 비슷하다. 한가지 중요한 차이는 셰이더 프로그램에서 이 서술자들에 접근할 것임을 뜻하는 D3D12_DESCRIPTOR_HEAP_FLAG_VISIBLE 플래그를 지정했다는 점이다. Constant Buffer View 를 생성하려면 D3D12_CONSTANT_BUFFER_VIEW_DESC 인스턴스를 채운 후 ID3D12Device::CreateConstantBufferView 를 호출해야 한다.
// 물체당 상수 자료
struct ObjectConstans
{
// 월드행렬×시야행렬×원근투영행렬 을 모두 적용한 변환행렬
XMFLOAT4x4 WorldViewProj = MathHelper::Identity4x4(); // 64byte
}
// 물체 n 개의 상수 자료를 담을 상수 버퍼
std::unique_ptr<UploadBuffer<ObjectConstant>> mObjectCB = nullptr;
mObjectCB = std::make_unique<UploadBuffer<ObjectConstant>>(md3dDevice.Get(), n, true);
// 사용자가 정의한 함수 CalcConstantBufferByteSize 함수를 사용했다.
UINT objCBByteSize = CalcConstantBufferByteSize(sizeof(ObjectConstants));
// 버퍼 자체의 시작 주소(0번째 상수 버퍼의 주소)
D3D12_GPU_VIRTUAL_ADDRESS cbAddress = mObjectCB->Resource()->GetGPUVirtualAddress();
// 버퍼에 담긴 i 번째 상수 버퍼의 오프셋
int boxCBufIndex = i;
cbAddress += boxCBufIndex * objCBByteSize;
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
cbvDesc.BufferLocation = cbAddress;
cbvDesc.SizeInbytes = CalcConstantBufferByteSize(sizeof(ObjectConstant)); // 256 BYTE
md3dDevice->CreateConstantBufferView(
&cbvDesc,
mCbvHeap->GetCPUDescriptorHandleForHeapStart() // 왜 CPU 지? UPLOAD_HEAP 의 위치는 RAM 이니까.
);
D3D12_CONSTANT_BUFFER_VIEW_DESC 구조체는 상수 버퍼 자원 중 HLSL 상수 버퍼 구조체에 묶일 부분을 서술한다. 앞에서 이야기 했듯이, 흔히 상수 버퍼에는 물체당 상수 자료 n 개의 배열을 저장한다. BufferLocation 과 SizeInBytes 를 적절히 지정함으로써 i 번째 물체의 상수 자료에 대한 뷰를 얻을 수 있다. 하드웨어의 제약 때문에, D3D12_COSNTANT_BUFFER_VIEW_DESC::SizeInBytes 멤버와 D3D12_CONSTANT_BUFFER_VIEW_DESC::OffsetInBytes 의 멤버는 반드시 256 byte 의 배수 이어야 한다. 예를들어 만일 이 멤버들에 64 를 지정하면 다음과 같은 디버그 오류 메시지들이 나온다.
std::unique_ptr<UploadBuffer<PassConstants>> PassCB = nullptr
이 버퍼는 하나의 렌더링 패스 전체에서 변하지 않는 상수 자료를 저장한다. 이를테면 시점 위치, 시야행렬과 투영 행렬, 그리고 화면(RenderTarget ; 렌더대상) 크기에 관한 정보가 이런 버퍼에 저장된다. 또한 셰이더 프로그램들이 유용하게 사용할 게임 시간 측정치 같은 정보도 이 버퍼에 저장된다. 예제 프로그램들이 아래에 나열된 모든 상수 자료를 항상 사용하는 것은 아니지만, 여분의 자료를 제공하는 데 드는 비용이 아주 낮으므로 그냥 모든 패스 상수 자료를 버퍼에 담아두는 것이 편하다. 예를 들어 이번 예제에는 렌더 대상 크기가 필요하지 않지만, 나중에 후처리 효과를 구현할 때가 되면 렌더 대상 크기 정보가 필요해 진다.
cbuffer cbPass : register(b1)
{
float4x4 gView;
float4x4 gInvView;
float4x4 gProj;
float4x4 gInvProj;
float4x4 gViewProj;
float4x4 gInViewProj;
float3 gEyePosW;
float cbPerObjectPad1;
float2 gRenderTargetSize;
float2 gInvRenderTargetSize;
float gNearZ;
float gFarZ;
float gTotalTime;
float gDeltaTime;
}
패스 관련 상수 자료들을 개별 버퍼에 넣어 두었으므로, 물체별 상수 버퍼는 해당 물체와 연관된 상수들만 담도록 수정한다. 지금 예제에서 물체를 그리는 데 필요한 상수 자료는 물체의 세계 행렬 뿐이다.
cbuffer cbPerObject : register(b0)
{
float4x4 gWorld;
}
이러한 수정들에는 상수들을 갱신 빈도에 근거해서 조직화한다는 개념이 깔렸다. 패스별 상수들은 렌더링 패스당 한번씩만 갱신하면 되고, 물체의 상수들은 지금 예제의 경우 물체의 세계 행렬이 변할 때에만 변경하면 된다. 장면에 나무 같은 정적 물체가 있다면, 해당 세계 행렬을 해당 상수 버퍼에 한번만 설정하면 된다. 이후 그 상수 버퍼는 갱신할 필요가 없다. 이책의 예제 프로그램들은 패스별 상수 버퍼들과 물체별 상수 버퍼들의 갱신을 처리하기 위해 다음과 같은 메서드들을 구현한다. 이 메서드들은 Update 메서드가 프레임당 한번씩 호출한다.
void ShapeApp::UpdateObjectCB(const GameTimer& gt)
{
auto CurrObjectCB = mCurrFrameResource->ObjectCB.Get();
for(auto& e : mAllRitems)
{
// 상수들이 바뀌었을 때에만 cbuffer 자료를 갱신한다.
// 이러한 갱신을 프레임 자원마다 수행해야 한다.
if(e->NumFramesDirty > 0)
{
XMMATRIX world = XMLoadFloat4x4(&e->World);
ObjectConstants objConstant;
XMStoreFloat4x4(&objConstants.World, XMMatrixTrnaspose(world));
currObjectCB->CopyData(e->ObjIndex, objConstants);
// 다음 프레임 자원으로 넘어간다.
e ->NumFramesDirty--;
}
}
}
void ShapesApp::UpdateMainPassCB(const GameTimer& gt)
{
XMMATRIX view = XMLoadFloat4x4(&mView);
XMMATRIX proj = XMLoadFloat4x4(&mProj);
XMMATRIX viewProj = XMMatrixMultiply(view, proj);
XMMATRIX invView = XMMatrixInverse(&XMMatrixDeterminant(view), view);
XMMATRIX invPorj = XMMatrixInverse(&XMMatrixDeterminant(Proj), proj);
XMMATRIX invVewProj = XMMatrixInverse(&XMMatrixDeterminant(viewProj), viewProj);
XMStoreFloat4x4(&mMainPassCB.View, XMMatrixTranspose(view));
XMStoreFloat4x4(&mMainPassCB.InvView, XMMatrixTranspose(invView));
XMStoreFloat4x4(&mMainPassCB.Prj, XMMatrixTranspose(proj));
XMStoreFloat4x4(&mMainPassCB.InvProj, XMMatrixTranspose(invProj));
XMStoreFloat4x4(&mMainPassCB.ViewProj, XMMatrixTranspose(viewProj));
XMStoreFloat4x4(&mMainPassCB.InvViewProj, XMMAtrixTranspose(invViewProj));
mMainPassCB.EyePosW = mEyePos;
mMainPassCB.RenderTargetSize = XMFLOAT2((float)mClientWidth, (float)mClientHeight);
mMainPassCB.InvRenderTargetSize = XMFLOAT2(1.0f / mClientWidth, 1.0f/mClientHeight);
mMainPassCB.NearZ = 1.0f;
mMainPassCB.FarZ = 1000.0f;
mMainPassCB.TotalTime = gt.TotalTime();
mMainPassCB.DeltaTime = gt.DeltaTime();
auto currPassCB = mCurrFrameResource->PassCB.Get();
CurrPassCB->CopyData(0, mMainPassCB);
}
상수 버퍼들의 이러한 변화에 맞게 정점 셰이더도 적절히 수정한다.
VertexOut VS(VertexIn vin)
{
VertexOut vout;
// 동차 절단 공간으로 변환한다.
float4 posW = mul(float4(vin.PosL), 1.0f, gWorld); // Local좌표계 -> World좌표계
vout.PosH = mul(posW, gViewProj); // World좌표계 -> 투영행렬
// 정점 색상을 그대로 픽셀 셰이더에 전달한다.
vout.Color = vin.Color;
return vout;
}
이러한 수정으로 정점마다 벡터 대 행렬 곱셈이 하나 늘었지만, 넉넉한 계산 능력을 갖춘 현세대 GPU 들에서는 무시할 수 있는 비용이다. 예제의 셰이더들이 기대하는 자원들도 변했다. 따라서 루트 서명도 갱신해야 한다. 다음에서 보듯이 새 루트 서명은 두개의 서술자 테이블을 받는다. (CBV 들이 서로 다른 빈도로 설정되므로 테이블이 두개 필요하다. 패스별 CBV 는 렌더링 패스당 한번씩만 설정하면 되지만, 물체별 CBV 는 렌더 항목마다 설정해야 한다.)
CD3DX12_DESCRIPTOR_RANGE cbvTable0;
cbvTable0.Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0); // CBV 타입 1개 0번 레지스터 등록
CD3DX12_DESCRIPTOR_RANGE cbvTable1;
cbvTable1.Int(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 1); // CBV 타입 1개 1번 레지스터 등록
// 루트 매개변수는 {서술자 테이블 or 루트 서술자 or 루트 상수} 이다.
CD3DX12_ROOT_PARAMETER slotRootParameter[2];
slotRootParameter[0].InitAsDescriptorTable(1, &cbvTable0);
slotRootParameter[1].InitAsDescriptorTable(1, &cbvTable1);
// 루트 서명은 루트 매개변수들의 배열이다.
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(2, slotRootParameter, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
참고로 셰이더가 사용하는 상수 버퍼가 너무 많아지지 않도록 해야한다. [Thiberoz13] 은 성능을 위해서는 그 수를 5 미만으로 두라고 권한다.