I saw Daniel Ingrassia's portfolio and could not stop moving my mouse over it. His hero is a person built out of tiny soft dots, floating in the dark, and your cursor is a small light that moves around him. So I made my own, with my hair, my half-rim specs and my quarter-zip, and it is live at avivashishta.com/lab/made-of-dots.
A ring of dots fills up while the page loads, and the number in the middle is the real download. At 100 every dot leaves the ring and flies into place, so you watch me build myself. As the last ones land, one band of light sweeps down from the top of my hair, so the whole thing shines once. After that your cursor is a light: it lights up whatever it is near, and my head turns to look at it. That is all it does. Every few seconds a blue glint runs across the specs.
The short version: one photo of me went to Codex, which made a clay sculpture version of me (front and side, faceless, no ears, just the hair, the specs, the jaw and the sweater). Higgsfield turned those two images into a 3D model. A Node script spreads 66,000 dots over that model, draws the specs as their own dots, and saves everything as one 680 KB file. The page streams that file for the loader, then one shader runs the entrance, the head turn and the light.
Why not just scan yourself?
I tried three versions before this one, and the failures are the useful part.
- Sculpting the bust from math (ellipsoids and capsules in a signed distance field). It ran, but it looked like a mannequin: too smooth, too symmetric. The reference works because the shape is a real person's.
- Image to 3D straight from my photo. Real likeness, but now it had my nose, my ears and my exact face. In dots, detail like that reads as noise. The reference has no face at all.
- Image to 3D from a clay version of me. This is the trick. Decide what the final shape should be in an image first, where it is cheap to change, then convert that image to 3D. The clay render has exactly what the dots need: my hairline, the specs, a square jaw, a strong neck, the collar and the zip, and nothing else.
Step 1: a clay version of you (Codex)
Give Codex (or any image model) one clear photo of yourself and ask for a sculpture. Run it twice: once for the front, once for a side profile.
A smooth matte light-grey clay sculpture of a head-and-shoulders bust of the man in the attached reference photo. Keep his exact hairstyle as sculpted clay: wavy, textured, swept up and forward on top, short tapered sides. He wears thin rectangular half-rim glasses, sculpted in the same clay, sitting clearly in front of the face. A sharp, strongly defined, square jawline and chin. A thick strong neck, broad shoulders and a quarter-zip sweater with a funnel collar, all in the same clay. Stylised minimal sculpture: the face below the glasses is smooth and featureless, no nose, no mouth, no lips, and no ears (the sides of the head are smooth). Plain pure white background, soft even studio light, the whole bust visible with space around it, nothing cropped. Perfectly straight-on front view, symmetrical, shoulders square to the camera.
For the second image, swap the last sentence for "Exact 90 degree side profile view, facing left." Change the hair, the glasses and the clothes to yours.
Step 2: images to 3D (Higgsfield)
Two views give the 3D model a real side profile instead of a guess. I used Higgsfield's Multi-Image to 3D from the command line; Tripo does the same thing in the browser.
higgsfield generate create multi_image_to_3d \
--image ./clay-front.png --image ./clay-side.png \
--should_texture false --wait
No texture: the dots only need the shape. You get back a GLB.
Step 3: dots on the surface
Three.js ships a sampler that picks random points on a mesh, weighted by triangle area, so the spread is even, the way a scan looks:
const sampler = new MeshSurfaceSampler(new THREE.Mesh(geo)).build();
for (let i = 0; i < 66000; i++) {
sampler.sample(p, n);
dots.push([p.x, p.y, p.z, n.x, n.y, n.z]);
}
Step 4: draw the specs yourself
Thin glasses frames do come through to 3D, but area-weighted sampling gives them a handful of dots, and they disappear. So I draw the frames as their own dots: a rounded rectangle per lens (a dense bar on top, a sparse wire underneath, because they are half-rims), a bridge and two arms. Frames are rigid, so they are one smooth curve across both lenses, fitted to clear the face everywhere they pass (the face comes from a height map built from the bust's own dots):
const lensZ = (x) => front - wrap * Math.max(0, Math.abs(x) - .14) ** 2;
My first try placed every frame dot on the face under it, and the frame bent and broke wherever the face curved. Then I delete the scan's own frame dots, but only those within a few millimetres of mine. Skip that and you see two pairs of glasses when the head turns; make the radius too big and the lenses go empty.
Step 5: bake it, and the loader tells the truth
All of that runs once in Node and writes one file: positions as 16-bit integers, normals as 8-bit, one byte for "bust or specs". 680 KB. The browser hands the arrays straight to Three.js, and because the page waits on exactly one file, the loader can show real progress:
const reader = res.body.getReader();
for (;;) {
const { done, value } = await reader.read();
if (done) break;
got += value.length;
setProgress(got / total);
}
The loader ring is SVG, and the WebGL dots start on that same ring (one pixel is 2 * dist * tan(fov / 2) / height world units at the bust's depth). Each dot takes its start angle and its delay from the same random number, so the ring unwinds into a person.
Step 6: what makes it look premium
This is where my versions looked cheap next to the reference, and the fixes were all small:
- Fewer, softer dots. The reference uses about 44,000 points. I went up to 140,000 tiny hard ones and it looked like sand. Soft dots with a gaussian falloff, sizes varying from small to about three times that, beat more dots every time.
- Even grey, not dramatic lighting. Most of the brightness is constant, so the body reads as a scan. The key light, the rim and the cursor's light only add a little on top. Dark interiors make it look like a render.
- The cursor is only a light. I tried dots that stepped aside and a click that scattered them. Both felt like a toy. The reference is calm, so the cursor lights things and the head turns toward it, and that is all.
- One shine, not constant sparkle. The band of light on load is a single uniform: each dot measures how far it is from a diagonal line that slides down the bust, and gets brighter and a little bigger near it. It runs once and stops.
- A room, not a void. Faint dust drifting through the whole frame and a little film grain over everything.
float b = .3 + key * .14 + lit * 1.5 + rim * .12; // mostly even grey
b *= mix(.55, 1., smoothstep(-.3, .25, facing)); // far side a little darker
b *= .7 + r2 * .5; // no two dots alike
// fragment: soft, like a lens, not a hard pixel
float a = exp(-r * r * 16.);