How a cubic becomes a shape
AKA
How a genotype is constructed
No textures, no model files. Every vertex gets computed at load from the Form’s seed. Here’s the whole chain, and where the math stops and I start. Heads up, it’s pretty technical. Not your thing? Skip straight to the lab and break stuff.
1 · The solution
Start with x³ + ax² + bx + c. Sub in y = x + a/3, the square term drops out and you get the depressed cubic y³ + py + q:
p = b − a²/3 q = 2a³/27 − ab/3 + c
Everything hangs on D = (α−β)(β−γ)(γ−α). Swap two roots and it flips sign, so it’s not symmetric. Square it though and it is, and that’s the discriminant:
Δ = D² = −4p³ − 27q²
Take ω = (−1 + i√3)/2, the primitive cube root of unity. The Lagrange resolvent (thanks Joseph) A = α + ωβ + ω²γ has a cube you can write purely in p, q and D, and the roots fall out of that:
A³ = −27q/2 − (3i√3/2)·D α = A/3 − p/A β = ω²A/3 − ωp/A γ = ωA/3 − ω²p/A
Mess with the coefficients and watch the whole chain follow: depressed form, discriminant, class, the Form itself. Every figure on this page is the real pipeline running live.
y³ − 15.33y − 10.93Δ = 11195.59Separated · 3 Real Distincty³ − 15.33y − 10.93Δ = 11195.59Separated · 3 Real Distincty³ − 15.33y − 10.93Δ = 11195.59Separated · 3 Real Distinct2 · The choice that becomes the Phase
A³ has three cube roots. Swap A for ωA and α → γ → β. Same set of roots, new labels. That leftover freedom, after you’ve picked a square root and then a cube root, is the only real wiggle room in the whole solution, and it’s what the Phase trait records. Each Form commits to one branch, which turns its ω-triad a third of a turn.
Early on I tried getting phase from arg(A). Turns out it’s nearly constant for most real-coefficient samples, with floating point noise deciding the rest, so 98.6% of generations got the same value and came out identical. Oops.
3 · From solution to Form
The sign of the discriminant picks a family:
| Class | Meaning | Forms |
|---|---|---|
| Separated Δ > 0 · ~36% | three distinct real roots, bodies sit apart on the real axis | Grid, Lattice, Tessellation, Tower, Strata, Labyrinth, Fold, Prism |
| Fractured Δ < 0 · ~55% | one real root and a complex conjugate pair, the form breaks open | Knot, Exploded, Cascade, Spiral, Shell, Orbital, Weave, Rift, Bloom, Arbor |
| Merged Δ ≈ 0 · ~9% | a repeated root, bodies collapse into each other | Void, Radial, Vessel, Aperture |
Family sizes just follow how often each class actually turns up in random real cubics, and if I split them evenly the Merged forms would each get a handful of Forms while a couple of Fractured ones ate the whole collection.
The repeated-root share is the one number I set instead of measuring, because random real cubics basically never land on exactly Δ = 0 in floating point, so I inject that tier on purpose at 8% of supply, which is enough for all four Merged forms to show up in 512 and it’s still the rarest tier by a mile.
Inside a family, the coefficients drive everything:
a | global scale and framing | |a| sets how tight the camera fits |
b | glyph grid density | 80 to 200 cells a side, |b| sets how fine |
c | curvature | bends, twists and opens primitives, also picks the archetype quartile |
a/3 | world translation | the depression's own shift, relative to the Form's size |
Δ | fracture magnitude | log-compressed, sets how hard the discriminant operator pushes things apart |
arg(A) | palette | the resolvent's argument picks the ANSI ramp |
branch | phase | which cube root of A³ gets used, turns the ω-triad a third |
roots | body placement | α, β, γ as points in ℂ, normalised to the unit disc |
max |root| | energy | how far out the roots sit, the scale the unit-disc normalisation throws away |
min |αᵢ−αⱼ| | feedback | how close the nearest two roots are, sets how hard the render feeds back on itself |
Same three solutions again. |c| bumps them between archetypes when it crosses a quartile boundary, |b| changes how fine the grid is, |a| moves the camera in or out.
Δ = 11195.59LatticeΔ = 11195.59TessellationΔ = 11195.59Tower4 · Energy and Feedback
Two traits read numbers the rest of the derivation throws away. Energy is the biggest root modulus, which matters because the roots get normalised to the unit disc before they place anything, and that used to toss the absolute scale so two solutions with the same root triangle at totally different sizes drew the same. Energy hands that number back, a Still Form keeps its bodies tight and a Violent one flings them apart, and it’s the one lever that reaches all twenty-two forms.
Feedback is the gap between the two closest roots, which Δ can’t see since it’s a product and one tiny gap hides behind two big ones. It closes a loop that’s normally open: ASCII renderers, mine included until I fixed it, only run one way, geometry in and characters out. Now last frame’s grid feeds into the luminance this frame measures, so bright cells drag a trail behind them, quantised to the same twelve glyph levels so it bands instead of blurring. You mostly see it once things move.
The sliders move the roots, not the traits, and Energy and Feedback just follow.
Δ = 753.83Energy Charged · Feedback EchoΔ = 100.20Energy Still · Feedback ResonantΔ = 0.21Energy Charged · Feedback Runaway| Energy | Still 18% · Charged 53% · Violent 30% |
| Feedback | None 52% · Echo 25% · Resonant 14% · Runaway 9% |
5 · The engine
This is the engine part of the name. three.js handles the 3D and that’s about it, everything after is my own pipeline. The scene renders offscreen at three by three samples per character cell, gets read back, and each cell’s luminance picks one of twelve glyphs from .:-=+*#%▒▓█ and one of five inks from the Form’s ANSI palette, with the properly saturated cells getting the accent. Then it paints the grid onto a 2D canvas character by character, batched by ink so it stays fast, and the SVG export is that same grid as plain text rows, so the vector version is the actual characters and not a trace.
Depth fog gives the ramp something to shade with or evenly lit surfaces flatten into one character, and exposure gets set per Form off a histogram of only the cells that actually have light in them, otherwise a sparse Orbital and a dense Tessellation get crushed in opposite directions.
6 · Sound
The lab can listen. Share a tab’s audio or pick a mic or line-in (BlackHole gets you system audio on a Mac) and it splits the signal into bass, mids and highs, each measured against a slowly fading peak so quiet and loud tracks both use the full range. The kick gets its own path, a steep band-pass around 60 Hz you can retune, measured against a floor that follows the level between kicks so a rolling sub doesn’t hold the form open, and the spacing between kicks gives a BPM with a flywheel that keeps pumping through breakdowns.
None of it touches the math or the genotype, it’s a live layer on top. The kick pumps the size, mids push the glyph feedback, highs and kicks add spin, hi-hats tilt it and tear the raster, all eased so it moves instead of twitching, and exports come out clean.
7 · Uniqueness
Every seed is SHA-256(master ‖ id ‖ nonce) and generation tosses anything too close to an existing Form on two gates, one comparing the math as a weighted vector and one comparing a 496-bit hash of the actual render, re-rolling the nonce until it passes. It’s all settled before the mint, minting only takes ids so nobody can sneak in their own coefficients, and the whole id → seed table is hashed into the contract with no setter, so you can check the art you saw is the art you got:
8 · What the math doesn’t decide
Being straight about this. The math decides the coefficients, the depressed form, Δ and its sign, the three roots as points in ℂ, the resolvent and its branch. Across the whole supply the worst residual |p(root)| is 1.3 × 10⁻⁷, so yes, the roots actually solve their equations.
The math does not decide that Δ < 0 should look like a knot and not a tower. The 22 archetypes are a vocabulary I wrote. The equation picks from it and drives every parameter inside, but the vocabulary is a design call, not a theorem. The primitives (torus knot, icosahedron, cone) are parametric generators from three.js, built at runtime, never loaded as assets.
Sources
- The derivation this is built on: How to Solve Cubic and Quartic Equations II: Cubics · hidden-phenomena.com
- The prompt for the whole thing: a post by Vitalik Buterin