LYSC STUDIO

LIST
Cover

Unity 6 Entities Graphics와 GPU Driven Rendering: 모바일에서 10만 오브젝트를 그리는 법

·Unity Optimization

Unity 6의 Entities Graphics 패키지가 가져온 GPU Driven Rendering 파이프라인의 핵심 원리를 분석하고, 모바일 환경에서 수만~수십만 개의 오브젝트를 60fps로 렌더링하기 위한 실전 최적화 전략을 다룹니다.

기존 렌더링 파이프라인의 한계와 GPU Driven의 등장 배경

유니티의 전통적인 렌더링 파이프라인은 CPU가 모든 오브젝트의 가시성을 판단하고, 머티리얼별로 정렬한 뒤, 하나하나 GPU에 드로우콜(Draw Call)을 발행하는 구조였습니다. 이 방식은 오브젝트 수가 수천 개를 넘어가는 순간 CPU 바운드 병목이 급격히 심화됩니다. SRP Batcher와 GPU Instancing이 이 문제를 부분적으로 완화해 주었지만, 근본적으로 "CPU가 모든 렌더링 의사결정을 담당한다"는 구조적 한계에서 벗어나지 못했습니다. 특히 오픈월드 게임처럼 동적으로 로딩되는 수십만 개의 풀, 나무, 돌맹이, 건물 파편이 동시에 화면에 존재하는 시나리오에서는 CPU가 프레임마다 처리해야 할 오브젝트 목록 자체가 프레임 버짓을 초과하는 지경에 이릅니다.

GPU Driven Rendering은 이 패러다임을 180도 뒤집습니다. 오브젝트의 가시성 판정(Culling), 인스턴스 데이터 수집, 그리고 실제 드로우 명령 발행까지의 전 과정을 CPU가 아닌 GPU의 컴퓨트 셰이더(Compute Shader)가 수행합니다. CPU는 프레임 시작 시 "이번 프레임에 그릴 수 있는 전체 오브젝트 버퍼"만 GPU에 한 번 넘겨주면 되므로, 오브젝트가 1만 개든 100만 개든 CPU 비용이 거의 일정(O(1)에 가까운)하게 유지됩니다. Unity 6의 Entities Graphics 패키지는 바로 이 GPU Driven 아키텍처를 DOTS(Data-Oriented Technology Stack) 위에 네이티브로 구현한 것입니다.

Entities Graphics의 내부 아키텍처 해부

Entities Graphics의 핵심은 BatchRendererGroup(BRG) API입니다. BRG는 기존 Graphics.DrawMeshInstanced()의 한계를 극복하기 위해 설계된 저수준(Low-level) 렌더링 인터페이스입니다. 개발자가 직접 구조화된 버퍼(Structured Buffer)에 오브젝트별 트랜스폼 매트릭스, 컬러, 커스텀 프로퍼티 등을 빽빽하게 패킹하여 GPU 메모리에 업로드하면, BRG가 이 버퍼를 기반으로 간접 렌더링(Indirect Rendering)을 수행합니다. DrawMeshInstancedIndirect와 유사하지만, BRG는 SRP(Scriptable Render Pipeline)와 완벽히 통합되어 라이팅, 그림자, LOD 전환까지 자동으로 처리해 준다는 결정적 차이가 있습니다.

내부적으로 Entities Graphics는 ECS의 RenderMesh 컴포넌트를 가진 모든 엔티티를 메시+머티리얼 조합별로 자동 배칭(Batching)합니다. 같은 메시와 같은 머티리얼을 공유하는 엔티티 1만 개가 있다면, CPU는 단 1회의 드로우콜만 발행하고 GPU가 컴퓨트 셰이더를 통해 1만 개 인스턴스의 트랜스폼을 병렬로 처리합니다. 여기에 GPU 기반의 프러스텀 컬링(Frustum Culling)과 오클루전 컬링(Occlusion Culling)이 컴퓨트 패스에서 실행되어, 카메라에 보이지 않는 인스턴스는 실제 래스터라이제이션 단계에 도달하기 전에 GPU 내부에서 즉시 폐기됩니다.

모바일 환경에서의 실전 최적화 전략

