Skip to content

Chunk tilemap physics - #102662

Merged
Repiteo merged 1 commit into
godotengine:masterfrom
groud:chunk_tilemap_physics
Mar 17, 2025
Merged

Chunk tilemap physics#102662
Repiteo merged 1 commit into
godotengine:masterfrom
groud:chunk_tilemap_physics

Conversation

@groud

@groud groud commented Feb 10, 2025

Copy link
Copy Markdown
Member

Right now, TileMapLayer physics is implemented using one collision body for each cell, which is likely lowering runtime performance (to be verified, but I doubt having thousands of colliders on a map has no impact). This PR reworks the TileMapLayer physics so that cells shapes get merged into bigger collision shapes, whenever possible. Aphysics_quadrant_size param allow changing the size of each collision chunk.

This video illustrate the processing done:

2025-02-10.16-16-07.mp4

Note this PR breaks compatibility for get_coords_for_body_rid, as with the PR, a single body can cover multiple cells.

Bugsquad edit: Fixes #84163 Fixes #89458

@Calinou

Calinou commented Feb 10, 2025

Copy link
Copy Markdown
Member

@groud

groud commented Feb 10, 2025

Copy link
Copy Markdown
Member Author

(this is essentially the physics counterpart of the existing TileMap navmesh baking).

More or less though. This PR makes the baking as built-in to the TileMaplayer itself, while navmesh baking is made on the navigation region side (like, you to define a region and bake it).

@KoBeWi

KoBeWi commented Feb 10, 2025

Copy link
Copy Markdown
Member

