Skip to content

Conversation

smix8
Copy link
Contributor

@smix8 smix8 commented Dec 2, 2024

Adds nodes and functions for 2D CSG boolean operations.

Implements proposal godotengine/godot-proposals#3731

PR to-do / status

Detail

Core

  • Create csg_2d engine module.
  • Add boolean operations and brush logic for union, intersection and subtraction.
  • Add CSG node types for rectangle, circle, capsule, mesh resource, polygon edit and combiner.

Brushes

  • Add CSGCapsule2D brush builder.
  • Add CSGCircle2D brush builder.
  • Add CSGMesh2D brush builder.
  • Add CSGPolygon2D brush builder.
  • Add CSGRectangle2D brush builder.

CSG result

  • Add triangle mesh creation.
  • Add option for mesh UV creation.
  • Add option for mesh vertex color creation.

Physics

  • Add CSG root node collision using concave segments shapes.
  • Add CSG root node collision using convex polygons shapes.

Conversions

  • Add CSG result to MeshInstance2D node conversion.
  • Add CSG result to CollisionShape2D nodes conversion.
  • Add CSG result to Polygon2D node conversion.
  • Add CSG result to LightOccluder2D nodes conversion.

Editor and Tooling

  • Add Editor EditorPluginCSG2D.
  • Add Editor CSGShape2DEditor.
  • Add Editor menu options for CSG root node.
  • Add Editor tooling for CSGCapsule2D.
  • Add Editor tooling for CSGCircle2D.
  • Add Editor tooling for CSGPolygon2D.
  • Add Editor tooling for CSGRectangle2D.
  • Add CSG icons for 2D.

Documentation

  • Add class documentation for CSGCapsule2D.
  • Add class documentation for CSGCircle2D.
  • Add class documentation for CSGCombiner2D.
  • Add class documentation for CSGMesh2D.
  • Add class documentation for CSGPolygon2D.
  • Add class documentation for CSGPrimitive2D.
  • Add class documentation for CSGRectangle2D.

Polish

  • Code cleanup and restructuring.
  • Bug sweep.
  • Update PR description.

Wait, what is CSG?

csg2d_bake_01

To quote from the older 3D CSG blog that can be found here.

CSG stands for “Construtive Solid Geometry”, and is a tool to combine basic (and not so basic) shapes to create more complex shapes. In the 3D modelling software, CSG is mostly known as “Boolean Operators”.

So basically you can do everything what you can do currently in scripts using the Geometry2D merge and slice related boolean operation functions but more convenient with nodes. There are also plenty of extra features on top.

CSG Node types

Largely copies the workflow and nodes from the 3D CSG although accounting for some specific 2D quirks.

csg2d

  • CSGCapsule2D
    Capsule shape with radius, height and corner segment properties.
  • CSGCircle2D
    Basic circle with radius and radial segment properties to control detail and turn the shape into e.g. a hexa or with only 3 segments into a triangle.
  • CSGCombiner2D
    Does nothing itself other than combining csg children and stops the csg propagation for better organisation.
  • CSGMesh2D
    Accepts a rendering mesh resource that uses the ARRAY_FLAG_USE_2D_VERTICES format.
  • CSGPolygon2D
    Drawable polygon outline similar to e.g. Polygon2D node.
  • CSGRectangle2D
    Basic rectangle with size property.

Each CSG root node can optionally use_collision. Depending on the collision_shape_type property this will be either a single concave collision shape or multiple convex collision shapes.

Note that certain Editor toolbar menu bake options (see further below) require that collision is enabled. Without collision enabled some of the very expensive shape data is not created so it can not be exported.

CSG Operators

The available operators are the same as in 3D:

  • UNION
  • INTERSECTION
  • SUBTRACTION

Union merges the shape to the parent or higher sibling node shape while substraction cuts into them. The intersection is the weird one that removes all shape parts that are not found in the combined shapes.

Operations are done in node tree order same as in 3D, or to quote from the old CSG 3D blog.

Every CSG node will first process it’s children nodes (an their operation: union, intersection substraction), in tree order and apply them to itself one after the other.

Note that as with 3D the update of CSG shapes happens deferred. This is nessary because CSG results depend on other CSG nodes fully loaded and fully updated in the order for the final result. You can not spawn a CSG node and expect an immediate result. Either you wait for the nodes or you use the Geometry2D class for procedual stuff that has no node dependency. See related proposal godotengine/godot-proposals#10395 for improving the usability with scrips.

CSG root node and result

Only the CSG root note creates a result and has properties to customize it.

