chore: separated terrain modifiers into own files

This commit is contained in:
Sara Gerretsen 2026-04-19 15:24:11 +02:00
parent 275870c4e6
commit 39d84347ee
9 changed files with 513 additions and 496 deletions

View file

@ -4,6 +4,9 @@
#include "terrain/terrain.h"
#include "terrain/terrain_chunk.h"
#include "terrain/terrain_modifier.h"
#include "terrain/terrain_modifier_composite.h"
#include "terrain/terrain_modifier_distance.h"
#include "terrain/terrain_modifier_path.h"
void initialize_terrain_module(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {

View file

@ -56,422 +56,3 @@ Rect2 TerrainModifier::get_bounds() const {
Vector3 TerrainModifier::get_thread_safe_global_position() const {
return this->thread_safe_global_position;
}
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());
}
SharedMutex::LockExclusive exclusive{ this->lock };
this->distance_weight_curve_buffer = this->distance_weight_curve.is_valid() ? this->distance_weight_curve->duplicate(true) : nullptr;
}
bool TerrainModifierDistance::update_bounds() {
bool changed{ false };
Rect2 bounds{};
{
SharedMutex::LockShared shared{ this->lock };
Rect2 const before{ get_bounds() };
Vector3 position{ get_thread_safe_global_position() };
bounds.position = { position.x, position.z };
bounds.size = { 0, 0 };
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 };
}
changed = before != bounds;
}
{
SharedMutex::LockExclusive exclusive{ this->lock };
set_bounds(bounds);
}
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) {
SharedMutex::LockShared shared{ this->lock };
if (this->distance_weight_curve_buffer.is_null()) {
return before;
}
float const distance{ distance_at(world_coordinate) };
if (distance >= this->distance_weight_curve_buffer->get_max_domain()) {
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) };
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) {
{
SharedMutex::LockExclusive exclusive{ this->lock };
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;
}
curves_changed();
update_configuration_warnings();
}
Ref<Curve> TerrainModifierDistance::get_distance_weight_curve() const {
return this->distance_weight_curve;
}
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() {
{
SharedMutex::LockExclusive exclusive{ this->lock };
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;
}
if (!update_bounds()) {
push_changed(get_bounds());
}
}
bool TerrainModifierPath::update_bounds() {
bool changed{ false };
Rect2 bounds{};
{
// calculate the bounds, no need to make an exclusive lock if we can avoid it
SharedMutex::LockShared shared{ this->lock };
if (this->path == nullptr) {
return false;
}
// which of the two curves is the furthest reaching
float margin{ 0.f };
if (this->curve_left.is_valid()) {
float const domain{ this->curve_left->get_max_domain() };
margin = domain > margin ? domain : margin;
}
if (this->curve_right.is_valid()) {
float const domain{ this->curve_right->get_max_domain() };
margin = domain > margin ? domain : margin;
}
// alias some known data
Transform3D curve_transform{ this->path->get_global_transform() };
PackedVector3Array const &baked_points{ this->path->get_curve()->get_baked_points() };
// find the highest and lowest x and z values
Vector2 min{}, max{};
for (int i{ 0 }; i < baked_points.size(); i += 10) {
Vector3 point{ baked_points[i] };
point = {
curve_transform.basis.get_column(0) * point.x +
curve_transform.basis.get_column(1) * point.y +
curve_transform.basis.get_column(2) * point.z +
curve_transform.origin
};
min = min.min({ point.x, point.z });
max = max.max({ point.x, point.z });
}
// extend found min and max with margins
min -= { margin, margin };
max += Vector2{ margin, margin };
// calculate bounds and check if any change is made
bounds = { min, max - min };
changed = bounds != get_bounds();
}
if (changed) {
// ensure we have an exclusive lock before writing thread-shared data
SharedMutex::LockExclusive exclusive{ this->lock };
set_bounds(bounds);
}
return changed;
}
void TerrainModifierPath::_notification(int what) {
switch (what) {
default:
return;
case NOTIFICATION_READY:
children_changed();
set_notify_transform(true);
update_bounds();
return;
case NOTIFICATION_TRANSFORM_CHANGED:
if (is_inside_tree()) {
path_changed();
}
return;
case NOTIFICATION_CHILD_ORDER_CHANGED:
if (is_ready()) {
children_changed();
}
return;
}
}
float TerrainModifierPath::evaluate_at(Vector2 world_coordinate, float before) {
SharedMutex::LockShared shared{ this->lock };
if (this->path == nullptr) {
print_error("no path");
return before;
}
if (this->curve_left_buffer.is_null()) {
print_error("no curves");
return before;
}
if (this->path_buffer.is_null()) {
print_error("no path buffer");
return before;
}
if (this->path_buffer->get_point_count() <= 1) {
print_error("path buffer functionally empty");
return before;
}
Ref<Curve> right_curve{ this->curve_right_buffer };
if (right_curve.is_null()) {
right_curve = this->curve_left_buffer;
}
Transform3D const inv_global_transform{ this->global_path_transform.inverse() };
// convert world coordinate from 2d world to 3d path-local space
Vector3 relative_position{
world_coordinate.x - this->global_path_transform.origin.x, 0.f, world_coordinate.y - this->global_path_transform.origin.z
};
relative_position = {
inv_global_transform.basis.get_column(0) * relative_position.x +
inv_global_transform.basis.get_column(1) * relative_position.y +
inv_global_transform.basis.get_column(2) * relative_position.z
};
// find the offset of the point closest to the world coordinate ...
real_t const offset{ this->path_buffer->get_closest_offset(relative_position) };
// ... and fetch the corresponding transform
Transform3D const curve_point{ this->path_buffer->sample_baked_with_rotation(offset) };
Vector3 global_curve_position{
curve_point.origin.x * this->global_path_transform.basis.get_column(0) +
curve_point.origin.y * this->global_path_transform.basis.get_column(1) +
curve_point.origin.z * this->global_path_transform.basis.get_column(2) +
this->global_path_transform.origin
};
// extract and xz position from sampled transform
Vector2 const world_curve_point{ Vector2{ global_curve_position.x, global_curve_position.z } };
// calculate the xz distance from the curve
float const distance{ world_curve_point.distance_to(world_coordinate) };
// exit early if we know for sure this point should not be affected by the path
if (distance > this->curve_left_buffer->get_max_domain() && distance > right_curve->get_max_domain()) {
return before;
}
// extract right direction and extract xz coordinates
Vector3 right_direction{ curve_point.basis.get_column(0) };
right_direction = {
right_direction.x * this->global_path_transform.basis.get_column(0) +
right_direction.y * this->global_path_transform.basis.get_column(1) +
right_direction.z * this->global_path_transform.basis.get_column(2)
};
Vector2 const right2d{ Vector2{ right_direction.x, right_direction.z }.normalized() };
// fetch the left and right curve weights according to the distance
float const left_weight{ this->curve_left_buffer->sample(distance) };
float const right_weight{ right_curve->sample(distance) };
// calculate xz dot product, normalized to the distance
float const dot{ right2d.dot(world_coordinate - world_curve_point) / distance };
// use the dot product to calculate the ratio between the left and right weights ...
float const right_left_ratio{ (dot + 1.f) / 2.f };
// ... and use that as the t-value in a lerp between left and right weights
float const weight{ Math::lerp(left_weight, right_weight, right_left_ratio) };
// which then is the t-value of the final lerp from the unchanged height to the curve's height at this point
return Math::lerp(before, global_curve_position.y, weight);
}
void TerrainModifierPath::path_changed() {
print_line("Path changed");
{
SharedMutex::LockExclusive exclusive{ this->lock };
if (this->path) {
this->path_buffer = this->path->get_curve()->duplicate_deep();
this->path_buffer->sample_baked(0.0).hash();
this->global_path_transform = this->path->get_global_transform();
print_line("path len:", this->path_buffer->get_point_count());
}
}
update_configuration_warnings();
if (!update_bounds()) {
push_changed(get_bounds());
}
}
void TerrainModifierPath::children_changed() {
if (!is_inside_tree()) {
return;
}
{
SharedMutex::LockExclusive exclusive{ this->lock };
if (this->path) {
this->path->disconnect("curve_changed", callable_mp(this, &self_type::path_changed));
}
for (Variant var : get_children()) {
if (Path3D * path{ cast_to<Path3D>(var) }) {
print_line("path found");
this->path = path;
this->path->connect("curve_changed", callable_mp(this, &self_type::path_changed));
break;
}
}
}
path_changed();
}
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");
}
if (this->path == nullptr) {
warnings.push_back("path is unassigned");
}
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;
if (!curve.is_valid() && this->curve_right.is_valid()) {
this->curve_left = this->curve_right;
this->lock.unlock_exclusive();
set_curve_right(nullptr);
} else {
this->lock.unlock_exclusive();
curves_changed();
update_configuration_warnings();
}
}
Ref<Curve> TerrainModifierPath::get_curve_left() const {
return this->curve_left;
}
void TerrainModifierPath::set_curve_right(Ref<Curve> curve) {
{
SharedMutex::LockExclusive exclusive{ this->lock };
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;
}
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;
}

