

This function is more or less around 25 arithmetic instructions.Ĭonst int nbTiles = int(1.0 / diffPackFactors.x) vec3 uvw0 = calculateTexturePackMipWrapped(uv, diffPackFactors) vec4 terrainType = texture2D(terrainLUT, vec2(slope, altitude)) int id0 = int(terrainType.x * 256.0) vec2 offset0 = vec2(mod(id0, nbTiles), id0 / nbTiles) diffuse = texture2DLod(diffusePack, uvw0.xy + diffPackFactors.xy * offset0, uvw0.z) / This function evaluates the mipmap LOD level for a 2D texture using the given texture coordinates/// and texture size (in pixels)float mipmapLevel(vec2 uv, vec2 textureSize) Here is the function I'm using to do that: The solution is to calculate the mipmap level manually. The fract() instructions kill the coherency between the tiles, and 1-pixel-width seams appear (which are viewer dependent, so extremely visible and annoying). This doesn't work with mipmapping, because the hardware uses the 2x2 neighbooring pixels to determine the mipmap level. Since UVs are always normalized, each tile is 1/4 th of the pack, hence the 0.25 ). ( remember that there are 4x4 tiles in the pack. U = fract(u)v = fract(v)u = tile_offset.x + u * 0.25v = tile_offset.y + v * 0.25 The first natural idea is to perform those two operations in the shader: You need to sample the tile within the pack, but with mipmapping and wrapping. The problem is that you can't sample the pack directly, as it contains many tiles. So, for each pixel you've got an UV to sample the tile. Since the ID is a small value (0-15), I have multiplied it by 16 to see it better in grayscale:

and we get in terrainType.x the ID of the tile (0-15) we need to use for the current pixel. Vec4 terrainType = texture2D(terrainLUT, vec2(slope, altitude)) Once the texture pack and the LUT are uploaded to the gpu, the shader is ready to do its job. It contains the ID of the layer / tile for the corresponding slope / altitude. The lookup table ( LUT ) is a RGBA texture, but at the moment I'm only using the red channel. For our use, it is sufficient to know that the lookup table indexes the dot-product of the slope on the horizontal / U axis, and the altitude on the vertical / V axis. There are many ways to create this table, but it's beyond the point of this article. Each layer / tile has a set of constraints ( for example, grass must only grow when the slope is lower than 20? and the altitude is between 50m and 3000m ). The slope is the dot product between the up vector and the vertex normal and normalized to.

To each vertex of the terrain is associated a slope and altitude. So you can complete it by downsampling with a box filter, or fill garbage it doesn't really matter. But it doesn't matter, because in the pixel shader, you can specific a maximum lod level when samplying the mipmap. Of course, from there, there is no way to complete the mipmaps chain in a coherent way. When you're generating the mipmaps chain, you will arrive at a point where each tile is 1x1 pixel in the pack ( so the whole pack will be 4x4 pixels ). The standard way of generating mipmaps ( by downsampling and applying a box filter ) doesn't work anymore, so you must construct the mipmaps chain yourself, and copy the border columns/rows so that it's seamless for all levels.

This constraint must be enforced when you're generating mipmaps. Here is an example of a pack with 13 tiles ( the top-right 3 are unused and stay black ):Įach image / tile was originally seamless: its right pixels column matches it left, and its top matches its bottom. You can pack 4x4 = 16 of them in a single 2048x2048. The idea is to create a texture pack that contains N images ( also called layers or tiles ), for example for grass, rock, snow, etc. The whole technique is based on sub-tiling. Many people have been asking me how the terrain texturing is implemented, so I'll make a special dev journal about it.
