<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://antoniospg.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://antoniospg.github.io/" rel="alternate" type="text/html" /><updated>2026-10-21T01:16:40-03:00</updated><id>https://antoniospg.github.io/feed.xml</id><title type="html">Antonio’s blog</title><subtitle>Notes on graphics, simulation, and systems programming.</subtitle><author><name>Antonio</name></author><entry><title type="html">Realistic Ocean Simulation</title><link href="https://antoniospg.github.io/2020/realistic-ocean-simulation/" rel="alternate" type="text/html" title="Realistic Ocean Simulation" /><published>2020-07-18T00:00:00-03:00</published><updated>2020-07-18T00:00:00-03:00</updated><id>https://antoniospg.github.io/2020/realistic-ocean-simulation</id><content type="html" xml:base="https://antoniospg.github.io/2020/realistic-ocean-simulation/"><![CDATA[<p><img src="/assets/posts/ocean-simulation/overview.png" alt="Simulated ocean surface" /></p>

<p class="caption"><strong>Figure 1:</strong> Simulated water surface with a realistic surface shader on a 256x256 grid with a total size of 100 meters.</p>

<p>Source code: <a href="https://github.com/antoniospg/UnityOcean">antoniospg/UnityOcean</a>.</p>

<h2 id="introduction">Introduction</h2>

<p>In this project, I implement the statistical wave model from Tessendorf’s paper [1] on simulating ocean water. The technique generates a heightmap by summing a vast number of waves using the Fast Fourier Transform, with user-controllable size and resolution, and the result can be tiled seamlessly over a larger domain.</p>

<p>The main idea is that the ocean can be modeled very well as a sum of “infinite” waves with different amplitudes traveling in different directions. These waves aren’t chosen at random; they come from statistical-empirical models of the ocean based on oceanographic research. In this post, I show how to recreate and animate the ocean surface and add visual features like foam and illumination.</p>

<h2 id="waves-and-the-fourier-transform">Waves and the Fourier Transform</h2>

<p>The technique consists of taking an inverse Fourier transform of the ocean height field in the frequency domain to get its spatial representation at each time \(t\). Every frame, for each pair \((x, z)\) in a rectangular grid, we compute the \(y\) component of that point, which is the ocean height at that location.</p>

<p>Given the height field in the spatial frequency domain, \(\tilde h(\mathbf{k}, t)\), we recover the spatial function with the inverse Fourier transform, that is, by evaluating the integral below:</p>

\[\int_{-\infty}^{\infty} \tilde h(\mathbf{k}, t) \exp(i\,\mathbf{k} \cdot \mathbf{x}) \, d\mathbf{k}\]

<p>To compute it, we sample the signal over a discrete support domain, so the integral becomes a sum:</p>

\[h(\mathbf{x}, t) = \sum_{\mathbf{k}} \tilde h(\mathbf{k}, t) \exp(i\,\mathbf{k} \cdot \mathbf{x})\]

<p>where \(\mathbf{k}\) is the wave vector, defined as:</p>

\[\mathbf{k} = (k_x, k_z), \qquad
k_x = \frac{2\pi n}{L_x}, \qquad
k_z = \frac{2\pi m}{L_z}\]

<p>with:</p>

\[-\frac{N}{2} \leq n \leq \frac{N}{2}, \qquad
-\frac{M}{2} \leq m \leq \frac{M}{2}\]

<p>For most of the implementation, though, we work with indices \(i\) and \(j\) in a different range:</p>

\[0 \leq i &lt; N, \qquad 0 \leq j &lt; M\]

<p>so we make the change of variables:</p>

\[n = i - \frac{N}{2}, \qquad m = j - \frac{M}{2}\]

<p>The FFT produces the height field at the discrete points:</p>

\[\mathbf{x} = \left( \frac{n L_x}{N}, \frac{m L_z}{M} \right)\]

<p>Finally, writing the 2D sum in terms of \(i\) and \(j\):</p>

