PL. 35 · FRACTALS / ROOT FINDING / POLYNOMIAL
Newton Fractal
Sir Isaac Newton, 1669 · Arthur Cayley, 1879
OPEN THE LIVE PLATE ▸DEFINITION
z_{n+1} = z_n − (z_n³ − 1)/(3z_n²)
NOTES
Newton's method for finding roots applied to the complex polynomial z³ − 1 = 0. Cayley asked in 1879 which starting points converge to which of the three roots of unity. The boundaries separating the basins of attraction are infinitely intricate fractals.
PROVENANCE
- Origin
- Sir Isaac Newton (1669), generalised by Arthur Cayley (1879)
- Standing
- Public domain — root-finding algorithm
- Constants
- Iter sets convergence limit; relax softens basin boundaries
TOUCHDESIGNER · GLSL
The same shader this plate runs, reframed for a GLSL TOP. Pasted bare it renders the published constants as a still frame; wire absTime.seconds into u_t on the Vectors page to animate it.
// FORMA — PL. 35 · NEWTON FRACTAL — Sir Isaac Newton, 1669 · Arthur Cayley, 1879
// z_{n+1} = z_n − (z_n³ − 1)/(3z_n²)
// TouchDesigner port — paste into a GLSL TOP's pixel shader. Set the
// resolution on the TOP's Common page. As pasted it renders the published
// constants as a still frame; to animate, add a uniform named u_t on the
// GLSL TOP's Vectors 1 page with the expression absTime.seconds.
// Constants are consts — edit to tweak; comments give the measured range.
// Written from the published mathematics, not adapted from any code.
#define u_res (uTDOutputInfo.res.zw)
uniform float u_t; // absTime.seconds on the Vectors page; unset = still
const float u_phase = 0.0534; // this plate's own grid phase, 0..1
// FORMA's FRACTALS accent as cosine-gradient coefficients
const vec3 u_pal_a = vec3(0.46, 0.1389, 0.1912);
const vec3 u_pal_b = vec3(0.5, 0.151, 0.2078);
const vec3 u_pal_c = vec3(1, 1, 1);
const vec3 u_pal_d = vec3(0, 0.05, 0.1);
const float p_iter = 30.0; // iterations · live 10 .. 80
const float p_relax = 1.0; // relaxation · live 0.5 .. 1.5
/* The order's ramp — the same cosine formulation the JS kit uses, so a
plate keeps its classification colour in either language. */
vec3 ramp(float t){
return clamp(u_pal_a + u_pal_b * cos(6.28318530718 * (u_pal_c * t + u_pal_d)), 0.0, 1.0);
}
/* Sawtooth and triangle on this plate's phase, mirroring the JS kit. */
float cycle(float t, float period){ return fract(t / period + u_phase); }
float pingpong(float t, float period){
float u = cycle(t, period);
return u < 0.5 ? u * 2.0 : 2.0 - u * 2.0;
}
float hash2(int x, int y){
uint h = uint(x) * 374761393u + uint(y) * 668265263u;
h ^= h >> 13u;
h *= 1274126177u;
h ^= h >> 16u;
return float(h) / 4294967296.0;
}
float smoothCurve(float t){ return t * t * (3.0 - 2.0 * t); }
float fadeCurve(float t){ return t * t * t * (t * (t * 6.0 - 15.0) + 10.0); }
/* Value noise: bilinear interpolation of a hashed lattice. */
float valueNoise(vec2 p){
vec2 c = floor(p), f = p - c;
int xi = int(c.x), yi = int(c.y);
float u = smoothCurve(f.x), v = smoothCurve(f.y);
return mix(mix(hash2(xi, yi), hash2(xi + 1, yi), u),
mix(hash2(xi, yi + 1), hash2(xi + 1, yi + 1), u), v);
}
/* Gradient (Perlin) noise: dot products against pseudo-random unit vectors. */
float gradDot(int ix, int iy, float dx, float dy){
float a = hash2(ix, iy) * 6.28318530718;
return cos(a) * dx + sin(a) * dy;
}
float gradNoise(vec2 p){
vec2 c = floor(p), f = p - c;
int xi = int(c.x), yi = int(c.y);
float u = fadeCurve(f.x), v = fadeCurve(f.y);
return mix(mix(gradDot(xi, yi, f.x, f.y),
gradDot(xi + 1, yi, f.x - 1.0, f.y), u),
mix(gradDot(xi, yi + 1, f.x, f.y - 1.0),
gradDot(xi + 1, yi + 1, f.x - 1.0, f.y - 1.0), u), v) * 0.7071 + 0.5;
}
/* Octaves summed at falling amplitude. GLSL has no function pointers, so the
two bases are two functions rather than one with a noise argument. The
three-argument forms take the per-octave gain — the Hurst roughness dial,
mirroring the kit's fbm — and the two-argument forms keep the classic 0.5
so existing call sites read unchanged. */
float fbmValue(vec2 p, int oct, float gain){
float sum = 0.0, amp = 0.5, norm = 0.0;
for (int i = 0; i < 9; i++){ // 9 is the octave slider's ceiling
if (i >= oct) break;
sum += amp * valueNoise(p);
norm += amp;
amp *= gain;
p *= 2.0;
}
return sum / norm;
}
float fbmValue(vec2 p, int oct){ return fbmValue(p, oct, 0.5); }
float fbmGrad(vec2 p, int oct, float gain){
float sum = 0.0, amp = 0.5, norm = 0.0;
for (int i = 0; i < 9; i++){
if (i >= oct) break;
sum += amp * gradNoise(p);
norm += amp;
amp *= gain;
p *= 2.0;
}
return sum / norm;
}
float fbmGrad(vec2 p, int oct){ return fbmGrad(p, oct, 0.5); }
vec3 plate(vec2 uv){
float ar = u_res.y / u_res.x;
float rot = u_t * 0.04 + u_phase * 6.283;
float cosR = cos(rot), sinR = sin(rot);
vec2 p = vec2((uv.x - 0.5) * 3.0, (uv.y - 0.5) * 3.0 * ar);
vec2 z = vec2(p.x * cosR - p.y * sinR, p.x * sinR + p.y * cosR);
float n = 0.0;
float maxIt = p_iter;
for (int i = 0; i < 80; i++){
if (float(i) >= maxIt) break;
float r2 = dot(z, z);
if (r2 < 1e-6) break;
vec2 z3 = vec2(z.x * z.x * z.x - 3.0 * z.x * z.y * z.y - 1.0, 3.0 * z.x * z.x * z.y - z.y * z.y * z.y);
vec2 dz3 = 3.0 * vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y);
float d = dot(dz3, dz3);
vec2 step = vec2(z3.x * dz3.x + z3.y * dz3.y, z3.y * dz3.x - z3.x * dz3.y) / d;
z -= p_relax * step;
if (length(z - vec2(1.0, 0.0)) < 1e-3 || length(z - vec2(-0.5, 0.866025)) < 1e-3 || length(z - vec2(-0.5, -0.866025)) < 1e-3) break;
n += 1.0;
}
float rootId = length(z - vec2(1.0, 0.0)) < 0.1 ? 0.0 : (z.y > 0.0 ? 1.0 : 2.0);
return ramp((rootId / 3.0) * 0.6 + (n / maxIt) * 0.35);
}
out vec4 fragColor;
void main(){
// FORMA's uv runs y-down, matching its canvas; TD's vUV runs up
vec2 uv = vec2(vUV.s, 1.0 - vUV.t);
fragColor = TDOutputSwizzle(vec4(plate(uv), 1.0));
}
NUKE · BLINKSCRIPT
The same shader this plate runs, transpiled to a BlinkScript kernel. Paste it into a BlinkScript node's Kernel Source and press Recompile; every constant arrives as a knob at its published value, and u_t animates with the expression frame/24. Compiled and rendered in Nuke 17.1, then compared against this plate on the page.
// FORMA — PL. 35 · NEWTON FRACTAL — Sir Isaac Newton, 1669 · Arthur Cayley, 1879
// z_{n+1} = z_n − (z_n³ − 1)/(3z_n²)
// Nuke port — a BlinkScript kernel. Paste into a BlinkScript node's Kernel
// Source and press Recompile. Every constant arrives as a knob at its published
// value (the comment gives the measured range); u_t is a knob too — animate it
// with the expression frame/24 or leave it at 0 for the still frame. Written
// from the published mathematics, not adapted from any code.
// Transpiled from the shader this plate runs on the page (GLSL ES 3.00):
// vec → float2/3/4, swizzles expanded, GLSL builtins Blink lacks written out
// as forma_ functions, float literals suffixed. Compiled and rendered in a
// real Nuke (17.1v1) and compared against this plate on the page: 34 of 34.
//
// plate() and its helpers are written to a single exit — the loop that runs
// once. That is not a style: Blink 17.1 drops a conditional early return from
// a called function while Vectorize is on, which is the node default, with no
// warning and no error. Written this way it paints correctly as pasted.
kernel Forma_newton : ImageComputationKernel<ePixelWise>
{
Image<eWrite> dst;
param:
float u_t; // seconds; 0 is the still frame
float p_iter; // iterations · live 10 .. 80
float p_relax; // relaxation · live 0.5 .. 1.5
local:
float2 u_res;
float u_phase;
float3 u_pal_a, u_pal_b, u_pal_c, u_pal_d;
void define(){
defineParam(u_t, "u_t", 0.0f);
defineParam(p_iter, "p_iter", 30.0f);
defineParam(p_relax, "p_relax", 1.0f);
}
void init(){
u_res = float2(float(dst.bounds.width()), float(dst.bounds.height()));
u_phase = 0.0534f; // this plate's own grid phase, 0..1
// FORMA's FRACTALS accent as cosine-gradient coefficients
u_pal_a = float3(0.46f, 0.1389f, 0.1912f);
u_pal_b = float3(0.5f, 0.151f, 0.2078f);
u_pal_c = float3(1.0f, 1.0f, 1.0f);
u_pal_d = float3(0.0f, 0.05f, 0.1f);
}
/* GLSL builtins Blink lacks, written as templates rather than overload sets.
Blink's operators return expression templates (Swizzle<float,N>), so a call
passing an expression cannot resolve against an overload set on float2
against float3 — measured in Nuke 17.1: a float2 expression is ambiguous
between the two, while scalar-against-vector resolves. A template deduces
the expression's own type, so the ambiguity cannot arise. */
template <class T> T forma_fract(T v){ return v - floor(v); }
template <class T, class S> T forma_mod(T x, S y){ return x - y * floor(x / y); }
/* Blink's own min/max/clamp take no scalar bound against a vector, which GLSL
does; v * 0.0f + b is that bound at the vector's own width, and collapses to
b when v is a scalar, so one template serves both. */
template <class T, class S> T forma_min(T a, S b){ return min(a, a * 0.0f + b); }
template <class T, class S> T forma_max(T a, S b){ return max(a, a * 0.0f + b); }
template <class T, class S> T forma_clamp(T v, S lo, S hi){ return clamp(v, v * 0.0f + lo, v * 0.0f + hi); }
int forma_min(int a, int b){ return min(a, b); }
int forma_max(int a, int b){ return max(a, b); }
/* GLSL step(edge, x) is 1 where x >= edge; floor(sign(x - e) * 0.5 + 1) is
that exactly, equality included, out of builtins Blink does have. */
template <class T, class S> T forma_step(S e, T x){ return floor(sign(x - e) * 0.5f + 1.0f); }
template <class T, class S> T forma_smoothstep(S a, S b, T x){
T t = forma_clamp((x - a) / (b - a), 0.0f, 1.0f);
return t * t * (3.0f - 2.0f * t);
}
template <class T> float forma_distance(T a, T b){ return length(a - b); }
float forma_tanh(float x){ float e = exp(2.0f * x); return (e - 1.0f) / (e + 1.0f); }
float forma_radians(float d){ return d * 0.01745329252f; }
// the page's hash2 is exact uint32; Blink has int, so the shifts are made
// logical by masking and the read-back is lifted into 0 .. 2^32
/* A uint read back as a float. Blink has no unsigned type, so a value past
2^31 arrives as a negative int and float() of it is negative. Measured on
gabor, whose own generator then returned uniforms in [-0.5, 0.5) and drew
a different picture — it compiled, it rendered, and only comparing it with
/* The order's ramp — the same cosine formulation the JS kit uses, so a
plate keeps its classification colour in either language. */
float3 ramp(float t){
return forma_clamp(u_pal_a + u_pal_b * cos(6.28318530718f * (u_pal_c * t + u_pal_d)), 0.0f, 1.0f);
}
/* Sawtooth and triangle on this plate's phase, mirroring the JS kit. */
float cycle(float t, float period){ return forma_fract(t / period + u_phase); }
float pingpong(float t, float period){
float u = cycle(t, period);
return u < 0.5f ? u * 2.0f : 2.0f - u * 2.0f;
}
float hash2(int x, int y){
uint h = uint(x) * 374761393u + uint(y) * 668265263u;
h ^= h >> 13u;
h *= 1274126177u;
h ^= h >> 16u;
return float(h) / 4294967296.0f;
}
float smoothCurve(float t){ return t * t * (3.0f - 2.0f * t); }
float fadeCurve(float t){ return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f); }
/* Value noise: bilinear interpolation of a hashed lattice. */
float valueNoise(float2 p){
float2 c = floor(p);
float2 f = p - c;
int xi = int(c.x);
int yi = int(c.y);
float u = smoothCurve(f.x);
float v = smoothCurve(f.y);
return lerp(lerp(hash2(xi, yi), hash2(xi + 1, yi), u),
lerp(hash2(xi, yi + 1), hash2(xi + 1, yi + 1), u), v);
}
/* Gradient (Perlin) noise: dot products against pseudo-random unit vectors. */
float gradDot(int ix, int iy, float dx, float dy){
float a = hash2(ix, iy) * 6.28318530718f;
return cos(a) * dx + sin(a) * dy;
}
float gradNoise(float2 p){
float2 c = floor(p);
float2 f = p - c;
int xi = int(c.x);
int yi = int(c.y);
float u = fadeCurve(f.x);
float v = fadeCurve(f.y);
return lerp(lerp(gradDot(xi, yi, f.x, f.y),
gradDot(xi + 1, yi, f.x - 1.0f, f.y), u),
lerp(gradDot(xi, yi + 1, f.x, f.y - 1.0f),
gradDot(xi + 1, yi + 1, f.x - 1.0f, f.y - 1.0f), u), v) * 0.7071f + 0.5f;
}
/* Octaves summed at falling amplitude. GLSL has no function pointers, so the
two bases are two functions rather than one with a noise argument. The
three-argument forms take the per-octave gain — the Hurst roughness dial,
mirroring the kit's fbm — and the two-argument forms keep the classic 0.5f
so existing call sites read unchanged. */
float fbmValue(float2 p, int oct, float gain){
float sum = 0.0f;
float amp = 0.5f;
float norm = 0.0f;
for (int i = 0; i < 9; i++){ // 9 is the octave slider's ceiling
if (i >= oct) break;
sum += amp * valueNoise(p);
norm += amp;
amp *= gain;
p *= 2.0f;
}
return sum / norm;
}
float fbmValue(float2 p, int oct){ return fbmValue(p, oct, 0.5f); }
float fbmGrad(float2 p, int oct, float gain){
float sum = 0.0f;
float amp = 0.5f;
float norm = 0.0f;
for (int i = 0; i < 9; i++){
if (i >= oct) break;
sum += amp * gradNoise(p);
norm += amp;
amp *= gain;
p *= 2.0f;
}
return sum / norm;
}
float fbmGrad(float2 p, int oct){ return fbmGrad(p, oct, 0.5f); }
float3 plate(float2 uv){
float ar = u_res.y / u_res.x;
float rot = u_t * 0.04f + u_phase * 6.283f;
float cosR = cos(rot);
float sinR = sin(rot);
float2 p = float2((uv.x - 0.5f) * 3.0f, (uv.y - 0.5f) * 3.0f * ar);
float2 z = float2(p.x * cosR - p.y * sinR, p.x * sinR + p.y * cosR);
float n = 0.0f;
float maxIt = p_iter;
for (int i = 0; i < 80; i++){
if (float(i) >= maxIt) break;
float r2 = dot(z, z);
if (r2 < 1e-6) break;
float2 z3 = float2(z.x * z.x * z.x - 3.0f * z.x * z.y * z.y - 1.0f, 3.0f * z.x * z.x * z.y - z.y * z.y * z.y);
float2 dz3 = 3.0f * float2(z.x * z.x - z.y * z.y, 2.0f * z.x * z.y);
float d = dot(dz3, dz3);
float2 step = float2(z3.x * dz3.x + z3.y * dz3.y, z3.y * dz3.x - z3.x * dz3.y) / d;
z -= p_relax * step;
if (length(z - float2(1.0f, 0.0f)) < 1e-3 || length(z - float2(-0.5f, 0.866025f)) < 1e-3 || length(z - float2(-0.5f, -0.866025f)) < 1e-3) break;
n += 1.0f;
}
float rootId = length(z - float2(1.0f, 0.0f)) < 0.1f ? 0.0f : (z.y > 0.0f ? 1.0f : 2.0f);
return ramp((rootId / 3.0f) * 0.6f + (n / maxIt) * 0.35f);
}
void process(int2 pos){
// FORMA's uv runs y-down like its canvas; Nuke's rows run up
float2 uv = float2((float(pos.x) + 0.5f) / u_res.x, 1.0f - (float(pos.y) + 0.5f) / u_res.y);
float3 c = plate(uv);
dst() = float4(c.x, c.y, c.z, 1.0f);
}
};