GPU Instancing으로 대규모 오브젝트 렌더링 최적화하기
GPU Instancing의 동작 원리와 적용 조건을 이해하고 Unity·Unreal에서 대량의 반복 오브젝트를 효율적으로 렌더링하는 방법을 살펴봅니다.

GPU Instancing이 필요한 이유
숲의 나무, 풀, 군중의 소품, 탄흔처럼 같은 메시를 수백~수천 개 그리는 장면에서는 드로우 콜이 빠르게 늘어난다. 드로우 콜마다 CPU는 메시와 머티리얼, 렌더 상태를 GPU에 전달해야 하므로 메시 자체가 단순해도 CPU가 병목이 될 수 있다.
GPU Instancing은 같은 메시와 같은 머티리얼을 공유하는 여러 오브젝트를 한 번의 드로우 콜 또는 적은 수의 드로우 콜로 묶어 그리는 기법이다. 각 인스턴스는 월드 변환 행렬, 색상, 애니메이션 단계 같은 개별 데이터를 가지면서도 메시 데이터와 셰이더 실행 경로를 공유한다.
인스턴싱은 주로 드로우 콜 수를 줄여 이 비용을 낮춘다. 다만 모든 장면에서 자동으로 빨라지는 기능은 아니다. 이미 GPU가 정점 처리나 픽셀 처리로 포화된 상황이라면 인스턴싱만으로 프레임 시간이 크게 줄지 않을 수 있다.
인스턴싱의 렌더링 흐름
일반 렌더링에서는 같은 바위 메시가 1,000개 있어도 오브젝트마다 렌더 명령을 제출하는 경우가 많다. 인스턴싱에서는 공용 메시와 머티리얼을 한 번 지정하고 인스턴스별 데이터를 배열이나 GPU 버퍼로 전달한다.
flowchart LR
A[공용 메시와 머티리얼] --> C[인스턴스 배치]
B[변환 행렬과 개별 속성] --> C
C --> D[인스턴스 버퍼]
D --> E[인스턴스 드로우 콜]
E --> F[GPU가 각 인스턴스 렌더링]
셰이더는 현재 처리 중인 인스턴스 번호를 이용해 변환 행렬과 개별 속성을 읽는다. 따라서 같은 큐브 메시를 사용해도 인스턴스마다 위치, 회전, 크기, 색상을 다르게 표현할 수 있다.

