Skip to main content Accueil Créateurs ck42bb procedural-grass-threejs procedural-grass
procedural-grass Generate procedural grass fields in Three.js using WebGPU compute with automatic WebGL2 fallback. Covers instanced blade geometry with bezier-curve profiles, multi-layer wind simulation, subsurface scattering approximation, distance-based LOD and density falloff, interactive displacement from players/objects, and configurable grass types (lawn, meadow, wheat, reeds, savanna, tundra). Use when building grass systems, fields, meadows, prairies, ground cover vegetation, or any scene requiring dense animated plant coverage. Triggers: "procedural grass", "grass field", "grass blades", "meadow", "grass rendering", "grass shader", "wind grass", "instanced grass", "grass LOD", "ground cover", "lawn", "wheat field", "tall grass".
Aller à l'installation Skills Marketplace Découvrez et explorez les compétences IA créées par la communauté.
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Copier le promptAfficher les détails du prompt Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
npx skills add https://github.com/CK42BB/procedural-grass-threejs --skill procedural-grassLa commande reste sur une seule ligne. Faites défiler horizontalement pour la vérifier avant de la copier.
Vous préférez une copie locale ? Téléchargez les fichiers actuellement disponibles dans SkillsMP.
Télécharger Zip Téléchargement...
Métiers associés SOC
Basé sur la classification professionnelle SOC
name procedural-grass description Generate procedural grass fields in Three.js using WebGPU compute with automatic WebGL2 fallback. Covers instanced blade geometry with bezier-curve profiles, multi-layer wind simulation, subsurface scattering approximation, distance-based LOD and density falloff, interactive displacement from players/objects, and configurable grass types (lawn, meadow, wheat, reeds, savanna, tundra). Use when building grass systems, fields, meadows, prairies, ground cover vegetation, or any scene requiring dense animated plant coverage. Triggers: "procedural grass", "grass field", "grass blades", "meadow", "grass rendering", "grass shader", "wind grass", "instanced grass", "grass LOD", "ground cover", "lawn", "wheat field", "tall grass".
Procedural Grass
Generate dense, animated, visually rich procedural grass in Three.js with a WebGPU-first
pipeline and automatic WebGL2 fallback.
Architecture Overview
┌──────────────────────────────────────────────────────┐
│ Grass Pipeline │
│ │
│ 1. Blade Geometry ── bezier-curved triangle strip │
│ 2. Placement ─────── terrain-aware scatter + density │
│ 3. Instancing ────── InstancedMesh / storage buffer │
│ 4. Wind ──────────── layered noise displacement │
│ 5. Shading ───────── SSS approx + color variation │
│ 6. LOD ───────────── density fade + blade simplify │
│ 7. Interaction ───── radial push from world objects │
├──────────────────────────────────────────────────────┤
│ WebGPU path: compute placement + storage buffers │
│ WebGL path: CPU placement + InstancedMesh │
└──────────────────────────────────────────────────────┘
Blade Geometry
Each grass blade is a tapered triangle strip shaped along a quadratic bezier curve.
This gives natural curvature with minimal vertex count.
Blade Mesh Generator
function createBladeGeometry (segments = , width = , height = , curvature = ) {
vertCount = (segments + ) * + ;
positions = (vertCount * );
uvs = (vertCount * );
indices = [];
( i = ; i <= segments; i++) {
t = i / segments;
x = * ( - t) * t * curvature;
y = t * height;
w = width * ( - t * );
vi = i * ;
positions[(vi) * ] = x - w * ;
positions[(vi) * + ] = y;
positions[(vi) * + ] = ;
uvs[(vi) * ] = ;
uvs[(vi) * + ] = t;
positions[(vi + ) * ] = x + w * ;
positions[(vi + ) * + ] = y;
positions[(vi + ) * + ] = ;
uvs[(vi + ) * ] = ;
uvs[(vi + ) * + ] = t;
}
tipIdx = (segments + ) * ;
tipX = * * * curvature;
positions[tipIdx * ] = curvature * ;
positions[tipIdx * + ] = height;
positions[tipIdx * + ] = ;
uvs[tipIdx * ] = ;
uvs[tipIdx * + ] = ;
( i = ; i < segments; i++) {
a = i * , b = i * + , c = (i + ) * , d = (i + ) * + ;
indices. (a, b, c, b, d, c);
}
lastL = segments * , lastR = segments * + ;
indices. (lastL, lastR, tipIdx);
geometry = . ();
geometry. ( , . (positions, ));
geometry. ( , . (uvs, ));
geometry. (indices);
geometry. ();
geometry;
}
4
0.06
1.0
0.3
const
1
2
1
const
new
Float32Array
3
const
new
Float32Array
2
const
for
let
0
const
const
2
1
const
const
1
0.8
const
2
3
0.5
3
1
3
2
0
2
0
2
1
1
3
0.5
1
3
1
1
3
2
0
1
2
1
1
2
1
const
1
2
const
2
0.5
0.5
3
0.5
3
1
3
2
0
2
0.5
2
1
1.0
for
let
0
const
2
2
1
1
2
1
2
1
push
const
2
2
1
push
const
new
THREE
BufferGeometry
setAttribute
'position'
new
THREE
BufferAttribute
3
setAttribute
'uv'
new
THREE
BufferAttribute
2
setIndex
computeVertexNormals
return
Segment count guide : 3 segments for distant LOD, 4–5 for mid-range, 6–8 for close-up hero grass.
Instance Data Layout Each blade instance stores placement and variation data packed into instance attributes.
function createGrassInstanceData (count ) {
return {
positionRotation : new Float32Array (count * 4 ),
scaleAndVariation : new Float32Array (count * 4 ),
};
}
Placement System
CPU Placement (WebGL path) Scatter blades on terrain with density modulation, slope rejection, and jittered grid for
uniform distribution without clumping.
function placeGrassOnTerrain ({
terrainSize, maxHeight, heightFn, noiseFn,
density = 40 , // blades per unit² at max density
minHeight = 0.05 , // normalized terrain height
maxSlopeAngle = 0.6 , // radians, reject steep slopes
seed = 0 ,
} = {} ) {
const gridStep = 1 / Math .sqrt (density);
const halfSize = terrainSize / 2 ;
const instances = [];
let rng = seed;
const random = ( ) => { rng = (rng * 16807 + 0 ) % 2147483647 ; return rng / 2147483647 ; };
for (let gx = -halfSize; gx < halfSize; gx += gridStep) {
for (let gz = -halfSize; gz < halfSize; gz += gridStep) {
const x = gx + (random () - 0.5 ) * gridStep;
const z = gz + (random () - 0.5 ) * gridStep;
const nx = x / terrainSize + 0.5 ;
const nz = z / terrainSize + 0.5 ;
if (nx < 0 || nx > 1 || nz < 0 || nz > 1 ) continue ;
const h = heightFn (nx, nz);
if (h < minHeight) continue ;
const eps = gridStep * 0.5 ;
const hx = heightFn (nx + eps / terrainSize, nz);
const hz = heightFn (nx, nz + eps / terrainSize);
const slope = Math .atan (Math .sqrt ((hx - h) ** 2 + (hz - h) ** 2 ) * maxHeight / eps);
if (slope > maxSlopeAngle) continue ;
const densityNoise = noiseFn (x * 0.05 , z * 0.05 );
if (densityNoise < -0.2 ) continue ;
if (random () > (densityNoise * 0.5 + 0.7 )) continue ;
const y = h * maxHeight;
const rotation = random () * Math .PI * 2 ;
const scaleX = 0.7 + random () * 0.6 ;
const scaleY = 0.6 + random () * 0.8 ;
const tilt = (random () - 0.5 ) * 0.3 ;
const colorVar = random ();
instances.push ({ x, y, z, rotation, scaleX, scaleY, tilt, colorVar });
}
}
return instances;
}
Building the InstancedMesh function buildGrassField (instances, bladeGeometry, material, maxCount ) {
const count = Math .min (instances.length , maxCount);
const mesh = new THREE .InstancedMesh (bladeGeometry, material, count);
const posRot = new Float32Array (count * 4 );
const scaleVar = new Float32Array (count * 4 );
for (let i = 0 ; i < count; i++) {
const inst = instances[i];
posRot[i * 4 ] = inst.x ;
posRot[i * 4 + 1 ] = inst.y ;
posRot[i * 4 + 2 ] = inst.z ;
posRot[i * 4 + 3 ] = inst.rotation ;
scaleVar[i * 4 ] = inst.scaleX ;
scaleVar[i * 4 + 1 ] = inst.scaleY ;
scaleVar[i * 4 + 2 ] = inst.tilt ;
scaleVar[i * 4 + 3 ] = inst.colorVar ;
}
const geo = mesh.geometry .clone ();
geo.setAttribute ('aPositionRotation' ,
new THREE .InstancedBufferAttribute (posRot, 4 ));
geo.setAttribute ('aScaleVariation' ,
new THREE .InstancedBufferAttribute (scaleVar, 4 ));
mesh.geometry = geo;
mesh.frustumCulled = false ;
return mesh;
}
Wind System Multi-layered wind combines a global directional flow, turbulent gusts, and per-blade
high-frequency flutter.
class WindSystem {
constructor ( ) {
this .direction = new THREE .Vector2 (1 , 0.3 ).normalize ();
this .baseStrength = 0.4 ;
this .gustStrength = 0.8 ;
this .gustFrequency = 0.3 ;
this .time = 0 ;
}
update (deltaTime ) {
this .time += deltaTime;
}
getUniforms ( ) {
return {
windTime : this .time ,
windDir : this .direction ,
windBase : this .baseStrength ,
windGust : this .gustStrength ,
windGustFreq : this .gustFrequency ,
};
}
}
Wind is applied in the vertex shader — see references/blade-shaders.md for the full
multi-layer wind displacement implementation.
Global sway : Low-frequency sinusoidal along wind direction. Affects all blades uniformly.
Gust waves : Medium-frequency noise waves that roll across the field, creating visible "wind fronts".
Turbulence : Per-blade high-frequency flutter from hash-based variation.
Height modulation : Displacement scales with blade UV.y² — roots stay fixed, tips move most.
Shading
Grass Material (WebGL — ShaderMaterial) The grass shader handles:
Per-instance color variation (base + tip gradient)
Subsurface scattering approximation (light through blades)
Ambient occlusion at blade roots
Distance fade to alpha for LOD blending
function createGrassMaterial (params = {} ) {
const {
baseColor = new THREE .Color (0x3a7d2c ),
tipColor = new THREE .Color (0x8bbf40 ),
dryColor = new THREE .Color (0xc4a84b ),
dryAmount = 0.0 ,
sssStrength = 0.5 ,
aoStrength = 0.6 ,
fadeStart = 60 ,
fadeEnd = 80 ,
} = params;
return new THREE .ShaderMaterial ({
uniforms : {
baseColor : { value : baseColor },
tipColor : { value : tipColor },
dryColor : { value : dryColor },
dryAmount : { value : dryAmount },
sssStrength : { value : sssStrength },
aoStrength : { value : aoStrength },
sunDir : { value : new THREE .Vector3 (0.5 , 0.8 , 0.3 ).normalize () },
sunColor : { value : new THREE .Color (0xfff4e5 ) },
ambientColor : { value : new THREE .Color (0x4488aa ) },
windTime : { value : 0 },
windDir : { value : new THREE .Vector2 (1 , 0.3 ) },
windBase : { value : 0.4 },
windGust : { value : 0.8 },
windGustFreq : { value : 0.3 },
fadeStart : { value : fadeStart },
fadeEnd : { value : fadeEnd },
cameraPos : { value : new THREE .Vector3 () },
pushPositions : { value : [new THREE .Vector3 (), new THREE .Vector3 (),
new THREE .Vector3 (), new THREE .Vector3 ()] },
pushRadii : { value : [0 , 0 , 0 , 0 ] },
},
vertexShader : GRASS_VERT ,
fragmentShader : GRASS_FRAG ,
side : THREE .DoubleSide ,
transparent : true ,
depthWrite : true ,
alphaTest : 0.1 ,
});
}
Full GLSL vertex and fragment shaders are in references/blade-shaders.md.
Grass Node Material (WebGPU — TSL) import { attribute, cameraPosition, color, dot, float as tslFloat, max as tslMax,
mix, normalize as tslNormalize, positionWorld, smoothstep, uniform,
vec2, vec3, vec4, MeshStandardNodeMaterial } from 'three/tsl' ;
function createGrassNodeMaterial (params = {} ) {
const material = new MeshStandardNodeMaterial ();
material.side = THREE .DoubleSide ;
const uv = attribute ('uv' );
const colorVar = attribute ('aScaleVariation' ).w ;
const heightT = uv.y ;
const base = color (params.baseColor ?? 0x3a7d2c );
const tip = color (params.tipColor ?? 0x8bbf40 );
let grassColor = mix (base, tip, heightT);
grassColor = mix (grassColor, color (params.dryColor ?? 0xc4a84b ),
colorVar.mul (tslFloat (params.dryAmount ?? 0 )));
const ao = mix (tslFloat (1.0 - (params.aoStrength ?? 0.6 )), tslFloat (1 ), heightT);
grassColor = grassColor.mul (ao);
material.colorNode = grassColor;
material.roughnessNode = tslFloat (0.8 );
material.metalness = 0 ;
return material;
}
LOD System
Distance-Based Density Rather than rendering all blades at full density everywhere, partition into rings
and reduce count with distance.
class GrassLODManager {
constructor (scene, terrain, options = {} ) {
this .scene = scene;
this .rings = [
{ radius : 20 , density : 1.0 , segments : 5 , label : 'near' },
{ radius : 45 , density : 0.4 , segments : 3 , label : 'mid' },
{ radius : 80 , density : 0.1 , segments : 2 , label : 'far' },
];
this .meshes = [];
this .grassMaterial = options.material ;
}
build (instances ) {
for (const ring of this .rings ) {
const bladeGeo = createBladeGeometry (ring.segments );
const ringInstances = instances.filter (inst => {
const d = Math .sqrt (inst.x ** 2 + inst.z ** 2 );
const prevRadius = this .rings [this .rings .indexOf (ring) - 1 ]?.radius ?? 0 ;
return d >= prevRadius && d < ring.radius ;
});
const thinned = ringInstances.filter ((_, i ) =>
i % Math .round (1 / ring.density ) === 0
);
if (thinned.length > 0 ) {
const mesh = buildGrassField (thinned, bladeGeo, this .grassMaterial , thinned.length );
this .scene .add (mesh);
this .meshes .push (mesh);
}
}
}
dispose ( ) {
for (const mesh of this .meshes ) {
this .scene .remove (mesh);
mesh.geometry .dispose ();
}
this .meshes = [];
}
}
For moving cameras : rebuild LOD rings when camera moves beyond a threshold (e.g. 10 units),
or use shader-based distance fade (simpler, no geometry rebuild):
// In fragment shader — fade alpha by distance
float dist = length(cameraPos - worldPos);
float fade = 1.0 - smoothstep(fadeStart, fadeEnd, dist);
if (fade < 0.01) discard;
gl_FragColor.a *= fade;
Interactive Displacement Push grass aside when players or objects move through it.
class GrassInteraction {
constructor (material, maxPushers = 4 ) {
this .material = material;
this .pushers = [];
this .maxPushers = maxPushers;
}
addPusher (object, radius = 1.5 ) {
if (this .pushers .length >= this .maxPushers ) return ;
this .pushers .push ({ object, radius });
}
update ( ) {
const positions = this .material .uniforms .pushPositions .value ;
const radii = this .material .uniforms .pushRadii .value ;
for (let i = 0 ; i < this .maxPushers ; i++) {
if (i < this .pushers .length ) {
const p = this .pushers [i];
positions[i].copy (p.object .position );
radii[i] = p.radius ;
} else {
radii[i] = 0 ;
}
}
}
}
The vertex shader applies radial displacement away from each pusher — see
references/blade-shaders.md for the implementation.
Complete Scene Assembly import * as THREE from 'three' ;
async function init ( ) {
const canvas = document .querySelector ('#canvas' );
let renderer, gpuAvailable = false ;
try {
const WebGPU = (await import ('three/addons/capabilities/WebGPU.js' )).default ;
if (WebGPU .isAvailable ()) {
const { default : WebGPURenderer } = await import (
'three/addons/renderers/webgpu/WebGPURenderer.js'
);
renderer = new WebGPURenderer ({ canvas, antialias : true });
await renderer.init ();
gpuAvailable = true ;
}
} catch (e) { }
if (!renderer) {
renderer = new THREE .WebGLRenderer ({ canvas, antialias : true });
}
renderer.setSize (innerWidth, innerHeight);
renderer.setPixelRatio (Math .min (devicePixelRatio, 2 ));
renderer.shadowMap .enabled = true ;
const scene = new THREE .Scene ();
scene.background = new THREE .Color (0x87ceeb );
scene.fog = new THREE .FogExp2 (0xc8e6c0 , 0.008 );
const camera = new THREE .PerspectiveCamera (60 , innerWidth / innerHeight, 0.1 , 200 );
camera.position .set (0 , 5 , 15 );
const { OrbitControls } = await import ('three/addons/controls/OrbitControls.js' );
const controls = new OrbitControls (camera, renderer.domElement );
controls.target .set (0 , 1 , 0 );
controls.enableDamping = true ;
const sun = new THREE .DirectionalLight (0xfff4e5 , 1.5 );
sun.position .set (30 , 40 , 20 );
sun.castShadow = true ;
scene.add (sun);
scene.add (new THREE .AmbientLight (0x88aacc , 0.4 ));
scene.add (new THREE .HemisphereLight (0x87ceeb , 0x4a7c3f , 0.3 ));
const ground = new THREE .Mesh (
new THREE .PlaneGeometry (100 , 100 ),
new THREE .MeshStandardMaterial ({ color : 0x3d6b2e , roughness : 0.9 })
);
ground.rotation .x = -Math .PI / 2 ;
ground.receiveShadow = true ;
scene.add (ground);
const noise = createNoise2D (42 );
const heightFn = (nx, nz ) => 0.001 ;
const instances = placeGrassOnTerrain ({
terrainSize : 60 , maxHeight : 1 , heightFn, noiseFn : noise,
density : 30 , minHeight : 0 , maxSlopeAngle : 1.5 ,
});
const bladeGeo = createBladeGeometry (5 , 0.06 , 1.0 , 0.3 );
const grassMat = createGrassMaterial ();
const grassMesh = buildGrassField (instances, bladeGeo, grassMat, 200000 );
scene.add (grassMesh);
const wind = new WindSystem ();
const clock = new THREE .Clock ();
renderer.setAnimationLoop (() => {
const dt = clock.getDelta ();
wind.update (dt);
const wu = wind.getUniforms ();
grassMat.uniforms .windTime .value = wu.windTime ;
grassMat.uniforms .windDir .value .copy (wu.windDir );
grassMat.uniforms .cameraPos .value .copy (camera.position );
controls.update ();
renderer.render (scene, camera);
});
window .addEventListener ('resize' , () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix ();
renderer.setSize (innerWidth, innerHeight);
});
}
init ();
Grass Type Presets Quick-start configurations for different grass types. Full species catalog with
blade profiles, dimensions, and color palettes in references/grass-types.md.
const GRASS_PRESETS = {
lawn : {
width : 0.03 , height : 0.3 , curvature : 0.1 , segments : 3 ,
density : 80 , baseColor : 0x2d7a1e , tipColor : 0x5cb33a ,
dryAmount : 0 , windBase : 0.15 , windGust : 0.2 ,
},
meadow : {
width : 0.06 , height : 1.0 , curvature : 0.3 , segments : 5 ,
density : 35 , baseColor : 0x3a7d2c , tipColor : 0x8bbf40 ,
dryAmount : 0.1 , windBase : 0.4 , windGust : 0.8 ,
},
tallGrass : {
width : 0.08 , height : 1.8 , curvature : 0.5 , segments : 6 ,
density : 20 , baseColor : 0x4a7c3f , tipColor : 0xa8c94e ,
dryAmount : 0.15 , windBase : 0.5 , windGust : 1.0 ,
},
wheat : {
width : 0.04 , height : 1.2 , curvature : 0.6 , segments : 5 ,
density : 50 , baseColor : 0x8b7d3c , tipColor : 0xd4c462 ,
dryAmount : 0.7 , windBase : 0.3 , windGust : 0.6 ,
},
savanna : {
width : 0.05 , height : 0.8 , curvature : 0.2 , segments : 4 ,
density : 15 , baseColor : 0x9b8b4a , tipColor : 0xd4c078 ,
dryAmount : 0.6 , windBase : 0.3 , windGust : 0.5 ,
},
tundra : {
width : 0.04 , height : 0.2 , curvature : 0.05 , segments : 2 ,
density : 25 , baseColor : 0x6b7d4a , tipColor : 0x8b9d5a ,
dryAmount : 0.3 , windBase : 0.6 , windGust : 1.2 ,
},
};
function createGrassFromPreset (presetName ) {
const p = GRASS_PRESETS [presetName];
const geo = createBladeGeometry (p.segments , p.width , p.height , p.curvature );
const mat = createGrassMaterial ({
baseColor : new THREE .Color (p.baseColor ),
tipColor : new THREE .Color (p.tipColor ),
dryAmount : p.dryAmount ,
});
return { geometry : geo, material : mat, preset : p };
}
Performance Guidelines Instance budget by platform :
Platform Max Blades Draw Calls Mobile 50K–100K 1–3 Desktop 200K–500K 1–5 High-end + WebGPU 500K–2M 1–3
Single draw call : InstancedMesh renders all blades in one call. Never create individual meshes.
frustumCulled = false : Wind displacement pushes blades outside bounding box. Disable frustum culling or expand bounds manually.
Geometry reuse : One BladeGeometry shared across all instances per LOD ring.
Avoid per-frame JS loops over instances : All animation happens in shaders via uniforms (time, wind). Instance data is static after placement.
Alpha test over alpha blend : alphaTest: 0.1 avoids costly transparent sorting. Use distance-fade discard in fragment shader.
Shadow casting : Grass rarely needs to cast shadows. Skip castShadow for massive performance gain. If needed, use a simplified shadow-only mesh.
Common Pitfalls
Black grass / no lighting : Grass uses ShaderMaterial which bypasses scene lights. Lighting is computed manually in the fragment shader. Ensure sunDir, sunColor, and ambientColor uniforms are set.
Blades all face same direction : Each instance needs a unique rotation in aPositionRotation.w. Random rotation + some alignment to wind direction looks natural.
Z-fighting with ground plane : Offset blade root Y slightly above ground (0.01 units) or use polygonOffset on ground material.
Popping during LOD transitions : Use alpha-based distance fade rather than abrupt show/hide. Overlap LOD ring boundaries.
Wind looks mechanical : Layer multiple frequencies. Add per-blade phase offset via hash of position. Vary gust speed over time.
References
references/blade-shaders.md — Complete GLSL vertex/fragment shaders for wind, SSS, interaction displacement, and WGSL compute placement.
references/grass-types.md — Detailed species profiles with blade dimensions, color palettes, density settings, and biome associations.