feat: extracted code from project

This commit is contained in:
Sara Gerretsen 2026-04-13 12:30:06 +02:00
commit f410d037e7
11 changed files with 1244 additions and 0 deletions

3
SCsub Normal file
View file

@ -0,0 +1,3 @@
Import('env')
env.add_source_files(env.modules_sources, "*.cpp")

14
TODO.org Normal file
View file

@ -0,0 +1,14 @@
- [x] Cache chunk meshes
- [ ] Noise modifier
- [ ] Min/Max modifier
- [ ] Only load defined chunks
- [ ] More accurate modifier bounds
- [ ] Separate terrain module into it's own repository
- [ ] Texturing step
- [ ] Use Semaphores in mesh gen multi-threading
- [ ] Separate source files for terrain modifiers
- [x] Stop threads when generation finishes
- [ ] Regenerate chunks when deleting/reordering modifiers
- [x] Stop processing terrain after generation is complete
- [x] Don't start generation threads if all meshes are cached
- [ ] Scaling modifiers

5
config.py Normal file
View file

@ -0,0 +1,5 @@
def can_build(env, platform):
return True;
def configure(env):
pass;

25
register_types.cpp Normal file
View file

@ -0,0 +1,25 @@
#include "register_types.h"
#include "core/object/class_db.h"
#include "terrain/terrain.h"
#include "terrain/terrain_chunk.h"
#include "terrain/terrain_modifier.h"
void initialize_terrain_module(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
ClassDB::register_class<Terrain>();
ClassDB::register_abstract_class<TerrainModifier>();
ClassDB::register_class<TerrainModifierDistance>();
ClassDB::register_class<TerrainModifierPath>();
ClassDB::register_class<TerrainModifierPathPoint>();
ClassDB::register_class<TerrainModifierComposite>();
ClassDB::register_class<TerrainChunkMesh>();
}
void uninitialize_terrain_module(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
}

9
register_types.h Normal file
View file

@ -0,0 +1,9 @@
#ifndef TERRAIN_REGISTER_TYPES_H
#define TERRAIN_REGISTER_TYPES_H
#include "modules/register_module_types.h"
void initialize_terrain_module(ModuleInitializationLevel p_level);
void uninitialize_terrain_module(ModuleInitializationLevel p_level);
#endif // !TERRAIN_REGISTER_TYPES_H

329
terrain.cpp Normal file
View file

