Mouse picking demo

click cubes to change their colour.

▶ Run in browser

Tags: 3d

Uses the engine’s GPU object-picking buffer: set shape.pickable = True and override on_picked. For the manual approach (casting a world-space ray with screen_to_ray and intersecting a CollisionWorld yourself) see raycast.py.

Usage: uv run python examples/features/3d/picking.py

Source

 1"""Mouse picking demo: click cubes to change their colour.
 2
 3Uses the engine's GPU object-picking buffer: set ``shape.pickable = True`` and
 4override ``on_picked``. For the manual approach (casting a world-space ray with
 5``screen_to_ray`` and intersecting a ``CollisionWorld`` yourself) see ``raycast.py``.
 6
 7Usage:
 8    uv run python examples/features/3d/picking.py
 9"""
10
11from simvx.core import (
12    Camera3D,
13    CollisionShape3D,
14    Material,
15    Mesh,
16    MeshInstance3D,
17    Node,
18    Text2D,
19)
20from simvx.graphics import App
21
22
23class PickableCube(MeshInstance3D):
24    """A cube that responds to mouse clicks."""
25
26    _COLOURS = [
27        (0.9, 0.1, 0.1, 1),
28        (0.1, 0.9, 0.1, 1),
29        (0.1, 0.1, 0.9, 1),
30        (0.9, 0.9, 0.1, 1),
31        (0.9, 0.1, 0.9, 1),
32        (0.1, 0.9, 0.9, 1),
33    ]
34
35    def __init__(self, label="Cube", colour_idx=0, **kwargs):
36        mat = Material(colour=self._COLOURS[colour_idx % len(self._COLOURS)])
37        super().__init__(material=mat, **kwargs)
38        self.label = label
39        self._colour_idx = colour_idx
40
41    def on_ready(self):
42        # Inscribed sphere (touches face centers) for tight picking
43        shape = CollisionShape3D(radius=0.5)
44        shape.pickable = True
45        self.add_child(shape)
46
47    def on_picked(self, event):
48        """Called when this node is picked by mouse ray."""
49        self._colour_idx = (self._colour_idx + 1) % len(self._COLOURS)
50        new_colour = self._COLOURS[self._colour_idx]
51        self.material.colour = new_colour
52        print(f"PICKED {self.label} -> colour {self._colour_idx}", flush=True)
53
54
55class PickingScene(Node):
56    def on_ready(self):
57        # Camera: looking down Y axis
58        cam = Camera3D(position=(0, -15, 0))
59        cam.look_at((0, 0, 0), up=(0, 0, 1))
60        self.add_child(cam)
61
62        # Shared mesh
63        cube_mesh = Mesh.cube()
64
65        # Grid of pickable cubes
66        positions = [
67            (-3, 0, -2),
68            (0, 0, -2),
69            (3, 0, -2),
70            (-3, 0, 2),
71            (0, 0, 2),
72            (3, 0, 2),
73        ]
74        for i, pos in enumerate(positions):
75            cube = PickableCube(
76                label=f"Cube_{i}",
77                colour_idx=i,
78                mesh=cube_mesh,
79                position=pos,
80            )
81            self.add_child(cube)
82
83        # HUD
84        self.add_child(Text2D(text="Click cubes to change colour", x=10, y=10, font_scale=1.5))
85
86    def on_process(self, dt):
87        pass
88
89
90if __name__ == "__main__":
91    app = App(title="Picking Demo", width=1280, height=720)
92    app.run(PickingScene())