A CSG root node is either a CSGCombiner2D or any CSG Node that has no other CSG node as parent.

By default the result creates an indexed 2d triangle mesh for rendering visuals.
This rendering mesh has UV range mapped around the mesh Rect.

The option to add vertex colors to the mesh exists by enabling use_vertex_color property and picking the desired vertex_color.

Debug

Inside the Editor the CSG result will color the mesh faces as well as display the edges of the convex polygons. This is not rendered outside the editor where the mesh is just plain white by default.

With a lot of CSG brushes involved things can get confusing what shape and brush does what.

There is the option to display the CSG brush outline as debug colored by the operation type.

  • OPERATION_UNION -> Green
  • OPERATION_INTERSECTION -> Orange
  • OPERATION_SUBTRACTION -> Red

This option can be enabled per CSG node individually with the debug_show_brush property.
By default this debug is off to not clutter the editor view so much.

brush_debug

Baking CSG results to static geometry

csg_bake_options

Similar to 3D PR #93252

CSG options to bake the CSG root node result to a static mesh or collision shapes or other node types. This can be used to "design" a level or some shape geometry with CSG and then bake the result to a more efficient static version for performance (or to avoid seam issues).

Bake to MeshInstance2D

Creates a MeshInstance2D node with a 2d triangle ArrayMesh that has UV mapped same as mesh Rect and optional vertex colors if use_vertex_color is enabled.

Bake to CollisionShape2D

Creates multiple CollisionShape2D nodes with either convex polygon shapes or concave segments shapes depending on collision_shape_type.

Bake Polygon2D

Creates a Polygon2D node and adds the CSG vertices to the polygon array and the CSG convex polygons indices to the polygons array.

Note that the Editor tooling of Polygon2D is broken with multiple polygons. This is an issue with Polygon2D editor plugin and not with the CSG conversion.

Bake LightOccluder2D

Creates multiple LightOccluder2D nodes from the CSG outlines, The used OccluderPolygon2D resources are not closed as that would not work if there are any holes in the CSG shapes. So if you want some of the occluders closed you need to set that manually.

Bake NavigationRegion2D

Creates a NavigationRegion2D with a NavigationPolygon resource and sets the vertices and convex polygon indices same as the CSG result.

Bake with scripts

There is also the option to create all the resources in script, not creating or involving additional nodes.

var baked_mesh: ArrayMesh = CSGShape2D.bake_static_mesh()
# Visual 2d mesh with UV based on Rect size and optional vertex colors.

var baked_shapes: Array = CSGShape2D.bake_collision_shapes()
# 2d has no sensible single shape resource that works for more complex shapes
# so it needs to return an array that has multiple convex or concave shapes.

var baked_navigation_mesh: NavigationPolygon = CSGShape2D.bake_navigation_mesh()

var baked_light_occluders: Array = CSGShape2D.bake_light_occluders()
# An array of multiple OccluderPolygon2D.

Why name it CSG in 2D?

Although CSG "Construtive Solid Geometry” is more a name used in 3D modelling context I stayed with the name for 2D because users are already very familiar with the term in Godot from 3D. The 2D and 3D nodes and workflows are kept very similar on purpose as it allows better knowledge and documentation sharing.

Performance

Compared to the far more complex 3D CSG the 2D version has actually pretty good performance for what it does. Although I still would not recommend planning to use it with hundreds of changing sub nodes at runtime.

For rendering performance, if only the CSG root node transform is changed that costs basically nothing at runtime. The 2D CSG does not use the Node2D draw functions for the polygons or lines like many other 2d related nodes. The actual rendering geometry is baked to a single static 2d mesh in the end of the operations. So the entire performance cost of moving the CSG root node at runtime is a canvas_item_set_transform() call.

This is similar to what the navmesh baking does but the huge difference for runtime change performance is that the specialised CSG nodes can all catch their own intermediate result. So on changes only the parts that actually change up in the tree order need to be reparsed and recalculated instead of absolutely everything.

Help! My polygons are all breaking!

The boolean operations are done with the Clipper2 polytree backend base on polygon outline paths. As such all the usual polygon outline limitations apply that can be read in detail here https://angusj.com/clipper2/Docs/Robustness.htm.

These outlines need to be converted to either triangles or convex polygons in the end. Any kind of resulting overlap or crossed edges, e.g. due to float precision issues, can break those conversions so work with some margin in mind.

As always when dealing with outline to polygon conversions, dont (upscale) float precision fumble your layouts, avoid self-intersection at all time and never cross the (edge) streams in any way guys!

crossthestreams
Don't cross the (edge) streams!

