GPU Instancing으로 잔디·폴리지 최적화하기: 드로우 콜과 오버드로우 줄이는 실전 가이드
GPU Instancing으로 반복 잔디와 폴리지의 드로우 콜을 줄이고 알파 클리핑·LOD·컬링으로 병목인 오버드로우까지 제어하는 Unity·Unreal 실전 최적화 방법을 정리합니다.

GPU Instancing은 같은 메시와 머티리얼을 공유하는 잔디·수풀 인스턴스를 한 번의 드로우 호출로 묶어 CPU 제출 비용을 낮추는 기법이다. 다만 폴리지의 실제 병목은 드로우 콜만이 아니라 알파 클리핑, 화면을 겹쳐 그리는 오버드로우, 그림자와 LOD 전환에 함께 발생한다. 따라서 인스턴싱을 기본으로 적용한 뒤 거리 기반 LOD, 컬링, 단순화된 셰이더를 결합해야 안정적인 성능을 얻는다.
GPU Instancing은 잔디와 폴리지 최적화에 왜 효과적인가?
잔디 한 포기나 나뭇잎 뭉치는 보통 같은 메시를 반복 배치한다. 인스턴싱 없이 각각을 렌더링하면 엔진은 개체마다 드로우 콜과 렌더 상태 제출을 수행해야 한다. 반면 GPU Instancing은 메시와 머티리얼을 한 번 바인딩하고 인스턴스별 월드 변환 행렬·색상·바람 위상 같은 데이터만 인스턴스 버퍼로 전달한다.
인스턴스 수를 , 인스턴싱으로 한 번에 처리하는 묶음 크기를 라고 하면 이상적인 드로우 콜 수는 대략 다음과 같이 줄어든다.
실제 수치는 렌더 파이프라인, 그림자 패스, LOD, 서로 다른 머티리얼 수에 따라 달라진다. 핵심은 동일한 렌더 상태를 유지할수록 묶음 효율이 높아진다는 점이다.
| 항목 | 개별 렌더링 | GPU Instancing |
|---|---|---|
| 메시 제출 | 오브젝트별 반복 | 메시 1개를 공유 |
| 변환 데이터 | 오브젝트 단위 | 인스턴스 버퍼 |
| CPU 드로우 콜 비용 | 인스턴스 수에 비례 | 배치 수에 비례 |
| 잘 맞는 대상 | 고유 메시가 많은 오브젝트 | 반복되는 잔디, 돌, 수풀, 나무 |
| 해결하지 못하는 문제 | - | 픽셀 오버드로우, 무거운 그림자, 과도한 폴리곤 |

