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