Yeah, I was able to figure it out finally.
You essentially have to move the calculations from the chunk scene into the second thread that’s running in your chunk-loading script. So you defer call the setting the position of the chunk in the chunk loading thread, then when the position is set, you have to have the chunk scene send the Surface tool calculations for generating all the triangles back to the chunk loading thread. Once those calculations are done, they need to send the surface tool commit back to the chunk scene to set the mesh.
So essentially the only thing that runs on the main thread is the setting position of the chunk, and the setting of the mesh.
Since you’ll have multiple instances where the calculation of the triangles are happening on a single thread along with the chunk loading you might want to do some sort of queue task:
extends Node3D
const DIMENSION = Global.DIMENSION
const GRID_SIZE = Global.GRID_SIZE
@onready var player = get_parent().get_parent().get_parent().get_node("Player")
var chunk_scene = preload("res://Scenes/Chunk.tscn")
var load_radius = 12
var chunk_list = []
var chunk_to_position = {}
var position_to_chunk = {}
var shared_player_position = Vector3()
var last_player_position = Vector3()
var chunk_position = Vector3.ZERO
#thread parameters
var task_queue = []
var task_queue_mutex = Mutex.new()
var background_thread = Thread.new()
var thread_running = false
var player_position_mutex = Mutex.new()
var update_chunk = Mutex.new()
func _ready():
island_generator = get_parent().get_parent().get_node("Island_Generation")
island_generator.connect("island_generation_complete", _on_island_generation_complete)
func _on_island_generation_complete():
start_chunk_generation()
func start_chunk_generation():
last_player_position = player.global_position
generate_initial_chunks()
thread_running = true
background_thread.start(_thread_process.bind(self))
#queue tasks ---------------------------------------------
func _get_next_task():
task_queue_mutex.lock()
var task = null
if task_queue.size() > 0:
task = task_queue.pop_front()
task_queue_mutex.unlock()
return task
func add_global_task(chunk_key, callback_object, callback_function):
task_queue_mutex.lock()
task_queue.push_front({"chunk_key": chunk_key, "callback_object": callback_object, "callback_function": callback_function})
task_queue_mutex.unlock()
func _execute_task(task):
var meshes = generate_mesh(task.chunk_key)
task.callback_object.call_deferred(task.callback_function, meshes)
func _exit_tree():
thread_running = false
background_thread.wait_to_finish()
#chunk_loading --------------------------------------------
func _thread_process(_userdata):
while thread_running and is_instance_valid(self):
var task = _get_next_task()
if task:
_execute_task(task)
else:
player_position_mutex.lock()
var player_chunk_pos = get_player_chunk_position()
player_position_mutex.unlock()
var new_positions = get_nearest_chunk_keys(player_chunk_pos)
for chunk in chunk_list:
var chunk_key = chunk_to_position[chunk]
if not new_positions.has(chunk_key):
var new_position = find_unoccupied_position(new_positions)
if new_position != Vector3.ZERO:
chunk.call_deferred("set_chunk_position", new_position)
chunk_to_position[chunk] = new_position
position_to_chunk[new_position] = chunk
position_to_chunk.erase(chunk_key)
func generate_initial_chunks():
var player_chunk_pos = get_player_chunk_position()
var nearest_chunk_keys = get_nearest_chunk_keys(player_chunk_pos)
for chunk_key in nearest_chunk_keys:
if Global.chunk_blocks.has(chunk_key):
var chunk = chunk_scene.instantiate()
add_child(chunk)
chunk.set_chunk_position(chunk_key)
chunk_list.append(chunk)
chunk_to_position[chunk] = chunk_key
position_to_chunk[chunk_key] = chunk
func find_unoccupied_position(new_positions):
for pos in new_positions:
if not position_to_chunk.has(pos):
return pos
return Vector3.ZERO
func get_player_chunk_position():
return Vector3(floor(shared_player_position.x / (Global.DIMENSION.x * Global.GRID_SIZE)),
floor(shared_player_position.y / (Global.DIMENSION.y * Global.GRID_SIZE)),
floor(shared_player_position.z / (Global.DIMENSION.z * Global.GRID_SIZE)))
func get_nearest_chunk_keys(player_chunk_pos):
var nearest_chunk_keys = []
for x in range(-load_radius, load_radius):
for y in range(-load_radius/2.0, load_radius/2.0):
for z in range(-load_radius, load_radius):
var chunk_key = player_chunk_pos + Vector3(x, y, z)
if Global.chunk_blocks.has(chunk_key):
nearest_chunk_keys.append(chunk_key)
return nearest_chunk_keys
func player_has_moved():
if player.position.distance_to(last_player_position) > .001:
last_player_position = player.position
return true
return false
func _process(_delta):
if player_has_moved():
shared_player_position = player.position
#Generate Mesh ---------------------------------------------------------
func generate__mesh(chunk_key):
var meshes = {}
var greedy_face = {}
meshes.clear()
greedy_face.clear()
if Global.chunk_blocks.has(chunk_key):
for block_pos in Global.chunk_blocks[chunk_key].keys():
var block_type = Global.chunk_blocks[chunk_key][block_pos]
if block_type != Global.BlockTypes.AIR:
for face in Global.FACE_INDICES.keys():
if should_create_face(block_pos, face):
create_block(block_pos, block_type, chunk_key, st):
func should_create_face(pos, face):
var direction = get_face_direction(face)
var neighbor_pos = pos + direction
var chunk_key = floor(neighbor_pos / (DIMENSION * GRID_SIZE))
if not Global.chunk_blocks.has(chunk_key):
return true
return not Global.chunk_blocks[chunk_key].has(neighbor_pos) or Global.chunk_blocks[chunk_key][neighbor_pos] == Global.BlockTypes.AIR
func get_face_direction(face):
match face:
Global.TOP: return Vector3(0, GRID_SIZE, 0)
Global.BOTTOM: return Vector3(0, -GRID_SIZE, 0)
Global.LEFT: return Vector3(-GRID_SIZE, 0, 0)
Global.RIGHT: return Vector3(GRID_SIZE, 0, 0)
Global.FRONT: return Vector3(0, 0, GRID_SIZE)
Global.BACK: return Vector3(0, 0, -GRID_SIZE)
func get_neighbor_positions(pos):
return [
pos + Vector3(GRID_SIZE, 0, 0),
pos + Vector3(-GRID_SIZE, 0, 0),
pos + Vector3(0, GRID_SIZE, 0),
pos + Vector3(0, -GRID_SIZE, 0),
pos + Vector3(0, 0, GRID_SIZE),
pos + Vector3(0, 0, -GRID_SIZE)
]
func create_block(pos, block_type, chunk_key, st):
if block_type == Global.AIR:
return
for face in Global.FACE_INDICES.keys():
if should_create_face(pos, face):
create_face(face, pos - (chunk_key * DIMENSION * GRID_SIZE), block_type, st)
func create_face(face, pos, block_type, st):
var face_vertices = Global.FACE_INDICES[face]
var offset = pos - (Vector3(GRID_SIZE, GRID_SIZE, GRID_SIZE) / 2.0)
var a = Global.vertices[face_vertices[0]] + offset
var b = Global.vertices[face_vertices[1]] + offset
var c = Global.vertices[face_vertices[2]] + offset
var d = Global.vertices[face_vertices[3]] + offset
var _normal1 = (c - a).cross(b - a)
var _normals1 = [_normal1, _normal1, _normal1]
var _normal2 = (d - a).cross(c - a)
var _normals2 = [_normal1, _normal1, _normal1]
st.add_triangle_fan([a, b, c], [], [], [], _normals1)
st.add_triangle_fan([a, c, d], [], [], [], _normals2)
So all this runs in the chunk loading script. Again, it might not exactly correspond to what you’re doing, since I’ve adapted my game a bit. For example, GRID_SIZE determines the size of the block. So in your case that would probably just be 1. So in the chunk script, what you’d have is something like this:
func set_chunk_position(new_position):
self.visible = false
set_global_position(new_position * (DIMENSION * GRID_SIZE))
update()
func update():
var chunk_key = floor(self.position / (DIMENSION * GRID_SIZE))
chunk_generation.add_global_task(chunk_key, self, "apply_mesh")
func apply_mesh():
child.queue_free()
var mesh = meshes[block_type]
mesh_instance = MeshInstance3D.new()
add_child(mesh_instance)
mesh_instance.set_mesh(mesh)
mesh_instance.create_trimesh_collision()
self.visible = true
func _process(_delta):
pass