First, before having gotten into terrain normal mapping, I added mouse picking for objects. I have some interactivity now!
This is taken from an XNA code sample, then I modified it so it supports instanced meshes. So now it’s able to pick the exact instances that the ray intersets, and displays their mesh name. It doesn’t do anything other than that for now, but it’s just the first step towards editing objects in the level editor.
Mapping the terrain
The new update was for fixing a problem that’s been bugging me for a few weeks- combining normal mapping with triplanar texturing. It was a tricky affair as the normal maps get re-oriented along three planes so you also have to shift the normals accordingly. After revising how I did my regular normal mapping for other objects, I was able to get correct triplanar normal mapping for the terrain. This goes for both forward and deferred rendering.
I have only two regular textures- the base texture for mostly flat areas, and a blend texture for cliffs in steep areas. My normal map is for the cliff texture, and no normal mapping is applied for the flat areas. You can also set a bump intensity which increases the roughness of the terrain. Naturally, with great roughness comes
great respons- less specular highlights. So you would have to tune the specular and roughness so it achieves a good balance. Most of the time terrain, doesn’t need specular lighting, but it’s needed for wet and icy areas.
Bump up the volume
Terrain normals, binormals, and tangents are all calculated on the CPU, which is the ideal way to go as it saves a lot of overhead of doing it every frame. In the vertex shader, the normal, binormal and tangent are transformed to view space and added to a 3×3 matrix.
output.TangentToWorld = mul(normalize(mul(input.tangent, World)), View);
output.TangentToWorld = mul(normalize(mul(input.binormal, World)), View);
output.TangentToWorld = mul(normalize(mul(input.Normal, World)), View);
In the main pixel shader function we must first compute the normal mapping output before it can be contributed to the vertex normal outputs.
PixelShaderOutput PixelTerrainGBuffer(VT_Output input)
// Sample normal map color. 4 is the texture scale
float3 normal = TriplanarNormalMapping(input, 4);
// Output the normal, in [0,1] space
// Get normal into world space
float3 normalFromMap = mul(normal, input.TangentToWorld);
normalFromMap = normalize(normalFromMap);
output.Normal.rgb = 0.5f * (normalFromMap + 1.0f);
// ... Then output the other G-Buffer stuff
The textures are expected to be in the [0, 1] range and TriplanarNormalMapping outputs them to [-1, 1] so they are properly transformed with the TBN matrix. After that we can set the normals right back to the [0, 1] range for the lighting pass. Remember that it outputs to an unsigned format, so if we don’t do this, all values below zero will be lost.
The following function computes triplanar normal mapping for terrains.
float3 TriplanarNormalMapping(VT_Output input, float scale = 1)
float tighten = 0.3679f;
float mXY = saturate(abs(input.Normal.z) - tighten);
float mXZ = saturate(abs(input.Normal.y) - tighten);
float mYZ = saturate(abs(input.Normal.x) - tighten);
float total = mXY + mXZ + mYZ;
mXY /= total;
mXZ /= total;
mYZ /= total;
float3 cXY = tex2D(normalMapSampler, input.NewPosition.xy / scale);
float3 cXZ = float3(0, 0, 1);
float3 cYZ = tex2D(normalMapSampler, input.NewPosition.zy / scale);
// Convert texture lookups to the [-1, 1] range
cXY = 2.0f * cXY - 1.0f;
cYZ = 2.0f * cYZ - 1.0f;
float3 normal = cXY * mXY + cXZ * mXZ + cYZ * mYZ;
normal.xy *= bumpIntensity;
Note that where I define the texture lookups, the XZ plane is just set to a normal pointing directly towards the viewer. The X and Y values are in the [-1, 1] range, and Z is by default 1 because it is not used for view-space coordinates. So don’t forget to flip normalized negative values! Then X and Y are multiplied by the bumpIntensity. The default roughness is 1, and a roughness of 0 will completely ignore the normal map for the final output.
A lot of my texture mapping code was adapted from Memoirs of a Texel. Take caution, that if you want to follow that guide, there is a glaring mistake in that code that I noticed only after seeing this GPU Gems example (see example 1-3). You need to clamp your weight values to between 0 and 1 before averaging them out. The blog article doesn’t do this in its code. Otherwise you will get many dark patches in your textures. I fixed this with the saturate() function shown in the above example. This goes for regular texture mapping as well as normal mapping.
Here are some screenshots with the normal mapping in place. The bump intensity is set to 1.8 for a greater effect.
Edit: I’ve used some better textures for testing now. I got some free texture samples at FilterForge.
Normal computation is the same for forward rendering as it is for deferred rendering. The normals as they contribute to lighting would still be in the [0, 1] range in view space.