Cutting holes in polygons (2D)

Godot Version

4.7

Question

The topic is part of the problem but not the whole story.

I’m making a strategy game with a map, and I found some code online which takes an image and create polygons from regions of the same colour to make an interactive map (https://www.youtube.com/watch?v=n3FvvmBDvBY is the youtube video for the code if that’s helpful). However, I have some regions which are fully surrounded by another, which causes the code to create two overlapping polygons, which I don’t want to happen, but when the code has finished generating all of the polygons (allowing me to check if there are any overlaps), they have already been scattered across many various nodes, and checking for collisions would require lots of looking across sibling or parent nodes, which has already caused me a headache before, so I was wondering if there was a faster solution that would allow me to fix the polygons?

This is a broad problem description indeed.

Are you familiar with Geometry2D already? A great utility class to do geometric operations with.

You could also look into my plugin’s codebase to see if you find what you need there. Multiple hole handling operations are in its Geometry2DUtil class.

If you were to adapt the video’s code to make my my plugin’s ScalableVectorShape2D nodes in stead of Polgyon2D directly you could select them in editor and assign them as clip paths for other shapes visually without looking around the scene tree.

More details on that:

A video to see how I first intended it.

(Edit): my second answer suggests a more hands-on solution (you could call it ‘low level’ if you want)

You could try to detect whether a generated polygon is a hole by using Geometry2D.is_polygon_clockwise, but that would have to happen during the generation, I think.

Although probably the check still works when you read out a Polygon2D’s polygon property.

Maybe you could try that:

# pseudo code
for poly in get_all_the_polygon_2d_nodes():
   if Geometry2D.is_polygon_clockwise(poly.polygon):
     print(poly, " - is a hole")

Again, not a guarantee.

If you have found a hole, the surrounding Polygon2D can mimic having a hole inside by using point indices in its polygons array to have it draw multiple polygons.

But do you still have the information what is the surrounding polygon? If not, you could find that back as well using Geometry2D.is_point_in_polygon.

But you’ll have to slice it in 2 first. Code for that is here: Geometry2DUtil.slice_polygon_vertical

After slicing it in 2 you need to clip both resulting halves with the polygon that was the original hole.

This whole approach would be easiest when you simply improve the original author’s algorithm to detect holes and apply the appropriate slices from the get go.

Also. If one polygon has 2 or more holes, you will need to slice it again for each hole, or try to make once slice through as many holes as possible (maybe an unnecessary optimization).

…but you were looking for a simpler way…

I don’t know if my first answer sounds simpler. But this second one is your alternative

Ok after a bit of coding I have been able to find the holes that are needed to be cut out of the larger shape, but I am still very confused as to how I should cut the holes out.

I am fairly new to godot and looking at the “slicing in two” code from the second comment gave me a headache to look at, and I’m not sure where the relevant code is in the github, so I was wondering if you could tell me which bits of code (from either source) I’d actually need considering:

  • I already know what the holes are
  • There will be multiple holes in some polygons

Before I try and answer: a screenshot would help. One containing a bit of your scene tree with (a) polygon(s) that should be a hole selected as well as the polygon that contains the holes… as well as what they look like visually.

I have made a fork and will try it out with a small test. This could take a while:

It’s a bit hard to get a screenshot as the polygons are generated, but the below image contains the remote tree and the map which is being modified. A polygon is being created for each blob of colour, however the light blue sea blob needs holes cut out for the slightly greyed out islands, which should be the same colour as the dark blue blob for the netherlands except it’s currently overlapping with the sea polygon.

The expanded Territory_Belgium node’s contents are similar to the other Area2D nodes, except a few have extra polygons, such as the netherlands having polygons for the islands

All polygons have the same shape as described/visible, so hopefully you know what you need to know.

Let me know if you need to see anything else, and thanks for your help!

Hi @SandpitSam,

I’ve checked out the code from the original demo and I’m sorry to say that I now understand why your holes are not being detected. It looks like the function to convert bitmap to polygons that is being used does not produce information about holes, like the Geometry2D functions I was referring to:

They use it here:

Hi again, @SandpitSam!

I fixed it (although I did kinda create a lake in their Norway to test with :grin: ).

It may be a little slower, but it only has to run once at startup, right? [1]

As you can see, I did need almost a whole copy of the Geometry2DUtil class to do the correct build-up of polygons from pixels. You may want to prune the dead code, but it’s good utility stuff either way.

Let me know if you need any more help getting it up and started.

Would you also be so kind as to leave a nice review in the godot asset store for my plugin?

Also, @Venidici , if you’re still looking for a pixel to polygon converter, you may also be interested in this fork from the grand strategy map thing (just as a starting point, but it’s a good proof of concept, my offer to work it out still stands, especially now I know more about how simple it could be).

[1]: You may want to convert the Main.gd script to a @tool, or save the resulting scene to a .tscn file containing all the polygons so you never have to run it again

I was able to transfer the code you made to make my game work, thank you! I would leave a review but I don’t have an account to leave one with.

Also I was wondering how exactly to turn Main.gd into a tool, as the map I’ll actually be using is incredibly large and the new script takes much longer to load, even for a smaller file (from <1 second to 30-60 for my test image which is only slightly bigger than the one you used), so I’d appreciate if you could let me know how to do it with this specific script, as the documentation has a lot of scary warnings.

The safer bet is not making a tool, but saving the entire scene to a tscn file after the script is done.

Looking for a good tutorial on that…

Just apply the example from the official docs (read my important note below):

Every node in the scene needs to be assigned an owner before saving. The owner must be your scene root.

Can you do this, or shall I find some time to build it into the for tonight?

Here’s an example to set the owner on all the nodes in your scene.

static func _recursive_set_owner(node : Node, new_owner : Node):
	if node != new_owner:
		node.set_owner(new_owner)
	for child in node.get_children():
		_recursive_set_owner(child, new_owner)

Just call it with the root of your generated scene:

_recursive_set_owner(root_of_scene, root_of_scene)

Then follow the example from the docs:

var scene = PackedScene.new()

var result = scene.pack(root_of_scene)
if result == OK:
	var error = ResourceSaver.save(scene, "res://path/name.tscn")  # Or "user://..."
	if error != OK:
		push_error("An error occurred while saving the scene to disk.")

I’d appreciate it if you coded the saving feature, as I think this is still all a bit too far out of reach for me to do on my own at the moment.

Ok. I may need a reminder. At the very earliest it will be 22:00 CEST

While you wait, you could create a nice account on the asset store and leave a kind review :rofl::wink: (just kidding)

Turns out that when I use a 10524x7440 pixel image it makes godot wait for 3.5 hours and counting, any idea as to how long it might take to load?

Hm, maybe you could splice up your image..

Don’t you simply have an SVG based Vectorized map? That would work so much better.

Anyway, I just set up a scene saver for you in the fork:

demo:

Not willing to find out. Seriously, if you want an HD map, get or design a free SVG map and load it with my plugin.

A demo:

File I used (result from a search in startpage.com with CC0 license filter on):
https://eu2-browse.startpage.com/av/anon-image?piurl=https%3A%2F%2Fsimplemaps.com%2Fstatic%2Fdemos%2Fresources%2Fsvg-library%2Fsvgs%2Feurope.svg&sp=1786304075Td71f7be1d0c346ceebad1d4496cf34d68acaea4e60c3e479dcbc6ff44c09fb09

You can just turn off my plugin and uninstall after you’re done importing the polygons.

Here’s an old explainer I recorded about it. It’s improved significantly since then:

All for you

@SandpitSam, because I cannot help myself I made a fully interactive map for you, based on scalable vector graphics.

You can download it here from my codeberg page:

(just like github, you can get a zip here)

More explaining

This video illustrates how you can use it and how it was made:

Making a new map

You’ll be wanting to make your own map, in order to make the script work out the box you need to keep one thing in mind:

The SVG file has to have one <g> (group node) per country and the groups should not be nested. Also the group node’s label is used to apply the name.

This you can see best do in the inkscape editor. The end of the youtube video I shared in this reply shows you how.

You can also delete the entire addon after doing the import. Because the scene contains all the polygons as well, you can even delete the svg image file from the assets you ship in your game.

Thank you so much! I will have to learn how to use inkscape so it’ll be a little while until I get back to you with how the code works, but it looks amazing!

Thanks! Beware, though. I tried it with a CC0 licensed map from Wikipedia with many many points. It works, but the editor in edit mode will be very very slow.

I think combining medium detail svg with a nice looking raster may look best

Good luck!

Ok a few questions about the format of the SVG:

Does the contents of the group need to look like the britain folder in the image below, or can it look like france too?

Does each country have to be filled in with a colour or can it be an outline? If so, does it have to be the same colour or can they be different colours?

I’m also assuming that each individual country in a group has to be a self contained shape, even if that means having bordering countries having overlapping lines, is that correct?

Also is there anything I have to do about layers? If I shouldn’t have any how do I delete them?

It can be like France too

Just an outline should work, but then the example project will not highlight anything, because it treats fill color as Polygon2D. You may have to adapt it to change the Line2D color.

Different colors is fine, but the example script overrides it so you may have to delete that bit of code from it. I can adapt it too but at some point you’ll have to be in charge of your own code. I’m not building your game for you either, right? :person_shrugging::slightly_smiling_face:

Correct. Like wooden puzzle pieces.

I don’t think it matters. The example code expect groups to be no more than exactly one level deep. I haven’t even looked at layers myself. Just ignore it and test if it works first.