4

Considering the following scenario: given an n number of Polygon2D nodes that act like "shadows" (black color with halved alpha value), how to combine all of them into an single Polygon2D node using the merge_polygons_2d() method?

An example of 3 superimposed polygons.

Using the following hard-coded code, I managed to successfully merge all three polygons into one. But I cannot figure out how to automate the process through iteration in order to avoid my approach in the case there would be more than 10.. 20.. polygons to merge for example.

func Merge_Map_Shadows() -> void:
# Get all "shadow" polygons and store them in an array,
# then apply xform transform in order to retain it's position and rotation.
# After that, delete the original shadow.
if Map_Shadows.get_child_count() > 0:
    var _detected_shadows: Array = []
    for _shadow in Map_Shadows.get_children():
        var _transformed_polygon: PoolVector2Array = []
        for _vector in _shadow.polygon: _transformed_polygon.append(_shadow.transform.xform(_vector))
        _detected_shadows.append(_transformed_polygon)
        _shadow.call_deferred("queue_free")
    
    # Create the "master shadow" node
    var _master_shadow: Polygon2D = Polygon2D.new() ; _master_shadow.color = Color.black ; _master_shadow.color.a = 0.5
    
    # Manually merge two "shadows" to the third and than apply the result to the master node
    var _merged_shadow_1: Array = []
    var _merged_shadow_2: Array = []
    _merged_shadow_1 = Geometry.merge_polygons_2d(_detected_shadows[0], _detected_shadows[2])
    _merged_shadow_2 = Geometry.merge_polygons_2d(_merged_shadow_1[0], _detected_shadows[1])
    _master_shadow.set_polygon(_merged_shadow_2.front())
    
    Map_Shadows.add_child(_master_shadow)

The result is exactly what I desired.

Before and after the polygon merging process on the node structure.

Thank you, Mike.

KanaszM
  • 151
  • 9

1 Answers1

4

Since you are removing and adding as children of the same node. I will take advantage of the list of children.

Before we begin, we need to remember a couple things:

  • Not all nodes are Polygon2D.
  • Not all Polygon2D overlap.
  • The Polygon2D nodes might have a non-identity transform applied.

The plan is to merge the polygons in place. I will make a list of the polygons that need to be deleted. Will comeback to that later.


Let us begin by iterating over the list of children. I'm using an integer index for reasons that will make sense later, so I have:

for child_index in Map_Shadows.get_child_count():
    pass

We of course need the child, so:

for child_index in Map_Shadows.get_child_count():
    var child = Map_Shadows.get_child(child_index)

But we need to make sure it is a Polygon2D

for child_index in Map_Shadows.get_child_count():
    var child = Map_Shadows.get_child(child_index)
    var found_polygon:Polygon2D = child as Polygon2D
    if found_polygon == null:
        continue

Furthermore, since we will be removing from this same list, we need to consider that it might be queued for deletion:

for child_index in Map_Shadows.get_child_count():
    var child = Map_Shadows.get_child(child_index)
    var found_polygon:Polygon2D = child as Polygon2D
    if found_polygon == null or found_polygon.is_queued_for_deletion():
        continue

Next we need to check if the child has a non-identity transform applied, and if it has, undo it:

if found_polygon.transform != Transform2D.IDENTITY:
    var transformed_polygon = found_polygon.transform.xform(found_polygon.polygon)
    found_polygon.transform = Transform2D.IDENTITY
    found_polygon.polygon = transformed_polygon

Notice that we do not need to iterate over the points of the polygon.


Now we will try to merge the polygon with all the polygons we have already seen. To do that we need another loop. Which looks just like the first one, except it goes up to the current index. And that is why I'm iterating with an index.

    for child_subindex in child_index:
        var other_child = Map_Shadows.get_child(child_subindex)
        var other_found_polygon:Polygon2D = other_child as Polygon2D
        if other_found_polygon == null or other_found_polygon.is_queued_for_deletion():
            continue

