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_figuresThe 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 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:
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
.
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:
-1.0
, it is unconstrained.sin(time * TAU * constraint_value)
.
0.0
The second component of this simulation is the sand particles.
They follow very simple rules
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.
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.
Lastly, if a particle falls off the plate, we teleport it back to a random location
You can find the full source code here! https://codeberg.org/Azorlogh/chladni_figures