Making Fast & Beautiful Grass


Making Fast & Beautiful Grass

Matharoo, the instructor of “Building a Crafting Game” on Udemy, is here to share a neat graphical technique in GameMaker that makes your game look better and still retains high performance.

Hey! In this quick tutorial, we're going to be creating lots of pretty grass. We will fill the room with detailed grass while still giving you thousands of FPS. Achieving a great look without the cost:

Grass
So fast!

Let’s take a look at how we’ll be approaching the problem

WHAT’S NOT FAST?

If you place thousands of grass instances in the room (by creating one object), your game will slow down, for sure.

In most top-down games, depth-based drawing is involved in some way. You might be familiar with this method:

depth = -y; // or -bbox_bottom

This code makes it so that when something is drawn it's based on it's y position in the room. Instances will be drawn from top to bottom.

So if you have many grass instances in the room, they won’t be drawn all at once, but the engine will be constantly switching between drawing one grass image, then some other object, then another grass image, and so on. That may cause texture swaps and vertex batch breaks, reducing the performance.

This is fine for regular instances, but not for placing thousands of grass blades.

So how do you solve this? Well, you use Vertex Buffers!

Note: if you handle depth ordering manually (using some data structure), then this grass technique will not work for you. You need to use the depth = -y method mentioned above.

PUT IT ON THE GPU

The concept of a vertex buffer is as simple as this:

  1. You give the GPU a bunch of triangles to draw,
  2. The GPU draws your triangles.

Good boy GPU.

That’s what we use vertex buffers for. A vertex buffer is simply a collection of points, which may form triangles (if you ask nicely).

Let’s say we want to draw just this one sprite with a vertex buffer:

Grass

You see a simple, rectangular image. The GPU, however, sees two triangles:

Grass

Each triangle has 3 vertices/points. To make up this grass image, we have to present 6 vertices to the GPU (through the vertex buffer).

Grass
Vertices

That’s how we’ll be coding our vertex buffer - writing up all six vertices to create the image.

But, we’ll be doing it inside a loop, so that it creates multiple grass images. That’s how we’ll end up filling a whole area with grass.

THE GML SIDE OF IT

We’re gonna create an object to represent an area of grass, and give it a rectangular sprite, which we can simply place in a room to set up our grass areas.

Grass

In the Create event, we’re gonna set up some variables:

// Get sprite data
sprite = Grass;
frames = sprite_get_number(sprite);
texture = sprite_get_texture(sprite, 0);

width = sprite_get_width(sprite);
height = sprite_get_height(sprite);

// Grass properties
grassCount = 500;

color = c_white;
alpha = 1;

// Get sprite data

First, we store the sprite that will be used for the grass (sGrass). Then we get the number of frames that are in the sprite. We’ll be using this to select a random frame for each grass image.

We also get the ID of its texture page and store it in texture.

In width and height, we store the size of the grass image.

// Grass properties

Then we set up the grassCount (500). That’s how many grass images we’ll have in this area’s vertex buffer.

Finally, we set up the color and alpha for the grass.

VERTEX FORMAT

Before building a vertex buffer, we need to create a vertex format. It defines what data each vertex/point needs (like position, color, and texture coordinates). We’ll create a vertex format, and also set up some 3D properties:

// 3D properties
gpu_set_ztestenable(true);
gpu_set_alphatestenable(true);

// Vertex format
vertex_format_begin();

vertex_format_add_position_3d();
vertex_format_add_texcoord();
vertex_format_add_color();

format = vertex_format_end();

With the two “gpu_” functions, we’re enabling z-testing, and then alpha testing.

Z-testing is how the renderer figures out what object is in front, and draws them accordingly (for depth ordering within a vertex buffer).

Alpha testing simply allows the transparent pixels in an image to be transparent, so that the objects behind it can be seen.

Then we come to the vertex format. We begin creating a format with a function call; then we add position_3d, texcoord and color to it, and then we finalize the format and save it in the format variable.

Now this format is what will be used to build our vertex buffer, and each point will take these parameters:

  • position_3d: The (x, y, z) position of the vertex in the room;
  • texcoord: The (x, y) coordinates on the texture page, for drawing the correct image;
  • color: The color and alpha of the vertex; if different vertices have different colors, then they’ll automatically be blended to create smooth shading

Now we can create a vertex buffer and fill in all the vertices.

VERTEX BUFFER

We’re now gonna add this code (still in the Create event):

// Vertex buffer
vbuff = vertex_create_buffer();

vertex_begin(vbuff, format);

repeat (grassCount) {
        // Next 4 code sections go here
        // 1
        // 2
        // 3
        // 4
}

We’re creating a vertex buffer and storing it in vbuff. We then run vertex_begin() so that we can start writing to our vertex buffer (using the specified vertex format).

A repeat() loop is then used with grassCount as its parameter. So any code you put inside its block{}, will be repeated 500 times (since our grass count is 500).

Now inside that repeat block, let’s add our first section:

    // 1: Grass coordinates
    var _x1 = irandom_range(bbox_left, bbox_right);
    var _y1 = irandom_range(bbox_top, bbox_bottom);
    var _x2 = _x1 + width;
    var _y2 = _y1 + height;

    var _depth = -_y2;

Make sure this goes into the repeat block!

