CSG

Boolean Operations on 3D Shapes.

▶ Run in browser

Tags: 3d

Constructive solid geometry builds a shape by combining simpler ones. A CSGCombiner3D takes CSG shape nodes as children: the first child seeds the volume, and each later child is merged into it by its own operation, a CSGOperation:

  • UNION keeps everything either shape covers.

  • SUBTRACT carves the child’s volume out of what came before.

  • INTERSECT keeps only the volume both share.

Reading combiner.mesh evaluates the booleans and returns an ordinary Mesh, which a MeshInstance3D renders like any other::

combiner = CSGCombiner3D()
combiner.add_child(CSGBox3D(size=(2, 2, 2)))
combiner.add_child(CSGSphere3D(radius=1.3, operation=CSGOperation.SUBTRACT))
self.add_child(MeshInstance3D(mesh=combiner.mesh, material=Material(colour=(0.9, 0.3, 0.2))))

The combiner is a builder here and never enters the tree. The scene shows the same box and sphere under each of the three operations, left to right, each named by a Text3D label that floats above it. An OrbitCamera3D spins slowly until the first press, drag, scroll or key, and then follows the player: drag or the right stick orbits, scroll, pinch or the triggers zoom, and the arrow keys orbit through a demo action that calls camera.orbit().

Source

  1# /// simvx
  2# web = { width = 1280, height = 720 }
  3# ///
  4"""CSG: Boolean Operations on 3D Shapes.
  5
  6Constructive solid geometry builds a shape by combining simpler ones. A
  7``CSGCombiner3D`` takes CSG shape nodes as children: the first child seeds the
  8volume, and each later child is merged into it by its own ``operation``, a
  9``CSGOperation``:
 10
 11- ``UNION`` keeps everything either shape covers.
 12- ``SUBTRACT`` carves the child's volume out of what came before.
 13- ``INTERSECT`` keeps only the volume both share.
 14
 15Reading ``combiner.mesh`` evaluates the booleans and returns an ordinary
 16``Mesh``, which a ``MeshInstance3D`` renders like any other::
 17
 18    combiner = CSGCombiner3D()
 19    combiner.add_child(CSGBox3D(size=(2, 2, 2)))
 20    combiner.add_child(CSGSphere3D(radius=1.3, operation=CSGOperation.SUBTRACT))
 21    self.add_child(MeshInstance3D(mesh=combiner.mesh, material=Material(colour=(0.9, 0.3, 0.2))))
 22
 23The combiner is a builder here and never enters the tree. The scene shows the
 24same box and sphere under each of the three operations, left to right, each
 25named by a ``Text3D`` label that floats above it. An ``OrbitCamera3D`` spins
 26slowly until the first press, drag, scroll or key, and then follows the
 27player: drag or the right stick orbits, scroll, pinch or the triggers zoom,
 28and the arrow keys orbit through a demo action that calls ``camera.orbit()``.
 29"""
 30
 31import math
 32
 33from simvx.core import (
 34    Anchor,
 35    Axis2D,
 36    CanvasLayer,
 37    CSGBox3D,
 38    CSGCombiner3D,
 39    CSGOperation,
 40    CSGSphere3D,
 41    DirectionalLight3D,
 42    Input,
 43    Label,
 44    Material,
 45    MeshInstance3D,
 46    Node,
 47    OrbitCamera3D,
 48    PointerEvent,
 49    Text3D,
 50)
 51from simvx.graphics import App
 52
 53IDLE_SPIN = math.radians(8.0)
 54DEMOS = [
 55    (CSGOperation.UNION, (0.2, 0.6, 0.9)),
 56    (CSGOperation.SUBTRACT, (0.9, 0.3, 0.2)),
 57    (CSGOperation.INTERSECT, (0.2, 0.8, 0.3)),
 58]
 59
 60
 61def box_and_sphere(operation: CSGOperation):
 62    """A 2-unit box combined with a sphere slightly larger than its half-width."""
 63    combiner = CSGCombiner3D()
 64    combiner.add_child(CSGBox3D(size=(2, 2, 2)))
 65    combiner.add_child(CSGSphere3D(radius=1.3, rings=20, sectors=20, operation=operation))
 66    return combiner.mesh
 67
 68
 69class CSGDemo(Node):
 70    input_context = {"orbit": [Axis2D.ARROWS]}
 71
 72    def on_ready(self):
 73        self.idle = True
 74        self.camera = self.add_child(
 75            OrbitCamera3D(
 76                distance=12.0,
 77                yaw=math.radians(30),
 78                pitch=math.radians(-25),
 79                min_pitch=math.radians(-80),
 80                max_pitch=math.radians(10),
 81                min_distance=5.0,
 82                max_distance=30.0,
 83                stick_speed=math.radians(60),
 84                fov=55,
 85            )
 86        )
 87        self.add_child(DirectionalLight3D(intensity=1.2, direction=(-1, -2, -1)))
 88        self.add_child(DirectionalLight3D(intensity=0.4, colour=(0.5, 0.6, 1.0), direction=(1, -1, 2)))
 89
 90        for i, (operation, colour) in enumerate(DEMOS):
 91            shape = self.add_child(
 92                MeshInstance3D(
 93                    name=operation.name.title(),
 94                    mesh=box_and_sphere(operation),
 95                    material=Material(colour=colour, roughness=0.35, metallic=0.3),
 96                    position=((i - 1) * 5.0, 0, 0),
 97                )
 98            )
 99            shape.add_child(Text3D(text=operation.name, position=(0, 2.2, 0), font_scale=3.0))
100
101        # Example help overlay: not part of the technique shown.
102        hint = "Drag, right stick or arrow keys: orbit   Scroll, pinch or triggers: zoom"
103        self.add_child(CanvasLayer()).add_child(Label(hint, anchor=Anchor.BOTTOM_LEFT, inset=10, font_size=16))
104
105    def on_input(self, event):
106        # Anything but a pointer merely passing over the window hands the camera to the player.
107        if not (isinstance(event, PointerEvent) and not event.contact):
108            self.idle = False
109
110    def on_update(self, dt):
111        # Up raises the camera, and a negative pitch is above the pivot, so the axis maps straight across.
112        keys = Input.value("orbit")
113        self.camera.orbit(-keys.x * self.camera.stick_speed * dt, keys.y * self.camera.stick_speed * dt)
114        if self.idle:
115            self.camera.orbit(IDLE_SPIN * dt, 0.0)
116
117
118if __name__ == "__main__":
119    App(title="CSG Boolean Operations", width=1280, height=720).run(CSGDemo())