대규모 잔디 렌더링 시스템의 실제 실행 로직이 구현되었습니다. 이번 업데이트는 Compute Shader 파일(GPUGrass.compute)을 생성하고, 잔디 시스템의 핵심인 GPU 버퍼 초기화 및 실제 렌더링 명령 실행 로직을 완성했습니다.
Compute Shader (GPUGrass.compute) 생성
이 파일은 GPU에서 실행되는 두 가지 핵심 커널(Kernel)을 정의합니다.
#pragma kernel Init // 초기 설정(CommandBuffer 작성)을 위한 커널 선언
#pragma kernel Tick // 매 프레임 잔디 업데이트를 위한 커널 선언
#define VERTEX_PER_BLADE 15 // 잔디 블레이드 하나당 구성되는 정점 개수 (렌더링 명령에 사용)
cbuffer Constants // CPU에서 전달받는 상수 버퍼 (C#의 Constants 구조체와 매칭)
{
    uint Capacity; // 렌더링할 잔디의 총 개수 (GrassBuffer의 크기)
}
RWByteAddressBuffer GrassBuffer; // 잔디 데이터 (Grass 구조체 배열)
RWByteAddressBuffer CommandBuffer; // 인다이렉트 드로우 명령 인자 버퍼
[numthreads(1, 1, 1)] // Init은 한 번만 실행되므로 스레드 그룹은 1x1x1
void Init()
{
    // CommandBuffer 초기화: Graphics.RenderPrimitivesIndirect를 위한 4개의 uint 인자를 작성
    // Store4(바이트 오프셋, uint4 값): 0번 오프셋에 4개의 uint를 저장
    // uint4(인덱스 수, 인스턴스 수, 시작 인덱스, 시작 인스턴스)
    // 인덱스 수: VERTEX_PER_BLADE (정점 수) * Capacity (잔디 개수)
    // 인스턴스 수: 1 (잔디 블레이드 메쉬를 Capacity번 인스턴싱하여 그릴 예정)
    CommandBuffer.Store4(0, uint4(VERTEX_PER_BLADE * Capacity, 1, 0, 0)); 
}
// BLADE 수 만큼 Tick (잔디 하나당 하나의 스레드/연산을 실행할 예정)
[numthreads(64, 1, 1)] // 스레드 그룹당 64개의 스레드가 잔디 데이터를 처리
void Tick()
{
    // ... (잔디 위치/바람/LOD 계산 로직이 여기에 추가될 예정) ...
}
Init() 커널의 핵심: CommandBuffer.Store4(0, ...) 코드는 인다이렉트 렌더링에 필요한 명령 인자 4개를 GPU에서 직접 메모리에 쓰는 과정입니다. 특히, 잔디 개수(Capacity)를 활용하여 총 렌더링할 정점의 개수를 계산합니다. 이는 CPU의 개입 없이 렌더링 준비를 마치는 GPU 기반 시스템의 핵심입니다.
GPUGrass.cs 변경점
상수 버퍼(Constants) 및 변수 추가
public struct Constants
{
    public static readonly int Size = Marshal.SizeOf<Constants>();
    public uint Capacity; // GPU로 전달될 잔디의 총 개수
}
// ...
private GraphicsBuffer _constBuffer; // GPU의 상수 버퍼(cbuffer)에 연결될 버퍼
private int _tickKernelIndex;       // Tick 커널의 인덱스 캐시
private int _threadGroupCount;      // Tick 커널 실행에 필요한 스레드 그룹 개수
private readonly Constants[] _constants = new Constants[1]; // CPU에서 GPU로 보낼 데이터를 담는 배열
Init() 함수: GPU 초기화 실행
private void Init()
{
    // ... (ComputeShader 인스턴스화 로직 생략) ...
    
    // 상수 버퍼(_constBuffer)를 초기화하고 Capacity 설정
    if (null == _constBuffer || !_constBuffer.IsValid() || _constBuffer.stride != Constants.Size)
    {
        _constBuffer?.Dispose();
        _constBuffer = NewConstantBuffer();
    }
    _constants[0].Capacity = (uint)capacity; // CPU 배열에 Capacity 설정
    _constBuffer.SetData(_constants); // 설정된 데이터를 GPU 버퍼로 전송
    // ... (GrassBuffer, CommandBuffer 초기화 로직 생략) ...
    // 1. Init 커널 실행: CommandBuffer 초기화
    var initKernelIndex = compute.FindKernel("Init");
    _compute.SetBuffer(initKernelIndex, GrassBuffer, _grassBuffer); // GrassBuffer 바인딩
    _compute.SetBuffer(initKernelIndex, CommandBuffer, _commandBuffer); // CommandBuffer 바인딩
    _compute.Dispatch(initKernelIndex, 1, 1, 1); // Init 커널을 1x1x1 그룹으로 한 번 실행
    // 2. Tick 커널 준비
    _tickKernelIndex = compute.FindKernel("Tick");
    _compute.SetBuffer(_tickKernelIndex, CommandBuffer, _commandBuffer); // CommandBuffer 바인딩
    // Tick 커널 실행을 위한 스레드 그룹 개수 계산: Capacity를 스레드 그룹당 64개로 나눔
    _threadGroupCount = (int)math.ceil(capacity / 64.0f); 
    
    // ... (RenderParams 및 Editor 로직 생략) ...
}
private GraphicsBuffer NewConstantBuffer() => new(GraphicsBuffer.Target.Constant, 1, Constants.Size);Init() 함수는 이제 상수 버퍼를 갱신하고 Init 커널을 실행하여 렌더링 명령을 준비합니다.
Tick() 함수: GPU 연산 및 렌더링 실행
매 프레임 호출되는 Tick() 함수는 이제 두 단계를 거칩니다.
private void Tick()
{
    if (!_compute) return;
    
    // 1. Compute Shader 실행: 잔디 위치/상태 업데이트 (GPU 연산)
    _compute.Dispatch(_tickKernelIndex, _threadGroupCount, 1, 1);
    
    // 2. 인다이렉트 렌더링 실행 (GPU 렌더링)
    // CommandBuffer에 저장된 명령 인자를 기반으로 잔디를 한 번에 그립니다.
    Graphics.RenderPrimitivesIndirect(_renderParams, MeshTopology.Triangles, _commandBuffer);
}이 구조는 전형적인 GPU 기반 렌더링 파이프라인을 따릅니다: [CPU] 데이터 준비 -> [GPU] Compute Shader (업데이트) -> [GPU] RenderPrimitivesIndirect (렌더링)
'공부 일지 > 그래픽스' 카테고리의 다른 글
| GPU로 풀 그리기 #5 (0) | 2025.10.14 | 
|---|---|
| GPU로 풀 그리기 #4 (0) | 2025.10.11 | 
| GPU로 풀 그리기 #2 (0) | 2025.10.05 | 
| GPU로 풀 그리기 #1 (0) | 2025.10.05 | 
| 스토카스틱 타일링 쉐이더 노멀 구현 (0) | 2025.10.05 |