afterglow/view/title_backdrop.pyΒΆ

Part of Afterglow.

 1"""TitleBackdrop: a clean, intentional abstract backdrop for the title menu.
 2
 3The title screen shows this instead of a live gameplay room, so nothing behind
 4the menu can be mistaken for a level you are meant to be playing. It is a
 5deliberately decorative scene: a dark, far field of a few soft glowing motes
 6drifting over the environment's gradient sky, with no tiles, platforms, spikes,
 7or Wisp.
 8
 9GPU-free to construct (MeshBuilder + Material are plain data). Add it under the
103D scene root and call :meth:`update` each frame for the slow drift.
11"""
12
13from __future__ import annotations
14
15import math
16
17import numpy as np
18
19from simvx.core import Material, MeshBuilder, MeshInstance3D, Node3D
20from simvx.core.mesh_builder import PrimitiveType
21
22# A handful of soft motes at varied depth; each entry is (x, y, z, radius, hue).
23# Hue tints the faint emissive glow; kept low-energy so nothing reads as gameplay.
24_MOTES: tuple[tuple[float, float, float, float, tuple[float, float, float]], ...] = (
25    (-3.6, 1.4, -2.0, 0.55, (0.55, 0.78, 0.95)),
26    (3.2, -1.1, -1.2, 0.42, (0.95, 0.82, 0.55)),
27    (1.1, 2.0, -3.0, 0.70, (0.70, 0.62, 0.95)),
28    (-1.9, -2.1, -1.6, 0.34, (0.60, 0.92, 0.78)),
29    (4.0, 1.8, -2.6, 0.30, (0.92, 0.70, 0.80)),
30    (-4.2, -1.0, -3.2, 0.48, (0.62, 0.80, 0.98)),
31)
32
33
34def _quad(st: MeshBuilder, cx: float, cy: float, cz: float, r: float) -> None:
35    """Emit one camera-facing quad (a billboard) centred at (cx, cy, cz)."""
36    base = st.vertex_count
37    corners = ((-r, -r), (r, -r), (r, r), (-r, r))
38    uvs = ((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0))
39    st.set_normal((0.0, 0.0, 1.0))
40    for (dx, dy), uv in zip(corners, uvs, strict=True):
41        st.set_uv(uv)
42        st.add_vertex((cx + dx, cy + dy, cz))
43    for i in (0, 1, 2, 0, 2, 3):
44        st.add_index(base + i)
45
46
47class TitleBackdrop(Node3D):
48    """A clean abstract menu backdrop: soft glowing motes over the gradient sky."""
49
50    def __init__(self, **kwargs):
51        super().__init__(**kwargs)
52        self._motes: list[MeshInstance3D] = []
53        self._t = 0.0
54
55    def build(self) -> None:
56        """Build the decorative motes. Idempotent (clears any prior build)."""
57        self.clear_children()
58        self._motes = []
59        for x, y, z, r, hue in _MOTES:
60            st = MeshBuilder()
61            st.begin(PrimitiveType.TRIANGLES)
62            _quad(st, 0.0, 0.0, 0.0, r)
63            mesh = st.commit()
64            glow = np.array(hue, dtype=np.float32)
65            mat = Material(
66                colour=(*(glow * 0.10), 1.0),
67                emissive_colour=tuple(glow),
68                emissive_strength=1.8,
69                unlit=True,
70            )
71            mi = MeshInstance3D(mesh=mesh, material=mat, name="mote")
72            mi.position = (x, y, z)
73            self.add_child(mi)
74            self._motes.append(mi)
75
76    def update(self, dt: float) -> None:
77        """Drift each mote on a slow, gentle bob so the backdrop feels alive."""
78        self._t += dt
79        for i, mi in enumerate(self._motes):
80            base = _MOTES[i]
81            phase = self._t * (0.20 + i * 0.04) + i * 1.7
82            mi.position = (
83                base[0] + math.sin(phase) * 0.35,
84                base[1] + math.cos(phase * 0.8) * 0.28,
85                base[2],
86            )