As you might know I am able to generate interesting terrain meshes, now it is time to get some real look, so how? The answer is simple - apply appropriate texture to the terrain mesh :-). How to get it? I bet that the best solution is to generate it procedurally, since the terrain is generated the same way..
The basic step is to generate terrain mesh, the mesh should be smooth. If not the result will be similar as on the following images.
To achieve smoother mesh you have to use more erosion or other post processing filter or use different combination for terrain genaration. I used the third option in my case. The terrain mesh that I will texture is on the following image.. The height map resolution is 256x256 in this case.
After the mesh is chosen it is time to generate a texture. We need to know a resolution at first. Well the higher it is the better visual quality the rendered terrain has and also the bigger HW requirements you need. I will use the resolution 1024x1024 in the following example.
After that you need to know what kind of surfaces do you want. I want grass, rock and snow for the big hills in this case. Then you find these textures somewhere. :)) (e.g. on some free texture websites or you can just take a photo of them) For simplicity I would recommend you the same resolution as for the generated texture. I guess it is easier to resize it in graphical application like Gimp or Photoshop rather then construct an algorithm that will use the appropriate pixels.
Then you have to specify the height distribution for the surfaces. This implies that you have to know min height and max height. For that purpose the height map could be normalized or analyzed to set up appropriate height distribution table.
surface | low height | high height |
grass | 0 | 64 |
rock | 64 | 192 |
snow | 192 | 255 |
Next step is the texture generation, basically you need to set up every pixel with right color based on the height value and combination of textures. I set it up inspired by this article.
where is index of surface, and is weight of the surface,
To achieve good looking results the blending variant is used. My weight algorithm is slightly different.
[codesyntax lang="cpp"]
float FinalRenderer::getWeight(float height, TextureRegion r) { float w = 0; if( height >= r.LowHeight +r.Range && height <= r.HighHeight - r.Range ) { w = 1; }else if( height < r.LowHeight + r.Range && height > r.LowHeight - r.Range){ w = (height + r.Range - r.LowHeight) / ( 2 * r.Range); }else if(height >= r.HighHeight - r.Range && height <= r.HighHeight + r.Range ) { w = 1- ((height + r.Range - r.HighHeight)/(2*r.Range)); } return w; }
[/codesyntax]
Where texture region is defined as:
[codesyntax lang="cpp"]
struct TextureRegion { float LowHeight; float Range; float HighHeight; SDL_Surface * srf; };
[/codesyntax]
SDL_Surface pointer points to loaded surface texture. The range is used as indicator where the surface is 100% and where is blended with others. I believe that you can understand it better from the code rather then from my description, so look at the code.
The texture generating method in this case could look like this:
[codesyntax lang="cpp"]
SDL_Surface * FinalRenderer::generateTexture() { int w = _map->getWidth(); int he = _map->getHeight(); int texWidth = 1024; int texHeight = 1024; float wR = (float)w / (float)texWidth; float hR = (float)he / (float)texHeight; SDL_Surface * texture = SDL_CreateRGBSurface(0,texWidth,texHeight,24,0,0,0,0); SDL_LockSurface(texture); for(int i = 0; i < texWidth; i++) { for(int j = 0; j < texHeight;j++) { int hx = (int)((float)i * wR); int hy = (int)((float)j * hR); float h = _map->getHeightMapValue({hx,hy}); float r = 0; float g = 0; float b = 0; for(int re = 0; re < 3;re++){ // for every surface region SDL_Color c = getSurfaceColor(_regions[re].srf,i,j); float w = getWeight(h,_regions[re]); if( re == 0 && h < _regions[re].LowHeight + _regions[re].Range){ r = c.r; b = c.b;g=c.g; }else if( re == 2 && h > _regions[re].HighHeight - _regions[re].Range){ r = c.r;b=c.b; g= c.g; }else { r += w * c.r; b += w * c.b; g += w * c.g; } } setSurfaceColor(texture,j,i,{(unsigned char)r,(unsigned char)g,(unsigned char)b,255}); } } SDL_UnlockSurface(texture); return texture; }
[/codesyntax]
Look at the variables wR a hR they are important because the height map has different resolution. As you can see the generated texture is linearly interpolated. Also take note that the special cases the first surface - grass and the las one - snow are handled to be 100% covered by the surface texture.
The code for getSurfaceColor(..) and setSurfaceColor is following for my textures (note that the BGR pixel format is used):
[codesyntax lang="cpp"]
void FinalRenderer::setSurfaceColor(SDL_Surface * s, int x, int y, SDL_Color c){ unsigned char * pxs = (unsigned char *)s->pixels; pxs[y * s->h *3 + (x *3)+2] = c.r; pxs[y * s->h *3 + (x *3)]= c.b; pxs[y * s->h *3 + (x *3)+1] = c.g; } SDL_Color FinalRenderer::getSurfaceColor(SDL_Surface* s, int x, int y){ unsigned char * pxs = (unsigned char *)s->pixels; SDL_Color ret; ret.r = pxs[y * s->h *3 + (x *3)+2]; ret.b = pxs[y * s->h *3 + (x *3)]; ret.g = pxs[y * s->h *3 + (x *3)+1]; return ret; }
[/codesyntax]
Using described methods I have achieved this result:
You can see the blending and little pixelization caused by the zooming on the terrain detail image. The smaller is the texture the bigger the effect it has. The 1024x1024 is also big texture. To achieve a more detail with a smaller texture resolution detail maps are used. I will focus on them in next article. I think that this one is long enough 🙂
The next article will be about detail map and rendering techniques. This one is rendered by brute force, which is slow. Better performance is usually achieved by LoD (level of details) algorithms which I haven't implemented yet.