Simulate String Vibration in Matter.js
This article explains how to model musical instrument string vibration in the 2D physics engine Matter.js by discretizing a continuous string into a series of linked micro-bodies. By connecting small rigid masses with elastic constraints between fixed boundaries, you can accurately approximate transverse wave propagation, fundamental frequencies, and harmonic damping directly in a browser environment.
Core Concept: Discretizing the String
A physical instrument string is a continuous medium governed by the 1D wave equation. To simulate this in a rigid-body physics engine like Matter.js, the string is discretized into a lumped-element model: a chain of \(N\) small, identical circular bodies (micro-bodies) interconnected by distance constraints that act as tension springs.
The physical fidelity of the simulation depends directly on:
- Node Count (\(N\)): Higher counts yield smoother wave shapes and higher-frequency harmonics but demand more computation.
- Mass Distribution: Total string mass divided evenly among all micro-bodies (\(m_i = M / N\)).
- Pre-tension: The initial strain introduced by stretching the resting constraint lengths beyond their rest distance or setting tight constraints between fixed anchor points.
Setting Up the Engine and Solver
Because musical strings vibrate at high frequencies and involve stiff constraints, standard physics engine settings will produce artificial sagging or solver instability. Increase the engine's iteration count to maintain constraint rigidity:
const engine = Matter.Engine.create({
positionIterations: 10,
velocityIterations: 10
});Disable global gravity so the string maintains an equilibrium state determined solely by tension:
engine.gravity.y = 0;
engine.gravity.x = 0;Constructing the Micro-Body Chain
Create two fixed boundary bodies representing the nut and the bridge of the instrument, then place \(N\) dynamic micro-bodies in a straight line between them.
const N = 30; // Number of micro-segments
const startX = 100;
const endX = 700;
const y = 300;
const segmentLength = (endX - startX) / (N + 1);
const radius = 2;
const bodies = [];
// Create particles
for (let i = 0; i <= N + 1; i++) {
const x = startX + i * segmentLength;
const isBoundary = (i === 0 || i === N + 1);
const body = Matter.Bodies.circle(x, y, radius, {
isStatic: isBoundary,
mass: 0.1,
frictionAir: 0.002, // Damping factor
collisionFilter: { group: -1 } // Prevent self-collision
});
bodies.push(body);
}Linking with Constraints
Connect each body to its neighbor using
Matter.Constraint. To simulate tension, keep the resting
length equal to or slightly less than the initial distance between
adjacent bodies, and set the stiffness close to 1:
const constraints = [];
for (let i = 0; i < bodies.length - 1; i++) {
const constraint = Matter.Constraint.create({
bodyA: bodies[i],
bodyB: bodies[i + 1],
stiffness: 0.95,
damping: 0.01,
length: segmentLength * 0.98 // Slight pre-tension
});
constraints.push(constraint);
}
Matter.Composite.add(engine.world, [...bodies, ...constraints]);Exciting the String (Plucking and Striking)
To generate vibrations, you must introduce initial energy into the system:
- Plucking (Displacement): Displace one or more interior nodes along the vertical axis (Y) before unfreezing them to let them oscillate from rest.
- Striking (Impulse): Apply an instantaneous impulse force to an interior body:
function strikeString(targetIndex, forceMagnitude) {
const targetBody = bodies[targetIndex];
Matter.Body.applyForce(targetBody, targetBody.position, {
x: 0,
y: forceMagnitude
});
}Managing Damping and Stability
- High-Frequency Decay: Set
frictionAirto a small non-zero value (e.g.,0.001to0.005) to simulate internal material friction and atmospheric resistance, causing vibrations to decay naturally. - Time Step Consistency: Run the engine update loop
with a fixed delta time
(
Matter.Engine.update(engine, 1000 / 60)) to prevent energy gain or erratic oscillation behaviors.