Skip to content

Add support for per-shape physics materials#100748

Open
tancop wants to merge 1 commit into
godotengine:masterfrom
tancop:add-per-shape-materials
Open

Add support for per-shape physics materials#100748
tancop wants to merge 1 commit into
godotengine:masterfrom
tancop:add-per-shape-materials

Conversation

@tancop
Copy link
Copy Markdown

@tancop tancop commented Dec 22, 2024

Closes godotengine/godot-proposals#7401.

This PR adds:

  • Methods shape_set_friction, shape_get_friction, shape_set_bounce, shape_get_bounce to PhysicsServer3D
  • Property physics_material to Shape3D

This is implemented for both Godot and Jolt Physics (using a custom Jolt physics material like one of the official samples).

Example project that uses two different materials on one body: per-shape-materials.zip

@tancop tancop requested review from a team as code owners December 22, 2024 18:57
@tancop tancop force-pushed the add-per-shape-materials branch 3 times, most recently from 6079b03 to 198281e Compare December 23, 2024 15:27
@AThousandShips AThousandShips added this to the 4.x milestone Dec 23, 2024
@tancop tancop force-pushed the add-per-shape-materials branch 2 times, most recently from 3afbf17 to 0713122 Compare December 24, 2024 09:11
@tancop
Copy link
Copy Markdown
Author

tancop commented Dec 24, 2024

Fixed the xml method order and added support for concave shapes.

@tancop tancop force-pushed the add-per-shape-materials branch from 0713122 to 0589886 Compare December 25, 2024 10:10
@tancop
Copy link
Copy Markdown
Author

tancop commented Dec 25, 2024

Added support for changing materials at runtime. I had to add a new custom shape type JoltCustomMaterialOverrideShape, add a new virtual method on JoltShape3D and change some others to return a mutable JPH::Ref<JPH::Shape> instead of a JPH::ShapeRefC.

@tancop tancop force-pushed the add-per-shape-materials branch 2 times, most recently from 08080d6 to e72672f Compare December 27, 2024 21:24
@tancop
Copy link
Copy Markdown
Author

tancop commented Dec 28, 2024

@AThousandShips I guess this is ready for review. If you see something wrong or confusing let me know

Comment thread modules/jolt_physics/shapes/jolt_concave_polygon_shape_3d.cpp Outdated
Comment thread modules/jolt_physics/shapes/jolt_height_map_shape_3d.cpp Outdated
Comment thread modules/jolt_physics/shapes/jolt_world_boundary_shape_3d.cpp Outdated
Comment thread modules/jolt_physics/shapes/jolt_custom_decorated_shape.h Outdated
jrouwe added a commit to jrouwe/JoltPhysics that referenced this pull request Dec 29, 2024
@tancop tancop force-pushed the add-per-shape-materials branch from e72672f to 65418b3 Compare December 30, 2024 09:40
@tancop
Copy link
Copy Markdown
Author

tancop commented Dec 30, 2024

@jrouwe thanks! I saw RestoreMaterialState in the docs but thought it was just for deserializing from binary state. You saved 200 lines and a custom shape slot

Comment thread modules/jolt_physics/shapes/jolt_custom_shape_type.h Outdated
Comment thread modules/jolt_physics/objects/jolt_shaped_object_3d.cpp Outdated
@tancop tancop force-pushed the add-per-shape-materials branch 2 times, most recently from 49b2190 to 07de536 Compare December 31, 2024 08:26
@tancop tancop force-pushed the add-per-shape-materials branch from 07de536 to 870c7db Compare December 31, 2024 17:46
@tancop
Copy link
Copy Markdown
Author

tancop commented Dec 31, 2024

Renamed JoltShape3D::_set_material to _update_material, it only changes the generated Jolt material not the shape itself

@tancop tancop force-pushed the add-per-shape-materials branch from 870c7db to 8967fc0 Compare January 3, 2025 13:06
@tancop
Copy link
Copy Markdown
Author

