How do I make a collision shape from a heightmap!?

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

I’m a beginner with Godot and have never tried coding a 3d game before.

I’m sure that there must be a simple way to convert a heightmap to a collisionshape. I’ve tried making a collision shape and setting option “shape” to HeightMapShape. But it seems that I have to insert all the height values by hand! How can i use an heightmap image I’ve created as the heightmap?

:bust_in_silhouette: Reply From: Zylann

You don’t have to set them all “by hand”. A heightmap can come from various sources, but the most common one is an image, and there is a way to pass image data to the shape.

To work with this shape, your image must be in the FORMAT_RF format (32-bit float precision, single channel). If your image is 8-bit, this won’t work well, because colors can only have 255 values are are quantified between 0 and 1, while heights can be somewhere between -500 to 500 for example.

I think there isn’t a built-in tool yet to set it up in the editor (only plugins), but here is a script example:

var heightmap = Image.new()

# Load EXR file, one of the formats Godot can handle.
heightmap.load("file.exr")

# Godot's EXR loader still doesn't load properly single-channel images,
# so you should make sure to convert it
heightmap.convert(Image.FORMAT_RF)

# Create the shape (if you don't have one already)
var shape = HeightMapShape.new()

# Assign size first, otherwise it won't work
shape.map_width = heightmap.get_width()
shape.map_height = heightmap.get_height()

# Assign the heights using the image's raw data.
# Because the format matches, this is straightforward
shape.map_data = heightmap.get_data()

# Now all is left to do is to assign the shape to your collision node.

This still needs to be improved, both in usability and efficiency (setting width and height allocates a whole heightmap for no reason since it gets set from the image anyways).

Thanks for your quick answer!

It had just a little typo where map_height should be map_depth. I also think that you shouldn’t declare shape as a variable because it’s already a property of the CollisionShape.

My code looks like this:

extends CollisionShape

var heightmap = Image.new()

func _ready():
# Load EXR file, one of the formats Godot can handle.
heightmap.load("res://assets/sprites/hm2.exr")

# Godot's EXR loader still doesn't load properly single-channel images,
# so you should make sure to convert it
heightmap.convert(Image.FORMAT_RF)

# Create the shape (if you don't have one already)
shape = HeightMapShape.new()

# Assign size first, otherwise it won't work
shape.map_width = heightmap.get_width()
shape.map_depth = heightmap.get_height()

# Assign the heights using the image's raw data.
# Because the format matches, this is straightforward
shape.map_data = heightmap.get_data()

# Now all is left to do is to assign the shape to your collision node.

But when I try running the code it throws this error message:
Invalid set index 'map_data'(On base Heightmapshape) with value of type poolByteArray

I noticed that Godot docs says that map_data data type should be PoolRealArray not PoolByteArray. Could this be the problem?

Kimmo_12345 | 2020-03-19 20:16

Yeah I’ve thrown out this code as an untested example, feel free to adapt :slight_smile:

I noticed that Godot docs says that map_data data type should be PoolRealArray not PoolByteArray. Could this be the problem?

Ugh. That’s annoying.
Weeeeeell

var float_array = PoolRealArray()
heightmap.lock()
var i = 0
for y in heightmap.get_height():
	for x in heightmap.get_width():
		float_array[i] = heightmap.get_pixel(x, y).r
		i += 1
heightmap.unlock()

Then use float_array instead.

Zylann | 2020-03-19 20:22

Invalid set index '0' (on base: 'PoolRealArray') with value of type 'float'

Just use float_array.append(heightmap.get_pixel(x, y).r) instead and get rid of the counter.

Tooniis | 2020-06-30 21:16

Just give it proper size heightmap.get_width() * heightmap.get_height() beforehand. The reason I didnt use append() is because at the time, Godot was reallocating the entire array everytime a single value was added, which adds up to horrible performance loss on large maps.

Zylann | 2020-06-30 22:37