Three.js Grass: How to Render a Grass Field

Three.js grass is built with InstancedMesh: one blade geometry (usually a couple of triangles or a simple bent-plane shape) drawn thousands of times in a single draw call, with per-blade position and rotation set via instance transforms and wind sway added in the vertex shader. Never one mesh per blade.

Last updated . Verified against three.js r181.

Read the FAQJump to code

The technique: one draw call, thousands of blades

A grass field is not thousands of separate meshes; that would murder your frame rate. The standard approach:

  1. One low-poly blade geometry. A single quad, a bent quad (a few segments so it can curve), or a simple 2-triangle blade. Keep the vertex count tiny; you're about to draw it thousands of times.
  2. THREE.InstancedMesh with that geometry and a shared material. You set an instance count up front (e.g. 50,000) and a 4x4 transform matrix per instance for position, rotation and scale, scattered across your terrain, usually with some randomness so the field doesn't look like a grid.
  3. Wind in the vertex shader. Static grass reads as a texture, not a plant. Bending the top vertices of each blade based on a scrolling noise or sine function (sampling world position + time) is what sells the effect, and it costs almost nothing since it runs per-vertex, not per-blade in JavaScript.
  4. Density via instance count, not geometry. If a field looks sparse, raise the instance count before you reach for more detailed blade geometry. Density reads more than shape at grass scale.

This is exactly the technique behind most "realistic grass field" demos you'll find in the showcase below, and it's a small amount of code for the result.

Adding wind sway

Wind is the difference between "grass" and "green triangles." The common approach in a custom ShaderMaterial (or a TSL node material on WebGPU):

  • Pass a uTime uniform, updated every frame.
  • In the vertex shader, only displace vertices near the top of the blade (interpolate by the blade's local Y so the base stays planted).
  • Drive the displacement with a low-frequency sine or simplex noise sampled from world-space XZ plus time, so neighbouring blades sway together in waves rather than independently. That coherence is what makes it read as wind rather than noise.
  • Two octaves (a slow broad sway plus a faster small flutter) reads far more convincing than one.

The same idea works identically in GLSL (ShaderMaterial) or in TSL (MeshStandardNodeMaterial with a custom positionNode); see the code tab below for both.

Keeping it fast at scale

Instancing gets you most of the way, but a large open field still needs a few more tricks once you're past tens of thousands of blades:

  • Frustum and distance culling. Don't instance grass the camera can't see or is too far to resolve. Chunk your field into tiles and only instance the tiles near the camera, swapping instance buffers as the camera moves.
  • LOD by distance. Full blade geometry up close, a flattened billboard or lower blade count far away.
  • GPU-driven placement. For very large fields, computing instance transforms in a compute shader (WebGPU/TSL) instead of uploading a CPU-computed buffer avoids a large upload and lets density scale with GPU headroom rather than JavaScript time.
  • Shared material, batched draw calls. Keep it to one InstancedMesh (or a handful of tiles) rather than many small ones; draw call count matters more than triangle count for this kind of scene.

Tools and libraries

Code

ShaderMaterial wind sway (r181)
1const bladeCount = 50000
2const geometry = new THREE.PlaneGeometry(0.1, 1, 1, 4) // a few segments so it can bend
3geometry.translate(0, 0.5, 0) // pivot at the base
4
5const material = new THREE.ShaderMaterial({
6 uniforms: { uTime: { value: 0 } },
7 vertexShader: /* glsl */ `
8 uniform float uTime;
9 varying vec2 vUv;
10 void main() {
11 vUv = uv;
12 vec3 pos = position;
13 float windStrength = uv.y * uv.y; // only the top of the blade moves
14 float wave = sin(uTime * 2.0 + (instanceMatrix * vec4(position, 1.0)).x * 0.5);
15 pos.x += wave * windStrength * 0.15;
16 gl_Position = projectionMatrix * modelViewMatrix * instanceMatrix * vec4(pos, 1.0);
17 }`,
18 fragmentShader: /* glsl */ `
19 varying vec2 vUv;
20 void main() {
21 vec3 base = mix(vec3(0.05, 0.2, 0.02), vec3(0.35, 0.6, 0.15), vUv.y);
22 gl_FragColor = vec4(base, 1.0);
23 }`,
24})
25
26const grass = new THREE.InstancedMesh(geometry, material, bladeCount)
27for (let i = 0; i < bladeCount; i++) {
28 const matrix = new THREE.Matrix4()
29 matrix.setPosition((Math.random() - 0.5) * 40, 0, (Math.random() - 0.5) * 40)
30 matrix.multiply(new THREE.Matrix4().makeRotationY(Math.random() * Math.PI))
31 grass.setMatrixAt(i, matrix)
32}
33scene.add(grass)
TSL wind sway (r181)
1import { uniform, uv, sin, positionLocal, timerLocal, vec3, mix, color } from 'three/tsl'
2
3const uTime = timerLocal()
4const windStrength = uv().y.mul(uv().y) // top-heavy displacement, base stays planted
5const wave = sin(uTime.mul(2.0).add(positionLocal.x.mul(0.5)))
6
7const material = new THREE.MeshBasicNodeMaterial()
8material.positionNode = positionLocal.add(vec3(wave.mul(windStrength).mul(0.15), 0, 0))
9material.colorNode = mix(color(0x0d3306), color(0x599926), uv().y)
10
11const grass = new THREE.InstancedMesh(geometry, material, bladeCount)
12// ...same per-instance matrix setup as the WebGL version

Learn this properly

Learn Practical TSL

Your First Node Material

Learn TSL node materials from the ground up before tackling wind-shader effects like this one.

Start the lesson (6 minutes)

Frequently asked questions

How do you make grass in Three.js?

With THREE.InstancedMesh: one small blade geometry drawn thousands of times in a single draw call, with per-instance position and rotation, plus a vertex shader that bends the top of each blade for wind. You never create one mesh per blade.

How many grass blades can Three.js handle?

Tens of thousands of instanced blades run comfortably on modest hardware. Beyond that, add distance-based culling and LOD (fewer or flattened blades far from the camera), or move instance placement to a compute shader on WebGPU for very large fields.

Do I need a custom shader for grass, or can I use a standard material?

A standard material (MeshStandardMaterial) will render static grass fine, but the wind sway that makes it look alive requires a custom vertex shader: either GLSL via ShaderMaterial, or a TSL positionNode on a node material.

Keep reading