CSG¶
Boolean Operations on 3D Shapes.
▶ Run in browserTags: 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:
UNIONkeeps everything either shape covers.SUBTRACTcarves the child’s volume out of what came before.INTERSECTkeeps 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())