Rxl-tech.art
Back to Home
Case Study·Graphics Programming·Demo

Compute-Driven Grass Rendering

A GPU-driven grass rendering prototype built in Unity URP.

Designed to explore compute shaders, GPU culling, persistent simulation state, and indirect instanced rendering at large instance counts.

Compute ShadersGPU InstancingIndirect RenderingGPU CullingHLSLUnity URP

My Contribution

  • GPU-driven grass rendering pipeline
  • Compute-shader interaction simulation and visibility culling
  • Append-buffer compaction and indirect instanced rendering
  • Procedural wind, blade deformation, and runtime controls
Compute-driven grass rendering still frame
01Overview

Rendering 400,000 Grass Blades with a GPU-Driven Pipeline

A GPU-driven grass rendering prototype built in Unity URP.

Designed to explore compute shaders, GPU culling, persistent simulation state, and indirect instanced rendering at large instance counts.

The scene contains 400,000 procedurally distributed grass blades.

Each frame, the GPU updates interaction state, tests blades against distance and camera frustum, compacts visible blade IDs, generates the indirect instance count, and reconstructs and deforms visible blades.

Impact
Demo
Platforms
PC · Unity URP
Tools
Unity 6 · C# · HLSL · Compute Shaders
Focus
Compute Shaders · GPU Instancing · Indirect Rendering · GPU Culling · HLSL · Unity URP
03Technical Breakdown
item.01

GPU-Driven Rendering Pipeline

The system renders 400,000 grass blades while keeping per-blade processing on the GPU.

Each frame:

  • Update camera and interaction parameters
  • Simulate persistent interaction state
  • Cull blades against distance and camera frustum
  • Build a compact list of visible blade IDs
  • Copy the visible count into the indirect draw arguments
  • Render the visible blades with one indirect instanced draw
Rendering diagram...

Per-frame GPU-driven rendering flow

Per frame

  • 400,000 blade instances
  • 2 compute dispatches
  • 1 indirect draw

The CPU coordinates the pipeline while the GPU handles the per-instance workload.

item.02

GPU Data & Persistent State

Each grass blade is represented by static data and dynamic state stored in GPU buffers.

Stores:

  • World position
  • Rotation
  • Height variation
  • Width variation
hlslStatic blade data generated during initialization and reused every frame
struct GrassBladeData
{
    float3 positionWS;
    float rotation;
    float heightScale;
    float widthScale;
};

Only interaction bending requires persistent state. Wind is calculated procedurally in the vertex shader instead of storing additional per-blade animation data.

This keeps static placement, persistent simulation, and procedural animation separate.

hlslDynamic state updated by the interaction compute shader
struct GrassBladeState
{
    float2 bendDirection;
    float bendAmount;
};
item.03

Compute Simulation & Interaction

Interaction bending is processed as a parallel compute workload. Each compute thread handles one grass blade.

Rendering diagram...

Compute workload layout for interaction simulation

For each blade, the compute shader:

  • Measures distance to the interactor
  • Calculates a bend direction
  • Updates bend strength
  • Gradually restores the blade when interaction stops

The resulting state stays in GPU memory and is consumed later by the vertex shader. This avoids updating or transferring per-blade interaction data from the CPU.

item.04

GPU Culling & Visibility Compaction

Processing all 400,000 blades through the graphics pipeline would waste work when large parts of the field are outside the camera. A compute kernel performs visibility testing before rendering.

Each blade is tested against:

  • Maximum render distance
  • Camera frustum planes
hlslVisible blades append their IDs to a GPU buffer
if (isVisible)
{
    _VisibleInstanceIds.Append(bladeIndex);
}
CodeThe append buffer converts sparse visibility results into a compact list
All blades

0   1   2   3   4   5   6   7
Y   N   N   Y   Y   N   N   Y

          GPU Culling

Visible IDs

0   3   4   7

Only those IDs are passed into rendering.

Trade-off

Culling reduces later vertex and fragment work, but the compute shader still evaluates every blade every frame. A larger-scale implementation could first reject spatial clusters before testing individual blades.

item.05

Indirect Instanced Rendering

The visible list feeds directly into an indirect instanced draw. After culling, the append buffer already contains the number of visible blades. `ComputeBuffer.CopyCount` transfers this counter into the indirect argument buffer without reading it back to the CPU.

Rendering diagram...

The visible count remains on the GPU and feeds the draw arguments directly

csharpThe draw call uses the GPU-generated count
Graphics.DrawMeshInstancedIndirect(...)
hlslSV_InstanceID resolves the original blade from the compact visible list
uint bladeId =
    _VisibleInstanceIds[instanceID];

GrassBladeData blade =
    _BladeData[bladeId];
Rendering diagram...

Instance IDs resolve the original blade data through the compact visible list

The vertex shader reconstructs the blade transform and applies per-blade scale and rotation, procedural wind, and persistent interaction bending. The CPU does not need to construct or read back the visible instance list.

item.06

Performance & Trade-offs

The prototype focuses on reducing CPU submission work and avoiding unnecessary rendering.

Key decisions

  • Single indirect draw for visible grass
  • GPU-side culling before vertex processing
  • Compact visible instance list generated on the GPU
  • No synchronous GPU readback in the rendering path
  • Minimal persistent state, with procedural wind evaluated during vertex processing

Current limitations

  • Culling still evaluates all 400,000 blades
  • No distance-based grass LOD
  • No occlusion culling
  • Culling uses approximate blade bounds
  • Normals do not fully follow blade deformation
  • Limited custom shadow support

Next steps

The main scaling improvement would be spatial or cluster-based culling.

Further improvements could include:

  • Distance-based geometry LOD
  • Improved deformation-aware normals
  • More complete shadow support
  • Profiling simulation, culling, vertex, and fragment costs independently
Rendering diagram...

Cluster-based culling can reject entire regions before individual blade tests