Skip to content
Bruno edited this page Jun 15, 2024 · 11 revisions

Constructive Solid Geometry

The CSG Builder

Geogram's CSG subsystem is implemented in geogram/mesh/mesh_CSG.h. The main class is the CSGBuilder. It applies boolean operations to a set of meshes, with an API made compatible with OpenSCAD's .csg file format. OpenSCAD is a 3D modeler with a programming language that lets you create objects using basic primitives (spheres, boxes, ...), geometric transforms and boolean operations to combine them. OpenSCAD supports two file formats:

  • .scad, which is its native file format, with all the constructs of the programming language (for loops, conditional statements, functions, modules ...)
  • .csg, which is a file format with only a CSG tree (primitives, transforms and boolean operations).

Geogram's CSGBuilder follows (as closely as possible) OpenSCAD's .csg file format. The example program src/examples/geogram/compute_CSG has four examples ported from OpenSCAD.

The CSGBuilder manipulates CSGMeshes. A CSGMesh is a geogram Mesh plus some additional information used by the CSGBuilder bookkeeping (bounding box, caching...). The type CSGMesh_var corresponds to a smart pointer to a CSGMesh (does memory management for you).

The C++ code to generate the first four OpenSCAD examples (see figure) is as follows. It uses curly-braces constructors for matrices, vectors and CSGScopes (that is, a list of CSGMeshes). The syntax is very close to OpenSCAD .csg files (with the exception of union that writes union_instr in CSGBuilder parlance, because union is a reserved C++ keyword !).

    GEO::CSGMesh_var example001() {
        using namespace GEO;
        CSGBuilder B;
        return B.difference({
                B.sphere(25.0),
                B.multmatrix(
                    {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}},
                    { B.cylinder(62.5, 12.5, 12.5) }
                ),
                B.multmatrix(
                    {{1, 0, 0, 0}, {0, 0, -1, 0}, {0, 1, 0, 0}, {0, 0, 0, 1}},
                    { B.cylinder(62.5, 12.5, 12.5) }
                ),
                B.multmatrix(
                    {{0, 0, 1, 0}, {0, 1, 0, 0}, {-1, 0, 0, 0}, {0, 0, 0, 1}},
                    { B.cylinder(62.5, 12.5, 12.5) }
                )
            });
    }
    GEO::CSGMesh_var example002() {
        using namespace GEO;
        CSGBuilder B;
        return B.intersection({
                B.difference({
                        B.union_instr({
                                B.cube({30,30,30}),
                                B.multmatrix(
                                    {{1, 0, 0,  0},
                                     {0, 1, 0,  0},
                                     {0, 0, 1, -25},
                                     {0, 0, 0, 1}},
                                    {B.cube({15,15,40})}
                                )
                            }),
                        B.union_instr({
                                B.cube({50,10,10}),
                                B.cube({10,50,10}),
                                B.cube({10,10,50})
                            }),
                    }),
                B.multmatrix(
                    {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 5}, {0, 0, 0, 1}},
                    { B.cylinder(50, 20, 5) }
                )
            }); 
    }
    GEO::CSGMesh_var example003() {
        using namespace GEO;
        CSGBuilder B;
        return B.difference({
                B.union_instr({
                        B.cube({30, 30, 30}),
                        B.cube({40, 15, 15}),
                        B.cube({15, 40, 15}),
                        B.cube({15, 15, 40})
                    }),
                B.union_instr({
                        B.cube({50, 10, 10}),
                        B.cube({10, 50, 10}),
                        B.cube({10, 10, 50})
                    })
            });
    }
    GEO::CSGMesh_var example004() {
        using namespace GEO;
        CSGBuilder B;
        return B.difference({
                B.cube({30,30,30}),
                B.sphere(20)
            });
    }

To generate these meshes, one can use:

$ compute_CSG example001

(resp. example002,example003,example004). This will generate the result in out.meshb. Then, to visualize the result, use:

$ vorpaview out.meshb

The CSG Compiler

Geogram can interpret OpenSCAD's .csg files, thanks to the CSGCompiler class. The CSGCompiler reads a CSG tree either from a file or for a string, interprets it and generates a mesh. One can use the compute_CSG example program to interpret .csg files:

$ compute_CSG file.csg outputfilename

where outputfilename is in one of the formats supported by Geogram (.obj, .mesh, .meshb, .geogram ...). If no output filename is specified, then the default is out.meshb. If OpenSCAD is installed in the system, one can directly call compute_CSG with a .scad file (then compute_CSG calls OpenSCAD to convert it into the .csg format).

You can find example OpenSCAD files here. Some of them are used by the geogram testsuite.

Programatically, one can compile a CSG file as follows:

#include <geogram/mesh/mesh_CSG.h>
...
CSGCompiler CSG;
std::string input_filename = ...;
CSGMesh_var result = CSG.compile_file(input_filename);
...

Alternatively, one can compile a CSG tree given as a string, as follows:

std::string input_string = ...;
CSGMesh_var result = CSG.compile_string(input_string);

CSG options

  • Delaunay (default) or non-Delaunay: When triangulating intersecting facets, the builder can use a constrained Delaunay triangulation (activated by default), or just a constrained triangulation. Without Delaunay, it can be a bit faster, but then handling of co-planar facets is no longuer guaranteed. It is recommended to keep this option on. See Builder::set_delaunay().

  • Simplifying co-planar facets: when computing intersections of objects that have co-planar facets, it is interesting to merge the co-planar facets and re-triangulate them. One can specify a tolerance (in degrees) for the deviation between the normal vectors of facets to merge. See Builder::set_simplify_coplanar_facets()

  • Detect intersection in neighboring facets: it is quite unlikely that two triangles that share an edge or a vertex have an interrsection. By default, all intersections are tested, but one can gain a bit of time by skipping the pair of adjacent triangles in intersection tests. See Builder::set_detect_intersecting_neighbors()

See also...

The optional geogramplus expansion package

To gain more speed and more robustness in the extreme cases, read about GeogramPlus.

Clone this wiki locally