View file

@ -1,12 +1,7 @@
#pragma once
#include "core/object/object.h"
#include "core/variant/variant.h"
#include "macros.h"
#include "scene/3d/marker_3d.h"
#include "scene/3d/path_3d.h"
#include "scene/resources/curve.h"
#include "shared_mutex.h"
#include <cmath>
class Terrain;
@ -41,75 +36,3 @@ public:
emit_signal(sig_terrain_changed, terrain);
}
};
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 TerrainModifierPath : public TerrainModifier {
GDCLASS(TerrainModifierPath, TerrainModifier);
static void _bind_methods();
void curves_changed();
bool update_bounds();
protected:
void _notification(int what);
public:
float evaluate_at(Vector2 world_coordinate, float before) override;
void path_changed();
void children_changed();
PackedStringArray get_configuration_warnings() const override;
private:
SharedMutex lock{};
Path3D *path{ nullptr };
Transform3D global_path_transform{};
Ref<Curve3D> path_buffer{};
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{};
};

View file

@ -0,0 +1,56 @@
#include "terrain_modifier_composite.h"
#include "terrain/terrain.h"
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;
}

View file

@ -0,0 +1,19 @@
#pragma once
#include "terrain/terrain_modifier.h"
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{};
};

View file

@ -0,0 +1,107 @@
#include "terrain_modifier_distance.h"
#include "core/typedefs.h"
#include "macros.h"
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());
}
SharedMutex::LockExclusive exclusive{ this->lock };
this->distance_weight_curve_buffer = this->distance_weight_curve.is_valid() ? this->distance_weight_curve->duplicate(true) : nullptr;
}
bool TerrainModifierDistance::update_bounds() {
bool changed{ false };
Rect2 bounds{};
{
SharedMutex::LockShared shared{ this->lock };
Rect2 const before{ get_bounds() };
Vector3 position{ get_thread_safe_global_position() };
bounds.position = { position.x, position.z };
bounds.size = { 0, 0 };
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 };
}
changed = before != bounds;
}
{
SharedMutex::LockExclusive exclusive{ this->lock };
set_bounds(bounds);
}
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) {
SharedMutex::LockShared shared{ this->lock };
if (this->distance_weight_curve_buffer.is_null()) {
return before;
}
float const distance{ distance_at(world_coordinate) };
if (distance >= this->distance_weight_curve_buffer->get_max_domain()) {
return before;
}
float const weight_offset{ 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) };
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) {
{
SharedMutex::LockExclusive exclusive{ this->lock };
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;
}
curves_changed();
update_configuration_warnings();
}
Ref<Curve> TerrainModifierDistance::get_distance_weight_curve() const {
return this->distance_weight_curve;
}

