Terrain from noise
A heightmap turned into an ArrayMesh with SurfaceTool, colored by height through vertex colors.
The editor above is running this project. Change a line and it reloads.
Godot classes used
SurfaceToolArrayMeshMeshInstance3D
The code
scripts/main.gd
extends Node
const GRID: int = 96
const SIZE: float = 34.0
const AMPLITUDE: float = 7.0
const SEED: int = 20260814
const BANDS: Array = [
{limit = 0.12, color = '#2a3a63'},
{limit = 0.20, color = '#f3d48d'},
{limit = 0.45, color = '#8eef97'},
{limit = 0.66, color = '#4c9c60'},
{limit = 0.86, color = '#2a2f3d'},
{limit = 2.00, color = '#e6ecff'},
]
var camera: Camera3D
var elapsed: float = 0.0
func _ready() -> void:
_add_environment()
_add_light()
var terrain: MeshInstance3D = MeshInstance3D.new()
terrain.mesh = _build_terrain()
terrain.material_override = _make_material()
add_child(terrain)
camera = Camera3D.new()
add_child(camera)
camera.current = true
print('the heightmap becomes an ArrayMesh through SurfaceTool, change SEED for another terrain')
func _process(delta: float) -> void:
elapsed += delta
var angle: float = elapsed * 0.15
camera.position = Vector3(sin(angle) * 27.0, 13.0, cos(angle) * 27.0)
camera.look_at(Vector3(0, 2.5, 0), Vector3.UP)
func _build_terrain() -> ArrayMesh:
var noise: FastNoiseLite = FastNoiseLite.new()
noise.seed = SEED
noise.noise_type = FastNoiseLite.TYPE_SIMPLEX
noise.fractal_octaves = 5
noise.frequency = 0.9 / GRID
var heights: PackedFloat32Array = PackedFloat32Array()
var lowest: float = 1.0
var highest: float = 0.0
for z in GRID + 1:
for x in GRID + 1:
var height: float = pow(noise.get_noise_2d(x, z) * 0.5 + 0.5, 1.6)
lowest = minf(lowest, height)
highest = maxf(highest, height)
heights.append(height)
for i in heights.size():
heights[i] = inverse_lerp(lowest, highest, heights[i])
var tool: SurfaceTool = SurfaceTool.new()
tool.begin(Mesh.PRIMITIVE_TRIANGLES)
for z in GRID:
for x in GRID:
var a: Vector3 = _vertex(heights, x, z)
var b: Vector3 = _vertex(heights, x + 1, z)
var c: Vector3 = _vertex(heights, x + 1, z + 1)
var d: Vector3 = _vertex(heights, x, z + 1)
for corner in [a, b, c, a, c, d]:
tool.set_color(_band_color(corner.y / AMPLITUDE))
tool.add_vertex(corner)
tool.generate_normals()
return tool.commit()
func _vertex(heights: PackedFloat32Array, x: int, z: int) -> Vector3:
var step: float = SIZE / GRID
return Vector3(x * step - SIZE * 0.5, heights[z * (GRID + 1) + x] * AMPLITUDE, z * step - SIZE * 0.5)
func _band_color(height: float) -> Color:
for band in BANDS:
if height < band.limit:
return Color(band.color)
return Color(BANDS[BANDS.size() - 1].color)
func _make_material() -> StandardMaterial3D:
var material: StandardMaterial3D = StandardMaterial3D.new()
material.vertex_color_use_as_albedo = true
material.roughness = 0.95
return material
func _add_environment() -> void:
var sky_material: ProceduralSkyMaterial = ProceduralSkyMaterial.new()
sky_material.sky_top_color = Color(0.04, 0.05, 0.1)
sky_material.sky_horizon_color = Color(0.12, 0.14, 0.22)
sky_material.ground_bottom_color = Color(0.02, 0.02, 0.04)
sky_material.ground_horizon_color = Color(0.09, 0.1, 0.15)
var sky: Sky = Sky.new()
sky.sky_material = sky_material
var env: Environment = Environment.new()
env.background_mode = Environment.BG_SKY
env.sky = sky
env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
env.ambient_light_energy = 0.8
var world: WorldEnvironment = WorldEnvironment.new()
world.environment = env
add_child(world)
func _add_light() -> void:
var light: DirectionalLight3D = DirectionalLight3D.new()
light.rotation_degrees = Vector3(-52, -40, 0)
light.light_energy = 1.2
add_child(light)