드로우 콜
게임을 개발하다보면 "드로우 콜을 줄이줘요" 라는 얘기를 많이 듣는다고 한다.
그렇다면 드로우 콜은 무엇인가?
우리가 지금까지 물체를 그리는 방법은 Mesh의 vertex에 대한 정보를 세팅하고 어떻게 그릴 것인지, 쉐이더는 어떻게 세팅하고 머티리얼 등등을 렌더링 파이프라인에 전부 세팅한 후에 마지막에 Draw... 함수 계열을 딱 호출하면 렌더링 파이프라인을 따라서 IA -> VS -> RS -> PS... 순으로 흘러가며 화면에 보여주게된다.
이때 Draw 함수 계열을 말 그대로 드로우 콜 이라고 부른다.
지금 포스팅 한대로 코딩을 했다면 물체가 100만개가 있다면 매 프레임마다 100만번의 드로우 콜을 해야할 것이고 이는 엄청난 과부화를 유발할 수 있다.
유니티를 기준으로 보면 stat을 확인 할 수 있는데 여기서 Batches를 확인하면 현재 얼마나 드로우 콜이 실행되고 있는지 확인할 수 있다.
여기서 보면 Saved 된 배치를 찾을 수 있는데 이것이 드로우 콜을 줄여 최적화 된 양을 의미한다.
즉 인스턴싱이라고 한다.
인스턴싱
인스턴싱을 한다는 건 결국 같은 물체를 여러번 그리는걸 아끼는 행위라고 할 수 있다. 가령 우리가 빨강, 파랑, 초록 색의 3개의 붓을 가지고 있고 빨간 원 30개, 파란 원 50개, 초록 원 50개를 그린다고 해보자.
그 누구도 빨강 붓을 들고 빨간 원을 20개만 그리다가 파란 붓으로 바꾼 후 파란원을 20개 그리고 다시 빨간 붓으로 바꿔 나머지 10개의 원을 그리는 사람은 없을 것이다.
당연하게도 빨간 붓을 든 순가 빨간 원 30개를 그릴거고 빨간색을 전부 그린 후에야 파란 붓으로 교체할 것이다.
이와 같은 원리를 그래픽스에 적용한다고 보면 된다.
그런데 같은 물체 라는 것의 정의는 무엇일까? 엔진 마다 다르겠지만 대부분 "같은 머티리얼"을 사용하는 물체를 같은 물체라고 본다. 물론 메쉬도 포함이다.
이제 이를 코드로 적용해보자.
일단 100개의 오브젝트를 만들어서 현재 상태를 테스트 해보자.
100개의 오브젝트가 만들어졌을 때는 프레임이 60 프레임으로 아주 잘 작동하는 모습이다.
그런데 만약 10000개를 만든다면 어떻게 될까?
프레임이 29로 절반까지 떨어진 모습이다. 이처럼 아직은 최적화가 되지 않았기 때문에 같은 물체임에도 불구하고 많은 드로우 콜을 요청하기에 프레임이 드랍되는 모습이다.
사용될 메쉬와 머티리얼이 무엇인지 지정하고 각 물체마다 사용될 월드 위치와 이 값들을 넣어줄 버퍼를 만들어준다.
shared_ptr<Mesh> _mesh;
shared_ptr<Material> _material;
vector<Matrix> _worlds;
shared_ptr<VertexBuffer> _instanceBuffer;
#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"
void InstancingDemo::Init()
{
RESOURCES->Init();
_shader = make_shared<Shader>(L"19. InstancingDemo.fx");
// Camera
_camera = make_shared<GameObject>();
_camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f, 1.f, -5.f });
_camera->AddComponent(make_shared<Camera>());
_camera->AddComponent(make_shared<CameraScript>());
{
shared_ptr<Material> material = make_shared<Material>();
material->SetShader(_shader);
auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\Resources\\Textures\\veigar.jpg");
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"Veigar", 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"Veigar"));
}
{
auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
obj->GetMeshRenderer()->SetMesh(mesh);
_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, 1);
}
void InstancingDemo::Update()
{
_camera->Update();
RENDER->Update();
{
LightDesc lightDesc;
lightDesc.ambient = Vec4(0.5f);
lightDesc.diffuse = Vec4(1.f);
lightDesc.specular = Vec4(1.f, 1.f, 1.f, 1.f);
lightDesc.direction = Vec3(0.f, -1.f, 0.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->DrawIndexedInstanced(0, 0, _mesh->GetIndexBuffer()->GetCount(), _objs.size());
}
void InstancingDemo::Render()
{
}
#include "00. Global.fx"
#include "00. Light.fx"
struct VS_IN
{
float4 position : POSITION;
float2 uv : TEXCOORD;
float3 normal : NORMAL;
float3 tangent : TANGENT;
// INSTANCING
matrix world : INST;
};
struct VS_OUT
{
float4 position : SV_POSITION;
float3 worldPosition : POSITION1;
float2 uv : TEXCOORD;
float3 normal : NORMAL;
};
//VS_OUT VS(VS_IN input)
//{
// VS_OUT output;
//
// output.position = mul(input.position, W);
// output.worldPosition = output.position;
// output.position = mul(output.position, VP);
// output.uv = input.uv;
// output.normal = input.normal;
//
// return output;
//}
VS_OUT VS(VS_IN input)
{
VS_OUT output;
output.position = mul(input.position, input.world); // W
output.worldPosition = output.position;
output.position = mul(output.position, VP);
output.uv = input.uv;
output.normal = input.normal;
return output;
}
float4 PS(VS_OUT input) : SV_TARGET
{
//float4 color = ComputeLight(input.normal, input.uv, input.worldPosition);
float4 color = DiffuseMap.Sample(LinearSampler, input.uv);
return color;
}
technique11 T0
{
PASS_VP(P0, VS, PS)
};
인스턴싱을 적용한 후에 60프레임으로 동작하는 모습이다.
모든 애들의 좌표를 인스턴스 버퍼라는 공간에 전부 밀어주고 쉐이더와 머티리얼은 동일하기 때문에 이는 공통적으로 한번만 밀어준다.
그리고 쉐이더 에서 인스턴스 버퍼를 통해 받은 좌표를 통해 트랜스폼을 맞춰주는게 핵심이다.
그 전 방식은 드로우 콜이 10000번 이지만 위와 같은 방법은 드로우 콜이 1번이다.
'C++ > DirectX 11' 카테고리의 다른 글
[DirectX] GPU - ViewPort, Collider, Picking (0) | 2024.10.17 |
---|---|
[DirectX] GPU - RawBuffer, System Value 분석, TextureBuffer, StructureBuffer (0) | 2024.10.15 |
[DirectX] 애니메이션 - 모델 애니메이션 (0) | 2024.10.09 |
[DirectX] 애니메이션 - 스키닝, 데이터 (1) | 2024.10.02 |
[DirectX] 모델 - 모델 띄우기, 계층 구조, ImGuI (1) | 2024.09.30 |