
GPU 인스턴싱으로 풀과 나무를 대량 렌더링하는 방법
폴리지를 수천 개 이상 배치할 때 드로우 콜과 개별 오브젝트 비용을 줄이는 GPU 인스턴싱의 원리와 구현 전략을 정리한다. Unity와 Unreal에서 확인할 지점, 셰이더 데이터 설계, 거리 기반 LOD와 컬링까지 실무적으로 설명한다.
왜 폴리지는 렌더링 비용이 커질까
풀, 관목, 나무는 화면을 채우기 위해 같은 메시를 매우 많이 반복한다. 문제는 삼각형 수만이 아니다. 각 오브젝트를 개별 렌더러로 관리하면 CPU가 드로우 콜을 준비하고 상태를 전환하며 컬링 결과를 제출하는 비용이 빠르게 커진다.
GPU 인스턴싱은 같은 메시와 같은 머티리얼을 공유하는 여러 개체를 한 번 또는 적은 횟수의 드로우 호출로 제출한다. GPU는 인스턴스마다 다른 변환 행렬, 색상, 바람 세기 같은 데이터를 읽어 같은 셰이더를 반복 실행한다.

다만 인스턴싱은 모든 폴리지를 하나의 호출로 마법처럼 해결하지 않는다. 메시, 머티리얼, 렌더 상태가 달라지면 배치가 나뉜다. 그림자 패스, 깊이 패스, 투명 패스도 별도 비용을 갖는다.
인스턴싱이 처리하는 데이터 흐름
인스턴스마다 최소한 월드 변환 행렬이 필요하다. 폴리지에서는 여기에 색상 변화, 스케일, 랜덤 시드, 바람 위상, LOD 선택값을 더하는 경우가 많다.
flowchart LR
A[폴리지 배치 데이터] --> B[가시 인스턴스 선별]
B --> C[인스턴스 버퍼]
C --> D[인스턴싱 드로우 호출]
D --> E[버텍스 셰이더]
E --> F[픽셀 셰이더]
CPU 기반 방식에서는 CPU가 프러스텀 컬링 후 보이는 인스턴스의 행렬을 업로드한다. 인스턴스 수가 매우 많다면 컴퓨트 셰이더에서 컬링하고 간접 드로우를 실행하는 GPU 기반 방식이 더 적합할 수 있다. 하지만 데이터 버퍼와 동기화, 디버깅 난도가 올라가므로 먼저 일반 인스턴싱과 거리 기반 LOD를 제대로 구성하는 편이 좋다.
배치가 깨지는 대표 원인
인스턴싱은 공유 조건이 중요하다. 다음 요소가 달라지면 별도 배치가 되거나 인스턴싱을 사용할 수 없다.
- 메시 또는 서브메시 구성
- 머티리얼과 셰이더 변형 키워드
- 렌더 큐와 블렌드 상태
- 라이트맵 인덱스나 일부 정적 조명 데이터
- 그림자 설정과 머티리얼별 렌더링 옵션
특히 풀마다 서로 다른 머티리얼을 만들면 인스턴싱의 이점이 사라진다. 색상이나 바람 차이는 머티리얼 복제 대신 인스턴스 속성이나 텍스처의 마스크 값으로 전달한다.
셰이더에서 인스턴스별 차이 만들기
아래 HLSL 예시는 인스턴스 ID로 색조와 바람 위상을 다르게 만드는 핵심 형태다. 실제 엔진에서는 인스턴스 데이터 선언 방식이 다르므로 해당 엔진의 매크로나 버퍼 API에 맞춰 연결해야 한다.
StructuredBuffer<float4x4> instanceWorld;
StructuredBuffer<float4> instanceParams;
float Hash01(uint value)
{
value ^= value >> 16;
value *= 0x7feb352d;
value ^= value >> 15;
return frac(value * 0.00000011920929);
}
VertexOutput Vert(VertexInput input, uint instanceId : SV_InstanceID)
{
VertexOutput output;
float4 param = instanceParams[instanceId];
float phase = param.x + _Time.y * param.y;
float sway = sin(phase + input.position.y * 1.8) * param.z;
float3 localPos = input.position;
localPos.x += sway * input.color.a;
float4 worldPos = mul(instanceWorld[instanceId], float4(localPos, 1.0));
output.positionCS = mul(_ViewProjection, worldPos);
float tint = lerp(0.85, 1.15, Hash01(instanceId));
output.color = float3(tint, tint, tint);
return output;
}
여기서 버텍스 색상 알파는 풀잎의 뿌리에서 흔들림을 줄이고 끝부분에서 키우는 마스크로 쓸 수 있다. 모든 정점이 같은 폭으로 움직이면 풀이 통째로 미끄러져 보이므로 높이 기반 마스크가 중요하다.

