Procedural material · Substance 3D Designer

Hyperbolic Lace Spiral

Five ribbons of lace spiralling into a point, built from an infinite tiling that can't exist on a flat surface. I didn't place a single node by hand. Claude wrote the whole graph as Python and I steered.

Output
4K, 16-bit heightmap
Graph
41 nodes on top, about 1,880 inside
Built with
Substance 3D Designer, Python, Claude Code
Inspired by
Andrei Zelenco's procedural lace
Five lace ribbons spiralling into a centre point, white thread on black, rendered as a heightmap
The finished heightmap. Rosettes, petals and connecting bars woven over and under, twisted cord on every ribbon edge, a hex tulle net behind.
Why I made this

Treating a material graph like code

I've been obsessed with Andrei Zelenco's lace in Substance Designer. He builds it by hand and you can see every node. I wanted to know what happens if you go the other way and let code write the graph.

So I set up a small bridge: a local server running inside Designer that accepts Python. Claude wrote scripts that create nodes, wire them, write the per-pixel functions and render the result. My job was direction and judgment. I picked the geometry worth chasing, looked at every output, and threw away most of them.

The lace you see is the one that survived. Everything below is how it works.

The math

Four steps from one triangle to a spiral

Every pixel in the image runs the same short pipeline. It works backwards: start from where the pixel sits in the spiral, unwrap it to a straight strip, fold the strip back into a disk, then fold the disk down to one small triangle where the motif actually lives. These four steps run in that order inside one Pixel Processor.

STEP 1 · THE TILING

Heptagons that overfill every corner

The lace sits on a {7,3} tiling: regular seven-sided tiles, three meeting at every corner. On a flat surface that's impossible. A regular heptagon's corner is about 128.6°, and three of them add up to 385.7°, more than a full turn. So the tiles live in the hyperbolic plane instead, drawn here as the Poincaré disk. The tiles are all the same size in that geometry. They only look smaller as they pack infinitely toward the rim. It's the same family of geometry Escher used for his Circle Limit prints.

The whole tiling is built from one triangle (copper, right) with angles π/7, π/3 and π/2. Three mirrors bound it: the horizontal axis, a ray at π/7, and a circle that crosses the rim at right angles, which is what a straight line looks like in this geometry.

D² − R² = 1circle centre D = 2.0122, radius R = 1.7461 (orthogonal to the unit disk)
Poincare disk tiled with alternating dark and light triangles, one copper triangle at the centre
{7,3} in the Poincaré disk. Triangles shaded by how many mirror flips it takes to reach them. Copper is the fundamental triangle, the only place the motif is ever drawn.
STEP 2 · THE FOLD

Reflect until you land home

One small motif (a rosette, some petals, a few connecting bars) is drawn once with FX-Map stamps, inside that single triangle.

For every pixel, a loop rotates the point into the right wedge, flips it across the π/7 ray if it's on the wrong side, and inverts it through the mirror circle if it's inside. Repeat until nothing moves, at most 40 passes. Now the pixel is in the copper triangle and reads the motif there. That one loop turns one triangle into the whole infinite pattern.

The loop also counts its flips. Odd or even decides which strand goes on top at each crossing, and because neighbouring triangles always differ by one flip, every thread alternates over and under along its whole length. That parity count is the weave.

z ↦ D + R²·(z − D) / |z − D|²inversion through the mirror circle, one per pass when the point is inside it
Close crop of the lace near the spiral centre showing rosettes and bars
Centre of the spiral. Every rosette here is the same motif from the same triangle, reached through a different chain of reflections.
STEP 3 · THE STRIP

Unrolling the disk into a ribbon

A disk is a medallion, not a ribbon. The band model fixes that. The map below sends an endless straight strip onto the whole disk, with the strip's centre line landing on the disk's horizontal diameter.

z = tanh(w / 2),   w = u + iv,   |v| < π/2

Sliding along the strip, u → u + T, is a hyperbolic translation. We found the smallest slide that maps the tiling exactly onto itself with the over/under parity intact, numerically: T = 2.89814944535515. Cut the lace to a band of that strip and it repeats every T with no seam.

The same tiling unrolled into a horizontal strip with copper period markers
Band model. The same {7,3} tiling, unrolled. Copper lines mark every period T. Each block between them is identical, top and bottom edges are the disk's rim.
STEP 4 · THE SPIRAL

Wrapping the strip with a logarithm

