Thursday, July 28, 2016

(B7) - Gravity Wells

$
\let\ds\displaystyle
\let\ss\scriptstyle
\let\sss\scriptscriptstyle
\let\mtt\mathtt
\let\bs\boldsymbol
\newcommand{\vshift}[2]{\raise#1ex\hbox{${#2}$}}
\newcommand{\sfrac}[2]{{\vshift{.2}{#1}} / {\vshift{-.2}{#2}}}
\newcommand{\uminus}{\vshift{.17}{\ss{-}}}
\newcommand{\+}{{\sss{\,}}}
\newcommand{\nl}{$\; \\$}
$One of the first things I did in my UE4 game was to add visual gravity wells under each of the celestial bodies. I think the result came out quite nice.


I apologize for the low-quality, semi-relevant gif (it was the most readily available). At some point I'll update this blurb with a better gif, but for now simply ignore the rotating ship and UI on the left. On the right you can see the moving gravity-wells under Earth and Mars.  I made the grid by applying a dynamic material to a UE4 landscape. Making the material aesthetically appealing was a challenge, specifically dealing with the aliasing of thin emissive lines, but that will be the topic of another post. In this blurb we will concern ourselves with the equations used to define the gravity wells.

Suppose we have a spherical planet of radius $R$ and mass $M=\rho \+ R^{\+3}$. We can think of $\rho$ as a normalized uniform mass-density. A spaceship of mass $m$ at a distance $r>R$ will experience a gravitational force of $\sfrac{\ss GmM}{r^2}$ towards the planet. To reduce clutter we will normalize the gravitational constant, taking $G=\mtt{1}$. When making a game we would typically define $G$ as an arbitrary global-scaling factor anyway. The distance $r$ is the distance between mass-centers, indeed a uniform sphere induces a gravitational force equivalent to that of a point mass located at its center. The gravitational potential of the ship is then

$$ V(r) \;\; = \;\;  \frac{1}{m} \int_{\infty}^r \! f \, ds \;\; = \;\;  \int_{\infty}^r \frac{M}{s^2} \, ds \;\; = \;\; -\frac{M}{r} $$

This potential function $V(r) = \uminus \sfrac{M}{r}$ defines our gravity-well outside the planet. Imagine the planet is gaseous (still uniform density) and that the spaceship is flying around inside it at distance $r< R$. By the shell theorem, a spherical shell induces no gravitational force on the inside, so we can ignore the planet-mass that falls outside the ships distance $r$. The effective planet-mass is thus $\rho \+ r^3 = \sfrac{M \+ r^3 \!}{R^{\+ 3}}$ and the potential becomes

$$ V(r) \;\; = \;\; \int_{\infty}^R \frac{M}{s^2} \, ds \; + \; \int_{R}^r \frac{M}{R^{\+ 3}}s \, ds \;\; = \;\; \frac{M}{2R^{\+ 3}} (r^2 - 3R^{\+ 2}) $$



In my game, each gravity body has a limited range of influence, where the gravitational force is clamped to $\mtt 0$ outside the body-specific radius $w_1$. Since there is no force when $r>w_1$, there shouldn't be any gravity-well either. The function above, however, defines a gravity-well that stretches to infinity. So we need a way to clamp the potential function outside $w_1$. Here are a few examples when $w_1 = \mtt 4$.




The red-dashed curve is the original potential function. One way to clamp $V$ is to simply add another conditional $V(r) = \{ \+ \mtt{0} \;\; \mtt{if}\;  r> \mtt 4$, but this leads to a discontinuity in the surface (yellow). To fix the discontinuity we can shift curve up by $V(4) = \sfrac{\uminus M}{4}$ (blue). This works well for most purposes, but it still introduces a corner at $w_1$. The corner can actually add a cliff-like effect to our gravity-well, so it's sometimes desirable, but generally we want to make the corner smooth. One way to achieve $C^1\!$-continuity is to patch the gravity well with a cubic polynomial (pink), yielding the green potential function on the right. If we make the cubic patch small enough we can still get a cliff-like effect while maintaining a smooth edge.



Before we define the cubic patch let's first generalize the potential function. Outside the planet, $V$ is inversely proportional to $r$, that is, $V(r) \propto r^{\uminus 1}$. Suppose that we don't really like the look of the resulting gravity-well in our game, and that we instead want $V(r) \propto r^{\uminus 2}$. This should be an easy thing to consider, so we'll introduce the outside dropoff exponent $d_o$ as well as inside exponent $d_i$. As before, we take $w_1$ to be the radius of influence and we let $w_0$ denote the start of the cubic patch. Putting this all together we have.

$$ V(r) \;\; = \;\;
\left\{ \begin{array}{lll}  
\ds \alpha \, r^{d_i} + \gamma \quad & \quad r < R \\[1em]
\ds \uminus r^{\uminus d_o} + w_1^{-d_o} \quad & \quad r \in [R,w_0) \\[1em]
\ds \psi(r) \quad & \quad r \in [w_0,  w_1) \\[1em]
\ds 0 \quad & \quad r \geq w_1  
\end{array}   \right.
$$
It is not difficult to extend $V$ to support $\;w_0<R\;$ or $\;w_1 < R$, but it just adds to the confusion while offering no real benefit, so we will assume $R \leq w_0 < w_1$. To reduce clutter, we have normalized the mass to $M= \mtt 1$. It is trivial to implement $M$ in-game; if the gravitational constant is $G$ and the planet has mass $M$, then it's gravity-well is simply described by $GM \cdot V(r)$. Note that the roman letters $d_i, d_o, w_i, w_o, R$ denote independent (given) variables whereas the greek letters must be determined to make sure the seams at $r \in \{R, w_0, w_1\}$ are $C^1$-smooth. Finding $\alpha$ and $\gamma$ is a simple matter. We simply take $V(R)$ and $V'(R)$ and equate the first two conditional statements.

$$
\begin{array}{llll}
V(R) \quad & = & \quad  \alpha \, R^{d_i} + \gamma  & = \; \uminus R^{\uminus d_o}+w_1^{\uminus d_o}\\
V'(R) \quad & = & \quad \alpha \, d_i \, R^{d_i-1} & = \; d_o \, R^{\uminus (d_o+1)} \\ \\
\end{array}
\\ \Downarrow \\ \; \\
\begin{array}{lll}
\alpha \; &= &\; \frac{d_o}{d_i} \, R^{\uminus (d_o+d_i)} \\
\gamma \; & = &\; \left(\uminus \frac{d_o}{d_i} - 1\right) R^{\uminus d_o} + w_1^{\uminus d_o}
\end{array}
$$
To find the cubic patch $\psi(r)$ we best define some notation. We let $\nu_0,\nu_1$ be the potential values and $\sigma_0,\sigma_1$ be the tangent slopes at $w_0,w_1$, respectively. We then let $\Delta w$ be the width of the cubic patch and $\Delta \nu$ be the height. Finally, we let $\lambda$ be a normalized-parametrization of $r$ (the percent along the way from $w_0$ to $w_1$).



So we need to find the unique cubic polynomial $\psi(r)$ that passes thru $(w_0,\nu_0)$ and $(w_1, 0)$ at slopes $\sigma_0$ and $\mtt{0}$, respectively. In my recent post I found the unique polynomial $s$ that passes thru $\mtt (0,0)$ and $\mtt (1,1)$ with given slopes $\tau_0$ and $\tau_1$.

$$ s(x) \;\;\; = \;\;\; (\uminus 2 + \tau_0 + \tau_1) \+ x^3 \; + \; (3 - 2\tau_0 - \tau_1) \+ x^2 \; + \; \tau_0 \+ x $$
By stretching the domain and range we equate $\tau_0 =  \frac{\Delta w}{\Delta v} \sigma_0 \,$ and $\,\psi(r) = v_0 + \Delta v \+ s(\lambda)$.

$$ \psi(r) \;\;\; = \;\;\; v_0 \; + \; \Delta v \left[ \, (\uminus 2 + \tau_0) \+ \lambda^3 \; + \; (3 - 2 \tau_0) \+ \lambda^2 \; + \; \tau_0 \+ \lambda \, \right] $$
Now there is one last thing to worry about. What if the cubic patch rises above the horizontal axis? For example,



Of course, the hump might be a cool effect in-game, but in general it's not what we want. To prevent the hump we require that the cubic patch be monotonic on the interval $[w_0, w_1]$, which boils down to $ \tau_0 =  \frac{\Delta w}{\Delta v} \sigma_0 \, \leq \mtt{3} $ (see the linked post). If we are defining an actual gravity potential with $d_i = 2$ and $d_o = \uminus 1$, then it turns out that this isn't something we need to worry about. We can simply require $w_0 \geq \sfrac{w_1}{3}$. More precisely, given an influence radius $w_1$, we get a no-hump patch $\mtt{iff}$ we choose $w_0$ in the following shaded region.




So how can we implement this in UE4? Suppose we have an expanse of multiple planets that each lie on the $xy$-plane. To make the grid we place a $\mtt{landscape}$ in our world that spans the $xy$-plane. We then apply a $\mtt{material}$ to the landscape and using the $\mtt{world}$ $\mtt{displacement}$ node we displace the $z$-coordinate of each vertex by the sum of the planets' potential functions. Let $V_i$ be the potential function of the $i\mtt{th}$ planet and let $\boldsymbol{x}_{ij} = \left(x_{ij}^{(1)}, x_{ij}^{(2)}, \mtt{0}\right)$ be vector from planet $i$ to vertex $j$. We then set the world displacement vector to $\left(\mtt{0}, \mtt{0}, \sum_i V_i(r_{ij})\right)$ where $r_{ij} = |\boldsymbol{x}_{ij}|$ is the distance between planet and vertex. To make a Tron-like grid similar to that in the gif, we need only use emissive contour lines. If we instead want to shade the landscape with lighting, we would also need to input the vertex normals into our material:

$$ \boldsymbol{N_j} = \left(\sum_i \frac{x_{ij}^{(1)}}{r_{ij}} V'_i(r_{ij}) \; , \;\; \sum_i \frac{x_{ij}^{(2)}}{r_{ij}}V'_i(r_{ij}) \; , \;\; \uminus 1 \right)$$
I'll leave the calculation of $V'(r)$ as an exercise to the reader. Finally, if we want the gravity-wells to move with the planets, we can use $\mtt{dynamic}$ $\mtt{material}$ $\mtt{parameters}$ to feed the positions of each body into the material. Since the mass and radii of the planets are unlikely to change with time, we might prefer to store them in a $\mtt{material}$ $\mtt{parameter}$ $\mtt{collection}$. Just for fun, below is some Python code where you can input the mass, radius, influence, etc. and it will spit out the potential function. You need the Sympy module for it to work.

No comments:

Post a Comment