In the previous part, Grass Rendering Series Part 2: Full-Geometry Grass in Godot, we worked on placing and shading blades of grass to get a realistic-looking field of grass into our scene. It looks nice, but it’s completely static: it looks good on screenshots, but in a game, you’d want your grass to look lively and react to what’s happening around it.
Therefore, in this third part, we’ll add three ways of animating and interacting with the grass: wind, displacement, and cutting.
You can get the full Godot project here: https://git.hexaquo.at/karl/godot-grass/src/commit/a198b71a26584837d6351e3ac02219823b9de505
Blowing Gusts of Wind Through the Grass
Looking at a large field of grass while a decent wind is blowing, you might notice a similarity to waves as gusts move through it. Wind neither simply shakes the grass blades around, nor does it bend the blade continuously. Rather, as a gust of wind moves through the meadow, it strongly bends and shakes the grass where it is currently strongest; once it has passed, the stalks stand mostly upright and shake only lightly for a moment, until another gust hits them.
We could try to come up with a mathematical solution for this, combining sines and cosines and similar functions to simulate the different scales of gusts and shakes and bends. However, the easier solution is to just use a noise texture, similarly to how a water shader would be implemented (and also similarly to how we implemented patchiness in the last chapter). By using the right settings, we can get small-scale noise for the continuous shaking as well as large-scale structures for gusts of wind. So we’ll add a new uniform sampler2D wind_noise
to the shader and fill it with a NoiseTexture2D. I find that a Simplex Smooth noise with the fractal Type set to Ridged does a good job at simulating gusts; the amount of small shaking in-between can be controlled by varying the fractal Gain.
Now, we once again want to read from this texture at world space, so we’ll need NODE_POSITION_WORLD.xz
. We also need to animate the texture though: it shouldn’t remain static, like the patches of grass, but it should move through the field. Therefore, we’ll offset the position based on TIME
, a wind direction, and a wind speed.
The code might look like this, before our previous vertex logic:
uniform sampler2D wind_noise;
uniform float wind_strength = 0.1;
uniform vec2 wind_direction = vec2(1.0, 0.0);
void vertex() {
bottom_to_top = 1.0 - UV.y;
vec2 wind_position = NODE_POSITION_WORLD.xz / 10.0;
wind_position -= TIME * wind_direction * wind_strength;
current_wind_bend = texture(wind_noise, wind_position).x;
[...]
}
Note that we subtract, rather than add, from the wind_position
because, from the perspective of the noise texture, the blades of grass are moving to the left. (This doesn’t matter yet, but the direction should align with the bend direction we’ll implement later.)
This gives us the power of the current wind gust at the position of the current grass blade. Since we read it from a texture, it’s scaled between 0.0 and 1.0. We might also want to scale it by wind_strength
, since the wind speed should not only move the gusts more quickly, but also cause a more significant bend. In addition, the bend should be strongest at the tip and zero at the bottom, since the grass should remain rooted in the ground. We have our bottom_to_top
parameter for this. Therefore, we can add these lines:
current_wind_bend *= wind_strength;
current_wind_bend *= bottom_to_top * 2.0;
This gives us a factor which tells us how much to offset the current vertex; however, we don’t yet have a direction. We could do the same thing we did with the general grass blades’ bend and simply subtract it from VERTEX.z
:
This does work and create a wind-like effect, but it looks a bit odd since we’re applying the offset in model space rather than world space. This means that the bend is relative to that grass blade’s rotation, or in other words, it’s applied before the random rotation is applied via the MultiMesh. Simulating different growth directions in this way makes sense: grass blades should grow into somewhat random directions, so there, we want to offset the vertex relative to the random rotation. However, applying wind the same way makes it look as if the grass blades are dancing rather than being blown by the wind. Instead, wind should have the same direction globally, depending on a global uniform vec2 wind_direction
.
Transforming Between Spaces
Switching between different coordinate systems is a fairly common problem in shader programming. In this case, we have the wind direction in world space, but we want it to be in model space, so that we can apply it to the vertex (which is also in model space). Whenever we want to transform between spaces, we need a matrix, since that’s what matrices do: transform from one coordinate system to the other.
The Godot shader documentation tells us that the MODEL_MATRIX
transforms a vector from model space to world space. This is the opposite of what we want; therefore, we need to invert this matrix, which we can do with inverse(MODEL_MATRIX)
.
The code for applying the wind bend based on the global wind direction, transformed into a local direction, looks like this:
uniform float wind_bend_strength = 2.0;
[...]
void vertex() {
[...]
mat4 inv_model = inverse(MODEL_MATRIX);
vec2 local_direction = (inv_model * vec4(wind_direction.x, 0.0, wind_direction.y, 0.0)).xz;
VERTEX.xz += current_wind_bend * local_direction;
[...]
}
We invert the model matrix to get the world-to-model transform, and we multiply the wind direction vector with the resulting matrix, since that’s how such a transformation is applied. Because it’s a 4-dimensional matrix, we need to turn the wind direction into a 4-dimensional vector. The y-coordinate of this vector is 0.0, since wind shouldn’t blow upwards or downwards. The fourth component of such matrix transformations in 3D space specifies whether an offset should also be applied (i.e. the world position of this grass blade). Since the wind direction should be an abstract direction and not a position, we use 0.0 in this fourth component.
With all grass blades now bending into the same direction, it’s starting to look like proper wind:
There’s one more thing we can do to further accentuate the wind: when grass is bent down by the wind, there’s more self-shadowing, since the grass is denser there. Therefore, grass is probably darker when it is currently bent being bent strongly by the wind. We can add this to our ambient occlusion logic:
AO = bottom_to_top - current_wind_bend * wind_ao_affect;
And now, we really get that wave-like effect we were going for:
With some adaptations, the same logic could also be used for other objects in the game such as trees. By accessing the same wind texture at world space, gusts of wind are synchronized between grass and everything else in the level.
Making Grass React to an Object
Wind already does a lot to make the grass feel alive and part of the virtual world. However, games live through interaction, and by allowing interaction with the grass, it can feel even more tangible. Here, we’ll implement a simple displacement logic: making the grass bend away from a point. This looks best for spheres, but it can be added as a subtle effect for all kinds of objects.
Making grass bend away from an object entails two things: firstly, the bend direction should be from the object’s center to the current grass blade’s position. Secondly, the amount of the bend should transition smoothly from a very strong bend at the center to a very light bend further away from the object, depending on a given radius.
In mathematical terms, we therefore need a factor which is strongest (1.0) close to the object and decreases to 0.0 after a certain radius. Here’s how we might calculate this (right below the wind logic):
uniform float object_radius = 1.0;
uniform vec3 object_position;
[...]
void vertex() {
[...]
float object_distance = distance(object_position, NODE_POSITION_WORLD);
float bend_away_strength = max(object_radius - object_distance, 0.0) / object_radius;
[...]
}
The direction is just the vector from the object to the current grass blade, normalized to length 1.0 so that we can scale it independently:
vec2 bend_direction = normalize(object_position.xz - NODE_POSITION_WORLD.xz);
Applying this puts us in front of a similar problem as the wind: we have a world space bend direction which we must apply to a model space vertex. Therefore, we must once again multiply our bend direction by the inverse model matrix (the world space to model space transform). We also multiply by a bend strength and by our bottom_to_top
parameter in order to increase the bend towards the tip of the grass.
VERTEX.xz -= (inv_model * vec4(bend_direction.x, 0.0, bend_direction.y, 0.0)).xz
* bend_away_strength * bottom_to_top;
Lastly, grass should not only be bent sideways, but also towards the ground the closer we are to the object’s center:
VERTEX.y -= bend_away_strength * bottom_to_top * 0.5;
And we’re done! This allows one object to interact with the grass; for multiple objects, just turn the object radius and position into arrays of the appropriate size.
You can attach a script to the grass MultiMesh to automatically update the object’s position each frame. For example, assuming the MultiMesh has a child MeshInstance3D which should interact with the grass:
func _process(delta: float) -> void:
material_override.set_shader_parameter("object_position", $MeshInstance3D.position)
Running that script as a @tool
allows you to see it right in the editor:
Cutting Grass
The last interaction we’ll implement is cutting grass. This will just be a quick sketch, but the concept opens up lots of possibilities.
Essentially, cutting grass means that vertices above a certain height should be removed. Actually removing vertices is not really possible in a vertex shader (it only processes vertices, it can’t create or delete them), so we’ll simply move vertices down if they exceed a certain height we name cut_height
. We need to do this right at the start of the vertex shader, since the following logic should react to this cutting functionality.
[...]
varying float cut_height;
void vertex() {
cut_height = 1.0;
VERTEX.y = min(VERTEX.y, cut_height);
[...]
}
We need to do something similar to the UV coordinates in order to make the rest of the bending and lighting logic work and in order to avoid these moved-down top vertices to act as if they’re higher up, i.e. bending more than they should. UV.y
is 1.0 at the bottom, therefore we need UV.y
to range between cut_height
and 1.0:
UV.y = max(UV.y, 1.0 - cut_height);
When grass is cut, more light can reach it; therefore, we might also want this to affect the ambient occlusion. The simplest solution is to just scale the AO_LIGHT_AFFECT
by our cut height:
AO_LIGHT_AFFECT = cut_height;
If a game requires the player to be able to cut the grass dynamically at any location - for example, when swinging a sword, like in Breath of the Wild - we could write “grass cut” events into a texture and use that for the cut_height
, again reading it in world space. This general approach of using on-the-fly utility textures for shader logic is introduced here: Rendering to Textures: Shadows, Cubemaps, and Special Effects
That’s it for interaction! In the next part, we’ll add a LOD system to our grass chunks in order to make it viable for a large-scale open world game.