vidar.games

Touch-bending grass

2026-11-10 ยท Discuss on X

A stone ball rolls through 48,000 grass billboards. Every tuft checks up to 16 benders in its vertex shader and leans away from any it is inside.

The problem

Billboard grass is cheap because each tuft is one flat quad with no physics. That is also why a ball or a character slides straight through it. The tufts in front of the object stay upright, the object clips into them, and the scene stops reading as touched.

How it works

A manager keeps a list of benders, each with a position, a bend radius and a priority. Every frame it sorts the list by priority, lower values first, and writes the first 16 into a fixed array of four floats each: x, y, z and radius.
The grass vertex shader loops over all 16 slots. For each one it takes the horizontal distance from the tuft's base to the bender. Bend power is 1 at the centre and falls linearly to 0 at the radius. The push direction is the normalised 3D vector from the bender to the tuft base, and only its x and z are used. The offset is that direction times power times tuft height, and it moves only the top two corners of the quad.
Drawing is split in two passes. Pass 1 alpha-tests at 0.95 and writes depth. Pass 2 alpha-blends the texels below 0.95 with depth writes off.

What I hit

Tufts right under the ball barely move. The push vector points from the ball's centre down to the ground, so its sideways part is close to zero there. With a 0.42 m ball and a radius of 1.0, the sideways push peaks about 0.35 to 0.4 m out from the centre.
There is no memory. Tufts stand straight again as soon as the ball moves on.
Low side-on views were the heaviest shots to render. Pass 2 stacks many blended layers on each pixel there.

My build

Ported the bend loop and the two-pass draw to GLSL in three.js, with instanced billboards facing the camera around the vertical axis.
48,000 tufts on a 32 by 31.5 m field, heights 0.42 to 0.72 m, procedural tuft texture.
Benders: the ball, radius 1.0, and a walking capsule, radius 0.8. In the debug shot 18 pebbles with radius 0.55 join them. That makes 20 benders, 16 get sent, and the 4 with the highest priority values bend nothing.

Trade-offs

Every vertex of every tuft runs 16 loop iterations, even when no bender is near. The limit is a hard cut: a bender past slot 16 simply stops working. The sideways shift stretches a blade instead of keeping its length. Pass 2 is unsorted, so overlapping soft edges can draw in the wrong order.

When to use it

Fields of billboard grass with a few characters, balls or wheels moving through them. If you want lasting trails or flattened patches, this alone will not do it, because nothing is stored between frames.

New breakdowns go out on X first: @vidar_0208. If this one helped, you can buy me a coffee.

Source and credits

Technique source: grass-bending by elringus (Artyom Sovetnikov), https://github.com/elringus/grass-bending, MIT license (Copyright 2018 Artyom Sovetnikov), commit 828302cf581577b906d37ba8cd783cadb6e9c44e. Ported to GLSL and JavaScript from its BendingGrass shader (the 16-slot bend loop: horizontal distance, linear bend power, bend direction, top-corner offset, and the two-pass draw with alpha test at 0.95 then alpha blend below it) and from GrassBendingManager with BenderPriorityComparer (sort by priority, lower first, first 16 sent, the rest get nothing). The repo's character model is not used.

Render framework: three.js by mrdoob and contributors, https://github.com/mrdoob/three.js, MIT license, r187. Used for drawing, instancing and shadows. It is not the source of the bending technique.

My own code: the meadow scene, procedural grass tuft, ground and stone textures, the ball, capsule and pebble movement, the bender debug rings, camera moves, labels and video. No external models or assets.