attractors.ts
examples/fractals/attractors.ts · raw · open the example · all examples · site home
Exports: ATTRACTORSattractorKind
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
// Strange attractors as threads of light. Samples come in threads of
// THREAD samples: a thread starts at a random point near the attractor,
// settles for `settle` steps, and its k-th sample integrates k more steps
// (midpoint rule). Every sample of a thread repeats the same arithmetic, so
// they line up into one continuous curve; each is a splat stretched along
// the velocity to its neighbors, lit as a fiber, with light pulses running
// along the thread (the draw's pulse uses the phase k / THREAD).
import { TSL, type TslNode } from "@sparkjsdev/radix";
import { paramUniforms, type BakeUniforms, type Kind, type Param } from "./kind.js";
import { Rng, pcg, sampleWriter, storeSample } from "./sample.js";
const { Loop, exp, float, int, length, max, normalize, select, sin, uint, vec3, vec4 } = TSL;
const THREAD = 1024;
type U = Record<string, TslNode>;
interface Attractor {
id: string;
label: string;
params: Record<string, Param>;
dt: number;
// The native point at the display origin
center: [number, number, number];
// Where threads start (default the center), how far apart, how long
// they settle, and their default width
start?: [number, number, number];
spread?: number;
settle?: number;
width?: number;
// Display units per native unit
scale: number;
// Native z is the display's up
zUp: boolean;
// Speed of a typical thread (for color)
speed: number;
derivative: (p: TslNode, u: U) => TslNode;
}
const p = (value: number, min: number, max: number): Param => ({ value, min, max, step: 0.001 });
export const ATTRACTORS: Attractor[] = [
{
id: "lorenz", label: "Lorenz attractor", dt: 0.0035, center: [0, 0, 25], start: [0, 2, 25], spread: 6, settle: 1500, scale: 1 / 21, zUp: true, speed: 60,
params: { sigma: p(10, 1, 20), rho: p(28, 5, 60), beta: p(8 / 3, 0.5, 6) },
derivative: (q, u) => vec3(u.sigma.mul(q.y.sub(q.x)), q.x.mul(u.rho.sub(q.z)).sub(q.y), q.x.mul(q.y).sub(u.beta.mul(q.z))),
},
{
id: "aizawa", label: "Aizawa attractor", dt: 0.008, center: [0, 0, 0.7], start: [0.1, 0, 0], width: 0.0015, scale: 0.58, zUp: true, speed: 1.5,
params: { a: p(0.95, 0.5, 1.2), b: p(0.7, 0.4, 1), c: p(0.6, 0.2, 1), d: p(3.5, 2, 5), e: p(0.25, 0, 0.6), f: p(0.1, 0, 0.3) },
derivative: (q, u) => {
const zb = q.z.sub(u.b);
return vec3(
zb.mul(q.x).sub(u.d.mul(q.y)),
u.d.mul(q.x).add(zb.mul(q.y)),
u.c.add(u.a.mul(q.z)).sub(q.z.mul(q.z).mul(q.z).div(3)).sub(q.x.mul(q.x).add(q.y.mul(q.y)).mul(u.e.mul(q.z).add(1))).add(u.f.mul(q.z).mul(q.x).mul(q.x).mul(q.x)),
);
},
},
{
id: "thomas", label: "Thomas attractor", dt: 0.05, center: [0, 0, 0], scale: 0.24, zUp: false, speed: 1.2,
params: { b: p(0.208186, 0.1, 0.33) },
derivative: (q, u) => vec3(sin(q.y).sub(u.b.mul(q.x)), sin(q.z).sub(u.b.mul(q.y)), sin(q.x).sub(u.b.mul(q.z))),
},
{
id: "halvorsen", label: "Halvorsen attractor", dt: 0.004, center: [-1.5, -1.5, -1.5], scale: 0.1, zUp: false, speed: 18,
params: { a: p(1.89, 1.2, 2.4) },
derivative: (q, u) => vec3(
u.a.negate().mul(q.x).sub(q.y.mul(4)).sub(q.z.mul(4)).sub(q.y.mul(q.y)),
u.a.negate().mul(q.y).sub(q.z.mul(4)).sub(q.x.mul(4)).sub(q.z.mul(q.z)),
u.a.negate().mul(q.z).sub(q.x.mul(4)).sub(q.y.mul(4)).sub(q.x.mul(q.x)),
),
},
{
id: "dadras", label: "Dadras attractor", dt: 0.004, center: [0, 0, 0], start: [1, 1, 0], scale: 0.075, zUp: true, speed: 25,
params: { a: p(3, 1, 5), b: p(2.7, 1, 4), c: p(1.7, 0.5, 3), d: p(2, 1, 4), e: p(9, 5, 12) },
derivative: (q, u) => vec3(
q.y.sub(u.a.mul(q.x)).add(u.b.mul(q.y).mul(q.z)),
u.c.mul(q.y).sub(q.x.mul(q.z)).add(q.z),
u.d.mul(q.x).mul(q.y).sub(u.e.mul(q.z)),
),
},
{
id: "chen", label: "Chen attractor", dt: 0.0018, center: [0, 0, 23], start: [0, 2, 22], spread: 6, settle: 1500, scale: 1 / 25, zUp: true, speed: 90,
params: { a: p(35, 20, 45), b: p(3, 1, 6), c: p(28, 15, 35) },
derivative: (q, u) => vec3(u.a.mul(q.y.sub(q.x)), u.c.sub(u.a).mul(q.x).sub(q.x.mul(q.z)).add(u.c.mul(q.y)), q.x.mul(q.y).sub(u.b.mul(q.z))),
},
{
id: "rossler", label: "Rössler attractor", dt: 0.012, center: [0, 0, 4], scale: 0.075, zUp: true, speed: 8,
params: { a: p(0.2, 0.05, 0.4), b: p(0.2, 0.05, 1), c: p(5.7, 3, 12) },
derivative: (q, u) => vec3(q.y.add(q.z).negate(), q.x.add(u.a.mul(q.y)), u.b.add(q.z.mul(q.x.sub(u.c)))),
},
];
export function attractorKind(attractor: Attractor, bake: BakeUniforms): Kind {
const params: Record<string, Param> = {
...attractor.params,
spread: { value: attractor.spread ?? 1, min: 0, max: 10, step: 0.01, label: "start spread" },
step: { value: 2, min: 1, max: 8, step: 1, label: "steps per sample" },
settle: { value: attractor.settle ?? 800, min: 0, max: 4000, step: 10, label: "settle steps" },
width: { value: attractor.width ?? 0.0025, min: 0.0005, max: 0.02, step: 0.0001, label: "thread width" },
};
const u = paramUniforms(params);
const dt = attractor.dt;
let writer: ReturnType<typeof sampleWriter> | undefined;
const make = () => sampleWriter((index, out) => {
const thread = uint(index).div(uint(THREAD));
const k = uint(index).mod(uint(THREAD));
const rng = new Rng(thread.bitXor(pcg(bake.seed)));
const q = vec3(...(attractor.start ?? attractor.center)).add(rng.ball().mul(u.spread)).add(vec3(0.01, 0, 0)).toVar();
const step = () => {
const mid = q.add(attractor.derivative(q, u).mul(dt * 0.5));
q.addAssign(attractor.derivative(mid, u).mul(dt));
};
Loop({ start: int(0), end: int(u.settle), type: "int", condition: "<" }, step);
Loop({ start: int(0), end: int(k).mul(int(u.step)), type: "int", condition: "<" }, step);
const v = attractor.derivative(q, u).toVar();
const speed = length(v);
const c = q.sub(vec3(...attractor.center)).mul(attractor.scale);
const placed = attractor.zUp ? vec3(c.x, c.z, c.y) : c;
const tangent = normalize(attractor.zUp ? vec3(v.x, v.z, v.y) : v);
const gap = speed.mul(dt * attractor.scale).mul(u.step);
const ok = length(placed).lessThan(4);
storeSample(out, {
pos: vec4(placed, 0),
normal: tangent,
shape: max(gap.mul(0.7).div(u.width), 1.01),
trapA: float(1).sub(exp(speed.div(attractor.speed).negate())),
trapB: rng.next(),
ao: float(0.8),
shadow: float(1),
motion: float(k).div(THREAD),
lit: float(0.6),
size: u.width,
alpha: select(ok, float(1), float(0)),
});
});
return {
id: attractor.id,
label: attractor.label,
family: "threads",
params,
uniforms: u,
writer: () => (writer ??= make()),
look: { flatness: 1, sway: 0, petals: 0, twinkle: 0, pulseSpeed: 0.12, pulseCount: 4 },
};
}