::azr_log
Blog Games About
Hint: Load this page in a WebGPU-supported browser to control a live simulation!

Draw on the canvas to place/remove constraints:

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.


A horizontal plate of metal with sand on top. Chladni's left middle finger & thumb press on the side of the plate. With his right hand, he bows the side of the plate using a violin bow.

There are two parts to this simulation:

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:

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:

To reflect this, we encode constraints as a separate float texture with a single channel:

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