Building a tape spaghetti simulation

Hi all,

In the middle of a recent Youtube rabbit hole, inspiration struck. The best description I can come up with for what we’re seeing is “writhing tape spaghetti”. I now want to recreate this in code.

Watch it in motion, it’s so mesmerizing! (The sequence starts at the 01:06 mark.)

The reason I don’t have any code to show yet is that a) I’m unsure where to start and b) there’s a nagging voice in the back of my mind telling me that trying to simulate such a fundamentally chaotic system might be close to futile.

For now, I’m trying to define the boundaries of what might constitute such a simulation. Here’s some first thoughts I’m operating with:

  • I’m concentrating on 2D only.
  • My initial thought was “rope simulation”. Though looking at simulations like this or this, I’m unsure if this is the correct approach. In those sketches the rope only gets influenced by external forces. But I think the tape needs to react to internal forces instead.
  • More specifically, instead of the tape being “floppy” or infinitely flexible, I believe the tape needs to have a small, but existing stiffness to it. Otherwise the tape would not lay down those wonderful curvy loops.
  • The tape (or it’s points) also needs mass to prevent the rope from intersecting itself.
  • Another difficulty may arise when the tape starts rubbing up against itself and pushes parts of itself around. I know that whenever you have forces between points that basically inhibit the same space, things can explode very easily. So maybe there needs to be some minimal safety distance between points/segments on the tape at all time?
  • I’m not trying to make the simulation be an exact replica of what we see in the video. Anything approaching that loopy, writhing, chaotic behaviour is a win.
  • A static interpretation of these visuals would be far easier to build – likely built upon a string of bezier curves. But it would also not be a good foundation to eventually expand into a moving simulation.

If anybody has ideas or experience with building something like this tape spaghetti simulation, I’d be super thankful for any pointers.

3 Likes

Update:

I wanted to see if anything about this idea is viable, so I cheepeeteed a prototype together. It took several iterations, but I’m quite frankly shocked that it managed to arrive at a thing which has first semblances of the writhing tape behaviour. Find the code below.

Some comments and learnings from along the way:

  • This is AI-generated / human-curated code.

  • ClickAndDrag any point on the tape to move it around.

  • Changing stiffness and damping variables give the tape different writhing characteristics.

  • As I posited earlier, the intersection of the tape upon itself is prevented by applying a repulsion force between all points on the tape. It works, but only if you move things around sloooowly. If you move too fast, things can easily intersect again. Though watching the tape “self-unfurl” is kinda neat.

  • The “stiffness” of a band is the bending resistance or angular stiffness. In essence, the stronger the bend at any point (the smaller the angle between the two links to the neighbouring points), the stronger it’s effect on it’s neighbours become.

  • I’m still not sure what to make of AI-generated sketches. For one, I’d absolutely not have been able to build something close like this, in a decent amount of time, on my own. But I hate that I don’t really understand the sketch clearly, line for line. It’s not “mine”. But maybe it’s fine as a rough prototype?I remain cautiously intrigued.

  • Is there a good verb for using ChatGPT? * cheepeeteed* feels… unwieldy. prompted? ai’d? chatted?

int numPoints = 80;         // Number of points on the band
float spacing = 10;         // Fixed length between points
int constraintIterations = 10; // Number of iterations for enforcing constraints
float damping = 0.5;       // Damping factor for velocity
float stiffness = 0.3;      // Stiffness coefficient for angular resistance
int selectedPoint = -1;     // Index of the point being dragged

float repulsionThreshold = 20; // Minimum distance between segments to avoid collision
float repulsionStrength = 0.5; // Strength of repulsive forces


PVector[] points;           // Current positions of the points
PVector[] prevPoints;       // Previous positions for Verlet integration

void setup() {
  size(800, 600);
  points = new PVector[numPoints];
  prevPoints = new PVector[numPoints];
  
  // Initialize points along a straight line
  for (int i = 0; i < numPoints; i++) {
    points[i] = new PVector(100 + i * spacing, height / 2);
    prevPoints[i] = points[i].copy(); // Start with no initial motion
  }
}

