Chase Camera¶
a third-person camera that lags behind its target.
▶ Run in browserTags: 3d
ChaseCamera is a Camera3D that follows a target node. Give it the node
to follow and where to sit relative to it, and it moves itself every frame:
ChaseCamera(target=player, offset=Vec3(0, 2.5, 6), look_offset=Vec3(0, 0.5, 0), half_life=0.18)
offset is in the target’s own frame (X right, Y up, +Z behind), so the camera
swings round to stay behind the target as it turns; set
rotate_with_target=False for a fixed world-space offset instead.
look_offset shifts the point it looks at, here just above the cube’s base.
half_life is the lag: the seconds it takes the camera to close half the gap
to where it wants to be. Zero snaps every frame, and larger values trail further
behind, which is what makes motion read as motion. snap() jumps straight to
the rest pose, for a teleport or a scene change.
The red cube drives like a tank: left and right turn it in place and forward and back walk it along its heading, so the lag you see is the camera’s follow and not a sideways slide.
Controls: WASD / arrows / left stick - Up and down walk, left and right turn Q / E, shoulder buttons - Less / more lag (half_life) R, A button - Snap the camera to its rest pose The on-screen pad drives all of these with a finger.
Source¶
1"""Chase Camera: a third-person camera that lags behind its target.
2
3``ChaseCamera`` is a ``Camera3D`` that follows a target node. Give it the node
4to follow and where to sit relative to it, and it moves itself every frame:
5
6 ChaseCamera(target=player, offset=Vec3(0, 2.5, 6), look_offset=Vec3(0, 0.5, 0), half_life=0.18)
7
8``offset`` is in the target's own frame (X right, Y up, +Z behind), so the camera
9swings round to stay behind the target as it turns; set
10``rotate_with_target=False`` for a fixed world-space offset instead.
11``look_offset`` shifts the point it looks at, here just above the cube's base.
12
13``half_life`` is the lag: the seconds it takes the camera to close half the gap
14to where it wants to be. Zero snaps every frame, and larger values trail further
15behind, which is what makes motion read as motion. ``snap()`` jumps straight to
16the rest pose, for a teleport or a scene change.
17
18The red cube drives like a tank: left and right turn it in place and forward
19and back walk it along its heading, so the lag you see is the camera's follow
20and not a sideways slide.
21
22Controls:
23 WASD / arrows / left stick - Up and down walk, left and right turn
24 Q / E, shoulder buttons - Less / more lag (half_life)
25 R, A button - Snap the camera to its rest pose
26 The on-screen pad drives all of these with a finger.
27"""
28
29import math
30
31from simvx.core import (
32 Anchor,
33 Axis1D,
34 Axis2D,
35 CanvasLayer,
36 ChaseCamera,
37 Controller,
38 DirectionalLight3D,
39 Input,
40 Key,
41 Label,
42 Material,
43 Mesh,
44 MeshInstance3D,
45 Node,
46 TouchController,
47 Vec3,
48 WorldEnvironment,
49 on_input,
50)
51from simvx.graphics import App
52
53WALK_SPEED = 6.0
54TURN_RATE = math.radians(140.0)
55LAG_RATE = 0.5 # half_life seconds gained per second of holding E
56
57
58class ChaseCameraScene(Node):
59 input_context = {
60 "move": [Axis2D.WASD, Axis2D.ARROWS, Controller.LEFT_STICK],
61 "lag": [Axis1D(low=Key.Q, high=Key.E), Axis1D(low=Controller.LEFT_BUMPER, high=Controller.RIGHT_BUMPER)],
62 "snap": [Key.R, Controller.A],
63 }
64
65 def on_ready(self):
66 self.add_child(WorldEnvironment(bloom_enabled=False, ambient_light_energy=0.6))
67 self.add_child(DirectionalLight3D(direction=(-1, -2, -1)))
68 self.add_child(MeshInstance3D(mesh=Mesh.plane(40), material=Material(colour=(0.18, 0.2, 0.22))))
69
70 # Obstacles on a checkerboard, so the lagging camera has something to slide past.
71 obstacle = Material(colour=(0.4, 0.5, 0.6))
72 for i in range(-2, 3):
73 for j in range(-2, 3):
74 if (i + j) % 2:
75 position = (i * 4, 0, j * 4)
76 self.add_child(
77 MeshInstance3D(mesh=Mesh.box(0.8), material=obstacle, pivot="bottom", position=position)
78 )
79
80 self.player = self.add_child(
81 MeshInstance3D(mesh=Mesh.box(1.0), material=Material(colour=(0.9, 0.3, 0.2)), pivot="bottom")
82 )
83 self.camera = self.add_child(
84 ChaseCamera(
85 target=self.player, offset=Vec3(0, 2.5, 6.0), look_offset=Vec3(0, 0.5, 0), half_life=0.18, fov=65.0
86 )
87 )
88
89 self.hud = self.add_child(CanvasLayer()).add_child(Label(anchor=Anchor.TOP_LEFT, inset=12, font_size=18))
90 self.add_child(TouchController(buttons=(Controller.A, Controller.LEFT_BUMPER, Controller.RIGHT_BUMPER)))
91
92 @on_input("snap")
93 def _snap(self, _event):
94 self.camera.snap()
95
96 def on_update(self, dt):
97 move = Input.value("move") # +y is back, as down the screen in 2D
98 self.player.rotate_y(-move.x * TURN_RATE * dt)
99 self.player.position += self.player.forward * (-move.y * WALK_SPEED * dt)
100
101 # half_life's range=(0, 5) clamps it, so holding Q stops at a snapping camera.
102 self.camera.half_life += Input.value("lag") * LAG_RATE * dt
103 self.hud.text = f"half_life = {self.camera.half_life:.2f} s\nWASD move, Q/E lag, R snap"
104
105
106if __name__ == "__main__":
107 App(title="Chase Camera", width=1280, height=720).run(ChaseCameraScene())