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)