#include "terrain.h" #include "core/config/engine.h" #include "terrain/terrain_chunk.h" #include "terrain/terrain_modifier.h" void Terrain::_bind_methods() { BIND_HPROPERTY(Variant::OBJECT, mesh_material, PROPERTY_HINT_RESOURCE_TYPE, "Material"); BIND_PROPERTY(Variant::INT, side_length); BIND_PROPERTY(Variant::INT, chunk_size); BIND_PROPERTY(Variant::INT, thread_count); BIND_HPROPERTY(Variant::ARRAY, terrain_meshes, PROPERTY_HINT_ARRAY_TYPE, vformat("%s/%s:TerrainMeshChunk", Variant::OBJECT, PROPERTY_HINT_NODE_TYPE), PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY); } void Terrain::stop_threads() { this->workload_lock.lock(); this->threads_stop = true; this->workload_lock.unlock(); for (Thread &thread : this->threads) { if (thread.is_started()) { thread.wait_to_finish(); } } } void Terrain::start_threads() { this->workload_lock.lock(); print_line(vformat("Starting threads; workload: %d", this->workload.size())); this->threads_stop = false; for (Thread &thread : this->threads) { if (!thread.is_started()) { thread.start(Terrain::generate_meshes_thread, this); } } this->workload_lock.unlock(); // don't let the threads proceed until all are started } void Terrain::child_order_changed() { this->modifiers.clear(); for (Variant var : get_children()) { if (TerrainModifier * mod{ cast_to(var) }) { mod->set_terrain(this); this->modifiers.push_back(mod); } } } void Terrain::update_meshes() { size_t num{ max_mesh_assignments_per_frame }; this->dirty_meshes_lock.lock(); num = num > this->dirty_meshes.size() ? this->dirty_meshes.size() : num; this->dirty_meshes_lock.unlock(); for (size_t i{ 0 }; i < num; i++) { this->dirty_meshes_lock.lock(); TerrainChunkMesh *mesh{ this->dirty_meshes[0] }; this->dirty_meshes.remove_at(0); this->dirty_meshes_lock.unlock(); mesh->apply_new_mesh(); } } void Terrain::update_threads() { if (this->workload.is_empty()) { stop_threads(); } else { start_threads(); } } void Terrain::update_process() { // check if there is any tasks going on that would require processing each frame // any running threads? for (Thread &thread : this->threads) { if (thread.is_started()) { return; } } // dirty meshes? this->dirty_meshes_lock.lock(); bool workload_empty{ this->dirty_meshes.is_empty() }; this->dirty_meshes_lock.unlock(); if (!workload_empty) { return; } // queued mesh generation tasks? this->workload_lock.lock(); workload_empty = this->workload.is_empty(); this->workload_lock.unlock(); if (!workload_empty) { return; } // stop processing each frame print_line("Terrain processing stopped"); set_process(false); } void Terrain::synchronous_generate_terrain() { print_line("Blocking regenerate terrain"); this->workload_lock.lock(); this->threads_stop = false; // queue all meshes this->workload.clear(); this->workload.append_array(this->meshes); start_threads(); // wait for workload to empty out do { this->workload_lock.unlock(); Thread::yield(); this->workload_lock.lock(); } while (!this->workload.is_empty()); this->threads_stop = true; this->workload_lock.unlock(); stop_threads(); for (TerrainChunkMesh *mesh : this->dirty_meshes) { mesh->apply_new_mesh(); } this->dirty_meshes.clear(); } void Terrain::_notification(int what) { if (!Engine::get_singleton()->is_editor_hint()) { return; } switch (what) { default: return; case NOTIFICATION_CHILD_ORDER_CHANGED: if (is_ready()) { this->child_order_changed(); } return; case NOTIFICATION_READY: { this->meshes.clear(); this->modifiers.clear(); for (Variant var : get_children()) { if (TerrainChunkMesh * mesh{ cast_to(var) }) { this->meshes.push_back(mesh); mesh->set_terrain(this); } if (TerrainModifier * mod{ cast_to(var) }) { this->modifiers.push_back(mod); mod->set_terrain(this); } } size_t expected_size{ this->side_length / this->chunk_size }; if (this->meshes.size() != expected_size * expected_size) { construct_chunk_grid(); synchronous_generate_terrain(); } return; } case NOTIFICATION_PROCESS: update_meshes(); update_threads(); update_process(); return; case NOTIFICATION_EXIT_TREE: this->workload_lock.lock(); this->threads_stop = true; this->workload_lock.unlock(); for (Thread &thread : this->threads) { if (thread.is_started()) { thread.wait_to_finish(); } } return; } } void Terrain::generate_meshes_thread(void *terrain) { Terrain *self{ static_cast(terrain) }; print_line("thread", Thread::get_caller_id(), "start"); for (;;) { self->workload_lock.lock(); if (self->threads_stop) { self->workload_lock.unlock(); print_line(Thread::get_caller_id(), "exiting"); break; } if (self->workload.is_empty()) { self->workload_lock.unlock(); Thread::yield(); continue; } TerrainChunkMesh *mesh{ self->workload[0] }; self->workload.remove_at(0); self->workload_lock.unlock(); if (!mesh->is_inside_tree()) { print_line(Thread::get_caller_id(), "mesh is outside tree, exiting"); break; } mesh->update_mesh(); Thread::yield(); } print_line(Thread::get_caller_id(), "done"); return; } void Terrain::construct_chunk_grid() { print_line("Constructing chunk grid"); this->workload_lock.lock(); this->threads_stop = true; this->workload_lock.unlock(); for (Thread &thread : this->threads) { if (thread.is_started()) { thread.wait_to_finish(); } } this->workload_lock.lock(); for (TerrainChunkMesh *mesh : this->meshes) { remove_child(mesh); mesh->queue_free(); } this->meshes.clear(); this->workload.clear(); size_t const chunks_per_side{ this->side_length / this->chunk_size }; Vector3 const origin{ (float)this->chunk_size / 2.f, 0.f, (float)this->chunk_size / 2.f }; for (size_t y{ 0 }; y < chunks_per_side; ++y) { for (size_t x{ 0 }; x < chunks_per_side; ++x) { TerrainChunkMesh *chunk{ memnew(TerrainChunkMesh) }; chunk->set_size(this->chunk_size); chunk->set_detail(this->detail); chunk->set_terrain(this); chunk->set_material_override(this->mesh_material); chunk->set_position(origin + Vector3{ (float)this->chunk_size * (float)x, 0.f, (float)this->chunk_size * (float)y }); chunk->set_name(vformat("Chunk%dx%d", x, y)); add_child(chunk); chunk->set_owner(get_owner()); this->meshes.push_back(chunk); this->workload.push_back(chunk); } } this->threads_stop = false; this->dirty_meshes.clear(); this->workload_lock.unlock(); set_process(true); } float Terrain::height_at(Vector2 world_coordinate) { float height{ 0 }; for (TerrainModifier *mod : this->modifiers) { if (!mod->is_inside_tree()) { return height; } height = mod->evaluate_at(world_coordinate, height); } return height; } void Terrain::push_changed(Rect2 area) { for (TerrainChunkMesh *mesh : this->meshes) { this->workload_lock.lock(); if (area.intersects(mesh->get_bounds()) && !this->workload.has(mesh)) { workload.push_back(mesh); } this->workload_lock.unlock(); } set_process(true); } void Terrain::mesh_dirty(TerrainChunkMesh *mesh) { this->dirty_meshes_lock.lock(); this->dirty_meshes.push_back(mesh); callable_mp(cast_to(this), &self_type::set_process).call_deferred(true); this->dirty_meshes_lock.unlock(); } void Terrain::set_mesh_material(Ref material) { this->mesh_material = material; for (TerrainChunkMesh *mesh : this->meshes) { mesh->set_material_override(material); } } Ref Terrain::get_mesh_material() const { return this->mesh_material; } void Terrain::set_side_length(size_t length) { this->side_length = length; if (is_inside_tree()) { construct_chunk_grid(); } } size_t Terrain::get_side_length() const { return this->side_length; } void Terrain::set_chunk_size(size_t size) { this->chunk_size = size; if (is_inside_tree()) { construct_chunk_grid(); } } size_t Terrain::get_chunk_size() const { return this->chunk_size; } void Terrain::set_detail(size_t detail) { this->detail = detail; if (is_inside_tree()) { construct_chunk_grid(); } } size_t Terrain::get_detail() const { return this->detail; } void Terrain::set_thread_count(size_t num) { this->workload_lock.lock(); this->threads_stop = true; this->workload_lock.unlock(); for (Thread &thread : this->threads) { thread.wait_to_finish(); } this->threads_stop = false; this->threads.resize_initialized(num); } size_t Terrain::get_thread_count() const { return this->threads.size(); } void Terrain::set_terrain_meshes(Array array) { return; this->meshes.clear(); for (Variant var : array) { if (TerrainChunkMesh * mesh{ cast_to(var) }) { mesh->set_terrain(this); this->meshes.push_back(mesh); } } } Array Terrain::get_terrain_meshes() const { Array a{}; for (TerrainChunkMesh *mesh : this->meshes) { a.push_back(mesh); } return a; }