void draw() {
  background(30);
  
  // Draw the band
  stroke(200);
  strokeWeight(2);
  for (int i = 0; i < numPoints - 1; i++) {
    line(points[i].x, points[i].y, points[i + 1].x, points[i + 1].y);
  }
  
  // Draw the points
  noStroke();
  fill(255, 100, 100);
  for (int i = 0; i < numPoints; i++) {
    ellipse(points[i].x, points[i].y, 10, 10);
  }
  
  // Update physics
  updatePhysics();
  
  // Enforce constraints
  enforceConstraints();
  
  // Apply bending stiffness
  applyBendingResistance();
  
  // Handle self-intersections
  handleCollisions();
}

void updatePhysics() {
  for (int i = 0; i < numPoints; i++) {
    if (i != selectedPoint) { // Skip the dragged point
      // Verlet integration with damping
      PVector temp = points[i].copy();
      PVector velocity = PVector.sub(points[i], prevPoints[i]); // Current - Previous
      velocity.mult(damping); // Apply damping to the velocity
      points[i].add(velocity); // Update the current position
      prevPoints[i] = temp; // Store the old position
    }
  }
}

void enforceConstraints() {
  for (int k = 0; k < constraintIterations; k++) { // Iterative solver
    for (int i = 0; i < numPoints - 1; i++) {
      PVector dir = PVector.sub(points[i + 1], points[i]);
      float dist = dir.mag();
      float error = dist - spacing; // How far the distance is from the desired length
      dir.normalize();
      dir.mult(0.5 * error); // Distribute the correction equally
      
      // Move points to satisfy the distance constraint
      if (i != selectedPoint) points[i].add(dir);
      if (i + 1 != selectedPoint) points[i + 1].sub(dir);
    }
  }
}

void applyBendingResistance() {
  for (int i = 1; i < numPoints - 1; i++) { // Skip endpoints
    PVector prevSegment = PVector.sub(points[i], points[i - 1]);
    PVector nextSegment = PVector.sub(points[i + 1], points[i]);
    
    prevSegment.normalize();
    nextSegment.normalize();
    
    float cosTheta = PVector.dot(prevSegment, nextSegment); // Cosine of the angle
    float angleDeviation = 1 - cosTheta; // Larger when angle is smaller
    
    PVector correction = PVector.add(prevSegment, nextSegment);
    correction.normalize();
    correction.mult(angleDeviation * stiffness); // Scale correction by stiffness
    
    // Apply correction to neighboring points
    if (i - 1 != selectedPoint) points[i - 1].sub(correction);
    if (i + 1 != selectedPoint) points[i + 1].add(correction);
  }
}

void handleCollisions() {
  for (int i = 0; i < numPoints - 1; i++) {
    for (int j = i + 2; j < numPoints - 1; j++) {
      // Ignore adjacent or overlapping segments
      if (abs(i - j) <= 1) continue;

      float dist = segmentDistance(points[i], points[i + 1], points[j], points[j + 1]);
      if (dist < repulsionThreshold) {
        applyRepulsion(i, j, dist);
      }
    }
  }
}

// Calculate the minimum distance between two line segments
float segmentDistance(PVector p1, PVector p2, PVector q1, PVector q2) {
  // Check all point-to-segment distances and take the minimum
  float d1 = pointToSegmentDistance(q1, p1, p2);
  float d2 = pointToSegmentDistance(q2, p1, p2);
  float d3 = pointToSegmentDistance(p1, q1, q2);
  float d4 = pointToSegmentDistance(p2, q1, q2);
  return min(min(d1, d2), min(d3, d4));
}

// Calculate the distance from a point to a line segment
float pointToSegmentDistance(PVector p, PVector a, PVector b) {
  PVector ap = PVector.sub(p, a);
  PVector ab = PVector.sub(b, a);
  float t = constrain(ap.dot(ab) / ab.magSq(), 0, 1); // Project p onto ab, clamped to segment
  PVector projection = PVector.add(a, ab.mult(t));
  return PVector.dist(p, projection);
}

