1. 드로우콜이란
상용엔진을 사용했다면 드로우콜이라는 용어가 많이 등장한다.
드로우콜이 어떤 의미인지 파악을 할 수 있어야 한다.
유니티엔진에서 보면
Stats를 눌러 보면 Batches: 2라고 되어 있는데 2가 드로우콜이다.
몇 번 만에 그리는지라고 생각하면 된다.
DX11로 이것 저것 물체를 그렸는데 지금까지는 물체를 하나 그릴 때 마다 부품을 넣어서 그 부품이 실제로 물체를 그리는 데까지 코드를 만들어 놨다.
큐브를 늘리면 Batches가 늘어난다.
물체가 여러개로 늘어나는데 최적화를 하지 않으면 batches가 늘어난다.
이런 거를 최적화 하는 부분이 있다.
Project setting에 가서 Player에 Dynamic Batching을 사용할지 옵션이 있는데 이걸 켜보면 된다.
그러면 Batches가 떨어지는 것을 볼 수 있다.
그리고 Saved by batching의 숫자가 늘어난 것을 볼 수 있다.
이게 뭘 의미하는 걸까?
DX 코드에서 인게임에 큐브를 100개 만들면 100개를 그릴 것이다.
드로우콜이란 드로우를 내가 몇 번 호출 했느냐 라는 의미가 되는 것인데
물체를 그릴 때 쉐이더를 만들어 주고, 큐브라는 메쉬를 만들어 주고, material 이란 개념을 넣어서 컬러를 넣건 텍스쳐를 넣어서 만들어 놨다. 유니티에서 제공하는 기능을 만들어 놨다.
유니티에서 큐브를 그린다면 meshRenderer가 실제로 그리는 역할을 해서 meshRenderer가 물체를 그리게 되면 렌더링파이프라인을 이것저것 세팅을 하고 최종적으로 드로우 함수를 호출해서 그걸 present 함수를 호출해 제출을 해서 렌더링 파이프라인을 타게 되는 것이고 그러면 우리가 만들어 줬던 쉐이더를 타게 되면서 Input Assembler 단계에서 시작해서 IA, VS, Rasterizer, PixelShader, Output merger 이 단계까지 거쳐서 물체가 나온다고 결론을 내릴 수 있다.
모든 물체가 몇백개가 있을 때 그런식으로 다 그린다고 하면 굉장히 힘들 것이다.
이 드로우콜이라는 건 최소화 할수록 좋은 것이다.
어떻게 아꼈을까 그것을 위한게 인스턴싱이라는 기법이다.
인스턴싱이 없으면 대규모로 물체가 그려졌을 때 처리하는게 불가능하다.
어떤 관점에서 물체가 똑같다고 인지할 수 있을까?
mesh가 같아도 material 이 다르다면 같다고 인지할 수 있을까? material이라는게 shader에 어떤 인자들을 넘길지가 포함된 것이라고 볼 수 있다. material이 다르다는 건 쉐이더가 다를 수 있다는 거니까 아애 다르게 그려야 되는 거고, shader는 같은데 옵션이 다르다면 material에서 다루는 수치들이 constant buffer 등을 통해서 렌더링 파이프라인에 묶어 줬어야 됐었다. 그러기 때문에 결국 옵션이 다르더라도 아애 다르게 그려야 한다는 얘기다. 동일하게 그리려면 mesh, material을 똑같은 애를 사용해야 된다.
언리얼이나 유닌티에서 최적화 기능들이 많이 들어있다.
Unitychan 같이 복잡한 건 자체적인 쉐이더가 있어서 Dynamic Batching을 사용해도 batches에 영향을 안 줄 수도 있다.
오늘의 주제는 어떻게 식으로 효율적으로 만드냐이다.
2. DrawIndexedInstanced 살펴보기
지금까지 어떤 식으로 작업을 했는지 살펴보면 된다.
StaticMeshDemo에서 StaticMesh를 만들었을 때 어떻게 했을지 생각해 보면
ModelRenderer::Update의
_shader->DrawIndexed(0, _pass, mesh->indexBuffer->GetCount(), 0, 0);
드로우 콜이라는 건 DrawIndexed를 몇번 호출했느냐 라고 볼 수 있다 .
그렇다면 하나의 물체를 그릴 때 모아서 한번에 그리는 방법이 없을까 생각을 해보면
Draw 버전 중에서 Index 버전 뿐만 아니라 Instance를 사용하는 버전이 있다.
_shader->DrawIndexedInstanced()
void Shader::DrawIndexedInstanced(UINT technique, UINT pass, UINT indexCountPerInstance, UINT instanceCount, UINT startIndexLocation, INT baseVertexLocation, UINT startInstanceLocation)
{
_techniques[technique].passes[pass].DrawIndexedInstanced(indexCountPerInstance, instanceCount, startIndexLocation, baseVertexLocation, startInstanceLocation);
}
이 버전을 사용한다고 하면 매개변수로 UINT instanceCount로 instance가 몇개인지를 받아 준다.
그래서 너가 물체를 몇 개를 그릴거냐를 따로 옵션으로 빼줘가지고 만드는 버전인데 DrawIndexed 보다 상위 호환이라고 볼 수 있다.
드로우콜 이라는 건 조금 더 이 함수를 개선해서 물체를 한번에 빠르게 그리는 방식이라고 보면 되는데 이거를 단계별로 나눠서 할 것이다.
Liting을 실습할 때도 Ambient, Diffuse, Specualr를 따로따로 한 다음에 3개를 뭉쳐서 한번에 관리 했던 것처럼
이것도 여러 단계에 걸쳐서 실습을 한텐데 이론은 단순한데 지금까지 만든 코드에 넣어서 동작하게 하는게 생각보다 까다로운 부분이 많다.
Client로 돌아와서 작업을 해본다.
Client/Game/Week2필터의 코드들은 더이상 필요가 없으니 삭제한다.
3. 기본 Mesh, Material을 띄우는 InstancingDemo 클래스와 19. InstancingDemo.fx를 생성하고 채워주기
1) 파일생성하기
Client/Game/Week3에 InstancingDemo라는 이름의 클래스를 추가한다.
이 개념은 애니메이션에 비하면 어렵지는 않지만 다른 의미에서 까다로운 게 있다.
#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;
};
그리고 탐색기에서 18. SkyDemo.fx를 복붙해서 이름을 19. InstancingDemo.fx로 한다.
Client/Shaders/Week3 필터에 넣어준다.
InstancingDemo 클래스의 코드를 만들 때는
지난 시간에 했던 AnimationDemo의 코드를 가져와서 작업을 하면 된다.
후반부에 SceneManager를 만들어서 코드를 옮기기 시작할 것이다. 정리작업도 슬슬 시작해야 한다.
일단 InstancingDemo.cpp에 헤더들을 넣어준다.
2) InstancingDemo::Init에서 기본 세팅하기
DX11 초반에 했던 것처럼 구 mesh 하나 만들어 줘서 texutre를 씌우고 그랬던 부분들을 다시 만들어 볼 것이다.
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);
}
for (int32 i = 0; i < 100; i++)
{
auto obj = make_shared<GameObject>();
obj->GetOrAddTransform()->SetPosition(Vec3(rand() % 10, 0, rand() % 10));
obj->AddComponent(make_shared<MeshRenderer>());
{
obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
}
{
auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
obj->GetMeshRenderer()->SetMesh(mesh);
}
_objs.push_back(obj);
}
RENDER->Init(_shader);
}
이렇게 하면 기본적인 세팅이 완료 된 것이고
3) InstancingDemo::Update
Update할 때
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();
}
}
이게 기본적으로 우리가 했던 방식이라고 볼 수가 있다.
물체를 이렇게 세팅해주고
Client/Main의 Main.cpp로 돌아가서
#include "InstancingDemo.h"
desc.app = make_shared<InstancingDemo>(); // 실행 단위
세팅을 하고
Client프로젝트를 빌드를 한다.
4) 19. InstancingDemo.fx 쉐이더 채우기
Shader를 건드려줘야 한다.
이 19. 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)
};
이렇게 코드를 만든다.
Client 프로젝트를 빌드한다.
4. 프레임 정보 로그 찍고 오브젝트를 늘려 테스트 하기
지금까지 우리가 하던 코드가 어떻게 동작하는지를 살펴본다.
Client를 시작프로젝트로 하고 실행을 하면
물체들이 많이 배치되어 있는 것을 볼 수 있다.
궁금한 것은 효율적으로 렌더링이 되는지가 궁금하다.
렌더링이 어느 속도로 되고 있는지 궁금하니 프레임을 로그를 찍어서 작업을 해본다.
로그를 찍는 방법은 다양하다.
Engine쪽에 Game::ShowFps() 함수를 를 만들어 넣어 놓는다. 어디서 해도 크게 상관은 없다.
void ShowFps();
void Game::ShowFps()
{
uint32 fps = GET_SINGLE(TimeManager)->GetFps();
WCHAR text[100] = L"";
::wsprintf(text, L"FPS : %d", fps);
::SetWindowText(_desc.hWnd, text); // 윈도의 이름쪽에 들어가서 출력이 된다.
}
그리고 Game::Update()에서 호출한다.
중요한 건 물체를 늘릴 때마다 얘가 어떻게 반응하는지 얼마나 느려지는지 빨라지는지가 관심사다.
실행을 해보면
이렇게 118~120 프레임이 유지 되는걸 볼 수 있다.
이제부터 테스트를 해보는 것이다.
InstancingDemo::Init에서
for (int32 i = 0; i < 10000; i++)
{
auto obj = make_shared<GameObject>();
obj->GetOrAddTransform()->SetPosition(Vec3(rand() % 100, 0, rand() % 100));
이렇게 10000개를 만들면 어떻게 될까
버벅거리면서 프레임이 떨어진 것을 볼 수 있다.
가만히 있는 물체임에도 불구하고 떨어지는 걸 알수 있다.
게임이 느려지는 걸 원치 않기 때문에 어떻게든 성능 향상을 내는 방식을 생각하면 된다.
5. Instancing을 할 때 오브젝트마다 다른 World값을 넣어줄 수 있게 하기
instancing 하면 되지 않을까 싶은데
물체마다 달라지는 정보가 있다.
VS_OUT VS(VS_IN input)
{
VS_OUT output;
output.position = mul(input.position, W);
이 World라는 애는 지금까지 공부했던 트랜스폼 값에 의해가지고 사실상 결정이 되는 것인데 물체가 10000개 있다고 하면 동일한 위치라면 상관 없겠지만 위치가 다 바뀌니까 이렇게 하면 안된다는 얘기가 된다.
방법이 여러가지가 있는데 인스턴싱을 적용할 때는 기본적으로 모든 좌표들을 연결을 해줘가지고 World를 각각 만들어 줘야 될 것이다.
그리고 VS_IN에 넣어줄 인스턴싱과 관련된 버퍼에 넣어주게 되면 걔네들이 따로 따로 들어오게 될 것이다.
전체 복사 작업은 한 번 해줘야 되는데 그 다음부터 실질적으로 요 쉐이더랑 머테리얼이 달라지지는 않는다고 보면 된다.
드로우 콜이나 인스턴싱을 얘기할 때는 공장에서 공장을 짓는다고 생각하면 되는데 컨베이어 벨트가 쭉 늘어서서 제품이 지어지는 상황이 사실상 우리의 렌더링 파이프라인이라고 볼 수가 있는 것인데 거기서 괜히 물체를 갈아 끼우거나 마테리얼을 갈아 끼우거나 라는 얘기는 공장을 부수고 다시 다른 라인을 깔아서 작업하는 방식이다.
그렇게 하지 않고, 라인은 그대로 유지한채로 이런저런 정보만 교체해서 만들 수 있으면 훨씬 빠르게 작업할 수 있게 될 것이기 때문에 이 전체 라인은 그대로 유지하고 이 World라는 이 아이만 어떻게든 교체해서 넣어주는 수단을 만들어 주면 된다고 볼 수 있다.
Renderer 쪽에다가 끼워 넣어가지고 작업을 해야 하는데
일단은 Client 단에서 간단하게 테스트 해본다.
1) 인스턴싱할 때 필요한 정보들 변수에 넣기
이제는 인스턴싱과 관련된 코드이다.
InstanceDemo.h에서
private:
// INSTANCING
shared_ptr<Mesh> _mesh; // 원본 메시
shared_ptr<Material> _material;
vector<Matrix> _worlds; // 물체들의 온갖 transform 정보, 월드 변환행렬을 들고 있는 거
shared_ptr<VertexBuffer> _instanceBuffer;
인스턴싱과 관련된 변수들을 추가하고
InstancingDemo.cpp에서 Instancing과 관련된 정보들이 추가가 되면 된다.
InstancingDemo::Init에서
//Material안에
// INSTANCING
_material = material;
material이 무엇이었는지 기억할 것이고,
// INSTANCING
_mesh = mesh;
mesh도 기억하고
2) 각 obj의 world를 vector<Matrix> _worlds에 넣어 VertexBuffer의 Create함수에 전달하기
// INSTANCING
_instanceBuffer = make_shared<VertexBuffer>();
_instanceBuffer->Create(_worlds);
이런식으로 Create를 할 때 정보를 받아주고, 이 정보를 토대로 버퍼를 채워 주게끔 VertexBuffer 클래스를 만들어 놨다.
VertexBuffer를 만드는 Create에 정보를 넘겨주기 전에
// INSTANCING
_instanceBuffer = make_shared<VertexBuffer>();
for (auto& obj : _objs)
{
Matrix world = obj->GetTransform()->GetWorldMatrix();
_worlds.push_back(world);
}
_instanceBuffer->Create(_worlds);
}
이렇게 각 obj의 world값을 _worlds에 넣어줬다.
3) VertexBuffer를 만들 때 vector<Matrix> _worlds와 slot 번호를 넣어 묶어줄 수 있게 VertexBuffer의 Create 수정하기
인스턴스 버퍼를 만들 때 일반 버텍스 버퍼랑 완전히 100% 동일하진 않고 달라지는 부분이 있다.
Engine부의 VertexBuffer클래스를 수정해야 한다.
slot 번호라는 추가 정보가 필요하다.
constant buffer 를 처음에 만들었을 때는 slot 번호 0번 1번 2번 3번 이런 식으로 constant buffer를 직접 조작을 했었다. 그 부분이 shader라는 클래스에 의해 숨겨져있긴 하지만 없는 건 아니다. 그거를 세팅해줘서 지금은 insatnace buffer를 1번에다 세팅을 하게끔 조정을 해주도록 한다.
VertexBuffer.h의 Create함수에서 추가적인 인자를 받을 건데
template<typename T>
void Create(const vector<T>& vertices, uint32 slot = 0, bool cpuWrite = false, bool gpuWrite = false)
{
slot, cpuWrite, gpuWrite 매개 변수를 추가한다.
그리고 변수가
_stride, _offset, _count 세가지 정보만 있었는데
uint32 _slot = 0;
bool _cpuWrite = false;
bool _gpuWrite = false;
옵션에 따라서 분기를 해보도록 할 것이다.
Create함수에서 매개 변수로 넣어준 애들을 기억해줄 것이다.
_slot = slot;
_cpuWrite = cpuWrite;
_gpuWrite = gpuWrite;
원래는 지금까지 사용할 때는 input assembler 단계에서 vertex 정보를 넣어주는 용도로만 이 버퍼를 사용했기 때문에 항상 _cpuWrite, _gpuWrite가 다 false, false였고
desc.Usage = D3D11_USAGE_IMMUTABLE;
immutable로 한번 정해지면 더 이상 고칠 수 없다라는 애로 이렇게 만들어졌지만
이제 경우에 따라서 여러가지 옵션에 따라서 달라질 수 있기 때문에 그 부분을 수정을 해보도록 할 것이다.
desc.Usage = D3D11_USAGE_IMMUTABLE;
이 부분이 날아가고
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;
}
이렇게 수정이 되었다고 보면 된다.
cpuWrite, gpuWrite 옵션이 뭐뭐 있냐에 따라가지고 옵션들을 각각 다르게 세팅했다는 걸 볼 수 있다.
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.Usage = D3D11_USAGE_IMMUTABLE;
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);
}
Create를 설정할 때 gpu랑 cpu랑 어떻게 접근할 수 있는지도 넣어줬다. 당장은 중요한게 slot 번호를 넣어줘서 슬롯 번호에 따라서 얘가 다른 공간에도 매핑이 될 수 있게끔 코드를 만들어주면 될 것이다.
uint32 GetSlot() { return _slot; }
이것도 추가한다.
엔진코드였기 때문에 엔진을 빌드해준다.
VertexBuffer가 수정이 되어서 추가적으로 몇 번 슬롯에다가 묶어 줄지를 정해줄 수가 있게 되었다 라고 볼 수가 있다.
InstancingDemo::Init에서
_instanceBuffer->Create(_worlds, /*slot*/ 1);
슬롯번호를 넣어줬다.
4) _worlds를 담아 만든 _instanceBuffer를 1번 slot으로 InputAssembler에 세팅하기
InstancingDemo::Update에서
for (auto& obj : _objs)
{
obj->Update();
}
_objs를 순회하며 obj→Update를 하면 obj에 MeshRenderer 컴포넌트가 있기 때문에
각 obj마다 MeshRenderer의 Update를 호출할 것이고,
shader->DrawIndexed(0, 0, _mesh->GetIndexBuffer()->GetCount(), 0, 0);
DrawIndexed를 obj의 숫자만큼 호출하게 된다.
이 것을 한번만 호출하게 수정을 해야 한다.
대표로 한명이 나머지 애들 몫까지도 한번만 호출해야지 모든 물체들이 돌아가면서 한 번씩 다 자기만의 버전으로 호출하는 개념이 아니다
일단 MeshRenderer::Update의 코드를 InstancingDemo::Update에 복붙해 놓 수정한다.
_material->Update(); // 렌더링 하는 부분이 케어가 된다. 온갖 잡동사니를 밀어 넣어주기 때문에 조명과 관련된 부분들이 세팅된다.
//auto world = GetTransform()->GetWorldMatrix();
//RENDER->PushTransformData(TransformDesc{ world });
uint32 stride = _mesh->GetVertexBuffer()->GetStride();
uint32 offset = _mesh->GetVertexBuffer()->GetOffset();
DC->IASetVertexBuffers(0, 1, _mesh->GetVertexBuffer()->GetComPtr().GetAddressOf(), &stride, &offset);
DC->IASetIndexBuffer(_mesh->GetIndexBuffer()->GetComPtr().Get(), DXGI_FORMAT_R32_UINT, 0);
shader->DrawIndexed(0, 0, _mesh->GetIndexBuffer()->GetCount(), 0, 0);
world는 이제 자기 자신의 world르 넣어주는게 아니라 모두가 공유해가지고 갖고 있는 InstanceBuffer를 이용해서 넣어줘야 되기 때문에
shared_ptr<VertexBuffer> _instanceBuffer;
_instanceBuffer를 이용해 줘야 한다.
IASetVertexBuffers, IASetIndexBuffer를 통해 정보들을 넣어주고 있는데 유심히 보면
StartSlot이라는게 있는데
DC->IASetVertexBuffers(0, 1, _mesh->GetVertexBuffer()->GetComPtr().GetAddressOf(), &stride, &offset);
원래는 자연스럽게 0번으로 사용하고 있었지만
DC->IASetVertexBuffers(1, 1, _mesh->GetVertexBuffer()->GetComPtr().GetAddressOf(), &stride, &offset);
이제는 1로 해서 이제는 1번을 이용해서 그거는
shared_ptr<VertexBuffer> _instanceBuffer;
_instanceBuffer로 바꿔치기 해주도록 할 것이다.
verexBuffer에다가 일일이 하나씩 갖고 와서 실질적으로 데이터를 밀어넣는 작업을 2번에 걸쳐서 하고 있지만 vertexBuffer 밀어넣고 하는 부분들이 나중에 많이 사용되게 될 것이다.
데이터도 밀어넣는 코드를 함수로 넣어주면 좋겠다는 생각이 든다.
VertexBuffer.h에 가서
void PushData()
{
DC->IASetVertexBuffers(_slot, 1, _vertexBuffer.GetAddressOf(), &_stride, &_offset);
}
IASetVertexBuffers를 사용해서 밀어 넣는 부분을 넣는다.
마찬가지로 IndexBuffer 코드에다가도 데이터를 밀어 넣는 함수를 정의한다.
void PushData()
{
DC->IASetIndexBuffer(_indexBuffer.Get(), DXGI_FORMAT_R32_UINT, 0);
}
코드를 반복적으로 하기 싫으니까 자주 사용할 것과 같은 부분만 함수로 넣어준 상태라고 보면 된다.
다시 InstancingDemo::Update의
_material->Update(); // 렌더링 하는 부분이 케어가 된다. 온갖 잡동사니를 밀어 넣어주기 때문에 조명과 관련된 부분들이 세팅된다.
//auto world = GetTransform()->GetWorldMatrix();
//RENDER->PushTransformData(TransformDesc{ world });
uint32 stride = _mesh->GetVertexBuffer()->GetStride();
uint32 offset = _mesh->GetVertexBuffer()->GetOffset();
DC->IASetVertexBuffers(0, 1, _mesh->GetVertexBuffer()->GetComPtr().GetAddressOf(), &stride, &offset);
DC->IASetIndexBuffer(_mesh->GetIndexBuffer()->GetComPtr().Get(), DXGI_FORMAT_R32_UINT, 0);
shader->DrawIndexed(0, 0, _mesh->GetIndexBuffer()->GetCount(), 0, 0);
이 코드를 깔끔하게 정리할 수 있게 됐다.
_mesh->GetVertexBuffer()->PushData();
이렇게 해주면
uint32 stride = _mesh->GetVertexBuffer()->GetStride();
uint32 offset = _mesh->GetVertexBuffer()->GetOffset();
DC->IASetVertexBuffers(0, 1, _mesh->GetVertexBuffer()->GetComPtr().GetAddressOf(), &stride, &offset);
이 삼총사가 한번에 케어가 된다.
_instanceBuffer->PushData();
이걸로 1번 슬롯에 넣어줄 것이다.
DC->IASetIndexBuffer(_mesh->GetIndexBuffer()->GetComPtr().Get(), DXGI_FORMAT_R32_UINT, 0);
이 코드대신
_mesh->GetIndexBuffer()->PushData();
를 해서 다시 한 번 밀어 넣어줄 것이다.
마지막으로
shader->DrawIndexed(0, 0, _mesh->GetIndexBuffer()->GetCount(), 0, 0);
이렇게 되어 있던 부분을
_shader->DrawIndexedInstanced(0, 0, _mesh->GetIndexBuffer()->GetCount(), _objs.size());
이렇게 한다.
결국 코드 수정이 많은 거 같지만 많지는 않았다.
원래 만들었던 클래스들이 범용적으로 사용하지 못하도록 되어 있었기 때문에 바뀐 것이고
실제로 바뀐 부분들을 보면 유일하게 바뀐 부분이 인스턴스 버퍼라는 게 추가가 되고 인스턴스 버퍼가 추가됨에 따라가지고 _instanceBuffer→PushData로 먼저 세팅해주고, drawIndexedInstance 함수를 호출해줬다 라는 부분만 달라진다고 보면 된다.
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());
}
5) 19. InstancingDemo.fx쉐이더에서 matrix world를 INST로 설정해서 Shader::CreateInputLayout에서 InputSlot이 1로 설정되게 하기
여기서 끝내는게 아니라 쉐이더 쪽에서도 뭔가를 건드려 줘야 하는데 19. InstancingDemo.fx에 가서 보면
W로 World를 받아서 물체를 그려보고 있었는데
VS_IN에서 INSTANCING이 들어간다고 하면
struct VS_IN
{
float4 position : POSITION;
float2 uv : TEXCOORD;
float3 normal : NORMAL;
float3 tangent : TANGENT;
// INSTANCING
matrix world : INST;
};
Shader.cpp에 가면 INST를 어떻게 인지하는지가 나와 있다.
Shader::CreateInputLayout에서
if (Utils::StartsWith(name, "INST") == true)
{
elementDesc.InputSlot = 1;
elementDesc.AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT;
elementDesc.InputSlotClass = D3D11_INPUT_PER_INSTANCE_DATA;
elementDesc.InstanceDataStepRate = 1;
}
InputSlot을 1번으로 인지를 해서 하겠다는 부분이 여기에 있기 때문에 matrix world : INST에 1번을 하건 2번을 하건 상관이 없다. 이름을 맞춰주기만 하면 된다.
Shader라는 클래스를 사용하지 않고, 오리지널하게 만든다고 하더라도 비슷한 방식으로 slot을 설정해가지고 세팅하는 부분이 들어가면 된다. 중요한 건 넣어준 Instance의 값들을 이용해서 실질적으로 물체마다의 world를 추출할 수 있게 하는거다.
6) VS 수정하기
19. InstancingDemo.fx쉐이더에서 기존의
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;
}
input.world를 꺼내주면 된다.
물체마다 들고있는 world라는 자신만의 변환 행렬이 있을테니까 그걸 이용해서 물체를 그려주면 된다.
7) 테스트하기
실행을 해보면
물체가 많아졌음에도 불구하고 프레임이 유지가 되면서 잘 된다.
근본적으로 여기까지 오기까지의 단계를 다 이해하는게 중요한게 아니라
드로우콜을 줄이기 위해서 이 인스턴싱이란게 무엇을 하는 것인지를 이해하는 게 중요하다.
6. 결론
원래는 물체를 그리기 위해 렌더링 파이프라인 단계를 다 세팅해주고 mesh, material 세팅하고 물체를 그려주세요가 draw라는 함수를 이용해서 하는 것을 1만번 했었는데
Instancing 버전에서는 모든 애들의 좌표들을 인스턴스 버퍼라는 거대한 버퍼를 하나 만들어 가지고 그거를 일단 밀어 넣기를 해줄 것이다. 그 부분은 복사가 들어가긴 하지만
전체의 공장의 컨베이어 벨트 같은 느낌은 동일한 방식으로 작업을 할 것이기 때문에 쉐이더, 메시, 마테리알 다 똑같기 때문에 거기에 거대한 화물로 모든 정보들을 가지고 와서 모든 정보들을 하나씩 넣어 줘서 모든 애들을 한번에 그린다고 보면 된다.
여기서 만개의 물체를 그리기 위해 호출한 draw함수의 횟수는 프레임당 1회다.
원래는 1만번이었다.
물론 각각의 좌표를 복사하는 부분이 들어가지만 그건 하찮은 것이다.
원래 GPU에서 결국에는 렌더링 파이프라인 단계에서 Material 수정하고, Shader 수정하면 그와 관련된 코드들이 컴파일된 코드가 밀어져서 세팅이 되고, 공장을 다시 지어야 한다.
공장을 다시 짓는 거 보다는 라인은 유지한채로 물체들만 바꿔서 찍으면 훨씬 빠르다.
핵심은 이거였다.
인스턴싱은 결국
shared_ptr<VertexBuffer> _instanceBuffer;
거대한 버퍼를 만들어 주고,
_instanceBuffer->PushData();
세팅해주고
_shader->DrawIndexedInstanced(0, 0, _mesh->GetIndexBuffer()->GetCount(), _objs.size());
그 다음에 다른 함수를 호출해주면 끝이구나 라고 생각할 수 있다 .
문제는 지금까지 만든 코드들이랑 호환이 전혀 되지가 않된다는 것이다.
지금까지는 한 물체를 만들고 한 물체 만들고 이런 식으로 작업을 했기 때문에
실질적으로 코드들을 MeshRenderer에 어떻게 넣어줄지가 고민이다.
설계적인 부분.
MeshRenderer, ModelRenderer, ModelAnimator 이렇게 3가지가 그리는 역할을 해주는데, 여기서 고민인거는 자기 자신에 대한 정보만 그리고 있다. 이제는 나랑 같은 모든 아이들에 대한 정보를 내가 담당해서 그려야 된다고 코드가 수정이 되어야 하는데, 그 부분을 어떻게 만들까가 큰 고민이다.
다음 시간 부터 고칠 부분이다.
'DirectX' 카테고리의 다른 글
68_인스턴싱_ModelRenderer(인스턴싱) (0) | 2024.03.16 |
---|---|
67_인스턴싱_MeshRenderer(인스턴싱) (0) | 2024.03.14 |
65. 애니메이션_SkyBox (0) | 2024.03.10 |
64. 애니메이션_애니메이션#4_tweening (0) | 2024.03.08 |
63. 애니메이션_애니메이션#3_shader에서 애니메이션 행렬로 연산, ImGUI로 테스트, 보간 (0) | 2024.03.08 |
댓글