3D Path Follow demo¶
camera rail fly-through around scattered geometry.
▶ Run in browserTags: 3d
Demonstrates:
Curve3D with bezier control points and tilt
Path3D / PathFollow3D for 3D motion along a curve
Camera3D parented to PathFollow3D for a rail fly-through
MeshInstance3D scene dressing (cubes, spheres)
Speed / pause controls
Controls: Up / Down - Increase / decrease speed Space - Pause / resume Escape - Quit
Run: uv run python examples/features/3d/path_follow.py
Source¶
1"""3D Path Follow demo -- camera rail fly-through around scattered geometry.
2
3Demonstrates:
4 - Curve3D with bezier control points and tilt
5 - Path3D / PathFollow3D for 3D motion along a curve
6 - Camera3D parented to PathFollow3D for a rail fly-through
7 - MeshInstance3D scene dressing (cubes, spheres)
8 - Speed / pause controls
9
10Controls:
11 Up / Down - Increase / decrease speed
12 Space - Pause / resume
13 Escape - Quit
14
15Run: uv run python examples/features/3d/path_follow.py
16"""
17
18
19import math
20
21from simvx.core import (
22 Camera3D,
23 Curve3D,
24 DirectionalLight3D,
25 Input,
26 InputMap,
27 Key,
28 Material,
29 Mesh,
30 MeshInstance3D,
31 Node,
32 Path3D,
33 PathFollow3D,
34 Property,
35 Text2D,
36 Vec3,
37)
38from simvx.graphics import App
39
40
41class CameraRailDemo(Node):
42 speed = Property(8.0, range=(1, 30))
43
44 def on_ready(self):
45 InputMap.add_action("speed_up", [Key.UP])
46 InputMap.add_action("speed_down", [Key.DOWN])
47 InputMap.add_action("toggle_pause", [Key.SPACE])
48 InputMap.add_action("quit", [Key.ESCAPE])
49
50 # Lighting
51 sun = self.add_child(DirectionalLight3D(name="Sun", intensity=1.2, colour=(1.0, 0.95, 0.85)))
52 sun.look_at((-1.0, -2.0, -0.5))
53
54 # Scene objects -- ring of cubes and spheres for visual reference
55 cube_mesh, sphere_mesh = Mesh.cube(), Mesh.sphere(radius=0.6)
56 colours = [
57 (0.9, 0.25, 0.2), (0.2, 0.7, 0.9), (0.9, 0.85, 0.2),
58 (0.6, 0.3, 0.8), (0.3, 0.9, 0.4), (0.9, 0.5, 0.1),
59 ]
60 for i in range(12):
61 angle = i * math.pi * 2 / 12
62 r = 10.0
63 x, z = r * math.cos(angle), r * math.sin(angle)
64 mesh = cube_mesh if i % 2 == 0 else sphere_mesh
65 mat = Material(colour=colours[i % len(colours)], roughness=0.4, metallic=0.2)
66 obj = self.add_child(MeshInstance3D(name=f"Obj{i}", mesh=mesh, material=mat))
67 obj.position = (x, 0.5, z)
68
69 # Ground plane
70 ground_mat = Material(colour=(0.35, 0.4, 0.35), roughness=0.9, metallic=0.0)
71 ground = self.add_child(MeshInstance3D(name="Ground", mesh=Mesh.cube(), material=ground_mat))
72 ground.scale = (30.0, 0.1, 30.0)
73 ground.position = (0.0, -0.05, 0.0)
74
75 # Build a helical path around the scene
76 curve = Curve3D(bake_interval=0.2)
77 turns, pts_per_turn, radius = 2, 8, 14.0
78 total_pts = turns * pts_per_turn
79 for i in range(total_pts + 1):
80 t = i / total_pts
81 a = t * turns * math.pi * 2
82 y = 2.0 + 6.0 * t # rise from 2 to 8
83 pos = Vec3(radius * math.cos(a), y, radius * math.sin(a))
84 # Tangential handles for smooth bezier
85 tangent_len = 4.0
86 da = turns * math.pi * 2 / total_pts
87 h_out = Vec3(-tangent_len * math.sin(a) * da, 6.0 * tangent_len / (total_pts), tangent_len * math.cos(a) * da)
88 curve.add_point(pos, handle_in=-h_out, handle_out=h_out)
89
90 self._path = self.add_child(Path3D(name="Rail"))
91 self._path.curve = curve
92
93 self._follower = self._path.add_child(PathFollow3D(name="CamFollow"))
94 self._follower.loop = True
95 self._follower.rotates = False # look_at handles camera orientation
96
97 # Camera attached to the path follower
98 self._cam = self._follower.add_child(Camera3D(name="Camera", fov=65, near=0.1, far=200.0))
99
100 # HUD
101 self._hud = self.add_child(Text2D(name="HUD", text="", font_scale=1.2))
102 self._hud.position = (10.0, 10.0)
103
104 self._paused = False
105
106 def on_process(self, dt: float):
107 if Input.is_action_just_pressed("quit"):
108 self.app.quit()
109 return
110 if Input.is_action_just_pressed("toggle_pause"):
111 self._paused = not self._paused
112 if Input.is_action_pressed("speed_up"):
113 self.speed = min(30.0, self.speed + 8.0 * dt)
114 if Input.is_action_pressed("speed_down"):
115 self.speed = max(1.0, self.speed - 8.0 * dt)
116
117 if not self._paused:
118 self._follower.progress += self.speed * dt
119
120 # Point camera toward the centre of the scene
121 self._cam.look_at((0.0, 1.0, 0.0))
122
123 ratio = self._follower.progress_ratio
124 state = "PAUSED" if self._paused else "Playing"
125 self._hud.text = (
126 f"Camera Rail [{state}] Speed: {self.speed:.1f} (Up/Down) "
127 f"Progress: {ratio:.1%} [Space] pause [Esc] quit"
128 )
129
130
131if __name__ == "__main__":
132 App(title="3D Camera Rail", width=1280, height=720).run(CameraRailDemo())