Since we’re inside a repeat loop, we only need to set up one grass image. We first need to figure out its position in the world, which will be random for each grass image (limited to the grass area).

Instead of just a 2D position (x and y), we have rectangular coordinates (x1, y1, x2, y2), because an image is rectangular.

So we get a random position within the area for x1 and y1, and to that add the width and height of the grass sprite to get x2 and y2.

Then we calculate the z/depth of the grass, for depth ordering (if you’ve read my other post, you’ll know that z and depth are the same thing). This is the same technique as when we do "depth = -bbox_bottom” in an object.

TEXTURE COORDINATES

Now in the same block as before (the repeat loop), let’s add the second section:

    // 2: Texture coordinates
    var _frame = irandom(frames - 1);
    var _uvs = sprite_get_uvs(sprite, _frame);

    var _uv_x1 = _uvs[0];
    var _uv_y1 = _uvs[1];
    var _uv_x2 = _uvs[2];
    var _uv_y2 = _uvs[3];

First, we select a random frame value to be used, by getting a random integer between 0 and frames - 1 (the last frame).

Then, we get the UV coordinates of our grass image. What are they, you ask?

Each texture page is a group of images, and to map our triangles onto one specific image on it (the grass), we need to get its rectangular coordinates (x1, y1, x2, y2).

Grass

And that’s what UV coordinates are. The function returns an array, and its first 4 elements are the rectangle coordinates, which we store into variables ( _uv_x1, _uv_y1, etc.).

Now it’s only a matter of setting up each vertex and passing in the appropriate data.

THE TRIANGLES

We’re gonna add our third section in the block, which sets up the first triangle:

    // 3: Triangle 1
    vertex_position_3d(vbuff, _x1, _y1, _depth);
    vertex_texcoord(vbuff, _uv_x1, _uv_y1);
    vertex_color(vbuff, color, alpha);

    vertex_position_3d(vbuff, _x2, _y1, _depth);
    vertex_texcoord(vbuff, _uv_x2, _uv_y1);
    vertex_color(vbuff, color, alpha);

    vertex_position_3d(vbuff, _x1, _y2, _depth);
    vertex_texcoord(vbuff, _uv_x1, _uv_y2);
    vertex_color(vbuff, color, alpha);
Grass

For each vertex, we pass in the 3D position, the matching texture coordinates, and then the color and alpha.

So our first triangle is set up with these three points: (x1, y1); (x2, y1); (x1, y2).

Now we’re gonna set up our second triangle, simply by replacing the (x1, y1) point with (x2, y2).

    // 4: Triangle 2
    vertex_position_3d(vbuff, _x2, _y1, _depth);
    vertex_texcoord(vbuff, _uv_x2, _uv_y1);
    vertex_color(vbuff, color, alpha);

    vertex_position_3d(vbuff, _x1, _y2, _depth);
    vertex_texcoord(vbuff, _uv_x1, _uv_y2);
    vertex_color(vbuff, color, alpha);

    vertex_position_3d(vbuff, _x2, _y2, _depth);
    vertex_texcoord(vbuff, _uv_x2, _uv_y2);
    vertex_color(vbuff, color, alpha);
Grass

And that finishes our vertex buffer. The repeat loop will simply repeat the code (all 4 sections) and create many grass images!

Note: the 4 "sections" in the repeat block are only for your reference within this blog post; they do not pertain to the code.

At this point you should close the repeat loop block (}), and after that, add this:

vertex_end(vbuff);
vertex_freeze(vbuff);

Here we simply end writing to the vertex buffer, and then freeze it. That makes it read-only (so you can’t change it), which makes it faster to draw!

Now to draw the vertex buffer, add the Draw event and put this there:

// Submit
vertex_submit(vbuff, pr_trianglelist, texture);

This submits the vertex buffer to the GPU, with a "triangle list" draw method (since we passed in a list of triangles, basically) and the texture of the sprite (that we got in the Create event).

So we set up the vertex buffer once, in the Create event; and then we just keep drawing it in the Draw event.

Now make sure to clean up the buffer and the format, in the Clean Up event:

vertex_delete_buffer(vbuff);
vertex_format_delete(format);

GRASS, GRASS, AND GRASS!

If you now place grass areas in the room, you can see them filled with grass:

Grass

Of course, larger areas will have grass that is more spread out, and as a result, they’ll look emptier. To fix that, you can change your grassCount from a constant 500 to an expression that calculates the count using the area’s size:

grassCount = (sprite_width * sprite_height) / 40;

That’ll make sure that no matter how large/small your area is, the grass always looks consistent.

SHADER MAGIC

We won’t be getting into shaders in this post, but I should let you know that vertex buffers can use shaders just like any other sprite. You can simply call vertex_submit() with a custom shader set, and it should work.

See this example, which uses a marketplace asset for wind:

Grass
The lower vertices have a darker color (c_ltgray) for shading

You should also know that you can’t simply collide/interact with the grass blades as they have no real presence in the room. But you can check if your player is in a grass area, and slow down the movement since a grass area is simply a rectangular instance that has a collision mask.

CONCLUSION

Thanks for reading! 

Happy GameMaking!



Written by Ross Manthorp

Ross Manthorp handles all things community at the GameMaker team. When he’s not pulling the strings from behind the scenes he’s enjoying Nintendo games, indie games, and getting emotional over cartoons and comics.