ModelRenderer (인스턴싱)

Jaemyeong Lee·2025년 4월 2일

✅ 배경: Mesh → Model → Animation 순으로 확장

이전에 학습한 Mesh Instancing은 구나 큐브처럼 하나의 메시로 구성된 오브젝트를 효율적으로 렌더링하는 방식이었습니다. 이번엔 FBX로 임포트한 복잡한 모델을 같은 방식으로 최적화합니다. 이 모델은 여러 개의 메시와 뼈(Bone) 구조를 가지고 있으며, Model, ModelRenderer, ModelAnimator로 구성됩니다.


🏗️ 1. ModelInstancingDemo 클래스 구현

먼저 MeshInstancingDemo를 복사해 ModelInstancingDemo 클래스를 만들고, Client/Game 필터에 배치합니다.
이 클래스는 500개의 타워 모델을 인스턴싱 방식으로 렌더링합니다.

💡 주요 코드 구성

// 모델 로드
shared_ptr<Model> model = make_shared<Model>();
model->ReadModel(L"Tower/Tower");
model->ReadMaterial(L"Tower/Tower");

// 게임 오브젝트 생성 및 모델 할당
for (int i = 0; i < 500; ++i)
{
    auto obj = make_shared<GameObject>();
    obj->GetOrAddTransform()->SetPosition(Vec3(rand() % 100, 0, rand() % 100));
    obj->GetOrAddTransform()->SetScale(Vec3(0.01f)); // 타워 모델 크기 조절
    obj->AddComponent(make_shared<ModelRenderer>(_shader));
    obj->GetModelRenderer()->SetModel(model);
    _objs.push_back(obj);
}

ModelRenderer를 통해 모델을 적용하고, 동일한 모델을 수백 개 복제해 관리합니다.


✨ 2. 쉐이더 구성 – ModelInstancingDemo.fx

인스턴싱 전용 쉐이더로 21. ModelInstancingDemo.fx를 새로 작성합니다. 이 쉐이더는 다음을 처리합니다:

  • 본(Bone) 변환
  • 인스턴스별 월드 행렬 적용
  • 광원 처리 및 UV 맵핑
cbuffer BoneBuffer
{
    matrix BoneTransforms[MAX_MODEL_TRANSFORMS];
};

uint BoneIndex;

struct VS_IN
{
    float4 position : POSITION;
    float2 uv : TEXCOORD;
    float3 normal : NORMAL;
    float4 blendIndices : BLEND_INDICES;
    float4 blendWeights : BLEND_WEIGHTS;
    uint instanceID : SV_InstanceID;
    matrix world : INST;
};

VS_OUT VS(VS_IN input)
{
    VS_OUT output;

    output.position = mul(input.position, BoneTransforms[BoneIndex]);
    output.position = mul(output.position, input.world); // 인스턴스 변환
    output.position = mul(output.position, VP); // 뷰-프로젝션
    output.uv = input.uv;
    output.normal = input.normal;

    return output;
}

핵심은 BoneIndex를 기반으로 각 메시의 본 변환을 적용한 후, 인스턴스별 world 행렬을 곱해 최종 좌표를 얻는 것입니다.


🧠 3. InstancingManager 확장 – ModelRenderer 지원

기존 MeshRenderer 전용이던 InstancingManagerModelRenderer용 처리를 추가합니다.

🔧 구조 변경 요약

  • RenderModelRenderer() 함수 추가
  • ModelRenderer 객체 분류 → InstanceID 기준 캐싱
  • RenderInstancing() 호출 → 본 데이터 처리 + 메시 렌더링
InstanceID ModelRenderer::GetInstanceID()
{
    return make_pair((uint64)_model.get(), (uint64)_shader.get());
}

모델 주소와 쉐이더 주소를 기준으로 같은 인스턴스를 묶어 렌더링합니다.


🖼️ 4. ModelRenderer::RenderInstancing 구현

이제 본격적으로 ModelRenderer에서 인스턴싱 렌더링을 수행합니다.

핵심 처리 흐름

  1. 모델의 본(Bone) 정보를 BoneBuffer에 푸시
  2. 각 메시마다:
    • 머티리얼 업데이트
    • BoneIndex 설정
    • 정점 & 인덱스 버퍼 푸시
    • 인스턴싱 버퍼 푸시
    • DrawIndexedInstanced 호출
void ModelRenderer::RenderInstancing(shared_ptr<InstancingBuffer>& buffer)
{
    if (_model == nullptr)
        return;

    // Bone 데이터 푸시
    BoneDesc boneDesc;
    for (uint32 i = 0; i < _model->GetBoneCount(); ++i)
    {
        auto bone = _model->GetBoneByIndex(i);
        boneDesc.transforms[i] = bone->transform;
    }
    RENDER->PushBoneData(boneDesc);

    // 메시 렌더링
    for (auto& mesh : _model->GetMeshes())
    {
        if (mesh->material)
            mesh->material->Update();

        _shader->GetScalar("BoneIndex")->SetInt(mesh->boneIndex);
        mesh->vertexBuffer->PushData();
        mesh->indexBuffer->PushData();
        buffer->PushData(); // World 행렬들
        _shader->DrawIndexedInstanced(0, _pass, mesh->indexBuffer->GetCount(), buffer->GetCount());
    }
}

⚠️ 성능 고려 사항

  • 모델 내부에 메시가 많을 경우, 하나의 인스턴스를 그릴 때도 메시 수만큼 드로우 콜이 발생합니다.

    • 예: 10개의 메시로 구성된 모델 → 1,000개 인스턴스 → 드로우콜 10개 1,000번 아님 → 10개 메시 1번 인스턴싱 호출
  • 메시를 합치는 방식(Mesh Combine)으로 모델을 하나의 메시로 구성하면 드로우콜을 더욱 줄일 수 있습니다.


🔍 추가

  • SRT(Scale, Rotation, Translation)이 모두 포함된 world 행렬 덕분에 회전, 크기 변화, 이동 모두 문제없이 인스턴싱 가능합니다.
  • 머티리얼 설정 값이나 셰이더가 다르면 인스턴싱이 불가능하므로 주의해야 합니다.
  • 이후에 Frustum Culling, LOD, Batching 등을 적용하면 더 고성능 구현도 가능합니다.

profile
李家네_공부방

0개의 댓글