You know how it is. Making landscapes out of heightmaps is great, but sooner or later, those rolling hills start looking a little flat. Real hills have grass sticking out of them.
But you can’t model each individual blade of grass without blowing your polygon budget. What do you do?
The obvious starting point is alpha blending. Find a side view image of a clump of grass, make it a texture, set the non-grass pixels transparent, and scatter little billboards bearing that texture all over your hillside. Instant grass, right?
Not so fast. Alpha blending requires careful attention to z-order. You have to draw all your billboards in just the right order, or background pixels will wipe out foreground pixels. To make matters worse, the “right order” depends on where the camera is at any given time. You’d have to mantain an octree of mesh objects and draw each separately. Never mind your polygon budget—your CPU budget’s just gone to hell.
Fortunately, the GLSL specification provides a solution: discard. This keyword, if issued as the last line of a fragment shader, forces the GPU to throw away any changes to the current pixel. Both the depth buffer and the color buffer are unaffected. Used with a conditional, discard lets you create “punch-out” regions that you can see through—and drawing order doesn’t affect a thing.
Enough talk. Here’s some code.
var ang, x0, z0, x1, z1; var i, j, d; for (ang = 0, i = 0, j = 0; i < 100; i++, j += 4) { // pick a starting point for the billboard // based on whatever the current angle is x0 = Math.cos(ang); z0 = Math.sin(ang); // pick a new angle that's between pi/2 and 3pi/2 radians ang += rng.get(Math.PI * 0.5, Math.PI * 1.5); // pick the ending point for the billboard x1 = Math.cos(ang); z1 = Math.sin(ang); // determine the distance between the two points // we use this in texture coordinates to prevent // longer billboards from stretching the texture d = Math.sqrt(Math.pow(x0 - x1, 2) + Math.pow(z0 - z1, 2)); // set the four points of the quad, bending slightly at the top // to create a "bent stalks" effect, and dropping below the dirt // so no connecting lines are visible. mesh.set(x0, -0.1, z0, 0, 0); mesh.set(x0 + rng.get(-0.1, 0.1), 0.25, z0 + rng.get(-0.1, 0.1), 0, 1); mesh.set(x1 + rng.get(-0.1, 0.1), 0.25, z1 + rng.get(-0.1, 0.1), d, 1); mesh.set(x1, -0.1, z1, d, 0); // generate the indicies mesh.index(j, j + 1, j + 3, j + 1, j + 2, j + 3); }
For this demo, I’m creating a circular patch of thick grass, so I pick random points around a circle and draw quads from one point to the next. If I weren’t discarding pixels, the end product would look like this.
You can probably imagine what a mess that would be with alpha-blending. Here’s what we do in the fragment shader instead.
precision mediump float; const float PI = 3.141592654; uniform sampler2D stem; varying vec3 obj; varying vec3 eye; varying vec2 tex; void main(void) { // this bit simply generates color and shading for the grass stems, // and has nothing at all to do with discarding pixels float l = texture2D(stem, tex * 0.01).r * texture2D(stem, tex * 0.1).r * 2.0; float c = texture2D(stem, tex * 0.02).r * texture2D(stem, tex * 0.2).r * 3.0; vec3 col = l * mix(vec3(0.0, 0.0, 0.0), vec3(0.57, 0.71, 0.14), c); gl_FragColor = vec4(col, 1.0); // generate a skinny sinewave along the x-axis float t = pow(sin(32.0 * tex.x), 8.0); // if the sinewave value is less than the y-axis // toss the fragment away if (t < tex.y) discard; }
It’s actually pretty simple, and if I’d used a grass texture instead of faking it with a sinewave, it would be even simpler. The first chunk of code generates the actual pixel color. Nothing staggering there. The magic happens in the second chunk.
I take advantage of the way I’ve set up the texture coordinates. On the x-axis, texture coordinates run from zero to the length of the billboard, so I can generate a sinewave along that length. I raise the sinewave to an even power to make it skinny and non-negative.
The y-axis runs from zero at the bottom to one at the top. If the sinewave output is smaller than this value, than we discard the pixel value—whatever we’ve set gl_FragCoord to above. No color value is placed in the buffer as a result. Instead of a color, we have a hole we can see through. Sweet.
The practical upshot is that I can generate something that looks like grass, then I can discard anything that isn’t grass. If I were using a grass texture, I could simply toss away anything where the alpha value was less than 0.5. This gives you the “good” effect of alpha blending without the “bad” effects of z-order.
There’s one caveat that I’m aware of. If you create a situation where the camera is looking through many layers of punchouts, the frame rate sinks like a stone. I suspect this has to do with hidden surface removal. The GPU is trying to sort out what looks like a massively complex scene with loads of objects at many different depths. It takes time.
The best solution I’ve found simply avoids the problem: keep the camera from looking through too many holes at once.
Want to see it all in action? Live Demo (WSAD to move, Q for fullscreen.)