nodes/camera.pyΒΆ
Part of HexGL.
1"""Racer chase camera: speed pull-back on a roll-free rig.
2
3Builds on the engine's :class:`simvx.core.ChaseCamera`, which owns the
4smoothing and the ``snap()``. All this subclass adds is the two things a
5racer needs on top, both of them upstream ``CameraChase.js`` behaviour:
6
7- the rig sits behind the ship along its *flattened* forward, so a banked
8 corner or a rolling hull never tips the horizon;
9- the rig stretches further back the faster the ship goes (upstream's
10 ``speedOffset``), which is most of the sensation of acceleration.
11"""
12
13from __future__ import annotations
14
15import math
16
17from simvx.core import ChaseCamera, Node3D, Vec3
18
19
20class RacerCamera(ChaseCamera):
21 """Chase camera that trails the ship in world space and pulls back with speed."""
22
23 def __init__(
24 self,
25 target: Node3D,
26 distance: float = 10.0,
27 height: float = 4.0,
28 look_ahead: float = 6.0,
29 speed_pull_back: float = 4.0,
30 **kwargs,
31 ) -> None:
32 super().__init__(target=target, **kwargs)
33 # The offset is recomputed in world space every frame, so the base
34 # class must not also rotate it by the ship's (rolling) orientation.
35 self.rotate_with_target = False
36 self.distance = float(distance)
37 self.height = float(height)
38 self.look_ahead = float(look_ahead)
39 self.speed_pull_back = float(speed_pull_back)
40 self._speed_offset = 0.0
41
42 def snap(self) -> None:
43 # Re-aim first: the base class snaps to the current offsets, which
44 # would otherwise be one frame (or one teleport) out of date. This
45 # also covers the base class's snap on entering the tree.
46 self._aim()
47 super().snap()
48
49 def on_update(self, dt: float) -> None:
50 speed_ratio = float(getattr(self.target, "speed_ratio", 0.0)) if self.target is not None else 0.0
51 self._speed_offset += (speed_ratio * self.speed_pull_back - self._speed_offset) * min(1.0, 4.0 * dt)
52 self._aim()
53 super().on_update(dt)
54
55 def _aim(self) -> None:
56 """Point the rig's offsets down the ship's flattened forward."""
57 target = self.target
58 if target is None:
59 return
60 forward = target.forward
61 fx, fz = float(forward.x), float(forward.z)
62 length = math.hypot(fx, fz)
63 if length > 1e-4:
64 fx /= length
65 fz /= length
66 else:
67 fx, fz = 0.0, -1.0
68 back = self.distance + self._speed_offset
69 self.offset = Vec3(-fx * back, self.height, -fz * back)
70 self.look_offset = Vec3(fx * self.look_ahead, 0.5, fz * self.look_ahead)