Mouse Picking¶
click cubes to change their colour.
▶ Run in browserTags: 3d
Picking is built into the scene tree: give a node a CollisionShape3D child
with pickable=True and override on_picked. Every press the UI does not
take casts a ray from the camera through the pointer, and the owner of the
nearest pickable shape it meets receives a PickEvent:
def on_picked(self, event):
if event.button in Pointer.PRIMARY:
print(event.node, event.point, event.normal, event.distance)
event.button is whichever button or finger pressed, so check it for the
action you mean; here the primary press (left mouse, a tap, a pen tip) steps
the cube to its next colour. Return True to claim the press so nothing
behind the pick sees it.
A shape outside a physics world, as here, is picked through its bounding
sphere, so each cube carries a sphere that just touches its faces. Put the
shape on a physics body instead and the ray hits the real collider. For
casting your own ray with Camera3D.ray_at and physics.raycast, see
raycast.py.
Each cube reports its pick through its own picked signal, and the scene
shows which cube was hit and the world point the ray landed on.
Source¶
1"""Mouse Picking: click cubes to change their colour.
2
3Picking is built into the scene tree: give a node a ``CollisionShape3D`` child
4with ``pickable=True`` and override ``on_picked``. Every press the UI does not
5take casts a ray from the camera through the pointer, and the owner of the
6nearest pickable shape it meets receives a ``PickEvent``:
7
8 def on_picked(self, event):
9 if event.button in Pointer.PRIMARY:
10 print(event.node, event.point, event.normal, event.distance)
11
12``event.button`` is whichever button or finger pressed, so check it for the
13action you mean; here the primary press (left mouse, a tap, a pen tip) steps
14the cube to its next colour. Return ``True`` to claim the press so nothing
15behind the pick sees it.
16
17A shape outside a physics world, as here, is picked through its bounding
18sphere, so each cube carries a sphere that just touches its faces. Put the
19shape on a physics body instead and the ray hits the real collider. For
20casting your own ray with ``Camera3D.ray_at`` and ``physics.raycast``, see
21``raycast.py``.
22
23Each cube reports its pick through its own ``picked`` signal, and the scene
24shows which cube was hit and the world point the ray landed on.
25"""
26
27from simvx.core import (
28 Anchor,
29 Camera3D,
30 CanvasLayer,
31 CollisionShape3D,
32 DirectionalLight3D,
33 Label,
34 Material,
35 Mesh,
36 MeshInstance3D,
37 Node,
38 Pointer,
39 Signal,
40 SphereShape3D,
41 Vec3,
42)
43from simvx.graphics import App
44
45# RGBA because assigning an RGB tuple to Material.colour after construction is not expanded to RGBA
46# the way the constructor does it, and the renderer then fails to pack the material.
47COLOURS = [
48 (0.9, 0.1, 0.1, 1.0),
49 (0.1, 0.9, 0.1, 1.0),
50 (0.1, 0.1, 0.9, 1.0),
51 (0.9, 0.9, 0.1, 1.0),
52 (0.9, 0.1, 0.9, 1.0),
53 (0.1, 0.9, 0.9, 1.0),
54]
55
56
57class PickableCube(MeshInstance3D):
58 picked = Signal(Node, Vec3) # after a primary press lands on this cube
59
60 def __init__(self, colour_index=0, **kwargs):
61 super().__init__(mesh=Mesh.box(), material=Material(colour=COLOURS[colour_index]), **kwargs)
62 self.colour_index = colour_index
63
64 def on_ready(self):
65 self.add_child(CollisionShape3D(shape=SphereShape3D(radius=0.5), pickable=True))
66
67 def on_picked(self, event):
68 if event.button not in Pointer.PRIMARY:
69 return None
70 self.colour_index = (self.colour_index + 1) % len(COLOURS)
71 self.material.colour = COLOURS[self.colour_index]
72 self.picked.emit(self, event.point)
73 return True
74
75
76class PickingScene(Node):
77 def on_ready(self):
78 self.add_child(Camera3D(position=(0, 9, 8), look_at=(0, 0, 0)))
79 self.add_child(DirectionalLight3D(intensity=1.5, direction=(-0.4, -1.0, -0.6)))
80 for i, (x, z) in enumerate([(-3, -2), (0, -2), (3, -2), (-3, 2), (0, 2), (3, 2)]):
81 cube = self.add_child(PickableCube(colour_index=i, name=f"Cube {i + 1}", position=(x, 0, z)))
82 cube.picked.connect(self.on_cube_picked)
83
84 hud = self.add_child(CanvasLayer())
85 hud.add_child(Label("Click a cube to change its colour", anchor=Anchor.TOP_LEFT, inset=10, font_size=22))
86 self.status = hud.add_child(Label("Nothing picked yet", anchor=Anchor.BOTTOM_LEFT, inset=10, font_size=18))
87
88 def on_cube_picked(self, cube, point):
89 x, y, z = point
90 self.status.text = f"Picked {cube.name} at ({x:.2f}, {y:.2f}, {z:.2f})"
91
92
93if __name__ == "__main__":
94 App(title="Mouse Picking", width=1280, height=720).run(PickingScene())