폴리지 성능 저하는 드로우 콜만으로 설명되지 않는 이유는?
카메라 가까이의 잔디는 화면의 같은 픽셀을 여러 장 겹쳐 그리기 쉽다. 특히 알파 블렌딩 잎사귀는 깊이 쓰기가 제한되므로 뒤쪽 픽셀까지 반복 음영 계산하게 된다. 이 현상이 오버드로우이며 GPU 픽셀 처리 시간이 높은 장면에서는 인스턴싱만 적용해도 프레임 시간이 거의 줄지 않을 수 있다.
알파 클리핑과 알파 블렌딩은 어떻게 선택할까?
풀잎과 잎사귀처럼 불투명한 형태를 마스크 텍스처로 잘라내는 경우에는 가능한 한 알파 클리핑을 우선 검토한다. 알파 클리핑은 깊이 버퍼에 기록할 수 있어 불필요한 후면 픽셀 처리를 줄이는 데 유리하다. 대신 경계가 거칠어질 수 있으므로 거리 기반 디더링과 TAA 사용 여부를 함께 확인해야 한다.
| 방식 | 장점 | 주의점 | 권장 대상 |
|---|---|---|---|
| Alpha Clip / Mask | 깊이 기록 가능, 정렬 부담이 적음 | 클리핑 비용과 계단 현상 | 잔디, 불투명 잎사귀 |
| Alpha Blend | 부드러운 반투명 표현 | 정렬과 오버드로우 비용이 큼 | 안개, 얇은 반투명 효과 |
| Opaque 카드 메시 | 가장 빠른 픽셀 처리 가능 | 실루엣 품질 제한 | 원거리 폴리지, 임포스터 |
셰이더에서 알파 클리핑 임계값을 과도하게 낮추면 작은 반투명 영역도 광범위하게 음영 처리한다. 잎 카드의 빈 공간을 줄인 텍스처 아틀라스와 타이트한 메시 윤곽이 오버드로우 절감에 직접적으로 도움이 된다.
GPU Instancing 잔디를 어떻게 구현할까?
다음 순서로 시작하면 과도한 시스템을 만들지 않고 병목을 분리할 수 있다.
- 공유 리소스를 고정한다. 같은 잔디 유형은 동일한 Mesh와 Material을 사용한다. 머티리얼 인스턴스나 키워드 조합이 달라지면 별도 배치가 된다.
- 인스턴스 데이터만 바꾼다. 위치, 회전, 스케일과 바람 위상은 행렬 또는 인스턴스 속성으로 전달한다. 개체마다 별도 머티리얼을 만들지 않는다.
- 거리와 화면 점유율로 줄인다. 가까운 영역만 풀 메시를 그리고 중거리에는 단순 메시, 원거리에는 임포스터 또는 제거 규칙을 적용한다.
Unity에서는 Mesh Renderer의 Enable GPU Instancing 옵션을 켜거나 Graphics.DrawMeshInstanced를 사용할 수 있다. 다음 예시는 동일한 잔디 메시를 행렬 배열로 나누어 제출하는 가장 작은 형태다.
using System.Collections.Generic;
using UnityEngine;
public sealed class GrassInstancer : MonoBehaviour
{
[SerializeField] Mesh grassMesh;
[SerializeField] Material grassMaterial;
[SerializeField] List<Matrix4x4> matrices = new List<Matrix4x4>();
private readonly Matrix4x4[] batch = new Matrix4x4[1023];
void Awake()
{
grassMaterial.enableInstancing = true;
}
void Update()
{
for (var start = 0; start < matrices.Count; start += batch.Length)
{
var count = Mathf.Min(batch.Length, matrices.Count - start);
matrices.CopyTo(start, batch, 0, count);
Graphics.DrawMeshInstanced(
grassMesh, 0, grassMaterial, batch, count);
}
}
}
Graphics.DrawMeshInstanced는 호출당 최대 1023개 행렬을 받으므로 큰 필드는 여러 호출로 나뉜다. 수십만 개 규모에서는 CPU에서 행렬 목록을 만들고 전송하는 비용도 다시 병목이 될 수 있다. 이때는 Unity의 Graphics.RenderMeshIndirect 또는 엔진의 GPU-driven 렌더링 기능처럼 Compute Shader와 간접 드로우를 사용하는 경로를 검토한다.
Unreal Engine에서는 Foliage Tool, InstancedStaticMeshComponent(ISM), HierarchicalInstancedStaticMeshComponent(HISM)가 반복 메시 렌더링을 위한 기본 선택지다. 넓은 지형의 폴리지는 HISM의 계층 컬링이 유리할 수 있지만 실제 선택은 인스턴스 분포와 프로파일링 결과로 결정해야 한다.
셰이더에서 인스턴스별 바람을 어떻게 다르게 보일까?
모든 잔디가 같은 시간값으로 흔들리면 하나의 판처럼 움직인다. 인스턴스 위치 또는 인스턴스 ID로 만든 위상을 사인파에 더하면 별도 애니메이션 데이터 없이 움직임을 분산할 수 있다.
Phase에는 월드 좌표의 해시값이나 인스턴스별 난수를 사용한다. 버텍스 높이를 가중치로 사용하면 뿌리는 고정하고 끝부분만 크게 흔들 수 있다.
float Hash21(float2 p)
{
return frac(sin(dot(p, float2(12.9898, 78.233))) * 43758.5453);
}
float3 ApplyWind(float3 positionOS, float3 positionWS, float time)
{
float phase = Hash21(floor(positionWS.xz));
float tipWeight = saturate(positionOS.y);
float sway = sin(time * 1.8 + phase * 6.2831853) * 0.08 * tipWeight;
positionOS.x += sway;
return positionOS;
}
이 코드는 시각적 다양성을 위한 최소 예시다. 강도, 속도, 방향을 머티리얼 파라미터로 두되 인스턴스마다 머티리얼을 복제하지 않아야 인스턴싱 배치가 유지된다.