거리 기반 LOD와 컬링을 함께 설계하기
인스턴싱만 적용하고 먼 거리의 풀까지 높은 밀도로 그리면 픽셀 처리와 오버드로우가 병목이 된다. 폴리지는 보통 다음 단계를 함께 둔다.
가까운 거리: 개별 형태 유지
카메라 가까이에서는 실제 풀 메시나 카드 묶음을 사용한다. 알파 클리핑을 쓴다면 임계값이 너무 낮아 불필요한 픽셀이 많이 살아남지 않도록 점검한다.
중간 거리: 밀도와 메시 단순화
더 단순한 메시로 바꾸고 인스턴스 밀도를 낮춘다. LOD 전환 시 패턴이 갑자기 바뀌지 않도록 디더링 전환이나 짧은 페이드 구간을 고려할 수 있다.
먼 거리: 제거 또는 집계
아주 먼 풀은 제거하거나 지형 컬러, 임포스터, 머티리얼 디테일로 집계한다. 개별 풀 메시를 수평선까지 유지하는 것은 대개 비용 대비 효과가 낮다.
화면 공간 크기를 기준으로 LOD를 정하면 해상도와 FOV 변화에 더 일관되게 대응할 수 있다. 원근 투영에서 대략적인 화면 점유 크기는 다음처럼 거리의 역수에 비례한다.
Unity에서 확인할 점
Unity에서는 메시와 머티리얼이 인스턴싱을 지원해야 하며 셰이더에서 인스턴스 속성을 올바르게 선언해야 한다. Graphics.DrawMeshInstanced는 배치당 인스턴스 수 제한이 있으므로 대량 배치에서는 여러 호출로 나누거나 Graphics.DrawMeshInstancedIndirect 및 Graphics.RenderMeshIndirect 계열을 검토한다.
URP나 HDRP를 사용할 때는 셰이더 그래프와 커스텀 셰이더가 해당 파이프라인의 인스턴싱 경로를 유지하는지 Frame Debugger로 확인한다. 머티리얼 프로퍼티 블록을 사용하더라도 인스턴싱 가능 데이터로 선언하지 않으면 기대와 다르게 배치가 분리될 수 있다.
Unreal Engine에서 확인할 점
Unreal Engine의 Foliage Tool과 Hierarchical Instanced Static Mesh는 반복 메시를 계층적으로 관리하고 컬링을 돕는 대표적인 경로다. 나무처럼 큰 오브젝트는 계층 인스턴싱과 LOD, 풀처럼 조밀한 오브젝트는 밀도 스케일과 컬링 거리를 함께 조절한다.
머티리얼에서는 Per Instance Random, Per Instance Custom Data 같은 값을 활용해 색상과 바람 위상을 달리할 수 있다. 다만 월드 포지션 오프셋을 크게 사용하면 바운드가 실제 움직임을 충분히 감싸지 못해 컬링 문제가 생길 수 있으므로 바운드와 그림자 결과를 함께 확인한다.
프로파일링 체크리스트
성능 개선 여부는 눈대중 대신 프레임 캡처로 확인한다.
- 드로우 콜 수와 CPU 렌더 스레드 시간을 확인한다.
- 깊이 패스와 그림자 패스까지 포함해 폴리지 비용을 비교한다.
- 가까운 풀에서 알파 클리핑과 오버드로우를 시각화한다.
- LOD 전환 거리와 컬링 거리에서 팝핑이 없는지 확인한다.
- 인스턴스 버퍼 업로드와 GPU 컬링 패스가 새 병목이 되지 않는지 확인한다.
마무리
GPU 인스턴싱은 반복되는 풀과 나무를 효율적으로 제출하는 기반 기술이다. 그러나 최종 성능은 인스턴싱 여부만으로 결정되지 않는다. 공유 가능한 메시와 머티리얼을 유지하고 인스턴스 데이터로 변화를 만들며 거리 기반 LOD와 컬링으로 보이지 않는 비용을 제거해야 한다. 이 순서로 설계하면 대규모 자연 환경에서도 품질과 프레임 시간을 함께 관리하기 쉬워진다.