View file

@ -0,0 +1,28 @@
#pragma once
#include "terrain/shared_mutex.h"
#include "terrain/terrain_modifier.h"
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;
};

View file

@ -0,0 +1,263 @@
#include "terrain_modifier_path.h"
#include "macros.h"
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() {
{
SharedMutex::LockExclusive exclusive{ this->lock };
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;
}
if (!update_bounds()) {
push_changed(get_bounds());
}
}
bool TerrainModifierPath::update_bounds() {
bool changed{ false };
Rect2 bounds{};
{
// calculate the bounds, no need to make an exclusive lock if we can avoid it
SharedMutex::LockShared shared{ this->lock };
if (this->path == nullptr) {
return false;
}
// which of the two curves is the furthest reaching
float margin{ 0.f };
if (this->curve_left.is_valid()) {
float const domain{ this->curve_left->get_max_domain() };
margin = domain > margin ? domain : margin;
}
if (this->curve_right.is_valid()) {
float const domain{ this->curve_right->get_max_domain() };
margin = domain > margin ? domain : margin;
}
// alias some known data
Transform3D curve_transform{ this->path->get_global_transform() };
PackedVector3Array const &baked_points{ this->path->get_curve()->get_baked_points() };
// find the highest and lowest x and z values
Vector2 min{}, max{};
for (int i{ 0 }; i < baked_points.size(); i += 10) {
Vector3 point{ baked_points[i] };
point = {
curve_transform.basis.get_column(0) * point.x +
curve_transform.basis.get_column(1) * point.y +
curve_transform.basis.get_column(2) * point.z +
curve_transform.origin
};
min = min.min({ point.x, point.z });
max = max.max({ point.x, point.z });
}
// extend found min and max with margins
min -= { margin, margin };
max += Vector2{ margin, margin };
// calculate bounds and check if any change is made
bounds = { min, max - min };
changed = bounds != get_bounds();
}
if (changed) {
// ensure we have an exclusive lock before writing thread-shared data
SharedMutex::LockExclusive exclusive{ this->lock };
set_bounds(bounds);
}
return changed;
}
void TerrainModifierPath::_notification(int what) {
switch (what) {
default:
return;
case NOTIFICATION_READY:
children_changed();
set_notify_transform(true);
update_bounds();
return;
case NOTIFICATION_TRANSFORM_CHANGED:
if (is_inside_tree()) {
path_changed();
}
return;
case NOTIFICATION_CHILD_ORDER_CHANGED:
if (is_ready()) {
children_changed();
}
return;
}
}
float TerrainModifierPath::evaluate_at(Vector2 world_coordinate, float before) {
SharedMutex::LockShared shared{ this->lock };
if (this->path == nullptr) {
print_error("no path");
return before;
}
if (this->curve_left_buffer.is_null()) {
print_error("no curves");
return before;
}
if (this->path_buffer.is_null()) {
print_error("no path buffer");
return before;
}
if (this->path_buffer->get_point_count() <= 1) {
print_error("path buffer functionally empty");
return before;
}
Ref<Curve> right_curve{ this->curve_right_buffer };
if (right_curve.is_null()) {
right_curve = this->curve_left_buffer;
}
Transform3D const inv_global_transform{ this->global_path_transform.inverse() };
// convert world coordinate from 2d world to 3d path-local space
Vector3 relative_position{
world_coordinate.x - this->global_path_transform.origin.x, 0.f, world_coordinate.y - this->global_path_transform.origin.z
};
relative_position = {
inv_global_transform.basis.get_column(0) * relative_position.x +
inv_global_transform.basis.get_column(1) * relative_position.y +
inv_global_transform.basis.get_column(2) * relative_position.z
};
// find the offset of the point closest to the world coordinate ...
real_t const offset{ this->path_buffer->get_closest_offset(relative_position) };
// ... and fetch the corresponding transform
Transform3D const curve_point{ this->path_buffer->sample_baked_with_rotation(offset) };
Vector3 global_curve_position{
curve_point.origin.x * this->global_path_transform.basis.get_column(0) +
curve_point.origin.y * this->global_path_transform.basis.get_column(1) +
curve_point.origin.z * this->global_path_transform.basis.get_column(2) +
this->global_path_transform.origin
};
// extract and xz position from sampled transform
Vector2 const world_curve_point{ Vector2{ global_curve_position.x, global_curve_position.z } };
// calculate the xz distance from the curve
float const distance{ world_curve_point.distance_to(world_coordinate) };
// exit early if we know for sure this point should not be affected by the path
if (distance > this->curve_left_buffer->get_max_domain() && distance > right_curve->get_max_domain()) {
return before;
}
// extract right direction and extract xz coordinates
Vector3 right_direction{ curve_point.basis.get_column(0) };
right_direction = {
right_direction.x * this->global_path_transform.basis.get_column(0) +
right_direction.y * this->global_path_transform.basis.get_column(1) +
right_direction.z * this->global_path_transform.basis.get_column(2)
};
Vector2 const right2d{ Vector2{ right_direction.x, right_direction.z }.normalized() };
// fetch the left and right curve weights according to the distance
float const left_weight{ this->curve_left_buffer->sample(distance) };
float const right_weight{ right_curve->sample(distance) };
// calculate xz dot product, normalized to the distance
float const dot{ right2d.dot(world_coordinate - world_curve_point) / distance };
// use the dot product to calculate the ratio between the left and right weights ...
float const right_left_ratio{ (dot + 1.f) / 2.f };
// ... and use that as the t-value in a lerp between left and right weights
float const weight{ Math::lerp(left_weight, right_weight, right_left_ratio) };
// which then is the t-value of the final lerp from the unchanged height to the curve's height at this point
return Math::lerp(before, global_curve_position.y, weight);
}
void TerrainModifierPath::path_changed() {
print_line("Path changed");
{
SharedMutex::LockExclusive exclusive{ this->lock };
if (this->path) {
this->path_buffer = this->path->get_curve()->duplicate_deep();
this->path_buffer->sample_baked(0.0).hash();
this->global_path_transform = this->path->get_global_transform();
print_line("path len:", this->path_buffer->get_point_count());
}
}
update_configuration_warnings();
if (!update_bounds()) {
push_changed(get_bounds());
}
}
void TerrainModifierPath::children_changed() {
if (!is_inside_tree()) {
return;
}
{
SharedMutex::LockExclusive exclusive{ this->lock };
if (this->path) {
this->path->disconnect("curve_changed", callable_mp(this, &self_type::path_changed));
}
for (Variant var : get_children()) {
if (Path3D * path{ cast_to<Path3D>(var) }) {
print_line("path found");
this->path = path;
this->path->connect("curve_changed", callable_mp(this, &self_type::path_changed));
break;
}
}
}
path_changed();
}
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");
}
if (this->path == nullptr) {
warnings.push_back("path is unassigned");
}
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;
if (!curve.is_valid() && this->curve_right.is_valid()) {
this->curve_left = this->curve_right;
this->lock.unlock_exclusive();
set_curve_right(nullptr);
} else {
this->lock.unlock_exclusive();
curves_changed();
update_configuration_warnings();
}
}
Ref<Curve> TerrainModifierPath::get_curve_left() const {
return this->curve_left;
}
void TerrainModifierPath::set_curve_right(Ref<Curve> curve) {
{
SharedMutex::LockExclusive exclusive{ this->lock };
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;
}
curves_changed();
update_configuration_warnings();
}
Ref<Curve> TerrainModifierPath::get_curve_right() const {
return this->curve_right;
}

View file

@ -0,0 +1,37 @@
#pragma once
#include "scene/3d/path_3d.h"
#include "terrain/shared_mutex.h"
#include "terrain/terrain_modifier.h"
class TerrainModifierPath : public TerrainModifier {
GDCLASS(TerrainModifierPath, TerrainModifier);
static void _bind_methods();
void curves_changed();
bool update_bounds();
protected:
void _notification(int what);
public:
float evaluate_at(Vector2 world_coordinate, float before) override;
void path_changed();
void children_changed();
PackedStringArray get_configuration_warnings() const override;
private:
SharedMutex lock{};
Path3D *path{ nullptr };
Transform3D global_path_transform{};
Ref<Curve3D> path_buffer{};
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;
};