chore: separated terrain modifiers into own files
This commit is contained in:
parent
275870c4e6
commit
39d84347ee
9 changed files with 513 additions and 496 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{};
|
||||
};
|
||||
|
|
|
|||
56
modules/terrain/terrain_modifier_composite.cpp
Normal file
56
modules/terrain/terrain_modifier_composite.cpp
Normal 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;
|
||||
}
|
||||
19
modules/terrain/terrain_modifier_composite.h
Normal file
19
modules/terrain/terrain_modifier_composite.h
Normal 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{};
|
||||
};
|
||||
107
modules/terrain/terrain_modifier_distance.cpp
Normal file
107
modules/terrain/terrain_modifier_distance.cpp
Normal 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;
|
||||
}
|
||||
28
modules/terrain/terrain_modifier_distance.h
Normal file
28
modules/terrain/terrain_modifier_distance.h
Normal 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;
|
||||
};
|
||||
263
modules/terrain/terrain_modifier_path.cpp
Normal file
263
modules/terrain/terrain_modifier_path.cpp
Normal 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;
|
||||
}
|
||||
37
modules/terrain/terrain_modifier_path.h
Normal file
37
modules/terrain/terrain_modifier_path.h
Normal 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;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue