proper terrain generation
This commit is contained in:
parent
4eef953ed6
commit
159b6274c9
5 changed files with 1544 additions and 86 deletions
|
@ -1,7 +1,6 @@
|
||||||
extends CharacterBody3D
|
extends CharacterBody3D
|
||||||
|
|
||||||
|
@export var SPEED:float = 30.0
|
||||||
const SPEED = 5.0
|
|
||||||
const JUMP_VELOCITY = 4.5
|
const JUMP_VELOCITY = 4.5
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,13 +10,14 @@ func _physics_process(delta: float) -> void:
|
||||||
velocity += get_gravity() * delta
|
velocity += get_gravity() * delta
|
||||||
|
|
||||||
# Handle jump.
|
# Handle jump.
|
||||||
if Input.is_action_just_pressed("ui_accept") and is_on_floor():
|
if Input.is_action_just_pressed("jump") and is_on_floor():
|
||||||
velocity.y = JUMP_VELOCITY
|
velocity.y = JUMP_VELOCITY
|
||||||
|
|
||||||
# Get the input direction and handle the movement/deceleration.
|
# Get the input direction and handle the movement/deceleration.
|
||||||
# As good practice, you should replace UI actions with custom gameplay actions.
|
# As good practice, you should replace UI actions with custom gameplay actions.
|
||||||
var input_dir := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
|
var input_dir := Input.get_vector("left", "right", "forward", "downard")
|
||||||
var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
|
var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
|
||||||
|
|
||||||
if direction:
|
if direction:
|
||||||
velocity.x = direction.x * SPEED
|
velocity.x = direction.x * SPEED
|
||||||
velocity.z = direction.z * SPEED
|
velocity.z = direction.z * SPEED
|
||||||
|
@ -25,4 +25,6 @@ func _physics_process(delta: float) -> void:
|
||||||
velocity.x = move_toward(velocity.x, 0, SPEED)
|
velocity.x = move_toward(velocity.x, 0, SPEED)
|
||||||
velocity.z = move_toward(velocity.z, 0, SPEED)
|
velocity.z = move_toward(velocity.z, 0, SPEED)
|
||||||
|
|
||||||
|
rotate_y(Input.get_axis("camera_right","camera_left")*0.1)
|
||||||
|
%Camera3D.rotate_x(Input.get_axis("camera_down","camera_up")*0.1)
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
|
|
1402
world_gen/main.tscn
1402
world_gen/main.tscn
File diff suppressed because one or more lines are too long
22
world_gen/player.tscn
Normal file
22
world_gen/player.tscn
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
[gd_scene load_steps=4 format=3 uid="uid://m3y8d8rf4wg8"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://b5io1eg4blrp5" path="res://player/player.gd" id="1_awywi"]
|
||||||
|
|
||||||
|
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_y7qka"]
|
||||||
|
resource_name = "PlayerCollision"
|
||||||
|
|
||||||
|
[sub_resource type="CapsuleMesh" id="CapsuleMesh_nwtrj"]
|
||||||
|
resource_name = "PlayerMesh"
|
||||||
|
|
||||||
|
[node name="Player" type="CharacterBody3D"]
|
||||||
|
script = ExtResource("1_awywi")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||||
|
shape = SubResource("CapsuleShape3D_y7qka")
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="CollisionShape3D"]
|
||||||
|
mesh = SubResource("CapsuleMesh_nwtrj")
|
||||||
|
|
||||||
|
[node name="Camera3D" type="Camera3D" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 0.9604348, 0.27850503, 0, -0.27850503, 0.9604348, 0, 1.8589027, 2.8465767)
|
8
world_gen/terrain_noise.tres
Normal file
8
world_gen/terrain_noise.tres
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
[gd_resource type="FastNoiseLite" format=3 uid="uid://dknkwbejk05gt"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
noise_type = 4
|
||||||
|
seed = -20
|
||||||
|
frequency = 0.0034
|
||||||
|
fractal_octaves = 3
|
||||||
|
fractal_lacunarity = 1.985
|
|
@ -5,7 +5,7 @@ var generation_thread: Thread = Thread.new()
|
||||||
@export_category("CHUNK GENERATION")
|
@export_category("CHUNK GENERATION")
|
||||||
|
|
||||||
const CHUNK_SIZE := 256
|
const CHUNK_SIZE := 256
|
||||||
const CHUNK_BOUNDS := 8
|
const CHUNK_BOUNDS := 4
|
||||||
|
|
||||||
## How detailed the chunk should be. This could be made a const when debug phase is finished
|
## How detailed the chunk should be. This could be made a const when debug phase is finished
|
||||||
@export_range(4, 256, 4) var chunk_resolution := 32:
|
@export_range(4, 256, 4) var chunk_resolution := 32:
|
||||||
|
@ -16,22 +16,19 @@ const CHUNK_BOUNDS := 8
|
||||||
#### WORLD GENERATION ####
|
#### WORLD GENERATION ####
|
||||||
@export_category("WORLD GENERATION")
|
@export_category("WORLD GENERATION")
|
||||||
@export_tool_button("Generate Chunks") var generate_chunk_callable = generate.bind(true)
|
@export_tool_button("Generate Chunks") var generate_chunk_callable = generate.bind(true)
|
||||||
@export var noise: FastNoiseLite:
|
@export_tool_button("Delete Chunks") var delete_chunk_callable = delete.bind(true)
|
||||||
set(new_noise):
|
@export var noise: FastNoiseLite
|
||||||
noise = new_noise
|
|
||||||
update_all_mesh()
|
|
||||||
if noise:
|
|
||||||
noise.changed.connect(update_all_mesh) #change all thes meshes after eahc changes
|
|
||||||
@export_range(4.0, 128.0, 4.0) var height := 64.0:
|
@export_range(4.0, 128.0, 4.0) var height := 64.0:
|
||||||
set(new_height):
|
set(new_height):
|
||||||
height = new_height
|
height = new_height
|
||||||
update_all_mesh()
|
update_all_mesh()
|
||||||
|
|
||||||
var loaded_chunks := {}
|
var loaded_chunks := {}# Key: Vector2i, Value: MeshInstance3D
|
||||||
|
|
||||||
|
|
||||||
# Called when the node enters the scene tree for the first time. ALSO IN EDITOR
|
# Called when the node enters the scene tree for the first time. ALSO IN EDITOR
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
if !(Engine.is_editor_hint()):
|
||||||
generate(true)
|
generate(true)
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,35 +37,53 @@ func generate(_ForceRegen:bool=false):
|
||||||
generate_chunks_around(Vector3(0, 0, 0), _ForceRegen)
|
generate_chunks_around(Vector3(0, 0, 0), _ForceRegen)
|
||||||
|
|
||||||
|
|
||||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
func delete(_Force:bool = true):
|
||||||
func _process(_delta: float) -> void:
|
loaded_chunks.clear()
|
||||||
pass
|
for c in get_children():
|
||||||
|
c.queue_free()
|
||||||
|
|
||||||
|
|
||||||
func generate_noise(_seed: float):
|
func generate_noise(_seed: float):
|
||||||
noise = FastNoiseLite.new()
|
noise = FastNoiseLite.new()
|
||||||
|
noise.noise_type = FastNoiseLite.TYPE_PERLIN
|
||||||
|
noise.seed = _seed
|
||||||
|
noise.frequency = 0.01
|
||||||
|
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
update_all_mesh()
|
||||||
|
|
||||||
|
|
||||||
# This function is better called in another thread, doing all the heavy process before adding it to the scene tree
|
|
||||||
func generate_chunk(position: Vector3, _ForceRegen: bool = false):
|
func generate_chunk(position: Vector3, _ForceRegen: bool = false):
|
||||||
|
|
||||||
WorkerThreadPool.add_task(func():
|
WorkerThreadPool.add_task(func():
|
||||||
var mesh_instance = MeshInstance3D.new()
|
var mesh_instance = MeshInstance3D.new()
|
||||||
update_mesh(mesh_instance)
|
update_mesh(mesh_instance, position)
|
||||||
mesh_instance.position = position
|
mesh_instance.position = position
|
||||||
call_deferred("add_child", mesh_instance)
|
|
||||||
var chunk_coord = get_chunk_coord(position)
|
var chunk_coord = get_chunk_coord(position)
|
||||||
loaded_chunks[chunk_coord] = mesh_instance
|
call_deferred("add_chunk_deferred", mesh_instance, chunk_coord)
|
||||||
print("chunk at : ",str(position),", coord ",str(chunk_coord), " loaded")
|
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func add_chunk_deferred(mesh_instance: MeshInstance3D, chunk_coord: Vector2i):
|
||||||
|
add_child(mesh_instance)
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
mesh_instance.owner = get_tree().edited_scene_root
|
||||||
|
|
||||||
|
loaded_chunks[chunk_coord] = mesh_instance
|
||||||
|
print("Chunk at: ", str(mesh_instance.position), ", coord: ", str(chunk_coord), " loaded")
|
||||||
|
|
||||||
|
|
||||||
func get_height(x: float, y: float) -> float:
|
func get_height(x: float, y: float) -> float:
|
||||||
|
if not noise:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
return noise.get_noise_2d(x, y) * height
|
return noise.get_noise_2d(x, y) * height
|
||||||
|
|
||||||
|
|
||||||
func get_normal(x: float, y: float) -> Vector3:
|
func get_normal(x: float, y: float) -> Vector3:
|
||||||
|
if not noise:
|
||||||
|
return Vector3.UP
|
||||||
|
|
||||||
var epsilon := CHUNK_SIZE / chunk_resolution
|
var epsilon := CHUNK_SIZE / chunk_resolution
|
||||||
var normal := Vector3(
|
var normal := Vector3(
|
||||||
(get_height(x + epsilon, y) - get_height(x - epsilon, y)) / (2.0 * epsilon),
|
(get_height(x + epsilon, y) - get_height(x - epsilon, y)) / (2.0 * epsilon),
|
||||||
|
@ -79,13 +94,33 @@ func get_normal(x:float,y:float) -> Vector3:
|
||||||
return normal.normalized()
|
return normal.normalized()
|
||||||
|
|
||||||
|
|
||||||
|
func get_biome(x: float, z: float) -> String:
|
||||||
|
if not noise:
|
||||||
|
return "default"
|
||||||
|
|
||||||
|
var biome_noise = FastNoiseLite.new()
|
||||||
|
biome_noise.noise_type = FastNoiseLite.TYPE_CELLULAR
|
||||||
|
biome_noise.seed = noise.seed + 1
|
||||||
|
biome_noise.frequency = 0.005
|
||||||
|
var value = biome_noise.get_noise_2d(x, z)
|
||||||
|
|
||||||
|
if value < -0.3:
|
||||||
|
return "desert"
|
||||||
|
elif value < 0.1:
|
||||||
|
return "forest"
|
||||||
|
elif value < 0.4:
|
||||||
|
return "mountain"
|
||||||
|
else:
|
||||||
|
return "ocean"
|
||||||
|
|
||||||
|
|
||||||
func update_all_mesh():
|
func update_all_mesh():
|
||||||
for chunk in loaded_chunks:
|
for chunk_coord in loaded_chunks:
|
||||||
update_mesh(loaded_chunks[chunk])
|
update_mesh(loaded_chunks[chunk_coord], loaded_chunks[chunk_coord].position)
|
||||||
|
|
||||||
|
|
||||||
func update_mesh(chunk_terrain_instance:MeshInstance3D):
|
func update_mesh(chunk_terrain_instance: MeshInstance3D, chunk_pos: Vector3):
|
||||||
var plane: PlaneMesh = PlaneMesh.new() #load("res://world_gen/new_plane_mesh.tres").duplicate()
|
var plane: PlaneMesh = load("res://world_gen/new_plane_mesh.tres").duplicate()
|
||||||
plane.subdivide_depth = chunk_resolution
|
plane.subdivide_depth = chunk_resolution
|
||||||
plane.subdivide_width = chunk_resolution
|
plane.subdivide_width = chunk_resolution
|
||||||
plane.size = Vector2(CHUNK_SIZE, CHUNK_SIZE)
|
plane.size = Vector2(CHUNK_SIZE, CHUNK_SIZE)
|
||||||
|
@ -97,9 +132,8 @@ func update_mesh(chunk_terrain_instance:MeshInstance3D):
|
||||||
|
|
||||||
for i: int in vertex_array.size():
|
for i: int in vertex_array.size():
|
||||||
var vertex := vertex_array[i]
|
var vertex := vertex_array[i]
|
||||||
# Convert local vertex coordinates to world coordinates
|
var world_x = chunk_pos.x + vertex.x
|
||||||
var world_x = chunk_terrain_instance.position.x + vertex.x
|
var world_z = chunk_pos.z + vertex.z
|
||||||
var world_z = chunk_terrain_instance.position.z + vertex.z
|
|
||||||
var normal := Vector3.UP
|
var normal := Vector3.UP
|
||||||
var tangent := Vector3.RIGHT
|
var tangent := Vector3.RIGHT
|
||||||
|
|
||||||
|
@ -115,9 +149,36 @@ func update_mesh(chunk_terrain_instance:MeshInstance3D):
|
||||||
tangent_array[4 * i + 2] = tangent.z
|
tangent_array[4 * i + 2] = tangent.z
|
||||||
tangent_array[4 * i + 3] = 1.0
|
tangent_array[4 * i + 3] = 1.0
|
||||||
|
|
||||||
|
plane_arrays[ArrayMesh.ARRAY_VERTEX] = vertex_array
|
||||||
|
plane_arrays[ArrayMesh.ARRAY_NORMAL] = normal_array
|
||||||
|
plane_arrays[ArrayMesh.ARRAY_TANGENT] = tangent_array
|
||||||
|
|
||||||
var array_mesh := ArrayMesh.new()
|
var array_mesh := ArrayMesh.new()
|
||||||
array_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, plane_arrays)
|
array_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, plane_arrays)
|
||||||
|
#array_mesh.surface_set_material(0, get_biome_mat(chunk_pos))
|
||||||
chunk_terrain_instance.mesh = array_mesh
|
chunk_terrain_instance.mesh = array_mesh
|
||||||
|
chunk_terrain_instance.create_trimesh_collision()
|
||||||
|
|
||||||
|
|
||||||
|
func get_biome_mat(chunk_pos) -> Material:
|
||||||
|
# Apply biome-based material
|
||||||
|
var material = StandardMaterial3D.new()
|
||||||
|
var biome = get_biome(chunk_pos.x + CHUNK_SIZE / 2.0, chunk_pos.z + CHUNK_SIZE / 2.0)
|
||||||
|
|
||||||
|
match biome:
|
||||||
|
"desert":
|
||||||
|
material.albedo_color = Color(1.0, 0.8, 0.4)
|
||||||
|
|
||||||
|
"forest":
|
||||||
|
material.albedo_color = Color(0.2, 0.6, 0.2)
|
||||||
|
|
||||||
|
"mountain":
|
||||||
|
material.albedo_color = Color(0.5, 0.5, 0.5)
|
||||||
|
|
||||||
|
"ocean":
|
||||||
|
material.albedo_color = Color(0.0, 0.4, 0.8)
|
||||||
|
|
||||||
|
return material
|
||||||
|
|
||||||
|
|
||||||
## Convert position to chunk coordinates
|
## Convert position to chunk coordinates
|
||||||
|
@ -128,22 +189,31 @@ func get_chunk_coord(position:Vector3) -> Vector2i:
|
||||||
|
|
||||||
|
|
||||||
func generate_chunks_around(position: Vector3, ForceRegen: bool = false):
|
func generate_chunks_around(position: Vector3, ForceRegen: bool = false):
|
||||||
|
|
||||||
# Convert position to chunk coordinates
|
# Convert position to chunk coordinates
|
||||||
var position_to_coord := get_chunk_coord(position)
|
var position_to_coord := get_chunk_coord(position)
|
||||||
|
print("Closest chunk: ", str(position_to_coord))
|
||||||
print("closest chunk : "+str(Vector2i(position_to_coord.x,position_to_coord.y)))
|
var new_chunks := {}
|
||||||
|
|
||||||
for x in range(position_to_coord.x - CHUNK_BOUNDS, position_to_coord.x + CHUNK_BOUNDS + 1):
|
for x in range(position_to_coord.x - CHUNK_BOUNDS, position_to_coord.x + CHUNK_BOUNDS + 1):
|
||||||
for z in range(position_to_coord.y - CHUNK_BOUNDS, position_to_coord.y + CHUNK_BOUNDS + 1):
|
for z in range(position_to_coord.y - CHUNK_BOUNDS, position_to_coord.y + CHUNK_BOUNDS + 1):
|
||||||
var chunk_coord := Vector2i(x, z)
|
var chunk_coord := Vector2i(x, z)
|
||||||
var chunck_position := Vector3i(x * CHUNK_SIZE,0,z * CHUNK_SIZE)
|
var chunk_position := Vector3(x * CHUNK_SIZE, 0, z * CHUNK_SIZE)
|
||||||
print("trying to load chunk at coordinate : "+str(chunk_coord))
|
print("Trying to load chunk at coordinate: ", chunk_coord)
|
||||||
if ForceRegen:
|
|
||||||
loaded_chunks.clear()
|
if ForceRegen and loaded_chunks.has(chunk_coord):
|
||||||
for child in get_children():
|
loaded_chunks[chunk_coord].queue_free()
|
||||||
child.queue_free()
|
loaded_chunks.erase(chunk_coord)
|
||||||
|
|
||||||
if not loaded_chunks.has(chunk_coord):
|
if not loaded_chunks.has(chunk_coord):
|
||||||
print("generate chunk at : "+str(chunck_position))
|
generate_chunk(chunk_position, ForceRegen)
|
||||||
generate_chunk(chunck_position)
|
new_chunks[chunk_coord] = null # Placeholder until chunk is added
|
||||||
|
else:
|
||||||
|
new_chunks[chunk_coord] = loaded_chunks[chunk_coord]
|
||||||
|
|
||||||
|
# Unload chunks outside bounds
|
||||||
|
for chunk_coord in loaded_chunks:
|
||||||
|
if not new_chunks.has(chunk_coord):
|
||||||
|
loaded_chunks[chunk_coord].queue_free()
|
||||||
|
loaded_chunks.erase(chunk_coord)
|
||||||
|
|
||||||
|
loaded_chunks = new_chunks
|
||||||
|
|
Loading…
Reference in a new issue