Voxel terrain generation

Godot Version

Godot4.6

Question

I am struggling to generate the ideal terrain that i have in my mind. I would appreciate it if you could help me.

The ideal terrain i want to generate is a smooth landscape (a combination of fairly wide flat areas and gentle elevation changes) with small cliff-life hills of moderate height (clusters about 5-6 blocks) generated at random intervals.

Images shows what current terrain looks like. three problems 1) 1 block is generated (i dont wanna generate hills made of 1 or 2 blocks), 2) the hills sometime have holes of 1 or 2 blocks, 3) i want my terrain to have some areas where small hills are densely clustered and others that open up into wide spaces.

var noise_continent = FastNoiseLite.new()
var noise_hills = FastNoiseLite.new()
var noise_detail = FastNoiseLite.new()
var noise_flat = FastNoiseLite.new()
var noise_cliff = FastNoiseLite.new()
var noise_cliff_detail = FastNoiseLite.new()
# -------------------------
# 地形パラメータ(ここをいじる)
# -------------------------
@export var CONTINENT_SCALE = 8.0
@export var HILLS_SCALE = 14.0
@export var DETAIL_SCALE = 1.5

@export var BASE_HEIGHT = 15.0

@export var CONTINENT_FREQ = 0.002
@export var HILLS_FREQ = 0.02
@export var DETAIL_FREQ = 0.02
@export var FLAT_FREQ = 0.01
@export var CLIFF_FREQ = 0.5 #低いほどまばら、高いほど広い
@export var CLIFF_DETAIL_FREQ  = 0.03

@export var FLAT_STRENGTH = 0.5

func setup(pos: Vector3, seed: int):

	position_offset = pos
	position = pos

	# 大陸ノイズ
	noise_continent.seed = seed
	noise_continent.noise_type = FastNoiseLite.TYPE_PERLIN
	noise_continent.frequency = CONTINENT_FREQ

	# 丘ノイズ
	noise_hills.seed = seed + 100
	noise_hills.noise_type = FastNoiseLite.TYPE_PERLIN
	noise_hills.frequency = HILLS_FREQ

	# ディテール
	noise_detail.seed = seed + 200
	noise_detail.noise_type = FastNoiseLite.TYPE_PERLIN
	noise_detail.frequency = DETAIL_FREQ

	# 平坦化
	noise_flat.seed = seed + 300
	noise_flat.noise_type = FastNoiseLite.TYPE_PERLIN
	noise_flat.frequency = FLAT_FREQ

	#崖マスク(広さ)
	noise_cliff.seed = seed + 500
	noise_cliff.noise_type = FastNoiseLite.TYPE_PERLIN
	noise_cliff.frequency = CLIFF_FREQ

	#崖ディティール
	noise_cliff_detail.seed = seed + 600
	noise_cliff_detail.noise_type = FastNoiseLite.TYPE_SIMPLEX
	noise_cliff_detail.frequency = CLIFF_DETAIL_FREQ  # 細かさ(重要)

func terrain_generation():

	for x in SIZE:
		for z in SIZE:

			var world_x = x + position_offset.x
			var world_z = z + position_offset.z

			# -------------------------
			# ① 大陸
			# -------------------------
			var continent = noise_continent.get_noise_2d(world_x, world_z)
			continent = (continent + 1.0) * 0.5
			continent *= CONTINENT_SCALE

			# -------------------------
			# ② 丘
			# -------------------------
			var hills = noise_hills.get_noise_2d(world_x, world_z)
			hills = (hills + 1.0) * 0.5
			hills *= HILLS_SCALE

			# -------------------------
			# ③ 細かい凹凸
			# -------------------------
			var detail = noise_detail.get_noise_2d(world_x, world_z)
			detail *= DETAIL_SCALE

			# -------------------------
			# 合成
			# -------------------------
			var height = continent + hills + detail

			# -------------------------
			# 崖生成(ここに追加!!!)
			# -------------------------
			var cliff_mask = noise_cliff.get_noise_2d(world_x, world_z)
			cliff_mask = (cliff_mask + 1.0) * 0.5

			if cliff_mask > 0.4: #低いほど出現率が高い
				var detail_cliff = noise_cliff_detail.get_noise_2d(world_x, world_z)
				detail_cliff = (detail_cliff + 1.0) * 0.5

				if detail_cliff > 0.75:
					height += detail_cliff * 10


			# -------------------------
			# 平原化
			# -------------------------
			var flat = noise_flat.get_noise_2d(world_x, world_z)
			flat = (flat + 1.0) * 0.5

			height = lerp(height, BASE_HEIGHT, flat * FLAT_STRENGTH)

			# -------------------------
			# 丸め
			# -------------------------
			height = int(round(height))

			# -------------------------
			# ブロック配置
			# -------------------------
			for y in HEIGHT:

				if y > height:
					blocks[x][y][z] = 0
				elif y == height:
					blocks[x][y][z] = 1
				elif y > height - 3:
					blocks[x][y][z] = 2
				else:
					blocks[x][y][z] = 3

People often think that because Minecraft seems simple, that it is easy to re-create. It is not. It is exceedingly hard. I do not recommend a Minecraft style game if you are new to either programming or game dev.

However, if you have your heart set on it, I recommend you read some of the many threads already on the forum here about it. There’s lots of help already available on the topic.

1 Like

I do not think Minecraft is simple and do not think it is easy to re-create. Thank you.

Then follow my advice in the second paragraph.

