The blade
A field of flat cards, all facing you
Every blade is a quad with four vertical segments, and it is not standing in the world. It is built directly in view space β the vertex shader takes the blade's world position, converts that one point to view coordinates, and adds a purely 2D card to it.
That is what keeps a billboard honest: there is no rotation to compute and no chance of catching one edge-on, because the card was never oriented in the world to begin with. Fifty-four thousand of them go down in one instanced pass, with five per-instance attributes: world position, size, seed, id, and a roll that promotes 1.4% of blades to flowers and another 1.6% to seed-heads.
The scatter is a sunflower distribution β each blade placed at the golden angle from the last, at a radius proportional to the square root of its index β which fills a disc evenly with no clumping and no grid. Blade height then fades toward the rim by 1 β (r/R)Β³, so the meadow thins out into the fog instead of ending at a hard circle.
Props get their footprints for free. A hole is a circle with a radius; blades vanish inside it and shorten across a fringe half again as wide, so a standing stone sits on bare ground rather than clipping through a hedge.
The roots are darkened by mix(0.68, 1.0, smoothstep(0, 0.55, uv.y)). It is one line, and it is the difference between a lawn and a thicket.
420 blades from the engine’s own buildGrass, over its own generated textures.
The wind
The wind is a picture
There is no wind simulation and no per-blade oscillator. Each blade reads a scrolling noise texture at its own world position and leans by what it finds there.
Bright means lean far, dark means stand up, and because neighbouring blades sample neighbouring pixels, a gust travels across the meadow as the texture drifts. Two copies are sampled β one rotated +10Β°, one β10Β°, at slightly different scales and speeds β and multiplied. That product is what stops the pattern from ever visibly tiling: two periodic functions at incommensurate angles have a beat far longer than either.
The result is remapped to β1β¦1 and used as an angle about an axis perpendicular to the wind direction, up to 42Β° at full gust. Switch on the wind debug view in the figure above and the shader paints that value straight to the screen: what you see is the actual picture the blades are reading.
The cloud shadows are the same idea aimed upward β two rotated noise reads, multiplied, sampled where a ray from the ground meets a plane 42 metres up. Weather and wind are one technique used twice.
Hands on
Try it
The whole meadow: procedural textures, sky, ground, props, the grass field, willow curtains, a duck walking a figure-of-eight and a couple of rabbits. Watch the wake open behind the duck and close again.
The actual buildGrass, buildGround, CameraRig and PixelPass, imported by this page. The demo itself is wanderable.
The lie
Faking the third dimension
A real blade leaning away from you should foreshorten: its far edge crowds toward its near edge. A flat card has no depth to do that with, so the fragment shader shears the texture instead.
It scales the horizontal texture coordinate by (1 β uv.y) Γ persp + 1 β full effect at the base, none at the tip β where persp is the blade's own wind value times 0.32. So a blade leaning hard gets a visibly tapered silhouette, and one standing straight gets none.
One factor makes it work rather than merely happen: persp is multiplied by the dot product of the camera's forward direction against the wind's. Look along the wind and the shear is at full strength, which is where foreshortening would really show; look across it and the shear falls to nothing, which is where a leaning blade genuinely keeps its width. The lie switches itself off exactly when it would be caught.
Character displacement gets the same treatment from the other axis: a blade pushed away from you shears by how much it is leaning toward or from the camera, so the wake reads as depth rather than as sliding.
The clock
Time in stop-motion
Smooth animation reads as digital. So the shader's clock is quantised: t = floor(t Γ fps + 0.5) / fps, at seven frames a second by default.
Everything downstream of that line β the wind sample, the idle sway, the noise scroll β snaps to the same seven steps, and the meadow gains the hand-animated stutter of stop-motion. The whole scene still renders at sixty; it is only the content of the animation that is coarse.
Doing that naively would make every blade change on the same frame, which reads as a strobe. Each blade instead offsets time by mod(10 Γ seed, 1/fps) before the quantisation β a phase inside a single frame β so the field updates continuously while every individual card is stepping. Drop the slider to two frames a second in the figure above and it becomes claymation without ever becoming a flicker.
The stepped clock and the noise wind depend on each other. A stepped clock over a sine would look broken; over a texture that is already spatially coherent, it looks drawn.
Colour and light
A hundred greens and four levels of sun
One flat green would read as artificial turf. Two more noise textures, at scales of 0.02 and 0.012 in world units, paint slow patches across the field: above 0.62 in the first a blade takes a darker green, above 0.7 in the second a paler one.
The ground beneath reads the same two textures at the same scales, so the gaps between blades match the blades. That agreement is most of what makes the meadow read as one surface rather than as cards standing on a mat.
Lighting is banded rather than smooth. The diffuse term is quantised into four steps, with a soft gradient at each boundary whose width is a tunable fraction of a step β a hybrid between hard cel shading and smooth. Then the cloud shadow is folded in with min() rather than multiplied, so a shadow pushes the surface down a whole band instead of dimming it slightly.
A blade claims a normal of straight up regardless of how far it is leaning. It is grass: it is lit by the sky, not by its own geometry, and giving each card an honest normal would make a leaning blade go dark for no reason a viewer could name.
Every texture on this page β the blade sprite, the flower, the seed-head, the wind, the clouds, both colour maps β is generated procedurally at load. There are no art assets in the repository.
The last step is the one that ties the look together, and it is the cheapest of all: the whole scene is rendered into a buffer 320 rows high and blitted back up to the canvas with nearest-neighbour filtering.
That is what turns a field of 3D billboards into pixel art. Every edge lands on a chunky grid, the toon bands become flat blocks of colour, and the stepped clock stops looking like a low framerate and starts looking like a decision. Slide the resolution up to native in the figure and the meadow is still pretty; it is just no longer the same thing.
It is also, incidentally, the performance budget. Rendering 54,000 alpha-tested billboards at 320 rows costs a fraction of what it costs at 1080, which is why the field can be that large in a browser at all.
The wake
Grass that feels you
Every creature in the meadow is handed to the grass shader as four numbers: a world position and a radius. Twelve slots, filled each frame.
Each blade measures its distance to each of those points, normalises by the radius, and takes (1 β d)^1.4 as a strength. The direction it bends is worked out against the camera, not the world: the vector from creature to blade is projected onto the camera's forward and its perpendicular, giving two independent amounts that rotate the card about its local Z and X. That is why the parting always reads correctly however you orbit β the displacement is authored in the same space the card is.
Twelve slots is a hard cap, and it is a budget rather than a limit on the cast. The player takes slot zero, the ducks and rabbits fill up from one, and anything past twelve simply does not part the grass. During a conversation the farthest creature's slot is lent to the camera β the two-shot puts the lens down among the blades, so they have to open in front of it.
A conversation also opens a clearing: blades inside a disc between the two speakers crouch to 40% of their height, because a face-to-face shot of a rabbit is no good if the rabbit is behind the lawn.
The bill
What this approach costs
Every technique here trades correctness for a look, deliberately. The trades are what make a meadow of this size run in a browser, and each of them has an edge you can find.
Nothing has depth. Blades do not occlude each other correctly at grazing angles, cannot cast shadows on one another, and have no thickness. Put the camera at ground level and the field is a stack of cut-outs, which is exactly what it is.
The normal is a lie, and it has to be. Every blade claims it is facing straight up. A second light source from a different direction would light the whole meadow as one flat surface, because as far as the shader is concerned it is one flat surface.
The fake perspective only works from one axis. It is scaled by how closely the camera aligns with the wind, which is what keeps it from being caught β but the price is that the effect is strongest exactly when the wind is blowing toward or away from you, and absent when it crosses. It is a correction that knows when to hide, not a correction that is right.
Twelve creatures displace grass; a thirteenth does not. The uniform array is fixed at compile time, and there is no spatial culling to decide which twelve matter most β it is first come, first served through the cast list.
The field is a disc, and it is finite. The blades are scattered once at load into a 74-metre radius and never regenerated. Fog and the rim height falloff hide the boundary; walking to it would find it.
The clock is coarse for everything or nothing. Quantising time is a single uniform read by the whole field, so a blade cannot step while a flower moves smoothly. It suits this meadow and would not suit a scene with anything precise in it.
The techniques are not original here: this is a from-scratch WebGL reimplementation of the rendering ideas in Dylearn's 3D Pixel Art Grass Demo, written in Godot. No code or assets were copied β the shaders were written from the technique up β and the debt is real and gladly stated.
Six cheats, one instanced draw call, and no art files. The meadow is entirely made of things that are not what they appear to be, which is the oldest trick in the medium.