← All posts

Building The Blob with Three.js and Rapier

March 24, 2026

This portfolio is mostly a collection of prototypes — things I built to learn something, explore an idea, or just see if I could make it work. The blob is a good example of how that process goes.

I've been playing with Three.js on and off for a couple of years. Not for any specific project — just exploring the library, following tutorials, seeing what's possible. One of those was Robot Bobby's liquid glass tutorial, a breakdown of the metaball technique using MarchingCubes. I built it, liked how it looked, and kept it around.

When I decided to build this portfolio in Next.js, I wanted to do more than just a static page. I had this blob prototype sitting there and thought it was a good fit — something alive in the background, something with personality. The challenge became: how do you take a vanilla Three.js experiment and integrate it properly into a Next.js app? That question turned into this: wiring in Rapier physics, connecting the scene to portfolio section transitions, and adding mouse interaction. Building it taught me more about Three.js internals than I expected.


Architecture

The stack is React Three Fiber for the scene graph, Rapier for physics simulation, and MarchingCubes for the isosurface rendering that gives metaballs their signature merged-blob look.

The main architectural decision was keeping all physics bodies alive at all times rather than mounting and unmounting them as sections change. The active count adjusts per section — some sections show more blobs, others fewer — but nothing is ever destroyed and recreated. This keeps transitions smooth without any visual pop.

Each section also has its own lighting config, so the blob color shifts alongside the accent colors of the UI as you scroll.


MarchingCubes Gotchas

The first hard bug I hit was color. The MarchingCubes API accepts a color parameter, and the docs show passing a hex integer. That's wrong for the version I was using — it expects a proper color object. The failure mode is subtle: the geometry renders fine, but everything comes out black or with wrong tints. I spent a while suspecting the material before tracing it to that one argument.

The second gotcha: MarchingCubes regenerates its full geometry every frame. You have to clear it at the start of each frame before re-adding all the balls. Miss that and balls accumulate — the isosurface grows into a single blob that swallows the screen.


Physics Integration

Rapier's physics component has an interpolation option that smooths visual positions between fixed simulation steps. Normally that's what you want. For metaballs it's the wrong behavior.

The reason: MarchingCubes rebuilds its shape from the raw physics positions I feed it each frame. If the physics engine is also applying a smoothed transform to the mesh separately, the two sources of truth diverge and the geometry ends up in the wrong place. Disabling interpolation and reading positions directly keeps everything in sync.


Mouse Interaction

A special physics body tracks mouse position. Unlike regular simulated bodies, this one is positioned directly each frame — I convert the mouse coordinates to world space and move the body there. The blobs react to it through normal physics collision, so it feels natural without any special-case logic.

One initialization detail worth noting: without a deliberate starting position, this body spawns at the center of the screen and sends the blobs scattering before the user has touched anything. Parking it off-screen on load fixes that.


Section Transitions

As you scroll between sections, the blob density changes. Because bodies are never unmounted — just repositioned — the transition is seamless. The ones not needed for the current section are moved out of the visible area and stop affecting the rendered shape.


Key Takeaways

  • Start from a good foundation. Building on Robot Bobby's tutorial let me focus on the integration challenges rather than figuring out MarchingCubes from scratch.
  • Read the source, not just the docs. The color parameter behavior wasn't what the docs described. When something doesn't work and the docs say it should, check the actual library version you're running.
  • Physics interpolation and geometry generators don't mix. When you're feeding raw positions to something that rebuilds geometry each frame, you need one consistent source of truth for those positions.
  • Keep things alive instead of remounting. Adjusting what's active is simpler and smoother than destroying and recreating physics bodies on every transition.
  • Mind the initial state. Bodies that start at the wrong position create visible glitches before the user has done anything. Get the starting state right.