-
[DirectX11] 인스턴싱(Instancing)과 드로우 콜(Draw Call)2024년 03월 12일
- 유니얼
-
작성자
-
2024.03.12.:19
728x90DirectX 11로 게임 엔진 아키텍처 만들기
DirectX11과 게임 엔진 아키텍쳐를 공부하면서, 그 중에서도 Instancing과 드로우 콜(Draw Call) 최적화는 게임 개발에서 매우 중요한 주제입니다. 이러한 개념을 이해하고 적절히 활용하면, 더 많은 객체를 화면에 렌더링하면서도 성능을 유지할 수 있습니다. 이 글에서는 Instancing과 드로우 콜에 대해 설명하고, DirectX11에서 이를 구현하는 방법을 예제 코드와 함께 살펴보겠습니다.
참고강의 링크:
[게임 프로그래머 도약반] DirectX11 입문 강의 - 인프런
게임 프로그래머 공부에 있어서 필수적인 DirectX 11 지식을 초보자들의 눈높이에 맞춰 설명하는 강의입니다., [사진][사진] [사진] 게임 개발자는 Unreal, Unity만 사용할 줄 알면 되는 거 아닌가요? 엔
www.inflearn.com
Instancing이란?
Instancing은 하나의 렌더링 호출로 여러 개의 객체를 화면에 그리는 기술입니다. 이 방법을 사용하면, 각 객체마다 별도의 드로우 콜을 발생시키지 않아도 동일한 메쉬(geometry)를 공유하는 여러 인스턴스를 효율적으로 렌더링할 수 있습니다. 예를 들어, 게임 내에 같은 형태의 나무가 수백 개 있다고 할 때, 각각의 나무에 대해 개별적으로 렌더링 호출을 하지 않고, Instancing을 사용하여 단일 드로우 콜로 모든 나무를 렌더링할 수 있습니다.
드로우 콜(Draw Call) 최적화
드로우 콜은 GPU에게 그래픽스 렌더링을 요청하는 호출입니다. 드로우 콜 수가 많을수록 CPU와 GPU 사이의 통신 부담이 증가하여 게임의 성능이 저하될 수 있습니다. 따라서, 드로우 콜 수를 최소화하는 것은 게임 성능 최적화에서 중요한 과제입니다. Instancing은 이러한 최적화를 달성하기 위한 효과적인 방법 중 하나입니다.
DirectX11에서의 Instancing 구현
DirectX11에서 Instancing을 구현하기 위해, 주로 두 가지 주요 개념을 사용합니다: Instanced Buffer와 Instanced Data. Instanced Buffer는 각 인스턴스의 고유 데이터(예: 위치, 회전 등)를 저장하는 버퍼이며, Instanced Data는 각 인스턴스에 적용할 데이터입니다.
#pragma once class VertexBuffer { public: VertexBuffer(); ~VertexBuffer(); ComPtr<ID3D11Buffer> GetComPtr() { return _vertexBuffer; } uint32 GetStride() { return _stride; } // 정점 하나의 크기 uint32 GetOffset() { return _offset; } // 버퍼 내에서의 오프셋 uint32 GetCount() { return _count; } // 버퍼에 저장된 정점의 수 uint32 GetSlot() { return _slot; } // 이 버퍼가 바인드될 슬롯 번호 // 정점 데이터를 이용해 버퍼를 생성하는 함수 template<typename T> void Create(const vector<T>& vertices, uint32 slot = 0, bool cpuWrite = false, bool gpuWrite = false) { _stride = sizeof(T); _count = static_cast<uint32>(vertices.size()); _slot = slot; _cpuWrite = cpuWrite; _gpuWrite = gpuWrite; D3D11_BUFFER_DESC desc; ZeroMemory(&desc, sizeof(desc)); desc.BindFlags = D3D11_BIND_VERTEX_BUFFER; // 이 버퍼가 정점 버퍼로 사용됨을 명시 desc.ByteWidth = (uint32)(_stride * _count); // 버퍼의 전체 크기 // 버퍼의 용도 설정 if (cpuWrite == false && gpuWrite == false) { desc.Usage = D3D11_USAGE_IMMUTABLE; // 변경 불가, GPU만 읽기 } else if (cpuWrite == true && gpuWrite == false) { desc.Usage = D3D11_USAGE_DYNAMIC; // CPU에서 쓰기 가능, GPU에서 읽기 desc.CPUAccessFlags = D3D10_CPU_ACCESS_WRITE; } else if (cpuWrite == false && gpuWrite == true) { desc.Usage = D3D11_USAGE_DEFAULT; // 기본 설정, GPU에서 읽고 쓰기 } else { desc.Usage = D3D11_USAGE_STAGING; // CPU와 GPU 모두에서 읽고 쓰기 가능 desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ | D3D11_CPU_ACCESS_WRITE; } D3D11_SUBRESOURCE_DATA data; ZeroMemory(&data, sizeof(data)); data.pSysMem = vertices.data(); // 정점 데이터의 포인터 // 버퍼 생성 HRESULT hr = DEVICE->CreateBuffer(&desc, &data, _vertexBuffer.GetAddressOf()); CHECK(hr); // 성공 여부 확인 } // 이 함수를 호출하면 버퍼가 입력 어셈블러 스테이지에 바인드됩니다. void PushData() { DC->IASetVertexBuffers(_slot, 1, _vertexBuffer.GetAddressOf(), &_stride, &_offset); } private: ComPtr<ID3D11Buffer> _vertexBuffer; // DirectX 정점 버퍼 uint32 _stride = 0; // 정점 하나의 바이트 크기 uint32 _offset = 0; // 버퍼 내에서의 오프셋, 일반적으로 0 uint32 _count = 0; // 버퍼에 저장된 정점의 수 uint32 _slot = 0; // 이 버퍼가 바인드될 슬롯 번호 bool _cpuWrite = false; // CPU에서 버퍼에 쓰기 가능 여부 bool _gpuWrite = false; // GPU에서 버퍼에 쓰기 가능 여부 };
프로젝트 호출
InstancingDemo.h
#pragma once #include "IExecute.h" class InstancingDemo : 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: // INSTANCING shared_ptr<Mesh> _mesh; shared_ptr<Material> _material; vector<Matrix> _worlds; shared_ptr<VertexBuffer> _instanceBuffer; };
InstancingDemo.cpp
#include "pch.h" #include "InstancingDemo.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 InstancingDemo::Init() { RESOURCES->Init(); _shader = make_shared<Shader>(L"19. InstancingDemo.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>()); // 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); // INSTANCING _material = material; } for (int32 i = 0; i < 10000; i++) { auto obj = make_shared<GameObject>(); obj->GetOrAddTransform()->SetPosition(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); // INSTANCING _mesh = mesh; } _objs.push_back(obj); } RENDER->Init(_shader); // INSTANCING _instanceBuffer = make_shared<VertexBuffer>(); for (auto& obj : _objs) { Matrix world = obj->GetTransform()->GetWorldMatrix(); _worlds.push_back(world); } _instanceBuffer->Create(_worlds, /*slot*/1); } void InstancingDemo::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); } /*for (auto& obj : _objs) { obj->Update(); }*/ _material->Update(); //auto world = GetTransform()->GetWorldMatrix(); //RENDER->PushTransformData(TransformDesc{ world }); _mesh->GetVertexBuffer()->PushData(); _instanceBuffer->PushData(); _mesh->GetIndexBuffer()->PushData(); //_shader->DrawIndexed(0, 0, _mesh->GetIndexBuffer()->GetCount(), 0, 0); _shader->DrawIndexedInstanced(0, 0, _mesh->GetIndexBuffer()->GetCount(), _objs.size()); } void InstancingDemo::Render() { }
결론
인스턴싱(Instancing)과 드로우 콜(Draw Call) 최적화는 게임 개발과 다양한 3D 애플리케이션에서 성능 향상을 위해 필수적인 기법입니다. 인스턴싱을 통해 같은 메시를 여러 번 렌더링할 때 발생하는 오버헤드를 줄이고, 드로우 콜의 수를 감소시킴으로써 CPU와 GPU 간의 통신 부하를 줄일 수 있습니다. 이러한 기법들을 적절히 활용하면, 대규모 장면을 더 효율적으로 렌더링하며, 전체적인 애플리케이션의 성능을 향상시킬 수 있습니다.
반응형다음글이전글이전 글이 없습니다.댓글