Now we try to merge:

        var merged_polygon = Geometry.merge_polygons_2d(found_polygon.polygon, other_found_polygon.polygon)

If they merged, we got an array with a single item (the merged polygon), if that is not the case, they didn't merge. Thus:

    if merged_polygon.size() != 1:
        continue

And finally, when they merged, we will remove the current polygon (well, we will put it in an array to remove it later) and set the other to the merged polygon:

    other_found_polygon.polygon = merged_polygon[0]
    polygons_to_remove.append(found_polygon)
    break

I break here because we already found a polygon to merge the current one with. No need to keep looking.


Of course, a single pass over the polygons might not do all the merges. So put the whole thing inside a while(true) loop that looks like this:

var polygons_to_remove:Array
while(true):
    polygons_to_remove = []

    # the rest of the code here

    if polygons_to_remove.size() == 0:
        break

    for polygon_to_remove in polygons_to_remove:
        polygon_to_remove.queue_free()

As I said at the start, we are keeping a list (well, an array) of the polygons that need to be deleted.

If we didn't merge any polygons, then we don't have to delete any polygons either. Which means we are done. Which is why we break out of the while(true) loop when the list of polygons to be deleted is empty.

Of course, if the list isn't empty, we actually need to delete those nodes. So call queue_free on them. No, we don't need call_deferred, in fact, we don't want call_deferred because we are checking is_queued_for_deletion, so we need them to be queued right away.


And because people like to copy and paste, this is the complete relevant code:

var polygons_to_remove:Array
while(true):
    polygons_to_remove = []
    for child_index in Map_Shadows.get_child_count():
        var child = Map_Shadows.get_child(child_index)
        var found_polygon:Polygon2D = child as Polygon2D
        if found_polygon == null or found_polygon.is_queued_for_deletion():
            continue

        if found_polygon.transform != Transform2D.IDENTITY:
            var transformed_polygon = found_polygon.transform.xform(found_polygon.polygon)
            found_polygon.transform = Transform2D.IDENTITY
            found_polygon.polygon = transformed_polygon

        for child_subindex in child_index:
            var other_child = Map_Shadows.get_child(child_subindex)
            var other_found_polygon:Polygon2D = other_child as Polygon2D
            if other_found_polygon == null or other_found_polygon.is_queued_for_deletion():
                continue

            var merged_polygon = Geometry.merge_polygons_2d(found_polygon.polygon, other_found_polygon.polygon)
            if merged_polygon.size() != 1:
                continue

            other_found_polygon.polygon = merged_polygon[0]
            polygons_to_remove.append(found_polygon)
            break

    if polygons_to_remove.size() == 0:
        break

    for polygon_to_remove in polygons_to_remove:
        polygon_to_remove.queue_free()

Yes, I tested that thing. It works. However, I'm assuming these are all well behaved polygons. There might be edge cases. In particular I did not test with a Polygon2D with less than three vertices, a Polygon2D with with invert_enable, or a Polygon2D with polygons set (see What does Polygon2D's polygons property do?).

I can also think of further optimization: we would only need to check for further merges among polygons that merged in the prior pass. If a polygon completed a pass without merging, it means it is isolated and we don't need to keep checking that one.

Theraot
  • 31,890
  • 5
  • 57
  • 86
  • Thank you so much for taking your time to answer my question. Your answer is invaluable. I have copy-pasted the code because I was so excited to see it run and it worked like a charm. But now, I will take it part-by-part and learn as much as possible from it. I was not aware of Transform2D.IDENTITY and I'm glad I know about it now. As well as node queuing practices that avoids redundancy like my initial attempt. – KanaszM May 06 '21 at 08:12
  • This was super helpful, I ended up using this solution to generate polygons for tilemap collisions. Link to the Gist: https://gist.github.com/afk-mario/15b5855ccce145516d1b458acfe29a28 – Mario Carballo Zama Feb 07 '23 at 15:12