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

Add 2D CSG boolean operations #99911

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open

Add 2D CSG boolean operations #99911

wants to merge 1 commit into from

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.

Copy link
Member

@rburing rburing left a comment

Choose a reason for hiding this comment

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

This mostly looks great to me! I have just some small comments.

@smix8 smix8 force-pushed the csg2d branch 3 times, most recently from 251eeae to c2e075f Compare January 9, 2025 01:14
@smix8 smix8 requested a review from rburing January 9, 2025 23:35
@smix8 smix8 modified the milestones: 4.4, 4.5 Jan 14, 2025
@KoBeWi
Copy link
Member

KoBeWi commented Jan 23, 2025

Baking CollisionShape fails for simple case:
image
Either it should create ConcavePolygonShape2D here, or you could add an option for CollisionPolygon2D (or the option could automatically fallback to CollisionPolygon2D if shape fails).

Also the bigger rectangle for rectangle shape looks weird:
image
It makes sense for other shapes, because it allows to make e.g. ovals, but for rectangle it's redundant. They don't need to be scaled I think.

EDIT:
CSGCombiner has a weird gizmo that can be used to move it only left.
image
It appears only if it has 0 effective size.

EDIT2:
Something is wrong with debug_show_brush.
image
All 3 squares have the same size. Duplicating a shape with enabled debug will create shape with wrong debug. Not sure how the wrong-sized one got created.
EDIT4: It breaks when you change size and undo.

EDIT3:
I made a tetronimo from squares and combiner, and it has weird geometry.
image
idk if it's expected.

EDIT5:
When you bake a shape with vertex_color into Polygon2D, the polygon does not use the same color. Intended?

@smix8
Copy link
Contributor Author

smix8 commented Feb 6, 2025

I made a tetronimo from squares and combiner, and it has weird geometry.

That is because the editor tooling for Polygon2D is broken when using the multi polygons array instead of the single polygon array. Your shape is concave so it is multiple polygons and Polygon2D can not display that correctly in the editor.

Baking CollisionShape fails for simple case:

Simple is relative. Outline based boolean ops regularly "fail" on perfectly axis-aligned geometry due to precision and because the interpretation of the outline becomes a coin flip when things are perfectly symmetrical.

For the algorithm it does not matter if you only use 1-2 outlines with an expected "obvious result" or 100-200 outlines. If one outline gets interpreted wrong it has a chance to fail the convex decomposition. It is the failed convex decomp which results in no shapes being generated no matter what node type you are using. Like if you have broken outline data and convert it to a convex CollisionPolygon that is also set to use convex shapes you have the same problems.

Also the bigger rectangle for rectangle shape looks weird:

I think the scaling rect looks and feels bad for all shapes but I added it for consistency with other 2d toolings. In general I do not like scaling of 2d nodes, it all just adds up to causing precision problems especially in all those lowres pixel focused 2d games.

When you bake a shape with vertex_color into Polygon2D, the polygon does not use the same color. Intended?

If you bake to a mesh it copies the set vertex color over but by default color is set to white. The editor display of the CSG is very different not just in color, but also because it has edge line and face geometry.

@smix8 smix8 force-pushed the csg2d branch 3 times, most recently from 1f4d461 to eb709ac Compare February 12, 2025 12:53
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.

Some feedback:

  • Bake CollisionShape2D should be renamed to Bake CollisionShape2D(s), as it can create multiple collision shapes in a single operation if the collision shape is convex but the combiner's shape is concave. When the collision shape is concave, it will also occur if the CSG combiner has multiple disjoint shapes in it. This does not occur with other baking types (even Polygon2D, surprisingly).
  • Bake Polygon2D does not reuse the vertex color that was defined in CSGCombiner2D, so it resets to white. In comparison, the color is preserved when baking to MeshInstance2D.

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

smix8 commented Feb 13, 2025

This does not occur with other baking types ...

There are other bake types that can result into multiple shapes or nodes depending on settings and composition, e.g. the light occluders. I added them but frankly those (s) behind node names look visually poor, especially on buttons.

Bake Polygon2D does not reuse the vertex color ...

Added that if use_vertex_color is enabled on the CSG that the baked Poygon2D has that color copied into the vertex_colors property.

Copy link
Member

@AThousandShips AThousandShips left a comment

Choose a reason for hiding this comment

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

Will do a second more in-depth pass over the documentation as well

#ifndef CSG_2D_H
#define CSG_2D_H

#include "core/math/rect2.h"
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
#include "core/math/rect2.h"

Already included

virtual bool _edit_is_selected_on_click(const Point2 &p_point, double p_tolerance) const override;
#endif // DEBUG_ENABLED

