Generating grass in compute graphic using procedural texture patterns and ray-marching.
Grass Ray-Marching
There are various ways of generating grass in computer graphics, such as
• Shells (layers) - which are multiple planes along the same direction with the same graphic creating the illusion of grass. The texture for the layers can be generated using procedural noise.
• Billboarding - you can create 2d quads scattered around the scene with grass mapped to the surface - the quads are billboarded so they always face towards the camera. The billboards can be deformed (bent) using trigonometric functions to create swaying and bending effects.
• Mesh models - you can render large number of mesh models using instancing and the geometry shader - so you generate 1 or a few dozen blades - then you can instance them hundreds of thousands or millions of times around the scene. The simplest way is instancing which is supported by most technologies - there is also the geometry shader which is supported with Vulkan and DirectX which allows you to generate geometry dynamically on the shader.
• Signed Distances Functions (SDF) - the blades of grass can be modelled using sdf functions (e.g., tubes or adding/differencing shapes like ellipsoids to create pointy blades that can be instanced lots of times). In ray tracing you can use modulus (repeating) to do this cheaply and quickly - creating entire worlds of grass without costing much comptuationally (or placing them around the scene using sdf functions).
In this tutorial/example we'll be using:
• Ray-marching with a height texture - this technique is similar to parallax mapping - we'll generate a texture and map it to a plane - then we'll ray-march to find the height for the the ray intersection (i.e., height of the grass at that point).
What is Parallax Mapping In a nutshell, parallax mapping is a technique used in 3D computer graphics to create the illusion of depth on textured surfaces by offsetting the texture coordinates based on a height map.
Parallax mapping textures is also known by several other terms, including: Offset mapping, Virtual displacement mapping, Height mapping, Displacement mapping, Parallax occlusion mapping and Relief mapping.
As we go through various examples and show code snippets and images - you can find the links for the demos/code and other information at the end.
Start Simple (Hello Shader)
So that you are able to follow this tutorial through from the beginning - even if you're very new to graphical programming - let's start with a minimal working fragment shader.
The complete effect will be done on the fragment shader in WGSL (webgpu shader language).
Fragment shader for drawing the texture coordinates as color.
We going to conver these texture coodinates into regions (also called cells). We can then draw a small circle in the centre of each cell (and some horizontal and vertical lines to help show the edges).
The steps for accomplishing this are:
• Scale the uv coordinates by
4.0
(instead of
0.0
to
1.0
it goes from
0.0
to
4.0
)
• Get the cell values using the
floor(..)
and
fact(..)
functions.
• Once we've got the cell sizes and positions - we can draw lines and a dot in the centre of each cell.
Convert the uv quad into a set of smaller sub-cell quadrants.
@fragment
fn main(@location(0) fragCoord : vec2<f32>) -> @location(0) vec4<f32>
{
var uv = fragCoord; // 0-1 full screen
uv *= 4.0;
var i = floor( uv ); // uv - 0, 1, 2, 3,
var f = fract( uv ); // uv - 0-1, 0-1, 0-1, 0-1
let cellSize = vec2(1.0);
var color = vec3(1.0); // background white
// red dot in middle of each square
let centreDot = i + cellSize*0.5;
let sizeOfDot = 0.04;
if ( length( uv - centreDot ) < sizeOfDot )
{
color = vec3(1.0, 0.0, 0.0); // red
}
// draw edges for each square
let lineThickness = 0.01;
if (abs(f.x) < lineThickness || abs(f.x - 1.0) < lineThickness ||
abs(f.y) < lineThickness || abs(f.y - 1.0) < lineThickness)
{
color = vec3(0.0);
}
return vec4<f32>( color, 1.0 );
}
Random Cell Positions
This is the beautiful thing - we can mix in a bit of randomness so the centre of each cell is offset by a small amount.
Add a simple dirty random function - and an extra line that offsets the centre by a random amount (-0.45 to 0.45).
Randomizing the cell centres so they're less uniform.
fn random(uv: vec2<f32>) -> f32 {
return fract(sin(dot(uv, vec2<f32>(12.9898, 78.233))) * 43758.5453);
}
@fragment
fn main(@location(0) fragCoord : vec2<f32>) -> @location(0) vec4<f32>
{
var uv = fragCoord; // 0-1 full screen
uv *= 4.0;
var i = floor( uv ); // uv - 0, 1, 2, 3,
var f = fract( uv ); // uv - 0-1, 0-1, 0-1, 0-1
let cellSize = vec2(1.0);
var color = vec3(1.0); // background white
// red dot in middle of each square
let centreDot = i + cellSize*0.5 + (-1.0 + 2.0*random(i))*0.45;
let sizeOfDot = 0.04;
if ( length( uv - centreDot ) < sizeOfDot )
{
color = vec3(1.0, 0.0, 0.0); // red
}
// draw edges for each square
let lineThickness = 0.01;
if (abs(f.x) < lineThickness || abs(f.x - 1.0) < lineThickness ||
abs(f.y) < lineThickness || abs(f.y - 1.0) < lineThickness)
{
color = vec3(0.0);
}
return vec4<f32>( color, 1.0 );
}
We can take this even further! Randomize the size of each centre dot - so instead of it being perfectly 0.04 - let's addin some more randomness so they size deviates.
Add in some randomness to the dot size.
Visitor:
Copyright (c) 2002-2026 xbdev.net - All rights reserved.
Designated articles, tutorials and software are the property of their respective owners.