Hint: Load this page in a WebGPU-supported browser to control a live simulation!
Draw on the canvas to place/remove constraints:
- Left click: Place a wall constraint (prevents the plate underneath from moving)
- Right click: Place an oscillator constraint (Forces the plate to move up and down)
- Middle click: Clear constraint
You can adjust the frequency of oscillators using "brush frequency" slider at the bottom.
This is a little simulation of Chladni patterns in Bevy, using compute shaders for the plate simulation and bevy_spatial for the particles.
Chladni figures are patterns obtained by placing sand on a metal plate, and vibrating it.
When resonating, a plate or membrane is divided into regions that vibrate in opposite directions, bounded by lines where no vibration occurs (nodal lines).
https://en.wikipedia.org/wiki/Ernst_Chladni#Chladni_figures
The sand will naturally be pushed toward the nodal lines, making the figures appear.
Depending on how you constrain the movement of the plate (e.g. pressing it down with your fingers), these regions will change and you can get a variety of different patterns.
There are two parts to this simulation:
- The plate: Represented as a 128x128 square grid, where each cell stores its elevation and velocity
- The sand: A bunch of little sprites
The plate
The plate is represented as a texure, with only a red and green channel, corresponding to elevation & velocity respectively. It is simulated within a compute shader, and each point feels a force from its 4 neighbors.
// get the data of the current cell
let data = textureLoad(input, location);
var pos = data.r;
var vel = data.g;
// sample the 4 directions around the current cell
for (var i: i32 = 0; i < 4; i++) {
let t = f32(i) / 4.0 * TAU;
let d = vec2f(cos(t), sin(t));
let other_data = sample_input(loc + d);
let other_pos = other_data.r;
let weight = other_data.g;
// apply a force if the other cell is at a different elevation
vel += (other_pos - pos) * TENSION * weight;
}
// integrate velocity
pos += vel * DT;
// store the updated cell data
textureStore(output, location, vec4f(vec2f(pos, vel), 0.0, 0.0));
The sample_input function will return two values packed as a vec2f:
- the red channel is position
- the green channel is the weight of the connection. If it is
0.0, this neighbor will not be connected to the current cell.
At the boundaries, sample_input returns a returns a connection weight of 0.0.
Constraints
One important aspect to the diversity of these patterns is the way that the plate is constrained. There are three cases:
- In some parts, the plate will be totally unconstrained, free to simply follow the movement of its neighbors
- In other parts, the plate might be pressed down with fingers or clamps, forcing it to have a fixed position and velocity
- In other parts, the plate might be bowed, resulting in an oscillation
To reflect this, we encode constraints as a separate float texture with a single channel:
- If a cell has a constraint value of
-1.0, it is unconstrained. - Otherwise, its position will be given by
sin(time * TAU * constraint_value).- This means that we can fix a cell in place by giving it a constraint value of
0.0 - And that we can force it to oscillate by setting it to any positive value
- This means that we can fix a cell in place by giving it a constraint value of
The sand
The second component of this simulation is the sand particles.
They follow very simple rules
Go where the plate moves the least
The plate oscillates above and below zero, therefore we take the absolute value of the elevation.
We then take the gradient of it at the sand particle's position. This gives us a vector going toward the biggest elevation offset from zero.
We move the sand particle in the opposite direction to that gradient.
Move away from other sand particles
To avoid them clumping up in the same location, we apply a small force to push them away from each other.
It would be very expensive to naively compute the repulsive force of every particle against every other particle. Thankfully bevy_spatial lets us efficiently query the K nearest neighbors of a given particle by keeping all positions in a spatial datastructure.
Stay on the plate
Lastly, if a particle falls off the plate, we teleport it back to a random location
Source code
You can find the full source code here! https://codeberg.org/Azorlogh/chladni_figures