The use of rust language in major companies is increasing and with it also the game dev community is growing.
Bevy is an ECS-based, 2D and 3D game engine currently under development, but it provides already many features for a simple game.
In this post, we will build a simple terrain visualization, while also exploring some of Bevy’s capabilities
Generating a mesh from a heightmap
Usually, terrain is described by a heightmap, usually a squared, grayscale image where each pixel encodes the height of the terrain at (x, z) coordinates
(note: we will work in a Y-up world).
Example Heightmap from Wikipedia
Our application will load the image and turn the heightmap into a mesh.
The simplest approach for doing this would be to partition a plane with a simple pattern of triangles, where the y component of each vector is sampled from the heightmap:
Simple triangle partition
For instance, by applying this approach on the simple heightmap on the left, we obtain the mesh on the right:
The above method is perfectly fine in most cases, but if we are going to generate and show multiple meshes (e.g. in an open-world game) the count of triangles is going to increase soon.
Reducing the number of triangles - RTIN
One simple option for reducing the number of triangles consists in approximating parts of the heightmap where the slope is low. This way, planar parts in our mesh will be composed by a minor number of triangles:
An approximated version of the previous mesh
In a nutshell, the algorithm divides the space into two right-triangles recursively (like in a BSP). The obtained hierarchy allows choosing which triangle should be used for the mesh and which triangle should be ignored. I advise reading the article if you want to know how it works.
accepts the heightmap image data and the maximum allowed error as input. It follows the same process described in the article:
- computes the approximation error for each triangle
- traverses the triangle hierarchy, collecting all the triangles that satisfy the error_threshold
- computes and returns the vertices and indices of each selected triangle:
1 2 3 4
Given an image of side N (where N is a power of 2) the terrain will extend from (0, 0) to (N, N) with a height ranging from 0.0 to 1.0
Build the Bevy mesh
Let’s go back to Bevy now. How can we visualize the triangles that we obtained?
It is very easy to create a new mesh: we choose the primitive topology (i.e. do you want to draw points, triangles, or lines?):
1 2 3 4 5
We then create the vectors that will contain the mesh vertex attributes and indices:
1 2 3
and we fill them starting from the
TerrainMeshData obtained above.
Vertices positition and color:
1 2 3 4 5 6 7 8 9
The vertices will have a color proportional to the terrain height. The palette library is used for generating the color gradient.
It is useful to show the terrain wireframe for debugging. So we will use different indices depending on whether we want to draw triangles or lines.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
We can now add these vectors to the mesh:
1 2 3 4 5 6 7 8
set_attribute function accepts an arbitrary string as the first argument. This string will later be referenced in the shader.
Rust type system and Bevy perform all the type conversion for us, so we avoid mangling with byte arrays and pointer casts.
Our mesh is now ready.
Bevy standard material allows to use a texture, but it does not support vertex colors. In order to draw the terrain with color proportional to the height, our shader has to use the
TerrainMaterial::ATTRIBUTE_COLOR mesh attribute.
In order to do this, we will use the simple render pipeline and shaders from the mesh_custom_attribute.rs example.
add_terrain_pipeline function, called during setup, creates and returns a new pipeline:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
Make the mesh available for use
Note: concepts such as Assets, Handle and Resources are described in the Bevy Introduction
We will not add the mesh directly to an Entity. Instead, the mesh will be owned by the
Assets asset manager.
Once we move the mesh to the manager, we obtain a
Handle in exchange:
Handle is now a clonable reference to the mesh. It is quite useful because, for instance, we can add it to a Bevy
1 2 3 4 5
So that we can reference later the mesh in any system. For instance, the following system allows us to switch between the shaded and wireframe mesh:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
Add the mesh to the scene
Finally, let’s spawn an entity in the scene with the terrain mesh:
1 2 3 4 5 6 7 8 9 10
The terrain mesh will now be visible in the scene:
Note that, beyond spawning the
MeshBundle, we added a
Terrain component. This empty struct actually does nothing at all! However, by adding it to our entity, it works as a tag for referencing this entity in a query. Check the previous definition of
1 2 3 4 5 6 7
terrain_query will only look for entities that contain both a mesh and a terrain component.
After a few days spent playing with this really promising framework I can totally say that using Bevy it’s a nice experience:
- There are enough examples for learning how to use its features
- The ECS system seems very powerful and composable. So far I was always under the impression that I could read and write every part of the application in a clean way, without incurring in borrow checker issues. I don’t know if this will hold true in larger applications, but I am optimistic about it
- Many features are missing right now (e.g. there are just a couple of UI widgets) but the development is pretty active.
In the next post, we will add support for generating random terrain. Until then, you can check the current source code here