적용 조건과 배치가 깨지는 이유
인스턴싱의 기본 조건은 메시와 머티리얼의 공유다. 엔진과 렌더 파이프라인에 따라 세부 조건은 다르지만 아래 항목이 달라지면 보통 별도 배치가 필요하다.
- 다른 메시 또는 다른 서브메시를 사용한다.
- 다른 머티리얼이나 셰이더 변형을 사용한다.
- 렌더 큐, 블렌드 모드, 깊이 상태, 그림자 설정이 다르다.
- 개별 머티리얼 인스턴스처럼 배치를 분리하는 방식으로 값을 바꾼다.
- 투명 오브젝트가 거리순 정렬을 요구한다.
특히 투명 오브젝트는 주의해야 한다. 일반적인 알파 블렌딩은 뒤쪽 물체부터 그려야 하므로 화면 전체에서 순서를 섞어 정렬해야 하는 인스턴스 집합은 배치 효율이 낮아질 수 있다. 가능하다면 불투명 또는 알파 클리핑 방식을 우선 검토하고 투명 효과는 별도 패스로 관리하는 편이 안전하다.
인스턴스별 데이터 설계
인스턴스마다 모두 다른 머티리얼을 만들면 인스턴싱의 장점이 사라진다. 색조나 손상도처럼 작은 차이는 머티리얼을 복제하지 말고 인스턴스 데이터로 전달한다.
예를 들어 나무에 약간의 색상 변화를 주려면 인스턴스별 색상과 바람 위상을 저장할 수 있다.
struct InstanceData
{
float4x4 worldMatrix;
float4 colorTint;
float windPhase;
float3 padding;
};
구조체의 실제 메모리 정렬은 사용하는 그래픽 API와 셰이더 버퍼 형식에 맞춰 확인해야 한다. CPU 구조체의 크기와 셰이더가 기대하는 스트라이드가 다르면 색상이나 변환 데이터가 깨질 수 있다.
셰이더 쪽에서는 인스턴스 ID로 버퍼를 조회한다. HLSL의 개념적 예시는 다음과 같다.
StructuredBuffer<InstanceData> _Instances;
struct Attributes
{
float3 positionOS : POSITION;
uint instanceID : SV_InstanceID;
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float4 color : COLOR0;
};
Varyings Vert(Attributes input)
{
InstanceData instance = _Instances[input.instanceID];
Varyings output;
float4 positionWS = mul(instance.worldMatrix, float4(input.positionOS, 1.0));
output.positionCS = mul(UNITY_MATRIX_VP, positionWS);
output.color = instance.colorTint;
return output;
}
위 코드는 원리를 보여 주기 위한 예시다. 실제 프로젝트에서는 Unity의 SRP Batcher 및 셰이더 규약, Unreal의 인스턴스드 스태틱 메시 컴포넌트 등 사용하는 엔진의 데이터 경로를 따라 구현해야 한다.
Unity에서 사용하는 방법
Unity에서는 머티리얼에서 Enable GPU Instancing 옵션을 켜고 인스턴싱을 지원하는 셰이더를 사용하면 MeshRenderer 기반의 반복 오브젝트가 인스턴싱 대상이 될 수 있다. 실제 배치 결과는 머티리얼, 메시, 조명 및 렌더 파이프라인 설정에 따라 달라지므로 Frame Debugger로 확인하는 것이 좋다.
많은 인스턴스를 직접 관리해야 할 때는 Graphics.DrawMeshInstanced 또는 Graphics.DrawMeshInstancedIndirect 계열 API를 고려할 수 있다. 전자는 호출 한 번당 전달 가능한 행렬 수에 제한이 있으므로 수량이 매우 많을 때는 여러 번 나누어 호출해야 한다.
using UnityEngine;
public class RockInstancer : MonoBehaviour
{
[SerializeField] private Mesh rockMesh;
[SerializeField] private Material rockMaterial;
[SerializeField] private int count = 500;
private Matrix4x4[] matrices;
private void Start()
{
matrices = new Matrix4x4[count];
for (int i = 0; i < count; i++)
{
Vector3 position = new Vector3(
Random.Range(-50f, 50f),
0f,
Random.Range(-50f, 50f));
float scale = Random.Range(0.8f, 1.2f);
matrices[i] = Matrix4x4.TRS(position, Quaternion.identity, Vector3.one * scale);
}
}
private void Update()
{
Graphics.DrawMeshInstanced(rockMesh, 0, rockMaterial, matrices);
}
}
이 예제는 개념 확인에 적합하지만 매 프레임 변환 배열을 새로 만들거나 불필요하게 갱신하면 CPU 비용과 가비지 컬렉션 문제가 생길 수 있다. 정적인 배치는 한 번 생성해 재사용하고 움직이는 인스턴스만 갱신 범위를 좁히는 방식이 좋다.
또한 DrawMeshInstanced는 자동 컬링이 제한적일 수 있다. 넓은 월드에 수만 개를 배치한다면 공간을 셀이나 청크로 나누고 카메라 주변 청크만 제출해야 한다. 더 큰 규모에서는 Compute Shader로 가시 인스턴스를 선별한 뒤 Indirect Draw로 넘기는 구조를 검토할 수 있다.
Unreal Engine에서 사용하는 방법
Unreal Engine에서는 반복되는 정적 메시를 Instanced Static Mesh Component로 관리할 수 있다. 같은 스태틱 메시와 머티리얼을 공유하는 인스턴스를 하나의 컴포넌트에 추가하는 방식이다. 나무나 돌처럼 정적인 반복 배치에 적합하다.
인스턴스가 넓은 공간에 흩어져 있다면 Hierarchical Instanced Static Mesh Component가 유리한 경우가 많다. 계층 구조를 사용해 인스턴스 단위보다 큰 묶음으로 컬링을 수행하므로 화면에 보이지 않는 군집을 줄이는 데 도움이 된다.
UHierarchicalInstancedStaticMeshComponent* Trees = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("Trees"));
Trees->SetStaticMesh(TreeMesh);
for (const FTransform& Transform : TreeTransforms)
{
Trees->AddInstance(Transform);
}
개별 인스턴스의 색상이나 변형 값을 사용해야 한다면 엔진이 제공하는 Per Instance Custom Data를 활용할 수 있다. 값의 수와 셰이더 접근 방식을 먼저 설계하면 불필요한 머티리얼 분화를 줄일 수 있다.

