• About
  • Experience
  • Education
  • Awards
  • Projects
  • Skills
  • Gallery
Loading scene...

Procedural Grassland

2024
Three.jsGLSLProcedural GenerationWebGL

A real-time procedural grassland with volumetric clouds, mountains, and 100k instanced grass blades — all procedural, no pre-baked assets. Try the post-processing filters above the writeup to see Kuwahara, spread, pixelate, and edge detection effects applied in real time.

The scene has five elements: a sky dome, terrain plane, instanced grass, volumetric clouds, and a mountain range. Everything is procedural. The pipeline targets 30 fps with frame-rate compensation.

100,000 blades are rendered using a single InstancedMesh. Each blade is a 2D ShapeGeometry defined by bezier curves, keeping vertex count low while maintaining a natural tapered shape.

Positions are sampled from the terrain using a custom DistanceBasedMeshSurfaceSampler that extends Three.js's MeshSurfaceSampler. It biases density toward the camera via inverse-distance weighting: 1 / (d^0.96 + 1), so the foreground gets the densest coverage. Blades outside the frustum are rejected at placement time.

Wind is done entirely in the vertex shader. Each blade is bent along a circular arc parameterized by inverse radius r_inv, driven by three octaves of simplex noise FBM. A time-varying noise layer makes the wind shift over time. The math maps each vertex height k to (sin(k * r_inv), cos(k * r_inv)).

Per-instance color comes from a hand-picked palette interpolated via Perlin noise FBM at each blade's world position. Lighting uses Blinn-Phong with normals recomputed per-fragment via dFdx/dFdy derivatives, since the vertex shader displacement invalidates the originals. Cloud shadows are sampled from the 3D noise texture.

Clouds use GPU-computed volumetric rendering. A custom GPUComputationRenderer3D generates a 256×256×256 3D noise texture slice-by-slice. Each slice combines Perlin and Worley noise:

  1. Billowy Perlin: 7-octave FBM, remapped with abs(x * 2 - 1) for billowy shapes
  2. Worley FBM: 4 layers of tileable Worley noise at increasing frequencies
  3. Final density: remap(perlinWorley, worleyFBM - 1, 1, 0, 1) with coverage cutoff

This follows Andrew Schneider's Real-Time Volumetric Cloudscapes from GPU Pro 7.

Rendering ray-marches through a box volume in 15 steps, sampling the 3D texture at each point. The noise scrolls over time for drift. A height gradient sculpts density — thinner near top and bottom. Jitter via Wang hash reduces banding. Shading uses directional derivatives (sampling density at two offset points) to approximate light scattering.

The mountain range uses Diffusion-Limited Aggregation (DLA) to generate a heightmap. Particles undergo random walks until they contact an existing cluster, forming fractal branching structures. The result is iteratively upscaled (7 expansion passes, doubling resolution each time with random perturbation). Each pass accumulates onto a canvas, blending layers.

A radial falloff tapers the edges. The texture serves as both color map and displacement map on a subdivided plane (200×200 segments), creating a mountain silhouette at the horizon.

The sky is an inverted sphere with a vertical gradient (dark blue top, light blue horizon), rendered with MeshBasicMaterial so fog doesn't affect it.

Terrain is a 10,000×10,000 unit subdivided plane with vertex displacement from low-frequency Perlin noise, creating gentle rolling hills. Blinn-Phong shading with a warm sandy color. Distance fog (THREE.Fog) between 5,450–5,550 units softens the terrain-sky transition.

Rendering goes through an EffectComposer pipeline with four toggleable ShaderPass filters:

  1. Kuwahara — Edge-preserving smoothing that produces a painterly/oil-painting look. Computes mean and variance across four quadrants of a local window, picking the quadrant with lowest variance.
  2. Spread — Replaces each pixel with a random nearby pixel within a configurable radius, creating a scattered/diffused texture reminiscent of pointillist brushwork.
  3. Pixelate — Snaps UV coordinates to a grid, producing a mosaic/pixel-art effect.
  4. Edge Detection — Sobel operator computing horizontal and vertical gradients, blended with the original image to overlay detected edges.

A ResizeObserver dynamically adjusts the renderer to match the container width while maintaining a 4:3 aspect ratio. Filters can be toggled via the controls above the scene.

  • GPU Pro 7: Real-Time Volumetric Cloudscapes (A. Schneider)
  • Simplex Noise (Ashima Arts / stegu)
  • Hash without Sine (David Hoskins, Shadertoy)
  • Diffusion-Limited Aggregation
  • Three.js InstancedMesh
  • Three.js MeshSurfaceSampler
© 2026 Ming Chong Lim