fields.ts
examples/fxedit/fx/nodes/fields.ts · raw · open the example · all examples · site home
Exports: SPARK_EXAMPLESfieldNodes
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 150 151 152 153 154 155 156 157 158 159
// Fields: a number per splat from its position (noise, distances, gradients),
// the usual source of masks.
import { TSL, type TslNode } from "@sparkjsdev/radix";
import type { NodeDef } from "../types.js";
import { IQ_DISTANCE, sdBox, sdPlane, sdSphere } from "./iq.js";
const { Loop, If, abs, clamp, dot, float, floatBitsToUint, fract, length, min, mx_fractal_noise_float, normalize, select, sin, step, vec3 } = TSL;
const THREE_MATERIALX = { name: "three.js MaterialX noise", url: "https://github.com/mrdoob/three.js/tree/dev/src/nodes/materialx", license: "MIT (MaterialX: Apache-2.0)" };
export const SPARK_EXAMPLES = { name: "Spark examples (World Labs)", url: "https://github.com/sparkjsdev/spark/tree/main/examples", license: "MIT" };
// PCG step on a uint (pcg-random.org, as in three's hash)
const pcg = (v: TslNode) => {
const state = v.mul(747796405).add(2891336453).toVar();
const word = state.shiftRight(state.shiftRight(28).add(4)).bitXor(state).mul(277803737).toVar();
return word.shiftRight(22).bitXor(word);
};
export const fieldNodes: NodeDef[] = [
{
id: "field.noise",
label: "Fractal noise",
description: "Smooth random clouds in space: layered Perlin noise at a position",
category: "field",
inputs: {
position: { type: "vec3", default: [0, 0, 0] },
scale: { type: "float", default: 2, min: 0.1, max: 20 },
offset: { type: "vec3", default: [0, 0, 0], min: -10, max: 10 },
lacunarity: { type: "float", default: 2, min: 1, max: 4 },
diminish: { type: "float", default: 0.5, min: 0, max: 1 },
},
options: { octaves: { default: 3, min: 1, max: 8 } },
// value: fractal Perlin noise, about [-1, 1]; unit: 0.5 + 0.5 value
outputs: { value: "float", unit: "float" },
source: THREE_MATERIALX,
build: ({ position, scale, offset, lacunarity, diminish }, ctx) => {
const value = mx_fractal_noise_float(position.mul(scale).add(offset), Number(ctx.option("octaves")), lacunarity, diminish).toVar();
return { value, unit: value.mul(0.5).add(0.5) };
},
},
{
id: "field.axisGradient",
label: "Axis gradient",
description: "How far along an axis a position is: a ramp through space",
category: "field",
inputs: {
position: { type: "vec3", default: [0, 0, 0] },
origin: { type: "vec3", default: [0, 0, 0], min: -2, max: 2 },
axis: { type: "vec3", default: [0, 1, 0], min: -1, max: 1 },
length: { type: "float", default: 1, min: 0.01, max: 10 },
},
// Signed distance along the axis from the origin, in units of length
outputs: { value: "float" },
build: ({ position, origin, axis, length }) => ({ value: dot(position.sub(origin), normalize(axis)).div(length) }),
},
{
id: "field.distance",
label: "Distance to point",
description: "How far a position is from a point",
category: "field",
inputs: {
position: { type: "vec3", default: [0, 0, 0] },
point: { type: "vec3", default: [0, 0, 0], min: -2, max: 2 },
},
// direction: unit vector from the point (zero at the point)
outputs: { value: "float", direction: "vec3" },
build: ({ position, point }) => {
const d = position.sub(point).toVar();
const r = length(d).toVar();
return { value: r, direction: select(r.greaterThan(1e-8), d.div(r), vec3(0)) };
},
},
{
id: "field.hash3",
label: "Random per position",
description: "Three random numbers per splat from its position (stable when the splat order changes)",
category: "field",
inputs: {
position: { type: "vec3", default: [0, 0, 0] },
seed: { type: "float", default: 0, min: 0, max: 100, step: 1 },
},
// value in [0, 1), signed in [-1, 1): PCG of the float bits
outputs: { value: "vec3", signed: "vec3" },
build: ({ position, seed }) => {
const h1 = pcg(floatBitsToUint(position.x).bitXor(pcg(floatBitsToUint(position.y).bitXor(pcg(floatBitsToUint(position.z).bitXor(floatBitsToUint(seed))))))).toVar();
const h2 = pcg(h1.add(0x9e3779b9)).toVar();
const h3 = pcg(h2.add(0x9e3779b9)).toVar();
const value = vec3(h1.toFloat(), h2.toFloat(), h3.toFloat()).mul(1 / 2 ** 32).toVar();
return { value, signed: value.mul(2).sub(1) };
},
},
{
id: "field.sineLattice",
label: "Sine lattice",
description: "A 3D grid of blobs: length(sin(position * frequency + phase)), small at the blob centers",
category: "field",
inputs: {
position: { type: "vec3", default: [0, 0, 0] },
frequency: { type: "float", default: 5, min: 0, max: 40 },
phase: { type: "float", default: 0 },
},
outputs: { value: "float" },
source: SPARK_EXAMPLES,
build: ({ position, frequency, phase }) => ({ value: length(sin(position.mul(frequency).add(phase))) }),
},
{
id: "field.foldFractal",
label: "Circuit fractal",
description: "Glowing circuit-like lines from a folding fractal (Spark's Electronic effect), 1 on a line",
category: "field",
inputs: {
position: { type: "vec3", default: [0, 0, 0] },
scale: { type: "float", default: 0.25, min: 0.02, max: 2 },
speed: { type: "float", default: 0.5, min: 0, max: 4 },
width: { type: "float", default: 0.5, min: 0.05, max: 2 },
},
options: { iterations: { default: 8, min: 3, max: 12 } },
outputs: { mask: "float", trap: "float" },
time: true,
source: SPARK_EXAMPLES,
build: ({ position, scale, speed, width }, ctx) => {
const p = position.mul(scale).add(vec3(0, 0.5, 0)).toVar();
const m = float(100).toVar();
const t = ctx.time.mul(speed).toVar();
Loop(Number(ctx.option("iterations")), ({ i }: { i: TslNode }) => {
p.assign(abs(p).div(clamp(abs(p.x.mul(p.y)), 0.3, 3.0)).sub(1.0));
// p.xy *= rot(90 degrees)
p.assign(vec3(p.y.negate(), p.x, p.z));
If(i.greaterThan(1), () => {
m.assign(min(m, length(p.xy).add(step(0.3, fract(p.z.mul(0.5).add(t).add(float(i).mul(0.2)))))));
});
});
return { mask: step(m, width), trap: m };
},
},
{
id: "field.sdfBasic",
label: "Shape distance",
description: "Signed distance to a sphere, box or plane (negative inside)",
category: "field",
inputs: {
position: { type: "vec3", default: [0, 0, 0] },
center: { type: "vec3", default: [0, 0, 0], min: -2, max: 2 },
radius: { type: "float", default: 0.5, min: 0, max: 3 },
halfSize: { type: "vec3", default: [0.5, 0.5, 0.5], min: 0, max: 3 },
normal: { type: "vec3", default: [0, 1, 0], min: -1, max: 1 },
},
// sphere uses radius, box halfSize, plane normal (through center)
options: { shape: { default: "sphere", choices: ["sphere", "box", "plane"] } },
// Signed distance: negative inside
outputs: { value: "float" },
source: IQ_DISTANCE,
build: ({ position, center, radius, halfSize, normal }, ctx) => {
const p = position.sub(center);
const shape = ctx.option("shape");
return { value: shape === "box" ? sdBox(p, halfSize) : shape === "plane" ? sdPlane(p, normalize(normal)) : sdSphere(p, radius) };
},
},
];