263 lines
8.9 KiB
C++
263 lines
8.9 KiB
C++
#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;
|
|
}
|