\[h(x, z, t) = \sum_{i=0}^{N-1} \sum_{j=0}^{M-1}
\tilde h\!\left(i - \tfrac{N}{2},\, j - \tfrac{M}{2},\, t\right)
\exp\!\left( \frac{i \pi x (2i - N)}{L_x} + \frac{i \pi z (2j - M)}{L_z} \right)\]

<h2 id="the-fft">The FFT</h2>

<p>Computing the sum above directly costs \(O(n^2)\) for a 1D transform, which is terrible, especially for 2D data in a real-time system. The Fast Fourier Transform does the same computation in \(O(n \log n)\) for 1D data. The algorithm is intricate, but in essence a radix-2 FFT recursively splits the data in half and uses the cyclic property of the \(n\)-th roots of unity to avoid redundant work. Below is an example for 1D data with eight elements; the shape of this graph is why the FFT is also called the “butterfly algorithm”.</p>

<p><img src="/assets/posts/ocean-simulation/fftsample.gif" alt="Butterfly diagram" /></p>

<p class="caption"><strong>Figure 2:</strong> Butterfly algorithm.</p>

<p>How big should the grid be? It depends on whether you run the calculations on the GPU or the CPU. On the GPU, especially in a compute shader, the massive parallelism makes them much faster, and a 128x128 grid is enough for real time. On the CPU, the resolution is much more limited: in my implementation, 64x64 was the best I could get.</p>

<h2 id="statistical-wave-model">Statistical wave model</h2>

<p>A height field realization can be seen as a sum of sinusoids with complex, time-varying amplitudes. Like any signal, it can be generated from its Fourier coefficients:</p>

\[h(\mathbf{x}, t) = \sum_{\mathbf{k}} \tilde h(\mathbf{k}, t) \exp(i\,\mathbf{k} \cdot \mathbf{x})\]

<p>The coefficients are given by:</p>

\[\tilde h(\mathbf{k}, t) = \tilde h_0(\mathbf{k}) \exp(i \omega(\mathbf{k}) t) + \tilde h_0^*(-\mathbf{k}) \exp(-i \omega(\mathbf{k}) t)\]

<p>where:</p>

<ul>
  <li>\(\tilde h_0(\mathbf{k})\) is the initial value at \(t = 0\).</li>
  <li>\(\tilde h_0^*(-\mathbf{k})\) is the complex conjugate of \(\tilde h_0(-\mathbf{k})\).</li>
  <li>\(\omega(\mathbf{k})\) is the dispersion relation, \(\omega(k) = \sqrt{g k}\), where \(k\) is the length of the wave vector.</li>
</ul>

<p>This equation animates the waves over time. The conjugate term propagates waves “to the left” and “to the right”. The dispersion relation links the wave vector to the frequency and comes from real physical models of ocean water.</p>

<p>Next we need the initial spectrum. For that we use an oceanographic model for its average value:</p>

\[P_h(\mathbf{k}) = \left\langle |\tilde h^*(\mathbf{k}, t)|^2 \right\rangle\]

<p>For \(P_h\) we choose a semi-empirical model from oceanographic research known as the Phillips spectrum:</p>

\[P_h(\mathbf{k}) = A \frac{\exp\left(-1/(kL)^2\right)}{k^4} \left| \hat{\mathbf{k}} \cdot \hat{\mathbf{v}} \right|^2,
\qquad L = \frac{v^2}{g}\]

<p>To suppress waves smaller than some length \(l \ll L\), multiply the Phillips spectrum by:</p>

\[\exp\left(-k^2 l^2\right)\]

<p>where:</p>

<ul>
  <li>\(A\) is a numeric constant.</li>
  <li>\(\mathbf{k}\) is the wave vector.</li>
  <li>\(g\) is the gravitational constant.</li>
  <li>\(\mathbf{v}\) is the wind velocity.</li>
</ul>

