Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Heightmap collision shape support in Godot Physics #47347

Merged
merged 1 commit into from
Apr 20, 2021

Conversation

pouleyKetchoupp
Copy link
Contributor

Functional HeightMapShape3DSW in Godot Physics 3D, based on btHeightfieldTerrainShape from Bullet Physics with a simplified implementation.

Test project:
heightmap-4.0.zip

@pouleyKetchoupp
Copy link
Contributor Author

CC @Zylann
In case you're interested in giving it a try.
Raycasting uses the Bresenham algorithm from Bullet. For now, I haven't implemented the chunk structure to optimize it on large grids, but it can be added later.

Copy link
Contributor

@Zylann Zylann left a comment

Choose a reason for hiding this comment

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

I am not setup for Godot 4 at all yet so I probably wont test this right now, but I can give it a read.

Note, if you want to check the validity and performance of raycasts, you can try [upgrading to Godot 4 and] using this plugin I made which scans the whole screen with raycasts and overlays the resulting normals: https://github.com/Zylann/godot_collision_scanner

About the acceleration grid structure, I hardcoded the Bullet implementation to use a grid of 16 cells. If the Godot version doesn't do that, it will be slower, but from what I remember it is still somewhat acceptable. However if you do very long raycasts that can traverse the whole terrain, that's where performance will be an issue. And this happens very quickly if the shape is pickable by default, which is why I turn that off in my plugin.

Most of my comments are about optimization. I assume it's good otherwise, the algorithm looks the same. I noticed that another C++ physics engine implemented that approach too with different code, if you want to check it out: DanielChappuis/reactphysics3d@d0fa4c2

servers/physics_3d/shape_3d_sw.h Outdated Show resolved Hide resolved
servers/physics_3d/shape_3d_sw.cpp Outdated Show resolved Hide resolved
servers/physics_3d/shape_3d_sw.cpp Show resolved Hide resolved
// Simple case for rays that don't traverse the grid horizontally.
// Just perform a test on the given cell.
int x = CLAMP(begin_x, 0, width - 2);
int z = CLAMP(begin_z, 0, depth - 2);
Copy link
Contributor

@Zylann Zylann Mar 25, 2021

Choose a reason for hiding this comment

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

Optimization comment:
Probably worth a different PR/issue, but careful about using the CLAMP macro. It expands the arguments multiple times, which can sometimes be expensive if the compiler cannot reduce the expression. In this present case I assume any decent compiler will figure it out.
I wonder why this is even a macro tbh

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I didn't realize that, it can be dangerous in some cases indeed. For this case it seems like it wouldn't make a huge difference anyway so I can probably leave it this way for readability.

servers/physics_3d/shape_3d_sw.cpp Outdated Show resolved Hide resolved
return false;
}

Vector3 local_begin = p_begin + local_origin;
Copy link
Contributor

Choose a reason for hiding this comment

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

What if the ray begins outside of the heightmap? If it starts outside but intersects the AABB it should ideally start on the box hit

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's what this code does, just before the loop:

// Start inside the grid.
int x_start = CLAMP(x, 0, width - 2);
int z_start = CLAMP(z, 0, depth - 2);

// Adjust initial cross values.
cross_x += delta_x * x_step * (x_start - x);
cross_z += delta_z * z_step * (z_start - z);

x = x_start;
z = z_start;

But I agree it might be better to just reduce the ray itself according to the AABB, to replace this and the check to stop when outside the grid within the loop.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Given that it's already skipping the beginning and end of the ray and the current version seems to work fine, I actually haven't added the extra AABB check to change the ray.

}

