대규모 잔디 렌더링 시스템의 실제 실행 로직이 구현되었습니다. 이번 업데이트는 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 |