전체 코드 : https://github.com/HyangRim/DirectX11-Engine-Client
GPU 인스턴싱을 통한 렌더링 최적화
GPU 인스턴싱이란?
GPU 인스턴싱은 동일한 Mesh 와 Material을 사용하는 여러 오브젝트를 한 번의 드로우 콜로 렌더링하는 기술이다.
기존의 개별 렌더링 방식과 비교해 보면
기존 방식의 문제점
- 100개의 나무 → 100번의 드로우 콜
- CPU - GPU 간 통신 오버헤드 증가
- CPU 병목 현상 발생
GPU 인스턴싱의 장점
- 100개의 나무 → 1번의 드로우 콜
- GPU 활용도 극대화
- CPU 부하 감소
실제 구현
RenderManager에서의 인스턴싱
이번 프로젝트에서 구현한 RenderManager에서는 다음과 같이 동일한 모델들을 그룹화하여 인스턴싱을 처리한다.
void RenderManager::RenderMeshRendererForward(vector<shared_ptr<GameObject>>& _gameObjects)
{
map<InstanceID, vector<shared_ptr<GameObject>>> cache;
// 1. 분류 단계: 동일한 메시와 머티리얼별로 그룹화
for (shared_ptr<GameObject>& gameObject : _gameObjects) {
if (gameObject->GetMeshRenderer() == nullptr)
continue;
const InstanceID instanceID = gameObject->GetMeshRenderer()->GetInstanceID();
cache[instanceID].push_back(gameObject);
}
// 2. 그룹별 인스턴싱 렌더링
for (auto& pair : cache) {
const vector<shared_ptr<GameObject>>& vec = pair.second;
const InstanceID instanceID = pair.first;
// 각 인스턴스의 월드 변환 행렬 수집
for (int32 idx = 0; idx < vec.size(); ++idx) {
const shared_ptr<GameObject>& gameObject = vec[idx];
InstancingData data;
data.m_world = gameObject->GetTransform()->GetWorldMatrix();
AddData(instanceID, data);
}
// 인스턴싱 버퍼로 한 번에 렌더링
shared_ptr<InstancingBuffer>& buffer = m_buffers[instanceID];
vec[0]->GetMeshRenderer()->RenderInstancing(buffer, m_isShadowTech);
}
}
void RenderManager::AddData(InstanceID _instanceID, InstancingData& _data)
{
if (m_buffers.find(_instanceID) == m_buffers.end()) {
m_buffers[_instanceID] = make_shared<InstancingBuffer>();
}
m_buffers[_instanceID]->AddData(_data);
}
핵심 최적화 포인트
1. 그룹화
const InstanceID instanceID = gameObject->GetMeshRenderer()->GetInstanceID();
...
InstanceID ModelAnimator::GetInstanceID()
{
return make_pair((uint64)m_model.get(), (uint64)m_shader.get());
}
- InstanceID는 Mesh와 셰이더 포인터의 조합으로 생성
- 동일한 렌더링 상태를 가진 오브젝트들을 효율적으로 분류
2. 데이터 구조
struct InstancingData {
Matrix m_world;
};
- 각 인스턴스마다 필요한 최소한의 데이터만 전송
- GPU로의 데이터 전송량 최소화
애니메이션 인스턴싱의 최적화
void RenderManager::RenderAnimRendererForward(vector<shared_ptr<GameObject>>& _gameObjects)
{
map<InstanceID, vector<shared_ptr<GameObject>>> cache;
for (auto& pair : cache) {
const vector<shared_ptr<GameObject>>& vec = pair.second;
shared_ptr<InstancedTweenDesc> tweenDesc = make_shared<InstancedTweenDesc>();
for (int32 idx = 0; idx < vec.size(); ++idx) {
const shared_ptr<GameObject>& gameObject = vec[idx];
// 그림자 패스가 아닐 때만 애니메이션 업데이트
if (!m_isShadowTech) {
gameObject->GetModelAnimator()->UpdateTweenData();
}
// 각 인스턴스의 애니메이션 상태 수집
tweenDesc->tweens[idx] = gameObject->GetModelAnimator()->GetTweenDesc();
}
// 애니메이션 데이터를 GPU에 일괄 전송
vec[0]->GetModelAnimator()->GetShader()->PushTweenData(*tweenDesc.get());
shared_ptr<InstancingBuffer>& buffer = m_buffers[instanceID];
vec[0]->GetModelAnimator()->RenderInstancing(buffer, m_isShadowTech);
}
}
GPU 애니메이션 데이터 최적화
ModelAnimator에서는 애니메이션 데이터를 텍스쳐 형태로 GPU에 업로드하여 성능을 극대화
void ModelAnimator::CreateTexture()
{
uint32 maxFrameCount = 0;
for (const auto& pair : m_model->GetAnimations()) {
maxFrameCount = max(maxFrameCount, pair.second->m_frameCount);
CreateAnimationTransform(pair.first);
}
// 2D 텍스처 배열로 모든 애니메이션 데이터 저장
D3D11_TEXTURE2D_DESC desc;
desc.Width = MAX_BONE_TRANSFORMS * 4; // 행렬 4개 컬럼
desc.Height = maxFrameCount; // 프레임 수
desc.ArraySize = m_model->GetAnimationCount(); // 애니메이션 개수
desc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
// 모든 애니메이션 데이터를 한 번에 업로드
const uint32 dataSize = MAX_BONE_TRANSFORMS * sizeof(Matrix);
const uint32 pageSize = dataSize * maxFrameCount;
void* mallocPtr = ::malloc(pageSize * m_model->GetAnimationCount());
// ... 데이터 패킹 및 업로드
HRESULT hr = DEVICE->CreateTexture2D(&desc, subResources.data(), m_texture.GetAddressOf());
}
텍스쳐 기반 애니메이션의 장점
- 메모리 접근 패턴 최적화
- 텍스쳐 캐시 활용으로 빠른 데이터 접근
- GPU에 최적화된 메모리 레이아웃
- 병렬 처리 최적화
- 모든 Bone 변환을 병렬로 처리
- 셰이더에서 효율적인 행렬 연산
셰이더에서의 인스턴싱 구현
// 인스턴스별 데이터 구조
struct VertexModel
{
float4 position : POSITION;
float2 uv : TEXCOORD;
float3 normal : NORMAL;
float3 tangent : TANGENT;
float4 blendIndices : BLEND_INDICES;
float4 blendWeights : BLEND_WEIGHTS;
// 인스턴싱 데이터
uint instanceID : SV_INSTANCEID;
matrix world : INST;
};
// 애니메이션 변환 행렬 계산
matrix GetAnimationWorldMatrix(VertexModel input)
{
float indices[4] = { input.blendIndices.x, input.blendIndices.y, input.blendIndices.z, input.blendIndices.w };
float weights[4] = { input.blendWeights.x, input.blendWeights.y, input.blendWeights.z, input.blendWeights.w };
// 인스턴스별 애니메이션 상태 가져오기
int animIndex = TweenFrames[input.instanceID].curr.animIndex;
int currFrame = TweenFrames[input.instanceID].curr.currFrame;
int nextFrame = TweenFrames[input.instanceID].curr.nextFrame;
float ratio = TweenFrames[input.instanceID].curr.ratio;
matrix transform = 0;
// 각 본별 가중치 적용된 변환 계산
for (int i = 0; i < 4; i++) {
// 텍스처에서 본 변환 행렬 로드
float4 c0 = TransformMap.Load(int4(indices[i] * 4 + 0, currFrame, animIndex, 0));
float4 c1 = TransformMap.Load(int4(indices[i] * 4 + 1, currFrame, animIndex, 0));
float4 c2 = TransformMap.Load(int4(indices[i] * 4 + 2, currFrame, animIndex, 0));
float4 c3 = TransformMap.Load(int4(indices[i] * 4 + 3, currFrame, animIndex, 0));
matrix curr = matrix(c0, c1, c2, c3);
// 다음 프레임과 보간
// ... 보간 계산
transform += mul(weights[i], result);
}
return transform;
}
디퍼드 렌더링과의 결합
디퍼드 렌더링 파이프라인에서도 인스턴싱을 활용했음.
void RenderManager::RenderAnimRendererDeferred(vector<shared_ptr<GameObject>>& _gameObjects)
{
// 투명 객체는 제외하고 불투명 객체만 처리
for (shared_ptr<GameObject>& gameObject : _gameObjects) {
auto modelAnimator = gameObject->GetModelAnimator();
if (auto material = modelAnimator->GetMaterial()) {
if (material->IsTransparent())
continue; // 포워드 렌더링으로 별도 처리
}
const InstanceID instanceID = gameObject->GetModelAnimator()->GetInstanceID();
cache[instanceID].push_back(gameObject);
}
// G-Buffer에 데이터 쓰기
vec[0]->GetModelAnimator()->RenderInstancingDeferred(buffer, m_isShadowTech);
}
결론
GPU 인스턴싱은 현대 게임 개발에서 필수라고 볼 수 있다. 특히 다음과 같은 경우에는 매우 큰 효과를 얻을 수 있다.
- 대량의 동일 오브젝트가 많은 경우 ( 나무 , 풀 , 건물 등 )
- 반복적인 파티클 시스템
- 군중 시뮬레이션
'DirectX11 > Eternal Return 모작' 카테고리의 다른 글
| [DirectX 11 Eternal Return 모작] 5. Component (0) | 2025.09.02 |
|---|---|
| [DirectX 11 Eternal Return 모작] 4. Shader & Material (0) | 2025.09.02 |
| [DirectX 11 Eternal Return 모작] 2. FOW ( Fog Of War ) (0) | 2025.09.02 |
| [DirectX 11 Eternal Return 모작] 1. 디퍼드 렌더링 파이프라인 (0) | 2025.09.02 |
| [DirectX 11 Eternal Return 모작] 0. 렌더링 파이프라인 흐름 (5) | 2025.07.23 |