Instancing은 똑같은 물체를 그릴 때 사용한다. 정확히는 Vertex, Index, Material, Shader 등이 모두 같을 경우 Instancing을 이용할 수 있다. Instancing을 이용하면 훨씬 빨리 여러개의 물체를 그릴 수 있다.
struct InstancingParams
{
Matrix matWorld;
Matrix matWV;
Matrix matWVP;
};
void InstancingBuffer::Init(uint32 maxCount)
{
_maxCount = maxCount;
const int32 bufferSize = sizeof(InstancingParams) * maxCount;
D3D12_HEAP_PROPERTIES heapProperty = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
D3D12_RESOURCE_DESC desc = CD3DX12_RESOURCE_DESC::Buffer(bufferSize);
void InstancingBuffer::AddData(InstancingParams& params)
{
_data.push_back(params);
}
Instancing을 위해 InstancingBuffer을 만든다. maxCount를 인자로 받아 그 수 만큼 bufferSize를 세팅한다. 그리고 _data 벡터에 InstancingParams를 추가한다.
void InstancingBuffer::PushData()
{
const uint32 dataCount = GetCount();
if (dataCount > _maxCount)
Init(dataCount);
const uint32 bufferSize = dataCount * sizeof(InstancingParams);
void* dataBuffer = nullptr;
D3D12_RANGE readRange{ 0, 0 };
_buffer->Map(0, &readRange, &dataBuffer);
memcpy(dataBuffer, &_data[0], bufferSize);
_buffer->Unmap(0, nullptr);
_bufferView.BufferLocation = _buffer->GetGPUVirtualAddress();
_bufferView.StrideInBytes = sizeof(InstancingParams);
_bufferView.SizeInBytes = bufferSize;
}
그리고 렌더링할 때 _data에 추가된 것들을 _buffer에 뚜껑 열어주고 복사한다음 뚜껑을 닫는다. 그리고 _bufferView 설정을 해주는데 InstancingParams의 크기로 사이즈를 맞춘다.
void InstancingManager::Render(vector<shared_ptr<GameObject>>& gameObjects)
{
map<uint64, vector<shared_ptr<GameObject>>> cache;
for (shared_ptr<GameObject>& gameObject : gameObjects)
{
const uint64 instanceId = gameObject->GetMeshRenderer()->GetInstanceID();
cache[instanceId].push_back(gameObject);
}
for (auto& pair : cache)
{
const vector<shared_ptr<GameObject>>& vec = pair.second;
if (vec.size() == 1)
{
vec[0]->GetMeshRenderer()->Render();
}
else
{
const uint64 instanceId = pair.first;
for (const shared_ptr<GameObject>& gameObject : vec)
{
InstancingParams params;
params.matWorld = gameObject->GetTransform()->GetLocalToWorldMatrix();
params.matWV = params.matWorld * Camera::S_MatView;
params.matWVP = params.matWorld * Camera::S_MatView * Camera::S_MatProjection;
AddParam(instanceId, params);
}
shared_ptr<InstancingBuffer>& buffer = _buffers[instanceId];
vec[0]->GetMeshRenderer()->Render(buffer);
}
}
}
그리고 InstancingBuffer들을 관리하기 InstancingManager을 만든다. Instancing 하는 물체별로 InstanceID를 가진다.
GameObject 별로 InstancingParams를 세팅해 InstancingBuffer에 넣고 한번에 다 Render한다.
union InstanceID
{
struct
{
uint32 meshID;
uint32 materialID;
};
uint64 id;
};
uint64 MeshRenderer::GetInstanceID()
{
if (_mesh == nullptr || _material == nullptr)
return 0;
//uint64 id = (_mesh->GetID() << 32) | _material->GetID();
InstanceID instanceID{ _mesh->GetID(), _material->GetID() };
return instanceID.id;
}
InstanceID는 mesh의 ID와 material의 ID를 합쳐 만들어진다. union은 InstanceID를 id로 한번에 표현할 수도 있고 meshID와 material로 나누어서 표현할 수도 있다는 뜻이다. 이러면 mesh와 material이 같으면 같은 물체로 인지한다.
void MeshRenderer::Render()
{
GetTransform()->PushData();
_material->PushGraphicsData();
_mesh->Render();
}
void MeshRenderer::Render(shared_ptr<InstancingBuffer>& buffer)
{
buffer->PushData();
_material->PushGraphicsData();
_mesh->Render(buffer);
}
struct VS_IN
{
float3 pos : POSITION;
float2 uv : TEXCOORD;
float3 normal : NORMAL;
float3 tangent : TANGENT;
row_major matrix matWorld : W;
row_major matrix matWV : WV;
row_major matrix matWVP : WVP;
uint instanceID : SV_InstanceID;
};
Instancing Buffer을 렌더링할 때는 원래 GetTransform으로 matWVP 를 구해 계산했지만 Instancing Buffer은 그런 행렬들을 buffer 안에 (VS_IN..) 갖고 있기 때문에 그럴 필요가 없다.
void Mesh::Render(shared_ptr<InstancingBuffer>& buffer)
{
D3D12_VERTEX_BUFFER_VIEW bufferViews[] = { _vertexBufferView, buffer->GetBufferView() };
GRAPHICS_CMD_LIST->IASetVertexBuffers(0, 2, bufferViews);
GRAPHICS_CMD_LIST->IASetIndexBuffer(&_indexBufferView);
GEngine->GetGraphicsDescHeap()->CommitTable();
GRAPHICS_CMD_LIST->DrawIndexedInstanced(_indexCount, buffer->GetCount(), 0, 0, 0);
}
Mesh::Render에도 bufferViews에 Instancing Buffer을 추가로 넘겨 렌더링한다.
void InstancingManager::ClearBuffer()
{
for (auto& pair : _buffers)
{
shared_ptr<InstancingBuffer>& buffer = pair.second;
buffer->Clear();
}
}
매 프레임마다 InstancingBuffer을 Update하기 위해 프레임마다 ClearBuffer을 해줘야 한다.
#pragma region Object
for (int32 i = 0; i < 50; i++)
{
shared_ptr<GameObject> obj = make_shared<GameObject>();
obj->AddComponent(make_shared<Transform>());
obj->GetTransform()->SetLocalScale(Vec3(25.f, 25.f, 25.f));
obj->GetTransform()->SetLocalPosition(Vec3(-300.f + i * 10.f, 0.f, 500.f));
shared_ptr<MeshRenderer> meshRenderer = make_shared<MeshRenderer>();
{
shared_ptr<Mesh> sphereMesh = GET_SINGLE(Resources)->LoadSphereMesh();
meshRenderer->SetMesh(sphereMesh);
}
{
shared_ptr<Material> material = GET_SINGLE(Resources)->Get<Material>(L"GameObject");
material->SetInt(0, 1);
meshRenderer->SetMaterial(material);
//material->SetInt(0, 0);
//meshRenderer->SetMaterial(material->Clone());
}
obj->AddComponent(meshRenderer);
scene->AddGameObject(obj);
}
SceneManager에서 Object를 만들 때 SetInt(0,0)하면 일반으로 SetInt(0,1)하면 Instancing으로 렌더링한다.
D3D12_INPUT_ELEMENT_DESC desc[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 20, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "TANGENT", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 32, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "W", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 0, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1},
{ "W", 1, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 16, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1},
{ "W", 2, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 32, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1},
{ "W", 3, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 48, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1},
{ "WV", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 64, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1},
{ "WV", 1, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 80, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1},
{ "WV", 2, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 96, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1},
{ "WV", 3, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 112, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1},
{ "WVP", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 128, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1},
{ "WVP", 1, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 144, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1},
{ "WVP", 2, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 160, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1},
{ "WVP", 3, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 176, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1},
};
VS_OUT VS_Main(VS_IN input)
{
VS_OUT output = (VS_OUT)0;
if (g_int_0 == 1)
{
output.pos = mul(float4(input.pos, 1.f), input.matWVP);
output.uv = input.uv;
output.viewPos = mul(float4(input.pos, 1.f), input.matWV).xyz;
output.viewNormal = normalize(mul(float4(input.normal, 0.f), input.matWV).xyz);
output.viewTangent = normalize(mul(float4(input.tangent, 0.f), input.matWV).xyz);
output.viewBinormal = normalize(cross(output.viewTangent, output.viewNormal));
}
else
{
output.pos = mul(float4(input.pos, 1.f), g_matWVP);
output.uv = input.uv;
output.viewPos = mul(float4(input.pos, 1.f), g_matWV).xyz;
output.viewNormal = normalize(mul(float4(input.normal, 0.f), g_matWV).xyz);
output.viewTangent = normalize(mul(float4(input.tangent, 0.f), g_matWV).xyz);
output.viewBinormal = normalize(cross(output.viewTangent, output.viewNormal));
}
return output;
}
Mesh::Render에서 bufferViews를 넘기는데 그것을 표현한 것이 첫번째 코드이다. 4번째 이후부터 Instancing을 위해 추가된 코드이며 4x4행렬을 표현하기 위해 네줄씩 작성되어있다. 그리고 번호가 0에서 1로 바뀌었는데 VS_Main에서 g_int_0이 1일 때 input을 받아 계산하므로 Instancing 은 SetInt(0,1)을 할 때 렌더링되는 것이다.
'그래픽스' 카테고리의 다른 글
| [DX12] Tessellation (0) | 2022.07.31 |
|---|---|
| [DX12] Shadow Mapping (0) | 2022.07.30 |
| [DX12] Particle System (0) | 2022.07.29 |
| [DX12] Compute Shader (0) | 2022.07.29 |
| [DX12] Deferred Rendering (0) | 2022.07.28 |