Chase Camera¶
3rd-person lag-and-spring follow camera.
▶ Run in browserTags: 3d
Demonstrates:
ChaseCamera node tracking a moving target with smoothed offset
half_life Property as the visible knob (lag vs snap)
The trail effect: camera lags behind so player motion reads naturally
Controls (third-person: left/right turn the player, up/down walk): W / Up : Walk forward S / Down : Walk backward (no rotation) A / Left : Turn left D / Right : Turn right Q / E : Decrease / increase half_life (snap <-> heavy lag) R : Snap camera to rest pose (no smoothing) Escape : Quit
Run: uv run python examples/features/3d/chase_camera.py
Source¶
1"""Chase Camera: 3rd-person lag-and-spring follow camera.
2
3Demonstrates:
4 - ChaseCamera node tracking a moving target with smoothed offset
5 - half_life Property as the visible knob (lag vs snap)
6 - The trail effect: camera lags behind so player motion reads naturally
7
8Controls (third-person: left/right turn the player, up/down walk):
9 W / Up : Walk forward
10 S / Down : Walk backward (no rotation)
11 A / Left : Turn left
12 D / Right : Turn right
13 Q / E : Decrease / increase half_life (snap <-> heavy lag)
14 R : Snap camera to rest pose (no smoothing)
15 Escape : Quit
16
17Run: uv run python examples/features/3d/chase_camera.py
18"""
19
20from __future__ import annotations
21
22import math
23
24from simvx.core import (
25 ChaseCamera,
26 DirectionalLight3D,
27 Input,
28 InputMap,
29 Key,
30 Material,
31 Mesh,
32 MeshInstance3D,
33 Node,
34 Text2D,
35 Vec3,
36 WorldEnvironment,
37)
38from simvx.graphics import App
39
40PLAYER_SPEED = 6.0
41TURN_RATE = math.radians(140.0) # deg/s: left/right turn the player in place
42
43
44class ChaseCameraScene(Node):
45 def on_ready(self):
46 InputMap.add_action("quit", [Key.ESCAPE])
47 InputMap.add_action("up", [Key.W, Key.UP])
48 InputMap.add_action("down", [Key.S, Key.DOWN])
49 InputMap.add_action("left", [Key.A, Key.LEFT])
50 InputMap.add_action("right", [Key.D, Key.RIGHT])
51 InputMap.add_action("lag_more", [Key.E])
52 InputMap.add_action("lag_less", [Key.Q])
53 InputMap.add_action("snap", [Key.R])
54
55 # Default gradient sky: it lights the scene well enough to read the
56 # camera lag, which is what this page is about.
57 env = self.add_child(WorldEnvironment())
58 env.bloom_enabled = False
59 env.ambient_light_energy = 0.6
60
61 self.add_child(DirectionalLight3D(position=(5, 10, 5)))
62
63 # Target the camera will chase. Turning it in place swings the camera's
64 # rest pose around with it, which is where the lag reads most clearly.
65 self._player = self.add_child(
66 MeshInstance3D(
67 mesh=Mesh.cube(size=1.0),
68 material=Material(colour=(0.9, 0.3, 0.2, 1.0)),
69 pivot="bottom",
70 position=(0, 0, 0),
71 name="Player",
72 )
73 )
74
75 # Ground reference: a checkerboard would be nicer but the cube
76 # primitive scaled flat is enough for the lag readout.
77 self.add_child(
78 MeshInstance3D(
79 mesh=Mesh.cube(size=1.0),
80 material=Material(colour=(0.18, 0.2, 0.22, 1.0)),
81 position=(0, -0.05, 0),
82 scale=Vec3(40, 0.1, 40),
83 name="Ground",
84 )
85 )
86
87 # A few static obstacles so the camera lag has geometry to slide past.
88 for i in range(-2, 3):
89 for j in range(-2, 3):
90 if (i + j) % 2 == 0:
91 continue
92 self.add_child(
93 MeshInstance3D(
94 mesh=Mesh.cube(size=0.8),
95 material=Material(colour=(0.4, 0.5, 0.6, 1.0)),
96 pivot="bottom",
97 position=(i * 4, 0, j * 4),
98 )
99 )
100
101 self._cam = self.add_child(
102 ChaseCamera(
103 target=self._player,
104 offset=Vec3(0, 2.5, 6.0),
105 look_offset=Vec3(0, 0.5, 0),
106 half_life=0.18,
107 fov=65.0,
108 )
109 )
110
111 self._hud = self.add_child(
112 Text2D(
113 text="",
114 position=(12, 12),
115 font_scale=1.1,
116 colour=(1, 1, 1, 1),
117 )
118 )
119 self._update_hud()
120
121 def _update_hud(self):
122 self._hud.text = (
123 f"ChaseCamera demo: half_life={float(self._cam.half_life):.2f} s\n"
124 "WASD / arrows move; Q/E lag less/more; R snap; Esc quit."
125 )
126
127 def on_update(self, dt: float):
128 if Input.is_action_just_pressed("quit"):
129 self.app.quit()
130 return
131
132 # Third-person tank controls: left/right rotate the player in place,
133 # up/down walk along the player's current heading. Turning (rather than
134 # strafing) keeps the rig's rest pose behind the player, so the lag the
135 # camera is showing is the lag of the follow and not of a sideways slide.
136 vf = Input.is_action_pressed("up") - Input.is_action_pressed("down")
137 vt = Input.is_action_pressed("right") - Input.is_action_pressed("left")
138 if vt:
139 self._player.rotate_y(-vt * TURN_RATE * dt)
140 if vf:
141 fwd = self._player.forward
142 mag = math.hypot(fwd.x, fwd.z) or 1.0
143 heading = Vec3(fwd.x / mag, 0.0, fwd.z / mag)
144 self._player.position = self._player.position + heading * (vf * PLAYER_SPEED * dt)
145
146 if Input.is_action_just_pressed("lag_more"):
147 self._cam.half_life = min(float(self._cam.half_life) + 0.05, 1.5)
148 self._update_hud()
149 if Input.is_action_just_pressed("lag_less"):
150 self._cam.half_life = max(float(self._cam.half_life) - 0.05, 0.0)
151 self._update_hud()
152 if Input.is_action_just_pressed("snap"):
153 self._cam.snap()
154
155
156if __name__ == "__main__":
157 App(title="ChaseCamera", width=1280, height=720).run(ChaseCameraScene())