#include "terrain_mesh_generator.h" #include "core/math/math_funcs.h" #include "core/math/rect2.h" #include "core/object/class_db.h" #include "core/object/object.h" #include "core/templates/local_vector.h" #include "scene/resources/packed_scene.h" #include "scene/resources/surface_tool.h" #include "terrain_editor/macros.h" #include "terrain_editor/terrain_chunk.h" #include "terrain_editor/terrain_primitive.h" #include String const TerrainMeshGenerator::sig_primitives_changed{ "primitives_changed" }; String const TerrainMeshGenerator::sig_primitive_list_changed{ "primitive_list_changed" }; void TerrainMeshGenerator::_bind_methods() { BIND_HPROPERTY(Variant::ARRAY, primitives, PROPERTY_HINT_ARRAY_TYPE, vformat("%s/%s:TerrainPrimitive", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE)); BIND_HPROPERTY(Variant::OBJECT, vertex_color_gradient, PROPERTY_HINT_RESOURCE_TYPE, "Gradient"); BIND_PROPERTY(Variant::FLOAT, color_gradient_start_height); BIND_PROPERTY(Variant::FLOAT, color_gradient_end_height); BIND_PROPERTY(Variant::INT, chunk_count); BIND_HPROPERTY(Variant::OBJECT, chunk_scene, PROPERTY_HINT_RESOURCE_TYPE, "PackedScene"); ADD_SIGNAL(MethodInfo(sig_primitives_changed)); ADD_SIGNAL(MethodInfo(sig_primitive_list_changed, PropertyInfo(Variant::ARRAY, "array", PROPERTY_HINT_ARRAY_TYPE, vformat("%s/%s:TerrainPrimitive", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE)))); ClassDB::bind_method(D_METHOD("push_task", "area", "out_mesh", "side_points"), &self_type::push_task); } void TerrainMeshGenerator::enter_tree() { this->thread.start(&TerrainMeshGenerator::background_generation_thread, this); } void TerrainMeshGenerator::process() { if (this->output_lock.try_lock()) { for (TerrainMeshTask &task : this->output_queue) { task.mesh->clear_surfaces(); task.out_surface->commit(task.mesh); task.callback.call(); } this->output_queue.clear(); this->output_lock.unlock(); } if (this->input_lock.try_lock()) { set_process(!this->input_queue.is_empty()); this->input_lock.unlock(); } } void TerrainMeshGenerator::on_configuration_changed() { this->settings_lock.lock(); // copy main thread primitives buffer data to worker thread buffer this->primitives.clear(); for (Variant var : this->primitives_buffer) { Ref primitive{ var }; if (primitive.is_valid()) { this->primitives.push_back(primitive->duplicate(true)); } } this->settings_lock.unlock(); if (is_ready()) { emit_signal(sig_primitive_list_changed, get_primitives()); emit_signal(sig_primitives_changed); } } void TerrainMeshGenerator::_notification(int what) { if (Engine::get_singleton()->is_editor_hint()) { return; } switch (what) { default: return; case NOTIFICATION_ENTER_TREE: set_process(true); enter_tree(); return; case NOTIFICATION_READY: rebuild_chunks(); break; case NOTIFICATION_PROCESS: process(); return; case NOTIFICATION_EXIT_TREE: if (this->thread.is_started()) { this->input_lock.lock(); this->end_thread = true; this->input_lock.unlock(); this->work_signal.post(); this->thread.wait_to_finish(); } return; } } void TerrainMeshGenerator::rebuild_chunks() { if (!is_ready()) { return; } for (TerrainChunk *chunk : this->chunks) { chunk->queue_free(); } this->chunks.clear(); if (!this->chunk_scene.is_valid()) { return; } for (size_t chunk_y{ 0 }; chunk_y < this->chunks_per_side; ++chunk_y) { for (size_t chunk_x{ 0 }; chunk_x < this->chunks_per_side; ++chunk_x) { TerrainChunk *chunk{ cast_to(this->chunk_scene->instantiate()) }; if (!chunk) { print_error("Attempt to rebuild chunks when no chunk scene is set"); return; } Vector3 const origin{ -Vector3{ (float)chunks_per_side, 0, (float)chunks_per_side } * chunk->get_size() / 2 }; this->chunks.push_back(chunk); Vector3 offset{ Vector3{ (float)chunk_x, 0.f, (float)chunk_y } * chunk->get_size() }; chunk->set_position(origin + offset); add_child(chunk); } } } void TerrainMeshGenerator::background_generation_thread(void *user) { TerrainMeshGenerator *self{ static_cast(user) }; if (self == nullptr) { return; } TerrainMeshTask task{}; for (;;) { // await more work signal self->work_signal.wait(); // pull task from input queue self->input_lock.lock(); if (self->end_thread) { self->input_lock.unlock(); break; } else { task = self->input_queue[0]; self->input_queue.remove_at(0); self->input_lock.unlock(); // create surface task.out_surface.instantiate(); // lock settings and generate grid self->settings_lock.lock(); if (self->primitives.is_empty()) { print_error("Terrain mesh generator thread has no primitive list"); } else { self->generate_grid(task.area, task.out_surface, task.side_points); } self->settings_lock.unlock(); // push output onto output queue self->output_lock.lock(); self->output_queue.push_back(task); self->call_deferred("set_process", true); self->output_lock.unlock(); } } } Color TerrainMeshGenerator::color_at_height(float height) const { float const mapped{ Math::remap(height, this->color_gradient_start_height, this->color_gradient_end_height, 0.f, 1.f) }; return this->vertex_color_gradient.is_valid() ? this->vertex_color_gradient->get_color_at_offset(mapped) : Color{ mapped, mapped, mapped, 1.f }; } float TerrainMeshGenerator::evaluate_point(Vector2 at) const { float height{ -std::numeric_limits::infinity() }; for (Ref primitive : this->primitives) { if (primitive.is_valid()) { primitive->evaluate(at, height); } } return height; } void face_from(Ref surface, size_t tl, size_t side_points) { LocalVector &vertices{ surface->get_vertex_array() }; float d1{ vertices[tl].vertex.distance_to(vertices[tl + side_points + 1].vertex) }; float d2{ vertices[tl + 1].vertex.distance_to(vertices[tl + side_points].vertex) }; if (d1 < d2) { surface->add_index(tl); surface->add_index(tl + side_points + 1); surface->add_index(tl + 1); surface->add_index(tl); surface->add_index(tl + side_points); surface->add_index(tl + side_points + 1); } else { surface->add_index(tl + side_points); surface->add_index(tl + side_points + 1); surface->add_index(tl + 1); surface->add_index(tl + 1); surface->add_index(tl); surface->add_index(tl + side_points); } } void TerrainMeshGenerator::generate_grid(Rect2 area, Ref mesh, size_t side_points) { Vector2 point_distance{ area.size / (float)side_points }; ++side_points; Vector2 at{ area.position }; mesh->begin(Mesh::PRIMITIVE_TRIANGLES); for (size_t xpoint{ 0 }; xpoint < side_points; ++xpoint) { for (size_t ypoint{ 0 }; ypoint < side_points; ++ypoint) { float const height{ evaluate_point(at) }; mesh->set_color(color_at_height(height)); mesh->set_uv({ at / area.size }); mesh->add_vertex({ at.x - area.position.x, height, at.y - area.position.y }); at.y += point_distance.y; } at.y = area.position.y; at.x += point_distance.x; } for (size_t xface{ 0 }; xface < side_points - 1; ++xface) { for (size_t yface{ 0 }; yface < side_points - 1; ++yface) { face_from(mesh, xface + yface * (size_t)side_points, side_points); } } mesh->generate_normals(); mesh->generate_tangents(); } void TerrainMeshGenerator::push_task(Rect2 area, Ref mesh, size_t points, Callable callback) { this->input_lock.lock(); for (TerrainMeshTask &task : this->input_queue) { if (task.mesh == mesh) { task.area = area; task.side_points = points; task.callback = callback; this->input_lock.unlock(); return; } } TerrainMeshTask task{ .mesh = mesh, .area = area, .side_points = points, .out_surface = nullptr, .callback = callback }; for (size_t i{ 0 }; i < this->input_queue.size(); ++i) { if (this->input_queue[i].side_points >= points) { this->input_queue.insert(i, task); this->work_signal.post(); this->input_lock.unlock(); return; } } this->input_queue.push_back(task); this->work_signal.post(); this->input_lock.unlock(); } void TerrainMeshGenerator::set_primitives(Array primitives) { // synchronise primitives for (Variant var : this->primitives_buffer) { Ref primitive{ var }; if (primitive.is_valid()) { primitive->disconnect_changed(this->generation_changed); } } this->primitives_buffer = primitives; for (Variant var : this->primitives_buffer) { Ref primitive{ var }; if (primitive.is_valid()) { primitive->connect_changed(this->generation_changed); } } on_configuration_changed(); } Array TerrainMeshGenerator::get_primitives() const { return this->primitives_buffer; } void TerrainMeshGenerator::set_vertex_color_gradient(Ref gradient) { this->settings_lock.lock(); if (this->vertex_color_gradient.is_valid()) { this->vertex_color_gradient->disconnect_changed(this->generation_changed); } this->vertex_color_gradient = gradient; if (gradient.is_valid()) { this->vertex_color_gradient->connect_changed(this->generation_changed); } this->settings_lock.unlock(); on_configuration_changed(); } Ref TerrainMeshGenerator::get_vertex_color_gradient() const { return this->vertex_color_gradient; } void TerrainMeshGenerator::set_color_gradient_start_height(float value) { this->settings_lock.lock(); this->color_gradient_start_height = value; this->settings_lock.unlock(); on_configuration_changed(); } float TerrainMeshGenerator::get_color_gradient_start_height() const { return this->color_gradient_start_height; } void TerrainMeshGenerator::set_color_gradient_end_height(float value) { this->settings_lock.lock(); this->color_gradient_end_height = value; this->settings_lock.unlock(); on_configuration_changed(); } float TerrainMeshGenerator::get_color_gradient_end_height() const { return this->color_gradient_end_height; } void TerrainMeshGenerator::set_chunk_count(int num) { this->settings_lock.lock(); if (num != this->chunks_per_side) { this->chunks_per_side = num; rebuild_chunks(); } this->settings_lock.unlock(); } int TerrainMeshGenerator::get_chunk_count() const { return this->chunks_per_side; } void TerrainMeshGenerator::set_chunk_scene(Ref scene) { this->settings_lock.lock(); if (!scene.is_valid() || scene->get_state()->get_node_type(0) == TerrainChunk::get_class_static()) { this->chunk_scene = scene; } this->settings_lock.unlock(); } Ref TerrainMeshGenerator::get_chunk_scene() const { return this->chunk_scene; }