학습 페이지
www.inflearn.com
오늘부터는인스턴싱과 드로우콜에 대해 알아보자. 드로우콜은 최적화에서 많이 등장하는 용어이다.
유니티에서는 게임화면에서 Stats의 Batches라고 보아도 된다. 몇번만에 물체를 그리는 지에 관한 정보이다.
만약 물체가 늘어나게 된다면 그려야하는 수도 같이 증가하기 때문에 Batches가 증가하게 된다. 만약 유니티 설정에서 Dynamic Batching을 활성화하게 된다면 배치 수가 확 줄어들게 된다.
만약 지금까지 만든 DX코드에서 물체를 100개 만들려면 실제로 물체를 100번 그려야한다. 드로우 콜은 드로우를 몇번 호출했는지에 관한 정보이다. 지금은 큐브를 화면에 그려주려면 큐브라는 매시모델을 만들어 주고 쉐이더, Material 등을 만들어주고 최종적으로 Mesh Renderer가 그려주게 된다.
이후에는 렌더링 파이프라인을 세팅해주고 Draw 함수를 호출하고 present 함수로 제출하여 렌더링 파이프라인을 타고 들어가게 된다. 이 모든 과정을 통해 물체가 그려진다. 이때 물체가 늘어나면 이러한 과정이 반복되어야하는데 이를 최소화하면 효율적으로 물체를 그려줄 수 있다.
인스턴싱
화면에 여러 큐브를 보면 메시는 같지만 Material은 다르다. Material은 어떤 shader를 사용할지와 어떤 값을 넘겨줄지에 관한 정보가 포함된 파일이라고 보면 된다. 그렇기 때문에 완전하게 같은 물체라는 것은 Mesh도 같고 Material도 같다는
의미이다. 만약 shader와 인자 값들이 같지만 객체의 이름이 다르다면 다른 Material로 인식한다.
위의 그림과 같이 초록색의 별을 여러개 그리고 여러 색깔의 별을 하나씩 그린다고 했을 때 색깔을 계속 바꾸면서 그리는 것은 비용이 많이 든다. 이럴 때 초록색 별을 미리 다 그려두고 다른 별을 그리는 것이 효율적일 것이다. 이러한 기술이 인스턴싱에 포함된다. 이러한 방법을 통해 비용(Batches)을 줄일 수 있다.
Skeletal Mesh는 자체의 shader를 통해 만들어주기 때문에 인스턴싱이 적용되지 않는다.
이제 코드를 보자면 드로우콜이라는 것은 아래와 같은 Draw함수를 몇번 호출했는 지에 관한 정보이다.
_shader->DrawIndexed(0, _pass, mesh->indexBuffer->GetCount(), 0, 0);
이때 인스턴싱을 사용하려면 DrawIndexedInstanced 이런 함수를 사용해주면 된다. 일단 단계별로 인스턴싱을 실습해보도록 하자.
일단 전에 사용했던 베이가 구와 같은 매쉬를 통해 인스턴싱을 실습해보자.
일단 인스턴싱을 사용하지 않고 기존 코드대로 해보자면
Material은 기존에 가져오던 방식대로 가져오고 오브젝트는 랜덤함수를 사용해서 임의의 위치에 그려주도록 하자.
InstancingDemo.h
#pragma once
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;
};
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"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 < 100; 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);
// INSTANCING
//_mesh = mesh;
}
_objs.push_back(obj);
}
RENDER->Init(_shader);
}
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();
}
}
void InstancingDemo::Render()
{
}
이렇게 해주고 shader는 이전에 베이가 구를 사용했을 때의 쉐이더와 비슷하게 해주면 된다.
InstancingDemo.fx
#include "00. Global.fx"
#include "00. Light.fx"
struct VS_IN
{
float4 position : POSITION;
float2 uv : TEXCOORD;
float3 normal : NORMAL;
float3 tangent : TANGENT;
// INSTANCING
};
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;
}
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)
};
이렇게 해주면 베이가 구가 100개가 그려지게 된다. 근데 얼마나 효율적으로 렌더링 되고 있는지 확인하기 위해 프레임을 로그찍어서 확인할 수 있도록 하자.
Game.h
class Game
{
private:
void ShowFps();
};
Game.cpp
void Game::Update()
{
TIME->Update();
INPUT->Update();
ShowFps();
GRAPHICS->RenderBegin();
SCENE->Update();
GUI->Update();
_desc.app->Update();
_desc.app->Render();
GUI->Render();
GRAPHICS->RenderEnd();
}
void Game::ShowFps()
{
uint32 fps = GET_SINGLE(TimeManager)->GetFps();
WCHAR text[100] = L"";
::wsprintf(text, L"FPS : %d", fps);
::SetWindowText(_desc.hWnd, text);
}
하지만 물체의 생성 수를 급격하게 늘려주게 되면 버벅거리면서 Fps가 엄청 떨어는 것을 볼 수 있다.
이제 인스턴싱을 실습해보도록 하자. 이때 DrawIndexedInstanced 함수를 사용해주게 될 것인데 1만에 해당하는 수를 넘겨주면되지만 물체마다 다른 Transform을 처리해주어야한다. 인스턴싱할 좌표들을 연결해준 다음, 인스턴싱과 관련된 버퍼에 넘겨주면 따로 따로 들어오게 된다. 즉 전체 복사작업은 해주어야하지만 shader와 material이 달라지지 않는다고 보면 된다. 공장에서 컨베이너 벨트에서 라인을 유지하고 World값만 교체해주는 것이라고 보면 된다.
인스턴싱을 위해 관련 정보들을 저장해주자.
Vertex 버퍼를 수정해서 이를 받아 주도록 하자. 이때 constant buffer를 지정해줄 때 사용했던 방법으로 슬롯번호 1번을Instance buffer를 지정해주기 위해 사용하도록 Create 함수를 수정해주자. 지금까지는 Vertex버퍼가 Input Assembler 단계에서 원본의 Vertex 정보만 넣어주면 되기 때문에 IMMUTABLE로 만들어주었지만 이제는 경우에 따라서 달라지도록
해야한다.
VertexBuffer.h
#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; // CPU Read, GPU Read
}
else if (cpuWrite == true && gpuWrite == false)
{
desc.Usage = D3D11_USAGE_DYNAMIC; // CPU Write, GPU Read
desc.CPUAccessFlags = D3D10_CPU_ACCESS_WRITE;
}
else if (cpuWrite == false && gpuWrite == true) // CPU Read, GPU Write
{
desc.Usage = D3D11_USAGE_DEFAULT;
}
else
{
desc.Usage = D3D11_USAGE_STAGING;
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;
uint32 _stride = 0;
uint32 _offset = 0;
uint32 _count = 0;
uint32 _slot = 0;
bool _cpuWrite = false;
bool _gpuWrite = false;
};
이제 오브젝트의 업데이트를 수정해주도록 하자. 지금은 업데이트를 통해 모든 오브젝트의 meshRenderer가 update 되고 있는데 이를 대표적으로 한 오브젝트만 업데이트하도록 하자.
MeshRenderer의 Update 코드를 가져와서 인스턴싱에 맞게 수정해주자. 일단 각각의 World를 가져오는 부분은 없애주고 대표적으로 한번만 InstanceBuffer를 통해 PushData를 하도록 해주자.
InstancingDemo.h
#pragma once
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"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);
// 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()
{
}
이에 맞게 shader코드도 수정해주자. 핵심은 Input.world를 통해 물체마다 World를 세팅해줄 수 있다는 점이다.
InstancingDemo.fx
#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;
//물체마다 World 가져오기
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)
};
이렇게 해주면 버벅거림 없이 일정한 FPS가 유지 된다.
핵심은 원래는 각각의 mesh 와 material을 설정해서 물체를 draw 함수를 통해 그려준 것인데
인스턴싱버전에서는 초반의 같은 오브젝트의 정보들을 인스턴스 버퍼라는 거대한 버퍼를 밀어넣고 모든 정보를을 하나씩 넣어줘서 한 큐에 처리한다. 다른 정보는 받은 정보를 각각의 World 좌표를 VS단계에서 계산해주는 방식으로 작동한다.
결국에는 만개가 있더라도 드로우콜을 1번만 호출한 것이라고 보면 된다.
'게임공부 > Directx11' 카테고리의 다른 글
[Directx11][C++][3D]19. 인스턴싱(ModelAnimator) (0) | 2024.09.26 |
---|---|
[Directx11][C++][3D]18. 인스턴싱(MeshRenderer,ModelRenderer) (0) | 2024.09.24 |
[Directx11][C++][3D]16. 애니메이션(활용4,스카이박스) (0) | 2024.09.21 |
[Directx11][C++][3D]15. 애니메이션(활용2~3) (0) | 2024.09.19 |
[Directx11][C++][3D]14. 애니메이션(데이터추출& 활용1) (0) | 2024.09.17 |