Procedural Voxel Grass Generation and Game Engine Updates
Welcome back to another development update for Littlecube Valley. Firstly, I would like to apologise for the long delay as certain portions of the game engine underwent a major overhaul in order to achieve what is seen in the video above. A lot of previous art assets had to be redone and technical algorithms rewritten to achieve a much more robust solution.
Soil types have also been reworked and are determined by the land biome and the type of the terrain tile. The different soil types are similar to what existed in our real-life, such as clay, loam, silt, or a combination of them. Material colours for terrain soil had been reworked to allow for mixing of different soil types within each texture.
Previously, there are some incorrect CSG operations for the ground terrain and that had been rectified to reuse a similar solution to sloping terrain. Additionally, terrain tiles for cliffs and slopes have also been reworked to address the updated list of soil types.
Jaggedness of soil materials has been applied to different soil types that lie next to each other, in order to establish a smooth wavy transition between them. This also helps to reduce the rigidity and blockiness of terrain soil regions.
The soil types are also used by grass objects that are not densely packed, revealing some of the soil that surrounds the grass. But for densely packed grass, it would cover the top surface of the terrain, while leaving the side surfaces exposed. The image below shows densely packed grass.
At the end of the cascaded world map generation process, we can determine the type of nature that exists on the surfaces of each tile on the region map.
In this case, we will be focusing on the type of grass that can possibly occur or grow on those surfaces. We make use of the existing voronoi cell network, together with the land biome type, drainage and moisture types. Each land biome type will have its own optimal conditions for drainage and moisture, in order to determine the height and density of the grass.
We have two types of generic grass in this demo. The image above shows a field of short-height grass, with grass blades protruding out of the ground. Another type of grass is surface-height grass, where the grass has very short height that flushes with the height of the surface. Medium-height and long-height grass will be deferred for a future update.
The temperate grassland biome tend to have soil that retained a lot of rich organic nutrients, due to the moderate decomposition of organic matter. So grass patches in this biome tend to be densely packed and have sufficient nutrients for growth. The height of the grass is determined by the moisture and drainage for that particular voronoi cell. The optimal conditions set for this biome are moderately wet moisture and good drainage. Areas that have little moisture or poor drainage tend to have surface grass instead.
In additional to grass height, there are various health states that determine the colour palette of the grass. The health states are classified as light, pale, lime, wilted, dried, flooded and parched. Within each health state, we use a grouping of three textures, each representing a slight variation of the group's main colour. Additionally, within each texture, there is also a minor grouping of three colour types.
The image below shows some short-height densely packed grass that are clustered according to light, pale and lime health states.
The image below that shows four clusters of densely packed grass, with the wilted grass cluster at the top of the image.
A small amount of randomisation is used to determine the resulting grass type that is based on a combination of grass height and health state. For example, under the optimal conditions, a particular voronoi cell has the possibility of selecting long-height grass that could have light, pale or lime health states or medium-height grass that could have light, pale or wilted health states. So this randomisation helps to diversify the height and colour of the grass that occurs across a series of voronoi cells.
As the moisture becomes wetter, better drainage is required to offset the increase in water retention, in order for optimal grass growth. For example, if the moisture is severely wet, rapid drainage will be required as an optimal condition. If the drainage remains moderate, the increase in water retention can result in the greater possibility of having long-height wilted grass. If the drainage is poor, then only long-height wilted or flooded grass can possibly occur.
However, as the moisture becomes drier, better water retention is required to offset the loss due to moisture evaporation. So having imperfect drainage actually helps in water retention and allows for medium-height grass that could have light, pale or lime health states or short-height grass that could have light, pale or wilted health states. Since there is also less water for absorption, long-height grass would not occur. As water drainage becomes more rapid or excessive, medium-height grass tends to become either dried or parched, while short-height grass could only be lime, dried or parched.
If the moisture is moderately dry, only short-height grass and surface-height grass could occur. If the moisture is severely dry, then only surface-height grass will occur. This generally would only occur in a small number of voronoi cells for a temperate grassland biome, depending on the solutions for generating moisture and drainage types.
The image shows another four clusters of densely packed short-height grass that belonged to the wilted, dried, flooded and parched health states. However, this is for illustration purposes only as such grass might be found in the temperate savanna or shrub-land biomes instead.
The image below shows three similar clusters of densely packed grass, but
the flooded cluster at the lower-right corner, consists of surface-level grass instead.
The short-height grass models were made in such a way that for every peak of the highest grass blades, there would be a cluster of lower grass blades surrounding it. This would also allow for pockets of empty spaces within the grass model, giving it a more natural look.
Darker shades of green were also painted at the lower voxels of the grass blades, while only some of the top-most voxels have the lightest colour. This is meant to partially simulate the different growth stages of the grass, with light green voxels representing the younger portions of the grass while the dark green voxels represent the older ones.
The surface-height grass models were made slightly different, where the protruding grass blades are of only one voxel height. While darker shades of green mostly covered the model, there are also numerous points of the grass model are of lighter colour. This is meant to represent the growing portions of the grass.
The image below shows the character standing on a sloping surface where only surface-height grass would occur. Similar mixing of such surface-height grass with differing neighbour health states are also done. Here is a mixture of light and pale health states.
The image below shows the character again on another sloping surface, with differing health states for the surface-height grass. The character is standing on parched grass, while he is facing dried grass and wilted grass occurs on his left.
Before the grass is planted onto the terrain soil, we first need to identify areas which would promote grass growth. After the coarse level terrain voxels have been generated, we look for large connected areas on the surface of the terrain and accept those that are not adjacent to any other coarse voxel or being next to a down-step in the voxel terrain.
After that, we identify fully covered regions within each terrain cell. This will give us the flat surfaces from which grass growth would have actively occured, because we assumed that flat surfaces tend to allow for better anchoring of grass rhizomes. For sloping surfaces, we use surface-height grass, which do not have grass blades protruding out of the ground.
After marking out areas for the short-height grass, we continue to identify areas for surface-height grass. This will help to fill up the regions where short-height grass would not occur. At the same time, we identify areas that have congestion and classify them, which include wall surfaces that are adjacent to the ground surfaces. This will help us later in precomputed ambient occlusion.
The type of grass is determined based on its height and health state. A similar solution was also implemented for achieving jaggedness for differing grass and surface types that are next to each other. This was taken from the generation of soil types.
Next, we use a flood-filling solution to plant the largest grass tile of 32x32 voxels, whereby all the grass attributes for each point are exactly the same for the entire tile. The same solution is repeated for remaining 16x16 and 8x8 tiles. Only surface-height grass models will have the 4x4 tile.
In order to ensure that short-height grass do not anyhow appear anywhere randomly, 16x16 and 8x8 tiles will only be added if they have a connectivity to 32x32 tiles. This will also help to fill up the gaps at corner locations where 32x32 tiles are positioned diagonally to each other.
As for grass trampling, there are also short-height grass models that have reduced voxel heights. For each undisturbed grass model, we have two additional grass models that represent the state of trampled grass. The image below shows all three types of short-height grass models together. There are a total of 16 such combinations as well.
The geometry for all three grass models are added to the graphics vertex and index buffers during the update process in the multi-core. However, we hide the rendering of these models in the vertex shader. The grass models were actually pre-partitioned during the modelling phase, so during the update process, it becomes easier to determine the partition identifier.
When the character's feet collide with the grass voxels spatially at the coarse level, we update the buffer of condition attributes for those specific locations, which are in size of 4x4 voxels for each partition. The vertex shader checks these conditions and determine which partitioned grass model is to be shown or hidden.
The trampled grass will revert back to its previous state after a very short amount of time. This allows the grass to recover back to the first model quickly as the character steps away from it. Trampled grass models also have a darker shading from its precomputed ambient occlusion.
As for precomputed ambient occlusion, the solution simply rasterises textured geometry that are pre-processed similarly to coloured geometry. The image below shows the intermediate result for such ambient occlusion.
For sloping surfaces, additional quad-fin geometry have to be pre-generated to address the vertical surfaces that are alongside the contours of the coarse voxels. This is because there are no surface-height grass geometry that covers the vertical sides of the coarse voxels.
All coloured and ambient occlusion geometry for short-height and surface-height grass are pre-generated using automated pre-processing. The individual geometry exist as tiny quads before the pre-processing algorithms assemble them together to form the grass geometry. Voxel data is obtained from Magicavoxel and is also pre-processed into point data in Blender with appropriate alignment, before subjected it to the automated mesh assembly algorithms.
A sparse virtual texture solution is also used to ensure that all the differing grass colours can be located in an atlas texture, and is to be updated when there are new colour textures to be phased-in or when previous colour textures are to be phased-out of the atlas when they are no longer in use.
The next set of short-height grass tiles are classified as regularly packed grass. The protruding grass blades tend to be clustered, leaving wide gaps for the soil surface.
A mixture of surface-height grass & soil is used to bridge the gap between one that is full of grass and the other that is purely consists of soil.
The following two images below show regularly packed grass, with small gaps revealing the soil surface around the short-height grass. In order to avoid uniform looking discontinuities from the soil surface, regularly packed grass is to be mixed with densely packed grass at 50% probability.
Similarly, surface-height grass also occurs alongside with a mixture of grass & soil, as shown in the two images below.
The next set of short-height grass tiles are classified as sparsely packed grass. The protruding grass blades tend to form disjoint clusters, with small loose surface-height grass sprinkled around them.
This surface-height tile consists of purely soil, which can be easily identified by its numerous patches and small individual holes.
The following two images below show sparsely packed short-height grass, with small gaps revealing the soil around it. In order to avoid uniform looking discontinuities from the soil surface, the sparsely packed grass is to be mixed with regularly packed grass at 50% probability.
The following two images below show surface-height grass & soil mixture alongside with surface soil.
At the moment, there is only one global texture for ambient occlusion. This can be expanded for use in day-to-night transitions, with darker shades for night-time usage. The following two images are purely just for visualisation of a night scene, without environmental lighting.
In order to get near pitch darkness, we added an environment texture similar to the ambient occlusion texture. The face of the character now has a slight bluish colour applied on it.
The playable environment has now been enlarged to nine connected region maps, compared to one region previously. There was also a hard border outlining the end of the current region previously and this has since been removed. Currently, the player can now roam around the nine connected regions, without having any loading or transition screens.
Origin rebasing has also been implemented to reset the coordinate system and to reduce the impact of floating point errors due to large floating numbers in the coordinate system. This will be integrated with an upcoming solution for a fully seamless world, whereby obscure regions of the world near the player will be generated in chunks and cells.
The rendering solution has also been reworked and a number of inefficient routines have been removed. The application can now run at near 60fps with minimal lag while the character runs around. However, memory usage now peaked at close to 800mb. Additionally, the multi-core updating solution for voxel and geometry data have been reworked and tested many times to weed out remaining race conditions.
The template character has been recoloured, its nose removed and its edges have been fixed. It has been observed that programmable blending tends not to perform well when there are a lot of edges in the scene. So the solution has now been adapted to allow edge rendering only for important objects in the scene, such as the player's character, party members or other important non-player characters or usable objects in the scene.
In conclusion, the project has now reached the stage in which it can demonstrate how many tiny little cubes can be used to describe a scene with high visual quality.
The next objective is to go back to the world generation and rework its solution with some new ideas, such as better voronoi network generation, a fully seamless world with offscreen loading, more world shapes and better meandering rivers. Then make it presentable in tiny voxels with interactivity and description during its generation. Additionally, we would be able to determine regions for generating civilisations.
We will also need to address the missing details for the cliff terrain surfaces, carve out river channels and setup some water simulation in voxels. We are also missing medium-height and long-height grass models. One way to reduce the monotonous green tone is to have a variety of plants, flowers, trees and wild animals and creatures roaming the environment.
EDIT: As of 23/07/20, the amount of memory usage has been drastically reduced to about 600mb from an even higher peak of 900mb. There is now more reuse of system memory resources and GPU hardware buffers are also released for non-visible chunks. We now rely more on runtime restoration of geometry data. Load balancing has also been tweaked slightly to minimise the latency that occurs during periodic world and geometry updates.
Thank you so much for reading!