A Simple Grass Shader
A little while ago I posted about a very basic grass shader I worked on (with unity) on my Real-Time Rendering and Animation of Small Soft-Stemmed Plants. Having said that, I had not worked with shaders in a very very long time…think days of 8-instruction-limit fragment shaders. What I’m presenting here is far from a perfect grass shader but is meant to be a basis for how you can go about writing a simple shader if you are just starting out with it.
The Algorithm
What I was looking to create was luscious grass, swaying in the wind, for my game which uses an overhead view camera. This is actually an important point when it comes to writing a shader because something that looks good from above may look terrible from gazing angles and vice-versa. The first step was clear: search for it. I quickly came across this paper:
Note that with this algorithm, depending on the viewing angle, the size of the grid, and the depth of ray tracing, the grid can become visible. In general it seems more suited for a first person view at gazing angles. Nevertheless I started trying to implement it and quickly came across my first hurdle: somewhat embarrassing to admit to but my laptop graphics chipset, being fairly dated, only supports pixel shader 2.0 which has a limited number of registers and instructions available (a side note to my North Americans friends out there: you have no idea how lucky you are with computer hardware prices!). I thought about using my work computer, but decided against spending late nights in the office. So I decided to look for something less complicated.
This is when I when I came across some fur shaders which look much like grass especially when viewed from a distance. The idea is simple: use dots on a grass texture and stack several layers on top of each other. The layered dots form a grass blade. Of course the more layers you can render, the more accurate this will look:
We require then, two textures. An alpha map with white dots representing grass blades, and the grass texture itself:
The implementations that I came across were actually rendering each layer separately and alpha blending the results. I wanted to accomplish this all in the shader for many reason, but mainly because Unity does not allow me to render the terrain multiple times.
The vertex shader simply transfers the texture coordinates and the view vector in tangent space (which unity makes quite convenient). We use the one set of texture coordinates for the grass and the alpha look up. As you will later see we just scale it for the alpha texture based on how thick we want the grass to look:
output.position = mul(UNITY_MATRIX_MVP, v.vertex);
output.texCoord = TRANSFORM_TEX(v.texCoord, _GrassTexture);
v.tangent = float4(1, 0, 0, -1);
float3 viewDir = WorldSpaceViewDir(v.vertex);
TANGENT_SPACE_ROTATION;
output.viewDirTan = normalize(mul(rotation, viewDir));
Note that unity doesn’t provide tangent vectors for the terrain vertices which is why I’m manually setting them in the shader. It’s easy enough since the terrain is an axis aligned grid. For the fragment shader we define a few constants which affect the look and feel of the grass:
#define GRASS_HEIGHT_SCALAR 0.005
#define GRASS_THICKNESS 0.5
#define NUM_LAYERS 4
I have used 4 layers because that’s how many I could squeeze in with pixel shader 2.0. Ideally you want to do a few more than that. The fragment shader first finds the point where the view vector hits the top layer of grass:
float3 rayEntry = float3(input.texCoord.xy, 0.0);
We can now loop through each layer (including the top layer), tracing the view ray into each layer and adding up the colors. Note that this is actually simple geometry. I insist that writing a shader is much more easily done with a pen and paper. Visualize it on paper first and writing the code becomes a breeze:
float4 color = float4(0, 0, 0, 0);
int layer;
for (layer = 0; layer < NUM_LAYERS - 1; layer++)
{
float distance = (layer * GRASS_HEIGHT_SCALAR) / -input.viewDirTan.z;
float3 hitpoint = rayEntry + (input.viewDirTan * distance);
float4 lookup = tex2D(_AlphaTexture, hitpoint.xy / GRASS_THICKNESS);
float srcAlpha = lookup.r;
float dstAlpha = 1.0 - color.a;
float alphaMix = srcAlpha * dstAlpha;
color.rgb += tex2D(_GrassTexture, hitpoint.xy) * alphaMix;
color.a += alphaMix;
}
I have used NUM_LAYERS – 1. Why? Well if you are drawing just the grass and no ground, and especially if you have a limited number of layers, you are going to end up with a lot of black gaps. So to mitigate that, we use whatever alpha we have remaining in that fragment and render the ground layer without looking up our alpha texture. So outside the loop we do something like this:
float dstAlpha = (1.0 - color.a) * 0.6; // Scale this component back a bit
float distance = (NUM_LAYERS * GRASS_HEIGHT_SCALAR) / -input.viewDirTan.z;
float3 hitpoint = rayEntry + (input.viewDirTan * distance);
color.rgb += tex2D(_GrassTexture, hitpoint.xy) * dstAlpha;
color.a += dstAlpha
The Wind
Of course without having the grass move against the wind, I could hardly call the results “luscious”. To achieve this, I took a similar approach as the aforementioned paper. Basically we take a “wind texture” and use it to modify the grass texture look up. The wind texture moves over time to simulate the wind. The vertex shader looks something like this:
output.texCoord1 = TRANSFORM_TEX(v.texCoord1, _WindTexture);
output.texCoord1.x += _Time * 1.4;
output.texCoord1.y += _Time * 0.5;
And in the fragment shader, when looping, we offset the hitpoint on each layer based on the height of the layer and the wind texture lookup:
float2 offset = tex2D(_WindTexture, input.texCoord1);
offset = offset - 0.5; // Make the range from -0.5 to 0.5
offset *= (NUM_LAYERS - layer) * OFFSET_SCALAR;
hitpoint += float3(offset.x, offset.y, 0);
The Sun
To enhance the wind effect a little, I decided to calculate a diffuse value for the grass blades. This is achieved by using a dot product between the wind vector and the sun light direction. Again in the fragment shader loop we calculate a diffuse color and multiply by the final color:
float3 normal = normalize(float3(offset.x, OFFSET_SCALAR_HALF, offset.y));
// hardcoded sunlight vector. we should grab this from unity
float diffuse = 1.0 - (dot(normal, float3(0.37, -0.93, 0)));
Here is a short video of the final product:
here. Please note that many of the variable names had to be changed to what the unity’s default terrain shader uses.