How can I automatically create a CollisionPolygon2D from an image using GDNative or GDScript?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By Austin

I am aware of the option to create it manually using the “Create CollisionPolygon2D Sibling” option. However, I will be using procedurally generated images and cannot do it manually. I also cannot find a way to do it using an array of numbers or by getting the transparent parts of the image. How can I do it using a script?

:bust_in_silhouette: Reply From: Zylann

I think there is some room to experiment with BitMap: BitMap — Godot Engine (3.1) documentation in English

You could create a BitMap by iterating pixels of the image and setting to true bits that you consider “opaque”. Then, use the opaque_to_polygons function, which appears to do what you want.
Beware I never used this, I just found it by luck some day and found those neat functions. It’s not documented so you might have to do some digging.

Other ways could involve marching squares, or just looking at Godot’s source code to see how it generates the polygon from the image.

I attempted to add the shape using this code in a script attached to my sprite:

func _create_collision_polygon():
	var bm = BitMap.new()
	bm.create_from_image_alpha(texture.get_data())
	var rect = Rect2(0, 0, texture.get_width(), texture.get_height())
	var my_array = bm.opaque_to_polygons(rect)
	var my_polygon = Polygon2D.new()
	my_polygon.set_polygons(my_array)
	get_parent().get_node("CollisionPolygon2D").set_polygon(my_polygon.polygon)

I also tried replacing my_polygon.polygon with my_polygon.polygons and my_polygon.uv and neither worked.

Are there any mistakes? Also, do you have any suggestions for how to modify it to get it to work?

Austin | 2019-08-09 04:12

:bust_in_silhouette: Reply From: Austin
func _create_collision_polygon():
	var bm = BitMap.new()
	bm.create_from_image_alpha(texture.get_data())
	var rect = Rect2(position.x, position.y, texture.get_width(), texture.get_height())
	var my_array = bm.opaque_to_polygons(rect)
	var my_polygon = Polygon2D.new()
	my_polygon.set_polygons(my_array)
	get_parent().get_node("CollisionPolygon2D").set_polygon(my_polygon.polygons[0])

Put this in a script attached to the sprite where the sprite has a StaticBody2D as its parent and its sibling is a CollisionPolygon2D. Make sure the sprite is not centered.

NOTE: This collision is not perfect and seems to be slightly incorrect on some parts. This can be fixed by replacing var my_array = bm.opaque_to_polygons(rect) with var my_array = bm.opaque_to_polygons(rect, 0.0001), although the second argument could probably be replaced with some other small number.

EDIT: Here is a way to do it when the image is made of multiple parts (the StaticBody2D node does not need to have a CollisionPolygon2D manually added):

func _create_collision_polygon():
	var bm = BitMap.new()
	bm.create_from_image_alpha(texture.get_data())
	var rect = Rect2(position.x, position.y, texture.get_width(), texture.get_height())
	var my_array = bm.opaque_to_polygons(rect, 0.0001)
	var my_polygon = Polygon2D.new()
	my_polygon.set_polygons(my_array)
	for i in range(my_polygon.polygons.size()):
		var my_collision = CollisionPolygon2D.new()
		my_collision.set_polygon(my_polygon.polygons[i])
		my_collision.scale = scale
		get_parent().call_deferred("add_child", my_collision)

EDIT2: And here is how I made it work with centered sprites:

func _create_collision_polygon():
    	var bm = BitMap.new()
    	bm.create_from_image_alpha(texture.get_data())
    	var rect = Rect2(position.x, position.y, texture.get_width(), texture.get_height())
    	var my_array = bm.opaque_to_polygons(rect, 0.0001)
    	var my_polygon = Polygon2D.new()
    	my_polygon.set_polygons(my_array)
    	var offsetX = 0
    	var offsetY = 0
    	if (texture.get_width() % 2 != 0):
    		offsetX = 1
    	if (texture.get_height() % 2 != 0):
    		offsetY = 1
    	for i in range(my_polygon.polygons.size()):
    		var my_collision = CollisionPolygon2D.new()
    		my_collision.set_polygon(my_polygon.polygons[i])
    		my_collision.position -= Vector2((texture.get_width() / 2) + offsetX, (texture.get_height() / 2) + offsetY) * scale.x
    		my_collision.scale = scale
    		get_parent().call_deferred("add_child", my_collision)

Is there a way to export the results of this to a saved file, like a tscn file. I have large maps that I would like to pre-compile.

bwata | 2020-07-30 17:35

This works great. Just to add that I think you need to call my_polygon.free() at the end of the function because Godot starts complaining about unfreed memory otherwise.

Rokner | 2021-09-21 00:32