ABSTRACT(original article): The Black Hole Explorer (BHEX) is a proposed mission to launch a sub-millimeter radio telescope into Earth orbit that will take the sharpest images in the history of astronomy and reveal novel horizon-scale features of supermassive black holes. Black Hole Vision is an open-source application, freely available on the iOS App Store, that produces lensed images which highlight the key features expected to appear in the black hole images BHEX will capture. The app combines video feeds from the front- and rear-facing iPhone cameras and uses the black hole lensing equations to synthesize an onscreen image displaying the user’s surroundings as if they were gravitationally lensed by a black hole within the cameras’ field of view. Here, we describe how light rays are lensed by non-rotating (Schwarzschild) and rotating (Kerr) black holes, and we list the equations needed for computing black-hole-lensed images. We also describe their specific implementation within Black Hole Vision. CITATION (original article): Roman Berens, Dominic O. Chang, Trevor Gravely, Alexandru Lupsasca (2026), Black Hole Vision: An Interactive iOS Application for Visualizing Black Holes, arXiv:2603.05810. https://doi.org/10.48550/arXiv.2603.05810​
​Download App: https://wolfr.am/Black-Hole-Vision​
​GitHub: https://github.com/graveltr/BlackHoleVision/tree/main
Black Hole Vision takes live video feeds from the front and rear facing cameras of the iPhone and simulates the effects of lensing by a rotating (Kerr) black hole. The result is displayed in real-time to the user’s screen.
The user interface is implemented using Swift and the SwiftUI framework. The lensing calculations are done on the GPU via Metal, Apple’s shading (graphics) language.
The code was written at Vanderbilt University by Trevor Gravely with input from Dr. Roman Berens and Prof. Alex Lupsasca. We are grateful to Dominic Chang for sharing his experience with his own lensing software, available on his personal website, https://dominic-chang.com/, or his GitHub, https://github.com/dominic-chang). This project was supported by CAREER award PHY-2340457 and grant AST-2307888 from the National Science Foundation.
Metal implementations of the Jacobi elliptic functions, incomplete elliptic integrals, and complete elliptic integrals are ported from https://www.gnu.org/software/gsl/.

Definitions

Screen to Phase Space Mappings