The CSG can fix a lot of weird shapes due to various merge steps but if the source geometry has already grievous geometry errors the CSG chain still can break. This will be more a problem with the CSGPolygon2D and CSGMesh2D nodes as they allow the creation of all kinds of invalid geometry input. It can also happen when weird node scaling is used as this may cause vertices to end up in unintended rasterization cells when float positions are upscales back and forth. Same can happen when shapes that are perfectly aligned with shared vertices at corners in the editor. These kind of "pixel-perfectionist" layouts regularly stop to work the moment the float positions get upscaled as suddenly vertices may end up inside other shapes, either keep some error margin or create those shapes separated. You have been warned :)

@fire
Copy link
Member

fire commented Dec 2, 2024

Awesome, need some time to test and fix up the integration tests but looks promising.

@fire
Copy link
Member

fire commented Dec 2, 2024

There's some whitespace in #include "csg_2d.h"

@fire
Copy link
Member

fire commented Dec 10, 2024

csg brush is crashing which needs debugging but the doc change seems ok to do

@smix8 smix8 force-pushed the csg2d branch 6 times, most recently from 6aee9dc to 6283ef3 Compare December 10, 2024 15:52
Copy link
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 locally, it works as expected.

Testing project: test_csg_2d.zip

image

Some feedback:

  • When Use Vertex Color is disabled, the CSG node will have a different appearance in the editor compared to the running project:
Editor Running project
image image

When enabled, it has the same appearance in the editor and running project. I'm guessing this may occur because when Use Vertex Color is disabled, the shape is still drawn in white but is drawn behind the debug gizmos.

  • When Use Vertex Color is disabled, the Vertex Color property should be hidden in the inspector.

@smix8
Copy link
Contributor Author

smix8 commented Dec 10, 2024

When enabled, it has the same appearance in the editor and running project. I'm guessing this may occur because when Use Vertex Color is disabled, the shape is still drawn in white but is drawn behind the debug gizmos.

That half transparent blue hue face color is editor specific to not have just plain white solid shapes in the editor. It swaps that color on the mesh in the CSGShape2D::draw_shape() function based on the editor hint and the use vertex color being enabled. Same with the outline or inner polygon edge line debug that is not added at runtime.

When Use Vertex Color is disabled, the Vertex Color property should be hidden in the inspector.

Done.

@smix8
Copy link
Contributor Author

smix8 commented Dec 11, 2024

Fixed a few things and added some additional debug visuals.
It was getting confusing with more CSG brushes involved what brush does what.

There is now the option to display the CSG brush outline as debug colored by the operation type.

  • OPERATION_UNION -> Green
  • OPERATION_INTERSECTION -> Orange
  • OPERATION_SUBTRACTION -> Red

This option can be enabled per CSG node individually with the debug_show_brush property.
By default this debug is off to not clutter the editor view so much.

brush_debug

@smix8
Copy link
Contributor Author

smix8 commented Dec 11, 2024

Pushed the last addition / changes to the PR. I think it is now good for review and testing.

@smix8 smix8 force-pushed the csg2d branch 2 times, most recently from bf6388f to 7856567 Compare June 9, 2025 22:04
@smix8
Copy link
Contributor Author

smix8 commented Jun 9, 2025

I rebased to not be 3500++ PRs behind master (time flies).

  • Addressed the documentation comments from ship (I think?)
  • Removed the Node2D rect related scaling boxes.

I have not found the right place yet where it may miss a queue redraw or something that causes the debug wit the debug brush sometimes not updating on node duplicates.

Not sure how to address kobi's comments on the collision bake breaking sometimes. The breaking of "simple" looking boolean ops is a very general issue with clip operations that involve convex decomp at the end. We have that issue for Geometry2D, navmesh baking, tilemap collision baking, ... everything that is using a convex decomp that is very brittle to overlap issues. I fear this PR will not be able to change much about this as it is more a core problem.

The fallback to CollisionPolygon2D is imo not a solution to do by default because that polygon is just a visual outline that also need to convert to convex polygons or segments when used as collision. If a user sets it to convex it will break either way when the input breaks it as it uses the same convex decomp behind the scene as the csg. The only reason why it may seem to not break is because all that back and forth might fix some tiny float errors but it is not a real fix, it just hides the same issue by luck. The mode that avoids all that is the segment collision is it does not usse the convex decomp. May add CollisionPolygon2D as a general node option to the menu though, just not as a default fallback.

@akien-mga akien-mga requested review from rburing and KoBeWi June 10, 2025 09:20
@KoBeWi
Copy link
Member

KoBeWi commented Jun 11, 2025

