Texture Blending

Texture blending with multiple textures is dealt with in many tutorials, principally in Caitlin Zima’s series of guides here. An issue when using this technique is that is truly does blend two dissimilar textures together, essentially blurring one into the other as shown below.

Merged

This is unrealistic. A better approach is documented here . This approach does not blend the values of the pixels by combining their values and dividing by a blend factor, but uses the blend factor to select a pixel from one or the other underlying texture. This means for any one pixel the result comes from one or the other texture but not a blend of the two.

The result can be seen here

Splat

This gives a more realistic looking result close up, with either stones or grass showing. In the link to Andrey Mishkinis’ example he provides the renderer with a height map alongside the visible texture and samples the height map of the two textures – the highest pixel wins and gets painted. This requires some complexity (and artistry) in terms of providing extra textures just for determining height. In my example above I side-step the issue to get a slightly less accurate effect but one which does not require an extra texture sample or further artwork.

To get the effect of depth on the texture I simply convert them to grayscale and sample the result – the darkest pixel is deemed to be the deeper, and does not get rendered. Because the conversion to grayscale is arbitrary (I could have chosen the reddest pixel as an alternative) the pixel painted is a bit arbitrary but consistent.

The HLSL to do this is (where groundCoverage and appliqueTexture are the result of a tex2D sample of my two textures.)

// Grayscale

float groundCoverageGrayscale = dot(groundCoverage.rgb, float3(0.30, 0.59, 0.11));
float appliqueGrayscale = dot(appliqueTexture.rgb, float3(0.30, 0.59, 0.11));

// Apply applique in preference to ground cover
if (appliqueGrayscale > groundCoverageGrayscale)
{
textureColor = normalize(appliqueTexture);
bump = appliqueBump;
}
else
{
textureColor = normalize(groundCoverage);
bump = groundCoverageBump;
}

Advertisements

Helpful Resources

Invaluable .net resources for landscape programming;

Clipper – An open source freeware polygon clipping library, with C#, Perl, Ruby examples.

Triangle.net – A free .net triangulation library.

Visual Studio 2013 Community Edition – free fully fledged .net development IDE including performance analysis tools.

XNA Game Studio – Although deprectated, the XNA libraries embedded in it and examples are a good starter – just don’t develop your application using Game Studio, just use the examples and binaries in standard VS.net

A Trip through the graphics pipeline – An excellent primer on the technology of graphics rendering.

Tessellating Tiles

Arranging in Distance Dependent Detail

One of the immediate problems to be solved when building a tiled landscape is the need to tessellate tiles of different scales. Assuming the tiles are repeats of the same regular mesh but with varying heights on the Y axis, and with each tile size being double the previous the following method can be used.

  • Arrange the tiles with respect to the distance from the viewer. In this example the number denotes the size of the tile, and the viewer is assumed to be on one of the most detailed “4” tiles.

image

  • Record the adjacencies of each tile to the next
  • Parse the adjacencies using the following rule

If the tiles are more than one size differential split the larger tile into its constituent four smaller tiles.

image

Results of pass 1 – reduces a “2” tile to its constituent “3” tiles.

image

Results of pass 2 – reduces the bottom right “1” tile to four constituent “2” tiles

image

Results of pass 3 – reduces the top right “2” tile to it four constituent “3” tiles

image

Results of pass 4 – reduces the top left corner.

After this iterative process the tile map now has no junctions where a tile meets another tile that is more than one size differential. This is important in the next step.

Preventing Ripping

  • Using the adjacency map record for each tile which edge abuts another tile which has a larger number.
  • When producing the mesh, store two numbers for the Y coordinate – its more accurate height, and the height as interpolated between its two neighbours, for each odd numbered vertex across the edge.

image

In the example above the numbered vertexes will have two values recorded for its Y (height) value – the actual value obtained from a heightmap or other source, and the value as a linear interpolation between its two neighbours, if it is an odd numbered vertex. So V1 interpolated height would be average of V0 and V2, V3 would be average of V2 and V4. This step is called “vertex welding”.

image

From the above diagram you can see that when rendering the black, more detailed mesh, you can choose to render the numbered vertexes either as their “natural” more accurate value, or the interpolated value – and that interpolated value will precisely match the “natural” value of the adjacent larger tile, as the interpolated point V1 will fall exactly on the line A-B and V3 on the line B-C. This will render a seamless join between the two tiles.

Tidying Up

Although no ripping will be evident the normals calculated for each vertex in a mesh are dependent on the values of their neighbours. At the edges of meshes you would not take into account the values of their neighbours in the adjacent mesh. In addition to prevent “jumping” or visible normal transition artefacts, the normals should be calculated with reference to an adjacent mesh at the same level of detail as the subject mesh.

Limitations of Runtime Heightmap Terrain

In previous posts I have discussed the techniques for runtime generation of landscape form from fixed meshes using heighmap textures to generate Y coordinate offsets, this technique is very fast in execution and very frugal in GPU bandwidth. It does have some limitations;

  • The heightmap size itself is limited to a single texture, meaning that its not infinitely extensible.
  • Ultimately the heightmap pixels map to a world voxel coordinate, and can only be interpolated to smaller world coordinates by using terrain-type specific noise (i.e. bumpy ground height noise etc).
  • Continual sampling of the heightmap at different resolutions using floating point can lead to sampling errors, especially near to the edge of the heightmap texture (DirectX no longer supports the margin property of the sampler so we have to include a gutter on the heightmap, further degrading its usable size).
  • Specific landform types cannot be usefully described – river channels, moraines, geological rock outcrops, and more generally any overhang, tunnel or cave.
  • The deformation of the heightmap by features such as rivers, roads, building platforms etc proved to be insurmountable – the heightmap resolution required in the near field of view was excessive, and the progressive halving of the resolution out in the medium and long field of view rendered these landscape features visibly incorrect (rivers got wider and less distinct, roads became impossible to depict).

These limitations can sometimes be overcome.

Heightmap Maximum Size

I generated a set of tessellating heightmaps to create an infinitely extensible heightfield. This approach caused the following issues;

  • In order to reduce to an acceptable limit the number of heightfield textures being submitted to the shader a progression of re-sampled heightmaps, each being the aggregation of four tessellating heightmaps were needed and a selection algorithm used to send the appropriately detailed map to the shader was introduced.
  • Each call to the shader required a minimum of four heightmaps as it was unlikely that the geometry mesh being drawn would match any given heightmap footprint.
  • Sampling errors at the junction between tessellations of different resolutions of heightmap and geometry became quite a problem.

Heightmap Minimum Resolution

As the viewer came closer to the landscape surface the resolution of the heightmap no longer gave a progressively more detailed landscape. Only the introduction of landscape specific noise using perlin textures or other procedural height generation solved this problem.

Next Steps

In order to overcome this problem my next steps will be to “go back to the beginning” and examine the techniques needed to render a large scale landscape without runtime height generation, using tiled meshes unique to each landscape section. This will require a lot more meshes, but its strength is that each landscape tile is a self contained mesh, and meshes are generally cheap. With a mesh you can describe any shape with a known level of resolution (which can vary over the mesh) and accurately describe linear features within the design pipeline rather than at runtime.