femdev avatar

Learn Creative Coding (#112) - 3D Printing Generative Objects

femdev

Published: 03 Jul 2026 › Updated: 03 Jul 2026Learn Creative Coding (#112) - 3D Printing Generative Objects

Learn Creative Coding (#112) - 3D Printing Generative Objects

Learn Creative Coding (#112) - 3D Printing Generative Objects

cc-banner

At the end of last episode I told you to start saving your favourite sketches, because some of them were about to become real. Well, here we are. Everything we've built in this whole physical-world chapter has still needed the power on - pull the plug and the plot is just paper, the projection just a dark wall. Today we make something with actual weight. Something you can pick up, hold in your hand, hand to a friend, and keep on a shelf long after the machine is switched off.

We're going to 3D print objects grown from code. And here's the lovely bit: we already know how to make the shapes. Way back in episode 63 we built procedural geometry in Three.js, and in episode 66 we generated meshes from scratch. Those meshes have been living on your screen this whole time, made of nothing. Today we teach them to leave the screen and become plastic. Allez, let's turn maths into a thing you can drop on your foot :-).

What a 3D printer actually does

Let me demystify the machine first, because it sounds more magic than it is. A 3D printer builds an object layer by layer, bottom to top, like a very patient cake. It never sees your beautiful 3D model as a whole - it only ever draws one flat slice at a time, then moves up a fraction of a millimetre and draws the next.

There are two flavours you'll meet most. The first is FDM - fused deposition modelling - which squirts melted plastic filament through a hot nozzle, drawing each layer like a hot glue gun on rails. Cheap, forgiving, the kind of printer that lives in schools and garages. The second is SLA - it uses a UV light to harden liquid resin one layer at a time, and it gives you gorgeous fine detail at the cost of mess and fumes.

// the mental model of every 3D print, in one comment.
//
//   your 3D MODEL  --sliced-->  hundreds of flat LAYERS
//                                     |
//   FDM: hot nozzle draws each layer in melted plastic
//   SLA: UV light hardens each layer in liquid resin
//                                     |
//              stack them up  ->  a solid OBJECT
//
// the printer never sees the "whole" shape. only slices, bottom to top.

The takeaway for us coders: the printer doesn't want your Three.js scene, your shaders, or your animation loop. It wants one thing - a description of the object's surface as a big bag of triangles. That format is called STL, and getting from a Three.js mesh to a clean STL is the whole game.

From Three.js mesh to a printable file

Remember BufferGeometry from episode 63? That's just a list of vertices and the triangles that connect them - which is exactly what an STL file is too. So exporting is almost suspiciously easy. Three.js ships an STLExporter that walks your geometry and spits out the file.

// export any Three.js mesh to an STL string, ready for a slicer.
// STLExporter lives in the examples/ addons, not the core library.
import { STLExporter } from 'three/addons/exporters/STLExporter.js';

function meshToSTL(mesh) {
  const exporter = new STLExporter();
  const stl = exporter.parse(mesh, { binary: false });   // ascii is human-readable
  return stl;   // a big string of triangles - save it as model.stl
}

That STL then goes into a slicer - software like Cura or PrusaSlicer - which is the program that actually chops your model into layers and writes the machine instructions (G-code) the printer follows. You don't write the G-code yourself, thank goodness. You hand the slicer an STL, tell it how big to print and how solid to fill it, and it does the layering maths for you.

Here's a helper to trigger a download of the STL straight from the browser, so your generative sketch can offer a "download for printing" button.

// let the browser download the STL string as a file.
// now your sketch can have a "print me" button next to "save png".
function downloadSTL(stlString, filename = 'generative.stl') {
  const blob = new Blob([stlString], { type: 'text/plain' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  a.click();
  URL.revokeObjectURL(url);   // tidy up after ourselves
}

The catch: printable is not the same as viewable

Now for the reality check, because this is where every beginner (me very much included) gets burned. A mesh that looks perfect on screen can be completely unprintable. On screen, a shape only needs to look right from the outside. A printer needs it to be a genuine solid, and it is a lot fussier.

The magic word is watertight, sometimes called manifold. Imagine filling your model with water - if it would leak, it's not printable. That means: no holes in the surface, no walls with zero thickness, and no faces that pass through each other. On screen a single flat plane looks like a wall. To a printer it's infinitely thin - it has no inside, so there's nothing to print.

// the printability checklist, as notes-in-code (not a program :-)
const mustBeTrue = {
  watertight:  "the surface fully encloses a volume. no gaps, no holes.",
  noThinWalls: "every wall has real thickness. a flat plane = zero = unprintable.",
  normalsOut:  "every triangle's normal points OUTWARD, consistently.",
  noSelfHit:   "the surface never passes through itself (intersecting shapes).",
  oneSolid:    "ideally one connected blob, not floating disconnected bits.",
};

That normalsOut one trips people up. Every triangle has a front and a back, decided by its normal - the little arrow saying "this way is out". If some of your triangles face inward, the slicer gets confused about what's inside versus outside and produces a mangled print. Tools like Meshmixer or the free Meshlab can check and repair all of this before you waste any plastic. Run your STL through a repair pass first - it's saved me more failed prints than I'd like to admit.

Giving your shapes thickness

Because "no zero-thickness walls" is the rule people break most, let me show the fix directly. Say you generated a lovely surface but it's a single-sided sheet. You need to give it a skin - an inner surface and an outer surface with a gap between them. The cheap-and-cheerful way is to push every vertex outward along its normal to make an outer shell, and inward to make an inner shell.

// thicken a surface into a printable shell by offsetting along normals.
// outer shell pushes vertices OUT, inner shell pushes them IN.
// the gap between them is your wall thickness (keep it >= 0.8mm for FDM).
function shellThickness(geometry, mm = 1.2) {
  geometry.computeVertexNormals();          // make sure normals exist
  const pos = geometry.attributes.position;
  const nor = geometry.attributes.normal;
  const half = mm / 2;
  for (let i = 0; i < pos.count; i++) {
    // nudge each vertex outward by half the wall thickness
    pos.setX(i, pos.getX(i) + nor.getX(i) * half);
    pos.setY(i, pos.getY(i) + nor.getY(i) * half);
    pos.setZ(i, pos.getZ(i) + nor.getZ(i) * half);
  }
  pos.needsUpdate = true;
  // (a full solution also builds the inner shell + rims - libraries do this)
  return geometry;
}

How thick is thick enough? An FDM printer draws with a nozzle that's usually 0.4mm wide, so a wall needs to be at least one nozzle wide to exist at all - and honestly you want two or three passes (0.8 to 1.2mm) so it isn't fragile. SLA can go much finer, down around 0.1mm, because it's hardening light instead of dragging plastic. Design for whichever machine you're actually using. This is the big shift from screen work: your geometry now has to survive in the real world.

Overhangs and the gravity problem

Here's a thing screens never made you think about: gravity. When an FDM printer draws a layer, that layer needs something underneath to sit on. If your shape juts out sideways into empty air - an overhang - the melted plastic has nothing to land on and just droops into spaghetti.

The rule of thumb is the 45 degree rule: an overhang steeper than about 45 degrees from vertical needs support material - throwaway scaffolding the slicer adds automatically and you snap off afterwards. Supports work, but they leave marks and waste plastic, so good generative design tries to avoid them.

// flag triangles that will need support: ones whose normal points
// too far DOWNWARD mean an overhang the printer can't bridge in mid-air.
function needsSupport(normal, maxAngleDeg = 45) {
  // normal.y near -1 means the face points straight down (a ceiling).
  const downwardness = -normal.y;               // 1 = faces fully down
  const threshold = Math.cos(maxAngleDeg * Math.PI / 180);
  return downwardness > threshold;              // true = add support here
}

You fight overhangs three ways: orient the print so the tricky bits point up instead of out, add chamfers (angled bevels under 45 degrees instead of flat ledges), or design shapes that are self-supporting from the start. A sphere sitting on a point needs support underneath; the same sphere sliced flat on the bottom prints happily. Small design choices, big difference in how clean the print comes out.

Now the fun part: generative objects

Right, enough rules. Let me show you the kinds of things that are magic to print, because they're basically impossible to make any other way. This is where 3D printing and generative code become best friends.

My favourite starting point is a noise-displaced sphere. Take a plain sphere, then push every vertex out or in by an amount from our old friend 3D Perlin noise (episode 12). Suddenly you have asteroids, alien eggs, weird organic pebbles - each one unique, none of them drawable by hand.

// grow a lumpy organic blob: a sphere displaced by 3D noise.
// same noise from episode 12, now sculpting a REAL object.
import * as THREE from 'three';
import { ImprovedNoise } from 'three/addons/math/ImprovedNoise.js';

function noiseBlob(radius = 20, detail = 5, amount = 6) {
  const geo = new THREE.IcosahedronGeometry(radius, detail);  // even triangles
  const noise = new ImprovedNoise();
  const pos = geo.attributes.position;
  for (let i = 0; i < pos.count; i++) {
    let x = pos.getX(i), y = pos.getY(i), z = pos.getZ(i);
    const len = Math.hypot(x, y, z);
    const n = noise.noise(x * 0.08, y * 0.08, z * 0.08);   // sample the field
    const scale = (len + n * amount) / len;                // push along radius
    pos.setXYZ(i, x * scale, y * scale, z * scale);
  }
  geo.computeVertexNormals();     // recompute so lighting + printing behave
  return geo;
}

An icosahedron gives us nice even triangles to displace, way better than a UV sphere with its pinched poles. Change the amount and you go from "slightly wobbly planet" to "spiky sea urchin". And because it's code, you don't make one - you make a whole family.

Parametric design: a family, not a single object

This is the idea that made 3D printing click for me as an artist, not just a maker. When your object is defined by a few numbers - parameters - you can sweep through those numbers and print a whole series exploring the space between them. Physical generative art. A shelf of siblings, each unique, all clearly related.

// a parametric object family: change the knobs, get a related-but-unique piece.
// print several across a parameter sweep = a physical generative series.
function makePiece({ radius, roughness, spikes, seed }) {
  // seed makes each piece reproducible (episode 24!) - print #7 again anytime
  const geo = noiseBlob(radius, 5, roughness);
  // ...apply 'spikes' twists, etc. all driven by the parameters...
  return geo;
}

// sweep one parameter to generate a whole row of related objects
const family = [];
for (let r = 2; r <= 10; r += 2) {
  family.push(makePiece({ radius: 20, roughness: r, spikes: 8, seed: 42 }));
}
// print all five -> one idea, five physical variations on a shelf

See how episode 24's seed-based art comes back yet again? Seeds mean you can preview fifty objects on screen, pick the best five, and print exactly those - and reprint any of them perfectly if a print fails halfway. That reproducibility is quietly the most useful thing in the whole workflow.

Voronoi shells: turning solid into lace

Here's a technique that makes people's jaws drop, and it's built from something we've already touched - Voronoi patterns, the "nearest seed point wins" partitioning. If you take a solid shape and replace its surface with only the edges of Voronoi cells, you get a delicate open lattice. Solid frame, open holes. Think coral, think a wire birdcage, think a lampshade that throws patterned shadows.

// conceptual voronoi shell: keep only the CELL EDGES, hollow the rest.
// scatter seeds on the surface, build cells, keep the walls as thin struts.
function voronoiShell(baseGeometry, seedCount = 120, strutWidth = 0.8) {
  const seeds = scatterOnSurface(baseGeometry, seedCount);  // random points
  const cells = voronoiCells(seeds);                        // nearest-point regions
  const struts = [];
  for (const cell of cells) {
    for (const edge of cell.edges) {
      struts.push(thickenEdge(edge, strutWidth));   // each edge -> a printable bar
    }
  }
  return mergeToSolid(struts);   // union of all struts = one printable lattice
}

I've left the helpers as concepts because a full manifold Voronoi mesh is genuinly fiddly - in practice you'd lean on a library like a mesh-boolean toolkit to union all those little struts into one watertight solid. But the idea is what matters: you're subtracting material in a controlled, organic pattern, and the result is something that looks grown rather than manufactured. Light passing through one of these is the whole reason I fell in love with printing.

Maths you can hold

Some of the most beautiful prints are pure mathematics made tangible - the kind of surfaces that are hard to even understand on a flat screen but become obvious the second you hold them. A torus knot, a rope looped through itself in a precise pattern. A gyroid, an infinitely repeating minimal surface that shows up in butterfly wings and, weirdly, in nature's own structural engineering. Three.js gives you some of these for free.

// mathematical sculptures Three.js hands you ready-made.
// a torus knot is defined by two integers p and q - try different pairs!
function torusKnotPiece(p = 3, q = 5) {
  // radius, tube, tubularSegments, radialSegments, p, q
  return new THREE.TorusKnotGeometry(16, 4, 220, 20, p, q);
}

// a gyroid is an implicit surface: print where this function crosses zero.
// sin(x)cos(y) + sin(y)cos(z) + sin(z)cos(x) = 0
function gyroidField(x, y, z) {
  return Math.sin(x) * Math.cos(y)
       + Math.sin(y) * Math.cos(z)
       + Math.sin(z) * Math.cos(x);   // marching-cubes this into a mesh
}

The gyroid is an implicit surface - you don't have vertices, you have a function that's positive inside and negative outside, and the surface is where it crosses zero. To turn that into printable triangles you run marching cubes, an algorithm that walks a 3D grid and stitches a mesh along the zero crossing. It's the 3D cousin of the contour-line idea, and it's how you print any shape defined by an equation. Menger sponges (a 3D fractal, cousin of the Mandelbrot work from episode 41), minimal surfaces, whatever the maths dreams up - marching cubes makes it real.

// the shape of using marching cubes on any implicit function.
// walk a grid, ask the field's sign at each corner, emit surface triangles.
function implicitToMesh(field, size = 40, res = 64) {
  const geometry = marchingCubes(field, {
    bounds: [-size, size],
    resolution: res,          // higher = finer detail, bigger file
    isoLevel: 0,              // the surface lives where field() == 0
  });
  geometry.computeVertexNormals();
  return geometry;            // now watertight-check it and print!
}

Even our botanical friends from the L-system episodes (54 and 55) can make the jump - a 3D L-system tree, its branches thickened into printable tubes, becomes a tiny sculpture. Every generative technique in this series is a potential object. That's the thing I want you to really feel: nothing here is new code. It's all shapes we already know how to grow, pointed at a machine that makes them solid.

After the print: the finishing is the craft

Serious face for a second, like every episode in this chapter. A raw print off the bed looks... printed. You can see the layer lines, there might be little support scars, FDM prints have a matte ridged texture. Turning that into something that looks like a designed object is a whole craft on its own, and it happens with your hands, not your code.

// femdev's finishing cheat sheet (notes-as-code, not a program :-)
const finishing = {
  sanding:    "wet-sand up through grits to knock down layer lines. patient work.",
  primer:     "a coat of filler-primer hides ridges before paint. game changer.",
  paint:      "acrylics or spray. suddenly it looks bought, not printed.",
  vaporSmooth:"ABS + a little acetone vapour = glossy melted-smooth surface (ventilate!).",
  clearcoat:  "a matte or gloss seal protects paint and unifies the look.",
};

The finishing genuinely is part of the art. I once printed a noise-blob that looked like grey plastic junk, sanded it, primed it, painted it a deep metallic bronze, and people at a little maker night refused to believe it came off a hobby printer. Same object, totally different soul - all in the finishing. Don't judge a print until you've given it that love.

Scale, and getting bigger than your printer

A desktop printer usually maxes out around a 20cm cube. That feels limiting until you realise two escape hatches. For big work, print in sections and glue or bolt them together - plenty of huge sculptures are secretly a jigsaw of printable-sized chunks. For tiny, precious work, an SLA resin printer gives you jewellery-scale detail an FDM machine could never manage.

// auto-split an object too big for the bed into printable chunks.
// slice along a plane, print each half, glue with a registration peg.
function splitForBed(geometry, bedSize = 200) {
  const box = boundingBox(geometry);
  if (box.maxDimension <= bedSize) return [geometry];   // fits, no split
  const pieces = sliceByPlane(geometry, chooseCutPlane(box));
  // add little pegs + holes at the cut so the pieces align when glued
  return pieces.map(addAlignmentPegs);
}

And if you don't own a printer at all - most people don't, and that's completely fine - you upload your STL to a service like a print bureau or a makerspace and they mail you the object in plastic, resin, even metal or ceramic. Your code becomes a physical thing shipped to your door. Wild, when you think about it.

Your exercise: a Voronoi lampshade

Time to build the thing that hooked me. We're going to make a generative lampshade - a sphere whose surface is a Voronoi lattice, so it's a solid open frame that light pours through, throwing a unique patterned shadow on your walls. Code becomes light becomes object becomes actual home decor. Here's the plan stitched together.

// the full pipeline: generate -> shell -> voronoi -> export.
// a one-of-a-kind lampshade grown from a seed number.
function makeLampshade(seed = 7) {
  randomSeed(seed);                                  // reproducible (episode 24)

  // 1. start from a lightly noise-warped sphere so it isn't boringly round
  let geo = noiseBlob(40, 4, 3);                     // gentle organic wobble

  // 2. carve the surface into an open Voronoi lattice
  geo = voronoiShell(geo, 140, 1.0);                 // struts 1mm thick

  // 3. make sure it's a real printable solid, then hand back the STL
  const mesh = new THREE.Mesh(geo);
  return meshToSTL(mesh);                            // -> download & slice & print
}

// wire it to a button in your sketch:
//   download the STL, upload to a slicer, print in white PLA,
//   drop a small LED puck inside (a SAFE cool LED - episode 109!), done.

The ritual: generate a handful of seeds, spin them around on screen, pick the one whose shadow pattern you love, download its STL, run it through a slicer (thin walls, maybe no infill since it's already a lattice), and print it in a pale translucent filament so the light glows through. Pop a battery LED puck inside - and please a cool safe LED, remember the heat warnings from the LED episode, not a hot bulb. You just made a lamp that exists nowhere else in the world.

Stretch goals, in rising order of "whoa":

  • Swap the Voronoi seeds for points placed by a reaction-diffusion pattern - organic, coral-like holes instead of cells.
  • Drive the strut thickness by height so the shade is dense at the base and lacy at the top.
  • Print a matched set from a parameter sweep - three lampshades that are clearly siblings.
  • Feed an LED strip (episode 109) inside and animate the light so the shadows breathe.

Do at least the basic lattice sphere if you do nothing else. There is a very specific joy the first time you switch on a lamp you grew from a maths function and watch its shadows land on your ceiling. It's the same lineage of joy as your first plot, your first laser cut, your first reactive wall - but this one you can wrap up and give to someone.

't Komt erop neer...

  • A 3D printer builds objects one flat layer at a time, bottom to top. FDM melts plastic filament (cheap, forgiving), SLA hardens liquid resin with UV (finer detail). Either way, it wants your object's surface as a bag of triangles - the STL format
  • Getting there from Three.js is easy because BufferGeometry (episode 63) is already vertices and triangles. STLExporter turns any mesh into an STL string you can download; a slicer (Cura, PrusaSlicer) chops it into layers and machine instructions
  • Printable is NOT the same as viewable. A mesh must be watertight (manifold): no holes, no zero-thickness walls, normals all pointing outward, no self-intersections. A flat plane looks like a wall on screen but has no inside to print. Repair STLs in Meshmixer or Meshlab first
  • Give surfaces real thickness by offsetting vertices along their normals into a shell. FDM walls want >= 0.8mm (nozzle is ~0.4mm); SLA can go to ~0.1mm. Your geometry now has to survive in the real world
  • Gravity matters: overhangs steeper than ~45 degrees droop and need throwaway support material. Fight it by reorienting the print, adding chamfers, or designing self-supporting shapes
  • Generative objects are where print + code become magic: noise-displaced spheres (episode 12) for organic blobs, Voronoi shells for open lattices, mathematical surfaces, L-system sculptures (episodes 54/55). None of it is new code - just shapes we already grow, made solid
  • Parametric design means sweeping a few numbers to print a whole family of related-but-unique objects. Seeds (episode 24) make each one reproducible - preview many, print the best, reprint any perfectly
  • Maths you can hold: torus knots, gyroids and other implicit surfaces (positive inside, negative outside) become printable via marching cubes, the 3D cousin of contour lines. Menger sponges tie back to the fractal work in episode 41
  • Finishing is its own craft and genuinely part of the art: sanding, filler-primer, paint, acetone vapour smoothing, clearcoat. The same object goes from "grey plastic junk" to "designed object" entirely by hand
  • Beat your printer's size limits by splitting big work into glued sections or going SLA for tiny detail - and if you own no printer, a print bureau will mail you the object in plastic, resin, or metal

So that's the biggest leap yet in this chapter: your generative shapes stop being light and start being matter. And did you catch the pattern one more time? Noise, seeds, Voronoi, L-systems, fractals - all tools we already owned, now feeding a machine that turns them into objects with weight and edges. The screen was never the limit. It was just the sketchbook.

We've plotted, cut, lit, projected, and now printed. But every one of these has been a single machine doing a single trick. What happens when you want something that senses, decides, and moves on its own - a thing with a little computer inside it, out in the world, running your code untethered from any laptop? That's a different kind of making entirely, and it's where we're heading next. Start thinking about what you'd build if your code could get up and walk around :-).

Sallukes! Thanks for reading.

X

femdevHive account@femdev

Leave Learn Creative Coding (#112) - 3D Printing Generative Objects to:

Written by

Software developer from Antwerpen. When I am not coding I am cycling along the Schelde or hunting for the best koffiekoeken in town. She/her.

Read more #stem posts


Best Posts From femdev

We have not curated any of femdev's posts yet. But you can encourage our curation team to review posts by visiting them regularly and by referring other readers. Because we give priority to frequently read content.

More Posts From femdev