threejs-grass
    Preparing search index...

    threejs-grass

    threejs-grass

    Infinite, terrain-aware grass for three.js and react-three-fiber, built on WebGPU (TSL).

    Live demo (three.js) · Live demo (React Three Fiber) · API docs · Website

    A dense meadow on rolling hills, with balls rolling through the grass. Click to open the live demo

    • Infinite grass: a tiled field that streams around the camera, so you never build one giant mesh.
    • Performance: four configurable LODs, per-cell density allocation, smooth blade-by-blade thinning, frustum-culled tiles, and a time-budgeted tile builder.
    • 12 presets: eight inspired by real species (Kentucky Bluegrass, Perennial Ryegrass, Tall Fescue, …) and four stylized looks.
    • Two grass types: detailed geometric blades, or lightweight billboard tufts.
    • Terrain aware: pass a mesh (baked into a heightfield automatically) or a height function. Grass follows slopes and skips terrain that is too steep.
    • Natural wind: direction, strength, noise scale and speed, with gusts that roll across the field.
    • Interaction: up to 16 objects push grass aside.
    • Grass maps: control where grass grows and how tall it is, for paths, clearings and fields.
    • Terrain painter: paint coverage and height directly on the terrain.
    • Realistic shading: clumping, field-scale colour patches, back-lit translucency, tinted sheen, root occlusion, shadows.
    const grass = await Grass.create({ camera, scene, renderer, terrain, tileSize: 25, maxDistance: 200 })
    

    That's it. The grass updates itself every time the scene renders.

    WebGPU only. Use WebGPURenderer from three/webgpu. It falls back to WebGL2 by itself when WebGPU is unavailable. Classic WebGLRenderer is not supported. Tested with three r186.

    Both demos run in the browser and expose every feature: presets, blade style (height up to 3 m), wind, the four LODs with a debug tint, the terrain painter (add, erase, raise, lower, save), lighting and bloom. Two balls roll through the field to show interaction, and a minimap shows the grass map.

    three.js demo React Three Fiber demo
    The three.js demo with its lil-gui control panel The React Three Fiber demo with its leva control panel
    Plain three.js + lil-gui · source <Grass> component + leva · source

    Tip: open the Levels of detail folder and turn on show LODs, or switch on paint in the Terrain Painter folder and drag across the field.

    All of these were captured from the demos. Click any image to open the live demo.

    A dirt path cut through dense grass by a grass map Grass maps: paths and clearings A spiral of grass painted onto bare terrain Terrain painter: paint grass in the scene
    Tall grass back-lit by a low sun Tall grass, back-lit translucency Billboard grass tufts Billboard grass: the lightweight type
    The field tinted by level of detail in four colours Four LODs (debugLods) The R3F demo with its leva control panel React Three Fiber demo with leva controls

    All twelve presets side by side The 12 presets. Top row: Kentucky Bluegrass, Perennial Ryegrass, Tall Fescue, Bermuda. Middle row: Zoysia, St. Augustine, Buffalo, Fine Fescue. Bottom row: Toon Meadow, Golden Savanna, Autumn Haze, Frostbite.



    npm i threejs-grass three
    # for React:
    npm i @react-three/fiber react

    Entry points:

    Import Contents
    threejs-grass Everything framework-agnostic
    threejs-grass/react The <Grass> component, plus everything from threejs-grass (the class is exported as GrassImpl)
    import * as THREE from 'three/webgpu'
    import { Grass } from 'threejs-grass'

    const renderer = new THREE.WebGPURenderer({ antialias: true })
    document.body.appendChild(renderer.domElement)

    const scene = new THREE.Scene()
    const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.1, 2000)
    scene.add(new THREE.HemisphereLight('#dff1ff', '#3b4a22', 1), new THREE.DirectionalLight('#fff', 3))

    const terrain = new THREE.Mesh(myTerrainGeometry, new THREE.MeshStandardNodeMaterial({ color: '#2a3d16' }))
    scene.add(terrain)

    const grass = await Grass.create({ camera, scene, renderer, terrain, tileSize: 25, maxDistance: 200 })

    renderer.setAnimationLoop(() => renderer.render(scene, camera)) // grass updates via scene.onBeforeRender
    import * as THREE from 'three/webgpu'
    import { Canvas, extend, type ThreeToJSXElements } from '@react-three/fiber'
    import { useRef } from 'react'
    import { Grass } from 'threejs-grass/react'

    declare module '@react-three/fiber' {
    interface ThreeElements extends ThreeToJSXElements<typeof THREE> {}
    }
    extend(THREE as any)

    function Scene() {
    const terrain = useRef<THREE.Mesh>(null)
    return (
    <>
    <hemisphereLight args={['#dff1ff', '#3b4a22', 1]} />
    <directionalLight position={[30, 50, 20]} intensity={3} />
    <mesh ref={terrain} geometry={myTerrainGeometry}>
    <meshStandardNodeMaterial color="#2a3d16" />
    </mesh>
    <Grass terrain={terrain} preset="kentuckyBluegrass" tileSize={25} maxDistance={200} />
    </>
    )
    }

    export default () => (
    <Canvas gl={async (props) => { const r = new THREE.WebGPURenderer(props as any); await r.init(); return r }}>
    <Scene />
    </Canvas>
    )

    Tiles. The XZ plane is split into tileSize squares. Every frame, tiles within maxDistance of the camera are kept, and tiles outside are disposed. Each tile is one instanced draw call, frustum-culled as a whole.

    Cells and LODs. Each tile is split into cells of about 5 m. A cell holds only as many blades as its distance needs, according to the four lods. In the shader, density is interpolated smoothly between LODs, and blades are removed one by one in a fixed random order. Thinning never pops and never reshuffles. Remaining blades get wider so coverage stays constant. Each LOD also sets the blades' vertical segments.

    Determinism. Blade placement depends only on world position and settings, so a revisited area looks exactly the same.

    Terrain. A mesh is baked once into a heightfield by CPU rasterisation, which works for any geometry including transformed groups. A height function (x, z) => y is called directly, which suits infinite procedural worlds. Grass is skipped where the height is NaN (off the mesh) or where the slope exceeds maxSlope.

    Grass map. An optional world-space texture: R controls coverage (thinning), G controls height. Edits are live because the map is sampled on the GPU.


    Every entry below has a three.js (vanilla) example and a React Three Fiber example. R3F snippets assume a <Canvas> with a WebGPURenderer as in Quick start: react-three-fiber, plus const terrain = useRef<THREE.Mesh>(null) on your ground mesh.

    An infinite, camera-centred grass field. In R3F, use the <Grass> component, which creates this class for you. The class itself is exported as GrassImpl from threejs-grass/react.

    class Grass {
    static create(options: GrassOptions): Promise<Grass>
    constructor(options: GrassOptions)

    readonly object: THREE.Group // root of all tiles; added to `scene` if you passed one
    readonly uniforms: GrassUniforms // shader uniforms (advanced)
    readonly stats: { tiles: number; instances: number }
    camera: THREE.Camera // may be swapped at any time
    settings: GrassSettings // resolved settings (read-only — use set())
    sampleHeight: HeightFn // terrain height at (x, z); NaN off-terrain

    set(input: GrassInput): this
    reset(input?: GrassInput): this
    setTerrain(terrain?: Terrain): void
    update(budgetMs?: number): void
    dispose(): void
    densityAt(distance: number): number
    mapNode(worldXZ: Node<'vec2'>): GrassMapSample
    }

    Validates the renderer, initialises it if needed, creates the field and builds every visible tile so the first frame is complete. Throws if renderer is not a WebGPURenderer. See Settings.

    three.js

    import { Grass } from 'threejs-grass'
    const grass = await Grass.create({ camera, scene, renderer, terrain, preset: 'tallFescue', maxDistance: 150 })

    React Three Fiber

    import { Grass } from 'threejs-grass/react'
    <Grass terrain={terrain} preset="tallFescue" maxDistance={150} />

    Takes the same options as create, but skips renderer validation and the initial full build, so tiles stream in over the next frames. Prefer Grass.create.

    three.js

    const grass = new Grass({ camera, terrain })
    scene.add(grass.object)
    renderer.setAnimationLoop(() => { grass.update(); renderer.render(scene, camera) })

    React Three Fiber

    // Only needed if you want to manage the instance yourself:
    import { GrassImpl } from 'threejs-grass/react'
    function ManualGrass() {
    const camera = useThree((s) => s.camera)
    const grass = useMemo(() => new GrassImpl({ camera, preset: 'bermudaGrass' }), [camera])
    useEffect(() => () => grass.dispose(), [grass])
    useFrame(() => grass.update())
    return <primitive object={grass.object} />
    }

    Changes any settings at runtime and returns this.

    • Visual settings (colours, height, width, droop, wind, LOD distances, debugLods, interaction, translucency, …) apply instantly through uniforms.
    • Layout settings (type, density, heightVariation, widthVariation, clumping, clumpSize, maxSlope, lods, tileSize) rebuild tiles in the background. Old blades stay visible until each tile is rebuilt.

    Values merge into what was set before and persist across calls. undefined values are ignored, and wind merges field by field. To drop earlier overrides, use reset.

    three.js

    grass.set({ preset: 'goldenSavanna', bladeHeight: 1.6, wind: { strength: 0.8 } })
    

    React Three Fiber

    // Just change props; the component applies them for you.
    const [tall, setTall] = useState(false)
    <Grass terrain={terrain} preset="goldenSavanna" bladeHeight={tall ? 1.6 : 0.8} wind={{ strength: 0.8 }} />

    Replaces all settings with input. Anything not given falls back to the preset, then to the defaults. Use it to switch presets cleanly after overriding style fields such as colours. <Grass> uses it internally, so props are declarative: removing a prop reverts it.

    three.js

    grass.set({ preset: 'tallFescue', tipColor: '#ff0000' })
    grass.set({ preset: 'frostbite' }) // still red tips: set() merges
    grass.reset({ preset: 'frostbite' }) // frostbite exactly as designed

    React Three Fiber

    // Props are reset() each render: when `highlight` turns off, tipColor returns to the preset's colour.
    <Grass terrain={terrain} preset="frostbite" tipColor={highlight ? '#ff0000' : undefined} />

    Swaps the terrain (mesh, group or height function) and regenerates all tiles.

    three.js

    grass.setTerrain((x, z) => Math.sin(x * 0.05) * 3)
    grass.setTerrain(otherTerrainMesh)

    React Three Fiber

    // Swap the `terrain` prop to another object/ref, or call setTerrain through the ref:
    const grass = useRef<GrassImpl>(null)
    <Grass ref={grass} terrain={terrain} />
    // later:
    grass.current?.setTerrain((x, z) => Math.sin(x * 0.05) * 3)

    Re-centres tiles on the camera, streams tiles in and out, and refreshes interactors, the sun direction and stats. budgetMs (default settings.buildBudget) caps time spent building tiles this frame.

    three.js

    // Automatic when `scene` was passed to create(). Otherwise, once per frame:
    renderer.setAnimationLoop(() => { grass.update(); renderer.render(scene, camera) })

    React Three Fiber

    // <Grass> already calls update() in useFrame. With GrassImpl directly:
    useFrame(() => grass.update())

    Removes the grass from its parent, unhooks the scene, and frees GPU resources.

    three.js

    grass.dispose()
    

    React Three Fiber

    // <Grass> disposes on unmount — just stop rendering it:
    {showGrass && <Grass terrain={terrain} />}

    Returns the fraction (0..1) of full density at a distance from the camera. This is the CPU mirror of the shader's LOD curve.

    three.js

    console.log(grass.densityAt(10), grass.densityAt(100)) // e.g. 0.88, 0.04
    

    React Three Fiber

    const grass = useRef<GrassImpl>(null)
    useEffect(() => console.log(grass.current?.densityAt(50)), [])
    <Grass ref={grass} terrain={terrain} />

    Returns TSL nodes { coverage, height } that read the grass map at a world position. Use them in your terrain material to show dirt where grass was erased. They track painting and grassMap swaps live.

    three.js

    import { mix, positionWorld, vec3 } from 'three/tsl'
    const { coverage } = grass.mapNode(positionWorld.xz)
    terrain.material.colorNode = mix(vec3(0.45, 0.35, 0.22), vec3(0.1, 0.16, 0.05), coverage)
    terrain.material.needsUpdate = true

    React Three Fiber

    import { mix, positionWorld, vec3 } from 'three/tsl'
    const [grass, setGrass] = useState<GrassImpl | null>(null)
    useEffect(() => {
    if (!grass || !terrain.current) return
    const material = terrain.current.material as THREE.MeshStandardNodeMaterial
    material.colorNode = mix(vec3(0.45, 0.35, 0.22), vec3(0.1, 0.16, 0.05), grass.mapNode(positionWorld.xz).coverage)
    material.needsUpdate = true
    }, [grass])
    <Grass ref={setGrass} terrain={terrain} grassMap={map} />

    Returns the terrain height the grass uses, which is handy for placing objects on the ground. Returns NaN off-terrain.

    three.js

    ball.position.y = grass.sampleHeight(ball.position.x, ball.position.z) + 0.5
    

    React Three Fiber

    const grass = useRef<GrassImpl>(null)
    const ball = useRef<THREE.Mesh>(null)
    useFrame(() => {
    const g = grass.current, b = ball.current
    if (g && b) b.position.y = g.sampleHeight(b.position.x, b.position.z) + 0.5
    })
    <Grass ref={grass} terrain={terrain} />

    { tiles, instances }, refreshed on every update.

    three.js

    hud.textContent = `${grass.stats.tiles} tiles · ${grass.stats.instances} blades`
    

    React Three Fiber

    const grass = useRef<GrassImpl>(null)
    useFrame(() => grass.current && (hud.current!.textContent = `${grass.current.stats.instances} blades`))
    • object is the Group containing all tiles.
    • camera can be reassigned.
    • settings is the resolved configuration. Read it, and change it with set().
    • uniforms is covered under Shader access.

    three.js

    grass.camera = carCamera            // follow another camera
    grass.object.visible = false // hide all grass
    console.log(grass.settings.bladeHeight)

    React Three Fiber

    const grass = useRef<GrassImpl>(null)
    useEffect(() => { if (grass.current) grass.current.object.visible = false }, []) // hide all grass
    <Grass ref={grass} camera={carCamera} terrain={terrain} /> // follow another camera

    • GrassOptions is passed to Grass.create: GrassInput plus camera, scene, renderer and terrain.
    • GrassInput is a partial GrassSettings, with wind also partial.
    • In React, every field is a prop of <Grass>.
    Option Type Default
    camera Camera (required; R3F: default camera) Camera the field follows
    scene Object3D Grass is added to it and auto-updates
    renderer GrassRenderer WebGPURenderer; initialised if needed
    terrain Terrain flat at y = 0 Mesh/group or (x, z) => y
    preset PresetName 'kentuckyBluegrass' Starting look
    type 'blades' | 'billboards' 'blades' Geometric blades or billboard tufts
    tileSize number 25 Tile edge (m)
    maxDistance number 200 Grass radius (m); fades over the last 15%
    lods GrassLOD[4] see below Exactly four levels, near → far
    wind Partial<WindOptions> { direction: [1, 0.35], strength: 0.45, scale: 0.045, speed: 0.8 }
    maxSlope number 1.2 Max gradient (rise/run) that grows grass; Infinity = no limit
    grassMap GrassMap | null null Coverage/height control
    interactors Interactor[] [] Up to 16 objects that push grass
    interactionStrength number 1 Push multiplier
    castShadow boolean true Near grass casts shadows
    shadowDistance number 20 Shadow-casting radius (m)
    receiveShadow boolean true
    sun DirectionalLight | null first DirectionalLight in the scene Drives back-lit translucency
    debugLods boolean false Tints LODs red / yellow / green / blue
    buildBudget number 3 ms per frame spent building tiles
    …every GrassStyle field from the preset Overrides the preset

    three.js

    const grass = await Grass.create({
    camera, scene, renderer, terrain,
    preset: 'perennialRyegrass',
    type: 'blades',
    tileSize: 25,
    maxDistance: 200,
    maxSlope: 0.9,
    castShadow: true,
    shadowDistance: 25,
    sun: sunLight,
    bladeHeight: 0.7,
    })

    React Three Fiber

    <Grass
    terrain={terrain}
    preset="perennialRyegrass"
    type="blades"
    tileSize={25}
    maxDistance={200}
    maxSlope={0.9}
    castShadow
    shadowDistance={25}
    sun={sunLight}
    bladeHeight={0.7}
    />
    interface GrassLOD {
    distance: number // where this LOD is fully reached (m)
    density: number // fraction of full density kept (0..1)
    segments: number // vertical segments per blade at this distance
    }

    Default lods:

    [
    { distance: 8, density: 1, segments: 5 },
    { distance: 25, density: 0.25, segments: 3 },
    { distance: 60, density: 0.06, segments: 2 },
    { distance: 120, density: 0.03, segments: 1 },
    ]

    three.js

    // Denser near field, cheaper far field; tint LODs while tuning.
    grass.set({
    debugLods: true,
    lods: [
    { distance: 12, density: 1, segments: 6 },
    { distance: 35, density: 0.3, segments: 3 },
    { distance: 80, density: 0.05, segments: 2 },
    { distance: 160, density: 0.02, segments: 1 },
    ],
    })

    React Three Fiber

    const lods: GrassLOD[] = [
    { distance: 12, density: 1, segments: 6 },
    { distance: 35, density: 0.3, segments: 3 },
    { distance: 80, density: 0.05, segments: 2 },
    { distance: 160, density: 0.02, segments: 1 },
    ]
    <Grass terrain={terrain} lods={lods} debugLods />
    interface WindOptions {
    direction: [x: number, z: number] // normalised internally
    strength: number // 0 still · 0.35 breeze · 1+ storm
    scale: number // noise frequency; smaller = broader gusts
    speed: number // how fast gusts travel
    }

    three.js

    grass.set({ wind: { direction: [0, 1], strength: 0.9, scale: 0.04, speed: 1.5 } })
    

    React Three Fiber

    <Grass terrain={terrain} wind={{ direction: [0, 1], strength: 0.9, scale: 0.04, speed: 1.5 }} />
    
    interface Interactor {
    object: Object3D | { current: Object3D | null } // object or React ref; world position read every frame
    radius: number // influence radius (m)
    }

    three.js

    grass.set({
    interactors: [{ object: player, radius: 1.2 }, { object: car, radius: 2.5 }],
    interactionStrength: 1.5,
    })

    React Three Fiber

    const player = useRef<THREE.Mesh>(null)   // refs work directly
    <Grass terrain={terrain} interactors={[{ object: player, radius: 1.2 }]} interactionStrength={1.5} />

    The minimal renderer shape Grass.create checks: { isWebGPURenderer?, hasInitialized?(), init?() }. Pass a WebGPURenderer from three/webgpu. It uses WebGPU when available and WebGL2 otherwise.

    three.js

    import * as THREE from 'three/webgpu'
    const renderer = new THREE.WebGPURenderer({ antialias: true })
    await renderer.init() // optional: Grass.create() does it if needed
    const grass = await Grass.create({ camera, scene, renderer, terrain })

    React Three Fiber

    // <Grass> reads the renderer from the Canvas; give the Canvas a WebGPURenderer:
    <Canvas gl={async (props) => { const r = new THREE.WebGPURenderer(props as any); await r.init(); return r }}>
    <Grass terrain={terrain} />
    </Canvas>

    presets is a record of 12 complete GrassStyles. PresetName is its key type.

    Real species Stylized
    kentuckyBluegrass, perennialRyegrass, tallFescue, bermudaGrass, zoysiaGrass, stAugustineGrass, buffaloGrass, fineFescue toonMeadow, goldenSavanna, autumnHaze, frostbite

    You can override any GrassStyle field individually. All of them apply live except the ones marked layout, which rebuild tiles:

    Field Meaning
    density Blades per m² at LOD 0 (layout)
    bladeHeight Height in metres. Any value works (lawn ≈ 0.1, meadow ≈ 0.5, tall grass 1–3)
    heightVariation 0..1 random height spread (layout)
    bladeWidth Base width in metres
    widthVariation 0..1 random width spread (layout)
    curvature 0..1 natural droop
    stiffness Resistance to wind and interaction
    clumping 0..1 how much blades gather into outward-splaying tufts (layout)
    clumpSize Tuft diameter in metres (layout)
    baseColor, tipColor CSS colours; gradient root → tip
    colorVariation 0..1 per-blade brightness variation
    patchiness 0..1 field-scale light/dark and dry patches
    translucency 0..1.5 back-lit glow when looking towards the sun

    three.js

    import { Grass, presets, type PresetName } from 'threejs-grass'
    const grass = await Grass.create({ camera, scene, renderer, terrain, preset: 'goldenSavanna', bladeHeight: 1.4 })
    console.log(presets.tallFescue.bladeHeight) // 0.85
    const names = Object.keys(presets) as PresetName[] // for a dropdown
    grass.set({ preset: 'frostbite', tipColor: '#ffffff' })

    React Three Fiber

    import { Grass, presets, type PresetName } from 'threejs-grass/react'
    const [preset, setPreset] = useState<PresetName>('frostbite')
    <Grass terrain={terrain} preset={preset} tipColor="#ffffff" patchiness={0.6} />

    A world-space control texture covering a square of terrain. R is coverage (0 none … 1 full) and G is height (0.5 = 1x … 1 = 2x). Outside the square, grass grows normally.

    class GrassMap {
    constructor(options?: GrassMapOptions)
    static fromImage(image, options?): GrassMap
    readonly texture: DataTexture
    readonly data: Uint8Array // RGBA, row 0 = minZ; set texture.needsUpdate after editing
    readonly resolution: number
    readonly size: number
    readonly minX: number
    readonly minZ: number
    load(image): this
    fill(coverage?: number, height?: number): void
    paint(x: number, z: number, brush: Brush): boolean
    toCanvas(): HTMLCanvasElement
    toDataURL(): string
    dispose(): void
    }
    Method
    fromImage(image, options) Creates a map from an image: red = coverage, green = height, top edge = −Z
    load(image) Replaces the contents with an image (scaled to resolution)
    fill(coverage = 1, height = 1) Fills everything; fill(0) clears all grass
    paint(x, z, brush) Soft round brush at world (x, z); returns false if the brush is outside the map
    toCanvas() / toDataURL() Exports the raw map (round-trips with fromImage / load)
    dispose() Frees the texture

    three.js

    import { GrassMap } from 'threejs-grass'

    // A dirt path and a clearing
    const map = new GrassMap({ center: [0, 0], size: 300, resolution: 1024 })
    for (let z = -150; z < 150; z += 0.5) map.paint(Math.sin(z * 0.05) * 20, z, { mode: 'erase', radius: 1.8, strength: 1 })
    map.paint(30, 30, { mode: 'erase', radius: 8, strength: 1 })
    grass.set({ grassMap: map })

    // Save / load
    localStorage.setItem('grass-map', map.toDataURL())
    const img = new Image()
    img.src = localStorage.getItem('grass-map')!
    await img.decode()
    map.load(img)

    React Three Fiber

    import { Grass, GrassMap } from 'threejs-grass/react'

    function Field({ pathImage }: { pathImage: HTMLImageElement }) {
    const map = useMemo(() => GrassMap.fromImage(pathImage, { size: 300 }), [pathImage])
    useEffect(() => () => map.dispose(), [map])
    return <Grass terrain={terrain} grassMap={map} />
    }
    interface GrassMapOptions {
    center?: [x: number, z: number] // default [0, 0]
    size?: number // metres, default 256
    resolution?: number // texels per side, default 512
    coverage?: number // initial 0..1, default 1
    height?: number // initial multiplier 0..2, default 1
    }

    three.js

    // Start empty and paint grass in (like a level editor)
    const map = new GrassMap({ center: [100, -50], size: 200, resolution: 512, coverage: 0 })

    React Three Fiber

    const map = useMemo(() => new GrassMap({ center: [100, -50], size: 200, coverage: 0 }), [])
    <Grass terrain={terrain} grassMap={map} />

    Paints a grass's grassMap directly on the terrain. Left-drag applies the brush, and a ring follows the pointer. Works with mesh and function terrain. Disable camera controls while painting, because they use the same pointer events.

    class TerrainPainter {
    constructor(grass: Grass, domElement: HTMLElement, brush?: Partial<Brush>) // throws without grassMap
    enabled: boolean
    brush: Brush
    readonly cursor: Mesh
    dispose(): void
    }

    three.js

    import { TerrainPainter } from 'threejs-grass'
    const painter = new TerrainPainter(grass, renderer.domElement, { mode: 'add', radius: 3, strength: 0.3 })
    controls.enabled = false // OrbitControls off while painting
    painter.brush.mode = 'erase' // change the brush at any time
    painter.enabled = false // pause
    painter.dispose() // remove listeners + cursor

    React Three Fiber

    // The `paint` prop creates/disposes the painter; brush changes apply live.
    const [painting, setPainting] = useState(false)
    <Grass terrain={terrain} grassMap={map} paint={painting && { mode: 'add', radius: 3, strength: 0.3 }} />
    <OrbitControls makeDefault enabled={!painting} />
    type BrushMode = 'add' | 'erase' | 'raise' | 'lower'   // coverage up/down, height up/down
    interface Brush {
    mode: BrushMode
    radius: number // metres
    strength: number // 0..1 applied per stroke event at the centre
    }

    three.js

    const brush: Brush = { mode: 'raise', radius: 6, strength: 0.2 }
    map.paint(x, z, brush) // programmatic stroke
    Object.assign(painter.brush, brush) // or set the interactive painter's brush

    React Three Fiber

    const [mode, setMode] = useState<BrushMode>('raise')
    <Grass terrain={terrain} grassMap={map} paint={{ mode, radius: 6, strength: 0.2 }} />

    type HeightFn = (x: number, z: number) => number   // NaN = no terrain
    type Terrain = Object3D | HeightFn

    function createHeightSampler(terrain?: Terrain, maxResolution = 1024): HeightFn
    function bakeHeightfield(root: Object3D, maxResolution = 1024): HeightFn
    function raycastHeight(sample: HeightFn, origin: Vector3, dir: Vector3, maxDistance = 2000, step = 0.5): Vector3 | null
    function slopeAt(sample: HeightFn, x: number, z: number, e = 0.25): number
    Function
    createHeightSampler Turns a Terrain into a height lookup: functions as-is, meshes baked, nothing = flat
    bakeHeightfield Rasterises every mesh under root (with world transforms) into a bilinear heightfield. The highest surface wins
    raycastHeight Ray-marches and bisects against a height function; returns the hit or null
    slopeAt Gradient magnitude (rise/run) by central differences

    three.js

    import { Grass, bakeHeightfield, raycastHeight, slopeAt, type HeightFn } from 'threejs-grass'

    // Infinite procedural terrain
    const height: HeightFn = (x, z) => Math.sin(x * 0.03) * 4 + Math.cos(z * 0.025) * 5
    const grass = await Grass.create({ camera, scene, renderer, terrain: height })

    // Click on the terrain
    const ray = new THREE.Raycaster()
    ray.setFromCamera(pointer, camera)
    const hit = raycastHeight(grass.sampleHeight, ray.ray.origin, ray.ray.direction)

    // Reuse a baked mesh heightfield for gameplay
    const ground = bakeHeightfield(terrainMesh)
    const steep = slopeAt(ground, 10, 20) > 1

    React Three Fiber

    import { raycastHeight } from 'threejs-grass/react'

    // Inline height functions are fine
    <Grass terrain={(x, z) => Math.sin(x * 0.03) * 4 + Math.cos(z * 0.025) * 5} />

    // Ray-march on pointer events
    const grass = useRef<GrassImpl>(null)
    <mesh onPointerDown={(e) => {
    const hit = grass.current && raycastHeight(grass.current.sampleHeight, e.ray.origin, e.ray.direction)
    if (hit) spawnFlower(hit)
    }} />

    function createBladeGeometry(segments: number): BufferGeometry      // tapered strip, x ∈ [-0.5, 0.5], y ∈ [0, 1]
    function createBillboardGeometry(segments: number): BufferGeometry // unit quad strip

    These are the base shapes Grass instances. They are exposed for custom instancing setups (e.g. a few hero blades), and you don't need them for normal use.

    three.js

    import { createBladeGeometry } from 'threejs-grass'
    const blades = new THREE.InstancedMesh(createBladeGeometry(6), new THREE.MeshStandardNodeMaterial({ side: THREE.DoubleSide }), 100)

    React Three Fiber

    import { createBladeGeometry } from 'threejs-grass/react'
    const geometry = useMemo(() => createBladeGeometry(6), [])
    <instancedMesh args={[geometry, undefined, 100]}><meshStandardNodeMaterial side={THREE.DoubleSide} /></instancedMesh>

    const MAX_INTERACTORS = 16
    type GrassUniforms = { baseColor, tipColor, bladeHeight, windStrength, windDirection, windScale, windSpeed,
    viewPosition, sunDirection, sunColor, map, mapBounds, interactors, … } // TSL nodes
    interface GrassMapSample { coverage: Node<'float'>; height: Node<'float'> }

    grass.uniforms exposes the live TSL uniforms, which are useful for syncing other effects with the grass (for example, wind on trees). Change values with grass.set(), because uniforms are overwritten on every set() call.

    three.js

    import { positionLocal, sin, time, vec3 } from 'three/tsl'
    const u = grass.uniforms
    treeMaterial.positionNode = positionLocal.add(
    vec3(u.windDirection.x, 0, u.windDirection.y).mul(sin(time.mul(u.windSpeed)).mul(u.windStrength).mul(positionLocal.y.mul(0.05))),
    )

    React Three Fiber

    const [grass, setGrass] = useState<GrassImpl | null>(null)
    const treeMaterial = useMemo(() => {
    if (!grass) return null
    const u = grass.uniforms
    const m = new THREE.MeshStandardNodeMaterial()
    m.positionNode = positionLocal.add(vec3(u.windDirection.x, 0, u.windDirection.y).mul(sin(time).mul(u.windStrength).mul(0.1)))
    return m
    }, [grass])
    <Grass ref={setGrass} terrain={terrain} />
    {treeMaterial && <mesh geometry={treeGeometry} material={treeMaterial} />}

    function Grass(props: GrassProps): JSX.Element | null
    interface GrassProps extends GrassInput {
    ref?: Ref<GrassImpl | null>
    terrain?: Terrain | RefObject<Object3D | null>
    camera?: Camera
    paint?: Partial<Brush> | false
    }
    export { Grass as GrassImpl } // the imperative class
    • Creates the field on mount (and when camera, the renderer or the terrain object changes) and disposes it on unmount.
    • Every other prop is applied with grass.reset() on render, so props are declarative (a removed prop reverts to the preset). This is cheap for visual props, and tiles rebuild only when layout props actually change.
    • Updates every frame through useFrame.
    • ref receives the GrassImpl instance.
    • paint toggles a TerrainPainter; brush changes apply without recreating it.
    • threejs-grass/react also re-exports everything from threejs-grass.

    React Three Fiber

    import { Grass, GrassMap, type GrassImpl } from 'threejs-grass/react'

    function Field() {
    const terrain = useRef<THREE.Mesh>(null)
    const ball = useRef<THREE.Mesh>(null)
    const grass = useRef<GrassImpl>(null)
    const map = useMemo(() => new GrassMap({ size: 300 }), [])
    useFrame(() => {
    const g = grass.current, b = ball.current
    if (g && b) b.position.y = g.sampleHeight(b.position.x, b.position.z) + 0.5
    })
    return (
    <>
    <mesh ref={terrain} geometry={hills} receiveShadow />
    <mesh ref={ball}><sphereGeometry args={[0.5]} /></mesh>
    <Grass
    ref={grass}
    terrain={terrain}
    preset="perennialRyegrass"
    type="blades"
    bladeHeight={0.8}
    grassMap={map}
    wind={{ strength: 0.5 }}
    interactors={[{ object: ball, radius: 1.2 }]}
    />
    </>
    )
    }

    three.js equivalent

    const map = new GrassMap({ size: 300 })
    const grass = await Grass.create({
    camera, scene, renderer, terrain,
    preset: 'perennialRyegrass', type: 'blades', bladeHeight: 0.8, grassMap: map,
    wind: { strength: 0.5 }, interactors: [{ object: ball, radius: 1.2 }],
    })
    renderer.setAnimationLoop(() => {
    ball.position.y = grass.sampleHeight(ball.position.x, ball.position.z) + 0.5
    renderer.render(scene, camera)
    })

    • density and the first two lods dominate cost. Check the result with grass.stats.instances and tune with debugLods.
    • Shadows are the next biggest cost. Keep shadowDistance small (default 20 m) or set castShadow: false.
    • Billboards (type: 'billboards') are several times cheaper than blades for large areas.
    • Cap the device pixel ratio (e.g. renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5))), because grass is fill-rate heavy.
    • buildBudget trades tile pop-in for frame-time stability when the camera moves fast.
    npm install
    npm run dev # http://localhost:5173 (three.js + lil-gui) and /r3f.html (React + leva)
    npm test # terrain baking / ray-march checks
    npm run build # ESM + .d.ts into dist/

    Both demos include a physical sky, sky-based ambient lighting, a sun with shadows that follows the camera, haze, ACES tone mapping, subtle bloom, a dirt path painted into a grass map, rolling balls that push the grass, a minimap, and controls for every feature.