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