A = -100

B = 0.5

See the full code in action here. My first two experiments with gravitational lensing were: Lensing, where I put a gravitational lensing effect on top of someone elses star field; and Cosmos, which features two simple FBM-based starfields. Both of them used a simple and arbitrary trick. This lensing method, which is more apparent in Cosmic Storm because of the background, rotates the ray direction by the correct angle of deflection as it passes the blackhole. This means that the effect isn't perfect because the path of the ray is a corner, not a curve.

This shader uses volumetric rendering, meaning rays are fired from a virtual camera through each pixel and as they pass through space they accumulate color. In my last post about the Hopf Fibration I mentioned that it would be really cool to learn about fluid dynamics because there is a certain solution to the Navier-Stokes equations whose flow lines are the projected circles of the Hopf fibration. I haven't really learned very much about fluid dynamics, but I got inspired by this shader's technique for rendering a 2D vector field. It uses Euler's method or the Runge-Kutta method to approximate the flow of an individual point under the velocity field. It samples a texture at two points along the flow and interpolates between the two colors based on time to make it look like the texture is moving. You can tell something similar is being used here because if you don't move the camera and look carefully you'll see that the image is periodic. Here's my 3D version of the color interpolation in GLSL:

```
vec3 interpolateColor(vec3 p){
```

float t1 = fract(0.5*time);

float t2 = fract(t1 + 0.5);

vec3 c1 = makeColor(approxFlow(p, t1 + 0.3));

vec3 c2 = makeColor(approxFlow(p, t2 + 0.3));

t1 = 2.0*abs(t1 - 0.5);

return mix(c1, c2, t1);

}

where `t1`

and `t2`

define the points in time where the flow will be approximated. `approxFlow(vec3 p, float t)`

approximates the flow of point `p`

at time `t`

; it uses 5 iterations of the Runge-Kutta method. `t1`

is set to `2.0*abs(t1 - 0.5)`

before linearly interpolating so that the color at `t2`

fades in as the color at `t1`

fades out and vice versa. `makeColor(vec3 p)`

makes a color based on the pressure and density of the solution to the Navier-Stokes equations. So here is the solution:
$$v(x,y,z) = A \left(a^2+x^2+y^2+z^2\right)^{-2} \left( 2(-ay+xz), 2(ax+yz) , a^2-x^2-y^2+z^2 \right),$$
$$p(x,y,z) = -A^2B \left(a^2+x^2+y^2+z^2\right)^{-3},$$
$$\rho(x,y,z) = 3B\left(a^2+x^2+y^2+z^2\right)^{-1}$$
where $v$ is velocity, $p$ is pressure, $\rho$ is density, $a$ is the distance to the unit circle in the $xy$ plane, and $A$ and $B$ are the arbitrary constants which you can manipulate using the sliders above!

`makeColor`

squishes pressure and density into the range $(0,\,1)$ with a logistic curve. It then calculates the hue of the color from the pressure, high pressure meaning more blue and lower pressure meaning more red, the reasoning being that high pressure corresponds to higher temperatures which in turn corresponds to higher frequency light being emitted from whatever gas it is swirling around this blackhole. The brightness of the color is the based off of the density, with the reasoning being that regions with more atoms will emit more light.

This was a fun technique to experiment with, but it makes me more curious about the physics behind fluids and how fluid simulations are actually carried out. If you start with a solution to the 2D Navier-Stokes equations, it might be interesting to deform some kind of image by accumulating the flow of each pixel in a buffer.