void set_radius(const float p_radius);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
void set_radius(const float p_radius);
void set_radius(float p_radius);

void set_radius(const float p_radius);
float get_radius() const;

void set_height(const float p_height);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
void set_height(const float p_height);
void set_height(float p_height);

ADD_PROPERTY(PropertyInfo(Variant::INT, "corner_segments", PROPERTY_HINT_RANGE, "1,100,1"), "set_corner_segments", "get_corner_segments");
}

void CSGCapsule2D::set_radius(const float p_radius) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
void CSGCapsule2D::set_radius(const float p_radius) {
void CSGCapsule2D::set_radius(float p_radius) {

return radius;
}

void CSGCapsule2D::set_height(const float p_height) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
void CSGCapsule2D::set_height(const float p_height) {
void CSGCapsule2D::set_height(float p_height) {

A 2D CSG circle shape.
</brief_description>
<description>
A 2D CSG node that allows to create circle shapes or other regular convex polygons for use with the 2D CSG system.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
A 2D CSG node that allows to create circle shapes or other regular convex polygons for use with the 2D CSG system.
A 2D CSG node that allows creating circle shapes or other regular convex polygons for use with the 2D CSG system.

A 2D CSG node to structure other 2D CSG nodes.
</brief_description>
<description>
For complex arrangements of shapes, it is sometimes needed to add structure to CSG nodes. A CSGCombiner2D node allows to create this structure. The node encapsulates the result of the CSG operations of its children. In this way, it is possible to do operations on one set of shapes that are children of one CSGCombiner2D node, and a set of separate operations on a second set of shapes that are children of a second CSGCombiner2D node, and then do an operation that takes the two end results as its input to create the final shape.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
For complex arrangements of shapes, it is sometimes needed to add structure to CSG nodes. A CSGCombiner2D node allows to create this structure. The node encapsulates the result of the CSG operations of its children. In this way, it is possible to do operations on one set of shapes that are children of one CSGCombiner2D node, and a set of separate operations on a second set of shapes that are children of a second CSGCombiner2D node, and then do an operation that takes the two end results as its input to create the final shape.
For complex arrangements of shapes, it is sometimes needed to add structure to CSG nodes. A CSGCombiner2D node allows creating this structure. The node encapsulates the result of the CSG operations of its children. In this way, it is possible to do operations on one set of shapes that are children of one CSGCombiner2D node, and a set of separate operations on a second set of shapes that are children of a second CSGCombiner2D node, and then do an operation that takes the two end results as its input to create the final shape.

A 2D CSG mesh shape.
</brief_description>
<description>
A 2D CSG node that allows to create a shape for the 2D CSG system based on the geometry of a 2D rendering mesh resource.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
A 2D CSG node that allows to create a shape for the 2D CSG system based on the geometry of a 2D rendering mesh resource.
A 2D CSG node that allows creating a shape for the 2D CSG system based on the geometry of a 2D rendering mesh resource.

A 2D CSG polygon shape.
</brief_description>
<description>
A 2D CSG node that allows to create a polygon for use with the 2D CSG system. For CSG union and intersection operations the polygon can be arbitrarily shaped. For CSG subtraction operations the polygons must be non-self-overlapping.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
A 2D CSG node that allows to create a polygon for use with the 2D CSG system. For CSG union and intersection operations the polygon can be arbitrarily shaped. For CSG subtraction operations the polygons must be non-self-overlapping.
A 2D CSG node that allows creating a polygon for use with the 2D CSG system. For CSG union and intersection operations the polygon can be arbitrarily shaped. For CSG subtraction operations the polygons must be non-self-overlapping.

A 2D CSG rectangle shape.
</brief_description>
<description>
A 2D CSG node that allows to create a rectangle for use with the 2D CSG system.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
A 2D CSG node that allows to create a rectangle for use with the 2D CSG system.
A 2D CSG node that allows creating a rectangle for use with the 2D CSG system.

@KoBeWi
Copy link
Member

KoBeWi commented Feb 13, 2025

From the things I reported previously, only the polygon color was fixed.
You should fix at least debug brushes, because they are completely broken with undo and duplication, and the empty combiner gizmo.

You could also resolve my point about failed CollisionShape2D baking. Polygon2D is already an option, so fallback to CollisionPolygon2D is possible too. CollisionShape2D and PolygonShape2D are more or less interchangeable and you can use the same data as in Polygon2D.
image

I added it for consistency with other 2d toolings. In general I do not like scaling of 2d nodes

Then you don't need to add it ;)
Especially for CSGRectangle2D it's almost useless and looks ugly. Scaling is still possible without gizmos with the Scale Tool.

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
7 participants