Mouse Picking

click cubes to change their colour.

▶ Run in browser

Tags: 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())