DEV Community

Printing auxetic lattices: generating gyroid and diamond TPMS meshes with real density control

Auxetic lattices are the structures that get wider when you stretch them - the opposite of ordinary materials. That property makes them excellent for cushioning, prosthetic liners, lightweight panels and things that need to conform under load.

The good news: the two most useful families (gyroid and diamond, both Triply Periodic Minimal Surfaces) fit in about 30 lines of Python, and print fine on a stock FDM machine. Here's how to generate them, and the density trap that most generators don't tell you about.

The math is one formula per lattice

A TPMS is defined by an implicit equation f(x, y, z) = t. Sweep t and you get nested surfaces through the volume. The two workhorses:

import numpy as np

def gyroid_field(X, Y, Z):
    return (np.sin(X) * np.cos(Y) + np.sin(Y) * np.cos(Z) + np.sin(Z) * np.cos(X))

def diamond_field(X, Y, Z):
    return (np.cos(X) * np.cos(Y) + np.cos(Y) * np.cos(Z) + np.cos(Z) * np.cos(X))

The solid is everything where f < t (or > t, depending on which phase you want).

From field to printable STL: marching cubes on a padded grid

from skimage import measure
import trimesh

def lattice_to_mesh(field_fn, size_mm=30.0, n=96, t=0.0):
    xs = np.linspace(-np.pi * 2, np.pi * 2, n)
    X, Y, Z = np.meshgrid(xs, xs, xs, indexing="ij")
    solid = (field_fn(X, Y, Z) < t).astype(float)
    # zero-pad: marching cubes MUST see closed boundaries or your mesh leaks
    padded = np.pad(solid, 1)
    spacing = size_mm / n
    verts, faces, _, _ = measure.marching_cubes(padded, level=0.5, spacing=(spacing,) * 3)
    mesh = trimesh.Trimesh(vertices=verts, faces=faces, process=False)
    if mesh.volume < 0:
        mesh.invert()
    return mesh

Two details that decide whether the print works:

  • Pad before marching cubes. Without the pad, surfaces touching the array edge stay open - the slicer sees a leaky shell and the print fails in ways that waste hours.
  • Run fix_normals with multibody=True. Depending on t, a TPMS has disconnected solid regions; a single-body normal fix flips the wrong walls.

The density trap: target ≠ measured

You'd think "set t so 50% of the voxels are solid, get 50% density." You don't. The isosurface cuts through the boundary voxels, and the faceted surface carves away material, so the printed mesh measures noticeably lighter than the voxel target.

I measured this on gyroid at three target fractions:

target measured (mesh volume)
0.30 ~0.27
0.50 ~0.43
0.70 ~0.64

Roughly a 3-15% loss depending on density - monotonic and predictable, which means you can calibrate: pick the target that lands where you actually want.

If a generator reports only the target fraction, it's hiding the number that matters. Measure the mesh volume, not the voxel count. (t isn't linear in density either - I pick it from the voxel histogram, scanning candidate levels until the histogram fraction matches the target.)

What to print first

A 30 mm cube of gyroid at measured ~0.43 is a good first test: prints without supports (the surfaces self-support at shallow angles almost everywhere), and you can feel the auxetic behavior by pulling opposite faces. Diamond at 0.3 is the stiff-but-light option.

If you want the pipeline without assembling it - level calibration, both lattices, Blender import, and measured-vs-target density reporting per part - that's my LatticeGen plugin (includes a Blender bridge), and PrintIQ adds bed packing and slicer time estimates on top. The pre-print mesh checks behind them are free in stl-precheck.

Start at 0.3-0.45 measured density: that's the sweet spot where auxetic behavior shows up without needing support material.

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.