From cb344d36b8d64009c857056f156281a9a1f5d5cb Mon Sep 17 00:00:00 2001 From: Relintai Date: Wed, 28 Jun 2023 13:36:14 +0200 Subject: [PATCH] Ported from godot: make autotiles fall back to the most similar bitmask using heuristics - wareya https://github.com/godotengine/godot/commit/73ad6517e4f5344f909a58dff7347eb69fe0a539 --- modules/tile_map/doc_classes/TileSet.xml | 20 +++ modules/tile_map/tile_set.cpp | 146 +++++++++++++++++--- modules/tile_map/tile_set.h | 18 ++- modules/tile_map/tile_set_editor_plugin.cpp | 6 + 4 files changed, 167 insertions(+), 23 deletions(-) diff --git a/modules/tile_map/doc_classes/TileSet.xml b/modules/tile_map/doc_classes/TileSet.xml index 7f21c1bee..a1e5690f7 100644 --- a/modules/tile_map/doc_classes/TileSet.xml +++ b/modules/tile_map/doc_classes/TileSet.xml @@ -50,6 +50,13 @@ + + + + + Returns the [enum FallbackMode] of the autotile. + + @@ -111,11 +118,20 @@ + + + + + + Returns the [enum FallbackMode] of the autotile. + + + The subtile defined as the icon may be used as a fallback when the atlas/autotile's bitmask information is incomplete. It will also be used to represent it in the TileSet editor. @@ -488,6 +504,10 @@ + + + + diff --git a/modules/tile_map/tile_set.cpp b/modules/tile_map/tile_set.cpp index c0944992a..e4159fc3e 100644 --- a/modules/tile_map/tile_set.cpp +++ b/modules/tile_map/tile_set.cpp @@ -29,8 +29,8 @@ /*************************************************************************/ #include "tile_set.h" -#include "core/variant/array.h" #include "core/config/engine.h" +#include "core/variant/array.h" bool TileSet::_set(const StringName &p_name, const Variant &p_value) { String n = p_name; @@ -376,6 +376,18 @@ void TileSet::create_tile(int p_id) { emit_changed(); } +void TileSet::autotile_set_fallback_mode(int p_id, FallbackMode p_mode) { + ERR_FAIL_COND_MSG(!tile_map.has(p_id), vformat("The TileSet doesn't have a tile with ID '%d'.", p_id)); + tile_map[p_id].autotile_data.fallback_mode = p_mode; + _change_notify(""); + emit_changed(); +} + +TileSet::FallbackMode TileSet::autotile_get_fallback_mode(int p_id) const { + ERR_FAIL_COND_V_MSG(!tile_map.has(p_id), FALLBACK_AUTO, vformat("The TileSet doesn't have a tile with ID '%d'.", p_id)); + return tile_map[p_id].autotile_data.fallback_mode; +} + void TileSet::autotile_set_bitmask_mode(int p_id, BitmaskMode p_mode) { ERR_FAIL_COND_MSG(!tile_map.has(p_id), vformat("The TileSet doesn't have a tile with ID '%d'.", p_id)); tile_map[p_id].autotile_data.bitmask_mode = p_mode; @@ -589,23 +601,8 @@ const RBMap &TileSet::autotile_get_bitmask_map(int p_id) { } } -Vector2 TileSet::autotile_get_subtile_for_bitmask(int p_id, uint16_t p_bitmask, const Node *p_tilemap_node, const Vector2 &p_tile_location) { - ERR_FAIL_COND_V_MSG(!tile_map.has(p_id), Vector2(), vformat("The TileSet doesn't have a tile with ID '%d'.", p_id)); - //First try to forward selection to script - if (p_tilemap_node->get_class_name() == "TileMap") { - if (get_script_instance() != nullptr) { - if (get_script_instance()->has_method("_forward_subtile_selection")) { - Variant ret = get_script_instance()->call("_forward_subtile_selection", p_id, p_bitmask, p_tilemap_node, p_tile_location); - if (ret.get_type() == Variant::VECTOR2) { - return ret; - } - } - } - } - +List TileSet::_autotile_get_subtile_candidates_for_bitmask(int p_id, uint16_t p_bitmask) const { List coords; - List priorities; - uint32_t priority_sum = 0; uint32_t mask; uint16_t mask_; uint16_t mask_ignore; @@ -619,16 +616,120 @@ Vector2 TileSet::autotile_get_subtile_for_bitmask(int p_id, uint16_t p_bitmask, mask_ignore = mask >> 16; if (((mask_ & (~mask_ignore)) == (p_bitmask & (~mask_ignore))) && (((~mask_) | mask_ignore) == ((~p_bitmask) | mask_ignore))) { - uint32_t priority = autotile_get_subtile_priority(p_id, E->key()); - priority_sum += priority; - priorities.push_back(priority); coords.push_back(E->key()); } } + return coords; +} + +uint32_t _count_bitmask_bits(uint32_t bitmask) { + uint32_t ret = 0; + + for (uint32_t i = 1; i <= 256; i <<= 1) { + if (bitmask & i) { + ret++; + } + } + + return ret; +} +uint32_t _score_bitmask_difference(uint32_t bitmask, uint32_t ref_bitmask) { + // low = less difference, high = more difference + uint32_t ret = 0; + + bitmask ^= ref_bitmask; + // add one to the score for each non-matching bit + for (uint32_t i = 1; i <= 256; i <<= 1) { + if (bitmask & i) { + ret += 1; + // make axial edge mismatches cost four times as much + if (i & (TileSet::BIND_TOP | TileSet::BIND_LEFT | TileSet::BIND_RIGHT | TileSet::BIND_BOTTOM)) { + ret += 3; + } + } + } + bitmask ^= ref_bitmask; + // artificially reduce difference for all-filled and all-but-center-empty bitmasks + // (511 is the non-IGNORE bitmasks all or'd together; 0x1FF) + if (ret > 0 && (bitmask == 511 || bitmask == TileSet::BIND_CENTER)) { + ret -= 1; + } + // artificially increase difference for non-symmetric bitmasks if testing against all-filled or all-but-center-empty bitmask + // (need to cast the bit tests from int to bool before comparing) + if ((ref_bitmask == 511 || ref_bitmask == TileSet::BIND_CENTER) && + (bool(bitmask & TileSet::BIND_LEFT) != bool(bitmask & TileSet::BIND_RIGHT) || + bool(bitmask & TileSet::BIND_TOP) != bool(bitmask & TileSet::BIND_BOTTOM) || + bool(bitmask & TileSet::BIND_TOPRIGHT) != bool(bitmask & TileSet::BIND_BOTTOMLEFT) || + bool(bitmask & TileSet::BIND_TOPLEFT) != bool(bitmask & TileSet::BIND_BOTTOMRIGHT))) { + ret += 16; + } + return ret; +} + +Vector2 TileSet::autotile_get_subtile_for_bitmask(int p_id, uint16_t p_bitmask, const Node *p_tilemap_node, const Vector2 &p_tile_location) { + ERR_FAIL_COND_V_MSG(!tile_map.has(p_id), Vector2(), vformat("The TileSet doesn't have a tile with ID '%d'.", p_id)); + // First try to forward selection to script + if (p_tilemap_node->get_class_name() == "TileMap") { + if (get_script_instance() != nullptr) { + if (get_script_instance()->has_method("_forward_subtile_selection")) { + Variant ret = get_script_instance()->call("_forward_subtile_selection", p_id, p_bitmask, p_tilemap_node, p_tile_location); + if (ret.get_type() == Variant::VECTOR2) { + return ret; + } + } + } + } + + // if no forward-selected tile, look for a matching tile + List coords = _autotile_get_subtile_candidates_for_bitmask(p_id, p_bitmask); + + // if we didn't find anything, and auto fallback is enagled, try falling back to a tile with a similar bitmask instead of the default tile + if (tile_map[p_id].autotile_data.fallback_mode == FALLBACK_AUTO && coords.size() == 0) { + uint32_t best_match_cost = 100000; // main point of comparison, general difference between bitmasks + uint32_t best_match_bitcount = 0; // bit count, as a tie breaker + uint16_t best_match_bitmask = 0; + + for (RBMap::Element *E = tile_map[p_id].autotile_data.flags.front(); E; E = E->next()) { + uint32_t mask = E->get(); + if (tile_map[p_id].autotile_data.bitmask_mode == BITMASK_2X2) { + mask |= (BIND_IGNORE_TOP | BIND_IGNORE_LEFT | BIND_IGNORE_CENTER | BIND_IGNORE_RIGHT | BIND_IGNORE_BOTTOM); + } + + uint16_t mask_ignore = mask >> 16; + uint16_t mask_low = mask & 0xFFFF; + mask_low &= ~mask_ignore; + mask_low |= p_bitmask & mask_ignore; + + // always skip bitmasks with no center bit, or that have already been matched as the best + if ((mask_low & BIND_CENTER) == 0 || mask_low == best_match_bitmask) { + continue; + } + + uint32_t cost = _score_bitmask_difference(mask_low, p_bitmask); + uint32_t bitcount = _count_bitmask_bits(mask_low); // to break ties, pick the bitmask with more set bits + + // if more similar, confirm match + if (cost < best_match_cost || (cost == best_match_cost && bitcount > best_match_bitcount)) { + best_match_cost = cost; + best_match_bitcount = bitcount; + best_match_bitmask = mask_low; + } + } + coords = _autotile_get_subtile_candidates_for_bitmask(p_id, best_match_bitmask); + } + if (coords.size() == 0) { return autotile_get_icon_coordinate(p_id); } else { + List priorities; + uint32_t priority_sum = 0; + for (List::Element *E = coords.front(); E; E = E->next()) { + uint32_t priority = autotile_get_subtile_priority(p_id, E->get()); + priority_sum += priority; + priorities.push_back(priority); + } + uint32_t picked_value = Math::rand() % priority_sum; uint32_t upper_bound; uint32_t lower_bound = 0; @@ -1135,6 +1236,8 @@ void TileSet::_bind_methods() { ClassDB::bind_method(D_METHOD("autotile_get_spacing", "id"), &TileSet::autotile_get_spacing); ClassDB::bind_method(D_METHOD("autotile_set_size", "id", "size"), &TileSet::autotile_set_size); ClassDB::bind_method(D_METHOD("autotile_get_size", "id"), &TileSet::autotile_get_size); + ClassDB::bind_method(D_METHOD("autotile_set_fallback_mode", "id", "mode"), &TileSet::autotile_set_fallback_mode); + ClassDB::bind_method(D_METHOD("autotile_get_fallback_mode", "id"), &TileSet::autotile_get_fallback_mode); ClassDB::bind_method(D_METHOD("tile_set_name", "id", "name"), &TileSet::tile_set_name); ClassDB::bind_method(D_METHOD("tile_get_name", "id"), &TileSet::tile_get_name); ClassDB::bind_method(D_METHOD("tile_set_texture", "id", "texture"), &TileSet::tile_set_texture); @@ -1196,6 +1299,9 @@ void TileSet::_bind_methods() { BIND_ENUM_CONSTANT(BITMASK_3X3_MINIMAL); BIND_ENUM_CONSTANT(BITMASK_3X3); + BIND_ENUM_CONSTANT(FALLBACK_AUTO); + BIND_ENUM_CONSTANT(FALLBACK_ICON); + BIND_ENUM_CONSTANT(BIND_TOPLEFT); BIND_ENUM_CONSTANT(BIND_TOP); BIND_ENUM_CONSTANT(BIND_TOPRIGHT); diff --git a/modules/tile_map/tile_set.h b/modules/tile_map/tile_set.h index 6317d09e3..1923e6b8b 100644 --- a/modules/tile_map/tile_set.h +++ b/modules/tile_map/tile_set.h @@ -32,12 +32,12 @@ #include "../fastnoise/fastnoise_noise_params.h" #include "../fastnoise/noise.h" -#include "core/variant/array.h" #include "core/object/resource.h" +#include "core/variant/array.h" #include "scene/2d/light_occluder_2d.h" #include "scene/2d/navigation_polygon_instance.h" -#include "scene/resources/navigation_polygon.h" #include "scene/resources/convex_polygon_shape_2d.h" +#include "scene/resources/navigation_polygon.h" #include "scene/resources/shape_2d.h" #include "scene/resources/texture.h" @@ -92,6 +92,11 @@ public: ATLAS_TILE }; + enum FallbackMode { + FALLBACK_AUTO, + FALLBACK_ICON + }; + struct AutotileData { BitmaskMode bitmask_mode; Size2 size; @@ -102,13 +107,15 @@ public: RBMap> navpoly_map; RBMap priority_map; RBMap z_index_map; + FallbackMode fallback_mode; // Default size to prevent invalid value explicit AutotileData() : bitmask_mode(BITMASK_2X2), size(64, 64), spacing(0), - icon_coord(0, 0) {} + icon_coord(0, 0), + fallback_mode(FALLBACK_AUTO) {} }; private: @@ -148,6 +155,7 @@ protected: Array _tile_get_shapes(int p_id) const; Array _get_tiles_ids() const; void _decompose_convex_shape(Ref p_shape); + List _autotile_get_subtile_candidates_for_bitmask(int p_id, uint16_t p_bitmask) const; static void _bind_methods(); @@ -193,6 +201,9 @@ public: int autotile_get_z_index(int p_id, const Vector2 &p_coord); const RBMap &autotile_get_z_index_map(int p_id) const; + void autotile_set_fallback_mode(int p_id, FallbackMode p_mode); + FallbackMode autotile_get_fallback_mode(int p_id) const; + void autotile_set_bitmask(int p_id, const Vector2 &p_coord, uint32_t p_flag); uint32_t autotile_get_bitmask(int p_id, const Vector2 &p_coord); const RBMap &autotile_get_bitmask_map(int p_id); @@ -273,5 +284,6 @@ public: VARIANT_ENUM_CAST(TileSet::AutotileBindings); VARIANT_ENUM_CAST(TileSet::BitmaskMode); VARIANT_ENUM_CAST(TileSet::TileMode); +VARIANT_ENUM_CAST(TileSet::FallbackMode); #endif // TILE_SET_H diff --git a/modules/tile_map/tile_set_editor_plugin.cpp b/modules/tile_map/tile_set_editor_plugin.cpp index d38875a24..b1a7c0272 100644 --- a/modules/tile_map/tile_set_editor_plugin.cpp +++ b/modules/tile_map/tile_set_editor_plugin.cpp @@ -2454,6 +2454,7 @@ void TileSetEditor::_undo_tile_removal(int p_id) { undo_redo->add_undo_method(tileset.ptr(), "autotile_set_size", p_id, tileset->autotile_get_size(p_id)); undo_redo->add_undo_method(tileset.ptr(), "autotile_set_spacing", p_id, tileset->autotile_get_spacing(p_id)); undo_redo->add_undo_method(tileset.ptr(), "autotile_set_bitmask_mode", p_id, tileset->autotile_get_bitmask_mode(p_id)); + undo_redo->add_undo_method(tileset.ptr(), "autotile_set_fallback_mode", p_id, tileset->autotile_get_fallback_mode(p_id)); } } @@ -3487,6 +3488,8 @@ bool RTilesetEditorContext::_set(const StringName &p_name, const Variant &p_valu tileset->set(String::num(tileset_editor->get_current_tile(), 0) + "/autotile/tile_size", p_value, &v); } else if (name2 == "subtile_spacing") { tileset->set(String::num(tileset_editor->get_current_tile(), 0) + "/autotile/spacing", p_value, &v); + } else if (name2 == "autotile_fallback_mode") { + tileset->set(String::num(tileset_editor->get_current_tile(), 0) + "/autotile/fallback_mode", p_value, &v); } else { tileset->set(String::num(tileset_editor->get_current_tile(), 0) + "/" + name2, p_value, &v); } @@ -3553,6 +3556,8 @@ bool RTilesetEditorContext::_get(const StringName &p_name, Variant &r_ret) const r_ret = tileset->get(String::num(tileset_editor->get_current_tile(), 0) + "/autotile/tile_size", &v); } else if (name == "subtile_spacing") { r_ret = tileset->get(String::num(tileset_editor->get_current_tile(), 0) + "/autotile/spacing", &v); + } else if (name == "autotile_fallback_mode") { + r_ret = tileset->get(String::num(tileset_editor->get_current_tile(), 0) + "/autotile/fallback_mode", &v); } else { r_ret = tileset->get(String::num(tileset_editor->get_current_tile(), 0) + "/" + name, &v); } @@ -3612,6 +3617,7 @@ void RTilesetEditorContext::_get_property_list(List *p_list) const p_list->push_back(PropertyInfo(Variant::INT, "tile_autotile_bitmask_mode", PROPERTY_HINT_ENUM, "2x2,3x3 (minimal),3x3")); p_list->push_back(PropertyInfo(Variant::VECTOR2, "tile_subtile_size")); p_list->push_back(PropertyInfo(Variant::INT, "tile_subtile_spacing", PROPERTY_HINT_RANGE, "0, 1024, 1")); + p_list->push_back(PropertyInfo(Variant::INT, "tile_autotile_fallback_mode", PROPERTY_HINT_ENUM, "Auto,Icon")); } else if (tileset->tile_get_tile_mode(id) == TileSet::ATLAS_TILE) { p_list->push_back(PropertyInfo(Variant::VECTOR2, "tile_subtile_size")); p_list->push_back(PropertyInfo(Variant::INT, "tile_subtile_spacing", PROPERTY_HINT_RANGE, "0, 1024, 1"));