Willowf's gemlog

Willowf's Home Page

Gemlog index

Previous post

Next post

I have released an indie game on itch.io! It is called "Beebo Wave Simulator". Check it out here:

https://willowf.itch.io/beebo-wave-simulator

The full source code is freely available along with the compiled binary and can be gotten from the same downloads page in a *.zip file.

what language is it written in

C

why did you write it in C

how does it work

Basically: matrix multiplication. The code implements a mathematical operation called the "discrete Laplace operator". That is a version of the "continuous Laplace operator" (noted as ∇²) which describes the propagation of energy potential on a continuous surface. For example, it could be used to describe the movement of waves on the surface of a shallow pond: under the influence of gravity, the pond wants to pull its surface flat and smooth, but pressure waves tend to distort the surface with ripples, which grow more diffuse as they spread out, and can bounce off obstacles like the shore, boats, rocks, etc. Calculating the continuous Laplace operator is a bunch of calculus that is honestly kind of over my head (but I must credit my dear friend Noah Zuckman of NIST for helping me try to understand it.)

Luckily, it is a lot easier, for me *and* for a computer, to approximate the calculation with the discrete version of the Laplace operator, which is really just matrix multiplication. One really simple version of the discrete Laplace operator is just a 3x3 matrix that looks like this:

[ 0  1  0 ]
[ 1 -4  1 ]
[ 0  1  0 ]

By applying that to a 2-dimensional matrix of floats every frame, it produces waves that look astonishingly "good" - round and circular, similar to those you get in a puddle by dropping a pebble in it. But they aren't perfectly circular; they have quadrilateral, not radial symmetry; they look like a circle slightly squished into the shape of a square, and this is especially apparent at lower resolutions (such as those that your computer can simulate in real time without dropping frames or melting). So instead of that, Beebo uses a version of the discrete Laplace operator that looks more like this:

[ π/10      1-(π/10)              π/10     ]
[ 1-(π/10)  -4(π/10)-4(1-(π/10))  1-(π/10) ]
[ π/10      1-(π/10)              π/10     ]

And the results of that speak for themselves:

/~willowf/gemlog/beebo-screenshot.png

The source code that implements this operator is presented here for your reading pleasure:

static inline void anti_aliased_laplace_operator(Wave_Field *wf, const int x, const int y) {
  /**
   * The discrete Laplace operator being applied here conforms to a matrix that looks like
   * ```
   * [ ac ae ac ]
   * [ ae am ae ]
   * [ ac ae ac ]
   * ```
   * with the names of the variables short for "amplitude of corner", "amplitude of edge",
   * and "amplitude of middle". Their values are defined below.
   */
  const float ac = 3.14159265f/10, ae = 1.f-ac, am = -(4 * ac + 4 * ae) - 0.01;

  const int this_cell = (y * wf->dimx) + x;
  const int north = this_cell - wf->dimx;
  const int south = this_cell + wf->dimx;
  const int east = this_cell + 1;
  const int west = this_cell - 1;
  const int northeast = north + 1;
  const int northwest = north - 1;
  const int southeast = south + 1;
  const int southwest = south - 1;
  wf->frame_next[this_cell] = wf->wave_propagation_velocity * (
    (ae * wf->frame_curr[west]) + (ae * wf->frame_curr[east])
    + (ae * wf->frame_curr[north]) + (ae * wf->frame_curr[south])
    + (ac * wf->frame_curr[northeast]) + (ac * wf->frame_curr[northwest])
    + (ac * wf->frame_curr[southeast]) + (ac * wf->frame_curr[southwest])
    + (am * wf->frame_curr[this_cell])
  );
  wf->frame_next[this_cell] += 2.f * wf->frame_curr[this_cell] - wf->frame_prev[this_cell];
}

void wave_field_update(Wave_Field *wf) {
  /**
   * Here we want to move the current frame into the previous frame
   * and the next frame into the current one.
   */
  void *frame_next_tmp = wf->frame_next;
  wf->frame_next = wf->frame_prev;
  wf->frame_prev = wf->frame_curr;
  wf->frame_curr = frame_next_tmp;
  for (int y = 1; y < wf->dimy-1; ++y) for (int x = 1; x < wf->dimx-1; ++x) {
    anti_aliased_laplace_operator(wf, x, y);
  }
}

The idea to call it "anti-aliased" was Noah's suggestion; he does not know C, but he knows calculus, and helped me work this out. We found that the value of π/10 for ac worked really well; we tried π/9, which I expected would work better, but in fact it did not look as good. Neither of us have any idea why the number 10 should be in there.

I'm returning Beebo to the backburner for now; eventually I would like to make the UI easier to use, and I would like to add new features to make it more fun, e.g. the ability to draw walls that can block or reflect waves.