I could confirm it fixes performance issues (#84163). Could not confirm fixing seams (#89458) because it's too unreliable, but it's safe to assume it's fixed too.

@KoBeWi

KoBeWi commented Feb 10, 2025

Copy link
Copy Markdown
Member

From further testing:

  • It would be nice to have a way to visualize chunks. If the chunk size is too small, it could still cause seams and visualization would make it easier to pick a better size. Although seems like there is no size limit, so you can just make it super big to cover whole map.
  • The chunks are regenerated every time you modify tiles. While it's expected, it causes problems in the editor, because too big chunks make editing super slow. Is there no way to avoid re-creating shapes in the editor? Maybe only do that when they are visible? Though it's easily solvable with a script that makes chunks bigger at runtime, so not much a problem.

@groud

groud commented Feb 10, 2025

Copy link
Copy Markdown
Member Author

It would be nice to have a way to visualize chunks. If the chunk size is too small, it could still cause seams and visualization would make it easier to pick a better size. Although seems like there is no size limit, so you can just make it super big to cover whole map.

Ah well, the colors are a bit randomized (to distinguish shapes a bit), but it is done per-chunk. So well, the first shape always use the same color so you cannot distinguish chunks. I guess making the seams visible should be doable, I'll give it a try.

The chunks are regenerated every time you modify tiles. While it's expected, it causes problems in the editor, because too big chunks make editing super slow. Is there no way to avoid re-creating shapes in the editor? Maybe only do that when they are visible? Though it's easily solvable with a script that makes chunks bigger at runtime, so not much a problem.

Hmm, I don't think it would be easily solvable. Modifying the TileSet kind of has to re-generate the whole TileMap. I guess it could be optimized but that's sadly a toooon of work. So for now I don't really see a good solution for the problem.

@KoBeWi

KoBeWi commented Feb 10, 2025

Copy link
Copy Markdown
Member

Modifying the TileSet kind of has to re-generate the whole TileMap.

I meant modifying the TileMap itself. When you paint or erase tiles, it seems to re-create modified physics chunks for no reason.

@groud

groud commented Feb 10, 2025

Copy link
Copy Markdown
Member Author

I meant modifying the TileMap itself. When you paint or erase tiles, it seems to re-create physics chunks for no reason.

Oh ok. Well yeah, it updates chunk by chunk, like for rendering. So indeed changing a tile in a given chunk re-renders the whole chunk. Not sure it could be optimized more though.

Maybe I could limit the chunk size in the editor, but It might hurt debugging I guess.

In any case, the polygon merging can maybe be optimized a bit, I need to check.

@djrain

djrain commented Feb 10, 2025

Copy link
Copy Markdown

This would be great to have for light occluders also. I can say that merging tiles greatly reduced the impact of shadows in our game in 4.3. Clay has made some shadow performance improvements in 4.4 that I've yet to test, but I'm guessing the chunking will still help.

@groud

groud commented Feb 10, 2025

Copy link
Copy Markdown
Member Author

This would be great to have for light occluders also. I can say that merging tiles greatly reduced the impact of shadows in our game in 4.3. Clay has made some shadow performance improvements in 4.4 that I've yet to test, but I'm guessing the chunking will still help.

That's a good point. It should be doable.

@smix8

smix8 commented Feb 10, 2025

Copy link
Copy Markdown
Contributor

It is definitely an improvement over the status quo.

Could not confirm fixing seams (#89458) because it's too unreliable, but it's safe to assume it's fixed too.

No full fix to physics ghost collision. This technically can not fix physics seam issues as when you bake it to chunks you still have chunk seams. For physics to really remove all those internal edge ghost collision issues it would need to merge all cells together that create a combined shape instead of slicing them to chunk size.

I don't think we can keep the debug lines, as it has a performance impact.

Change from rendering the debug with the very inefficient canvas item polyline function to bake a chunk mesh to render, it is dirt cheap in comparison.

@groud

groud commented Feb 10, 2025

Copy link
Copy Markdown
Member Author

Change from rendering the debug with the very inefficient canvas item polyline function to bake a chunk mesh to render, it is dirt cheap in comparison.

Hmm, interesting idea. IIRC, I had issues with 2D meshes back in the days, but maybe it got better? I'll try it out.

@smix8

smix8 commented Feb 10, 2025

Copy link
Copy Markdown
Contributor

Maybe there were issues but by now the 2d navmesh, csg and other places with a lot of edge debug use it already with no reported issues so I think it should not be a problem for TileMap to adopt it as well to improve performance.

@KoBeWi

KoBeWi commented Feb 10, 2025

Copy link
Copy Markdown
Member

This technically can not fix physics seam issues as when you bake it to chunks you still have chunk seams.

You can set chunk size to cover the whole TileMap. I checked my biggest one, chunk size 512 and works without problems. It does not impact loading time, but modifying the tiles is slower (luckily I don't need to do it).

@smix8

smix8 commented Feb 10, 2025

Copy link
Copy Markdown
Contributor

You can set chunk size to cover the whole TileMap.

Ok I did not thought about that option, just assumed there would be a limit of quadrant size of sorts, yeah that could work.

@groud

groud commented Feb 10, 2025

Copy link
Copy Markdown
Member Author

I'm thinking about something. Maybe I could try implementing some sort of an automatic "edge smoothing" ?
edgesmoothing

Like, a 45° on the edge of each chunk's polygon might help avoiding characters being stuck when sliding? I guess we gotta try by making this change in tile first maybe. By like, adding a 1px "chamfer" and if there still are issues.

@Calinou

Calinou commented Feb 10, 2025

Copy link
Copy Markdown
Member

You can set chunk size to cover the whole TileMap. I checked my biggest one, chunk size 512 and works without problems. It does not impact loading time, but modifying the tiles is slower (luckily I don't need to do it).

I'm wondering if we should improve the usability of huge chunk sizes by limiting their size in the editor, so that you can use one in the project and not suffer from slow editing speeds.

On the other hand, I know it's been discouraged in the past to have huge collision shapes spanning the whole level, as each dynamic object will have to test collisions against the whole shape regardless of its position. This is particularly the case in 3D where trimesh collision is generally much more complex than tile collision in 2D.

I'm thinking about something. Maybe I could try implementing some sort of an automatic "edge smoothing" ?

I think this is better solved at a character controller level by implementing skimming, so that you don't need to make the collision shapes more complex.

At a core level, skimming refers to preserving the character's velocity when hitting something for a short amount of time. This means that you will keep sliding until you no longer hit the surface, and as soon as you are no longer colliding, you will keep the velocity you've had before hitting the surface. You could see it as a reverse coyote time of sorts.

Skimming as a concept has been used in both 2D and 3D games, with varying implementations (some being more generous than others).

Note that regardless of this, I don't think skimming (or adding chamfers to collision shapes) this resolves the issue of characters bumping when sliding on the ground. One way to minimize the issue is to disable gravity while on a flat surface.

@Calinou Calinou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested locally, it works as expected.

Testing project: test_tilemap_large.zip

Physics seams issues

Holding left arrow in the testing project's main scene.

Before

rolling_ball_before.mp4

After

Physics seams issues still occur occasionally, but are much rarer and less severe.

rolling_ball_after.mp4

After (physics quadrant size set to 2**31 - 1)

This fully avoids the issue, but makes editing slower in the editor and may have adverse performance issues in large scenes (due to each object having to check collision against the entire level's shape).

rolling_ball_after_max_quadrant.mp4

Performance

PC specifications
  • CPU: Intel Core i9-13900K
  • GPU: NVIDIA GeForce RTX 4090
  • RAM: 64 GB (2×32 GB DDR5-5800 C30)
  • SSD: Solidigm P44 Pro 2 TB
  • OS: Linux (Fedora 41)

Command line used for testing (we render in a tiny window to avoid GPU bottlenecks1):

godot --path /path/to/project lots_of_tiles.tscn --resolution 64x64 --print-fps

1000 RigidBodies enclosed in a complex TileMap.

Before

We encounter a "physics spiral of death" phenomenon, hence the sudden decrease in FPS at some point.

Project FPS: 391 (2.55 mspf)
Project FPS: 577 (1.73 mspf)
Project FPS: 11 (90.90 mspf)
Project FPS: 8 (125.00 mspf)
Project FPS: 60 (16.66 mspf)
Project FPS: 9 (111.11 mspf)
Project FPS: 20 (50.00 mspf)
Project FPS: 590 (1.69 mspf)
Project FPS: 328 (3.04 mspf)
Project FPS: 399 (2.50 mspf)
Project FPS: 442 (2.26 mspf)

After

No more physics spiral of death, and overall performance is greatly improved.

Project FPS: 837 (1.19 mspf)
Project FPS: 488 (2.04 mspf)
Project FPS: 698 (1.43 mspf)
Project FPS: 876 (1.14 mspf)
Project FPS: 900 (1.11 mspf)
Project FPS: 872 (1.14 mspf)
Project FPS: 880 (1.13 mspf)
Project FPS: 649 (1.54 mspf)

Feedback

  • The property should be documented in the class reference, and it should have a property hint added to set a suitable range. I'd suggest something like 1,1024,1 as sizes above 1024 will likely run into the aforementioned performance issue, on top of being very slow for editing.
  • We could sidestep the editing performance problem at large quadrant sizes by clamping the size of the physics quadrant while in the editor to a value like 16 or 32.

Footnotes

  1. --disable-render-loop breaks --print-fps reporting, so we can't use it to test physics performance.

@KoBeWi

KoBeWi commented Feb 10, 2025

Copy link
Copy Markdown
Member

it should have a property hint added to set a suitable range. I'd suggest something like 1,1024,1 as sizes above 1024 will likely run into the aforementioned performance issue

That depends. The chunk size is square, while the TileMap can be wide. My widest level is 128x1664, which is much smaller than 1024x1024, but with size limit it couldn't be covered by single chunk. Though with or_greater it would be fine.

The slow editor editing should be fixed not circumvented. As I said, physics chunks don't need to be created in the editor if the shapes are not visible.

@smix8

smix8 commented Feb 10, 2025

Copy link
Copy Markdown
Contributor

For editing performance could just do as the navmesh baking does. The navmesh editing has a timer that triggers and sets the time on any editing. So only when the user stops editing the timer fires and the navmesh gets rebaked. Could work the same for this collision bake.

Comment thread scene/2d/tile_map_layer.h Outdated
Comment thread scene/2d/tile_map_layer.h Outdated
Comment thread scene/2d/tile_map_layer.cpp Outdated
Comment thread scene/2d/tile_map_layer.cpp Outdated
@KoBeWi

KoBeWi commented Feb 10, 2025

Copy link
Copy Markdown
Member

Ah well, the colors are a bit randomized (to distinguish shapes a bit), but it is done per-chunk. So well, the first shape always use the same color so you cannot distinguish chunks.

From what I see, you randomize colors once per chunk, so each chunk should have unique color. But:

  • rand.random returns the same value for whatever reason.
  • The color stays unchanged anyway, even if randomizing the same number should change it (because you are adding to previous color).
  • You are using (-1, 1) range for color randomization, which can eventually go out of bounds. Not sure if set_hsv() can handle it.

This is what I got by changing rand.random() to Matf::randf() (and tweaking some values):
image
You could distinguish both chunks and shapes by randomizing hue for chunks and value for shapes.

Comment thread scene/2d/tile_map_layer.cpp Outdated
Comment on lines 3154 to 3196

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually I've seen error checks before guards, but not sure if it's consistent.

@Mickeon

Mickeon commented Mar 17, 2025

Copy link
Copy Markdown
Member

I would like to ask a question just as food for thought before we regret it later.

Rotated, one-way platforms are not technically possible to register in the current TileSet workflow. My own project checks a vast area for collision RIDs that correspond to a specific tile. No need to go in the specifics, but if they do correspond, they are rotated from within the physics server.

Does this PR affect this kind of routine? And how to work around it?

@Mickeon Mickeon left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure if "quadrant" has ever been the most accurate word to describe this. Even in geometry it's not accurate, but, oh well. That's been their name.

</member>
<member name="physics_quadrant_size" type="int" setter="set_physics_quadrant_size" getter="get_physics_quadrant_size" default="16">
The [TileMapLayer]'s physics quadrant size. Within a physics quadrant, cells with similar physics properties are grouped together and their collision shapes get merged. [member physics_quadrant_size] defines the length of a square's side, in the map's coordinate system, that forms the quadrant. Thus, the default quadrant size groups together [code]16 * 16 = 256[/code] tiles.
[b]Note:[/b] As quadrants are created according to the map's coordinate system, the quadrant's "square shape" might not look like square in the [TileMapLayer]'s local coordinate system.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[b]Note:[/b] As quadrants are created according to the map's coordinate system, the quadrant's "square shape" might not look like square in the [TileMapLayer]'s local coordinate system.
[b]Note:[/b] As quadrants are created according to the map's coordinate system, the quadrants may not look like squares in the [TileMapLayer]'s local coordinate system, depending on the [member tile_set]'s [member TileSet.tile_shape].

Maybe it's too verbose?

@KeyboardDanni

Copy link
Copy Markdown
Contributor

I would like to ask a question just as food for thought before we regret it later.

Rotated, one-way platforms are not technically possible to register in the current TileSet workflow. My own project checks a vast area for collision RIDs that correspond to a specific tile. No need to go in the specifics, but if they do correspond, they are rotated from within the physics server.

Does this PR affect this kind of routine? And how to work around it?

This would affect the collision RIDs from my understanding, since this is basically just creating merged shapes out of each chunk. As a workaround you could set the quadrant size to 1. But if you still wanted the benefits of this PR, you could have a separate TileMapLayer with just the one-ways. Not an elegant solution but it'd work.

@Mickeon

Mickeon commented Mar 17, 2025

Copy link
Copy Markdown
Member

This would affect the collision RIDs from my understanding, since this is basically just creating merged shapes out of each chunk. As a workaround you could set the quadrant size to 1. But if you still wanted the benefits of this PR, you could have a separate TileMapLayer with just the one-ways. Not an elegant solution but it'd work.

Okay. It's among the things worth sharing around and documenting when this gets merged, so let's take note.

@Repiteo
Repiteo merged commit 85258ec into godotengine:master Mar 17, 2025
@Repiteo

Repiteo commented Mar 17, 2025

Copy link
Copy Markdown
Contributor

Thanks!

@ShinryakuTako

Copy link
Copy Markdown

Performance improvements are always awesome. Of course it breaks the destructible tiles system I JUST finished implementing yesterday 🥲

Does anyone have a workaround for getting the cell coordinates from an Area2D script?

func onBodyShapeEntered(bodyRID: RID, bodyEntered: Node2D, bodyShapeIndex: int, localShapeIndex: int) -> void:
    if bodyEntered is TileMapLayer:
       var cellCoordinates: Vector2i = bodyEntered.get_coords_for_body_rid(bodyRID)

doesn't work anymore.

@KoBeWi

KoBeWi commented Mar 21, 2025

Copy link
Copy Markdown
Member

You can set quadrant size to 1 and it should behave as before (including the performance). Bigger quadrants will result in slower updating, so you might want to disable them anyway.

AFAIK there is no reliable way to get a tile from a position (see #35344). For some use-cases, where you don't need precise coordinates (like for detecting floor type), you can use a couple of raycasts to get tile data and approximate the result.

@dginovker

Copy link
Copy Markdown

WOW this PR is amazing. You still get the collision issues on corners of tilemaps but building Godot from source to get this change has saved my project!

@h0lley

h0lley commented Sep 21, 2025

Copy link
Copy Markdown

unfortunately this broke my enemy behavior.

my enemies have an Area2D with the body_shape_entered signal hooked up. if body_shape_entered passes a TileMapLayer, I need to know the position of the specific tile that clipped into the Area2D so that an avoidance vector can be updated accordingly.

this was previously possible by using PhysicsServer2D.body_get_direct_state(body_rid).transform.origin. now that presumably the physics bodies have been consolidated into one I am no longer getting a tile position from this.

@KeyboardDanni

Copy link
Copy Markdown
Contributor

As discussed previously, you can get the old behavior back by setting the chunk size to 1. Unless you're saying that this is broken too...?

@h0lley

h0lley commented Sep 21, 2025

Copy link
Copy Markdown

As discussed previously, you can get the old behavior back by setting the chunk size to 1. Unless you're saying that this is broken too...?

yes, that did the trick! thank you and apologies for not combing through the entire thread.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Physics bodies can collide with tile seams Big TileMap with collisions is extremely slow