Horizontal Displacement
January 19, 2024
One thing most developers need in their game is terrain. And not flat terrain, we want mountains and vistas. One of the problems someone might have seen is that the terrain can only be displaced upwards. The problem with that is that we cannot create overhangs or have nice details on sloped terrain. This is because most terrain tools can only displace the terrain vertically and not horizontally.
I am going to talk about vertical displacement, horizontal displacement, why horizontal displacement is needed and the different techniques that can be used to implement it.
Author: Djanco Dewus Shader / Technical artist
Vertical displacement
Let's talk about what vertical displacement is. Vertical displacement displaces the terrain perpendicular to the surface. This is what most games use for their terrain. While you can get some good results with it, it lacks some of the things horizontal displacement adds.
With vertical displacement, we are limited to only displacing perpendicular to the surface. While this can get you pretty far, you will get issues when your terrain becomes steep. If you want to add more detail to the terrain one might think of adding higher-frequency noise. However, this displacement goes in the same direction. Therefore, it will not work that great on already sloped surfaces.
Quick note for code snippets: for the displacement, the code snippets use a procedural noise but this could be replaced with a texture or something else.
float3 vertex_position = get_mesh_position(...); // Mesh vertex positions
float3 vertex_normal = get_mesh_normal(...); // Mesh vertex normals
float displacement = generate_displacement(vertex_position);
vertex_position += displacement * vertex_normal;
vertex_normal = calculate_normal(vertex_position);
Where the surface is flat the displacement works great. However, on steep slopes the effect worsens. This limitation comes from vertical displacement, which can only be applied on one axis. To fix this we can use horizontal displacement.
float3 vertex_position = get_mesh_position(...); // Mesh vertex positions
float3 vertex_normal = get_mesh_normal(...); // Mesh vertex normals
float displacement = generate_displacement(vertex_position);
float displacement_high_frequency = noise(vertex_position);
vertex_position += (displacement + displacement_high_frequency) * vertex_normal;
vertex_normal = calculate_normal(vertex_position);
Horizontal displacement
As mentioned earlier, horizontal displacement is an addition to vertical displacement. By displacing in other directions than perpendicular to the surface we can create details and overhangs. We are going to talk about 3 implementations of horizontal displacement:
- Separate axis displacement
- Vector displacement
- Surface-aligned displacement
Separate axis displacement
Instead of only generating one displacement that will go perpendicular to the surface, we displace it in two more directions. We will have a displacement for every axis (so x, y, and z-axis).
We now have horizontal displacement. We have overhangs and can have better detail with better vertex distribution. The big problem with separate axis displacement is that it is not intuitive to set up. A user needs to supply 3 different displacements and it's hard to predict what the final result is going to look like.
float3 vertex_position = get_mesh_position(...); // Mesh vertex positions
float3 vertex_normal = get_mesh_normal(...); // Mesh vertex normals
float displacement = generate_displacement(vertex_position);
// Get 3 different noises
float displacement_horizontal_1 = noise1(vertex_position);
float displacement_horizontal_2 = noise2(vertex_position);
float displacement_horizontal_3 = noise3(vertex_position);
// Combines noises
float3 displacement_horizontal = float3(displacement_horizontal_1,
displacement_horizontal_2,
displacement_horizontal_3);
vertex_position += displacement * vertex_normal + displacement_horizontal;
vertex_normal = calculate_normal(vertex_position);
Vector displacement
With vector displacement the 3 displacements are combined. This means that the channels of a texture or the components of a vector are the three directions.
Although this will also be horizontal displacement, it is not intuitive to use either. It can create strange artifacts like sharp creases and inverted parts of a mesh. While the displacement is easier to create, for the user it's really hard to predict how the final result is going to behave. It also has a lot of chances for ugly artifacts.
Vector displacement maps are still useful when using prebaked displacement maps, more info. For something that is procedural or not fully controllable this is probably not the best option.
float3 vertex_position = get_mesh_position(...); // Mesh vertex positions
float3 vertex_normal = get_mesh_normal(...); // Mesh vertex normals
float displacement = generate_displacement(vertex_position);
float displacement_vector = noise_3d(vertex_position);
vertex_position += displacement * vertex_normal + displacement_vector;
vertex_normal = calculate_normal(vertex_position);
Surface aligned displacement
This technique displaces the terrain vertically and adds a second displacement that is aligned to the vertical displacement. This is what I call “Surface-aligned displacement“. This has some benefits as it is easy to create and predict the result.
float3 vertex_position = get_mesh_position(...); // Mesh vertex positions
float3 vertex_normal = get_mesh_normal(...); // Mesh vertex normals
float displacement = generated_displacement(vertex_position);
vertex_position += displacement * vertex_normal;
vertex_normal = calculate_normal(vertex_position);
// Surface aligned displacement
float displacement_surfaced_aligned = noise(vertex_position);
// note: the normals we use is different because we recalculated the normals at line7
vertex_position += displacement_surfaced_aligned * vertex_normal;
vertex_normal = calculate_normal(vertex_position);
Implementation
In this section I will talk about how we implemented horizontal displacement in our project. As stated before, the horizontal displacement implementation are an addition to an already displaced terrain. For this project we use procedural and ML-generated terrain and enhance it with horizontal displacement.
Selection
For our project we chose the “Surface aligned displacement“ implementation. We used this as it is easier to author and we found that it also provides better results. Given the displacement is always applied perpendicular to the terrain, we found it isn’t as error-prone as the other implementations. Another reason we chose this implementation is because it's rather simple (only 1 additional displacement). We strive to have ML also generate the horizontal displacement in the future so a predictable and simple implementation would help with training ML to generate this.
Noise
For now we are using a procedural noise to generate the horizontal displacement. However, there is a problem when using procedural noise. Our project is planet-scale thus we cannot generate procedural noise over the whole planet due to floating point imprecision. We use a noise that repeats after a certain amount. This will cause the noise to tile. The tilling distance covers a considerable distance and since horizontal displacement is an addition, we won't notice the repetition.
Masking
For masking we don't use anything complicated. We use the slope of the terrain to calculate the weight of the horizontal displacement. This ensures that horizontal displacement is only applied at high slopes, to better simulate the shapes of cliffs. Horizontal displacement works best on steep slopes. We don't want it on flat surfaces as it would create too much noise and remove any flat terrain. We calculate the slope of the surface using a dot product between the surface world normal and the direction to the center of the planet.
float3 vertex_normal = get_mesh_normal(...); // Mesh vertex normals
// We dot the terrain normal to the direction to the centre of the planet to get the slope
float displacement_weight = dot(vertex_normal, planet_normal);
Future improvements
One of the problems we are facing right now is the UV stretching problem. If you vertically displace the terrain you get stretched UVs at steep angles. If you use the UVs to generate the horizontal displacement then they will be stretched on the slope which gives deformed results.
Potential fixes for this could be using projected UVs like tri/bi-planar projection or using world position to generate the noise.
In engine screenshots
Conclusion
With these techniques you should be able to generate visually appealing horizontal displacement. Although this technique requires tweaking and tuning of the textures and noises, the results will be better than using no horizontal displacement. As mentioned before, horizontal displacement is needed when you want to add details and overhangs to steep slopes. Which is impossible when only using vertical displacement.
Share: Twitter Facebook Copy link Next article
Keep me posted
Stay updated on our journey to create massive, procedurally generated open worlds.
For more latest news and updates you can also follow us on Twitter