3D Path Follow demo

camera rail fly-through around scattered geometry.

▶ Run in browser

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