Menu
Projects

WebGL

Three.js Examples

The Three.js work built for this site, gathered on one page: a liquid orb whose vertex and fragment stages are hand-written GLSL, plus the loading and rendering rules that keep a real-time scene from taxing everything around it.

Live WebGL, not a capture. Move your cursor across the orb.

In simple terms

A live exhibit of the 3D graphics work on this site. The centerpiece is a liquid orb, drawn in real time by your graphics card, that reacts when you move your cursor across it.

The orb used to be the first thing you saw on the landing page. When that page was redesigned to be calmer, the work was too good to throw away, so it lives here as a running exhibit instead of a screenshot of one.

The page also explains the manners: the orb stops rendering the moment it scrolls out of view, and no other page on the site downloads any of the 3D code.

The impact. It settled a house rule about where heavy graphics belong: the pages you pass through every day stay light, and the pages you choose to visit are allowed to spend.

Why this page exists

The orb above used to open this site: for a while it was the landing hero. When the landing page calmed down, a real-time WebGL scene stopped earning its place there, but the work behind it was worth keeping. The rule now is placement rather than abstinence: Three.js lives here and inside Visualizer Eden, and nowhere else on the site. The pages you pass through stay light; the pages you choose to visit can spend.

The liquid orb

The orb is one Three.js mesh carrying a ShaderMaterial with both stages written by hand. The vertex stage stacks three sine waves for the wave motion, adds a bulge term from multi-octave value noise (a six-octave fbm), and a viscous stretch along the vertical so the blob reads as liquid rather than as a wobbling sphere. When the cursor is over the canvas, vertices near it are pulled toward it, bulged, and rippled, so the surface answers you instead of just looping.

The fragment stage does the material: a fresnel term that foams the rim, a fake subsurface scatter from above, cavity shadows in the crevices, film grain, and a saturation push at the end. The three small droplets orbiting the main body are the one part that is not custom GLSL: they are meshPhysicalMaterial spheres with clearcoat, because a stock material was already the right look there and a shader would have been vanity.

The card this page has on the projects wall is the same object again, written a third way. A plate cannot import Three.js without handing the whole projects page a payload it has no use for, so the orb is restated there as a single raymarched fragment shader carrying the same displacement, the same palette and the same rotation. Three renderings of one shape, and the arithmetic is the part that travels.

Keeping it polite

A portfolio page has no business running a render loop you cannot see. An IntersectionObserver flips the canvas frameloop between running and stopped as the orb enters and leaves the viewport, and device pixel ratio is clamped to 1.5 so a retina laptop does not pay four times the fragment cost for sharpness nobody perceives on a moving surface.

Pointer handling follows the house rule that scroll-linked and animated state never touches React: the wrapper writes cursor position into a ref, the frame loop reads it into a shader uniform, and no pointer move ever re-renders the tree. The canvas itself keeps pointer-events: none so it can never eat a scroll. On narrow viewports the orb drops its droplets, slows its motion, and softens the grain, because small GPUs and small pixels turn both into shimmer. And the whole Three.js payload is chunk-split behind a dynamic import, so no other route on the site downloads a byte of it.

The one rule that took a bug to learn: draw one frame before any of that gating runs. Animation frames do not fire in a background tab, so a page opened into one and left there paints nothing at all until you look at it, and the first thing you see on arrival is an empty frame filling in. One unconditional frame at setup costs nothing and removes the whole class of problem.

Visualizer Eden: the same stack, driven by sound

The larger piece of this body of work is Visualizer Eden, and it is the same three parts as the orb: an @react-three/fiber canvas, a high-poly mesh, and a ShaderMaterial whose vertex stage does all the interesting work. It adds one thing the orb does not have: a clock it did not write.

A Web Audio AnalyserNode sits in-line with playback, and every frame the byte spectrum is reduced to four numbers, bass, mid, high and overall volume, which are written straight into the material's uniforms alongside the control values. So where the orb displaces itself against time, that mesh displaces itself against the music, and the shader is the only thing that knows the difference. It also takes OrbitControls from drei, which is the one place on this site where the camera is yours to move.

The orb here is the etude. That one is the piece: the same instrument, played against a signal instead of a clock.

Brooklyn Dead: the same geometry, a different runtime

The other 3D work on this site is Brooklyn Dead, and it earns a section here for the contrast rather than the overlap. Nothing in that project runs Three.js: geometry is generated by Blender's Python API, exported as glTF 2.0, and rendered by Godot 4.

What connects them is that export format. glTF is the interchange format Three.js loads natively through GLTFLoader, so an asset that satisfies that pipeline's validators is already a web asset: the same file the game opens would drop into the scene on this page. That is why Three.js sits in that project's skill list next to Godot. The pipeline targets a format, not an engine.

The difference is worth stating plainly, because it is where the two halves of 3D work actually diverge. Everything on this page computes its geometry live, on the GPU, from a handful of uniforms, and nothing survives the frame it was drawn in. That pipeline does the opposite: geometry is written ahead of time by a Python file, gated by validators that refuse a bad rebuild, and handed to an engine as an asset on disk. One is a surface you are drawing right now; the other is a thing you made that has to still be correct tomorrow.