Fosforonero
Dev6 giugno 2026 · 5 min di lettura

Buco nero di Kerr, ray-tracing in tempo reale

Come ho reso le geodetiche di Kerr in un fragment shader WebGL: Kerr–Schild, tre integratori, corpo nero con Doppler e la battaglia col compilatore mobile.

Buco nero di Kerr ray-tracciato in WebGL: disco di accrescimento lensato, photon ring e beaming Doppler relativistico, dal simulatore Fosforonero

Buco nero di Kerr ray-tracciato in WebGL: geodetiche nulle esatte, disco, photon ring, beaming Doppler Risultato del renderer: buco nero di Kerr (spin a = 0.7), disco di accrescimento lensato con beaming Doppler: il lato che avanza (sinistra) è più caldo e più brillante. Photon ring: la luce che ha orbita 1.5 volte il buco nero prima di arrivare all'occhio. Render: Fosforonero.

La parte facile di un simulatore di buco nero sono i dati. La parte difficile è far girare la relatività generale dentro i ~16 millisecondi a fotogramma di un fragment shader WebGL, su un telefono. Queste sono le note tecniche di come ci sono riuscito per un buco nero di Kerr.

L'idea: sparare la luce all'indietro

Per ogni pixel dello schermo sparo un raggio dalla camera e lo seguo nello spazio-tempo curvo. Sembra al contrario (la luce va verso l'occhio, non da esso), ma in relatività generale i cammini dei fotoni sono reversibili: tracciare all'indietro dà la stessa identica immagine. È il trucco che ci risparmia di simulare miliardi di fotoni che non colpiranno mai lo schermo. Ne lanciamo uno solo per pixel, esattamente quello che serve.

La metrica: dove vive la geometria

Qui la gravità non è una forza, è la forma dello spazio-tempo, scritta nella metrica gμνg_{\mu\nu}. Per un buco nero rotante uso la metrica di Kerr nella forma cartesiana di Kerr–Schild:

gμν=ημν+fkμkν,f=2Mr3r4+a2z2g_{\mu\nu} = \eta_{\mu\nu} + f\,k_\mu k_\nu, \qquad f = \frac{2 M r^{3}}{r^{4} + a^{2} z^{2}}

dove η\eta è lo spazio-tempo piatto di Minkowski, kk un vettore nullo e a=J/Ma = J/M lo spin. Perché questa forma e non le solite coordinate di Boyer–Lindquist? Perché non ha la singolarità di coordinate all'orizzonte ed è asintoticamente piatta: così il fotone, alla camera lontana, parte con un momento banale (energia E=1E=1, impulso spaziale uguale alla direzione del raggio) e non devo costruire la tetrade dell'osservatore, uno dei punti dove è facilissimo sbagliarsi. A spin nullo (a=0a=0) si riduce esattamente a Schwarzschild.

L'integrazione: hamiltoniana

Un fotone viaggia su una geodetica nulla. La tratto come il flusso di un'hamiltoniana, perché così posso scrivere il passo in modo pulito:

H=12gμνpμpν=0,x˙μ=Hpμ,p˙μ=HxμH = \tfrac{1}{2}\,g^{\mu\nu} p_\mu p_\nu = 0, \qquad \dot x^{\mu} = \frac{\partial H}{\partial p_\mu}, \quad \dot p_\mu = -\frac{\partial H}{\partial x^{\mu}}

Il drift (x˙\dot x, come si muove la posizione) ha forma chiusa; il kick (p˙\dot p, come cambia l'impulso) lo calcolo per differenze finite del gradiente di HH. Il passo è adattivo: fine vicino al buco (dove la curvatura conta e si forma il photon ring) e grosso lontano, dopo un avanzamento analitico in linea retta fino alla sfera d'influenza, per non bruciare passi nel vuoto.

Tre integratori per tre esigenze

Integrare un'equazione differenziale è come camminare al buio a passi finiti: troppo lunghi e sbagli strada, troppo corti e non arrivi mai. Uso tre «andature», scelte dalla qualità.

  • Eulero simplettico (media/bassa): un solo gradiente per passo, economico, 1° ordine.
  • Runge–Kutta del 4° ordine (alta): valuta la pendenza quattro volte e ne fa una media pesata,
yn+1=yn+h6(k1+2k2+2k3+k4)y_{n+1} = y_n + \tfrac{h}{6}\left(k_1 + 2k_2 + 2k_3 + k_4\right)

portando l'errore di deflessione a ~10⁻⁷ rad (l'ho verificato offline contro una soluzione di riferimento).

  • Yoshida del 6° ordine, simplettico (Ultra): qui c'è un trucco. Yoshida compone passi leapfrog, ma il leapfrog vuole un'hamiltoniana separabile; la nostra non lo è. Serve il metodo di Tao (2016), che raddoppia lo spazio delle fasi per renderla integrabile. La demo «Orbite» ha invece l'equazione di Binet, che è separabile: lì un Yoshida diretto tiene il drift dell'energia a ~10⁻¹³, e lo mostro dal vivo.

Il colore non lo scelgo io

Il disco emette come un corpo nero: ogni anello ha una temperatura, da cui ricavo il vero colore planckiano. Poi lo sposto con il fattore Doppler relativistico, esatto per un'orbita circolare di Kerr:

g=1ut(1Ωλ)g = \frac{1}{u^{t}\,(1 - \Omega\,\lambda)}

con Ω\Omega velocità angolare del gas, utu^{t} la componente temporale della sua 4-velocità (la dilatazione del tempo) e λ\lambda il momento angolare assiale conservato del fotone. La cosa elegante: non devo «trasportare» il colore lungo il raggio. La quantità Iν/ν3I_\nu/\nu^{3} è un invariante (teorema di Liouville: i fotoni nel loro spazio delle fasi non si accalcano né si diradano), quindi un corpo nero visto con fattore gg resta un corpo nero a temperatura gTg\,T. Niente palette arbitrarie, e un lato del disco diventa abbagliante e bluastro, l'altro cupo e rosso.

La battaglia col compilatore shader mobile

L'errore più istruttivo. Avevo messo l'integratore Yoshida-6 (Tao) dentro lo shader. Su desktop: perfetto. Su mobile: il disco spariva, anche in qualità media. La causa non era la velocità, era la dimensione: quel codice veniva compilato nell'unico shader, sempre, e superava il limite del compilatore della GPU del telefono → falliva l'intero rettangolo a schermo (lensing e disco insieme). La soluzione: isolarlo dietro un #define GLSL, compilato solo quando selezioni «Ultra». Lo shader di default resta piccolo e gira ovunque. Lezione che mi porto dietro: su WebGL mobile conta quanto è grande il programma da compilare, non solo quanto è veloce.

Stack e onestà

Three.js + React Three Fiber, GLSL3, pipeline a 16-bit con dithering anti-banding, KaTeX per le equazioni. Ogni approssimazione è dichiarata: il disco è un modello (non una soluzione GRMHD), i getti sono stilizzati, i corpi del playground non sono lensati. La derivazione completa, con le fonti accademiche, è nella pagina delle equazioni.

In sintesi: cosa porto a casa

  • La fisica è calcolabile, l'emissione no. Il lensing di Kerr gira in tempo reale; il plasma magnetizzato del disco (GRMHD) no → lo si modella e lo si dichiara.
  • Kerr–Schild evita la tetrade. Alla camera lontana il momento del fotone è semplicemente la direzione del raggio: un classico punto d'errore in meno.
  • Il colore non si sceglie. L'invariante Iᵥ/ν³ (teorema di Liouville) porta un corpo nero da T a g·T senza trasportare nulla lungo il raggio.
  • Su WebGL mobile conta la dimensione dello shader, non solo la velocità. L'integratore Ultra vive dietro un #define, compilato solo quando serve, altrimenti su telefono falliva l'intero rettangolo.

Apri la simulazione → · Le equazioni →