Skip to content

Add Tapered collision shapes - #116707

Open
Moddingear wants to merge 10 commits into
godotengine:masterfrom
Moddingear:tapered_capsules
Open

Moddingear wants to merge 10 commits into
godotengine:masterfrom
Moddingear:tapered_capsules

Conversation

@Moddingear

@Moddingear Moddingear commented Feb 24, 2026 •

Copy link
Copy Markdown

This is the continuation of #109009

I've fixed the mesh generation of the tapered capsule, and right now am looking at also adding tapered cylinders.

Bugsquad edit:

Comment thread doc/classes/TaperedCapsuleMesh.xml Outdated
Comment thread scene/resources/3d/tapered_capsule_mesh.cpp Outdated
Comment thread scene/resources/3d/tapered_capsule_mesh.cpp Outdated
Comment thread scene/resources/3d/tapered_capsule_mesh.cpp Outdated
Comment thread scene/resources/3d/tapered_capsule_mesh.h Outdated
Comment thread scene/resources/3d/tapered_capsule_shape_3d.cpp Outdated
Comment thread scene/resources/3d/tapered_capsule_shape_3d.cpp Outdated
@Moddingear

Moddingear commented Feb 24, 2026 •

Copy link
Copy Markdown
Author

Here is a video demo:

Screencast.From.2026-02-24.14-24-54.mp4

The UVs on the top of the tapered cylinder aren't quite correct yet

@Moddingear

Moddingear commented Feb 24, 2026 •

Copy link
Copy Markdown
Author

godotengine/godot-proposals#610 is implemented by this PR

Just to clarify : I haven't yet checked the physics in game, there may be a mismatch between what's seen and what's collided with

@Moddingear

Copy link
Copy Markdown
Author

sorry about the wrong styles, I hadn't setup everything to have it run automatically, this is my first contrib...

@Moddingear

Copy link
Copy Markdown
Author

For completeness: this also addresses the original proposal, godotengine/godot-proposals#12875

@Moddingear
Moddingear marked this pull request as draft February 25, 2026 14:07
@Moddingear

Copy link
Copy Markdown
Author

Physics have been made and tested to work with both Godot physics server (using regular non-tapered variants) and Jolt

tapered_physics.mp4

@Moddingear
Moddingear marked this pull request as ready for review February 25, 2026 14:43
@fire
fire requested a review from aaronfranke February 25, 2026 15:51
Comment thread doc/classes/PhysicsServer3DExtension.xml Outdated
@Moddingear

Copy link
Copy Markdown
Author

I'll note here that a lot of the code between the tapered Cylinder and tapered Capsule is very similar, maybe it could be merged into a single parent class?

Also, I haven't made icons for the meshes or the shapes. I tried to have a go at it, but in the end it was programmer art...

Finally, maybe there could be a button to convert non-tapered cylinder/capsules into their tapered variants and vice-versa?

@aaronfranke

Copy link
Copy Markdown
Member

@Moddingear Please run --doctool after compiling Godot to update the documentation automatically.

@aaronfranke aaronfranke 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.

Looks pretty good from my perspective. Needs these property info changes, and a rebase+squash, and approval from physics folks.

Comment thread scene/resources/3d/tapered_capsule_shape_3d.cpp Outdated
Comment thread scene/resources/3d/tapered_capsule_mesh.cpp Outdated
@Moddingear
Moddingear force-pushed the tapered_capsules branch 2 times, most recently from 02e46a7 to 0ab802f Compare February 26, 2026 17:26
@Moddingear

Copy link
Copy Markdown
Author

I did the squash+rebase, but I kept the original commit from #109009 to keep their contribution

@Moddingear

Copy link
Copy Markdown
Author

Last change is so that the tapered capsule, both mesh and collision, correctly handle being turned into spheres. There was a superfluous test to avoid that, but Jolt actually handles the case just fine...

@smix8

smix8 commented Jul 20, 2026 •

Copy link
Copy Markdown
Contributor

The issue I see with that is one of migration, the current capsules use radius and height, but tapered capsules use top/bottom radius and mid_height, same for cylinders. Is there a way for the update to not break stuff ?

You could add a tapered_enabled bool on the shape/mesh that toggles the new properties for tapered in the inspector to avoid the user interface clutter and have an easy way to detect if a geometry is used as tapered or not.