◼
  • textureCoordToPixel
    — Convert a normalised texture coordinate
    (u, v) ∈ [0,1]²
    to centred pixel coordinates by scaling up to the full texture resolution and shifting the origin to the image centre.
  • ◼
  • pixelToScreen
    — Map centred pixel coordinates to physical screen coordinates
    (α, β)
    . Apply a piecewise radial scale: inside a critical radius
    rcrit = 300
    pixels use a flat linear scale
    base = 0.04
    ; outside, add a power-law correction that grows with distance from
    rcrit
    , gently stretching the outer field of view to capture the photon ring region at larger impact parameters.
  • ◼
  • αβToηλ
    — Convert screen coordinates
    (α, β)
    to the geodesic conserved quantities
    (η, λ)
    using the standard Kerr relations:
    η(
    2
    α
    -
    2
    a
    )
    2
    cos
    θ
    o
    +
    2
    β
    and
    λ-αsin
    θ
    o
    .
  • ◼
  • normalizeAngle2
    — Fold any angle into the symmetric interval
    (-π,π]
    by first reducing modulo
    2π
    then subtracting
    2π
    if the result exceeds
    π
    .
  • ◼
  • FromSphericalCoordinatesCustom
    — Convert spherical coordinates to Cartesian, pre-normalising the azimuthal angle via
    normalizeAngle2
    to ensure the built-in conversion receives a consistently ranged input.
  • ◼
  • ToSphericalCoordinatesCustom
    — Convert Cartesian coordinates back to spherical, normalising the output azimuth into
    [0,2π)
    via
    Mod
    so all downstream code works with a consistent convention.
  • ◼
  • rotateSphericalCoordinate
    — Rotate the observer-frame intercept point
    vo
    into the frame defined by the source direction
    vs
    . Build an orthonormal triad
    (n1, n2, n3)
    with
    n1
    pointing along
    vs
    , apply the resulting rotation matrix
    M
    , then return the rotated point in standard spherical coordinates. This realigns the geodesic endpoint so that texture coordinates can be read off directly relative to the observer's viewing axis.
  • In[]:=
    Clear[textureCoordToPixel,pixelToScreen,αβToηλ,rotateSphericalCoordinate,FromSphericalCoordinatesCustom,ToSphericalCoordinatesCustom,normalizeAngle2]​​textureCoordToPixel[textureWidth_,textureHeight_][u_,v_]:=Module[{pixelCoords,center},​​pixelCoords={u*textureWidth,v*textureHeight};​​center={textureWidth/2.0,textureHeight/2.0};​​pixelCoords-center​​]​​pixelToScreen[x_,y_]:=Module{base,rcrit,alpha,n,r},​​base=0.04;​​rcrit=300;​​alpha=1000;​​n=1;​​r=
    2
    x
    +
    2
    y
    ;​​Ifr<rcrit,base*{x,y},
    1
    alpha
    n
    (r-rcrit)
    +base*{x,y}​​​​αβToηλ[a_][θs_][α_,β_]:=Module[{},​​{(
    2
    α
    -
    2
    a
    )*
    2
    Cos[θs]
    +
    2
    β
    ,-1*α*Sin[θs]}​​]​​normalizeAngle2[angle_]:=Module[{a=Mod[angle,2π]},If[a>π,a-2π,a]]​​FromSphericalCoordinatesCustom[v_]:=Module[{},​​FromSphericalCoordinates[{v[[1]],v[[2]],normalizeAngle2[v[[3]]]}]​​]​​ToSphericalCoordinatesCustom[v_]:=Module[{vSpherical},​​vSpherical=ToSphericalCoordinates[v];​​{vSpherical[[1]],vSpherical[[2]],Mod[vSpherical[[3]],2π]}​​]​​rotateSphericalCoordinate[vs_,vo_]:=Module[{vsCartesian,voCartesian,zhat,n1,v2,n2,n3,voHatCartesian,M},​​vsCartesian=FromSphericalCoordinatesCustom[vs];​​voCartesian=FromSphericalCoordinatesCustom[vo];​​​​zhat={0,0,1};​​n1=vsCartesian/Norm[vsCartesian];​​​​v2=zhat-(zhat.n1)*n1;​​n2=If[Norm[v2]==0,{0,1,0},v2/Norm[v2]];​​​​n3=Cross[n2,n1];​​​​M={n1,n3,n2};​​voHatCartesian=M.voCartesian;​​​​ToSphericalCoordinatesCustom[voHatCartesian]​​]

    Radial Functions and Integrals
    

    Angular Functions and Integrals
    

    Utility

    Define a diagnostic helper
    textureToAlpha
    that, given a texture coordinate
    (u, v)
    , converts it through the full pipeline — from texture space to pixel coordinates to screen coordinates — and returns the screen α value. Use this to probe and verify the coordinate mappings at any point in the grid, including a boundary correction that nudges
    v
    away from the exact midpoint to avoid edge ambiguity.
    Clear[textureToAlpha]​​textureToAlpha[u_,v_]:=Module[{textureWidth,textureHeight,i,j,α,β,η,λ,νθs,cosθf,θf,ϕf,ERRORSTRING,rotatedSphericalCoordinates,ϕfNormalized,oneTwoBdd,threeFourBdd,vp,up,TEXTURE,vCoordSpacing},​​vCoordSpacing=1/height;​​vp=If[v==1/2,v+(1/2)vCoordSpacing,v];​​{i,j}=textureCoordToPixel[1920,1080][u,vp];​​{α,β}=pixelToScreen[j,i];​​α​​]

    Computing the Lensing Map

    Define the core Kerr geodesic integrator kerrLense. Given conserved quantities
    (η,λ)
    , an initial polar direction
    νθ
    , and observer/source radii, compute the radial roots of the potential, integrate the radial Mino time
    Ir
    , then use it to evaluate the final polar angle via CosθObserver and the total azimuth via
    Iφ+λ·Gφ
    . Return the source-sphere intercept
    (cosθf,φf)
    or an error label if the ray has complex roots or originates inside the horizon.
    In[]:=
    Clear[kerrLense]​​kerrLense[DEBUG_][a_,M_][η_,λ_][θs_,νθs_,ro_,rs_]:=Module{r1,r2,r3,r4,cosθf,ϕf,ERRORSTRING,IrValue,τ,rplus,cosθo,GϕValue,IϕValue},​​cosθf=-1.0;​​ϕf=-1.0;​​ERRORSTRING="FAILURE";​​​​(*Obtaintheroots*)​​{r1,r2,r3,r4}=ObtainRadialRoots[a,M][η,λ];​​If[DEBUG,Print["r1 = ",r1,", r2 = ",r2,", r3 = ",r3,", r4 = ",r4]];​​​​(*Ifanyarecomplexreturn*)​​If[AnyTrue[{r1,r2,r3,r4},Im[#]!=0&],Return[{-1.0,-1.0,"COMPLEX ROOTS"}]];​​​​(*Makesureincorrectsubcaseof(2)*)​​rplus=M+
    2
    M
    -
    2
    a
    ;​​If[r4<rplus,Return[{-1.0,-1.0,"EMITTED FROM BLACK HOLE"}]];​​​​IrValue=Ir[r1,r2,r3,r4][ro,rs];​​If[DEBUG,Print["Ir = ",IrValue]];​​​​τ=IrValue;​​​​cosθo=CosθObserver[a][η,λ,νθs,θs][τ];​​If[DEBUG,Print["cos(​
    θ
    o
    ​) = ",cosθo]];​​​​GϕValue=Gϕ[DEBUG][a][η,λ,θs][τ,νθs];​​If[DEBUG,Print["Gϕ = ",GϕValue]];​​​​IϕValue=Iϕ[a,M][η,λ][r1,r2,r3,r4][ro,rs];​​If[DEBUG,Print["Iϕ = ",IϕValue]];​​​​cosθf=cosθo;​​ϕf=IϕValue+λ*GϕValue;​​ERRORSTRING="SUCCESS";​​{cosθf,ϕf,ERRORSTRING}​​
    Normalised vertical coordinate of the image's central row as a sanity check on the pixel-to-texture mapping.

    Series Expansion

    Texture Export

    Export the data to csv

    CITE THIS NOTEBOOK

    Black Hole Vision: an interactive iOS application for visualizing black holes​
    by Roman Berens, Dominic O. Chang, Trevor Gravely, Alexandru Lupsasca​
    Wolfram Community, STAFF PICKS, March 10, 2026
    ​https://community.wolfram.com/groups/-/m/t/3656544