diff --git a/doc/classes/BitMap.xml b/doc/classes/BitMap.xml
index 9dd7428e229e..bccb08bf57f6 100644
--- a/doc/classes/BitMap.xml
+++ b/doc/classes/BitMap.xml
@@ -69,6 +69,7 @@
+
Creates an [Array] of polygons covering a rectangular portion of the bitmap. It uses a marching squares algorithm, followed by Ramer-Douglas-Peucker (RDP) reduction of the number of vertices. Each polygon is described as a [PackedVector2Array] of its vertices.
To get polygons covering the whole bitmap, pass:
@@ -76,6 +77,7 @@
Rect2(Vector2(), get_size())
[/codeblock]
[param epsilon] is passed to RDP to control how accurately the polygons cover the bitmap: a lower [param epsilon] corresponds to more points in the polygons.
+ If [param advanced_rdp] is [code]true[/code], uses a more advanced RDP algorithm that prevents self-intersections, but is considerably slower.
diff --git a/doc/classes/ResourceImporterTextureAtlas.xml b/doc/classes/ResourceImporterTextureAtlas.xml
index f65eb5bb2679..13148adb4fd7 100644
--- a/doc/classes/ResourceImporterTextureAtlas.xml
+++ b/doc/classes/ResourceImporterTextureAtlas.xml
@@ -10,6 +10,10 @@
+
+ If [code]true[/code], a slower [url=https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm]Ramer Douglas Peuker Algorithm[/url] is used that does not self-intersect.
+ [b]Note:[/b] Only effective if [member import_mode] is [b]Mesh2D[/b].
+
Path to the atlas spritesheet. This [i]must[/i] be set to valid path to a PNG image. Otherwise, the atlas will fail to import.
diff --git a/editor/import/resource_importer_texture_atlas.cpp b/editor/import/resource_importer_texture_atlas.cpp
index 9b734bef3661..aa3df27f9497 100644
--- a/editor/import/resource_importer_texture_atlas.cpp
+++ b/editor/import/resource_importer_texture_atlas.cpp
@@ -67,6 +67,8 @@ bool ResourceImporterTextureAtlas::get_option_visibility(const String &p_path, c
return false;
} else if (p_option == "trim_alpha_border_from_region" && int(p_options["import_mode"]) != IMPORT_MODE_REGION) {
return false;
+ } else if (p_option == "advanced_rdp" && int(p_options["import_mode"]) != IMPORT_MODE_2D_MESH) {
+ return false;
}
return true;
@@ -85,6 +87,7 @@ void ResourceImporterTextureAtlas::get_import_options(const String &p_path, List
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "import_mode", PROPERTY_HINT_ENUM, "Region,Mesh2D", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), 0));
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "crop_to_region"), false));
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "trim_alpha_border_from_region"), true));
+ r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "advanced_rdp"), false));
}
String ResourceImporterTextureAtlas::get_option_group_file() const {
@@ -252,7 +255,7 @@ Error ResourceImporterTextureAtlas::import_group_file(const String &p_group_file
Ref bit_map;
bit_map.instantiate();
bit_map->create_from_image_alpha(image);
- Vector> polygons = bit_map->clip_opaque_to_polygons(Rect2(Vector2(), image->get_size()));
+ Vector> polygons = bit_map->clip_opaque_to_polygons(Rect2(Vector2(), image->get_size()), 2.0, options["advanced_rdp"]);
for (int j = 0; j < polygons.size(); j++) {
EditorAtlasPacker::Chart chart;
diff --git a/editor/scene/2d/sprite_2d_editor_plugin.cpp b/editor/scene/2d/sprite_2d_editor_plugin.cpp
index 79622ab0b170..12ff1615a307 100644
--- a/editor/scene/2d/sprite_2d_editor_plugin.cpp
+++ b/editor/scene/2d/sprite_2d_editor_plugin.cpp
@@ -192,8 +192,9 @@ void Sprite2DEditor::_update_mesh_data() {
}
float epsilon = simplification->get_value();
+ bool advanced_rdp = enable_advanced_rdp->is_pressed();
- Vector> lines = bm->clip_opaque_to_polygons(rect, epsilon);
+ Vector> lines = bm->clip_opaque_to_polygons(rect, epsilon, advanced_rdp);
uv_lines.clear();
@@ -711,6 +712,10 @@ Sprite2DEditor::Sprite2DEditor() {
grow_pixels->set_accessibility_name(TTRC("Grow (Pixels):"));
hb->add_child(grow_pixels);
hb->add_spacer();
+ hb->add_child(memnew(Label(TTRC("Enable Advanced RDP:"))));
+ enable_advanced_rdp = memnew(CheckBox);
+ hb->add_child(enable_advanced_rdp);
+ hb->add_spacer();
update_preview = memnew(Button);
update_preview->set_text(TTR("Update Preview"));
update_preview->connect(SceneStringName(pressed), callable_mp(this, &Sprite2DEditor::_update_mesh_data));
diff --git a/editor/scene/2d/sprite_2d_editor_plugin.h b/editor/scene/2d/sprite_2d_editor_plugin.h
index 708be9f25ea9..f851667d659a 100644
--- a/editor/scene/2d/sprite_2d_editor_plugin.h
+++ b/editor/scene/2d/sprite_2d_editor_plugin.h
@@ -42,6 +42,7 @@ class HBoxContainer;
class MenuButton;
class Panel;
class ViewPanner;
+class CheckBox;
class Sprite2DEditor : public Control {
GDCLASS(Sprite2DEditor, Control);
@@ -85,6 +86,7 @@ class Sprite2DEditor : public Control {
SpinBox *simplification = nullptr;
SpinBox *grow_pixels = nullptr;
SpinBox *shrink_pixels = nullptr;
+ CheckBox *enable_advanced_rdp = nullptr;
Button *update_preview = nullptr;
void _menu_option(int p_option);
diff --git a/misc/extension_api_validation/4.7-stable/GH-94602.txt b/misc/extension_api_validation/4.7-stable/GH-94602.txt
new file mode 100644
index 000000000000..5aeba98e7f1e
--- /dev/null
+++ b/misc/extension_api_validation/4.7-stable/GH-94602.txt
@@ -0,0 +1,5 @@
+GH-94602
+--------
+Validate extension JSON: Error: Field 'classes/BitMap/methods/opaque_to_polygons/arguments': size changed value in new API, from 2 to 3.
+
+Add an optional argument to use a star-region-restricted Ramer-Douglas-Peucker Algorithm. No adjustments should be necessary.
diff --git a/scene/resources/bit_map.compat.inc b/scene/resources/bit_map.compat.inc
new file mode 100644
index 000000000000..bd5db74a0270
--- /dev/null
+++ b/scene/resources/bit_map.compat.inc
@@ -0,0 +1,48 @@
+/**************************************************************************/
+/* bit_map.compat.inc */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef DISABLE_DEPRECATED
+
+#include "core/object/class_db.h"
+#include "core/variant/typed_array.h"
+
+Vector> BitMap::_clip_opaque_to_polygons_bind_compat_94602(const Rect2i &p_rect, float p_epsilon) const {
+ return clip_opaque_to_polygons(p_rect, p_epsilon, false);
+}
+
+TypedArray BitMap::_opaque_to_polygons_bind_compat_94602(const Rect2i &p_rect, float p_epsilon) const {
+ return _opaque_to_polygons_bind(p_rect, p_epsilon, false);
+}
+
+void BitMap::_bind_compatibility_methods() {
+ ClassDB::bind_compatibility_method(D_METHOD("opaque_to_polygons", "rect", "epsilon"), &BitMap::_opaque_to_polygons_bind_compat_94602, DEFVAL(2.0));
+}
+
+#endif // DISABLE_DEPRECATED
diff --git a/scene/resources/bit_map.cpp b/scene/resources/bit_map.cpp
index 05d7c9086c23..d86c4ffb727b 100644
--- a/scene/resources/bit_map.cpp
+++ b/scene/resources/bit_map.cpp
@@ -29,7 +29,9 @@
/**************************************************************************/
#include "bit_map.h"
+#include "bit_map.compat.inc"
+#include "core/math/geometry_2d.h"
#include "core/object/class_db.h"
#include "core/variant/typed_array.h"
@@ -365,19 +367,37 @@ Vector> BitMap::_march_square(const Rect2i &p_rect, const Point2
return ret;
}
+/**
+ * Check if a point(b) is between two line segment (a and c) perpendicular range. Does not include endpoints.
+ * Uses dot product to get the directions for both endpoints and if their signs are different then the point is out of range
+ */
+static bool is_in_line_range(const Vector2 &b, const Vector2 &a, const Vector2 &c) {
+ Vector2 ba = a - b;
+ Vector2 bc = c - b;
+ Vector2 ac = c - a;
+
+ float dot1 = ba.dot(ac);
+ float dot2 = bc.dot(ac);
+ return (dot1 * dot2 < 0);
+}
+
static float perpendicular_distance(const Vector2 &i, const Vector2 &start, const Vector2 &end) {
float res;
float slope;
float intercept;
- if (start.x == end.x) {
- res = Math::abs(i.x - end.x);
- } else if (start.y == end.y) {
- res = Math::abs(i.y - end.y);
+ if (is_in_line_range(i, start, end)) {
+ if (start.x == end.x) {
+ res = Math::abs(i.x - end.x);
+ } else if (start.y == end.y) {
+ res = Math::abs(i.y - end.y);
+ } else {
+ slope = (end.y - start.y) / (end.x - start.x);
+ intercept = start.y - (slope * start.x);
+ res = Math::abs(slope * i.x - i.y + intercept) / Math::sqrt(Math::pow(slope, 2.0f) + 1.0);
+ }
} else {
- slope = (end.y - start.y) / (end.x - start.x);
- intercept = start.y - (slope * start.x);
- res = Math::abs(slope * i.x - i.y + intercept) / Math::sqrt(Math::pow(slope, 2.0f) + 1.0);
+ res = MIN(i.distance_to(start), i.distance_to(end));
}
return res;
}
@@ -391,7 +411,7 @@ static Vector rdp(const Vector &v, float optimization) {
float dist = 0.0;
// Not looping first and last point.
for (size_t i = 1, size = v.size(); i < size - 1; ++i) {
- float cdist = perpendicular_distance(v[i], v[0], v[v.size() - 1]);
+ float cdist = perpendicular_distance(v[i], v[0], v[size - 1]);
if (cdist > dist) {
dist = cdist;
index = static_cast(i);
@@ -399,8 +419,8 @@ static Vector rdp(const Vector &v, float optimization) {
}
if (dist > optimization) {
Vector left, right;
- left.resize(index);
- for (int i = 0; i < index; i++) {
+ left.resize(index + 1);
+ for (int i = 0; i < index + 1; i++) {
left.write[i] = v[i];
}
right.resize(v.size() - index);
@@ -410,8 +430,8 @@ static Vector rdp(const Vector &v, float optimization) {
Vector r1 = rdp(left, optimization);
Vector r2 = rdp(right, optimization);
- int middle = r1.size();
- r1.resize(r1.size() + r2.size());
+ int middle = r1.size() - 1;
+ r1.resize(r1.size() + r2.size() - 1);
for (int i = 0; i < r2.size(); i++) {
r1.write[middle + i] = r2[i];
}
@@ -424,7 +444,699 @@ static Vector rdp(const Vector &v, float optimization) {
}
}
-static Vector reduce(const Vector &points, const Rect2i &rect, float epsilon) {
+// X-Axis dependent. Stores the pointer (address) of the point
+static List::Element *> generate_mono_chains(List &pl) {
+ if (pl.size() < 2) {
+ return List::Element *>();
+ }
+ List::Element *> mono_chain_lst;
+
+ List::Element *iter_node = pl.front();
+ mono_chain_lst.push_back(iter_node);
+
+ float dx = iter_node->next()->get()[0] - iter_node->get()[0];
+ iter_node = iter_node->next();
+ while (iter_node->next()) {
+ iter_node = iter_node->next();
+ float ndx = iter_node->get()[0] - iter_node->prev()->get()[0];
+ if (dx * ndx <= 0) { // If they are not the same direction
+ mono_chain_lst.push_back(iter_node->prev());
+ dx = ndx;
+ }
+ }
+ mono_chain_lst.push_back(pl.back()); // Get the end
+
+ return mono_chain_lst;
+}
+
+// Move each of the calipers as far as possible
+int advance(const PackedVector2Array &pl, const int pl_len, int k, const Vector2 &vec) {
+ int k_next;
+ Vector2 p, pn;
+ while (true) {
+ k_next = (k + 1) % pl_len;
+ p = pl[k];
+ pn = pl[k_next];
+ if ((pn - p).dot(vec) >= 0) {
+ k = k_next;
+ } else {
+ break;
+ }
+ }
+ return k;
+}
+
+void find_extrema(const PackedVector2Array &points,
+ int &top, int &bottom, int &left, int &right) {
+ int n = points.size();
+ top = bottom = left = right = 0;
+
+ for (int i = 1; i < n; i++) {
+ const Vector2 &p = points[i];
+
+ if (p.y > points[top].y) {
+ top = i;
+ }
+ if (p.y < points[bottom].y) {
+ bottom = i;
+ }
+ if (p.x < points[left].x) {
+ left = i;
+ }
+ if (p.x > points[right].x) {
+ right = i;
+ }
+ }
+}
+
+// Rotating Calipers Algorithm to determine the Minimum Area Enclosed Rectangle(OBB)
+static PackedVector2Array generate_obb_from_polyline(const Vector &pl) {
+ if (pl.size() <= 2) {
+ return pl;
+ }
+
+ PackedVector2Array polygon = Geometry2D::convex_hull(pl);
+ if (polygon.size() <= 3) { // There needs to be 3 distinct points. convex_hull returns the first point as the last.
+ return PackedVector2Array{ pl[0], pl[pl.size() - 1] };
+ }
+
+ polygon.remove_at(polygon.size() - 1);
+
+ int n = polygon.size();
+ float min_area = INFINITY;
+
+ PackedVector2Array best_rect;
+ best_rect.resize(4);
+
+ // Precalc edge vectors
+ PackedVector2Array edge_vectors;
+ Vector2 p1, p2;
+ for (int i = 0; i < n; ++i) {
+ p1 = polygon[i];
+ p2 = polygon[(i + 1) % n];
+ Vector2 d = p2 - p1;
+ float inv_len = 1.0 / sqrt(d.x * d.x + d.y * d.y);
+ Vector2 u = d * inv_len;
+ edge_vectors.push_back(u);
+ }
+
+ int top, bottom, left, right;
+ find_extrema(polygon, top, bottom, left, right);
+
+ for (int i = 0; i < n; ++i) {
+ Vector2 u = edge_vectors[i];
+ Vector2 v = Vector2(-u.y, u.x); // Perpendicular Vector
+
+ top = advance(polygon, n, top, v); // max projection in +v
+ bottom = advance(polygon, n, bottom, -v); // min projection in -v
+ right = advance(polygon, n, right, u); // max projection in +u
+ left = advance(polygon, n, left, -u); // min projection in -u
+
+ // Min/max projections along edge and perpendicular
+ float min_u = polygon[left].dot(u);
+ float max_u = polygon[right].dot(u);
+ float min_v = polygon[bottom].dot(v);
+ float max_v = polygon[top].dot(v);
+
+ float width = max_u - min_u;
+ float height = max_v - min_v;
+ float area = width * height;
+
+ if (area < min_area) {
+ min_area = area;
+
+ // Origin (bottom-left corner)
+ float ox = u.x * min_u + v.x * min_v;
+ float oy = u.y * min_u + v.y * min_v;
+
+ // Construct the new MER(minimum-area enclosed Rectangle)
+ best_rect = PackedVector2Array(
+ { { ox, oy },
+ { ox + u.x * width, oy + u.y * width },
+ { 0, 0 },
+ { ox + v.x * height, oy + v.y * height } });
+ best_rect.write[2] = { best_rect[1].x + v.x * height, best_rect[1].y + v.y * height };
+ }
+ }
+
+ return best_rect;
+}
+
+/**
+ * This does not include endpoints because if 2 lines intersect at their end point, it is impossible
+ * for any point that is added to remove such an intersection, causing the algorithm to try and put
+ * non-existent points between the segments, causing a crash.
+ */
+static bool non_endpoint_segment_intersection(const Vector2 &l1_p1, const Vector2 &l1_p2, const Vector2 &l2_p1, const Vector2 &l2_p2) {
+ // Returns true if they intersect, false otherwise.
+ bool intersect = Geometry2D::segment_intersects_segment(l1_p1, l1_p2, l2_p1, l2_p2, nullptr); // No Result. Only care if they collide
+
+ // Exclude the endpoints
+ if (intersect &&
+ (l1_p1.is_equal_approx(l2_p1) ||
+ l1_p1.is_equal_approx(l2_p2) ||
+ l1_p2.is_equal_approx(l2_p1) ||
+ l1_p2.is_equal_approx(l2_p2))) {
+ intersect = false;
+ }
+
+ return intersect;
+}
+
+static bool does_obb_collide_with_line(const Vector &obb, const Vector &line) {
+ DEV_ASSERT(obb.size() == 4 && line.size() >= 2);
+
+ int obb_size = obb.size();
+ Vector2 bb_p1, bb_p2;
+ for (int i = 0; i < obb_size; ++i) {
+ bb_p1 = obb[i];
+ bb_p2 = obb[(i + 1) % obb_size];
+ if (non_endpoint_segment_intersection(line[0], line[1], bb_p1, bb_p2)) {
+ return true;
+ }
+ }
+
+ // Edge case where 1 point matches a obb point, while the other point is inside of the obb
+ if (Geometry2D::is_point_in_polygon(line[0], obb) || Geometry2D::is_point_in_polygon(line[1], obb)) {
+ return true;
+ }
+
+ return false;
+}
+
+struct OBBCacheKey {
+ Vector2 a; // Chain start pnt
+ Vector2 b; // Chain end pnt
+
+ _FORCE_INLINE_ bool operator==(const OBBCacheKey &o) const {
+ return a == o.a && b == o.b;
+ }
+
+ _FORCE_INLINE_ bool is_null() const {
+ return a == Vector2() && b == Vector2();
+ }
+};
+
+struct OBBCacheKeyComparator {
+ static _FORCE_INLINE_ bool compare(const OBBCacheKey &x, const OBBCacheKey &y) {
+ return x == y;
+ }
+};
+
+struct OBBCacheKeyHasher {
+ static _FORCE_INLINE_ uint32_t hash(const OBBCacheKey &k) {
+ uint32_t h = k.a.hash();
+ h = hash_murmur3_one_32(k.b.hash(), h);
+ return h;
+ }
+};
+
+static bool does_polyline_obbs_collide(
+ const PackedVector2Array &p1,
+ const PackedVector2Array &p2,
+ HashMap &obb_cache,
+ OBBCacheKey p1_key,
+ OBBCacheKey p2_key,
+ const bool is_p1_an_obb = false,
+ const bool is_p2_an_obb = false) {
+ int p1_len = p1.size();
+ int p2_len = p2.size();
+
+ PackedVector2Array p1_obb, p2_obb;
+ if (is_p1_an_obb) {
+ p1_obb = p1;
+ if (!p1_key.is_null()) {
+ obb_cache[p1_key] = p1_obb;
+ }
+ } else if (obb_cache.has(p1_key)) {
+ p1_obb = obb_cache[p1_key];
+ } else {
+ p1_obb = generate_obb_from_polyline(p1);
+ if (p1_obb.size() <= 2) {
+ p1_len = 2;
+ } else if (!p1_key.is_null()) {
+ obb_cache[p1_key] = p1_obb;
+ }
+ }
+
+ if (is_p2_an_obb) {
+ p2_obb = p2;
+ if (!p2_key.is_null()) {
+ obb_cache[p2_key] = p2_obb;
+ }
+ } else if (obb_cache.has(p2_key)) {
+ p2_obb = obb_cache[p2_key];
+ } else {
+ p2_obb = generate_obb_from_polyline(p2);
+ if (p2_obb.size() <= 2) {
+ p2_len = 2;
+ } else if (!p2_key.is_null()) {
+ obb_cache[p2_key] = p2_obb;
+ }
+ }
+
+ // Edge Case: Line/Colinear vs Line/Colinear
+ if (p1_len == 2 && p2_len == 2) {
+ return non_endpoint_segment_intersection(p1_obb[0], p1_obb[1], p2_obb[0], p2_obb[1]);
+ }
+ // OBB vs Line
+ if (p1_len == 2) { // If p1 is a line then p2 is an obb
+ return does_obb_collide_with_line(p2_obb, p1_obb);
+ }
+ if (p2_len == 2) { // If p2 is a line then p1 is an obb
+ return does_obb_collide_with_line(p1_obb, p2_obb);
+ }
+
+ // OBB vs OBB
+ return !(Geometry2D::intersect_polygons(p2_obb, p1_obb).is_empty());
+}
+
+// Squared distance
+static float high_speed_perp_dist(const Vector2 &p, const Vector2 &e1, const Vector2 &e2) {
+ float res = -1.0;
+ if (e2.x == e1.x) {
+ res = Math::pow(Math::abs(p.x - e2.x), 2);
+
+ } else if (e2.y == e1.y) {
+ res = Math::pow(Math::abs(p.y - e2.y), 2);
+
+ } else {
+ float slope = (e2.y - e1.y) / (e2.x - e1.x);
+ float intercept = e1.y - (slope * e1.x);
+ float numerator = slope * p.x - p.y + intercept;
+ res = (numerator * numerator) / (slope * slope + 1);
+ }
+ return res;
+}
+
+// Returns the index of the point with the furthest perpendicular distance from the edge in the given range
+static int find_furthest_perp_point_from_edge(const PackedVector2Array &pl, const int start, const int end) {
+ if (start < 0 || end >= pl.size() || start >= end) {
+ return -1;
+ }
+
+ float max_dist = -1;
+ int idx = -1;
+
+ Vector2 st_pnt = pl[start];
+ Vector2 end_pnt = pl[end];
+ for (int pnt_i = start + 1; pnt_i < end; ++pnt_i) {
+ float dist = high_speed_perp_dist(pl[pnt_i], st_pnt, end_pnt);
+ if (dist > max_dist) {
+ max_dist = dist;
+ idx = pnt_i;
+ }
+ }
+
+ // Don't allow colinear additions.
+ if (Math::is_zero_approx(max_dist)) {
+ idx = -1;
+ }
+
+ return idx;
+}
+
+static Vector retrieve_list_from_id_range(List::Element *start_ptr, const List::Element *end_ptr) {
+ Vector list;
+
+ int count = 0;
+ List::Element *it = start_ptr;
+ while (it) {
+ count++;
+ if (it == end_ptr) {
+ break;
+ }
+ it = it->next();
+ }
+
+ list.resize(count);
+
+ it = start_ptr;
+ for (int i = 0; i < count; i++) {
+ list.write[i] = it->get();
+ it = it->next();
+ }
+
+ return list;
+}
+
+struct ChainBoxData {
+ Vector4 aabb;
+ List::Element *>::Element *chain_start_node;
+ List::Element *>::Element *chain_end_node;
+ PackedVector2Array obb;
+};
+
+static Vector generate_obbs_aabbs(List::Element *> &mono_chain_lst) {
+ Vector obb_aabb_data;
+
+ PackedVector2Array obb;
+ List::Element *>::Element *iter_node = mono_chain_lst.front();
+ List::Element *>::Element *ch_end_node;
+
+ if (iter_node == nullptr) {
+ return obb_aabb_data;
+ }
+
+ while (iter_node->next()) {
+ ch_end_node = iter_node->next();
+
+ obb = generate_obb_from_polyline(retrieve_list_from_id_range(iter_node->get(), ch_end_node->get()));
+
+ // Create the AABB from the OBB
+ int min_x, min_y, max_x, max_y;
+ find_extrema(obb, max_y, min_y, min_x, max_x);
+
+ obb_aabb_data.push_back({ Vector4({ obb[min_x].x, obb[min_y].y, obb[max_x].x, obb[max_y].y }),
+ iter_node,
+ ch_end_node,
+ obb });
+ iter_node = iter_node->next();
+ }
+
+ return obb_aabb_data;
+}
+
+// Returns an array of intersecting aabbs: [(aabb1, aabb9),...]
+static Vector> find_intersections_sweep(const Vector &obb_aabb_data) {
+ class SweepEvent {
+ public:
+ float x;
+ int type; // type: 0 - start | 1 - end
+ float y1, y2;
+ const ChainBoxData *event_box;
+
+ SweepEvent() {}
+
+ SweepEvent(float p_x, int p_type, float p_y1, float p_y2, const ChainBoxData *p_index) :
+ x(p_x), type(p_type), y1(p_y1), y2(p_y2), event_box(p_index) {}
+
+ bool operator<(const SweepEvent &p_ev) const { // for sort()
+ return (x < p_ev.x || (x == p_ev.x && type < p_ev.type));
+ }
+ };
+
+ // Each event: (x, type, y_min, y_max, index).
+ Vector events;
+
+ for (int i = 0; i < obb_aabb_data.size(); ++i) {
+ const Vector4 rect = obb_aabb_data[i].aabb;
+ events.push_back({ (float)rect[0], 0, (float)rect[1], (float)rect[3], &obb_aabb_data[i] });
+ events.push_back({ (float)rect[2], 1, (float)rect[1], (float)rect[3], &obb_aabb_data[i] });
+ }
+ // Sort by x; 'start' comes before 'end' if equal
+ events.sort();
+
+ struct ActiveEntry {
+ float ay1, ay2;
+ const ChainBoxData *active_box;
+ };
+
+ Vector active;
+ Vector> result;
+
+ for (SweepEvent event : events) {
+ if (event.type == 0) {
+ for (ActiveEntry &a : active) {
+ if (!(event.y2 <= a.ay1 || a.ay2 <= event.y1)) {
+ result.push_back({ a.active_box, event.event_box }); // active_box and event_box collide
+ }
+ }
+ active.push_back({ event.y1, event.y2, event.event_box });
+ } else {
+ // Remove all entries from active where active_box == event.event_box
+ for (int i = active.size() - 1; i >= 0; --i) {
+ if (active[i].active_box == event.event_box) {
+ active.remove_at(i);
+ }
+ }
+ }
+ }
+
+ return result;
+}
+
+// Find the index of an item based on it's value in a linked list
+static int list_index(List &list, const List::Element *item) {
+ int idx = 0;
+
+ List::Element *iter_node = list.front();
+ while (iter_node) {
+ if (item == iter_node) {
+ return idx;
+ }
+ iter_node = iter_node->next();
+ ++idx;
+ }
+
+ return -1;
+}
+
+// Predeclaration to handle the call in recursive_obb_collision_check
+void iterative_refinement(List::Element *>::Element *ch1_list, List::Element *ch1_end_node,
+ List::Element *>::Element *ch2_list, List::Element *ch2_end_node,
+ Vector &obb_collision, HashMap> &obb_collisions_mapping,
+ HashMap &obb_cache,
+ List &result, PackedInt64Array &mapped_result, List::Element *> &mono_chain_lst, const PackedVector2Array &pl);
+
+void recursive_obb_collision_check(List::Element *>::Element *ch1_list, List::Element *ch1_start, List::Element *ch1_end,
+ List::Element *>::Element *ch2_list, List::Element *ch2_start, List::Element *ch2_end,
+ Vector &obb_collision, HashMap> &obb_collisions_mapping,
+ HashMap &obb_cache,
+ List &result, PackedInt64Array &mapped_result, List::Element *> &mono_chain_lst, const PackedVector2Array &pl) {
+ // Step 5: Recursively finds the smallest segment pair causing a collision.
+ struct StackEntry {
+ List::Element *ch1_start, *ch1_end, *ch2_start, *ch2_end;
+ };
+ Vector stack;
+ stack.push_back({ ch1_start, ch1_end, ch2_start, ch2_end });
+
+ StackEntry current;
+ while (!stack.is_empty()) {
+ current = stack[stack.size() - 1]; // Get the end( pop() )
+ stack.remove_at(stack.size() - 1);
+
+ Vector ch1 = retrieve_list_from_id_range(current.ch1_start, current.ch1_end);
+ Vector ch2 = retrieve_list_from_id_range(current.ch2_start, current.ch2_end);
+
+ int len1 = ch1.size();
+ int len2 = ch2.size();
+
+ // Base case: both are single segments
+ if (len1 == 2 && len2 == 2) {
+ if (non_endpoint_segment_intersection(ch1[0], ch1[1], ch2[0], ch2[1])) {
+ iterative_refinement(ch1_list, current.ch1_end, ch2_list, current.ch2_end,
+ obb_collision, obb_collisions_mapping, obb_cache,
+ result, mapped_result, mono_chain_lst, pl);
+ }
+ continue;
+ }
+
+ // No collision for subchains
+ if (!does_polyline_obbs_collide(ch1, ch2, obb_cache,
+ { current.ch1_start->get(), current.ch1_end->get() }, { current.ch2_start->get(), current.ch2_end->get() })) {
+ continue;
+ }
+
+ if (len1 >= len2 && len1 > 2) {
+ // Split Chain 1
+ int mid_idx = int((len1 + 1) / 2);
+ List::Element *mid_node = current.ch1_start;
+ for (int i = 0; i < mid_idx - 1; ++i) {
+ mid_node = mid_node->next();
+ }
+
+ // Push halves onto stack. Push second then first to preserve recursion order detailed in paper(not necessary, but makes algo. tracing easier).
+ stack.push_back({ mid_node, current.ch1_end, current.ch2_start, current.ch2_end });
+ stack.push_back({ current.ch1_start, mid_node, current.ch2_start, current.ch2_end });
+
+ } else if (len2 > 2) {
+ // Split Chain 2
+ int mid_idx = int((len2 + 1) / 2);
+ List::Element *mid_node = current.ch2_start;
+ for (int i = 0; i < mid_idx - 1; ++i) {
+ mid_node = mid_node->next();
+ }
+
+ stack.push_back({ current.ch1_start, current.ch1_end, mid_node, current.ch2_end });
+ stack.push_back({ current.ch1_start, current.ch1_end, current.ch2_start, mid_node });
+ }
+ }
+}
+
+// Step 8: Iterative refinement loop
+void iterative_refinement(List::Element *>::Element *ch1_list, List::Element *ch1_end_node,
+ List::Element *>::Element *ch2_list, List::Element *ch2_end_node,
+ Vector &obb_collision, HashMap> &obb_collisions_mapping,
+ HashMap &obb_cache,
+ List &result, PackedInt64Array &mapped_result, List::Element *> &mono_chain_lst, const PackedVector2Array &pl) {
+ // Get the index of the ends of both chains in the original list
+ int ch1_idx = list_index(result, ch1_end_node);
+ int ch2_idx = list_index(result, ch2_end_node);
+ if (ch1_idx == -1 || ch2_idx == -1) {
+ return; // The point mapping has been messed up???
+ }
+
+ PackedInt64Array edges = {
+ mapped_result[ch1_idx - 1], // First chain edges
+ mapped_result[ch1_idx],
+
+ mapped_result[ch2_idx - 1], // Second chain edges
+ mapped_result[ch2_idx]
+ };
+
+ Vector2i pnt_additions = {
+ find_furthest_perp_point_from_edge(pl, edges[0], edges[1]), // Point addition for the first chain
+ find_furthest_perp_point_from_edge(pl, edges[2], edges[3]) // Point addition for the second chain
+ };
+
+ if (pnt_additions[0] == -1 && pnt_additions[1] == -1) {
+ // Error: There are no points available to fix the polygon.
+ // This means the original polygon was invalid. (Resume the fixing for the rest of the polygon)
+ return;
+ }
+
+ // Add the points
+ Vector::Element *> new_chain_end_nodes; // List that stores if the chain intersects. [first_new_chain, second_new_chain].
+ for (int pnt_i = 0; pnt_i < 2; ++pnt_i) { // 0 = first chain, 1 = second chain
+ int pnt = pnt_additions[pnt_i];
+
+ List::Element *chain_node_to_split;
+ List::Element *>::Element *ch_list;
+
+ if (pnt_i == 0) {
+ chain_node_to_split = ch1_end_node;
+ ch_list = ch1_list;
+ } else {
+ chain_node_to_split = ch2_end_node;
+ ch_list = ch2_list;
+ }
+
+ if (pnt != -1) {
+ // Bin-search to find the idx to insert the point
+ int low = mapped_result.bsearch(pnt, true);
+
+ // Insert the point
+ mapped_result.insert(low, pnt);
+
+ // Insert the point in the same location in mapped_result
+ List::Element *insert_node = result.front();
+ for (int i = 0; i < low; ++i) {
+ insert_node = insert_node->next();
+ }
+ result.insert_before(insert_node, pl[pnt]);
+
+ // Add the point as an new split in the monochain
+ mono_chain_lst.insert_before(ch_list->next(), insert_node->prev());
+
+ // Add the two new chains to the list
+ new_chain_end_nodes.push_back(chain_node_to_split->prev()); // low - 1 -> low
+ new_chain_end_nodes.push_back(chain_node_to_split); // low -> low + 1
+ } else {
+ // There are no points that can be added to fix the chain
+ new_chain_end_nodes.push_back(nullptr);
+ new_chain_end_nodes.push_back(nullptr);
+ }
+ }
+ // Check the new chains against chains whose AABBs intersected the unsplit chain's AABB
+ for (int i = 0; i < new_chain_end_nodes.size(); ++i) {
+ List::Element *new_chain = new_chain_end_nodes[i];
+
+ if (new_chain != nullptr) {
+ for (const ChainBoxData *colliding_obb : obb_collisions_mapping[obb_collision[i / 2]]) {
+ List::Element *>::Element *col_chain_start = colliding_obb->chain_start_node;
+ List::Element *>::Element *col_chain_end = colliding_obb->chain_end_node;
+
+ // Erase the obb so it is recalculated with the new chain
+ // The new chain is always three points...maybe custom calc for speed? Nah, this isn't reached often enough to matter.
+ obb_cache.erase({ new_chain->prev()->get(), new_chain->get() });
+
+ recursive_obb_collision_check(
+ ch1_list, new_chain->prev(), new_chain,
+ col_chain_start, col_chain_start->get(), col_chain_end->get(),
+ obb_collision, obb_collisions_mapping, obb_cache,
+ result, mapped_result, mono_chain_lst, pl);
+ }
+ }
+ }
+}
+
+void find_intersections(List::Element *> &mono_chain_lst, List &result, PackedInt64Array &mapped_result, Vector &pl) {
+ Vector obbs_data = generate_obbs_aabbs(mono_chain_lst);
+ Vector> aabb_collisions = find_intersections_sweep(obbs_data); // Use AABBs to avoid costly OBB generation + recursion
+
+ HashMap> obb_collisions_mapping; // Stores the unsplit chain collisions. Used in iterative_refinement to limit sub-chain checks to parent chains.
+ HashMap obb_cache; // Stores previous OBB calculations. Key: {Chain start pnt, Chain end pnt}
+
+ for (Vector &collision : aabb_collisions) {
+ const ChainBoxData *box0 = collision[0];
+ const ChainBoxData *box1 = collision[1];
+
+ List::Element *>::Element *ch1_end_node = box0->chain_end_node;
+ List::Element *>::Element *ch2_end_node = box1->chain_end_node;
+
+ List::Element *>::Element *ch1_start_node = box0->chain_start_node;
+ List::Element *>::Element *ch2_start_node = box1->chain_start_node;
+
+ PackedVector2Array obb1 = box0->obb;
+ PackedVector2Array obb2 = box1->obb;
+
+ Vector obb_collision = { box0, box1 };
+ if (!obb_collisions_mapping.has(box0)) {
+ obb_collisions_mapping[box0] = {};
+ }
+ if (!obb_collisions_mapping.has(box1)) {
+ obb_collisions_mapping[box1] = {};
+ }
+ obb_collisions_mapping[box0].push_back(box1);
+ obb_collisions_mapping[box1].push_back(box0);
+
+ // Only push valid (4 point) obbs to the hashmap
+ if (obb1.size() == 4) {
+ obb_cache[{ ch1_start_node->get()->get(), ch1_end_node->get()->get() }] = obb1;
+ }
+ if (obb2.size() == 4) {
+ obb_cache[{ ch2_start_node->get()->get(), ch2_end_node->get()->get() }] = obb2;
+ }
+ recursive_obb_collision_check(ch1_start_node, ch1_start_node->get(), ch1_end_node->get(),
+ ch2_start_node, ch2_start_node->get(), ch2_end_node->get(),
+ obb_collision, obb_collisions_mapping, obb_cache,
+ result, mapped_result, mono_chain_lst, pl);
+ }
+}
+
+static Vector monotonic_chain_rdp(Vector &pl, const float optimization) {
+ Vector orig_res = rdp(pl, optimization);
+ if (orig_res.size() <= 3) {
+ return orig_res; // 3 points can't intersect.
+ }
+
+ List result;
+ for (int i = 0; i < orig_res.size(); ++i) {
+ result.push_back(orig_res[i]);
+ }
+ List::Element *> mono_chain_lst = generate_mono_chains(result);
+
+ // Result mapped to the original points
+ PackedInt64Array mapped_result;
+ mapped_result.resize(result.size());
+ int res_idx = 0;
+ List::Element *res_idx_node = result.front();
+ for (int i = 0; i < pl.size(); ++i) {
+ if (res_idx_node && pl[i] == res_idx_node->get()) {
+ mapped_result.write[res_idx] = i;
+ ++res_idx;
+ res_idx_node = res_idx_node->next();
+ }
+ }
+
+ find_intersections(mono_chain_lst, result, mapped_result, pl);
+
+ // Turn it back into a Vector and return the fixed polygon
+ return retrieve_list_from_id_range(result.front(), result.back());
+}
+
+static Vector reduce(Vector &points, const Rect2i &rect, float epsilon, bool p_advanced_rdp) {
int size = points.size();
// If there are less than 3 points, then we have nothing.
ERR_FAIL_COND_V(size < 3, Vector());
@@ -435,14 +1147,14 @@ static Vector reduce(const Vector &points, const Rect2i &rect,
float maxEp = MIN(rect.size.width, rect.size.height);
float ep = CLAMP(epsilon, 0.0, maxEp / 2);
- Vector result = rdp(points, ep);
- Vector2 last = result[result.size() - 1];
-
- if (last.y > result[0].y && last.distance_to(result[0]) < ep * 0.5f) {
- result.write[0].y = last.y;
- result.resize(result.size() - 1);
+ Vector result;
+ if (p_advanced_rdp) {
+ result = monotonic_chain_rdp(points, ep);
+ } else {
+ result = rdp(points, ep);
}
+
return result;
}
@@ -521,7 +1233,7 @@ static void fill_bits(const BitMap *p_src, Ref &p_map, const Point2i &p_
} while (reenter || popped);
}
-Vector> BitMap::clip_opaque_to_polygons(const Rect2i &p_rect, float p_epsilon) const {
+Vector> BitMap::clip_opaque_to_polygons(const Rect2i &p_rect, float p_epsilon, bool p_star_rdp) const {
Rect2i r = Rect2i(0, 0, width, height).intersection(p_rect);
Ref fill;
@@ -535,7 +1247,7 @@ Vector> BitMap::clip_opaque_to_polygons(const Rect2i &p_rect, fl
fill_bits(this, fill, Point2i(j, i), r);
for (Vector polygon : _march_square(r, Point2i(j, i))) {
- polygon = reduce(polygon, r, p_epsilon);
+ polygon = reduce(polygon, r, p_epsilon, p_star_rdp);
if (polygon.size() < 3) {
print_verbose("Invalid polygon, skipped");
@@ -614,8 +1326,8 @@ void BitMap::shrink_mask(int p_pixels, const Rect2i &p_rect) {
grow_mask(-p_pixels, p_rect);
}
-TypedArray BitMap::_opaque_to_polygons_bind(const Rect2i &p_rect, float p_epsilon) const {
- Vector> result = clip_opaque_to_polygons(p_rect, p_epsilon);
+TypedArray BitMap::_opaque_to_polygons_bind(const Rect2i &p_rect, float p_epsilon, bool p_advanced_rdp) const {
+ Vector> result = clip_opaque_to_polygons(p_rect, p_epsilon, p_advanced_rdp);
// Convert result to bindable types.
@@ -724,7 +1436,7 @@ void BitMap::_bind_methods() {
ClassDB::bind_method(D_METHOD("grow_mask", "pixels", "rect"), &BitMap::grow_mask);
ClassDB::bind_method(D_METHOD("convert_to_image"), &BitMap::convert_to_image);
- ClassDB::bind_method(D_METHOD("opaque_to_polygons", "rect", "epsilon"), &BitMap::_opaque_to_polygons_bind, DEFVAL(2.0));
+ ClassDB::bind_method(D_METHOD("opaque_to_polygons", "rect", "epsilon", "advanced_rdp"), &BitMap::_opaque_to_polygons_bind, DEFVAL(2.0), DEFVAL(false));
ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "data", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_data", "_get_data");
}
diff --git a/scene/resources/bit_map.h b/scene/resources/bit_map.h
index d1300e40e63d..eac67acf8759 100644
--- a/scene/resources/bit_map.h
+++ b/scene/resources/bit_map.h
@@ -46,7 +46,7 @@ class BitMap : public Resource {
Vector> _march_square(const Rect2i &p_rect, const Point2i &p_start) const;
- TypedArray _opaque_to_polygons_bind(const Rect2i &p_rect, float p_epsilon) const;
+ TypedArray _opaque_to_polygons_bind(const Rect2i &p_rect, float p_epsilon, bool p_advanced_rdp) const;
protected:
void _set_data(const Dictionary &p_d);
@@ -54,6 +54,12 @@ class BitMap : public Resource {
static void _bind_methods();
+#ifndef DISABLE_DEPRECATED
+ Vector> _clip_opaque_to_polygons_bind_compat_94602(const Rect2i &p_rect, float p_epsilon = 2.0) const;
+ TypedArray _opaque_to_polygons_bind_compat_94602(const Rect2i &p_rect, float p_epsilon) const;
+ static void _bind_compatibility_methods();
+#endif
+
public:
void create(const Size2i &p_size);
void create_from_image_alpha(const Ref &p_image, float p_threshold = 0.1);
@@ -75,5 +81,5 @@ class BitMap : public Resource {
void blit(const Vector2i &p_pos, const Ref &p_bitmap);
Ref convert_to_image() const;
- Vector> clip_opaque_to_polygons(const Rect2i &p_rect, float p_epsilon = 2.0) const;
+ Vector> clip_opaque_to_polygons(const Rect2i &p_rect, float p_epsilon = 2.0, bool p_advanced_rdp = false) const;
};
diff --git a/tests/scene/test_bit_map.cpp b/tests/scene/test_bit_map.cpp
index 89ebf6d3fe17..80a61213ce36 100644
--- a/tests/scene/test_bit_map.cpp
+++ b/tests/scene/test_bit_map.cpp
@@ -460,6 +460,13 @@ TEST_CASE("[BitMap] Clip to polygon") {
CHECK_MESSAGE(polygons.size() == 1, "We should have exactly 1 polygon");
CHECK_MESSAGE(polygons[0].size() == 12, "The polygon should have exactly 12 points");
+ reset_bit_map(bit_map);
+ bit_map.set_bit_rect(Rect2i(124, 112, 8, 32), true);
+ bit_map.set_bit_rect(Rect2i(112, 124, 32, 8), true);
+ polygons = bit_map.clip_opaque_to_polygons(Rect2i(0, 0, 256, 256), 2.0, true);
+ CHECK_MESSAGE(polygons.size() == 1, "We should have exactly 1 polygon");
+ CHECK_MESSAGE(polygons[0].size() == 12, "The polygon should have exactly 12 points");
+
reset_bit_map(bit_map);
bit_map.set_bit_rect(Rect2i(124, 112, 8, 32), true);
bit_map.set_bit_rect(Rect2i(112, 124, 32, 8), true);