Then add those tapered specific properties that can not be shared / reused from the normal version as extra properties to the Resource. E.g. in case of cylinders add top_radius and bottom_radius but keep the height as height. When a user toggles the tapered_enabled on and off all that visibly should change is the tapered part of the shape but the height should stay the same no matter how it is internally calculated.

Assuming we have such a tapered bool to detect, you can update the Mesh::create_mesh_array() code of the related mesh primitives and branch internal so that it creates the old version or the new tapered version depending on what is used. That way you dont need to bother updating each parser tool individual as they will just get whatever version is set.

To sum up.

  • add properties to existing, related shapes and mesh primitives with a toggle tapered_enabled and keep tapered default disabled.
  • Reuse existing properties that make sense, add new when required. Keep the overall shape size the same when toggled, see height example above.
  • update any geometry functions of those existing resources to handle tapered_enabled.
    E.g.
    Shape3D::get_debug_mesh_lines()
    Shape3D::get_debug_arraymesh_faces()
    Mesh::create_mesh_array()
    ...
  • update server internals to branch between normal and tapered
    when it matters like for jolt, in case of unsupported like GodotPhysics3D throw the warning if tapered is used
  • update documentation and documentation images yada-yada
    You can join dev chat and documentation channel if you need help with any of that and/or poke skyace65 for doc specifically.

@Moddingear

Copy link
Copy Markdown
Author

I've had to rework the way gizmo handles work so that the gizmos can allow switching between tapered and non-tapered.
This implementation removes the translation on the gizmo for the height handles.

Screencast.From.2026-07-31.14-11-44.mp4