<p>Realizations of water height fields are observed to be Gaussian random numbers with spatial spectra of a prescribed form, and this is most efficiently done directly in the Fourier domain. The Fourier amplitudes of a height field can be produced by:</p>

\[\tilde h_0(\mathbf{k}) = \frac{1}{\sqrt{2}} (\xi_r + i \xi_i) \sqrt{P_h(\mathbf{k})}\]

<p>where \(\xi_r\) and \(\xi_i\) are independent draws from a Gaussian distribution with mean 0 and standard deviation 1.</p>

<p>With all of that, we get the first version of the ocean surface:</p>

<p><img src="/assets/posts/ocean-simulation/first.gif" alt="First simulation" /></p>

<p class="caption"><strong>Figure 3:</strong> First simulation, using only the height field.</p>

<h2 id="additional-calculations">Additional calculations</h2>

<h3 id="normals">Normals</h3>

<p>Lighting needs the surface normal at each point. Instead of running another FFT, we save work by taking central differences over the height field to get its gradient \(\boldsymbol{\epsilon}(\mathbf{x}, t) = \nabla h(\mathbf{x}, t)\). The normal then follows from:</p>

\[\hat{\mathbf{n}}(\mathbf{x}, t) = \frac{\hat{\mathbf{y}} - \boldsymbol{\epsilon}(\mathbf{x}, t)}{\sqrt{1 + \epsilon^2(\mathbf{x}, t)}}\]

<h3 id="horizontal-displacement">Horizontal displacement</h3>

<p>At this point the waves have rounded peaks, which don’t look like a real ocean. To sharpen them, we also compute horizontal displacements. Ocean waves don’t only oscillate in \(y\); they oscillate horizontally too. To capture this, we decompose the spectrum along the \(x\) and \(z\) axes, giving a horizontal displacement:</p>

\[\mathbf{D}(\mathbf{x}, t) = \sum_{\mathbf{k}} -i \frac{\mathbf{k}}{k} \tilde h(\mathbf{k}, t) \exp(i\,\mathbf{k} \cdot \mathbf{x})\]

<p>We then add it to the original grid positions. The user-controllable factor \(\lambda\) sets how strong the horizontal displacement is, that is, how sharp the waves get:</p>

\[\mathbf{x}_f = \mathbf{x} + \lambda \mathbf{D}(\mathbf{x}, t)\]

<p>Here is the result of applying the displacement to the waves:</p>

<p><img src="/assets/posts/ocean-simulation/dispmap.gif" alt="Displacement map" /></p>

<p class="caption"><strong>Figure 4:</strong> Simulation with horizontal displacement.</p>

<h2 id="ocean-effects">Ocean effects</h2>

<h3 id="foam">Foam</h3>

<p>To place foam, we need to know where the waves fold into themselves. A good measure is the Jacobian of the displacement map: it tells us whether the transformation is unique. When \(J = 0\), two grid points get mapped to the same position, so the Jacobian tells us where the waves fold, and we want to put foam wherever \(J \leq 0\).</p>

\[\begin{aligned}
J_{xx} &amp;= 1 + \lambda \frac{\partial D_x(\mathbf{x})}{\partial x} \\
J_{zz} &amp;= 1 + \lambda \frac{\partial D_z(\mathbf{x})}{\partial z} \\
J_{xz} = J_{zx} &amp;= \lambda \frac{\partial D_x(\mathbf{x})}{\partial z} \\
J(\mathbf{x}) &amp;= J_{xx} J_{zz} - J_{xz} J_{zx}
\end{aligned}\]

<p>To apply the foam, we compute a folding map that masks a custom foam texture. The mask is a texture with one pixel per grid point. Then, for every position, we compute the foam intensity:</p>

\[I = \operatorname{clamp}\left(0, 1, M - J(\mathbf{x})\right)\]

<p>This clamps \(M - J(\mathbf{x})\) between zero and one. Rather than drawing foam only where \(J \leq 0\), it works better to pick a custom threshold and draw foam where \(J &lt; M\), using \(M - J(\mathbf{x})\) as the intensity. Figure 5 shows how the surface behaves for different values of \(J\).</p>