The fallback to CollisionPolygon2D is imo not a solution to do by default because that polygon is just a visual outline that also need to convert to convex polygons or segments when used as collision. If a user sets it to convex it will break either way when the input breaks it as it uses the same convex decomp behind the scene as the csg.

Right now there is no way to make CollisionPolygon2D at all. You need to make Polygon2D and then change type, afterwards the polygon works correctly. I don't see why this can't be an option. The reason conversion "fails" is because CollisionShape2D is very limited. You can't bake e.g. a triangle, which is a perfectly valid shape.
image

EDIT:
Actually baking shape seems to be completely broken.

godot.windows.editor.dev.x86_64_sajfugOaUo.mp4

idk what I need to make it create a shape.

EDIT2:

I have not found the right place yet where it may miss a queue redraw or something that causes the debug wit the debug brush sometimes not updating on node duplicates.

It fails to draw because brush_mesh() is not initialized. Calling force_update_shape() fixes it.

@smix8
Copy link
Contributor Author

smix8 commented Jun 11, 2025

@KoBeWi silly question but did you forget to enable use_collision in your tests on the root node?

Note that certain Editor toolbar menu bake options (see further below) require that collision is enabled. Without collision enabled some of the very expensive shape data is not created so it can not be exported.

Because without use_collision enabled I get the very same bake errors as you with that same node and shape setup. With collision enabled it all works just fine for me.

@KoBeWi
Copy link
Member

KoBeWi commented Jun 11, 2025

This needs to be mentioned somewhere. Also collision baking should be disabled when use_collision is disabled, if it does nothing.

}

void CSGShape2D::force_shape_update() {
_update_shape();
Copy link
Member

Choose a reason for hiding this comment

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

Now you could just make _update_shape() public.

@akien-mga
Copy link
Member

Moving this to the 4.6 milestone as we're entering feature freeze.
Implementation wise this PR seems mostly ready so it could nearly have made it into 4.5 before freeze, but after discussion we think that we need some more time to evaluate how needed this feature is in the first place.

The proposal it implements got a total of 4 upvotes in 4 years, which isn't much. This PR has significantly higher success but it also just sounds cool 😎 which doesn't necessarily correlate an actual need for users.

We should also evaluate this in context of other related proposals such as godotengine/godot-proposals#1126 (which on the other hand has very high demand) and #100574 and #99210 which implement it (and conversely have seen less organic engagement than this PR).

All this to say, this might very well be the right tool for the job, but let's make sure this is properly assessed and solves the needs from the community.

@akien-mga akien-mga modified the milestones: 4.5, 4.6 Jun 12, 2025
@akien-mga akien-mga requested a review from kitbdev June 12, 2025 07:30
@smix8 smix8 force-pushed the csg2d branch 4 times, most recently from af1431f to 7f6a8ba Compare July 19, 2025 10:22
@smix8
Copy link
Contributor Author

smix8 commented Jul 19, 2025

This needs to be mentioned somewhere. Also collision baking should be disabled when use_collision is disabled, if it does nothing.

Changed it so that when no csg collision exists or is enabled that the editor menu option and function create all the collision shapes procedural.

@KoBeWi
Copy link
Member

KoBeWi commented Jul 19, 2025

The debug_show_brush bug I mentioned earlier still exists, and there is a similar bug where if you duplicate a shape under CSGCombiner, the CSG menu will appear while it shouldn't. Though I think this can be ironed out later, all other problems seem to be resolved.

Adds nodes and functions for 2D CSG boolean operations.
@kitbdev
Copy link
Contributor

kitbdev commented Jul 21, 2025

The proposal is lacking detail and mentions destructive terrain as a use case, but that was already called out as not being a good fit. So the use case for this seems to be prototyping, just like 3D CSG.
But I don't think it is really needed in 2D.
I feel like 2D CSG isn't as popular as 3D CSG since there are other easy ways to make 2D shapes. We have Polygon2D which can already be used to create custom shapes. Also sprites can be used instead in many cases.

For basic shapes, godotengine/godot-proposals#1126, I think either #100574 or #99210 are a better fit. Those approaches are simpler, easier to use, and don't have the performance considerations that this does.
For more complex shapes, you can use multiple shapes together and Polygon2D.
If the user needs it to be a single combined 2D mesh and Polygon2D won't work for some reason, then it's more niche or not really a prototype anymore.

I'm sure 2D CSG is good for some workflows, so I think 2D CSG would be a better fit as an addon. As mentioned, the base functionality for it is already available through Geometry2D, and it is very self-contained.

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 2D CSG nodes
9 participants