⛰️ Tempest terrain 2: height + slope materials
Terrain: height + slope blending
I've been working on getting the height modification to work. And also height + slope blending to work the same way across clipmap rings, patch borders and while the camera moves.
What we're building on
We're rendering terrain with a geometry clipmap: multiple LOD rings follow the camera, and each ring is the same grid scaled per LOD. The world is split into patches, and each patch provides:
- height data
- splat weights / material control
- per-material inputs (albedo/normal/ORM + parameters)
This setup is great for coverage and performance, but it also makes it easy to run into sampling problems. Patch borders and ring boundaries are amkes sampling a little more complicated because of discontinuities in how data is laid out, even if the world itself is continuous.
Height mips: matching the sample to the ring scale
Early on I was sampling height at full resolution everywhere. Close to the camera it looked fine, but the coarse rings cover a lot of ground per vertex, and they were still reacting to tiny height details that show up as artifacty shimmer during movement, which I don't want since TAA is already an issue.
To fix that, we can generate a mip chain for the height field and choose a mip based on the ring's world-space scale.
which should make:
- coarse rings sample a blurrier height
- reconstructed normals stop reacting to tiny texel changes far away
- slope/cliff decisions become more stable as the camera moves
Patch issues
Patch UVs behave nicely inside a patch, but they reset at the border. That means the UVs jump from 1 back to 0 when we cross into the next patch.
I switched to taking derivatives from world-space XZ and scaling them into patch UV space. which helped a bit but still isn't enough, however, the seams have largely been elimanted and we use the same sampling.
Slopes blending
We want to blend the terrain between slopes and across patch regions seamlessly.
Initially the slope blending was quite unstable, and that was because i was feeding a noise normal map which i thought I could get away with, but it just doesn't look right. Instead now I reconstruct a geometric normal from the height field by sampling around the current point and building a gradient. But we need to make sure that the sampling offset is consistent in world space and we need to make sure the height sample itself comes from the right mip otherwise it would look bad.
Height blending
With slope somewhat stable, we need to fix the height blending so layers settle naturally instead of looking like a flat mix. I've exposed these variables in the shader
- blend strength
- sharpness
- bias
- contrast
like this:
vec4 ApplyHeightBlend(vec4 w, vec4 h)
{
float strength = clamp(uCB.HeightBlendParams.x, 0.0, 1.0);
if (strength <= 0.0) return w;
float sharp = max(uCB.HeightBlendParams.y, 0.001);
float contrast = max(uCB.HeightBlendParams.z, 0.001);
float bias = uCB.HeightBlendParams.w;
h = clamp((h - 0.5) * contrast + 0.5 + bias, 0.0, 1.0);
float maxH = max(max(h.x, h.y), max(h.z, h.w));
vec4 a = (h - vec4(maxH)) * (sharp * strength);
vec4 ww = w * exp2(a);
ww = max(ww, w * 0.02);
float s = max(dot(ww, vec4(1.0)), 1e-6);
return ww / s;
}
the inputs w is the sampled splat/weight mask (using gradients for correct mip selection) from earlier and h is each layers height, so in this case you'll be right in guessing we support a max of 4 heights, hence vec4, but i'd like to extend the layer system in the future.
The result of ApplyHeightBlend is a weighed-sum which we then apply to AO, normal, and baseColor.
Also we want it to be easy to tune per biome/material set without it turning into banding or noise.