MeshRenderer (인스턴싱)

Jaemyeong Lee·2025년 4월 2일

1️⃣ InstancingBuffer – 인스턴스 데이터를 담는 전용 버퍼 클래스

매 프레임마다 동일한 메쉬와 머티리얼을 가진 오브젝트들을 하나의 버퍼에 묶어 GPU에 전달해야 한다. 이 역할을 하는 클래스가 InstancingBuffer다.

핵심 구조

struct InstancingData
{
    Matrix world; // 인스턴스 개별의 월드 행렬
};

class InstancingBuffer
{
public:
    void ClearData();              // 프레임마다 초기화
    void AddData(InstancingData&); // 오브젝트 world 추가
    void PushData();               // GPU에 Map/Unmap 후 전송

private:
    vector<InstancingData> _data;
    shared_ptr<VertexBuffer> _instanceBuffer;
    uint32 _maxCount;
};
  • CreateBuffer: 최대 인스턴스 수 기준으로 CPU write가 가능한 버퍼 생성
  • PushData: Map → memcpy → Unmap → GPU 전송

사용 이유

이 Buffer 하나만 잘 만들면, 다수의 오브젝트를 대표 1명이 GPU에 한 번에 전송 가능하다.


2️⃣ InstancingManager – 오브젝트들을 모아서 대표가 그리게 만드는 컨트롤러

기존에는 각각의 GameObject가 알아서 렌더링을 호출했지만, 인스턴싱에서는 대표 1명이 호출해야 한다. 이를 위해 InstancingManager가 등장한다.

역할

  • Render(vector<GameObject>) 함수 하나로 모든 인스턴싱 관리
  • 같은 Mesh + Material을 가진 오브젝트들을 묶고, 대표를 정해 그리게 한다

내부 흐름

void InstancingManager::RenderMeshRenderer(vector<shared_ptr<GameObject>>& objs)
{
    map<InstanceID, vector<shared_ptr<GameObject>>> cache;

    // 1. InstanceID 기준으로 분류
    for (auto& obj : objs)
    {
        if (!obj->GetMeshRenderer()) continue;
        auto id = obj->GetMeshRenderer()->GetInstanceID();
        cache[id].push_back(obj);
    }

    // 2. 각각의 그룹 처리
    for (auto& [id, group] : cache)
    {
        for (auto& obj : group)
        {
            InstancingData data{ obj->GetTransform()->GetWorldMatrix() };
            AddData(id, data);
        }

        // 대표가 인스턴싱으로 렌더링
        group[0]->GetMeshRenderer()->RenderInstancing(_buffers[id]);
    }
}

3️⃣ MeshRenderer 변경 – 대표 한 명이 묶어서 렌더링

기존 문제점

기존 방식은 각 오브젝트가 자기 Update()에서 렌더링을 호출했기 때문에, 수천 개 오브젝트가 모두 Draw 호출을 반복하게 됨 → 성능 저하

개선: RenderInstancing(shared_ptr<InstancingBuffer>)

void MeshRenderer::RenderInstancing(shared_ptr<InstancingBuffer>& buffer)
{
    if (!_mesh || !_material) return;

    _material->Update(); // Light, Tex, Shader 세팅
    _mesh->GetVertexBuffer()->PushData();
    _mesh->GetIndexBuffer()->PushData();
    buffer->PushData(); // 인스턴스 데이터 전송

    _material->GetShader()->DrawIndexedInstanced(0, _pass,
        _mesh->GetIndexBuffer()->GetCount(), buffer->GetCount());
}

그리고 기존 Update()는 제거. InstancingManager가 호출하는 방식으로 완전히 변경.


4️⃣ 실제 적용: MeshInstancingDemo 예제

void MeshInstancingDemo::Update()
{
    _camera->Update();
    RENDER->Update();

    // 조명 세팅
    LightDesc light;
    light.ambient = Vec4(0.4f);
    light.diffuse = Vec4(1.f);
    light.specular = Vec4(0.1f);
    light.direction = Vec3(1.f, 0.f, 1.f);
    RENDER->PushLightData(light);

    // 모든 오브젝트를 InstancingManager에 위임
    INSTANCING->Render(_objs);
}

기존에 _mesh, _instanceBuffer, _worlds 등을 사용하던 복잡한 코드는 모두 사라졌고, 관리 책임은 전적으로 InstancingManager로 이전.


✅ 인스턴싱 렌더링 흐름

GameObjects → InstancingManager::Render
        ↓
분류 (Mesh + Material 기준)
        ↓
InstancingBuffer에 world 정보 저장
        ↓
대표 하나가 RenderInstancing 호출
        ↓
DrawIndexedInstanced로 GPU 한방 렌더링!
  • 매 프레임마다 기존 데이터는 ClearData()로 리셋
  • 다시 모든 GameObject 순회하여 AddData()
  • MeshRenderer는 대표 1명만 DrawIndexedInstanced() 호출

💡 보충 : SV_InstanceID

향후 애니메이션과 같은 기능에서 각 인스턴스의 ID가 필요할 수 있다면 VS_IN에 다음과 같이 추가한다.

uint instanceID : SV_InstanceID;

이 값은 쉐이더에서 현재 인스턴스가 몇 번째인지 알려주는 값이며, 예를 들어 애니메이션 배열에서 자신의 애니메이션 데이터를 가져올 때 유용하게 쓰인다.


profile
李家네_공부방

0개의 댓글