데스크톱과 콘솔에서는 Entities Graphics가 그대로도 충분한 성능을 발휘하지만, 모바일 GPU는 대역폭(Bandwidth)과 셰이더 유닛 수에서 확연한 제약이 있으므로 추가적인 최적화 전략이 필수적입니다. 첫 번째는 LOD(Level of Detail) 그룹 구성의 극대화입니다. Entities Graphics는 LODGroup을 ECS 컴포넌트 수준에서 지원하므로, 각 엔티티에 3~4단계의 LOD 메시를 설정하면 GPU 컬링 패스에서 카메라 거리에 따라 자동으로 저폴리곤 메시로 교체됩니다. 원거리 오브젝트가 고해상도 메시로 래스터라이즈 되는 비효율을 차단하는 것만으로도 모바일에서의 오버드로우(Overdraw)를 40~60% 감소시킬 수 있습니다.

두 번째는 머티리얼 아틀라스(Material Atlas) 전략입니다. BRG의 배칭 효율은 "같은 머티리얼을 공유하는 인스턴스가 얼마나 많은가"에 직결됩니다. 따라서 환경 오브젝트(풀, 돌, 나무, 울타리 등)의 텍스처를 하나의 아틀라스 텍스처로 통합하고, 각 인스턴스가 UV 오프셋만 달리하여 자신의 영역을 샘플링하도록 구성하면, 수십 종류의 환경 오브젝트를 단일 드로우콜로 처리할 수 있습니다. 이 기법은 모바일 GPU의 상태 변경(State Change) 비용을 획기적으로 줄여줍니다.

세 번째는 인스턴스 데이터의 메모리 레이아웃 최적화입니다. Entities Graphics는 내부적으로 SoA(Structure of Arrays) 방식으로 인스턴스 데이터를 저장합니다. 이 방식은 GPU의 메모리 접근 패턴에 최적화되어 캐시 히트율을 극대화하지만, 개발자가 커스텀 머티리얼 프로퍼티를 추가할 때는 반드시 [MaterialProperty] 어트리뷰트로 정의하여 BRG의 데이터 스트리밍 파이프라인에 올바르게 편입시켜야 합니다. 임의로 MaterialPropertyBlock을 사용하면 BRG 배칭이 깨지면서 성능이 오히려 기존 방식보다 나빠질 수 있으므로, 이 점을 특히 주의해야 합니다.

DOTS 기반 시스템과의 시너지

Entities Graphics의 진정한 위력은 Unity DOTS 생태계 전체와 결합될 때 발현됩니다. ECS의 IJobEntitySystemAPI.Query를 활용하면, 수십만 개의 인스턴스가 단순히 정적으로 배치되는 것을 넘어서 각자 독립적으로 애니메이션하고 물리 시뮬레이션에 반응하는 것이 가능해집니다. 예를 들어, 바람에 흔들리는 풀밭을 구현할 때, 전통적 방식에서는 버텍스 셰이더에서 사인(sin) 함수로 흔들림을 시뮬레이션했습니다. 하지만 DOTS + Entities Graphics 조합에서는 Job System이 워커 스레드에서 10만 개 풀 인스턴스의 트랜스폼을 병렬로 갱신하고, 갱신된 매트릭스가 곧바로 GPU 버퍼에 반영되어 렌더링됩니다. CPU도 GPU도 유휴 시간 없이 파이프라인이 풀가동됩니다.

Burst Compiler와의 조합도 빼놓을 수 없습니다. Entities Graphics가 요구하는 대용량의 트랜스폼 연산(매트릭스 곱셈, 쿼터니언 회전 등)을 Burst로 컴파일하면, IL2CPP 대비 4~8배의 연산 속도 향상을 얻을 수 있습니다. 특히 모바일 기기의 ARM 프로세서에서 Burst가 생성하는 NEON SIMD 명령어는 부동소수점 매트릭스 연산을 네이티브 수준으로 가속하므로, "10만 오브젝트의 트랜스폼을 매 프레임 갱신"하는 시나리오에서도 CPU 프레임 타임이 2ms를 넘지 않는 수준까지 끌어내릴 수 있습니다.

