361 lines
11 KiB
C++
361 lines
11 KiB
C++
#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 <limits>
|
|
|
|
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->input_lock.try_lock()) {
|
|
set_process(!this->input_queue.is_empty());
|
|
this->input_lock.unlock();
|
|
}
|
|
if (this->output_lock.try_lock()) {
|
|
for (TerrainMeshTask &task : this->output_queue) {
|
|
task.mesh->clear_surfaces();
|
|
task.out_surface->commit(task.mesh);
|
|
for (TerrainMeshTask &queued : this->input_queue) {
|
|
if (queued.mesh == task.mesh) {
|
|
goto exit_outer;
|
|
}
|
|
}
|
|
task.callback.call();
|
|
}
|
|
exit_outer:
|
|
this->output_queue.clear();
|
|
this->output_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<TerrainPrimitive> 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<TerrainChunk>(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<TerrainMeshGenerator *>(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<float>::infinity() };
|
|
for (Ref<TerrainPrimitive> primitive : this->primitives) {
|
|
if (primitive.is_valid()) {
|
|
primitive->evaluate(at, height);
|
|
}
|
|
}
|
|
return height;
|
|
}
|
|
|
|
void face_from(Ref<SurfaceTool> surface, size_t tl, size_t side_points) {
|
|
LocalVector<SurfaceTool::Vertex> &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<SurfaceTool> 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<ArrayMesh> 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::add_primitive(Ref<TerrainPrimitive> primitive) {
|
|
Array list = get_primitives();
|
|
list.push_front(primitive);
|
|
set_primitives(list);
|
|
}
|
|
|
|
void TerrainMeshGenerator::remove_primitive(Ref<TerrainPrimitive> primitive) {
|
|
Array list = get_primitives();
|
|
int idx{ list.find(primitive) };
|
|
if (idx >= 0 && idx < list.size()) {
|
|
list.remove_at(idx);
|
|
}
|
|
set_primitives(list);
|
|
}
|
|
|
|
void TerrainMeshGenerator::set_primitives(Array primitives) {
|
|
// synchronise primitives
|
|
for (Variant var : this->primitives_buffer) {
|
|
Ref<TerrainPrimitive> primitive{ var };
|
|
if (primitive.is_valid()) {
|
|
primitive->disconnect_changed(this->generation_changed);
|
|
}
|
|
}
|
|
this->primitives_buffer = primitives;
|
|
for (Variant var : this->primitives_buffer) {
|
|
Ref<TerrainPrimitive> 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> 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<Gradient> 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<PackedScene> 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<PackedScene> TerrainMeshGenerator::get_chunk_scene() const {
|
|
return this->chunk_scene;
|
|
}
|