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 fn main(@location(0) fragCoord : vec2<f32>) -> @location(0) vec4<f32> { var uv = fragCoord; // 0-1 full screen
return vec4<f32>(uv, 0.0, 1.0); }
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.
@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).
@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.
Visitor:
Copyright (c) 2002-2024 xbdev.net - All rights reserved.
Designated articles, tutorials and software are the property of their respective owners.