A complex logarithm turns straight lines into spirals. The trick is picking the constant so that one full turn around the centre lands exactly on a repeat of the pattern.

w = A · log p,   2πi · A = M·T + i·N·HsN = 5 ribbons, M = 6 lace repeats per turn, Hs = 2.4 ribbon spacing → A ≈ 1.910 − 2.768i

Go once around and w shifts by exactly six lace periods along the ribbon and five ribbon widths across. Both are symmetries, so the five arms close with no seam.

The log is conformal: it preserves angles. The lace shrinks toward the centre in proportion to distance, |dw/dp| = |A| / |p|, but never stretches or shears. Where a thread would get thinner than a pixel, the detail fades out instead of aliasing.

Five spiral ribbons of the tiling winding into the centre with white gaps between them
The strip, wrapped. Same tiling, same parity shading, now in five log-spiral ribbons. The pale gaps are where the tulle net sits in the final render.

Finishing, and changing only the numbers

On top of the folded lace: a twisted cord runs along each ribbon edge, picot loops sit just outside it, and a hex tulle net fills the space behind. Then the fun part. Every variation below came from changing a few numbers in the script. Three arms, eight arms, and a double spiral that flows out of one point and into another, made by a Möbius map q = (p − a) / (p + a) placed in front of the log.

Three-arm lace spiral
N = 3. Three ribbons, wider pitch.
Eight-arm lace spiral
N = 8. Eight ribbons, tighter wind.
Double lace spiral with a source and a sink
Möbius double. Source at a, sink at −a.
Close crop of a ribbon edge with twisted cord and picot loops
Ribbon edge. Twisted cord, picot loops, tulle behind. All of it still a heightmap.
The graph

Small on the outside

Open the package and the graph looks modest. Open the containers and it keeps going. Andrei's graphs are built by hand so you can read every node. This one was generated by code and packed into a few containers, so it hides its size and it's harder to read. That's the trade.

41nodes on the top level
518inside one Pixel Processor: fold, strip, spiral
~1,300in the functions that place each FX-Map stamp
~1,880nodes in total
Level 0 · Main graph Substance Designer main graph with 41 nodes
The top level. Motif stamps, the Pixel Processor that does all the geometry, cord and picot passes, tulle, and the blends that stack them into one height.
Level 1 · Stamp placement Function graph controlling an FX-Map pattern offset
Where each stamp goes. One FX-Map pattern offset function, 81 nodes of sin, cos, floor and division placing petals and bars inside the fundamental triangle. There are many of these.
Level 2 · Pixel Processor The full Pixel Processor per-pixel function graph
The per-pixel function. 518 nodes: the log spiral, the band map, the reflection loop with its parity counter, and the level-of-detail fade.
Level 3 · Detail Zoomed section of the Pixel Processor function
Zoomed in. A section of the per-pixel function. Each complex multiply and inversion from the math above is spelled out as individual nodes.
What I learned

The math was the easy part for the AI

The split surprised me. Hyperbolic geometry, conformal maps and generating thousands of correctly wired nodes came back right. The part I assumed would be easy came back wrong every time.

Where it was strong

  • Deriving the mirror circle, the band period and the spiral constant
  • Writing a while-loop fold as a Designer function graph
  • Generating and wiring about 1,880 nodes from code
  • Turning one script into variations by changing a few numbers

Where it fell flat

  • Every finished, beautiful render I asked for: gold embroidery on velvet, a naturalist illustration, needle lace
  • Judging its own output. It kept telling me renders were improving when they weren't
  • Knowing when to stop and say a look wasn't close

I scrapped all of those render attempts. What worked was a clean split: let it own the math and the structure, and keep the taste call with me. The lab now ships heightmaps and graphs, and the look gets made in a real material pipeline.

Reference

Parameters

SymbolValueMeaning
{p, q}{7, 3}heptagons, three per corner; hyperbolic because 1/7 + 1/3 < 1/2
D, R2.0122, 1.7461mirror circle centre and radius
max passes40reflection loop cap per pixel
T2.89814944535515band period, found numerically
H0.92ribbon half-height in the strip
Hs2.4spacing between ribbons
N, M5, 6ribbons, lace repeats per turn
a(0.45, 0.12)Möbius pole for the double spiral
output4096², 16-bitheight channel
Jonathan Beals · 2026
Inspired by Andrei Zelenco's procedural lace work. Built in Adobe Substance 3D Designer with Claude Code driving it over a local Python bridge. Explainer diagrams rendered from the same math in numpy.