Terrain Tool

A terrain tool based on heightmaps, developed in our engine Cobra for four weeks (4h/day) during an individual specialization course. I took a lot of inspiration from Unreal Engine's landscape system

The Landscape class

The structure of the landscape followed the structure in Unreal Engine. The landscape is made up out of Components (not the same components as in our ECS), which are divided into Sections that consists of quads. The specified number of quads per section is the number of quads at maximum tesselation, which is done on the GPU. Since DirectX 11 supports a maximum tesselation of 64, the specified number of vertices (number of quads + 1), would best be chosen as a multiple of 64. On the CPU side, the actual number of vertices is thus the specified number of vertices divided by 64.

The landscape itself is simply a grid with heights taken from a heightmap (sampled in the shaders). The size of the heightmap is simply the product of the number of components, number of sections per component, and number of vertices per section. 

Tjhe

Dynamic tesselation

The landscape supported dynamic tesselation based on distance from the camera. This was based largely on the solution by Frank D Luna in his book Introduction to 3D Game Programming with DirectX 11.

This was a lot of fun but also put constraints on how to do the actual painting. Since there were almost no actual vertices to "paint on", the painting had to be done on textures which then could be sampled at the tesselated vertices in the shaders. 

distanceBasedTesselation.gif

Raycast on heightmap

The first part in actually editing the heightmap was finding the picking point on the landscape. I solved this by projecting the picking ray on to the plane of the landscape. Then, at each increment on the ray, I performed  a distance check between the point on the original ray and the point on the projected ray to see if the distance was below a threshold.

raycastOnHeightmap.gif

Debug draw for the raycast on the landscape 

Once the intersection point on the landscape had been found, it was just a matter of iterating over the rows and columns and editing the height value at the vertices based on properties such as distance, falloff, strength and brush type.

Painting the textures

Since the landscape not only supported the painting of heights, but also of "regular" textures, I wanted the both cases to be handled in the same way. Both the heightmap and the texture containing the weights used for blending the other textures were represented by classes inheriting from TerrainTexture.

GetCurrentTexture() returned a pointer to a TerrainTexture, and combined with overloading the operator(size_t,size_t) for accessing elements in the texture, the calling function didn't have to care about what kind of texture it was painting on.

Falloff

The GetNormalizedValue() function in the image to the right scaled the normalized distance to match the current Falloff (the name perhaps wasn't the greatest). 

textureClasses.png
painting.png
sphericalLinearFalloff.png

Examples of spherical (left) and linear (right) falloff

Sketches of the GetNormalizedValue() for two different falloff types. Alpha is the falloff value, ranging from 0 to 1.

GetIncrement()

The GetIncrement() function then calculated the actual value to add to the current, based on the current brush type (circle, noise, smooth).

noiseBrush.gif
smooth.gif

The noise brush used Perlin Noise, with a factor for scaling

The smooth convoluted the texture with a Gaussian kernel

Paint mode

The landscape supports painting on up to four sets of textures. At the moment, all textures are blended together linearly using the weights painted. In the future, I would like to add blending based on height.

4 blended textures.png

Non-destructive layers

nonDestructiveLayers.gif

The landscape also supports painting (both for height and "regular" textures) on different layers. These can be toggled on or off, and controlled by a parameter ranging from -1 to 1, with -1 inverting the layer. For the "regular" textures, the lower bound is 0, not -1.