generation
September 16, 2024

Demo scene. Landscape form.

Telegram: Infinity World

Introduction

The second stage of creating a demo scene involves defining the basic landscape form, which will serve as the starting point for further development of the environment. The landscape form plays a crucial role in many aspects, from the predominant color of the scene to environmental objects like boulders, rocks, trees, shrubs, and so on.

In the first part, I determined what biome I wanted to feature in the demo scene, here is a quote from that article:

The biome consists of low hills with medium - frequency outcrops and cliffs.

In other words, no mountains or deep valleys — just hills, outcrops, and cliffs to add some variety to the view.

You can check out the first part here, where I discuss the concept of the demo scene.

1. Generation

After I transitioned voxel generation to GPGPU (Compute Shaders), the entire landscape surface description is now represented by HLSL code. For this, I use the FastNoise library for HLSL. It's not the most convenient option, but for now, it meets all my needs.

Here’s an example of HLSL code:

Figure 1. Example of HLSL code. One of the small functions describing the basic surface of the landscape.

Each biome in my project is represented by a separate .hlsl file, which is specified in the generation kernel as one of the possible biomes for selection. However, since I only have one biome in the demo scene, it is always chosen, and the biome selection logic is linear.

It’s worth noting that this approach, where we have N biomes and select the necessary code segment based on input data, is rather inefficient. There are two main problems with it.
The first problem is that the computational shader ends up with many branches, leading to low GPU occupancy. In other words, our computational resources are underutilized.
The second problem is more of a convenience issue: the compilation time for all the branches in the code increases exponentially. I’ve encountered this even while working with just one biome since I use many noise variations.

1.1 Base layer

Where should any generation start? With the foundation! The primary (base) layer should define the overall shape: this is what makes the landscape consist of hills, mountains, plains, canyons, etc. This base layer is then complemented by medium and small details.

For the base layer, I used the most common Simplex noise in its 2D version. Why Simplex and not Perlin? On average, Simplex produces better results and tends to avoid excessive repetition, while also being slightly faster. However, in this case, performance is of little concern, as the math involved is quite simple.

Figure 2. Example of Simplex noise.

Using Simplex noise as a base, I also modified it by adding Fractal Brownian Motion (FBM) to create fractal noise, incorporating both large and medium/small details, resulting in a more interesting landscape.

Fractal Brownian Motion (FBM) is essentially a series of N iterations, where in each iteration, we add noise while adjusting frequency and amplitude, which allows us to generate finer details.
Figure 3. The same noise as shown in Figure 2, but with the application of Fractal Brownian Motion (FBM).

As seen in Figure 3, FBM significantly alters the noise, adding numerous details with each iteration. It's a very powerful tool: parameters like lacunarity, gain, the number of octaves, and others are adjusted depending on the desired outcome.

After experimenting with the parameters a bit, I achieved a result (Figure 4) that fits my needs.

Figure 4. FBM Simplex 2D as the base layer of the landscape.

In Figure 4, the landscape resembles low hills. They are smooth and of various shapes, but they look unnatural. This unnaturalness comes from the fact that the hills are too smooth, and while their shapes are different, there’s too much similarity between them.

To eliminate this unnaturalness, we can add a bit of chaos: either by introducing more noise with different frequencies and amplitudes, or by trying to modify the coordinates used to generate this noise. In the first case, we’re likely to still end up with a poor result, as the unnaturalness originates from the noise itself. In the second case, we might achieve better results, as we’d be altering the noise at the coordinate grid level rather than the noise level.

To introduce chaos, we can use the Domain Warp technique. It’s based on the same FBM (I mentioned earlier how powerful this tool is, right?), which is applied to the coordinates. The result is shown in Figure 5.

Figure 5. The result of applying Domain Warp.

This already looks much better! The pattern has broken up, and a lot of medium details like depressions, small outcrops, and elevations have been added.

1.2 Large Details

