Back to Blog
4 min read

Killing the Heightmap: Why We Switched to Noise-Driven Terrain Texturing in Our Civ-Style RTS

The Problem with Height-Based Terrain

For weeks, I was stuck on a classic trap: using elevation alone to determine terrain texturing in our Civ-style RTS. Grass at low elevations, dirt in the mid, rock up top—simple, predictable, and boring. Worse, it didn’t reflect how real biomes work. Deserts aren’t just lowlands. Forests don’t avoid hills just because they’re tall. By tying texture choice strictly to height, we were forcing artificial boundaries and missing out on organic variety.

The rigidity showed in gameplay too. Players could predict terrain types from contour lines. That kills exploration. It also meant transitions were either hardcoded or precomputed—leading to bloated tilesets and brittle logic. One commit summed it up: [Civ RTS] cleanup, decent trantions, textures wrong but placed well. We had clean code, decent transitions, but the textures themselves were still off. Why? Because we were still feeding the wrong inputs into a system that couldn’t adapt.

Enter Noise-Driven Influence Maps

So we killed the heightmap’s authority.

Not literally—we still use elevation as one factor—but we stopped letting it dictate texture choice. Instead, we built a noise-driven influence system that blends multiple procedural inputs: moisture, temperature, erosion, and regional variation—all generated via 2D noise fields sampled per-tile.

Each terrain type (grass, sand, forest, rock, etc.) now has an ‘influence weight’ calculated using a blend of these noise values. Instead of saying “if height > 0.7, use rock”, we now say “rock gets +0.3 from height, +0.4 from low moisture, +0.2 from high erosion”, and so on. The final texture is a weighted blend of the top 2–3 contenders.

This lets us do things like:

  • Generate rocky deserts at high elevations in arid zones
  • Place lush forests in mid-height, high-moisture valleys
  • Scatter grasslands across rolling lowlands with moderate variation

And because the inputs are procedural, we don’t need to store transition maps. No more precomputed tilesets for ‘grass-to-rock-slope-north’. The shader handles blending on the fly based on live weights.

Blending Smarter in the Shader

The real win came when we moved the blending logic into the GPU. Here’s a stripped-down version of the fragment shader we’re now using in Godot (via a custom ShaderMaterial on our tile mesh):

shader_type spatial;

uniform sampler2D u_grass;
uniform sampler2D u_rock;
uniform sampler2D u_sand;
uniform sampler2D u_noise_map; // Combined moisture/erosion/variation

varying vec2 v_uv;
varying vec3 v_weights; // Pre-passed influence weights: (grass, rock, sand)

void fragment() {
	vec2 noise = texture(u_noise_map, v_uv * 5.0).rg; // Tiled noise for detail
	
	// Sample textures
	vec3 grass_col = texture(u_grass, v_uv * 10.0).rgb;
	vec3 rock_col = texture(u_rock, v_uv * 10.0).rgb;
	vec3 sand_col = texture(u_sand, v_uv * 10.0).rgb;
	
	// Blend based on weights
	vec3 blended = grass_col * v_weights.x;
	blended = mix(blended, rock_col, v_weights.y);
	blended = mix(blended, sand_col, v_weights.z);
	
	// Add subtle noise variation to break repetition
	blended *= 0.9 + noise.r * 0.2;
	
	ALBEDO = blended;
}

The v_weights are calculated on the CPU during tile generation and passed as vertex attributes—cheap, predictable, and flexible. The noise map breaks up tiling without adding draw calls. And because we’re not relying on atlas indices or prebuilt transition tiles, our asset pipeline is simpler and more scalable.

Today’s commit—[Civ RTS] needs tweaking, but textures are blending—feels like a milestone. It’s not perfect yet. The weights need tuning. Some biomes still look off. But for the first time, the terrain feels alive. It’s not just height. It’s context. It’s climate. It’s chance.

And that’s the point. In a strategy game about civilizations rising and falling, the world should feel unpredictable, varied, and full of possibility. We’re not there yet—but now, at least, the ground beneath their feet is starting to earn its place.

Newer post

How We Solved Biome Blending in a Hex-Based 3D RTS Using Precomputed Transition Maps

Older post

How We Fixed Biome Transitions in Our Hex-Based 3D RTS (And Made Terrain Rendering Faster)