Making Unity's grass look less shitty
Dec 4, 2025
15 min
This is Unity grass:

After:

This is grass on a Unity terrain.
___
Origin of the idea
For most games, the terrain textures include the grass itself. Look at this barebone terrain.

The green color comes from the individual grass blades in the texture. So the grass patches spawned on the terrain should have the same color as the terrain.
However, in Unity, no matter what I do with the grass settings, some patches always look off.

There is also an issue with the grass always rotating straight to the ground. When viewed from above, the grass blades disappear:

___
The goal
In this article, I will solve both issues and walk you through all the steps.
I will ensure the grass is colored based on the underlying terrain color...

...and I will also tilt the grass blades a little so they look nice from above.

___
Figuring out how to customize the grass shader
First step is to figure out how I can modify the original grass shader that is used to render the grass.
___
Customizing the shader
I looked into the Frame Debugger to see the draw calls that draw the grass to find the reference to the shader file.

Then I copied the grass shader files. I used the Billboard version because each blade in this version can rotate towards the camera. This shader must contain some quad rotation logic, which is useful for customization. These are the files I copied and placed in the project:
:center-px:

Then I entered the shader and modified its name...
...and I modified the #include directives to use the copied HLSL files instead of the original ones.
I also changed the fragment shader to always return an orange color for the grass. This way, I can recognize when my shader is used:
Now it's time to make the terrain use my shader instead of the original.
___
Swapping the internal shader reference
I want the terrain to use my shader instead of the URP one, but there is nothing in the documentation on how to do that.
I use Unity version 6000.2.9 and URP. I searched for grass shader in the URP source, and here is what I found:
:center-px:

This code is in the UniversalRenderPipelineAsset.DefaultResources.cs. It contains the terrainDetailGrassBillboardShader property, which is probably used internally by the Unity Engine to set the grass shader in the terrain. It uses the shadersResources.terrainDetailGrassBillboardShader to get the shader reference.
This is in UniversalRenderPipelineRuntimeShaders.cs, which contains a getter and setter for the grass shader.
I can probably just replace this reference with my own shader, so I wrote this code:
The code above runs just after assembly reload and swaps the internal shader reference. However, the terrain seems to cache things internally, so I needed to adjust the grass settings to make it update correctly. Here is how it works in action: the shader is used for billboard rendering.
___
Basemap rendering
Now that my grass shader is running, I will color the grass blades using the terrain color underneath. For that, I need to access the terrain color in the shader...
I couldn't find an obvious way to access the terrain color in the shader. So my plan is to render a low-resolution texture of the terrain and sample this texture in the shader. This texture will be called a basemap texture.

___
How terrain rendering works
The Unity terrain renderer combines terrain textures using the alphamap texture. The alphamap encodes the alpha of four terrain layers in the RGBA channels.

To illustrate what happens during the splatmap rendering, each terrain texture is applied using a different "alpha" value, based on the alphamap texture RGBA values. See the animated image below:

I can use this algorithm to generate a basemap texture for the terrain.
___
Component for rendering the basemap
I created a TerrainBasemap component to render the terrain basemap texture, a low-resolution texture containing the terrain colors.
For each alphamap texture in the terrain, I execute a draw call that renders four terrain layers into the render texture. The final basemap texture is set as a global texture for access in the shader under the _GlobalBasemap property.
I also set the terrain boundaries in the shader property, which I use later to calculate the UV for basemap sampling.
This is the component's code inside the OnEnable function. The comments in the code explain what happens.
You can access the full source code here: https://gitlab.com/-/snippets/4909443
Here is the component.
:center-px:

___
Shader for basemap rendering
Now that the CPU rendering part is done, I need a shader to render this basemap.
In the C# code, I set the _Alphamap, _Texture0, _Texture1, _Texture2, and _Texture3 textures. I will use all of them to render the basemap texture additively.
This is the fragment shader code:
This is the most important part of the shader, which blends the terrain layers using the alphamap values. Here is the full shader's source code: https://gitlab.com/-/snippets/4909443
Then, I updated the shader reference in the component.
:center-px:

And this is the texture I got.
:center-px:

Looks good to me! Now it's time to access this texture in the grass shader.
___
Modifying the grass shader
Now my goal is to use the created texture inside the grass shader to color it using the terrain colors.
The code I am modifying is in WavingGrassPasses.hlsl. It contains the vertex and fragment shaders for the grass. I'll start by adding the global properties to this file. I modified the beginning of the file:
___
Sampling the basemap
Let's sample the basemap color in the fragment shader. The fragment shader code is located in the half4 LitPassFragmentGrass(GrassVertexOutput input) : SV_Target function. I modified it so it returns the basemap color as a result.
It looks like it works, but there is still a long way to go to make it look better.

