Skip to content

Grass Field

App: apps/grass_field/

Demonstrates enhanced instancing (engine PLM-248 / ADR 0081) end to end: one InstancedMesh draws 4096 grass blades in a single draw call, each blade carrying its own base colour and height / wind-phase params, swaying against the global frame clock + wind, with per-instance frustum culling dropping the blades outside the view. The blade count exceeds the instance SSBO’s initial capacity, so it also exercises the renderer’s per-frame SSBO grow.

Terminal window
% ./plume3d grass_field

The camera sits raised near one edge and looks across the field, so the far and side blades fall outside the frustum and per-instance culling has something to drop. The demo captures a screenshot at frame 80 — printing visibleCount versus the total — and exits at frame 100.

  • Loads a grass shader (it must be the active shader — the instanced draw path uses the active shader), builds a single blade prototype (two crossed upright quads so it reads as 3D from any horizontal angle), and creates a GPU-instanced mesh with culling on:

    Graphics.loadShader("shaders/grass")
    var blade = Graphics.newMesh(bladeVerts(), "triangles")
    _grass = _scene.createInstancedMesh("grass")
    _grass.setMesh(blade)
    _grass.useGpuInstancing = true
    _grass.setCullingEnabled(true) // per-instance frustum cull
  • Scatters a 64 × 64 jittered grid (4096 blades over a 48 × 48 world area). Each blade gets a per-instance colour via addInstanceColored (a green varied toward yellow-green / deep-green across the field) and per-instance params via setInstanceParams (a height multiplier 0.7–1.25 and a wind-phase offset so blades don’t sway in lockstep):

    var idx = _grass.addInstanceColored(x, 0.0, z, 1.0, r, g, b)
    var scaleMul = 0.7 + 0.55 * hash(ix + 300, iz + 9)
    var windPhase = (x + z) * 0.6 + 6.28 * hash(ix + 17, iz + 41)
    _grass.setInstanceParams(idx, scaleMul, windPhase, 0.0)
  • Sets a global wind direction + strength (fed to the frame block at set 0) that the grass shader reads to sway the blades:

    Graphics.setWind(1.0, 0.0, 0.35, 0.18)
  • Reads back the diagnostic visibleCount to prove the culling is live — with this camera roughly 1700 of the 4096 blades are off-screen and not drawn:

    System.print("visibleCount = %(_grass.visibleCount) / %(_total) (culling on)")
  • InstancedMesh — createInstancedMesh, setMesh, useGpuInstancing, setCullingEnabled, addInstanceColored, setInstanceParams, visibleCount.
  • Graphics — loadShader, newMesh, setWind (the global frame / wind uniform).
  • Scene — createInstancedMesh, addNode, addCamera.
  • The per-instance base colour and params ride the widened 96-byte instance SSBO record ([transform, baseColor, params]) — the same buffer as the transform, no extra descriptor. The mesh colour of the blade prototype is white; the shader tints it by each instance’s baseColor.
  • visibleCount is a diagnostic only. It equals the instance count with culling off and drops below it as blades leave the frustum; under split-screen it reflects the last viewport.
  • Enhanced instancing scales to large foliage / scatter fields. Multi-prototype scatter (more than one GPU-instanced mesh in a frame) is a documented follow-up — grass_field uses a single instanced mesh.

See InstancedMesh for the full instancing surface and the Instanced Mesh GPU example for the plain transform-only path.