// Apply a repulsive force to separate two intersecting segments
void applyRepulsion(int i, int j, float dist) {
  // Calculate the correction vector
  float correctionMagnitude = map(dist, 0, repulsionThreshold, repulsionStrength, 0);
  PVector correction = PVector.sub(points[j], points[i]).normalize().mult(correctionMagnitude);

  // Apply corrections to segment points
  if (i != selectedPoint) points[i].sub(correction);
  if (i + 1 != selectedPoint) points[i + 1].sub(correction);
  if (j != selectedPoint) points[j].add(correction);
  if (j + 1 != selectedPoint) points[j + 1].add(correction);
}


void mousePressed() {
  // Check if a point is clicked
  for (int i = 0; i < numPoints; i++) {
    if (dist(mouseX, mouseY, points[i].x, points[i].y) < 10) {
      selectedPoint = i;
      break;
    }
  }
}

void mouseDragged() {
  if (selectedPoint != -1) {
    // Move the selected point with the mouse
    points[selectedPoint].set(mouseX, mouseY);
  }
}

void mouseReleased() {
  if (selectedPoint != -1) {
    // Reset velocity by syncing current and previous positions
    prevPoints[selectedPoint] = points[selectedPoint].copy();
    selectedPoint = -1;  // Release the selected point
  }
}
3 Likes

So you want to be able to make this. Or

Or this.

The code you posted is pretty good. For a lot of points, I use a grid of linked lists of the particles for the collision detection and avoid PVector in favor of just float x, float y. The particles are in a wrapping list – particle i tries to stay near i+1 and i-1 while pushing off of all the others.

You want the particles to not just form a chain, but also try to form a straight line. Rather than think about springs or constraints, I think of each triple of points A-B-C going in each direction. A-B form a line and point C has a force that pushes it to be on that line just beyond B. B then experiences the opposite force to balance things out.

Avoiding self-intersection is the biggest challenge. It helps to have the integrator take many small steps per draw and to keep the chain forces smaller than the collision ones. It would likely also be better to have the chain segments repel each other rather than just the particles, but I haven’t worked out how to do that yet.

You can also go 3-D.

4 Likes

Oh wow, that’s precisely what I imagined this pot of tape spaghetti to look like. No surprise at all, that this strange idea would already have been explored by someone.

With that second example, how did you achieve the colouring? Are you creating PShapes from the individual lines and then applying a fill? Also, do you add in new points as a figure elongates? I recently saw a differential growth tutorial and this reminded somewhat of it.

I’m fully willing to dump my prototype code in favour of rebuilding it from the scratch with something more deliberate. Will definitely take your pointers into account. Thanks!

1 Like

Yes, it’s multiple nested loops rendered by filled PShapes, e.g. https://genart.social/@scdollins/111552398738563736

You can find many more of these if you scroll back through my genart.social or my now-defunct https://x.com/scdollins account. It looks like I started posting these around April of 2022. But, of course, I was inspired by someone else who had posted something similar before.

3 Likes

Progress report:

I’m certainly seeing why you called this an incredibly fickle system. I rarely manage to create a continuous whole system.

Right now, I’m starting every system with 3 particles. So I can have a basic loop, where every particle therein has a n-1 and n+1 link. Then I insert a new particle every cycle to that loop, making it grow.

Every system starts out small (image1) and then slowly grows and expands. Though most of the time some link manages to tear apart, and I end up with several spaghetti segments (image2). They may look disconnected, but if I show all line connections you can see that the loop is still closed (image3).

I need to be more deliberate when adding in the new particles and prevent the tears from happening.

r

1 Like

Progress Report II:

We’re getting somewhere now. Still not without flaws – looking at you, random intersections – but far more stable than previously. Still need to dial in the sweet spot for all the forces. And also need to make the band “stiffer” or “straighter”.

