Tuesday, May 12, 2009

Dynamically Generated Heightmap Textures

Everyone has to do several of these in their life I'm sure. I mean, implementing some kind of blend scheme for making a texture for a height field is terrain 101.

Terrain Texture Image

heightmap image 2

The particular method I've been working on is based on Ysaneyva's latest method. He generates a texture for each of his terrain patches using a shader that blends up to 16 textures based on a look up table. The inputs for the look up table are slope and height - read more about it at Ysaneya's dev journal post on atmospheric scattering and terrain texturing

My test implementation here uses Ogre3d as the graphics engine. I'm using the Octree scene manager plugin to do the quick heightmap - but I highjack the textures used by the heightmap and overwrite them with my dynamically generated texture. The process is as follows:

1. create heightmap with octree terrain manager
2. load the heightmap image and create a slope map from it
3. render a full screen quad that has the the texture generation shader which combines up to 16 textures based on the terrain height,slope. The height corresponds to the y value and the slope corresponds to the x value in the look up table.
4. write the render to texture into the texture used by the heightfield

Here's what my look up table looks like:
Look up table image

I'm still experimenting with look up table values and blending between altitudes and slopes. Also, I need to add some random noise to the altitude/slope values to mix things up a bit.

Of course the actual planet textures won't be so blurry, or if I do end up using this resolution I will probably implement some kind of detail map on top of it.

Here's the fragment shader (GLSL) for the texture generation:

void main()
{

float altitude = texture2D(heightMap, gl_TexCoord[0].st).r;
float slope = texture2D(slopeMap, gl_TexCoord[0].st).r;

vec2 uvSize = vec2(uvExtents.z - uvExtents.x, uvExtents.w - uvExtents.y);

// diffuse UV should be the UV position relative to the face on the cube
// if we just used gl_TexCoord[0] we would have UV position relative to
// each quadrant and we'd get seams
vec2 diffUV = vec2(uvExtents.x + (gl_TexCoord[0].s * uvSize.x),
uvExtents.y + (gl_TexCoord[0].t * uvSize.y));

// tile the textures so they look better in this test
diffUV.x *= 10.0;
diffUV.y *= 10.0;

// get the 16 blending weights for this slope and altitude
vec4 weights0 = texture2D(lutTex, vec2(slope * 0.25 + 0.00, altitude));
vec4 weights1 = texture2D(lutTex, vec2(slope * 0.25 + 0.25, altitude));
vec4 weights2 = texture2D(lutTex, vec2(slope * 0.25 + 0.50, altitude));
vec4 weights3 = texture2D(lutTex, vec2(slope * 0.25 + 0.75, altitude));

// use w,x,y,z order because PNG uses pixel format A8R8G8B8
gl_FragColor = texture2D(diffTex0, diffUV) * weights0.w +
texture2D(diffTex1, diffUV) * weights0.x +
texture2D(diffTex2, diffUV) * weights0.y +
texture2D(diffTex3, diffUV) * weights0.z +
texture2D(diffTex4, diffUV) * weights1.w +
texture2D(diffTex5, diffUV) * weights1.x +
texture2D(diffTex6, diffUV) * weights1.y +
texture2D(diffTex7, diffUV) * weights1.z +
texture2D(diffTex8, diffUV) * weights2.w +
texture2D(diffTex9, diffUV) * weights2.x +
texture2D(diffTex10, diffUV) * weights2.y +
texture2D(diffTex11, diffUV) * weights2.z;
}


*edit* I should note that the above shader only combines 12 textures - of which I'm only using 8 for now - you should have no trouble adding the extra lines for diffTex12-diffTex15 and the weights3 vec4.

No comments: