This is another view from inside Valhalla Provincial Park, looking southeast.
In this image, there are three 1024 x 1024 normal maps tiled over the terrain. Each normal map has a different UV scale. The GLSL fragment shader adds the normals from the three normal maps together, then normalizes the result. It looks correct, although I'm not sure if it's actually correct or not.
Land-o-Rama took a severe frame-rate hit. On my semi-ancient PC (AMD Athlon 2000 XP, NVidia GeForce FX 5950 Ultra), the frame rate went down from ~60 to ~24 frames per second. Strangely, it appears that the vertex shader is the bottleneck. If I look straight down so that only a few triangles appear in the view frustum, the frame rate goes up to ~80 fps. Hmmm, I though that it would have been the fragment shader that was the bottleneck.
Adding normal mapping to Land-o-Rama was quite the learning experience. And it was a little frustrating. I've learned two important things.
Generating tangent vectors isn't as easy as it looks:
A tangent vector is simply a vector that is perpendicular to the normal. Sounds easy to generate, right? Well it wasn't so easy to me :-)
There are an infinite number of vectors that are perpendicular to a given normal. You can't just pick one and use it. You need them to be consistently oriented.
I had two choices when it came to generating the tangent vectors:
- Pre-calculate the tangent vectors and create another GLSL attribute array to store them. This would require a lot of changes to Land-o-Rama.
- Calculate the tangent vectors within the vertex shader. The code to do this is somewhat complex and would probably slow down the shader. Since this calculation requires the UV coordinates from adjacent vertices, I'd have to create another GLSL attribute array to store these UV coordinates. In which case, I might as well use this array to store the tangent vectors instead, just like the first method mentioned above.
So it looked like a there was a lot of work ahead of me. But then I thought of something. Since I'm rendering a regular grid, maybe there's a shortcut I could use to generate tangent vectors. As mentioned above, tangent vectors must have a consistent orientation. What if in the vertex shader, you take the normal and rotate it 90 degrees in all three axes? This sounds like it would be consistent. I added that code to the shader and tried it out.
Wow, it appears to actually work! Lighting appears on the correct side of the bumps. w00t! I'm not sure if it is mathematically correct though. I don't pretend to understand the math behind generating these vectors. But so far, I haven't seen anything strange with the lighting.
My GLSL code for bump mapping looks like the following:
From the vertex shader:
/* Create a tangent vector for this normal. Since we are rendering a
regular terrain grid, we can get away with rotating the normal 90 degrees
in each direction and using that as the tangent vector (I think). */
vec3 tangent = normal.zyx * vec3 (1.0, -1.0, 1.0);
/* Create a TBN matrix from the normal and tangent vectors. This matrix
transforms vectors from eye space to tangent space. */
vec3 nPartOfTBNMatrix = normalize (gl_NormalMatrix * normal);
vec3 tPartOfTBNMatrix = normalize (gl_NormalMatrix * tangent);
vec3 bPartOfTBNMatrix = cross (tPartOfTBNMatrix, nPartOfTBNMatrix);
/* Transform the light vector from eye space to tangent space. */
vec3 eLightVector3 = gl_LightSource.position.xyz;
TangentLightVector.x = dot (eLightVector3, tPartOfTBNMatrix);
TangentLightVector.y = dot (eLightVector3, bPartOfTBNMatrix);
TangentLightVector.z = dot (eLightVector3, nPartOfTBNMatrix);
From the fragment shader:
/* Sample the normal-map textures. */
vec3 normal1 = texture2D (NormalMapSampler1, normalMapCoord1).xyz;
vec3 normal2 = texture2D (NormalMapSampler2, normalMapCoord2).xyz;
vec3 normal3 = texture2D (NormalMapSampler3, normalMapCoord3).xyz;
/* Add the normals from the three normal maps together, then normalize the
result. Because normals have positive and negative values, we need to
map each normal from the (0..1) range to the (-0.5..+0.5) range. We can
perform the mapping on all three normals at once by subtracting 1.5 from
the summed normal (0.5 * 3). */
vec3 normal = normal1 + normal2 + normal3 - 1.5;
normal = normalize (normal);
/* Calculate the lighting amount. */
float lightAmount = max (dot (TangentLightVector, normal), 0.0);
Texture quality is very important:
Texture quality makes a huge difference. Look at the textures in Resident Evil 4 on the Gamecube. I found that RE4 was easily the nicest looking game from the previous console generation, even though the Gamecube's hardware is inferior to the XBox's.
So even if you had the best engine in the world running on a quad-SLI NVidia 999900 Uber-XForce GT(TM), the resulting renderings would look like crap if the texture maps were of poor quality.
My first normal maps produced very shoddy renderings. After much playing around, I now have normal maps that produce much better results. They're not that great, but they're a hell of a lot better than they used to be.
This is the normal map I ended up using. Do what you want with it.
To create it, I used The GIMP and some other software. This is the process I used:
- Create a grayscale image with some Perlin noise.
- Use World Machine to apply some water erosion to the noise.
- Use the GIMP Resynthesizer plugin to create a seamless texture from the eroded noise.
- Generate another image with some more Perlin noise and apply a curve to it such that it produces lots of flat areas with some cracks in between.
- Blend the two images together. I used a 70-30 blend; 70% came from the image from Step 3 and 30% came from the image from Step 4.
- Use the GIMP normalmap plugin to generate the normal map.
Next, I'll add detail textures (rock, grass, etc.) to Land-o-Rama.