The base layer turned out well, even with some details, but there still aren’t enough of them, making the landscape feel too monotonous. We need to add large details that will stand out significantly.

Creating a procedural landscape with unique details across the entire area is nearly impossible — there will always be some repetition. Therefore, solving the problem solely through landscape shaping won’t be enough, and additional methods like placing environment objects or adding large structures will be necessary.

For large details, I decided to add big outcrops at the tops of hills, as if they rise slightly with sharp cliffs in certain areas.

The first step is to determine where these details will appear. The result will be a mask where black (0) indicates the absence of details and white (1) indicates their presence. Since we need the hilltops, we take the base height and cut off by a certain threshold.

Figure 6. The mask for large details, where the hilltops are marked in white.

The mask in Figure 6 would give us an outcrop on every hilltop, which again feels unnatural, so I’ll probably remove some of them based on another noise pattern (Figure 7).

Figure 7. The final mask for the hilltops, where white indicates an outcrop and black indicates its absence.
At this stage, I realized that some functionality is missing in the generator: the presence or absence of outcrops might have been better determined based on the size of the hill, and this information can only be obtained through an iterative approach to voxel generation.

The outcrop is simply added to the base height using the generated mask.

Figure 8. The landscape with outcrops on the hilltops.

1.3 Medium Details

To add more variety, we need to introduce additional details, though not as large as in the previous step. These should also be outcrops, but smaller and located on the slopes, as the slopes currently appear empty.

As with the large details, we need to determine where we can add these outcrops and where we cannot — essentially, we need to generate a mask. However, this time the mask will be based on the slopes of the hills.

To identify whether a point is on a slope, we can use the concept of partial derivatives. Since the landscape is currently based on 2D noise, we can ignore the derivative along the Y-axis and calculate only for the X and Z axes, which will slightly reduce the computational load.

Figure 9. A mask where the hill slopes are shown in white.

Now we just need to add the outcrops themselves, but what exactly are they? The idea is to take a simple geometric shape and integrate it into the base landscape. For this purpose, I used a cylinder and added a "cap" — a hemisphere. It turned out quite well, although I couldn't fully blend it into the landscape just yet.

At this point, I reconsidered the iterative generator: with it, I could determine the gradient of the base surface within a certain area (the cylinder) and blend the shape accordingly. Without iteration, this is impossible, as there’s no information about the intermediate results of neighboring voxels.
Figure 10. Additional outcrops on the slopes of the hills.

1.4 Small Details

For small details, adding anything too complex won’t work, as the grid resolution is low, making it impossible to show intricate features. You could scatter some more outcrops or indentations, but at the current scale, it would be easier to achieve this with objects like small rocks and boulders.

However, there is still something we can do. If you look at the surface itself, it’s too smooth. While this surface lacks textures, the landscape should convey roughness through geometry, rather than being smooth like glass.

Figure 11. The landscape without small details — smooth, perhaps excessively so.

By adding a small amount of noise across the entire surface, the smoothness will disappear.

Figure 12. The landscape with small details, now appearing rougher and less smooth.

After adding roughness to the landscape, the seam between different LODs became very noticeable. Up until now, this issue had escaped my attention. Well, time to log it as a task and fix it at some point!


Conclusion

Procedural generation is far from producing truly unique results, but there are still tools available to address this issue. I demonstrated a relatively simple approach, yet even with this, it was possible to break up the "sameness" using multilayering techniques.

Key points that emerged during the creation of the landscape form for this biome:

  • It seems worth considering an iterative voxel generator. This is a small part of the overall world generator, entirely on the GPU side, but it’s the most critical part. By "iterative" here, I mean generating all voxels step-by-step, so that each subsequent step has access to the results of the previous step for all voxels. This would allow for more complex generation and yield much better results.
  • An error surfaced in normal calculations at the seams of chunks with different LODs. This is due to the different steps used to evaluate space in each voxel. Fixing it shouldn't be difficult; it just requires decoupling the border voxels from the LOD system.

The next step: defining the base colors of the landscape.