tancop commented Jan 3, 2025

Rebased on latest master

@tancop tancop force-pushed the add-per-shape-materials branch 3 times, most recently from 09aedba to cb5eca2 Compare May 14, 2025 11:34
Copy link
Copy Markdown
Contributor

@mihe mihe left a comment

Choose a reason for hiding this comment

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

Apologies for dragging this out. I want to sit down and review this PR in-depth, but making time for lengthy Jolt-related tasks has been tough these past few weeks.

Just quickly trying this out with Jolt I ran into a pretty fundamental issue right away, as seen in my review comment, so that's something that needs to be addressed at least.

I'll set aside time this friday to do a more in-depth review.

Comment thread modules/jolt_physics/spaces/jolt_contact_listener_3d.cpp Outdated
@tancop tancop force-pushed the add-per-shape-materials branch 6 times, most recently from 4ce08ea to 9de1fb4 Compare May 16, 2025 15:56
Copy link
Copy Markdown
Contributor

@mihe mihe left a comment

Choose a reason for hiding this comment

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

I've had a closer look at this PR now, and I have a couple of changes lined up that I'd like to see done for the Jolt side of things.

Instead of going back-and-forth with review comments I decided to just go ahead and make the changes myself, which you'll find in this diff.

I've tried to isolate the commits as best as possible, so you should hopefully be able to inspect them individually.

These are the more notable changes I guess:

  1. I moved the all the shape material stuff from JoltShapedObject3D to JoltBody3D. There wasn't much of a reason to have it be shared with JoltArea3D, and this maps better to them being stored in GodotBody3D for Godot Physics.
  2. The updating of manifold reduction was not being done correctly, and resulted in areas enabling it, which they shouldn't do. I also couldn't quite understand why it was being done in JoltShapedObject3D::commit_shapes, so that's all similarly been moved to JoltBody3D, and now follows the same thing_changed() update_thing() pattern that's used elsewhere in that file and the module in general, to (hopefully) ensure that this state doesn't go stale.
  3. Having uses_shape_materials() potentially loop over hundreds of friction values and hundreds of bounce values for complex compound shapes had me concerned, given that it was being called for every contact in the contact listener. I went ahead and added caching of that value instead, which gets updated whenever the shape materials change in any way.
  4. Having _try_override_collision_response be involved in deciding whether to call _override_contact_properties seemed unnecessary. That now instead follows the _try_do_thing pattern that's used for the other stuff in the contact listener.
  5. I moved the combiner functions into jolt_math_funcs.h, to avoid duplicating them in jolt_contact_listener_3d.cpp. They probably don't belong there per se, but I couldn't justify creating a whole new file for them.
  6. There wasn't much point to storing friction/bounce as real_t, since Jolt itself doesn't use double-precision for these, so these now get converted to/from float at the physics server boundary, same as a lot of other floating-point values in that module. This technically means you won't always return exactly the same value from the getter that's passed to the setter, but I feel that's a minor concession.

It's up to you how you'd prefer to merge these in, assuming you don't have any objections to the changes, or spot some potential problems with them. I think I have the necessary permissions to push to your branch, if you're fine with that. If not, you should be able to git apply this patch as well, or just add my fork as a remote and squash them in.

I haven't looked too closely at the Godot Physics side of the PR, so I can't say much about that, but I do have a couple of concerns, which I've left here as review comments.

Comment thread scene/2d/physics/collision_shape_2d.cpp Outdated
Comment thread scene/2d/physics/collision_shape_2d.cpp Outdated
Comment thread scene/2d/physics/collision_shape_2d.cpp
Comment thread scene/3d/physics/collision_shape_3d.cpp Outdated
@tancop tancop force-pushed the add-per-shape-materials branch 2 times, most recently from b28dbc2 to 9e65693 Compare May 17, 2025 09:45
@tancop
Copy link
Copy Markdown
Author

tancop commented May 17, 2025

