-
[DirectX11] Mesh,Model,Animation Instancing2024년 03월 12일
- 유니얼
-
작성자
-
2024.03.12.:41
728x90DirectX 11로 게임 엔진 아키텍처 만들기
Instancing은 3D 그래픽스에서 렌더링 성능을 향상시키는 중요한 기법 중 하나입니다. 이 기술을 이용하면 동일한 메시(mesh)를 사용하는 여러 객체를 단일 드로우 콜로 렌더링할 수 있어, CPU와 GPU 간의 통신 오버헤드를 크게 줄일 수 있습니다. 이는 특히 대규모 장면에서 수천 개의 동일한 객체를 렌더링해야 하는 게임이나 시뮬레이션에서 매우 유용합니다.
참고강의 링크:
Mesh와 Model 데이터에서의 Instancing 적용
Mesh Instancing
Mesh는 3D 모델을 구성하는 기본 단위로, 정점(vertex) 데이터와 이를 연결하는 인덱스(index) 데이터로 구성됩니다. Mesh Instancing은 이러한 메시를 기반으로 하여 동일한 메시를 가진 여러 객체를 효율적으로 렌더링하는 기법입니다.
// 메시를 인스턴싱하여 렌더링하는 함수 void MeshRenderer::RenderInstancing(shared_ptr<class InstancingBuffer>& buffer) { // 메시나 재질이 설정되지 않았으면 함수를 종료합니다. if (_mesh == nullptr || _material == nullptr) return; // 재질로부터 셰이더 객체를 가져옵니다. auto shader = _material->GetShader(); if (shader == nullptr) return; // 셰이더가 없다면 함수 종료 // 카메라의 뷰 매트릭스와 프로젝션 매트릭스를 셰이더에 전역 데이터로 전달합니다. shader->PushGlobalData(Camera::S_MatView, Camera::S_MatProjection); // 현재 씬의 광원 객체를 가져와 셰이더에 광원 데이터를 전달합니다. auto lightObj = SCENE->GetCurrentScene()->GetLight(); if (lightObj == nullptr) return; // 광원 객체가 없다면 함수 종료 shader->PushLightData(lightObj->GetLight()->GetLightDesc()); // 재질을 업데이트합니다. 재질에 설정된 모든 유니폼 변수들이 GPU로 전송됩니다. _material->Update(); // 메시의 정점 버퍼와 인덱스 버퍼 데이터를 GPU로 전송합니다. _mesh->GetVertexBuffer()->PushData(); _mesh->GetIndexBuffer()->PushData(); // 인스턴싱 버퍼의 데이터를 GPU로 전송합니다. 인스턴싱 데이터에는 각 인스턴스의 변환 정보 등이 포함됩니다. buffer->PushData(); // 인스턴스화된 드로우 콜을 수행하여 메시를 렌더링합니다. 이때, 인스턴싱 버퍼에 저장된 각 인스턴스 정보를 사용해 // 동일한 메시를 여러 번 그리되, 각각 다른 변환을 적용하여 그립니다. shader->DrawIndexedInstanced(0, _pass, _mesh->GetIndexBuffer()->GetCount(), buffer->GetCount()); }
Mesh Instancing을 적용할 때는 각 인스턴스에 대한 유니크한 변환 정보(위치, 회전, 스케일)를 포함하는 추가 버퍼를 사용합니다. 이 변환 정보를 정점 셰이더에 전달하여, 셰이더가 각 인스턴스의 메시를 올바른 위치에 렌더링할 수 있도록 합니다. 이 과정에서 중요한 점은 메시 데이터는 한 번만 GPU에 전송되며, 변환 정보만 각 인스턴스마다 다르게 적용된다는 것입니다.
Model Instancing
Model은 여러 메시로 구성된 3D 객체를 의미합니다. Model Instancing은 동일한 모델을 사용하는 여러 객체를 효율적으로 렌더링하기 위해 Instancing 기법을 적용하는 것을 말합니다. 이를 위해서는 모델을 구성하는 모든 메시에 대해 Instancing을 적용해야 합니다.
// 인스턴싱 버퍼를 사용하여 모델 렌더링 void ModelRenderer::RenderInstancing(shared_ptr<class InstancingBuffer>& buffer) { // 모델이 설정되지 않았으면 함수 종료 if (_model == nullptr) return; // 전역 데이터(카메라의 뷰 매트릭스와 프로젝션 매트릭스)를 셰이더에 전달 _shader->PushGlobalData(Camera::S_MatView, Camera::S_MatProjection); // 현재 장면의 광원 데이터 가져오기 및 셰이더에 전달 auto lightObj = SCENE->GetCurrentScene()->GetLight(); if (lightObj == nullptr) return; // 광원 객체가 없으면 함수 종료 _shader->PushLightData(lightObj->GetLight()->GetLightDesc()); // 본 데이터 준비 BoneDesc boneDesc; // 모델의 본 개수를 가져오고 각 본의 변환 매트릭스를 셰이더에 전달 const uint32 boneCount = _model->GetBoneCount(); for (uint32 i = 0; i < boneCount; i++) { shared_ptr<ModelBone> bone = _model->GetBoneByIndex(i); boneDesc.transforms[i] = bone->transform; // 각 본의 변환 매트릭스를 본 데이터에 저장 } _shader->PushBoneData(boneDesc); // 준비된 본 데이터를 셰이더에 전달 // 모델의 모든 메시에 대해 반복하여 렌더링 const auto& meshes = _model->GetMeshes(); for (auto& mesh : meshes) { if (mesh->material) mesh->material->Update(); // 메시의 재질이 있다면 업데이트 // 현재 메시의 본 인덱스를 셰이더에 설정 _shader->GetScalar("BoneIndex")->SetInt(mesh->boneIndex); // 입력 어셈블러 설정: 정점 버퍼와 인덱스 버퍼를 GPU에 전달 mesh->vertexBuffer->PushData(); mesh->indexBuffer->PushData(); // 인스턴싱 버퍼의 데이터를 GPU에 전달 buffer->PushData(); // 인스턴싱을 사용하여 메시 렌더링 _shader->DrawIndexedInstanced(0, _pass, mesh->indexBuffer->GetCount(), buffer->GetCount(), 0); } }
Model Instancing을 구현하는 한 가지 방법은 각 인스턴스의 메시마다 변환 정보를 적용하는 것입니다. 또 다른 방법으로는 모든 인스턴스에 대한 변환 정보를 하나의 큰 버퍼에 저장하고, 이 정보를 각 메시의 정점 셰이더에서 참조하는 방식이 있습니다. 후자의 방식을 사용하면 메모리 사용량을 최적화하고, 렌더링 파이프라인을 단순화할 수 있습니다.
Animation Instancing
Animation Instancing을 구현하기 위해선, 각 인스턴스의 애니메이션 상태를 관리할 수 있는 메커니즘이 필요합니다. 이는 애니메이션 프레임, 재생 속도, 현재 재생 중인 애니메이션 클립 등의 정보를 포함할 수 있습니다. 이러한 정보는 일반적으로 애니메이션 데이터와 함께 GPU에 전송되어, 셰이더에서 각 인스턴스마다 적절한 애니메이션 프레임을 계산하고 적용할 수 있도록 합니다.
// 모델의 메시들을 인스턴싱을 사용하여 렌더링하는 함수 void ModelAnimator::RenderInstancing(shared_ptr<class InstancingBuffer>& buffer) { // 모델이 설정되지 않은 경우 함수를 종료합니다. if (_model == nullptr) return; // 텍스처가 설정되지 않은 경우 텍스처를 생성합니다. if (_texture == nullptr) CreateTexture(); // 카메라의 뷰 매트릭스와 프로젝션 매트릭스를 셰이더의 전역 데이터로 설정합니다. _shader->PushGlobalData(Camera::S_MatView, Camera::S_MatProjection); // 현재 씬의 광원 정보를 가져와서 셰이더에 광원 데이터를 설정합니다. auto lightObj = SCENE->GetCurrentScene()->GetLight(); if (lightObj == nullptr) return; // 광원 객체가 없으면 함수를 종료합니다. _shader->PushLightData(lightObj->GetLight()->GetLightDesc()); // 변환 매트릭스 정보를 셰이더 리소스 뷰(SRV)를 통해 셰이더에 전달합니다. _shader->GetSRV("TransformMap")->SetResource(_srv.Get()); // 본 데이터를 준비합니다. BoneDesc boneDesc; // 모델의 본 개수를 가져와서 각 본의 변환 매트릭스를 셰이더에 설정합니다. const uint32 boneCount = _model->GetBoneCount(); for (uint32 i = 0; i < boneCount; i++) { shared_ptr<ModelBone> bone = _model->GetBoneByIndex(i); boneDesc.transforms[i] = bone->transform; } _shader->PushBoneData(boneDesc); // 모델의 모든 메시에 대해 반복 처리합니다. const auto& meshes = _model->GetMeshes(); for (auto& mesh : meshes) { if (mesh->material) mesh->material->Update(); // 메시의 재질이 있으면 업데이트합니다. // 현재 메시의 본 인덱스를 셰이더에 설정합니다. _shader->GetScalar("BoneIndex")->SetInt(mesh->boneIndex); // 메시의 정점 버퍼와 인덱스 버퍼 데이터를 GPU에 전송합니다. mesh->vertexBuffer->PushData(); mesh->indexBuffer->PushData(); // 인스턴싱 버퍼의 데이터를 GPU에 전송합니다. buffer->PushData(); // 인스턴싱을 사용하여 메시를 렌더링합니다. _shader->DrawIndexedInstanced(0, _pass, mesh->indexBuffer->GetCount(), buffer->GetCount()); } }
변환 매트릭스(transform matrix)를 사용하는 것은 Animation Instancing의 중요한 부분입니다. 각 본(bone)에 대한 변환 매트릭스는 해당 본의 위치, 회전, 스케일을 나타내며, 애니메이션을 적용할 때 이 매트릭스를 사용하여 메시의 각 정점을 적절한 위치로 이동시킵니다. 인스턴싱을 사용할 때, 모든 인스턴스에 대한 변환 매트릭스 배열을 셰이더에 전달하여, GPU가 각 인스턴스에 대해 병렬로 애니메이션 계산을 수행할 수 있습니다.
Instancing의 장점
- 성능 향상: 동일한 메시/모델을 사용하는 객체를 단일 드로우 콜로 렌더링함으로써 CPU와 GPU 간의 통신 부하를 줄일 수 있습니다.
- 메모리 사용 최적화: 메시 데이터를 각 인스턴스마다 중복해서 저장할 필요가 없으므로, 메모리 사용량을 줄일 수 있습니다.
- 대규모 장면 처리 용이: 수천 개의 객체를 렌더링해야 하는 대규모 장면에서 특히 효과적입니다.
프로젝트 호출
RenderDemo.h
#pragma once #include "IExecute.h" class RenderDemo :public IExecute { public: void Init() override; void Update() override; void Render() override; private: shared_ptr<Shader> _shader; shared_ptr<GameObject> _camera; vector<shared_ptr<GameObject>> _objs; private: };
RenderDemo.cpp
#include "pch.h" #include "RenderDemo.h" #include "GeometryHelper.h" #include "Camera.h" #include "GameObject.h" #include "CameraScript.h" #include "MeshRenderer.h" #include "Mesh.h" #include "Material.h" #include "Model.h" #include "ModelRenderer.h" #include "ModelAnimator.h" #include "Mesh.h" #include "Transform.h" #include "VertexBuffer.h" #include "IndexBuffer.h" void RenderDemo::Init() { RESOURCES->Init(); _shader = make_shared<Shader>(L"23. RenderDemo.fx"); // Camera _camera = make_shared<GameObject>(); _camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f, 0.f, -5.f }); _camera->AddComponent(make_shared<Camera>()); _camera->AddComponent(make_shared<CameraScript>()); shared_ptr<class Model> m1 = make_shared<Model>(); m1->ReadModel(L"Kachujin/Kachujin"); m1->ReadMaterial(L"Kachujin/Kachujin"); m1->ReadAnimation(L"Kachujin/Idle"); m1->ReadAnimation(L"Kachujin/Run"); m1->ReadAnimation(L"Kachujin/Slash"); for (int32 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<ModelAnimator>(_shader)); { obj->GetModelAnimator()->SetModel(m1); obj->GetModelAnimator()->SetPass(2); } _objs.push_back(obj); } // Model shared_ptr<class Model> m2 = make_shared<Model>(); m2->ReadModel(L"Tower/Tower"); m2->ReadMaterial(L"Tower/Tower"); for (int32 i = 0; i < 100; 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(m2); obj->GetModelRenderer()->SetPass(1); } _objs.push_back(obj); } // Mesh // Material { shared_ptr<Material> material = make_shared<Material>(); material->SetShader(_shader); auto texture = RESOURCES->Load<Texture>(L"UnityLogo", L"..\\Resources\\Textures\\UnityLogo.png"); material->SetDiffuseMap(texture); MaterialDesc& desc = material->GetMaterialDesc(); desc.ambient = Vec4(1.f); desc.diffuse = Vec4(1.f); desc.specular = Vec4(1.f); RESOURCES->Add(L"UnityLogo", material); } for (int32 i = 0; i < 100; i++) { auto obj = make_shared<GameObject>(); obj->GetOrAddTransform()->SetLocalPosition(Vec3(rand() % 100, 0, rand() % 100)); obj->AddComponent(make_shared<MeshRenderer>()); { obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"UnityLogo")); } { auto mesh = RESOURCES->Get<Mesh>(L"Sphere"); obj->GetMeshRenderer()->SetMesh(mesh); obj->GetMeshRenderer()->SetPass(0); } _objs.push_back(obj); } RENDER->Init(_shader); } void RenderDemo::Update() { _camera->Update(); RENDER->Update(); { LightDesc lightDesc; lightDesc.ambient = Vec4(0.4f); lightDesc.diffuse = Vec4(1.f); lightDesc.specular = Vec4(0.1f); lightDesc.direction = Vec3(1.f, 0.f, 1.f); RENDER->PushLightData(lightDesc); } // INSTANCING INSTANCING->Render(_objs); } void RenderDemo::Render() { }
결론
Mesh와 Model 데이터에서 Instancing을 적용함으로써, 렌더링 성능을 크게 향상시킬 수 있습니다. 이 기법은 게임 개발, 가상 현실, 시뮬레이션 등 다양한 분야에서 널리 사용되며, 현대적인 3D 그래픽스 프로그래밍에서 중요한 최적화 전략 중 하나로 자리 잡고 있습니다. Instancing을 통해 개발자들은 더욱 풍부하고 복잡한 3D 장면을 효율적으로 렌더링할 수 있게 되었습니다.
반응형다음글이전글이전 글이 없습니다.댓글