Non-tapered shapes support being represented by tapered shapes, and vice-versa (though, when a tapered shape is represented by a non-tapered shape, the collision doesn't match ofc).

@Moddingear

Copy link
Copy Markdown
Author

It now uses non-tapered shapes when top_radius and bottom_radius are the same.

There is an accessor for radius, which sets both top_radius and bottom_radius to the same value, and when read, gives the average of both.

Tapered shapes have 6 longitudinal lines, as opposed to non-tapered shapes which have 4, so they can easily be told apart.

I haven't tested migrating cylinder shapes from 4.7 to this version.

@fire

fire commented Jul 31, 2026

Copy link
Copy Markdown
Member
Some sort of compile error. Click to expand logs.
  ./scene/resources/3d/primitive_meshes.cpp:426:6: error: no declaration matches 'void CapsuleMesh::create_mesh_array(Array&, float, float, int, int, bool, float)'
    426 | void CapsuleMesh::create_mesh_array(Array &p_arr, const float radius, const float height, const int radial_segments, const int rings, bool p_add_uv2, const float p_uv2_padding) {
        |      ^~~~~~~~~~~
  In file included from ./modules/gltf/tests/test_gltf_extras.h:44,
                   from ./modules/modules_tests.gen.h:45,
                   from ./tests/test_main.cpp:78,
                   from tests/.scu/scu_tests_exception_1.gen.cpp:1:
  ./scene/resources/3d/primitive_meshes.h:154:21: note: candidates are: 'static void CapsuleMesh::create_mesh_array(Array&, real_t, real_t, real_t, int, int, bool, real_t)'
    154 |         static void create_mesh_array(Array &p_arr, real_t p_radius_top, real_t p_radius_bottom, real_t p_mid_height, int p_radial_segments = 64, int p_rings = 16, bool p_add_uv2 = false, const real_t p_uv2_padding = 1.0);
        |                     ^~~~~~~~~~~~~~~~~
  ./scene/resources/3d/primitive_meshes.h:152:21: note:                 'static void CapsuleMesh::create_mesh_array(Array&, real_t, real_t, int, int, bool, real_t)'
    152 |         static void create_mesh_array(Array &p_arr, real_t p_radius, real_t p_height, int p_radial_segments = 64, int p_rings = 16, bool p_add_uv2 = false, const real_t p_uv2_padding = 1.0);
        |                     ^~~~~~~~~~~~~~~~~
  In file included from ./modules/gltf/tests/test_gltf_extras.h:44,
                   from ./modules/modules_tests.gen.h:45,
                   from ./tests/test_main.cpp:78,
                   from tests/.scu/scu_tests_exception_1.gen.cpp:1:
  ./scene/resources/3d/primitive_meshes.h:127:7: note: 'class CapsuleMesh' defined here
    127 | class CapsuleMesh : public PrimitiveMesh {
        |       ^~~~~~~~~~~
  scons: *** [tests/.scu/scu_tests_exception_1.gen.linuxbsd.editor.dev.double.x86_64.san.o] Error 1
  ranlib modules/libmodules.linuxbsd.editor.dev.double.x86_64.san.a
  scons: building terminated because of errors.
  INFO: Time elapsed: 00:35:38.51
  Error: Process completed with exit code 2.

@smix8

smix8 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

The shape resources with the new tapered properties combined look mostly fine. Combining that tapered feature with existing shape types implied doing it on the PhysicsServer API and extension layer as well.

Right now those server and extension files have extra entries in enums and functions that should not exist. It should not concern the user at the server API level if they use e.g. a normal capsule or a tapered capsule, it is all just capsule with some extra tapered properties.

Now there are valid reason to handle them different (mostly performance) but that is something the physics engine backend can and should internally branch to more optimized paths if it detects certain properties in use. It is not a concern of the front server API to do this technical split.

So virtual RID tapered_capsule_shape_create() = 0; and virtual RID tapered_cylinder_shape_create() = 0; and everything related should be removed and wrapped into the existing and only branches if necessary on the internals of the module code, aka inside GodotPhysics or Jolt.

@Moddingear

Copy link
Copy Markdown
Author

@smix8 Removing that would mean either always creating tapered shapes for engines that can, or transferring a lot more data to be, in fine, discarded.

Got a preference on how to do that ?

@Moddingear

Copy link
Copy Markdown
Author

Regarding data to transfer, I see two way:

  1. All the data
    • Height/mid height
    • Radius
    • Radius top
    • Radius bottom
  2. Differential coding
    • Height/mid height
    • Radius
    • Radius difference (Radius top - Radius bottom)

Option 2 is a bit more elegant but less straightforward

@smix8

smix8 commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

I am not sure I follow with the "always creating more data".

The only place you need to send geometry data is with the shape_set_data() and that is a Variant that is a Dictionary for the shapes with more than one property.

So you can add key-value pairs for tapered related properties when tapered is enabled and used on that shape and leave them out when not. Everything else is up to the physics backend and does not concern the core physics server directly, e.g. falling back to the simpler default shape when the tapered keys are not found in the dict.

On creation of e.g. a capsule shape all that is happening (or should) is a request for a RID handle in the shape rid pool that sets the type to e.g. capsule shape. The shape has no real data at that point (outside some almost useless default values) and it does not know what it really wants to be until the first shape_set_data() call that defines the actual shape geometry.

@Moddingear

Copy link
Copy Markdown
Author

@smix8 Coming back on this now:
Capsule used to require height and radius. Requiring mid_height, top_radius and bottom_radius are better arguments to make a capsule since they aren't linked parameters; The new shapes use these parameters, but the old ones are also included in the dictionary for backwards-compatibility.

This also means that setting height before radius isn't the same as setting height after radius, but it was the same before.

IMO this PR now accomplishes what you asked.

@smix8

smix8 commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

The way to avoid this change of properties and magic radius calculation that will create compatibility problems and make users trip is to add the tapered_enabled toggle that we talked about early.

It is okay when users need to click a single bool to toggle a new and more niche feature. Lots of Resources do this in the Editor for their more complex stuff.

You can rename your current tapered and is_tapered() to ...

  1. member to bool tapered_enabled = false;
  2. setter to tapered_set_enabled(bool p_enabled)
  3. getter to bool tapered_is_enabled()

That way ...

  • The old and the new properties are kept without conflict with zero compatibility problems because tapered will be disabled by default and the old properties and their values still stored on the Resources.
  • No need to compare various values for diff which is brittle, just check a bool.
  • Physics engine and similar backends don't need to do magic to detect. They can check tapered_is_enabled() to branch between normal shape handing and supported new tapered shape handling. Those that dont want to update and support tapered can just ignore it as the old properties are still the same.
  • In the property validation for the Editor Inspector you can check if tapered_enabled == true and if it is not you can hide all the properties that are only required for tapered shapes to de-clutter the interface.

@Moddingear

Copy link
Copy Markdown
Author

@smix8 I've added what you wanted, but this makes it worse:

  • There are now more variables serialized, and they're fighting for the same data:
    • Radius clashes with top_radius and bottom_radius on load
    • Checking for equality between floats is stable when they're both set to the same value by assignment
    • Now, 4 variables are fighting
  • Cylinder gizmo is broken for some reason

Please reconsider the implications of what you're asking. A checkbox doesn't make it better.

Also, I haven't found a way to show/hide parameters based on that boolean. Only thing I found was to hide a whole group, but this isn't what we want here.

Perhaps this change is better done as a editor-only change, that is only the controls change, but behind the hood, there is only top_radius, bottom_radius and mid_height.

Comment thread doc/classes/CapsuleMesh.xml Outdated
Comment thread doc/classes/CapsuleShape3D.xml Outdated
Comment thread modules/godot_physics_3d/godot_physics_server_3d.cpp Outdated
Comment thread scene/resources/3d/capsule_shape_3d.h Outdated
Comment thread scene/resources/3d/cylinder_shape_3d.h Outdated
@fire

fire commented Aug 28, 2026 •

Copy link
Copy Markdown
Member

Radius clashes with top_radius and bottom_radius on load
Checking for equality between floats is stable when they're both set to the same value by assignment

You can do a tiered system similar to the problem in MeshInstance3D which needs to get the material but there's a material in the mesh, a material in the meshinstance3d's list of material overrides and the base class's default material.

For your tier list of:

  • radius
  • top_radius and bottom_radius

You can use the condition of checking if property is set to the default value or not to choose the tiers.

Alternatively, it might be legal to not serialize the old properties and only serialize the new ones.

Co-authored-by: Aaron Franke <arnfranke@yahoo.com>
Co-authored-by: K. S. Ernest (iFire) Lee <ernest.lee@chibifire.com>
…nify gizmo handles across Capsule and Cylinders
@Moddingear

Copy link
Copy Markdown
Author

I've undone the last changes, as I haven't been able to make them work like you asked (that is, i've reverted to top_radius, bottom_radius and height/mid_height).

It's also rebased to master.

The documentation for CapsuleMesh get_tangent_angle doesn't work for some reason.

Comment thread modules/godot_physics_3d/godot_physics_server_3d.h
Comment thread doc/classes/CapsuleMesh.xml Outdated
Comment thread doc/classes/CapsuleMesh.xml Outdated

@AThousandShips AThousandShips 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.

Otherwise the style looks good, haven't tested

Comment thread editor/icons/TaperedCapsuleMesh.svg Outdated
@NoctemCat

NoctemCat commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

You can use _validate_property to customize the existing properties by changing its usage flags inside it, like should it be shown in the editor or should it be serialized or not, I would recommend looking at the existing code to see how it is used (the docs are also should be correct if mapped to C++). That's what this part meant:

In the property validation for the Editor Inspector you can check if tapered_enabled == true and if it is not you can hide all the properties that are only required for tapered shapes to de-clutter the interface.

I'm not sure what approach is best when it comes to handling clashing properties that maps to the same underlying variable, one approach is to use _validate_property to also handle what should be serialized and pick not clashing combinations that are toggled by bool, another is to ignore values directly in the setters based on the current type, I can't remember if it is safe to rely on the order of serialized properties so you know the current type is always deserialized first.

For the data transfer you don't need to always send all the data, for example:

void CylinderShape3D::_update_shape() {
	Dictionary d;
	d.reserve(is_tapered() ? 5 : 2);
	d["radius"] = get_radius();
	d["height"] = get_height();
	if(is_tapered()) {
		d["tapered"] = is_tapered();
		d["radius_top"] = get_top_radius();
		d["radius_bottom"] = get_bottom_radius();
	}
	PhysicsServer3D::get_singleton()->shape_set_data(get_shape(), d);
	Shape3D::_update_shape();
}

And then you will only extract the new properties if the "tapered" actually exists and is true, same for get_data. I'm not sure what is better here, always sending "tapered" or not sending it when disabled, and checking if the key exists inside shape_set_data. I guess this will also be better for compatibility if the physics server could parse the old data, it just needs to be able to parse it correctly, it can still use the new code if it is easier to implement that way. I remember some plugins creating shapes directly with the physics server, like Terrain3D using heightmap_shape_create, which is, yeah, not cylinder_shape_create, but someone can actually still use cylinder_shape_create and passing the old data to the shape_set_data. Nevermind I think it handles it, but get_data can send less data back if it detected the old data format.

@Moddingear

Copy link
Copy Markdown
Author

@NoctemCat I've added the requested _validate_property for the shapes. They now have a tapered checkbox, that only shows and saves radius when disabled, and shows and save top_radius and bottom_radius when enabled.

Gizmos are still not the best but they work.

@Moddingear

Copy link
Copy Markdown
Author

Will squash everything once it's all okay'ed

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 tapered capsule as mesh and physics shape Add a ConeShape primitive 3D collision shape