// Stop when outside the grid.
if ((x < 0) || (z < 0) || (x >= width - 1) || (z >= depth - 1)) {
Copy link
Contributor

@Zylann Zylann Mar 25, 2021

Choose a reason for hiding this comment

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

Optimization comment:
If we clamp the maximum distance before entering the big loop it could allow to remove this check?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually for this one, the check is needed in any case because it could fall outside the grid boundaries and crash otherwise (it's also checked for each quad in Bullet).

Copy link
Contributor

Choose a reason for hiding this comment

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

The idea was to clamp the max distance to the exit border so that case would never actually happen. However float imprecision might make this not doable anyways.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah it would seem a little bit risky not to have a test at all given that it's based on float calculation and might cause crashes in rare cases.

servers/physics_3d/shape_3d_sw.cpp Show resolved Hide resolved
servers/physics_3d/shape_3d_sw.cpp Show resolved Hide resolved
@pouleyKetchoupp
Copy link
Contributor Author

@Zylann Thanks for your review! I'm going to check your comments in details, but just so you know there's also a 3.x backport of this PR with the exact same implementation: #47349.

@Zylann
Copy link
Contributor

Zylann commented Mar 25, 2021

I tested your PR with my heightmap plugin.

Upon opening my project (which has no default scene so it opens on a blank scene), I get this error:

ERROR: PhysicsServerSW::shape_get_data: Condition "!shape->is_configured()" is true. Returned: Variant()
   At: servers\physics\physics_server_sw.cpp:127

Followed later by this error:

ERROR: RID_Owner<class BodySW>::get: Condition "!p_rid.is_valid()" is true. Returned: 0
   At: D:\PROJETS\INFO\GODOT\Engine\nekomatata_fork\core/rid.h:149
ERROR: PhysicsServerSW::body_attach_object_instance_id: Condition "!body" is true.
   At: servers\physics\physics_server_sw.cpp:673
ERROR: Invalid ID.
   At: servers\physics\physics_server_sw.cpp:1402

I don't know what these are or if they are even related to your PR, but they happen before opening any scene.

I then tested a few things:

  • Throwing rigidbody cubes around

    • Debug build:
      Worked. Can't tell about performance, I don't have a benchmark scene setup for this

    • ReleaseDebug build:
      Also works

  • Raycast scanning:

    • Debug build:
      It works, however it is A LOT slower than Bullet, maybe 4 times slower. I'll assume the debug build makes things a bit meaningless, but as stated in the review the GodotPhysics shape does not use any accelerator.
      It appears to match the visuals but it's too slow to wait for the full detailed pass.

    • ReleaseDebug build:
      Also works, faster as well.
      The x4 performance ratio can still be observed though. This is how long it takes to fill the viewport:

      • GodotPhysics: 4min53s
      • Bullet: 58s

    Comparing two screenshots, the result is identical:

GodotPhysics:
image

Bullet:
image

@pouleyKetchoupp
Copy link
Contributor Author

@Zylann I've just pushed changes based on your comments:

  • _face_cull_segment, _cell_cull_segment, _get_point and _get_height are now inlined
  • Early exit on hit
  • Forcing Vector<float> instead of using real_t. I've also changed the scene node code and the Bullet server code the same way, but I haven't ported this change to the 3.x version. There's no way to distinguish an array of float from double in a variant on 3.x, so it's safer to stick to real_t.

Thanks for running the performance tests! Given the x5 performance difference in release_debug, it's definitely worth adding the raycast acceleration based on chunks. I'll add it in a different PR later.

@kalysti
Copy link
Contributor

kalysti commented Apr 19, 2021

Btw its working well. Can u bring it into master. Because i need a heightmap collider for my terrain tool ;-)

@pouleyKetchoupp
Copy link
Contributor Author

@sboron Thanks for testing! This PR still needs a review and approval from a maintainer to be merged. In the meantime you can cherry-pick the commit to your custom branch.

@akien-mga akien-mga merged commit fdf041a into godotengine:master Apr 20, 2021
@akien-mga
Copy link
Member

Thanks!

@pouleyKetchoupp pouleyKetchoupp deleted the heightmap-support branch April 20, 2021 16:49
@Zylann
Copy link
Contributor

Zylann commented Apr 21, 2021

I just remembered, we may consider supporting holes using nans/infs at some point, which would make it way easier to have openings in the map Zylann/godot_heightmap_plugin#125

@pouleyKetchoupp
Copy link
Contributor Author

@Zylann That sounds useful! Feel free to open a proposal if you want, otherwise I'll just add it to my todo list and write one when I find some time.

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.

5 participants