terrain-module/terrain_modifier_path.cpp

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;
}