Godot terrain generation experiment
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

383 lines
10 KiB

tool
extends Spatial
var data = {}
var noise = OpenSimplexNoise.new()
var mesh = MeshInstance.new()
var marker = MeshInstance.new()
var marker_small = MeshInstance.new()
var markers = Spatial.new()
var st = SurfaceTool.new()
export var update = false
export var update_delay = 5000
export var noise_seed = 42
export var noise_period = 256
export var noise_octaves = 8
export var noise_persistence = 1/3.0
export var noise_lacunarity = 3
export var noise_offset_x = 0
export var noise_offset_y = 0
export var amplitude = 800
export var width = 100
export var length = 100
export var power = 4 # higher = more flat surfaces
export var run_erosion = false
export var erosion_delay = 500
export var rain_decimation = 0.05
export var rain_decimation_dampening = 1.0
export var rain_constraint = false
export var rain_constraint_x = 84
export var rain_constraint_y = 74
export var rain_constraint_spread = 6
export var inertia_dampening = 0.1
var last_update = 0
var last_erosion = 0
export var color = Color(0.5, 1, 0)
var material = SpatialMaterial.new()
var material_marker_decimation = SpatialMaterial.new()
var material_marker_deposit = SpatialMaterial.new()
# Called when the node enters the scene tree for the first time.
func _ready():
# add markers and mesh to self so they are actually rendered
self.mesh.name = "mesh"
self.markers.name = "markers"
self.add_child(self.mesh)
self.add_child(self.markers)
self.noise.seed = self.noise_seed
self.noise.set_period(self.noise_period)
self.noise.set_octaves(self.noise_octaves)
self.noise.set_persistence(self.noise_persistence)
self.noise.set_lacunarity(self.noise_lacunarity)
self.material.albedo_color = self.color
self.material.roughness = 0.8
self.material.metallic = 0.3
#self.material.set_grow_enabled(true)
#self.material.set_grow(0.5)
self.material_marker_decimation.albedo_color = Color(0, 0.5, 1)
self.material_marker_deposit.albedo_color = Color(1, 0.5, 0)
var sphere = SphereMesh.new()
sphere.set_radius(0.125)
sphere.set_height(0.25)
sphere.set_rings(3)
sphere.set_radial_segments(3)
self.marker.name = "marker"
self.marker.set_mesh(sphere)
self.marker.set_material_override(self.material_marker_decimation)
sphere = SphereMesh.new()
sphere.set_radius(0.125/2)
sphere.set_height(0.125)
sphere.set_rings(1)
sphere.set_radial_segments(1)
self.marker_small.name = "marker_small"
self.marker_small.set_mesh(sphere)
self.marker_small.set_material_override(self.material_marker_decimation)
# build terrain data
for x in range(self.width + 1):
for y in range(0, self.length + 1):
self.data[Vector2(x, y)] = self.get_noise_2d(x, y)
if x + 0.5 < self.width and y + 0.5 < self.length:
self.data[Vector2(x + 0.5, y + 0.5)] = self.get_noise_2d(x + 0.5, y + 0.5)
self.update_terrain() # render data into a mesh
func _process(delta):
var t = OS.get_system_time_msecs()
if self.update and t >= self.last_update + self.update_delay:
self.update_terrain()
self.last_update = t
if self.run_erosion and t >= self.last_erosion + self.erosion_delay:
for node in self.markers.get_children():
self.markers.remove_child(node)
node.queue_free()
var x = 0
var y = 0
if self.rain_constraint == true:
x = int(self.rain_constraint_x - self.rain_constraint_spread / 2 + randi() % (self.rain_constraint_spread + 1))
y = int(self.rain_constraint_y - self.rain_constraint_spread / 2 + randi() % (self.rain_constraint_spread + 1))
else:
x = randi() % self.width
y = randi() % self.length
if randi() % 2 and x < self.width and y < self.length:
x += 0.5
y += 0.5
self.apply_raindrop(x, y)
self.last_erosion = t
func get_noise_2d(x, y):
return pow(self.noise.get_noise_2d(self.noise_offset_x + x, self.noise_offset_y + y), self.power) * amplitude
func get_quadrant_coordinates(x, y):
# NOTE: x and y correspond to left front vertex of a quadrant
return {
'center': Vector2(x + 0.5, y + 0.5),
'left_front': Vector2(x, y),
'left_back': Vector2(x, y + 1),
'right_front': Vector2(x + 1, y),
'right_back': Vector2(x + 1, y + 1)
}
func get_connected_coordinates(x, y):
var coords = {}
if x - int(x) == 0: # means we have a coordinate NOT in the center of a quadrant
if x - 1 >= 0:
coords['left'] = Vector2(x - 1, y)
if x + 1 <= self.width:
coords['right'] = Vector2(x + 1, y)
if y - 1 >= 0:
coords['front'] = Vector2(x, y - 1)
if y + 1 <= self.length:
coords['back'] = Vector2(x, y + 1)
if x - 0.5 >= 0:
if y - 0.5 >= 0:
coords['left_front'] = Vector2(x - 0.5, y - 0.5)
if y + 0.5 <= self.length:
coords['left_back'] = Vector2(x - 0.5, y + 0.5)
if x + 0.5 <= self.width:
if y - 0.5 >= 0:
coords['right_front'] = Vector2(x + 0.5, y - 0.5)
if y + 0.5 <= self.length:
coords['right_back'] = Vector2(x + 0.5, y + 0.5)
return coords
func get_steepness_data(x, y):
var steepness = {}
var own_amplitude = self.data[Vector2(x, y)]
var connected_coordinates = self.get_connected_coordinates(x, y)
for pos in connected_coordinates:
var amplitude = self.data[connected_coordinates[pos]]
steepness[pos] = amplitude - own_amplitude
if '_' in pos:
steepness[pos] /= sqrt(0.5) # adjust for less distance between diagonal points
#steepness[pos] *= 2
return steepness
func get_flow_data(x, y, flow, decimation):
var own_coord = Vector2(x, y)
var connected = self.get_connected_coordinates(x, y)
var positions = []
var data = {}
if flow in ['left', 'right']:
positions += ['front', 'back']
elif flow in ['front', 'back']:
positions += ['left', 'right']
elif flow in ['left_front', 'right_back']:
positions += ['left_back', 'right_front']
elif flow in ['left_back', 'right_front']:
positions += ['left_front', 'right_back']
for pos in positions:
if connected.has(pos):
var flow_coord = connected[pos]
var local_steepness = self.data[flow_coord] - self.data[own_coord]
var flow_decimation = decimation * 0.5
#if flow_coord[0] - int(flow_coord[0]) == 0.5: # center vertex of a quadrant
#flow_decimation = decimation * sqrt(0.5)
#flow_decimation = flow_decimation / sqrt(0.5)
#flow_decimation = decimation * 0.75
#flow_decimation = decimation * (1 - 0.5 * sqrt(0.5))
#flow_decimation = decimation
#if local_steepness > 0:
# flow_decimation = 0
#elif flow_decimation > local_steepness * -1:
if flow_decimation > local_steepness * -1 and local_steepness < 0:
flow_decimation = local_steepness * -1
#print("flow decimation: ", flow_decimation)
data[flow_coord] = flow_decimation
# add trailing center vertices with full decimation
var trail_coords = []
if flow == 'left':
if x - 0.5 > 0:
if y - 0.5 > 0:
trail_coords.append(Vector2(x - 0.5, y - 0.5))
if y + 0.5 < self.length:
trail_coords.append(Vector2(x - 0.5, y + 0.5))
elif flow == 'right':
if x + 0.5 < self.width:
if y - 0.5 > 0:
trail_coords.append(Vector2(x + 0.5, y - 0.5))
if y + 0.5 < self.length:
trail_coords.append(Vector2(x + 0.5, y + 0.5))
elif flow == 'front':
if y - 0.5 > 0:
if x - 0.5 > 0:
trail_coords.append(Vector2(x - 0.5, y - 0.5))
if x + 0.5 < self.width:
trail_coords.append(Vector2(x + 0.5, y - 0.5))
elif flow == 'back':
if y + 0.5 < self.length:
if x - 0.5 > 0:
trail_coords.append(Vector2(x - 0.5, y + 0.5))
if x + 0.5 < self.width:
trail_coords.append(Vector2(x + 0.5, y + 0.5))
for coord in trail_coords:
var local_steepness = self.data[coord] - self.data[own_coord]
var trail_decimation = decimation
if trail_decimation > local_steepness * -1 and local_steepness < 0:
trail_decimation = local_steepness * -1
data[coord] = trail_decimation
return data
func get_deposit_coordinates(x, y):
var own_coord = Vector2(x, y)
var coords = []
for coord in self.get_connected_coordinates(x, y).values():
var steepness = self.data[coord] - self.data[own_coord]
if steepness <= 0:
coords.append(coord)
return coords
func update_terrain():
# generates a mesh from self.data
self.st.clear()
self.st.begin(Mesh.PRIMITIVE_TRIANGLES)
self.st.set_material(self.material)
self.st.add_uv(Vector2(0,0))
for x in range(0, self.width):
for y in range(0, self.length):
var coords2d = self.get_quadrant_coordinates(x, y)
for pos in ['right_front', 'left_front', 'center',\
'left_back', 'right_back', 'center',\
'left_front', 'left_back', 'center',\
'right_back', 'right_front', 'center']:
var coord = coords2d[pos]
var amplitude = self.data[coord]
self.st.add_vertex(Vector3(coord[0], amplitude, coord[1] * -1))
self.st.add_uv(Vector2(1,1))
st.generate_normals()
st.generate_tangents()
self.mesh.set_mesh(st.commit())
func add_marker(x, y, type='decimation'):
var amplitude = self.data[Vector2(x, y)]
var m
if type == 'flow':
m = self.marker_small.duplicate()
else:
m = self.marker.duplicate()
if type == 'deposit':
m.set_material_override(self.material_marker_deposit)
m.translate(Vector3(x, amplitude, y * -1))
self.markers.add_child(m)
func apply_raindrop(x, y, level=1, deposit=0):
var own_coord = Vector2(x, y)
var own_amplitude = self.data[own_coord]
var connected_coords = self.get_connected_coordinates(x, y)
var steepness = self.get_steepness_data(x, y)
var steepest = steepness.values().min()
var flow = 'stop'
if steepest <= 0:
var steepest_key_idx = steepness.values().find(steepest)
flow = steepness.keys()[steepest_key_idx]
if flow == 'stop' or level > 255:
self.data[own_coord] += deposit / 2
self.add_marker(x, y, 'deposit')
var deposit_coords = self.get_deposit_coordinates(x, y)
for coord in deposit_coords:
self.data[coord] += deposit / (2 * len(deposit_coords))
self.add_marker(coord[0], coord[1], 'deposit')
else:
var next_coord = connected_coords[flow] # what coordinate the drop is flowing to
#var decimation = self.rain_decimation / level
var max_decimation = self.rain_decimation / (1 + self.rain_decimation_dampening * pow(level - 1, 2))
var decimation = max_decimation
if decimation > steepest * -1:
decimation = steepest * -1
var flow_data = self.get_flow_data(x, y, flow, max_decimation)
self.data[own_coord] -= decimation
deposit += decimation
self.add_marker(x, y, 'decimation')
for flow_coord in flow_data:
var flow_decimation = flow_data[flow_coord]
self.data[flow_coord] -= flow_decimation
deposit += flow_decimation
self.add_marker(flow_coord[0], flow_coord[1], 'flow')
self.apply_raindrop(next_coord[0], next_coord[1], level + 1, deposit)