@mihe thanks for the review just merged all the changes. I see you removed some casts to uint32_t in new JoltBody3D methods, had to add them back because -Wsign-compare is turned on for official builds

@tancop
Copy link
Copy Markdown
Author

tancop commented May 22, 2025

@mihe this good to go now or you still got questions? just wanna make sure everything clean before it gets merged

Copy link
Copy Markdown
Contributor

@mihe mihe left a comment

Choose a reason for hiding this comment

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

I found a fairly critical issue with the Godot Physics implementation, and some very minor cosmetic remarks in the Jolt implementation.

Outside of these review comments, I must admit that I'm becoming more and more uncomfortable with the API of these two PhysicsServer*D methods:

void body_set_shape_friction_override(RID p_body, int p_shape_idx, bool p_enable, real_t p_friction = 0.0);
void body_set_shape_bounce_override(RID p_body, int p_shape_idx, bool p_enable, real_t p_bounce = 0.0);

I realize it's a bit late to be having this conversation, and that the current design stems from feedback that have been given here already, so I won't push too hard on this, but the whole thing with having p_enable there, and then defaulting the values to 0.0, just so you can pass false without needing to give it a value. It makes for an odd-looking API in my opinion.

Unfortunately I struggle to think of an alternative that wouldn't ideally require a bool to be added for each material state as well, which seems like a hefty price to pay for what is only (subjectively) a better API.

Anyway, I just figured I'd voice my concern at least, in case anyone had any other ideas, or know of other Godot APIs that do something similar already.

Comment thread modules/godot_physics_2d/godot_body_2d.h
Comment thread modules/godot_physics_3d/godot_body_3d.h
Comment thread modules/jolt_physics/objects/jolt_body_3d.cpp Outdated
Comment thread modules/jolt_physics/objects/jolt_body_3d.cpp Outdated
Comment thread modules/jolt_physics/objects/jolt_body_3d.cpp Outdated
Comment thread modules/jolt_physics/objects/jolt_body_3d.h Outdated
Comment thread scene/2d/physics/collision_shape_2d.cpp
Comment thread scene/3d/physics/collision_shape_3d.cpp
Comment thread scene/2d/physics/collision_shape_2d.cpp Outdated
Comment thread scene/3d/physics/collision_shape_3d.cpp Outdated
Comment thread scene/3d/physics/collision_shape_3d.h
@tancop
Copy link
Copy Markdown
Author

tancop commented May 27, 2025

@mihe switched this to use shape owner methods can you take a look if i did it right

@tancop
Copy link
Copy Markdown
Author

tancop commented Jun 1, 2025

@Calinou looks like mihe doesnt have time to review. is anyone else free right now?

Copy link
Copy Markdown
Member

@Calinou Calinou left a comment

Choose a reason for hiding this comment

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

Tested again, it works as expected with GodotPhysics3D and Jolt Physics. Code and documentation look good to me.

I suggest having mihe take a final look at the API still, just in case. We can't change what's exposed once it lands in a stable release, so we need to make sure it's rock-solid.

@mihe

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@mihe mihe left a comment

Choose a reason for hiding this comment

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

I did another pass on this and found some bugs with the recent changes, which you'll find in the review comments. I also threw in some more nitpicks while I was at it.

I've also spent some more time thinking about the PhysicsServer*D API, and I think I've arrived at a better API for this:

func body_set_shape_friction_override(body: RID, shape_idx: int, friction: float) -> void
func body_get_shape_friction(body: RID, shape_idx: int) -> float
func body_is_shape_friction_overridden(body: RID, shape_idx: int) -> bool
func body_clear_shape_friction_override(body: RID, shape_idx: int) -> void

This gets rid of NAN from the API completely, by having body_get_shape_friction fall back on the body's friction when there is no override set, while still not burdening implementers with a separate enabled state.

To save us some time, I've gone ahead and made these changes already, and just like before you'll find the changes here (patch here), which also includes fixes for the new review comments.