Some more ovservations:

  • The trick for more stability at the beginning is to not add in new particles every draw cycle. And to also not draw in every new particle at the same position in the band – e.g. after the first position – but distributed at random positions along the whole band. This prevents particles from bunching up at the same position, resulting in issues with overlapping forces. Instead, the whole band can more easily accommodate and “even out” with every new particle.
  • I also added rudimentary spatial partitioning, squeezing significantly more performance out of the whole sketch. I’m not very knowledgable on that topic and at 35k particles it’s obviously only managing to creep along, but I’m happy that it’s able to reach that point. Image1 has a larger grid size than image2, hence the worse frame rate given the number of particles.
  • Omitting PVectors didn’t give me any noticeable performance gains though. Maybe I’m doing it wrong?
  • Maybe shaders? But I know zero about them, so that’s a wholly new topic I would have to tackle. 'Tis for another day.
  • Only when creating a screenshot do I draw the connectors between the particles – makes the band more visible but massively tanks the frame rate. Otherwise I’m just drawing the particles as points.

2 Likes

I start with between 100 and 1000 particles in a squiggly circle and introduce a few particles each frame at random locations along the loop. I also start them with a small radius and grow them gradually so that they don’t immediately violently shove their neighbors apart. New particles start halfway between their loop neighbors but with a small jitter so that they will bend off to one side or the other.

Try weakening your chain forces a bit vs the collision forces. Or try shortening the chain distance between particles relative to the collision distance leaving less room for them to slip through.

Rendering with beginShape() should be pretty fast, but I switched to opengl calls, rendering quads, for much greater speed and flexibility with the coloring – animating colors with shaders. https://x.com/scdollins/status/1554236331462578176

2 Likes

So many good tips, thanks! A lot to investigate.

As a side-note, I also just remembered this recent post:

I tested it out and replaced every point() in my sketch with rect(). At 35k particles (not drawing any connector lines) it does indeed result in a slight improvement, 14 vs 10 fps.

1 Like

Using P2D or P3D and beginShape(LINES); with vertex() should be faster since it will batch all of the graphics calls together.

Edit: without LINES. The default is a line strip, though they don’t call it that.

1 Like

Hi guys i a read everything and i really like where this thread is going. i might play with the concept and see what i can make out of it. if ill do, ill post the results here.

1 Like

Another day, some more progress:

Introducing PShape absolutely leads to a noticable improvement in performance. And the suggestion to start every system by arranging its initial set of particles on a circle, makes for a far more controlled starting phase.

Still haven’t managed to introduce “stiffness” between the particles, leading to straighter segments and more even curves.

Another topic is how new particles get added to the band. As I have it right now, when a particle is added somewhere between two existing particles, it “blips” into existence, adding a little shock to the surrounding band. This is visible in the outlined visuals as those little triangular snags (for example in image1 in the top-right dark grey system. I need a way to add in new particles less violently. Probably a matter of balancing out all the forces.

Now also the fun starts with exploring different starting configurations and visualisations. Next I want to explore introducing systems with different behaviours.

3 Likes

If you start the particles small and grow them into their target size, they can smooth out the transition. You can then also give them different target sizes:

2 Likes

Something a bit similar I’ve made a while ago (this is based on Belousov–Zhabotinsky reaction, I have hundreds of screenshots):



Animations are very slow, even with FX2D renderer (using just pixels)…

1 Like

Dam, that’s pretty cool. would you share the code?

1 Like

The base code is from the book “Creating Procedural Artworks with Processing - A Holistic Guide” by Penny de Byl. I’m not sure if I am allowed to post it here due to copyright issues (if any). I have about 10 sketches that’s based on that but I’ve essentially just sped it up somewhat, played with values, changed the formulas and adapted it in different ways (so it can be applied to photos or be sound-reactive etc.)

1 Like

I can highly recommend these two great video tutorials on Reaction Diffusion algorithms.

Also, I love how the answer to “Did Dan Shiffman cover that topic?” turns out to be “YES!” for 99% of the times.

1 Like

I’ve watched both videos and I agree (on both accounts :D)

2 Likes

Thanks a lot ill make sure to check that out!