@tool extends Node #var generation_thread: Thread = Thread.new() #### CHUNK GENERATION #### @export_category("CHUNK GENERATION") const CHUNK_SIZE := 256 const CHUNK_BOUNDS := 6 ## 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: set(new_resolution): chunk_resolution = new_resolution update_all_mesh() #### WORLD GENERATION #### @export_category("WORLD GENERATION") @export_tool_button("Generate Chunks") var generate_chunk_callable = generate.bind(true) @export_tool_button("Delete Chunks") var delete_chunk_callable = delete.bind(true) @export var terrainnoise: FastNoiseLite @export_range(4.0, 256.0, 4.0) var height := 64.0: set(new_height): height = new_height update_all_mesh() var loaded_chunks := {}# Key: Vector2i, Value: MeshInstance3D @export_category("player") var player_coord := Vector2i(0,0) # Called when the node enters the scene tree for the first time. ALSO IN EDITOR func _ready() -> void: if !(Engine.is_editor_hint()): delete(true) generate(true) func generate(_ForceRegen: bool = false): print('Generation starts') generate_chunks_around(Vector3(0, 0, 0), _ForceRegen) func _process(_delta): var new_player_coord = get_chunk_coord($"../Player".position) if new_player_coord == player_coord: return generate_chunks_around($"../Player".position) player_coord = new_player_coord func delete(_Force:bool = true): for c in get_children(): c.free() loaded_chunks.clear() func generate_chunk(position: Vector3, _ForceRegen: bool = false): WorkerThreadPool.add_task(func(): var mesh_instance = MeshInstance3D.new() update_mesh(mesh_instance, position) mesh_instance.position = position var chunk_coord = get_chunk_coord(position) call_deferred("add_chunk_deferred", mesh_instance, chunk_coord) ) 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: if not terrainnoise: return 0.0 return terrainnoise.get_noise_2d(x, y) * height func get_normal(x: float, y: float) -> Vector3: if not terrainnoise: return Vector3.UP var epsilon := CHUNK_SIZE / chunk_resolution var normal := Vector3( (get_height(x + epsilon, y) - get_height(x - epsilon, y)) / (2.0 * epsilon), 1.0, (get_height(x, y + epsilon) - get_height(x, y - epsilon)) / (2.0 * epsilon) ) return normal.normalized() func get_biome(x: float, z: float) -> String: if not terrainnoise: return "default" var biome_noise = load("res://world_gen/biome_noise.tres") biome_noise.seed = terrainnoise.seed 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(): for chunk_coord in loaded_chunks: update_mesh(loaded_chunks[chunk_coord], loaded_chunks[chunk_coord].position) func update_mesh(chunk_terrain_instance: MeshInstance3D, chunk_pos: Vector3): var plane: PlaneMesh = load("uid://rhv7l5ya1qnf").duplicate() # Used as a template for UV/vertex/normals that the final array mesh will inherit/expand plane.subdivide_depth = chunk_resolution plane.subdivide_width = chunk_resolution plane.size = Vector2(CHUNK_SIZE, CHUNK_SIZE) var plane_arrays := plane.get_mesh_arrays() var vertex_array: PackedVector3Array = plane_arrays[ArrayMesh.ARRAY_VERTEX] var normal_array: PackedVector3Array = plane_arrays[ArrayMesh.ARRAY_NORMAL] var tangent_array: PackedFloat32Array = plane_arrays[ArrayMesh.ARRAY_TANGENT] for i: int in vertex_array.size(): var vertex := vertex_array[i] var world_x = chunk_pos.x + vertex.x var world_z = chunk_pos.z + vertex.z var normal := Vector3.UP var tangent := Vector3.RIGHT if terrainnoise: vertex.y = get_height(world_x, world_z) normal = get_normal(world_x, world_z) tangent = normal.cross(Vector3.UP) vertex_array[i] = vertex normal_array[i] = normal tangent_array[4 * i] = tangent.x tangent_array[4 * i + 1] = tangent.y tangent_array[4 * i + 2] = tangent.z 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() array_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, plane_arrays) if get_biome_mat(chunk_pos): array_mesh.surface_set_material(0, get_biome_mat(chunk_pos)) 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() if material: material.albedo_texture = load("res://smooth+sand+dunes.jpg") var biome = get_biome(chunk_pos.x + CHUNK_SIZE / 2.0, chunk_pos.z + CHUNK_SIZE / 2.0) match biome: "desert": if material: material.albedo_color = Color(1.0, 0.8, 0.4) "forest": if material: material.albedo_color = Color(0.2, 0.6, 0.2) "mountain": if material: material.albedo_color = Color(0.5, 0.5, 0.5) "ocean": if material: material.albedo_color = Color(0.0, 0.4, 0.8) return material ## Convert position to chunk coordinates func get_chunk_coord(position: Vector3) -> Vector2i: var coord_chunk_x = int(floor(position.x / CHUNK_SIZE)) var coord_chunk_z = int(floor(position.z / CHUNK_SIZE)) return Vector2i(coord_chunk_x, coord_chunk_z) func generate_chunks_around(position: Vector3, ForceRegen: bool = false): # Convert position to chunk coordinates var position_to_coord := get_chunk_coord(position) print("Closest chunk: ", str(position_to_coord)) var new_chunks := {} 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): var chunk_coord := Vector2i(x, z) var chunk_position := Vector3(x * CHUNK_SIZE, 0, z * CHUNK_SIZE) print("Trying to load chunk at coordinate: ", chunk_coord) if loaded_chunks.has(chunk_coord): new_chunks[chunk_coord] = loaded_chunks[chunk_coord] else: generate_chunk(chunk_position, ForceRegen) new_chunks[chunk_coord] = null # Placeholder until chunk is added # Unload chunks outside bounds for coord in loaded_chunks.keys(): if not new_chunks.has(coord) && loaded_chunks[coord]: loaded_chunks[coord].queue_free() loaded_chunks[coord].call_deferred("queue_free") print("chunk at ",str(coord), "unloading") loaded_chunks.erase(coord) loaded_chunks = new_chunks