인스턴싱과 컬링을 함께 설계하기
인스턴싱은 제출 횟수를 줄이지만 보이지 않는 인스턴스를 모두 그리면 GPU 작업은 여전히 남는다. 대규모 씬에서는 배치와 컬링을 분리해서 생각해야 한다.
- 가까운 공간 단위만 CPU 또는 엔진 컬링으로 제출한다.
- 제출된 인스턴스는 같은 메시와 머티리얼 기준으로 묶는다.
- 필요하면 GPU에서 프러스텀 컬링과 오클루전 컬링을 수행한다.
- 살아남은 인스턴스 수를 Indirect Draw 인자 버퍼에 기록한다.
- GPU가 가시 인스턴스만 드로우한다.
카메라에서 멀어질수록 LOD를 낮추는 전략도 중요하다. 다만 LOD 단계마다 메시가 달라지므로 서로 다른 LOD는 별도 인스턴싱 배치가 된다. 각 LOD에 충분한 인스턴스가 모이도록 거리 구간과 메시 구성을 조정해야 한다.
측정으로 효과 확인하기
최적화는 눈으로 판단하지 말고 병목을 확인한 뒤 적용해야 한다. 다음 순서로 측정하면 인스턴싱의 효과를 분명히 파악할 수 있다.
- CPU와 GPU 프레임 시간을 각각 확인한다.
- Frame Debugger, RenderDoc, Unreal Insights 같은 도구로 드로우 콜과 배치 분리 원인을 확인한다.
- 반복 오브젝트만 인스턴싱한 전후를 같은 카메라 위치와 해상도에서 비교한다.
- 가시 인스턴스 수, 삼각형 수, 픽셀 처리량도 함께 기록한다.
- 그림자 패스와 깊이 프리패스까지 포함한 전체 비용을 비교한다.
드로우 콜이 줄었는데 프레임 시간이 거의 변하지 않는다면 GPU 병목일 가능성이 있다. 이때는 메시 LOD, 그림자 거리, 오버드로우, 텍스처 대역폭, 복잡한 픽셀 셰이더를 함께 점검해야 한다.
실전 점검 목록
- 같은 메시와 머티리얼을 공유하는 반복 오브젝트인가?
- 인스턴스별 차이를 머티리얼 복제 대신 인스턴스 데이터로 표현할 수 있는가?
- 투명 정렬 때문에 배치가 분리되지 않는가?
- 넓은 월드에서 보이지 않는 인스턴스를 제출하고 있지 않은가?
- LOD와 그림자 패스에서도 인스턴싱 효과를 확인했는가?
- 프로파일러에서 CPU 제출 비용이 실제 병목으로 나타났는가?
GPU Instancing은 반복 오브젝트를 많이 배치하는 씬에서 강력한 기본 도구다. 핵심은 단순히 인스턴싱 옵션을 켜는 데 있지 않다. 공유 가능한 렌더 상태를 설계하고 인스턴스별 데이터를 작게 유지하며 컬링과 LOD를 함께 구성해야 대규모 렌더링에서 안정적인 성능 개선으로 이어진다.