<p><img src="/assets/posts/ocean-simulation/jacobiangraph.jpg" alt="Points with different Jacobian values" /></p>

<p class="caption"><strong>Figure 5:</strong> Points with different values of \(J\) [2].</p>

<p>Below, the folding map (left) modulates a custom foam texture (right); the result is multiplied into the ocean’s final color.</p>

<div class="side-by-side">
  <img src="/assets/posts/ocean-simulation/folding.png" alt="Folding map" />
  <span>&times;</span>
  <img src="/assets/posts/ocean-simulation/foam-texture.png" alt="Foam texture" />
</div>

<p class="caption"><strong>Figure 6:</strong> Folding map modulating a custom foam texture.</p>

<h3 id="illumination">Illumination</h3>

<p>The minimum lighting effects an ocean surface needs are reflection and refraction. In computer graphics, these are quantified by the reflectivity \(R\) and transmissivity \(T\), given by the Fresnel equations for s-polarized light:</p>

\[R + T = 1\]

\[R(\theta_i, \theta_t) = \left( \frac{n_1 \cos\theta_i - n_2 \cos\theta_t}{n_1 \cos\theta_i + n_2 \cos\theta_t} \right)^2\]

<p>where:</p>

<ul>
  <li>\(\theta_i\) is the incident angle.</li>
  <li>\(\theta_t\) is the transmitted angle.</li>
  <li>\(n_1\) is the index of refraction of air.</li>
  <li>\(n_2\) is the index of refraction of water.</li>
</ul>

<p>\(\theta_i\) and \(\theta_t\) are related by Snell’s law:</p>

\[n_1 \sin\theta_i = n_2 \sin\theta_t\]

<p>\(R\) and \(T\) lie in \([0, 1]\), so they can be read as the fractions of the final color coming from reflected sky light and from light refracted out of the water volume. Since \(R \in [0, 1]\), we can use it to interpolate between the sky color and the ocean color:</p>

\[C_f = \operatorname{lerp}(\text{OceanColor}, \text{SkyColor}, R)\]

<p>Evaluating the full Fresnel term per point is too expensive, so we use Schlick’s approximation instead:</p>

\[R_0 = \left( \frac{n_1 - n_2}{n_1 + n_2} \right)^2, \qquad
R(\theta) = R_0 + (1 - R_0)(1 - \cos\theta)^5\]

<p>Applying this in the ocean shader gives:</p>

<p><img src="/assets/posts/ocean-simulation/fresnel.gif" alt="Fresnel shading" /></p>

<p class="caption"><strong>Figure 7:</strong> Simulation with illumination effects.</p>

<h2 id="final-result-and-future-work">Final result and future work</h2>

<p><img src="/assets/posts/ocean-simulation/final.gif" alt="Final result" /></p>

<p class="caption"><strong>Figure 8:</strong> Final result, rendered on a 64x64 grid.</p>

<p>As Figure 8 shows, the whole project runs on a 64x64 grid. It looks good, but the result is much better at high resolution, as in Figure 1. To get there, all the calculations, especially the FFTs, need to move to the GPU in a compute shader to take advantage of its parallelism.</p>

<h2 id="references">References</h2>

<ol>
  <li>Tessendorf, Jerry. <em>Simulating Ocean Water.</em> In SIGGRAPH 2002 Course Notes #9 (Simulating Nature: Realistic and Interactive Techniques), ACM Press.</li>
  <li>CGDC 2015, <em>Ocean simulation and rendering in War Thunder.</em></li>
</ol>]]></content><author><name>Antonio</name></author><category term="graphics" /><category term="simulation" /><category term="unity" /><summary type="html"><![CDATA[Tessendorf's FFT wave model in Unity, with foam, horizontal displacement and Fresnel shading.]]></summary></entry></feed>