This content provides an in-depth tutorial on creating stylized, dynamic grass shaders in a game engine (GDAU), focusing on advanced techniques for realism and visual appeal, including wind simulation, perspective effects, and reactive movement.
Mind Map
Click to expand
Click to explore the full interactive mind map • Zoom, pan, and navigate
Making grass is one of the first things
many people learn to do when starting
their gamedev journey. Since my last
video came out, a lot of people have
been asking for a more in-depth
explanation on how to recreate the grass
effect. There's a million and one ways
we could make grass in GDAU, but I
wanted to show you the thought process
behind my grass so that you could better
understand the patterns and techniques
that I use to create my shaders and
maybe inspire you to make your own.
Recently, I've made quite a few updates
to the grass, which I think make it look
much better. Let's quickly recap what
stayed the same. Each piece of grass is
spawned on a floor plane using a
multimesh instance node. I'm using quad
meshes and setting them to face their Z
direction upon spawning. so that the
normals are in line with the terrain
they spawn on. This is important to
ensure correct lighting calculations
later on. After spawning, I attach this
tune shader to the quads as a base,
which I'm going to edit to get some
extra effects. We set the albido to our
desired color and select the grass
sprite as our albido texture.
Setting the alpha scissor to one, the
unwanted bits will disappear. We'll turn
off shadow casting and billboard the
quads so that they always face the
camera. This should get you a graph that
looks like this. From here, I've changed
up a few things.
Previously, I had to recompile GDAU to
allow me to modify the vertex
information inside of the fragment
shader. Many of you pointed out that I
can send information from the vertex
shader to the fragment shader using
varyings, which is correct, and that's
exactly what I was doing. The reason I
had to recompile GDAU was to allow me to
change the vertex values after the
vertex shader had already run. This
meant that the vertices would appear on
screen in one place. But their shadows
could be calculated based on the
position I modified it to in the
fragment shader. This method is now
completely obsolete, however, as good
4.3 introduced the light vertex
built-in, which allows you to do exactly
that without having to recompile the engine.
There are two main ways we can add
hetrogenity to visually break up the
grass, and those are color patches and
accent grass.
To get our grass patches in the fragment
shader, we can sample world space noise
textures, checking them against the
threshold value to determine which grass
color is chosen. Repeat the process with
a new noise texture in color, and it
starts to look a lot more full. I made
sure to do this on both the grass and
the floor below it to ensure consistent coloring.
coloring.
For the accent grass, we create a random
seed value based on the instance ID of
each quad. Comparing it to a threshold
value, we can modify a subset of grass
squads to be visually distinct, changing
their size and height in the vertex
shader and their color and sprite in the
fragment shader. Like with the color
patches, do this a couple of times. Now
Previously, I was rotating the quads in
viewpace using a sign function with
time. This worked fine for the video
demo, but I'd like to have an immersive
weather system in my game, and this
simple rotation just doesn't quite cut
it. So, I decided to make the rotation
consistent in world space. This way, I
can better simulate wind moving through
the grass and displacing it.
Before the billboarding is applied, I
sample a noise texture at the world
coordinates of our grass center, which
is going to act as our wind. And in the
sampling calculation, I include time,
speed, and direction, which will
modulate the noise texture in said
direction at that speed as time passes.
To calculate the world space axis that I
want my quads to rotate around, I take
the wind direction and find its
perpendicular vector using this calculation.
calculation.
I then set a maximum angle that I want
them to rotate and scale the rotation
based on the wind value from our noise
texture. That noise texture works as a
start, but it has no variation and could
quickly become repetitive. It's
something that most people wouldn't
notice, but I think it's these little
details that separate good from
excellent. Instead of sampling one noise
texture, here's what I do. Take the wind
direction and rotate it by a divergence
angle. Do this for both a positive and
negative rotation, saving each direction separately.
separately.
Sample the noise texture at the grass
center using time, speed, and the first
direction. Then do the same for the
other direction, but change the speed
and UV scale of the noise. Using an
irrational number like pi in this
calculation should make it so that when
you sample both noises, they never
repeat the same pattern. This of course
is subject to the accuracy of pi that
good is approximating. Though in our
case, it's good enough. Multiplying the
two noises together and clamping them
between zero and one gives us a noise
pattern that looks like this.
The last touch is adding an arbitrary
value to adjust the brightness of the pattern.
pattern.
Taking our new calculated noise and
plugging it into the rotation, we get
Here I've set the albido to the sampled
noise value so you can better see what's
going on.
One issue that arises with world space
rotation is that as we're using an
orthogonal camera, that is a camera with
no perspective, it's hard to tell which
way the grass is rotating at certain angles.
>> My solution to this was adding back in a
fake perspective in the shader. The idea
is to simply scale up and down the top
part of a quad as the grass moves
towards or away from the camera. I first
tried altering the quads UVs in the
vertex shader, which is possible, but it
doesn't work in the extremes as only one
of the two triangles our quad is made of
is being manipulated, and therefore the
texture is only being squished on that
triangle, which doesn't look quite
right. The solution that worked was
scaling the UVs in the fragment shader
instead. Multiplying the UVX by a scale
factor, we can shrink or stretch the
grass sprite from the albido texture.
This will scale around zero, however, so
we need to minus 0.5 from our UVX before
scaling and add it back again after
scaling before clamping the result
between 0 and one to stop the texture
from tiling. In our scaling calculation,
we're going to take the noise sample
from our wind and multiply it by a
scaling value to control the intensity
of the effect. Because we only want this
effect to occur when the camera's view
is parallel to the wind direction, we
can use the dotproduct of these two
vectors to return a value between 0 and
one based on their alignment, which we
will then use to scale the effect. But
hang on a minute, this isn't quite right
yet. We're currently scaling the whole
sprite. If we want a fake perspective,
we need to only scale the parts that
move to and from the camera. If we think
about how this should work, the parts
that move the most should have their
size changed the most, and every other
part should be scaled proportionally
based on how much it moves. I've talked
a lot about UVs, but I haven't really
explained what they are. UVs are a set
of 2D coordinates that tell the renderer
where to place the textures on a mesh. X
holds the red channel and Y holds the
green channel. And when combined, they
look like this. What's cool is we can
actually sample the UV coordinates and
use that number in our calculation. The
Y-axis is perfect for this as it holds a
vertical gradient of values from 0 to 1
that we can use to proportionally scale
our calculation. Right now, it's the
opposite of what we want. So, we will
minus it from one to invert it. Then
multiply it by our noise sample from
before and add one to the final result.
Like I mentioned before, after scaling,
we need to add back the 0.5 we took off
before scaling and clamp the value. And
now it should look like the grass has
perspective with its movement despite
the camera being orthogonal.
With just the world space rotation, the
grass still feels like it's missing
something. Adding back in the view space
rotation from before at minimal levels
Some animations have a certain charm to
them that come from the use of low frame
rates. I wanted to see if I could
capture this charm using the same
technique for the grass movement.
At first, I tried to quantize the
rotation, restricting its movement only
to certain positions. This worked on the
surface, but I soon realized the effect
fell apart when rotating through small
angles, only updating at certain angular
thresholds. On top of that, the frame
rate was not consistent.
>> as it relied on rotation speed,
something that was changing constantly
throughout the rotation.
I instead quantized the time value and
sampling the wind noise. This solved
those two issues from before, but
uncovered another. Because every leaf
was updating on the same frame, it just
looked laggy. I needed a way to make
each grass instance move at a set frame
rate but not update at the same time as
the others to avoid the laggy feeling.
But this would mean I need something
that's unique to each grass instance in
the calculation. I end up using the
world location of the grass and
randomizing it to get a location seed.
Finding the modulus of the seed with
respect to frame time, we get a value
between zero and the frame time which we
can use to shift the phase of each
leaf's frame rate individually.
By doing this, I was able to fix the
laggy feeling while still capturing that
low frame rate charm. I'd say it looks
pretty good, but I can't decide which
frame rate looks the best. Let me know
which one you like the most in the comments.
comments.
>> Uh, I'm not finished.
>> Another subtle detail that I wanted to
add was to have grass move reactively
based on the character's movement in the
scene. As I'd already figured out how to
rotate the grass both in view space and
world space, I thought this would be
pretty easy to implement, right?
>> Very nice words, but happens to be wrong.
wrong.
>> I ended up trying a few different
methods to rotate the grass rights based
on their position relative to the player
and the vector that connects them in
world space, but I just couldn't figure
it out. Doing it this way, it would work
from some camera angles, but then from
others, the grass would be rotating
completely the wrong way, or worse, a
mix of both. I was scratching my head
for quite a long time before I worked
out that if I separated the rotation
around the viewpace Z and X axis, it
would simplify things and I could have a
lot more control over what was
happening. While editing this video, I
realized what I was doing wrong before,
but I'm happy I didn't earlier because I
actually prefer the other method that I
came up with when it wasn't working
properly. The first thing we do is
calculate an intensity mask for the
rotation effect. This is based on the
distance from the player's position to
the origin of each piece of grass and
scaled by our radius size. This gives us
a circular gradient around the player's
location, but it's flipped. So, we need
to minus it from one to invert it and
then we can raise it by an exponent to
control the steepness of the gradient.
Now that we have our mask for each of
the grass quads that are currently in a
position to be displaced, we calculate
the direction from the player to this
quad. Again, using the dot product, we
can calculate the alignment of this
vector to the camera's forward vector.
This will give us a pattern that looks
like this, which we can use to rotate to
and from the camera around the viewpace
x-axis. Calculating the dot productduct
of this vector and the vector
perpendicular to the camera's direction,
we will instead get this, which we can
use to rotate around the viewpace Z axis.
axis.
Combining our mask with each of the dot
product values separately, we get a
displacement value for each axis, which
can be used to rotate the quad around
those axes. We also send one of these
values to the fragment shader to fake
perspective like we did before.
This looks great, but only supports one
location for grass displacement. So,
what if I want to have multiple
characters in my scene?
Well, I did some digging, and it turns
out that you can pass an array of
character positions to the shader.
Though GDO shaders do not support
dynamic array sizing, so the number of
elements has to be constant, which could
be a problem if you've got characters
spawning and despawning. But that's okay
because I've got a workound. I created a
character manager node that keeps track
of all the objects in the scene that are
in the character group. In the shader
code, I set our array size to 64
characters. So the character manager
creates an array of 64 vector 4s with
all values set to zero. These vector 4s
contain the location of the character
and the size of the displacement radius
for that character. Then the character
manager loops through all the objects in
the character group and saves their
location to the elements in the array,
leaving all the other entries as zeros.
Passing these values to the shader, we
can loop through each of the elements in
this array and calculate the
displacement for each character based on
the size and location, cumulatively
adding them together before clamping
them between minus1 and one to get our
final rotation value. What's great about
this is that all of the elements in the
array with zeros will not cause any
displacement as their size value is set
to zero. Therefore, the size of the
radius the effect is zero. This solves
our problem of not being able to use
dynamic array sizes in the shader. So
now we have multiple characters whose
displacement effect can be combined. All
that's left to do in terms of
displacement is convert it to a lower
I haven't found any way that I can do
this reliably in the shader. But what
worked for me is adding logic to the
character manager to only update the
positions on given ticks based on a
frame rate that we provide. Generally,
having this frame rate a little higher
than the frame rate of the wind rotation
looks better, especially when you might
have characters that move through the
grass quickly. With this, it really
starts to look authentic, almost like a
When a grass instance moves from one
lighting cut to another, the change is
abrupt, which can break immersion. Not
to mention leaves on the border of
lighting cuts, which sometimes create
these ugly flickering artifacts.
To fix this, I've come up with a
solution that I'm calling hybrid tune
shading. I'm sure I'm not the first
person to use this technique, but I've
never seen anyone talk about it before.
The general premise of the idea is that
we want to retain the stepped tune
shading, but smooth the transitions
between the lighting cuts to feel more
natural. We define a variable threshold
gradient size between 0 and one that
determines how much of each cut we want
the smooth gradient to cover. The number
of cuts we set determines how many light
bands Toune Shader calculates and the
inverse of this number gives us the
width of a single lighting band.
We will take the lighting value, round
to the closest threshold and build a
gradient around it.
We then see how far along the gradient
our lighting value falls and smooth step
between the two lighting cuts using this value.
value.
If it falls outside the gradient, we
just default to the standard tune
shader. The result should be similar to
a simple tune shader, but the hard edges
are subtly smoothed out. This works
great for our grass instances as it
looks like each blade of grass quickly
fades to the next cut instead of
Last but not least, let's talk about
clouds. When I initially developed the
clouds in my game, I used a large quad
with a noise texture to cast shadow onto
my scene based on the shader I found on
this Reddit post. This had its
limitations though with camera clipping
and finite size causing it to look bad
at certain camera angles and light
directions respectively.
I then realized I could store a 2D noise
texture as a global shader variable
which completely changed how I thought
about this.
We can simulate clouds casting a shadow
using this noise texture as long as we
have a world space coordinate light
direction and a world space height for
the clouds. With this information, we
can march along the light direction
using linear algebra to solve for the X
and Z coordinates at which the light
intersects our world space height. At
this position, we sample the noise,
which will return a value from 0 to one
that we can apply to the diffuse light.
Like with the wind, we can animate these
clouds by offsetting our sample by time,
speed, and direction.
I put this all inside a handy function
called get cloud noise that's stored
inside a shader include file. If you
don't know what that is, it's a type of
shader file that you can import to other
shaders by using the include tag.
These are great for storing functions
like our cloud sampler because we can
then include them in many different
shaders without having to duplicate the
code. You can change the global shader
values of the clouds in the project
settings under global and shader
globals. Though, for ease of access, I
created a note to store and update the
global shader variables upon changes,
which I'll probably turn into a weather
system node at some point in the future.
After all that, we have some pretty
sweet looking stylized grass. Although
I'm still in the early development
stages, I've made a Steam page for the
game. If you'd like to play it in the
future, go ahead and add it to your wish list.
I've left a link to a demo project in
the description so you can have a look
and see how it all works under the hood.
If you make your own grass using this
method, come and show me in the Discord.
I'd love to see. But for now, I hope you
Click on any text or timestamp to jump to that moment in the video
Share:
Most transcripts ready in under 5 seconds
One-Click Copy125+ LanguagesSearch ContentJump to Timestamps
Paste YouTube URL
Enter any YouTube video link to get the full transcript
Transcript Extraction Form
Most transcripts ready in under 5 seconds
Get Our Chrome Extension
Get transcripts instantly without leaving YouTube. Install our Chrome extension for one-click access to any video's transcript directly on the watch page.