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