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
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.
My Contribution

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.
The system renders 400,000 grass blades while keeping per-blade processing on the GPU.
Each frame:
Per-frame GPU-driven rendering flow
Per frame
The CPU coordinates the pipeline while the GPU handles the per-instance workload.
Each grass blade is represented by static data and dynamic state stored in GPU buffers.
Stores:
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.
struct GrassBladeState
{
float2 bendDirection;
float bendAmount;
};Interaction bending is processed as a parallel compute workload. Each compute thread handles one grass blade.
Compute workload layout for interaction simulation
For each blade, the compute shader:
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.
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:
if (isVisible)
{
_VisibleInstanceIds.Append(bladeIndex);
}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 7Only 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.
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.
The visible count remains on the GPU and feeds the draw arguments directly
Graphics.DrawMeshInstancedIndirect(...)uint bladeId =
_VisibleInstanceIds[instanceID];
GrassBladeData blade =
_BladeData[bladeId];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.
The prototype focuses on reducing CPU submission work and avoiding unnecessary rendering.
Key decisions
Current limitations
Next steps
The main scaling improvement would be spatial or cluster-based culling.
Further improvements could include:
Cluster-based culling can reject entire regions before individual blade tests