이전에 학습한 Mesh Instancing은 구나 큐브처럼 하나의 메시로 구성된 오브젝트를 효율적으로 렌더링하는 방식이었습니다. 이번엔 FBX로 임포트한 복잡한 모델을 같은 방식으로 최적화합니다. 이 모델은 여러 개의 메시와 뼈(Bone) 구조를 가지고 있으며, Model, ModelRenderer, ModelAnimator로 구성됩니다.
먼저 MeshInstancingDemo를 복사해 ModelInstancingDemo 클래스를 만들고, Client/Game 필터에 배치합니다.
이 클래스는 500개의 타워 모델을 인스턴싱 방식으로 렌더링합니다.
// 모델 로드
shared_ptr<Model> model = make_shared<Model>();
model->ReadModel(L"Tower/Tower");
model->ReadMaterial(L"Tower/Tower");
// 게임 오브젝트 생성 및 모델 할당
for (int i = 0; i < 500; ++i)
{
auto obj = make_shared<GameObject>();
obj->GetOrAddTransform()->SetPosition(Vec3(rand() % 100, 0, rand() % 100));
obj->GetOrAddTransform()->SetScale(Vec3(0.01f)); // 타워 모델 크기 조절
obj->AddComponent(make_shared<ModelRenderer>(_shader));
obj->GetModelRenderer()->SetModel(model);
_objs.push_back(obj);
}
ModelRenderer를 통해 모델을 적용하고, 동일한 모델을 수백 개 복제해 관리합니다.
ModelInstancingDemo.fx인스턴싱 전용 쉐이더로 21. ModelInstancingDemo.fx를 새로 작성합니다. 이 쉐이더는 다음을 처리합니다:
cbuffer BoneBuffer
{
matrix BoneTransforms[MAX_MODEL_TRANSFORMS];
};
uint BoneIndex;
struct VS_IN
{
float4 position : POSITION;
float2 uv : TEXCOORD;
float3 normal : NORMAL;
float4 blendIndices : BLEND_INDICES;
float4 blendWeights : BLEND_WEIGHTS;
uint instanceID : SV_InstanceID;
matrix world : INST;
};
VS_OUT VS(VS_IN input)
{
VS_OUT output;
output.position = mul(input.position, BoneTransforms[BoneIndex]);
output.position = mul(output.position, input.world); // 인스턴스 변환
output.position = mul(output.position, VP); // 뷰-프로젝션
output.uv = input.uv;
output.normal = input.normal;
return output;
}
핵심은
BoneIndex를 기반으로 각 메시의 본 변환을 적용한 후, 인스턴스별world행렬을 곱해 최종 좌표를 얻는 것입니다.
기존 MeshRenderer 전용이던 InstancingManager에 ModelRenderer용 처리를 추가합니다.
RenderModelRenderer() 함수 추가ModelRenderer 객체 분류 → InstanceID 기준 캐싱RenderInstancing() 호출 → 본 데이터 처리 + 메시 렌더링InstanceID ModelRenderer::GetInstanceID()
{
return make_pair((uint64)_model.get(), (uint64)_shader.get());
}
모델 주소와 쉐이더 주소를 기준으로 같은 인스턴스를 묶어 렌더링합니다.
이제 본격적으로 ModelRenderer에서 인스턴싱 렌더링을 수행합니다.
BoneBuffer에 푸시BoneIndex 설정DrawIndexedInstanced 호출void ModelRenderer::RenderInstancing(shared_ptr<InstancingBuffer>& buffer)
{
if (_model == nullptr)
return;
// Bone 데이터 푸시
BoneDesc boneDesc;
for (uint32 i = 0; i < _model->GetBoneCount(); ++i)
{
auto bone = _model->GetBoneByIndex(i);
boneDesc.transforms[i] = bone->transform;
}
RENDER->PushBoneData(boneDesc);
// 메시 렌더링
for (auto& mesh : _model->GetMeshes())
{
if (mesh->material)
mesh->material->Update();
_shader->GetScalar("BoneIndex")->SetInt(mesh->boneIndex);
mesh->vertexBuffer->PushData();
mesh->indexBuffer->PushData();
buffer->PushData(); // World 행렬들
_shader->DrawIndexedInstanced(0, _pass, mesh->indexBuffer->GetCount(), buffer->GetCount());
}
}
모델 내부에 메시가 많을 경우, 하나의 인스턴스를 그릴 때도 메시 수만큼 드로우 콜이 발생합니다.
메시를 합치는 방식(Mesh Combine)으로 모델을 하나의 메시로 구성하면 드로우콜을 더욱 줄일 수 있습니다.
SRT(Scale, Rotation, Translation)이 모두 포함된 world 행렬 덕분에 회전, 크기 변화, 이동 모두 문제없이 인스턴싱 가능합니다.Frustum Culling, LOD, Batching 등을 적용하면 더 고성능 구현도 가능합니다.