Like before, if you have no objections to these changes, I can go ahead and push directly to your fork/branch as well. We're only a few days away from the 4.5 feature freeze, and I don't see anything else in this PR that should prevent this from being merged by then.

EDIT: Changes rebased on top of latest push (de20ef0).

Comment on lines +178 to +179
_FORCE_INLINE_ bool is_area() const { return area; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This isn't needed anymore.

Suggested change
_FORCE_INLINE_ bool is_area() const { return area; }

Comment on lines +180 to +181
_FORCE_INLINE_ bool is_area() const { return area; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same feedback as with 2D.

Suggested change
_FORCE_INLINE_ bool is_area() const { return area; }

Comment on lines +262 to +265
if (shape.is_null()) {
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This check doesn't make sense anymore.

Suggested change
if (shape.is_null()) {
return;
}

Comment on lines +254 to +257
if (shape.is_null()) {
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same feedback as with 2D.

Suggested change
if (shape.is_null()) {
return;
}

@@ -245,6 +252,28 @@ Color CollisionShape2D::get_debug_color() const {
return debug_color;
}

void CollisionShape2D::set_physics_material(const Ref<PhysicsMaterial> &p_material) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You're not disconnecting from the changed signal when the physics material is cleared. See CollisionShape2D::set_shape for reference.

Comment on lines +235 to +239
GDVIRTUAL_BIND(_body_set_shape_bounce_override, "body", "shape_idx", "enable", "bounce");
GDVIRTUAL_BIND(_body_set_shape_friction_override, "body", "shape_idx", "enable", "friction");

GDVIRTUAL_BIND(_body_get_shape_bounce_override, "body", "shape_idx");
GDVIRTUAL_BIND(_body_get_shape_friction_override, "body", "shape_idx");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Very nitpicky, but in most other places friction is listed before bounce.

Suggested change
GDVIRTUAL_BIND(_body_set_shape_bounce_override, "body", "shape_idx", "enable", "bounce");
GDVIRTUAL_BIND(_body_set_shape_friction_override, "body", "shape_idx", "enable", "friction");
GDVIRTUAL_BIND(_body_get_shape_bounce_override, "body", "shape_idx");
GDVIRTUAL_BIND(_body_get_shape_friction_override, "body", "shape_idx");
GDVIRTUAL_BIND(_body_set_shape_friction_override, "body", "shape_idx", "enable", "friction");
GDVIRTUAL_BIND(_body_set_shape_bounce_override, "body", "shape_idx", "enable", "bounce");
GDVIRTUAL_BIND(_body_get_shape_friction_override, "body", "shape_idx");
GDVIRTUAL_BIND(_body_get_shape_bounce_override, "body", "shape_idx");

Comment on lines +698 to +702
ClassDB::bind_method(D_METHOD("body_set_shape_bounce_override", "body", "shape_idx", "enable", "bounce"), &PhysicsServer2D::body_set_shape_bounce_override, DEFVAL(0.0));
ClassDB::bind_method(D_METHOD("body_set_shape_friction_override", "body", "shape_idx", "enable", "friction"), &PhysicsServer2D::body_set_shape_friction_override, DEFVAL(0.0));

ClassDB::bind_method(D_METHOD("body_get_shape_bounce_override", "body", "shape_idx"), &PhysicsServer2D::body_get_shape_bounce_override);
ClassDB::bind_method(D_METHOD("body_get_shape_friction_override", "body", "shape_idx"), &PhysicsServer2D::body_get_shape_friction_override);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same nitpick about the ordering here.

Suggested change
ClassDB::bind_method(D_METHOD("body_set_shape_bounce_override", "body", "shape_idx", "enable", "bounce"), &PhysicsServer2D::body_set_shape_bounce_override, DEFVAL(0.0));
ClassDB::bind_method(D_METHOD("body_set_shape_friction_override", "body", "shape_idx", "enable", "friction"), &PhysicsServer2D::body_set_shape_friction_override, DEFVAL(0.0));
ClassDB::bind_method(D_METHOD("body_get_shape_bounce_override", "body", "shape_idx"), &PhysicsServer2D::body_get_shape_bounce_override);
ClassDB::bind_method(D_METHOD("body_get_shape_friction_override", "body", "shape_idx"), &PhysicsServer2D::body_get_shape_friction_override);
ClassDB::bind_method(D_METHOD("body_set_shape_friction_override", "body", "shape_idx", "enable", "friction"), &PhysicsServer2D::body_set_shape_friction_override, DEFVAL(0.0));
ClassDB::bind_method(D_METHOD("body_set_shape_bounce_override", "body", "shape_idx", "enable", "bounce"), &PhysicsServer2D::body_set_shape_bounce_override, DEFVAL(0.0));
ClassDB::bind_method(D_METHOD("body_get_shape_friction_override", "body", "shape_idx"), &PhysicsServer2D::body_get_shape_friction_override);
ClassDB::bind_method(D_METHOD("body_get_shape_bounce_override", "body", "shape_idx"), &PhysicsServer2D::body_get_shape_bounce_override);

Comment on lines +347 to +349
if (sd.material == p_material) {
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is not correct, since it'll keep the same reference even when we change values within the material.

Suggested change
if (sd.material == p_material) {
return;
}

Comment on lines +579 to +581
if (sd.material == p_material) {
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same feedback as with 2D.

Suggested change
if (sd.material == p_material) {
return;
}

@@ -33,6 +33,7 @@
#include "godot_area_2d.h"
#include "godot_body_direct_state_2d.h"
#include "godot_constraint_2d.h"
#include "godot_physics_server_2d.h"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is not needed.

@mihe
Copy link
Copy Markdown
Contributor

mihe commented Jun 9, 2025

As another last-minute decision, I've omitted the body_is_shape_(friction|bounce)_overridden physics server methods from the API that I suggested above, because I struggle to see a use-case for such a query, and it's not something that's needed by the scene nodes anyway.

If anyone needs this in the future it can trivially be added then instead.

So the PhysicsServer*D API would look like this instead, which I'm mostly happy with:

func body_set_shape_friction_override(body: RID, shape_idx: int, friction: float) -> void
func body_clear_shape_friction_override(body: RID, shape_idx: int) -> void
func body_get_shape_friction(body: RID, shape_idx: int) -> float

The branch/patch linked above should have updated accordingly, but here (patch here) it is again if you need it.

EDIT: Changes have rebased on top of latest push (de20ef0).

@Xtarsia
Copy link
Copy Markdown

Xtarsia commented Jul 10, 2025

Does this have any chance of working with Heightmap Shapes? (I belive these construct a grid of Quad shapes under the hood, that are tested for in a specific way due to the nature of heightmaps.)

In Terrain3D I would like to be able to send a physics material "ID" for each Quad. Since We have access to an index map that could determine the dominant texture for any given quad, and that texture has other properties associated with it already, linking each texture ID (upto 32) to a physics material, would essentially Let us have paintable physics materials.

@mihe
Copy link
Copy Markdown
Contributor

mihe commented Jul 11, 2025

Does this have any chance of working with Heightmap Shapes? [...] In Terrain3D I would like to be able to send a physics material "ID" for each Quad.

@Xtarsia Not as-is, no. The same goes for individual triangles in ConcavePolygonShape3D. It's technically doable with Jolt, which supports this for both shapes, but it's not immediately clear to me how you'd go about implementing it for Godot Physics.

Either way, we'd need some way of actually assigning material values to groups of faces, both from a UX perspective and from an API perspective, which is likely quite a bit of work, and better suited for a future PR. The texture approach might make some sense for HeightMapShape3D, but I'm not sure it suits ConcavePolygonShape3D as well.

This lets users override bounce and friction for individual collision shapes instead of using
the same properties for the whole body.
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.

Add per-shape physics materials to the 3D physics system

10 participants