GPU 인스턴싱과 잔디 기하학(Geometry) 생성 로직을 완성했습니다. 이로써 CPU는 렌더링 명령을 단 한 번만 내리고, 모든 잔디의 위치 계산과 렌더링은 GPU가 전담하는 진정한 GPU-Driven 파이프라인이 구축되었습니다. 잔디 메쉬를 GPU에서 직접 생성(Geometry)하고, GrassBuffer에서 위치 데이터를 읽어와 그리는 구조를 확립했습니다.
코드 업데이트 핵심 요약

GPUGrass.cs
C# 스크립트는 이제 GPU 리소스를 관리하고, 매 프레임 Compute Shader와 렌더링 셰이더를 순서대로 실행하는 파이프라인 관리자 역할을 수행합니다.
private void OnValidate()
{
    // capacity를 0과 MaxCapacity(10000) 사이로 안전하게 제한
    capacity = (int)math.clamp(capacity, 0, MaxCapacity); 
    Init();
}
// ...
private void Dispose()
{
    // Unity의 Destroying assets 오류를 피하기 위해 인스턴스를 명시적으로 파괴
    if (Application.isPlaying) Destroy(_compute);
    else DestroyImmediate(_compute);
    // ... (GraphicsBuffer Dispose)
}
math.clamp: capacity 값이 유효한 범위 내에 있도록 보장하여 GPU 버퍼 할당 오류를 방지합니다.
private void Tick()
{
    if (!_compute) return;
    
    // 1. Compute Shader 실행: 잔디 위치/상태 업데이트 (GPU 연산)
    _compute.Dispatch(_tickKernelIndex, _threadGroupCount, 1, 1);
    
    if (renderMaterial)
    {
        // 렌더링 경계 설정: 잔디가 화면에서 사라지는 것을 방지하기 위해 큰 월드 공간 경계를 설정
        _renderParams.worldBounds = new Bounds(transform.position, Vector3.one * 100.0f);
        
        // 2. 인다이렉트 렌더링 실행: CommandBuffer의 명령을 읽어 GPU가 잔디를 그림
        Graphics.RenderPrimitivesIndirect(_renderParams, MeshTopology.Triangles, _commandBuffer);
    }
}
- _compute.Dispatch: Tick 커널을 실행하여 잔디의 위치(GrassBuffer)를 업데이트합니다.
- Graphics.RenderPrimitivesIndirect: 업데이트된 GrassBuffer를 참조하여 단 한 번의 드로우 콜로 잔디를 렌더링합니다.
- worldBounds: RenderPrimitivesIndirect를 사용할 때 뷰 프러스텀 컬링(Frustum Culling)이 올바르게 작동하도록 렌더링 영역을 명시적으로 설정합니다.
GPUGrass.compute
GPUGrass.compute 파일의 Tick 커널은 현재 잔디의 초기 위치를 설정하는 역할만 수행합니다.
// ...
//BLADE 수 만큼 Tick
[numthreads(64, 1, 1)]
void Tick(const uint id : SV_DispatchThreadID)
{
    if (id >= Capacity) { return; } // Capacity를 초과하는 스레드는 무시
    
    // 잔디의 위치(Grass.Position)를 (0, 0, 0)으로 초기화 (추후 랜덤 배치 로직으로 대체됨)
    // mad(id, 3, 0) << 2 : id * 3 * 4 (바이트 오프셋 계산)
    GrassBuffer.Store3(mad(id, 3, 0)<<2, asuint(float3(0.0f, 0.0f, 0.0f)));
}
- id >= Capacity: GPU 스레드가 Capacity를 초과하여 접근하지 않도록 안전 장치를 마련했습니다.
- GrassBuffer.Store3(...): float3 데이터를 GrassBuffer에 저장합니다. 현재는 잔디 블레이드를 모두 월드 원점(0, 0, 0)에 쌓아두는 역할을 합니다.
GPUGrass.shader
GPUGrass.shader는 메쉬 데이터를 CPU로부터 받지 않고, 정점 셰이더 (Vert) 내에서 잔디의 형상을 직접 구성하는 것이 특징입니다.
// ...
// 렌더링 파이프라인에서 잔디 데이터(위치)를 읽어올 버퍼
ByteAddressBuffer GrassBuffer; 
// 잔디 블레이드의 기하학적 형상 (로컬 공간 정점 좌표 15개)
static float3 grassMesh[VERTEX_PER_BLADE] =
{
    // 3개의 삼각형으로 구성된 평면 잔디 블레이드 정의 (총 15개 정점)
    // ...
};
정점쉐이더 Vety()
Varyings Vert(const Attribute input)
{
    const uint globalVertexID = input.vertexID; // GPU가 생성한 전체 정점 인덱스
    
    // 1. 어떤 잔디 블레이드인지 계산 (인스턴싱 효과)
    const uint bladeID = uint(floor(globalVertexID / VERTEX_PER_BLADE)); 
    
    // 2. 블레이드 내에서 몇 번째 정점인지 계산
    const uint localVertex = globalVertexID % VERTEX_PER_BLADE; 
    
    // 3. GrassBuffer에서 해당 블레이드의 월드 위치를 읽어옴
    const float3 bladePosition = asfloat(GrassBuffer.Load3(mad(bladeID ,3 , 0) << 2));
    
    // 4. 미리 정의된 로컬 정점 위치를 가져옴
    const float3 vertexPosition = grassMesh[localVertex];
    Varyings output;
    // 월드 공간 위치 (블레이드 위치 + 로컬 정점 위치)를 최종 클립 공간으로 변환
    output.positionCS = TransformWorldToHClip(bladePosition + vertexPosition); 
    return output;
}
- SV_VertexID: Graphics.RenderPrimitivesIndirect가 생성하는 정점 인덱스입니다. 이 인덱스를 기반으로 bladeID (잔디 인스턴스 번호)와 localVertex (잔디 블레이드 내의 정점 번호)를 계산하여 인스턴싱 효과를 만듭니다.
- GrassBuffer.Load3(...): GrassBuffer에서 bladeID에 해당하는 잔디의 월드 위치를 로드합니다.
- bladePosition + vertexPosition: 로컬 잔디 형상을 월드 위치로 오프셋하여 최종 정점 위치를 확정하고 화면에 투영합니다.
결과

'공부 일지 > 그래픽스' 카테고리의 다른 글
| GPU로 풀 그리기 #6 (完) (0) | 2025.10.14 | 
|---|---|
| GPU로 풀 그리기 #5 (0) | 2025.10.14 | 
| GPU로 풀 그리기 #3 (0) | 2025.10.05 | 
| GPU로 풀 그리기 #2 (0) | 2025.10.05 | 
| GPU로 풀 그리기 #1 (0) | 2025.10.05 | 
