Unity에서 수많은 오브젝트를 효율적으로 렌더링하기 위해 Compute Shader를 활용하는 시스템, GPU 기반 잔디 렌더링 시스템을 구현해보겠다. 게임에서 맵 중 땅에 넓게 심는 풀들을 생각하면 될 것이다.
CPU 초기 전체 코드
using System.Runtime.InteropServices;
using Unity.Mathematics;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
namespace GPUGrass
{
    public struct Grass
    {
        public static readonly int Size = Marshal.SizeOf<Grass>();
        public float3 Position;
    }
    
    [ExecuteAlways]
    public class GPUGrass : MonoBehaviour
    {
        [SerializeField] private ComputeShader compute;
        [SerializeField] private int capacity;
        private ComputeShader _compute;
        private GraphicsBuffer _grassBuffer;
        private void OnValidate() => Init();
        private void OnEnable() => Init();
        private void OnDisable() => Dispose();
        private void Update()
        {
            if (Application.isPlaying) Tick();
        }
#if UNITY_EDITOR
        private void UpdateEditor()
        {
            if (!Application.isPlaying) Tick();
        }
#endif
    
        private void Init()
        {
            if (!compute) return; 
        
            _compute = Instantiate(compute);
            if (null == _grassBuffer|| !_grassBuffer.IsValid()) _grassBuffer = NewGrassBuffer();
            else if (_grassBuffer.count != capacity)
            {
                _grassBuffer?.Dispose();
                _grassBuffer = NewGrassBuffer();
            }
            
        
#if UNITY_EDITOR
            UnityEditor.EditorApplication.update -= UpdateEditor;
            UnityEditor.EditorApplication.update += UpdateEditor;
#endif
        }
        
        private GraphicsBuffer NewGrassBuffer() => new GraphicsBuffer(GraphicsBuffer.Target.Raw, capacity, Grass.Size);
        private void Dispose()
        {
            if (Application.isPlaying) Destroy(_compute);
            else DestroyImmediate(compute);
            _grassBuffer?.Dispose();
        
#if UNITY_EDITOR
            UnityEditor.EditorApplication.update -= UpdateEditor;
#endif
        }
        private void Tick()
        {
            if (!_compute) return;
        }
    }
}
Struct Grass
public struct Grass
{
    // C# 코드에서 이 구조체의 메모리 크기를 바이트 단위로 계산 (GPU 버퍼 할당에 필요)
    public static readonly int Size = Marshal.SizeOf<Grass>();
    // 잔디의 3차원 위치. Unity.Mathematics의 float3 사용.
    public float3 Position; 
}
- float3 Position: 잔디가 월드 공간에서 어디에 위치하는지를 나타냅니다. 나중에 색상, 바람의 영향, 높이 등의 데이터가 추가될 수 있습니다.
- Marshal.SizeOf<Grass>(): GPU 메모리(버퍼)를 할당할 때, 구조체 하나의 정확한 크기가 필요합니다. 이 코드는 .NET의 Marshal 기능을 사용하여 이를 계산합니다.
리소스 초기화 및 정리 (Init / Dispose)
Init
Init은 OnValidate (에디터 값 변경 시), OnEnable (스크립트 활성화 시)에 호출됩니다.
private void Init()
{
    if (!compute) return; 
    
    // Compute Shader를 인스턴스화하여 메모리에 로드 (Destroy를 위해 필요)
    _compute = Instantiate(compute);
    // 잔디 버퍼 생성 또는 갱신 로직
    if (null == _grassBuffer || !_grassBuffer.IsValid()) 
        _grassBuffer = NewGrassBuffer();
    else if (_grassBuffer.count != capacity) // 용량(capacity)이 변경되면 버퍼를 재생성
    {
        _grassBuffer?.Dispose();
        _grassBuffer = NewGrassBuffer();
    }
    
    // Unity Editor에서 Play 모드가 아닐 때도 업데이트를 실행하기 위한 로직
    #if UNITY_EDITOR
        UnityEditor.EditorApplication.update -= UpdateEditor;
        UnityEditor.EditorApplication.update += UpdateEditor;
    #endif
}
private GraphicsBuffer NewGrassBuffer() 
    => new GraphicsBuffer(GraphicsBuffer.Target.Raw, capacity, Grass.Size);
- _compute = Instantiate(compute);: 스크립트 인스턴스마다 독립적인 Compute Shader 인스턴스를 확보하여 GPU 연산에 사용할 준비를 합니다.
- GraphicsBuffer: 잔디 데이터 배열(Grass[])을 CPU에서 GPU로 옮겨 담는 고속 메모리 영역입니다. capacity 변수만큼 Grass 구조체를 저장할 수 있습니다.
Dispose
OnDisable에서 호출되며, 리소스를 사용 후 반드시 해제하여 메모리 누수(Memory Leak)를 방지합니다.
private void Dispose()
{
    // Compute Shader 인스턴스 해제
    if (Application.isPlaying) Destroy(_compute);
    else DestroyImmediate(compute); // 에디터 모드에서는 즉시 해제
    // GPU 버퍼 해제 (가장 중요!)
    _grassBuffer?.Dispose(); 
    // 에디터 업데이트 이벤트 제거
    #if UNITY_EDITOR
        UnityEditor.EditorApplication.update -= UpdateEditor;
    #endif
}
_grassBuffer?.Dispose(): GPU 버퍼는 관리되지 않는(Unmanaged) 리소스이므로, 반드시 명시적으로 Dispose()를 호출해 GPU 메모리에서 해제해야 합니다.
실행 루프 (Tick)
private void Tick()
{
    if (!_compute) return;
    // TODO: 1. 잔디 데이터 초기화 및 업데이트 (Compute Shader 실행)
    // TODO: 2. DrawProceduralIndirect를 사용하여 잔디 그리기 (Graphics.DrawProcedural 등)
}
현재 코드는 Tick 함수 내부가 비어 있지만, 완성 단계에서는 다음과 같은 역할을 수행하게 됩니다.
- Compute Shader Dispatch: 바람이나 LOD(Level of Detail) 등 잔디의 위치/상태를 GPU에서 계산하도록 Compute Shader를 실행합니다.
- 렌더링 호출: Graphics.DrawProceduralIndirect 함수 등을 사용하여, CPU를 거치지 않고 GPU가 직접 버퍼의 잔디 데이터를 읽어와 수천 개의 메쉬를 한 번에 렌더링하도록 명령합니다.
'공부 일지 > 그래픽스' 카테고리의 다른 글
| GPU로 풀 그리기 #3 (0) | 2025.10.05 | 
|---|---|
| GPU로 풀 그리기 #2 (0) | 2025.10.05 | 
| 스토카스틱 타일링 쉐이더 노멀 구현 (0) | 2025.10.05 | 
| 스토카스틱 타일링 쉐이더 구현 (0) | 2025.10.05 | 
| OpenGL API [OpenGL로 배우는 3차원 컴퓨터 그래픽스] (0) | 2025.01.20 |