
The bump mapping was a bit painful, due to using tri-planar texturing for the terrain. While the relevant GPU Gems article touches on the subject of bump mapping, it doesn't really go into detail. Still, I seem to have ended up using a hacky method similar to the code in the article.
The scribbles below show my thinking out the tri-planar texturing method, and the coordinates needed. I'm using the same texture on multiple "sides" of the projection for the moment, and seem to have a slightly different mapping from the article (notice that on all the sides of the cube UP is upwards, not on its side). Splitting out the 6 cases I wanted, the correct texture coordinates can be figured out from the world (/object) space position (they can be combined into 3 by using the sign of the relevant normal).

So far I've not managed to properly generate tangents to perturb the normal, so I'm just swapping the x / y displacement to the relevant world-space axis each for the tri-planar projection, adding it to the normal and normalizing. This is what the bump vector is on the right of the scribbles.
And here's a test image of the terrain using the tri-planar projection:

The snippet of glsl below shows the end result of the scribbles in code. I'm currently using a default texture, and an array of alpha maps / other textures - I might simplify this to include the default in the array at some point. I'm using noise to sharpen and randomise the transition between different textures - this could also be improved, perhaps taking into account the bump map.
// get terrain normal (already normalized)
vec3 normal = texture(NormalsTexture, TexCoords).rgb;
// calculate triplanar texture weights
vec3 tpweights = abs(normal);
tpweights = (tpweights - 0.2) * 7.0;
tpweights = max(tpweights, vec3(0.0));
tpweights /= tpweights.x + tpweights.y + tpweights.z;
vec3 signs = sign(normal);
vec2 tpcoord1 = vec2(vec2(-signs.x * Position.z, Position.y) * DetailTexCoordScale);
vec2 tpcoord2 = vec2(vec2(signs.y * Position.x, -Position.z) * DetailTexCoordScale);
vec2 tpcoord3 = vec2(vec2(signs.z * Position.x, Position.y) * DetailTexCoordScale);
// noise value for blending
float noise = 0.0;
noise += tpweights.x * texture(NoiseTexture, tpcoord1 * NoiseCoordScale).r;
noise += tpweights.y * texture(NoiseTexture, tpcoord2 * NoiseCoordScale).r;
noise += tpweights.z * texture(NoiseTexture, tpcoord3 * NoiseCoordScale).r;
// collect detail texture data based on alpha maps
float alpha_accum = 0.0;
vec3 detail_color = vec3(0.0);
vec3 detail_normal = vec3(0.0);
for (int i = 0; i != NumDetailTextures; ++i) {
float alpha = texture(AlphaTextureArray, vec3(TexCoords, i)).r;
// modulate alpha value with noise, to get sharper, but noisy transitions
alpha = smoothstep(0.48, 0.52, alpha + noise - 0.5);
// calclate triplanar detail colour
vec3 dc = vec3(0.0);
dc += tpweights.x * texture(AlbedoTextureArray, vec3(tpcoord1, i)).rgb;
dc += tpweights.y * texture(AlbedoTextureArray, vec3(tpcoord2, i)).rgb;
dc += tpweights.z * texture(AlbedoTextureArray, vec3(tpcoord3, i)).rgb;
detail_color += min(alpha, 1.0 - alpha_accum) * dc;
vec3 bump1 = tpweights.x * normalize(texture(NormalTextureArray, vec3(tpcoord1, i)).rgb * 2.0 - 1.0);
vec3 bump2 = tpweights.y * normalize(texture(NormalTextureArray, vec3(tpcoord2, i)).rgb * 2.0 - 1.0);
vec3 bump3 = tpweights.z * normalize(texture(NormalTextureArray, vec3(tpcoord3, i)).rgb * 2.0 - 1.0);
vec3 dn = vec3(0.0);
dn += vec3(0.0, bump1.y, -signs.x * bump1.x);
dn += vec3(signs.y * bump2.x, 0.0, -bump2.y);
dn += vec3(signs.z * bump3.x, bump3.y, 0.0);
detail_normal += min(alpha, 1.0 - alpha_accum) * dn;
alpha_accum = min(1.0, alpha_accum + alpha);
}
// fill remaining alpha "space" with default detail colour
vec3 ddc = vec3(0.0);
ddc += tpweights.x * texture(DefaultAlbedoTexture, tpcoord1).rgb;
ddc += tpweights.y * texture(DefaultAlbedoTexture, tpcoord2).rgb;
ddc += tpweights.z * texture(DefaultAlbedoTexture, tpcoord3).rgb;
detail_color += (1.0 - alpha_accum) * ddc;
vec3 bump1 = tpweights.x * normalize(texture(DefaultNormalTexture, tpcoord1).rgb * 2.0 - 1.0);
vec3 bump2 = tpweights.y * normalize(texture(DefaultNormalTexture, tpcoord2).rgb * 2.0 - 1.0);
vec3 bump3 = tpweights.z * normalize(texture(DefaultNormalTexture, tpcoord3).rgb * 2.0 - 1.0);
vec3 ddn = vec3(0.0);
ddn += vec3(0.0, bump1.y, -signs.x * bump1.x);
ddn += vec3(signs.y * bump2.x, 0.0, -bump2.y);
ddn += vec3(signs.z * bump3.x, bump3.y, 0.0);
detail_normal += (1.0 - alpha_accum) * ddn;
// hacky normal mapping
normal = normalize(normal + detail_normal * BumpScale);
I'm still not 100% satisfied with this, and will probably try out the method of generating a tangent basis touched upon in the GPU Gems article and see if it looks better. In the mean time I'd love to hear if anyone else has done any bump mapping with tri-planar texturing, and how you got it to work.
Cheers,
__sprite