잔디 LOD와 컬링은 어떤 기준으로 설정해야 할까?
거리만으로 LOD를 정하면 FOV와 해상도 변화에 취약하다. 가능하면 화면에서 차지하는 크기와 프로파일링 결과를 함께 보고 기준을 정한다. 가까운 잔디는 실루엣과 바람을 유지하고 멀어질수록 카드 수와 버텍스 수를 줄이며 최종 단계에서는 페이드 아웃 또는 임포스터로 전환한다.
flowchart LR
A[카메라와 폴리지 거리 계산] --> B{컬링 거리 이내인가?}
B -- 아니오 --> C[렌더링 제외]
B -- 예 --> D{LOD 구간 선택}
D --> E[Near: 풀 메시와 바람]
D --> F[Mid: 단순 메시]
D --> G[Far: 임포스터 또는 제거]
그림자도 별도 패스에서 폴리지를 다시 렌더링하므로 메인 패스만 최적화하면 부족하다. 원거리 잔디의 그림자 거리, 캐스팅 여부, 그림자용 LOD를 별도로 조절한다. 작은 잔디가 화면에서 보이지 않는 거리까지 그림자를 만드는 설정은 비용 대비 효과가 낮다.
폴리지 GPU 최적화를 검증하는 체크리스트
- 프레임 디버거로 잔디가 실제로 인스턴싱 배치되는지 확인한다.
- CPU-bound인지 GPU-bound인지 먼저 구분한다. CPU-bound라면 배치와 제출 횟수를 GPU-bound라면 오버드로우·그림자·셰이더를 우선 확인한다.
- 같은 잔디 유형에 서로 다른 머티리얼, 셰이더 키워드, 라이트맵 상태가 섞이지 않았는지 점검한다.
- 컬링 거리 밖 인스턴스가 그림자 패스에서 계속 렌더링되지 않는지 확인한다.
- LOD 전환 구간에서 팝핑과 디더링 비용을 실제 타깃 해상도에서 비교한다.
- 모바일이나 저사양 GPU에서는 텍스처 샘플 수, 투명 픽셀 비율, 그림자 품질을 별도 예산으로 관리한다.
자주 묻는 질문 (FAQ)
GPU Instancing을 켜면 모든 잔디 장면이 빨라지나요?
아니다. CPU 드로우 콜이 병목일 때 가장 효과가 크다. 알파 블렌딩 오버드로우나 그림자가 GPU 병목이면 메시 인스턴싱만으로는 충분하지 않다.
SRP Batcher와 GPU Instancing은 같은 기능인가요?
아니다. SRP Batcher는 호환되는 셰이더의 CPU 렌더 상태 설정 비용을 줄이는 기능이고 GPU Instancing은 같은 메시를 여러 변환으로 한 번에 그리는 기능이다. 프로젝트의 렌더 파이프라인과 셰이더 구조에 따라 둘의 적용 효과를 개별 프로파일링해야 한다.
폴리지마다 다른 색을 주면 인스턴싱이 깨지나요?
별도 머티리얼을 만들면 배치가 나뉜다. 인스턴스 색상 데이터나 버텍스 컬러, 위치 기반 색 변화처럼 공유 머티리얼 안에서 처리하면 인스턴싱을 유지할 수 있다.
정리
잔디와 폴리지 최적화의 출발점은 공유 Mesh·Material 기반의 GPU Instancing이다. 이후에는 알파 클리핑으로 오버드로우를 관리하고 LOD·거리 컬링·그림자 범위를 함께 줄여야 한다. 최종 설정은 드로우 콜 수만 보지 말고 CPU 시간, GPU 시간, 픽셀 오버드로우를 같은 장면에서 측정해 결정하는 것이 가장 안전하다.



