3D Path Follow

camera rail fly-through around scattered geometry.

▶ Run in browser

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