Car Open World 프로젝트 적용 사례

LYSC 스튜디오의 대표작인 Car Open World에서는 20km × 20km 심리스 맵 위에 수십만 개의 환경 오브젝트(도로변 가로등, 가드레일, 풀, 표지판 등)가 배치되어 있습니다. 기존에는 GPU Instancing + 커스텀 LOD 시스템을 사용하여 이들을 관리했지만, 차량이 고속으로 이동할 때 청크 로딩과 인스턴싱 갱신이 동시에 발생하면서 프레임 드롭이 빈번했습니다. Unity 6로 업그레이드하면서 환경 오브젝트 시스템 전체를 Entities Graphics 기반으로 마이그레이션한 결과, 드로우콜이 기존 2,400회에서 87회로 감소했고, CPU 렌더링 비용이 프레임당 12ms에서 0.8ms로 줄어들었습니다. 가장 극적인 개선은 고속 주행 시의 스터터링이 완전히 사라졌다는 점입니다.

다만, 마이그레이션 과정에서 주의할 점이 있었습니다. 기존 MonoBehaviour 기반의 오브젝트 풀링 시스템과 Entities Graphics는 직접적으로 호환되지 않습니다. MonoBehaviour는 관리 힙(Managed Heap) 위에 존재하는 반면, ECS 엔티티는 네이티브 메모리 영역에 할당되기 때문입니다. 따라서 하이브리드 전환 기간 동안에는 CompanionGameObjectUpdateTransformSystem을 활용하여 ECS 엔티티의 트랜스폼을 기존 GameObject에 동기화하는 브릿지 레이어를 유지했으며, 완전한 ECS 마이그레이션이 완료된 후에야 이 브릿지를 제거하여 오버헤드 없는 순수 DOTS 렌더링을 달성할 수 있었습니다.

벤치마크 결과 및 결론

항목기존 (GPU Instancing)Entities Graphics개선율
드로우콜 (Draw Calls)2,4008796.4% ↓
CPU 렌더링 비용12.0 ms0.8 ms93.3% ↓
GPU 프레임 타임8.2 ms6.1 ms25.6% ↓
메모리 사용량340 MB285 MB16.2% ↓
고속 주행 스터터링빈번없음해결

GPU Driven Rendering은 더 이상 AAA 스튜디오만의 전유물이 아닙니다. Unity 6의 Entities Graphics는 인디 개발자도 충분히 접근 가능한 수준으로 이 기술의 진입 장벽을 낮추었습니다. 핵심은 "CPU가 할 일을 GPU에게 위임한다"는 단순한 원칙이지만, 이를 올바르게 적용하기 위해서는 데이터 레이아웃 설계, 머티리얼 전략, LOD 구성, 그리고 DOTS 생태계에 대한 깊은 이해가 반드시 수반되어야 합니다. 이 글이 모바일 오픈월드를 꿈꾸는 인디 개발자 여러분에게 GPU Driven Rendering 도입의 실질적인 로드맵이 되기를 바랍니다.

Implementation C# / Unity
// Entities Graphics - 커스텀 MaterialProperty 정의 예시
[MaterialProperty("_WindOffset")]
public struct WindOffset : IComponentData
{
    public float Value;
}

// 바람 시뮬레이션 Job (Burst 컴파일 대상)
[BurstCompile]
public partial struct WindSimulationJob : IJobEntity
{
    public float Time;
    public float WindStrength;

    void Execute(ref LocalTransform transform, ref WindOffset wind)
    {
        // 각 인스턴스의 위치를 기반으로 고유한 바람 오프셋 계산
        float phase = transform.Position.x * 0.1f + transform.Position.z * 0.07f;
        wind.Value = math.sin(Time * 2.0f + phase) * WindStrength;
    }
}
GPU Driven Rendering의 도입은 단순한 성능 최적화를 넘어, 게임 월드의 규모와 밀도를 근본적으로 재정의하는 아키텍처 혁신입니다. 데이터 지향 설계의 원칙을 체화하고, CPU와 GPU의 역할 분담을 재설계하는 것이 모바일 오픈월드의 미래를 여는 열쇠가 될 것입니다.