@ -0,0 +1,329 @@
#include "terrain.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::child_order_changed() {
this->modifiers.clear();
for (Variant var : get_children()) {
if (TerrainModifier * mod{ cast_to<TerrainModifier>(var) }) {
mod->set_terrain(this);
this->modifiers.push_back(mod);
}
}
}
void Terrain::update_meshes() {
size_t num{ 1 };
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() {
this->workload_lock.lock();
if (this->workload.is_empty()) {
this->threads_stop = true;
this->workload_lock.unlock();
for (Thread &thread : this->threads) {
if (thread.is_started()) {
thread.wait_to_finish();
}
}
} else {
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();
}
}
void Terrain::update_process() {
for (Thread &thread : this->threads) {
if (thread.is_started()) {
return;
}
}
this->dirty_meshes_lock.lock();
bool workload_empty{ this->dirty_meshes.is_empty() };
this->dirty_meshes_lock.unlock();
if (!workload_empty) {
return;
}
this->workload_lock.lock();
workload_empty = this->workload.is_empty();
this->workload_lock.unlock();
if (!workload_empty) {
return;
}
print_line("Terrain processing stopped");
set_process(false);
}
void Terrain::synchronous_generate_terrain() {
print_line("Force-regenerating entire terrain in one go.");
this->workload_lock.lock();
this->threads_stop = false;
this->workload.clear();
this->workload.append_array(this->meshes);
for (Thread &thread : this->threads) {
if (!thread.is_started()) {
thread.start(&Terrain::generate_meshes_thread, this);
}
}
do {
this->workload_lock.unlock();
Thread::yield();
this->workload_lock.lock();
} while (!this->workload.is_empty());
this->threads_stop = true;
this->workload_lock.unlock();
for (Thread &thread : this->threads) {
thread.wait_to_finish();
}
for (TerrainChunkMesh *mesh : this->dirty_meshes) {
mesh->apply_new_mesh();
}
this->dirty_meshes.clear();
}
void Terrain::_notification(int what) {
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<TerrainChunkMesh>(var) }) {
this->meshes.push_back(mesh);
mesh->set_terrain(this);
}
if (TerrainModifier * mod{ cast_to<TerrainModifier>(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 *>(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<Node>(this), &self_type::set_process).call_deferred(true);
this->dirty_meshes_lock.unlock();
}
void Terrain::set_mesh_material(Ref<Material> material) {
this->mesh_material = material;
for (TerrainChunkMesh *mesh : this->meshes) {
mesh->set_material_override(material);
}
}
Ref<Material> 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<TerrainChunkMesh>(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;
}

61
terrain.h Normal file
View file

@ -0,0 +1,61 @@
#pragma once
#include "core/math/rect2.h"
#include "core/os/mutex.h"
#include "core/os/thread.h"
#include "core/templates/vector.h"
#include "scene/main/node.h"
#include "scene/resources/material.h"
class TerrainChunkMesh;
class TerrainModifier;
class Terrain : public Node {
GDCLASS(Terrain, Node);
static void _bind_methods();
void child_order_changed();
void update_meshes();
void update_threads();
void update_process();
void synchronous_generate_terrain();
protected:
void _notification(int what);
static void generate_meshes_thread(void *terrain);
public:
void construct_chunk_grid();
float height_at(Vector2 world_coordinate);
void push_changed(Rect2 area);
void mesh_dirty(TerrainChunkMesh *mesh);
private:
Ref<Material> mesh_material{};
Vector<TerrainChunkMesh *> workload{};
bool threads_stop{ false };
Mutex workload_lock;
Vector<TerrainChunkMesh *> dirty_meshes{};
Mutex dirty_meshes_lock{};
Vector<TerrainChunkMesh *> meshes{};
Vector<TerrainModifier *> modifiers{};
LocalVector<Thread> threads{};
size_t side_length{ 200 };
size_t chunk_size{ 50 };
size_t detail{ 1 };
public:
void set_mesh_material(Ref<Material> material);
Ref<Material> get_mesh_material() const;
void set_side_length(size_t length);
size_t get_side_length() const;
void set_chunk_size(size_t size);
size_t get_chunk_size() const;
void set_detail(size_t detail);
size_t get_detail() const;
void set_thread_count(size_t num);
size_t get_thread_count() const;
void set_terrain_meshes(Array array);
Array get_terrain_meshes() const;
};

137
terrain_chunk.cpp Normal file
View file

@ -0,0 +1,137 @@
#include "terrain_chunk.h"
#include "core/math/math_defs.h"
#include "core/math/math_funcs.h"
#include "core/object/object.h"
#include "core/variant/variant.h"
#include "scene/3d/physics/collision_shape_3d.h"
#include "scene/3d/physics/static_body_3d.h"
#include "scene/resources/3d/height_map_shape_3d.h"
#include "scene/resources/surface_tool.h"
#include "terrain/terrain.h"
void TerrainChunkMesh::_bind_methods() {
BIND_HPROPERTY(Variant::OBJECT, shape, PROPERTY_HINT_RESOURCE_TYPE, "HeightMapShape3D", PROPERTY_USAGE_READ_ONLY | PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE);
BIND_HPROPERTY(Variant::INT, size, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_READ_ONLY | PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE);
}
void TerrainChunkMesh::ready() {
this->position_buffer = get_global_position();
float const sizef{ (float)get_size() };
this->bounds.position = { this->position_buffer.x - sizef / 2.f, this->position_buffer.z - sizef / 2.f };
this->bounds.size = { sizef, sizef };
add_child(this->body = memnew(StaticBody3D));
ERR_FAIL_COND_EDMSG(this->body == nullptr, "Failed to instantiate StaticBody3D");
this->body->add_child(this->collider = memnew(CollisionShape3D));
this->body->set_owner(this);
ERR_FAIL_COND_EDMSG(this->collider == nullptr, "Failed to instantiate CollisionShape3D");
this->collider->set_owner(this);
if (this->shape.is_null()) {
this->shape = memnew(HeightMapShape3D);
this->shape->set_map_depth(heightmap_side_length());
this->shape->set_map_width(heightmap_side_length());
} else {
this->heightmap.append_array(this->shape->get_map_data());
}
ERR_FAIL_COND_EDMSG(!this->shape.is_valid(), "Failed to instantiate HeightMapShape3D");
this->collider->set_shape(this->shape);
}
void TerrainChunkMesh::generate_vertices() {
ERR_FAIL_COND_EDMSG(this->terrain == nullptr, "TerrainChunkMesh::generate_vertices: no terrain assigned");
ERR_FAIL_COND_EDMSG(this->size <= 0.f, "TerrainChunkMesh::generate_vertices: size <= 0");
ERR_FAIL_COND_EDMSG(points_per_side() <= 0, "TerrainChunkMesh::generate_vertices: points per side <= 0");
float const half_extent{ (float)this->size / 2.f };
float const point_distance{ (float)this->size / ((float)points_per_side() - 1) };
Vector3 origin{ this->position_buffer - Vector3{ half_extent, 0, half_extent } };
for (size_t x{ 0 }; x < points_per_side(); ++x) {
for (size_t y{ 0 }; y < points_per_side(); ++y) {
Vector2 const coordinate{ origin.x + point_distance * x, origin.z + point_distance * y };
this->surface->set_uv({ (float)x / (float)points_per_side(), (float)y / (float)points_per_side() });
float height{ this->terrain->height_at(coordinate) };
this->surface->add_vertex({ coordinate.x - this->position_buffer.x, height, coordinate.y - this->position_buffer.z });
}
}
this->heightmap.resize_initialized(heightmap_side_length() * heightmap_side_length());
for (size_t x{ 0 }; x < heightmap_side_length(); x++) {
for (size_t y{ 0 }; y < heightmap_side_length(); y++) {
Vector2 coordinate{ origin.x + x, origin.z + y };
this->heightmap.set(x + y * heightmap_side_length(), this->terrain->height_at(coordinate));
}
}
}
void TerrainChunkMesh::generate_faces() {
LocalVector<SurfaceTool::Vertex> &verts{ this->surface->get_vertex_array() };
ERR_FAIL_COND_EDMSG(verts.size() == 0, "TerrainChunkMesh::generate_faces: no vertices in surface, call generate_vertices first");
size_t const faces_per_side{ points_per_side() - 1 };
for (size_t x{ 0 }; x < faces_per_side; ++x) {
for (size_t y{ 0 }; y < faces_per_side; ++y) {
size_t const tl{ x + y * points_per_side() };
float tl_br{ verts[tl].vertex.distance_to(verts[tl + points_per_side() + 1].vertex) };
float tr_bl{ verts[tl + 1].vertex.distance_to(verts[tl + points_per_side()].vertex) };
if (tl_br < tr_bl) {
surface->add_index(tl);
surface->add_index(tl + points_per_side() + 1);
surface->add_index(tl + 1);
surface->add_index(tl);
surface->add_index(tl + points_per_side());
surface->add_index(tl + points_per_side() + 1);
} else {
surface->add_index(tl + points_per_side());
surface->add_index(tl + points_per_side() + 1);
surface->add_index(tl + 1);
surface->add_index(tl + 1);
surface->add_index(tl);
surface->add_index(tl + points_per_side());
}
}
}
}
void TerrainChunkMesh::_notification(int what) {
switch (what) {
default:
return;
case NOTIFICATION_ENTER_TREE:
if (!is_ready()) {
ready();
}
return;
}
}
void TerrainChunkMesh::apply_new_mesh() {
this->lock.lock();
set_mesh(this->new_mesh);
if (this->shape->get_map_depth() != heightmap_side_length() || this->shape->get_map_width() != heightmap_side_length()) {
this->shape->set_map_depth(heightmap_side_length());
this->shape->set_map_width(heightmap_side_length());
}
this->shape->set_map_data(heightmap);
this->lock.unlock();
}
void TerrainChunkMesh::update_mesh() {
ERR_FAIL_COND_EDMSG(this->size <= 0.f, "TerrainChunkMesh::generate: size <= 0");
ERR_FAIL_COND_EDMSG(points_per_side() <= 0, "TerrainChunkMesh::generate: points per side <= 0");
this->lock.lock();
this->surface = memnew(SurfaceTool);
this->surface->begin(Mesh::PRIMITIVE_TRIANGLES);
generate_vertices();
generate_faces();
this->surface->generate_normals();
this->surface->generate_tangents();
this->new_mesh = memnew(ArrayMesh);
this->surface->commit(this->new_mesh);
this->lock.unlock();
this->terrain->mesh_dirty(this);
}
size_t TerrainChunkMesh::points_per_side() const {
return this->size * this->detail;
}
int TerrainChunkMesh::heightmap_side_length() const {
return get_size() + 1;
}

50
terrain_chunk.h Normal file
View file

@ -0,0 +1,50 @@
#pragma once
#include "core/math/rect2.h"
#include "macros.h"
#include "scene/3d/mesh_instance_3d.h"
#include "scene/3d/physics/collision_shape_3d.h"
#include "scene/3d/physics/static_body_3d.h"
#include "scene/resources/3d/height_map_shape_3d.h"
#include "scene/resources/mesh.h"
#include "scene/resources/surface_tool.h"
class Terrain;
class TerrainChunkMesh : public MeshInstance3D {
GDCLASS(TerrainChunkMesh, MeshInstance3D);
static void _bind_methods();
void ready();
void generate_vertices();
void generate_faces();
protected:
void _notification(int what);
public:
void apply_new_mesh();
void update_mesh();
size_t points_per_side() const;
int heightmap_side_length() const;
private:
Mutex lock{};
Vector3 position_buffer{};
Ref<SurfaceTool> surface{};
Ref<ArrayMesh> new_mesh{};
Terrain *terrain{ nullptr };
size_t detail{ 1 };
size_t size{ 1 };
Rect2 bounds{};
Ref<HeightMapShape3D> shape{};
StaticBody3D *body{ nullptr };
CollisionShape3D *collider{ nullptr };
Vector<real_t> heightmap{};
public:
GET_SET_FNS(Rect2, bounds);
GET_SET_FNS(Terrain *, terrain);
GET_SET_FNS(Ref<HeightMapShape3D>, shape);
GET_SET_FNS(size_t, detail);
GET_SET_FNS(size_t, size);
};

475
terrain_modifier.cpp Normal file
View file

@ -0,0 +1,475 @@
#include "terrain_modifier.h"
#include "core/config/engine.h"
#include "core/math/math_funcs.h"
#include "core/variant/variant.h"
#include "macros.h"
#include "scene/main/node.h"
#include "terrain/terrain.h"
#include <algorithm>
String TerrainModifier::sig_terrain_changed{ "terrain_changed" };
void TerrainModifier::_bind_methods() {
ADD_SIGNAL(MethodInfo(sig_terrain_changed, PropertyInfo(Variant::OBJECT, "terrain", PROPERTY_HINT_NODE_TYPE, "Terrain")));
}
void TerrainModifier::_notification(int what) {
switch (what) {
default:
return;
case NOTIFICATION_ENTER_TREE:
if (Engine::get_singleton()->is_editor_hint()) {
set_notify_transform(true);
}
this->thread_safe_global_position = get_global_position();
case NOTIFICATION_TRANSFORM_CHANGED:
this->thread_safe_global_position = get_global_position();
return;
}
}
void TerrainModifier::push_changed(Rect2 area) {
if (this->terrain) {
this->terrain->push_changed(area);
}
}
float TerrainModifier::evaluate_at(Vector2 world_coordinate, float before) {
Vector3 const global_position{ get_thread_safe_global_position() };
return global_position.y;
}
void TerrainModifier::set_bounds(Rect2 bounds) {
if (this->bounds != bounds) {
push_changed(bounds);
push_changed(this->bounds);
this->bounds = bounds;
}
}
Rect2 TerrainModifier::get_bounds() const {
return this->bounds;
}
Vector3 TerrainModifier::get_thread_safe_global_position() const {
return this->thread_safe_global_position;
}
void SharedMutex::lock_shared() {
this->lock.lock();
this->shared_count++;
this->lock.unlock();
}
void SharedMutex::unlock_shared() {
this->lock.lock();
this->shared_count--;
this->lock.unlock();
}
void SharedMutex::lock_exclusive() {
while (true) {
this->lock.lock();
if (this->shared_count == 0) {
return;
}
this->lock.unlock();
}
}
void SharedMutex::unlock_exclusive() {
this->lock.unlock();
}
void TerrainModifierDistance::_bind_methods() {
BIND_HPROPERTY(Variant::OBJECT, distance_weight_curve, PROPERTY_HINT_RESOURCE_TYPE, "Curve");
}
void TerrainModifierDistance::curves_changed() {
if (!update_bounds()) {
push_changed(get_bounds());
}
this->lock.lock_exclusive();
this->distance_weight_curve_buffer = this->distance_weight_curve.is_valid() ? this->distance_weight_curve->duplicate(true) : nullptr;
this->lock.unlock_exclusive();
}
bool TerrainModifierDistance::update_bounds() {
Rect2 const before{ get_bounds() };
Rect2 bounds{};
Vector3 position{ get_thread_safe_global_position() };
bounds.position = { position.x, position.z };
bounds.size = { 0, 0 };
this->lock.lock_shared();
if (this->distance_weight_curve.is_valid()) {
float const max_radius{ this->distance_weight_curve->get_max_domain() };
float const max_diameter{ 2.f * max_radius };
bounds.size = { max_diameter, max_diameter };
bounds.position -= { max_radius, max_radius };
}
bool const changed{ before != bounds };
this->lock.unlock_shared();
this->lock.lock_exclusive();
set_bounds(bounds);
this->lock.unlock_exclusive();
return changed;
}
void TerrainModifierDistance::_notification(int what) {
switch (what) {
default:
return;
case NOTIFICATION_READY:
update_bounds();
set_notify_transform(true);
return;
case NOTIFICATION_TRANSFORM_CHANGED:
if (is_inside_tree()) {
if (!update_bounds()) {
push_changed(get_bounds());
}
}
return;
}
}
float TerrainModifierDistance::distance_at(Vector2 const &world_coordinate) {
Vector3 const global_position{ get_thread_safe_global_position() };
return world_coordinate.distance_to({ global_position.x, global_position.z });
}
float TerrainModifierDistance::evaluate_at(Vector2 world_coordinate, float before) {
this->lock.lock_shared();
if (this->distance_weight_curve_buffer.is_null()) {
this->lock.unlock_shared();
return before;
}
float const distance{ distance_at(world_coordinate) };
if (distance >= this->distance_weight_curve_buffer->get_max_domain()) {
this->lock.unlock_shared();
return before;
}
float const weight_offset{ std::clamp(distance, this->distance_weight_curve_buffer->get_min_domain(), this->distance_weight_curve_buffer->get_max_domain()) };
float const weight{ this->distance_weight_curve_buffer->sample(weight_offset) };
float out{ weight <= 0.f ? before : Math::lerp(before, get_thread_safe_global_position().y, weight) };
this->lock.unlock_shared();
return out;
}
PackedStringArray TerrainModifierDistance::get_configuration_warnings() const {
PackedStringArray warnings{ super_type::get_configuration_warnings() };
if (this->distance_weight_curve.is_null()) {
warnings.push_back("distance_weight_curve is invalid, add a valid distance_weight_curve");
}
return warnings;
}
void TerrainModifierDistance::set_distance_weight_curve(Ref<Curve> curve) {
this->lock.lock_exclusive();
if (Engine::get_singleton()->is_editor_hint()) {
if (this->distance_weight_curve.is_valid()) {
this->distance_weight_curve->disconnect_changed(callable_mp(this, &self_type::curves_changed));
}
if (curve.is_valid()) {
curve->connect_changed(callable_mp(this, &self_type::curves_changed));
}
}
this->distance_weight_curve = curve;
this->lock.unlock_exclusive();
curves_changed();
update_configuration_warnings();
}
Ref<Curve> TerrainModifierDistance::get_distance_weight_curve() const {
return this->distance_weight_curve;
}
void TerrainModifierPathPoint::_bind_methods() {}
void TerrainModifierPathPoint::_notification(int what) {
switch (what) {
default:
return;
case NOTIFICATION_ENTER_TREE:
this->path = cast_to<TerrainModifierPath>(get_parent());
return;
case NOTIFICATION_READY:
set_notify_transform(true);
if (this->path) {
this->path->path_changed();
}
return;
case NOTIFICATION_TRANSFORM_CHANGED:
if (this->path && is_inside_tree()) {
this->path->path_changed();
}
return;
case NOTIFICATION_EXIT_TREE:
this->path = nullptr;
return;
}
}
void TerrainModifierPath::_bind_methods() {
BIND_HPROPERTY(Variant::OBJECT, curve_left, PROPERTY_HINT_RESOURCE_TYPE, "Curve");
BIND_HPROPERTY(Variant::OBJECT, curve_right, PROPERTY_HINT_RESOURCE_TYPE, "Curve");
}
void TerrainModifierPath::curves_changed() {
if (!update_bounds()) {
push_changed(get_bounds());
}
this->lock.lock_exclusive();
this->curve_left_buffer = this->curve_left.is_valid() ? this->curve_left->duplicate(true) : nullptr;
this->curve_right_buffer = this->curve_right.is_valid() ? this->curve_right->duplicate(true) : nullptr;
this->lock.unlock_exclusive();
}
bool TerrainModifierPath::update_bounds() {
Vector2 min{}, max{};
this->lock.lock_shared();
if (this->points.is_empty() || this->curve_left.is_null()) {
Vector3 point{ this->get_thread_safe_global_position() };
min.x = point.x;
min.y = point.z;
max = min;
} else {
max = min = { this->points[0].x, this->points[0].y };
for (Vector3 const &point : this->points) {
max.x = max.x > point.x ? max.x : point.x;
max.y = max.y > point.z ? max.y : point.z;
min.x = min.x < point.x ? min.x : point.x;
min.y = min.y < point.z ? min.y : point.z;
}
float max_distance_left{ this->curve_left->get_max_domain() };
float max_distance{ max_distance_left };
if (this->curve_right.is_valid()) {
float max_distance_right{ this->curve_right->get_max_domain() };
max_distance = max_distance_right > max_distance ? max_distance_right : max_distance;
}
min -= { max_distance, max_distance };
max += { max_distance * 2.f, max_distance * 2.f };
}
Rect2 bounds{ min, max - min };
bool const changed{ bounds != get_bounds() };
this->lock.unlock_shared();
this->lock.lock_exclusive();
set_bounds(bounds);
this->lock.unlock_exclusive();
return changed;
}
void TerrainModifierPath::_notification(int what) {
switch (what) {
default:
return;
case NOTIFICATION_READY:
update_bounds();
set_notify_transform(true);
return;
case NOTIFICATION_TRANSFORM_CHANGED:
if (is_inside_tree()) {
if (!update_bounds()) {
push_changed(get_bounds());
}
}
return;
case NOTIFICATION_CHILD_ORDER_CHANGED:
path_changed();
return;
}
}
float TerrainModifierPath::evaluate_line(Vector3 a, bool a_end, Vector3 b, bool b_end, Vector2 world_coordinate, float &out_dot, float &out_distance, float &out_percentage) {
Vector2 a2{ a.x, a.z }, b2{ b.x, b.z };
Vector2 const relative_coordinate{ world_coordinate - a2 };
Vector2 const difference2{ b2 - a2 };
float w{ difference2.normalized().dot(relative_coordinate) / difference2.length() };
Vector3 const difference{ b - a };
Vector3 const closest_on_line{ a + difference * (w > 0 ? (w < 1 ? w : 1) : 0) };
Vector2 const right{ -difference.z, difference.x };
out_dot = right.normalized().dot(relative_coordinate);
out_distance = world_coordinate.distance_to({ closest_on_line.x, closest_on_line.z });
out_percentage = w;
return a.y + (b.y - a.y) * w;
}
float TerrainModifierPath::evaluate_at(Vector2 world_coordinate, float before) {
this->lock.lock_shared();
if (this->curve_left_buffer.is_null() || this->points.size() <= 1) {
this->lock.unlock_shared();
return before;
}
Ref<Curve> right_curve{ this->curve_right_buffer };
if (right_curve.is_null()) {
right_curve = this->curve_left_buffer;
}
float out_height{ before };
long const count{ this->closed ? this->points.size() : this->points.size() - 1 };
for (int i{ 0 }; i < count; i++) {
Vector3 const ipos{ this->points[i] };
Vector3 const next_pos{ this->points[Math::wrapi(i + 1, 0, this->points.size())] };
if (ipos == next_pos) {
continue;
}
float dot, distance, percentage;
bool const is_start{ !this->closed && i == 0 }, is_end{ !this->closed && i == count - 1 };
float height{ evaluate_line(ipos, is_start, next_pos, is_end, world_coordinate, dot, distance, percentage) };
float const left{ this->curve_left_buffer->sample(distance) };
float const right{ right_curve->sample(distance) };
float const ndot{ dot / distance };
float separation{ ndot / 2.f + 0.5f };
if (percentage > 0.f && percentage < 1.f) {
separation = Math::round(separation);
}
float const weight{ left * (1.f - separation) + right * separation };
float const blended_height{ Math::lerp(out_height, height, weight) };
out_height = blended_height;
}
this->lock.unlock_shared();
return out_height;
}
void TerrainModifierPath::path_changed() {
if (!is_inside_tree()) {
return;
}
this->lock.lock_exclusive();
this->points.clear();
this->min_height = INFINITY;
this->max_height = -INFINITY;
Vector3 last{ INFINITY, INFINITY, INFINITY };
for (Variant var : get_children()) {
if (TerrainModifierPathPoint * point{ cast_to<TerrainModifierPathPoint>(var) }) {
Vector3 position{ point->get_global_position() };
if (position != last) {
this->points.push_back(position);
if (position.y > this->max_height) {
this->max_height = position.y;
}
if (position.y < this->min_height) {
this->min_height = position.y;
}
}
}
last = var;
}
this->lock.unlock_exclusive();
if (!update_bounds()) {
push_changed(get_bounds());
}
}
PackedStringArray TerrainModifierPath::get_configuration_warnings() const {
PackedStringArray warnings{ super_type::get_configuration_warnings() };
if (this->curve_left.is_null()) {
warnings.push_back("curve_left is invalid, add a valid curve_left");
}
return warnings;
}
void TerrainModifierPath::set_curve_left(Ref<Curve> curve) {
this->lock.lock_exclusive();
if (curve.is_valid() && curve == this->curve_right) {
curve = curve->duplicate();
}
if (Engine::get_singleton()->is_editor_hint()) {
if (this->curve_left.is_valid()) {
this->curve_left->disconnect_changed(callable_mp(this, &self_type::curves_changed));
}
if (curve.is_valid()) {
curve->connect_changed(callable_mp(this, &self_type::curves_changed));
}
}
this->curve_left = curve;
this->lock.unlock_exclusive();
if (!curve.is_valid() && this->curve_right.is_valid()) {
curve = this->curve_right;
set_curve_right(nullptr);
} else {
curves_changed();
update_configuration_warnings();
}
}
Ref<Curve> TerrainModifierPath::get_curve_left() const {
return this->curve_left;
}
void TerrainModifierPath::set_curve_right(Ref<Curve> curve) {
this->lock.lock_exclusive();
if (curve.is_valid() && curve == this->curve_left) {
curve = curve->duplicate();
}
if (Engine::get_singleton()->is_editor_hint()) {
if (this->curve_right.is_valid()) {
this->curve_right->disconnect_changed(callable_mp(this, &self_type::curves_changed));
}
if (curve.is_valid()) {
curve->connect_changed(callable_mp(this, &self_type::curves_changed));
}
}
this->curve_right = curve;
this->lock.unlock_exclusive();
curves_changed();
update_configuration_warnings();
}
Ref<Curve> TerrainModifierPath::get_curve_right() const {
return this->curve_right;
}
void TerrainModifierComposite::update_sub_modifiers() {
for (TerrainModifier *mod : this->sub_modifiers) {
push_changed(mod->get_bounds());
}
this->sub_modifiers.clear();
for (Variant var : get_children()) {
if (TerrainModifier * mod{ cast_to<TerrainModifier>(var) }) {
this->sub_modifiers.push_back(mod);
mod->set_terrain(get_terrain());
push_changed(mod->get_bounds());
}
}
}
void TerrainModifierComposite::terrain_changed(Terrain *terrain) {
for (TerrainModifier *mod : this->sub_modifiers) {
mod->set_terrain(terrain);
}
}
void TerrainModifierComposite::_notification(int what) {
switch (what) {
default:
return;
case NOTIFICATION_ENTER_TREE:
set_notify_transform(true);
if (!is_ready()) {
connect(sig_terrain_changed, callable_mp(this, &self_type::terrain_changed));
}
return;
case NOTIFICATION_TRANSFORM_CHANGED:
for (TerrainModifier *mod : this->sub_modifiers) {
push_changed(mod->get_bounds());
}
return;
case NOTIFICATION_CHILD_ORDER_CHANGED:
if (!is_ready()) {
return;
}
// fall through
case NOTIFICATION_READY:
update_sub_modifiers();
return;
}
}
float TerrainModifierComposite::evaluate_at(Vector2 world_coordinate, float before) {
float result{ 0.f };
for (TerrainModifier *mod : sub_modifiers) {
result = mod->evaluate_at(world_coordinate, result);
}
return result + before;
}

136
terrain_modifier.h Normal file
View file

@ -0,0 +1,136 @@
#pragma once
#include "core/object/object.h"
#include "core/variant/variant.h"
#include "macros.h"
#include "scene/3d/marker_3d.h"
#include "scene/resources/curve.h"
#include <cmath>
class Terrain;
class TerrainModifier : public Marker3D {
GDCLASS(TerrainModifier, Marker3D);
static void _bind_methods();
protected:
void _notification(int what);
public:
void push_changed(Rect2 bounds);
virtual float evaluate_at(Vector2 world_coordinate, float before);
private:
Vector3 thread_safe_global_position{};
Terrain *terrain{ nullptr };
Rect2 bounds{ { -INFINITY, -INFINITY }, { INFINITY, INFINITY } };
protected:
void set_bounds(Rect2 bounds);
public:
static String sig_terrain_changed;
Rect2 get_bounds() const;
Vector3 get_thread_safe_global_position() const;
Terrain *get_terrain() const {
return this->terrain;
}
void set_terrain(Terrain *terrain) {
this->terrain = terrain;
emit_signal(sig_terrain_changed, terrain);
}
};
struct SharedMutex {
void lock_shared();
void unlock_shared();
void lock_exclusive();
void unlock_exclusive();
private:
Mutex lock{};
int shared_count{};
};
class TerrainModifierDistance : public TerrainModifier {
GDCLASS(TerrainModifierDistance, TerrainModifier);
static void _bind_methods();
void curves_changed();
bool update_bounds();
protected:
void _notification(int what);
float distance_at(Vector2 const &world_coordinate);
public:
float evaluate_at(Vector2 world_coordinate, float before) override;
PackedStringArray get_configuration_warnings() const override;
private:
SharedMutex lock{};
Ref<Curve> distance_weight_curve{};
Ref<Curve> distance_weight_curve_buffer{};
public:
void set_distance_weight_curve(Ref<Curve> curve);
Ref<Curve> get_distance_weight_curve() const;
};
class TerrainModifierPathPoint : public Marker3D {
GDCLASS(TerrainModifierPathPoint, Marker3D);
static void _bind_methods();
protected:
void _notification(int what);
public:
class TerrainModifierPath *path{ nullptr };
};
class TerrainModifierPath : public TerrainModifier {
GDCLASS(TerrainModifierPath, TerrainModifier);
static void _bind_methods();
void curves_changed();
bool update_bounds();
protected:
void _notification(int what);
float evaluate_line(Vector3 a, bool a_end, Vector3 b, bool b_end, Vector2 world_coordinate, float &out_dot, float &out_distance, float &out_percentage);
public:
float evaluate_at(Vector2 world_coordinate, float before) override;
void path_changed();
PackedStringArray get_configuration_warnings() const override;
private:
SharedMutex lock{};
float min_height{};
float max_height{};
bool closed{ false };
Vector<Vector3> points{};
Ref<Curve> curve_left_buffer{};
Ref<Curve> curve_left{};
Ref<Curve> curve_right_buffer{};
Ref<Curve> curve_right{};
public:
void set_curve_left(Ref<Curve> curve);
Ref<Curve> get_curve_left() const;
void set_curve_right(Ref<Curve> curve);
Ref<Curve> get_curve_right() const;
};
class TerrainModifierComposite : public TerrainModifier {
GDCLASS(TerrainModifierComposite, TerrainModifier);
static void _bind_methods() {}
void update_sub_modifiers();
void terrain_changed(Terrain *terrain);
protected:
void _notification(int what);
public:
float evaluate_at(Vector2 world_coordinate, float before) override;
private:
Vector<TerrainModifier *> sub_modifiers{};
};