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 )