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.

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 . Per un buco nero rotante uso la metrica di Kerr nella forma cartesiana di Kerr–Schild:
dove è lo spazio-tempo piatto di Minkowski, un vettore nullo e 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 , impulso spaziale uguale alla direzione del raggio) e non devo costruire la tetrade dell'osservatore, uno dei punti dove è facilissimo sbagliarsi. A spin nullo () 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:
Il drift (, come si muove la posizione) ha forma chiusa; il kick (, come cambia l'impulso) lo calcolo per differenze finite del gradiente di . 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,
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:
con velocità angolare del gas, la componente temporale della sua 4-velocità (la dilatazione del tempo) e il momento angolare assiale conservato del fotone. La cosa elegante: non devo «trasportare» il colore lungo il raggio. La quantità è 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 resta un corpo nero a temperatura . 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.