All the individual quads have the same color. The color also does not match the terrain well because it skips the lighting. So I modified the shader to work on the albedo color before the light is calculated:
And it looks much better.

However, I don't like that each quad has no details in it, and blends with its neighbors too much.
___
Randomizing the colors
I will randomize the colors a bit to make each quad stand out. Randomness in shaders is usually achieved by hashing some values. So I need to find a value that is the same for all vertices of a quad but different for each quad.
I did some GPU debugging, and I figured out that each quad in the grass renderer is stored in this format:
:center-px:

Each vertex contains the bottom-center position of the quad in the POSITION attribute. And the offset for this position is stored in the TANGENT attribute in XY components. This offset is used to create the quad.
To achieve randomness, I can hash the POSITION attribute. Let's try that.
Then I modified the vertex shader for the billboards to generate the random value based on that. This is a modified vertex shader:
Then I modified the fragment shader to see if the random per-quad works correctly.
:center-px:

And it works like a charm.
Next, I modified the code so the random 0-1 value modulates the color:

It looks better, but the individual quads still use a single color without any variety. I lost all detail from the grass texture except the alpha cutout.
___
Blending basemap color with the original one
Here, you can see that the individual quads have no details in the texture. This is because I fully replaced the albedo color with the basemap color.

I will fix this by interpolating the albedo color with the basemap. I modified how the basemap color is applied:

Now, the top of the grass is a little brighter.
___
Fake ambient occlusion
Now, to add a little bit of polish, I will add a fake ambient occlusion, darkening the grass blades a bit at the bottom and brightening them at the top - to balance the average color.
I added this interpolator:
This line goes in the vertex shader. The goal is to have a value of 1.0 in the top vertices and 0.0 in the bottom. The grass is rendered using quads, so the step function on the Y offset is enough here.
This is how it looks when displayed by the fragment shader:

ao interpolator displayed as a color.
I use this AO value to darken the grass and brighten the top of the blades. It's a subtle effect, but it adds nice color variety.

The grass with fake AO looks less dull.
___
Random grass blades rotation
Currently, all the grass blades are billboards that always face the camera. This is not the most realistic approach to grass rendering.
I want to rotate each quad independently. I want to give it a random pitch and yaw rotation. Pitch rotation can be achieved by rotating YZ components around the center, and yaw rotation is done by rotating XZ components after the pitch is applied. And since all the vertices are created by applying an offset in the :TANGENT attribute, I can just rotate this :TANGENT at the beginning of the vertex shader.
:center-px:

I removed the TerrainBillboardGrass function from the vertex shader and replaced it with my custom randomized rotation:
And there is no more weird camera-related rotation.
Also, the top-down view improved a bit, because the grass looks denser. There are no top-down holes:

In the center, you can see that Unity's grass creates a "hole" in the grass, because all the quads have a similar pitch rotation. There are no such artifacts when the grass is customized.
___
Comparison
And here is the comparison, before and after.
Before:

After:

___
Performance
Now, the most important part. Development ends with profiling, not just nice visuals. Let's see how I can improve the performance of the grass rendering.
I created a build with this camera angle and I profiled on RTX3060 on FullHD.

___
Original Unity's grass
Let's start with profiling the original Unity's grass.
:center-px:

Because the grass is rotated differently, it renders a different number of pixels on the screen, so I will also calculate how much time it takes to render 1 million pixels:
Unity grass:
3.42ms / 4.517mln pixels
which is:
0.757ms / 1mln pixels
___
Custom grass
Those are the metrics for the grass for full HD:
2.47ms / 3.425mln pixels
which is:
0.721ms / 1mln pixels
:center-px:

Bottleneck on SM units:
:center-px:

My shader is slightly faster than Unity's. How is that possible when I only added new features?
Texturing units must have been underutilized before on this GPU, so adding one more texture sample didn't make a dent.
The grass is rotated differently, so the individual quads are placed differently. The workload distribution on the GPU is probably slightly different, maybe more efficient in this case.
However, I would argue that this slight time difference is just a measurement error.
I tried to optimize the shader by moving the texture samples into the vertex shader, but it did not make it run faster.
___
Summary
In this article, I showed how to customize Unity's terrain grass shader to:
Color grass based on terrain - By rendering a basemap texture and sampling it in the grass shader
Add visual variety - Through random per-quad color modulation and fake ambient occlusion
Fix the top-down view - By replacing billboard rotation with random pitch and yaw rotation
The custom shader performs similarly to Unity's original, while providing much better visual integration with the terrain.