Well I have looked many threads based on the keyword (procedural terrain generation, voxel trerrain generation, perlin noise, etc.), yet I have found none which matches my specific concern. Thus, here I have created a new post.

Modulate the parameters of a noise function with other noise functions.

I will recommend this. Create a texture and apply the results of your process as a gray scale image. Allow to change scale X/Y and see how it smooth to choose proper boundaries for your values. Hard changes in the gray means the values are changing too abruptly and the result are those high stepped hills. From there you can start playing on modulations, running the result for another pass changing the frequency and scale as @normalized stated. Perlin Noise allows to to change fractal_octaves, fractal_lacunarity and fractal_gain to achieve that.

Here is an example I use on my own generator:

extends Node

class_name NoiseUtils

#	noise.fractal_octaves = 8
#	noise.fractal_lacunarity = 2.75
#	noise.fractal_gain = 0.4

static func generate_noise_map_perlin(noise_seed:int, fractal_octaves : int, fractal_lacunarity : float, fractal_gain : float, noise_scale:float, size:int, noise_offset:Vector2, soft_exp:float = 0.0) -> PackedFloat32Array:
	
	var noise = FastNoiseLite.new()

	noise.noise_type = FastNoiseLite.TYPE_PERLIN
	noise.seed = noise_seed

	noise.fractal_octaves = fractal_octaves
	noise.fractal_lacunarity = 2.75
	noise.fractal_gain = 0.4

	var heights = PackedFloat32Array()
	var min_noise_value : float = 2.0
	var max_noise_value : float = -2.0
	
	for z in range(size + 1):
		for x in range(size + 1):

			var noise_position = Vector2(x, z)
			noise_position += noise_offset
			noise_position *= noise_scale

			var noise_value : float = noise.get_noise_2d(noise_position.x, noise_position.y)

			noise_value = noise_value + 0.5
			noise_value = clamp( noise_value, 0.0, 1.0 )
			
			#  Help for Island / Beaches / smooth mountain sides
			if soft_exp != 0.0:
				noise_value = pow(noise_value, soft_exp);
			
			if noise_value > max_noise_value:
				max_noise_value = noise_value
			if noise_value < min_noise_value:
				min_noise_value = noise_value
			
			heights.append(noise_value)

	#print("Min Noise value : " + str(min_noise_value))
	#print("Max Noise Value : " + str(max_noise_value))
	
	return heights
	


static func generate_noise_map_voronoi(noise_seed:int, fractal_octaves : int, fractal_lacunarity : float, fractal_gain : float, noise_scale:float, size:int, noise_offset:Vector2, soft_exp:float = 0.0) -> PackedFloat32Array:
	
	var noise = FastNoiseLite.new()

	noise.noise_type = FastNoiseLite.TYPE_CELLULAR
	noise.seed = noise_seed
	noise.frequency = 0.01

	noise.cellular_distance_function = FastNoiseLite.CellularDistanceFunction.DISTANCE_EUCLIDEAN_SQUARED
	noise.cellular_jitter = 0.75
	noise.cellular_return_type = FastNoiseLite.CellularReturnType.RETURN_DISTANCE2_DIV
	
	#noise_scale = 0.050
	#noise.fractal_octaves = fractal_octaves
	#noise.fractal_lacunarity = 2.75
	#noise.fractal_gain = 0.4

	var heights = PackedFloat32Array()
	var min_noise_value : float = 2.0
	var max_noise_value : float = -2.0
	
	for z in range(size + 1):
		for x in range(size + 1):

			var noise_position = Vector2(x, z)
			noise_position += noise_offset
			noise_position *= noise_scale

			var noise_value : float = noise.get_noise_2d(noise_position.x, noise_position.y)
			noise_value = (noise_value + 1.0) 
			
			noise_value = clamp( noise_value, 0.0, 1.0 )
			
			#  Help for Island / Beaches / smooth mountain sides
			if soft_exp != 0.0:
				noise_value = pow(noise_value, soft_exp);
			
			if noise_value > max_noise_value:
				max_noise_value = noise_value
			if noise_value < min_noise_value:
				min_noise_value = noise_value
			
			heights.append(noise_value)

	#print("Min Noise value : " + str(min_noise_value))
	#print("Max Noise Value : " + str(max_noise_value))
	
	return heights
	
	
		
static func generate_flat_map(size:int, value : float = 1.0) -> PackedFloat32Array :
	
	var heights = PackedFloat32Array()
	
	for z in range(size + 1):
		for x in range(size + 1):
			heights.append(value)

	return heights


static func remap_values(noise : PackedFloat32Array, curve : Curve) -> PackedFloat32Array :
	
	for n in range(noise.size()):
		noise[n] = curve.sample(noise[n])

	return noise
1 Like

Thank you very much for your idea. Will try to add other noise functions to the parameters.

One approach I find very useful is to employ low-frequency thresholded or posterized noise as a mix factor between multiple different noise functions. This works well to combat the main weakness of the standard generated noise - perceptual uniformity. It helps emulating some naturally-looking abrupt changes in frequency. I used a lot of such mixing/modulating for planet maps in Cataclysm. Here’s some alpha footage I already have uploaded. If you look closely you’ll see that variations in the terrain detail and tree distribution frequencies are much more interesting than what you can get from a naively used single noise function.

3 Likes

Thank you very much for giving me an idea on how to improve my terrain generation. And big thank you for providing me an absolutely brilliant example of code on how you did it. Creating a texture sounds great idea so that I can visually see how my noise doing.