@aethervtt/shard-noise 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,44 @@
1
+ /** The planet graph from the spec. */
2
+ export declare const PLANET: {
3
+ output: string;
4
+ nodes: {
5
+ continents: {
6
+ fbm: {
7
+ source: string;
8
+ octaves: number;
9
+ frequency: number;
10
+ gain: number;
11
+ lacunarity: number;
12
+ seed: number;
13
+ };
14
+ };
15
+ mountains: {
16
+ ridged: {
17
+ source: string;
18
+ octaves: number;
19
+ frequency: number;
20
+ seed: number;
21
+ };
22
+ };
23
+ mask: {
24
+ remap: {
25
+ input: string;
26
+ from: number[];
27
+ to: number[];
28
+ clamp: boolean;
29
+ };
30
+ };
31
+ warped: {
32
+ warp: {
33
+ input: string;
34
+ by: string;
35
+ amount: number;
36
+ };
37
+ };
38
+ height: {
39
+ add: (string | {
40
+ multiply: string[];
41
+ })[];
42
+ };
43
+ };
44
+ };
package/dist/planet.js ADDED
@@ -0,0 +1,15 @@
1
+ const PLANET = {
2
+ output: "height",
3
+ nodes: {
4
+ continents: {
5
+ fbm: { source: "simplex", octaves: 5, frequency: 0.8, gain: 0.5, lacunarity: 2, seed: 1 }
6
+ },
7
+ mountains: { ridged: { source: "simplex", octaves: 6, frequency: 3.2, seed: 2 } },
8
+ mask: { remap: { input: "continents", from: [0.1, 0.4], to: [0, 1], clamp: true } },
9
+ warped: { warp: { input: "mountains", by: "continents", amount: 0.15 } },
10
+ height: { add: ["continents", { multiply: ["warped", "mask"] }] }
11
+ }
12
+ };
13
+ export {
14
+ PLANET
15
+ };
@@ -0,0 +1,2 @@
1
+ /** noise.sample and noise.stats, and the kernel loaded before the first frame. */
2
+ export declare const noisePlugin: import("@aethervtt/shard-runtime").Plugin;
package/dist/plugin.js ADDED
@@ -0,0 +1,18 @@
1
+ import { definePlugin } from "@aethervtt/shard-runtime";
2
+ import { loadNoiseKernel } from "./loader.js";
3
+ import * as methodsModule from "./methods.js";
4
+ import { noiseMethods } from "./methods.js";
5
+ import * as noiseGraphModule from "./noise-graph.js";
6
+ const noisePlugin = definePlugin({
7
+ name: "noise",
8
+ provides: [methodsModule, noiseGraphModule],
9
+ build(app) {
10
+ app.addMethod(...noiseMethods);
11
+ },
12
+ async ready() {
13
+ await loadNoiseKernel();
14
+ }
15
+ });
16
+ export {
17
+ noisePlugin
18
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Test-only f64 reference evaluators for the gradient sources: the kernel's algorithms with every
3
+ * position in f64 and cells as exact integers. They show what the true value at a far-away point
4
+ * is, so tests can check that origin-offset sampling finds it (and plain f32 sampling doesn't).
5
+ */
6
+ /** Perlin 3D at an f64 lattice position. */
7
+ export declare function perlin3Reference(seed: number, x: number, y: number, z: number): number;
8
+ /** OpenSimplex2 3D at an f64 position in lattice units (before the rotation). */
9
+ export declare function simplex3Reference(seed: number, x: number, y: number, z: number): number;
@@ -0,0 +1,97 @@
1
+ const PX = 501125321;
2
+ const PY = 1136930381;
3
+ const PZ = 1720413743;
4
+ const SEED_FLIP_3D = 1389709235;
5
+ const R3 = Math.fround(0.6);
6
+ const TWO_THIRDS = Math.fround(2 / 3);
7
+ const gradHash3 = (s, x, y, z) => Math.imul(s ^ x ^ y ^ z, 668265261) >>> 28;
8
+ function grad3(g, x, y, z) {
9
+ const drop = g >> 2;
10
+ const a = drop === 0 ? y : x;
11
+ const b = drop === 2 ? y : z;
12
+ return (g & 1 ? -a : a) + (g & 2 ? -b : b);
13
+ }
14
+ const fade = (t) => t * t * t * (t * (t * 6 - 15) + 10);
15
+ const lerp = (a, b, t) => a + t * (b - a);
16
+ function perlin3Reference(seed, x, y, z) {
17
+ const fx = Math.floor(x);
18
+ const fy = Math.floor(y);
19
+ const fz = Math.floor(z);
20
+ const tx = x - fx;
21
+ const ty = y - fy;
22
+ const tz = z - fz;
23
+ const x0 = Math.imul(fx, PX);
24
+ const y0 = Math.imul(fy, PY);
25
+ const z0 = Math.imul(fz, PZ);
26
+ const x1 = x0 + PX | 0;
27
+ const y1 = y0 + PY | 0;
28
+ const z1 = z0 + PZ | 0;
29
+ const g = (a2, b2, c, dx, dy, dz) => grad3(gradHash3(seed, a2, b2, c), dx, dy, dz);
30
+ const u = fade(tx);
31
+ const v = fade(ty);
32
+ const w = fade(tz);
33
+ const a = lerp(g(x0, y0, z0, tx, ty, tz), g(x1, y0, z0, tx - 1, ty, tz), u);
34
+ const b = lerp(g(x0, y1, z0, tx, ty - 1, tz), g(x1, y1, z0, tx - 1, ty - 1, tz), u);
35
+ const d = lerp(g(x0, y0, z1, tx, ty, tz - 1), g(x1, y0, z1, tx - 1, ty, tz - 1), u);
36
+ const e = lerp(g(x0, y1, z1, tx, ty - 1, tz - 1), g(x1, y1, z1, tx - 1, ty - 1, tz - 1), u);
37
+ return Math.max(-1, Math.min(1, lerp(lerp(a, b, v), lerp(d, e, v), w)));
38
+ }
39
+ function simplex3Reference(seed, x, y, z) {
40
+ const r = (x + y + z) * TWO_THIRDS;
41
+ const px = r - x;
42
+ const py = r - y;
43
+ const pz = r - z;
44
+ const rx = Math.round(px);
45
+ const ry = Math.round(py);
46
+ const rz = Math.round(pz);
47
+ let xb = Math.imul(rx, PX);
48
+ let yb = Math.imul(ry, PY);
49
+ let zb = Math.imul(rz, PZ);
50
+ let xr = px - rx;
51
+ let yr = py - ry;
52
+ let zr = pz - rz;
53
+ let xs = xr < 0 ? 1 : -1;
54
+ let ys = yr < 0 ? 1 : -1;
55
+ let zs = zr < 0 ? 1 : -1;
56
+ let ax = Math.abs(xr);
57
+ let ay = Math.abs(yr);
58
+ let az = Math.abs(zr);
59
+ let a = R3 - xr * xr - (yr * yr + zr * zr);
60
+ let s = seed;
61
+ let value = 0;
62
+ for (let l = 0; ; l++) {
63
+ const a0 = Math.max(a, 0);
64
+ value += a0 * a0 * a0 * a0 * grad3(gradHash3(s, xb, yb, zb), xr, yr, zr);
65
+ const mx = ax >= ay && ax >= az;
66
+ const my = !mx && ay >= az;
67
+ const am = Math.max(ax, ay, az);
68
+ const b0 = Math.max(a + am + am - 1, 0);
69
+ const hx = mx ? xb - Math.imul(xs, PX) | 0 : xb;
70
+ const hy = my ? yb - Math.imul(ys, PY) | 0 : yb;
71
+ const hz = !mx && !my ? zb - Math.imul(zs, PZ) | 0 : zb;
72
+ const ox = xr + (mx ? xs : 0);
73
+ const oy = yr + (my ? ys : 0);
74
+ const oz = zr + (!mx && !my ? zs : 0);
75
+ value += b0 * b0 * b0 * b0 * grad3(gradHash3(s, hx, hy, hz), ox, oy, oz);
76
+ if (l === 1) break;
77
+ ax = 0.5 - ax;
78
+ ay = 0.5 - ay;
79
+ az = 0.5 - az;
80
+ xr = xs * ax;
81
+ yr = ys * ay;
82
+ zr = zs * az;
83
+ a += 0.75 - ax - (ay + az);
84
+ if (xs === -1) xb = xb + PX | 0;
85
+ if (ys === -1) yb = yb + PY | 0;
86
+ if (zs === -1) zb = zb + PZ | 0;
87
+ xs = -xs;
88
+ ys = -ys;
89
+ zs = -zs;
90
+ s ^= SEED_FLIP_3D;
91
+ }
92
+ return Math.max(-1, Math.min(1, value * 32));
93
+ }
94
+ export {
95
+ perlin3Reference,
96
+ simplex3Reference
97
+ };
@@ -0,0 +1,67 @@
1
+ import type { Workers } from '@aethervtt/shard-platform';
2
+ import { type Grid2d, type SpherePatch } from './kernel.js';
3
+ import type { NoiseGraph } from './noise-graph.js';
4
+ export type { Grid2d, SpherePatch } from './kernel.js';
5
+ /** A grid as the helpers take it: `size` and `resolution` may be one number for both axes. */
6
+ export interface GridParams {
7
+ readonly origin: readonly [number, number];
8
+ readonly size: number | readonly [number, number];
9
+ readonly resolution: number | readonly [number, number];
10
+ readonly z?: number;
11
+ }
12
+ /**
13
+ * Samples a graph at absolute positions (`xyz…`, or `xyzw…` for a 4D graph) into `out`. Precise
14
+ * near the origin only (past ~10⁵ units fine octaves lose their fraction bits): use `sampleOffset`
15
+ * for large domains. Sync, on the calling thread; meant for small batches (a raycast's worth).
16
+ */
17
+ export declare function sampleNoise(graph: NoiseGraph, seed: number, points: Float32Array, out: Float32Array, node?: string): void;
18
+ /**
19
+ * Samples at `origin + local`: `origin` is an f64 point (a chunk's center) and `local` small f32
20
+ * offsets from it. Every source splits its lattice position of the origin into an integer cell and
21
+ * a fraction in f64, so precision depends only on the size of `local`, not on where the origin is.
22
+ * Throws `noise/frequency-too-high` if a lattice cell leaves the i32 range.
23
+ */
24
+ export declare function sampleOffset(graph: NoiseGraph, seed: number, origin: ArrayLike<number>, localPoints: Float32Array, out: Float32Array, node?: string): void;
25
+ /** The origin records `noise_<name>_at` reads in WGSL, for sampling around `origin`. */
26
+ export declare function noiseOrigins(graph: NoiseGraph, origin: ArrayLike<number>, out?: Int32Array): Int32Array;
27
+ export declare function normalizeGrid(g: GridParams): Grid2d;
28
+ /**
29
+ * Samples a `resolution` grid spanning `origin` to `origin + size` on the z plane (x along a row,
30
+ * rows along y) into `out`, row by row. Offsets from the grid's center, so it's precise anywhere.
31
+ */
32
+ export declare function sampleGrid2d(graph: NoiseGraph, seed: number, grid: GridParams, out: Float32Array, node?: string): void;
33
+ /**
34
+ * Samples a cube-sphere patch: `resolution`² vertices over face coordinates [x0, x0 + extent] ×
35
+ * [y0, y0 + extent] (tangent-adjusted, as 0043's terrain), at direction × radius. Offsets from the
36
+ * patch center, so a chunk on a gas giant samples as precisely as one at the origin.
37
+ */
38
+ export declare function sampleSpherePatch(graph: NoiseGraph, seed: number, patch: SpherePatch, out: Float32Array, node?: string): void;
39
+ /**
40
+ * The gradient of the graph at each point, by central differences of `h` (default: a thousandth
41
+ * of the finest lattice cell), written to `dx`, `dy`, `dz`; `out` gets the values. For CPU normals
42
+ * and slope masks. Positions are absolute, as in `sampleNoise`.
43
+ */
44
+ export declare function sampleNoiseGradient(graph: NoiseGraph, seed: number, points: Float32Array, out: Float32Array, dx: Float32Array, dy: Float32Array, dz: Float32Array, h?: number): void;
45
+ export interface AsyncOptions {
46
+ readonly priority?: 'high' | 'normal';
47
+ /** Sample `node` instead of the graph's output. */
48
+ readonly node?: string;
49
+ }
50
+ /**
51
+ * `sampleNoise` (or `sampleOffset`, with `origin`) on the worker pool. `points` is copied and
52
+ * moves to the worker; the values move back and are copied into `out`.
53
+ */
54
+ export declare function sampleNoiseAsync(workers: Workers, graph: NoiseGraph, seed: number, points: Float32Array, out: Float32Array, options?: AsyncOptions & {
55
+ origin?: ArrayLike<number>;
56
+ }): Promise<void>;
57
+ /**
58
+ * Milliseconds the calling thread spent in pool sampling since the last reset: posting jobs and
59
+ * copying results into `out`. The bench checks it stays small next to the work itself.
60
+ */
61
+ export declare const poolTiming: {
62
+ mainThreadMs: number;
63
+ };
64
+ /** `sampleSpherePatch` on the worker pool, split by rows across its workers. */
65
+ export declare function sampleSpherePatchAsync(workers: Workers, graph: NoiseGraph, seed: number, patch: SpherePatch, out: Float32Array, options?: AsyncOptions): Promise<void>;
66
+ /** `sampleGrid2d` on the worker pool, split by rows across its workers. */
67
+ export declare function sampleGrid2dAsync(workers: Workers, graph: NoiseGraph, seed: number, grid: GridParams, out: Float32Array, options?: AsyncOptions): Promise<void>;
package/dist/sample.js ADDED
@@ -0,0 +1,232 @@
1
+ import { ShardError } from "@aethervtt/shard-core";
2
+ import {
3
+ computeOrigins,
4
+ evalProgram,
5
+ gridOrigin,
6
+ gridPoints,
7
+ patchOrigin,
8
+ patchPoints
9
+ } from "./kernel.js";
10
+ import { loadNoiseKernel, noiseKernel } from "./loader.js";
11
+ const WORKER = new URL("./worker.js", import.meta.url).href;
12
+ let originScratch = new Int32Array(0);
13
+ let localScratch = new Float32Array(0);
14
+ const origin64 = new Float64Array(4);
15
+ function origins(program) {
16
+ const n = program.zeroOrigins.length;
17
+ if (originScratch.length < n) originScratch = new Int32Array(n);
18
+ return n === originScratch.length ? originScratch : originScratch.subarray(0, n);
19
+ }
20
+ function local(n) {
21
+ if (localScratch.length < n) localScratch = new Float32Array(n);
22
+ return localScratch;
23
+ }
24
+ function shard(err) {
25
+ if (err instanceof ShardError) throw err;
26
+ const e = err;
27
+ throw new ShardError(e.code ?? "noise/sample-failed", e.message ?? String(err), {
28
+ ...e.hint ? { hint: e.hint } : {}
29
+ });
30
+ }
31
+ function stride(graph) {
32
+ return graph.program.dimensions === 4 ? 4 : 3;
33
+ }
34
+ function check(count, out) {
35
+ if (out.length < count) {
36
+ throw new ShardError(
37
+ "noise/out-too-small",
38
+ `out holds ${out.length} values; the call writes ${count}`,
39
+ {
40
+ hint: "Size out to one value per point (resolution\xB2 for patches, nx \xD7 ny for grids)."
41
+ }
42
+ );
43
+ }
44
+ }
45
+ function sampleNoise(graph, seed, points, out, node) {
46
+ const program = graph.programFor(node);
47
+ const s = stride(graph);
48
+ const count = Math.floor(points.length / s);
49
+ check(count, out);
50
+ evalProgram(noiseKernel().state, program, seed, program.zeroOrigins, points, s, count, out);
51
+ }
52
+ function sampleOffset(graph, seed, origin, localPoints, out, node) {
53
+ const program = graph.programFor(node);
54
+ const s = stride(graph);
55
+ const count = Math.floor(localPoints.length / s);
56
+ check(count, out);
57
+ const records = origins(program);
58
+ try {
59
+ computeOrigins(program.terms, origin, records);
60
+ } catch (err) {
61
+ shard(err);
62
+ }
63
+ evalProgram(noiseKernel().state, program, seed, records, localPoints, s, count, out);
64
+ }
65
+ function noiseOrigins(graph, origin, out) {
66
+ const program = graph.program;
67
+ const records = out ?? new Int32Array(program.zeroOrigins.length);
68
+ try {
69
+ return computeOrigins(program.terms, origin, records);
70
+ } catch (err) {
71
+ shard(err);
72
+ }
73
+ }
74
+ function normalizeGrid(g) {
75
+ const size = typeof g.size === "number" ? [g.size, g.size] : g.size;
76
+ const res = typeof g.resolution === "number" ? [g.resolution, g.resolution] : g.resolution;
77
+ return { origin: g.origin, size, resolution: res, ...g.z !== void 0 ? { z: g.z } : {} };
78
+ }
79
+ function sampleGrid2d(graph, seed, grid, out, node) {
80
+ const g = normalizeGrid(grid);
81
+ const [nx, ny] = g.resolution;
82
+ check(nx * ny, out);
83
+ gridOrigin(g, origin64);
84
+ const pts = gridPoints(g, origin64, 0, ny, local(nx * ny * 3));
85
+ sampleArea(graph, seed, pts, nx * ny, out, node);
86
+ }
87
+ function sampleSpherePatch(graph, seed, patch, out, node) {
88
+ const n = patch.resolution;
89
+ check(n * n, out);
90
+ patchOrigin(patch, origin64);
91
+ const pts = patchPoints(patch, origin64, 0, n, local(n * n * 3));
92
+ sampleArea(graph, seed, pts, n * n, out, node);
93
+ }
94
+ function sampleArea(graph, seed, pts, count, out, node) {
95
+ const program = graph.programFor(node);
96
+ if (program.dimensions === 4) {
97
+ throw new ShardError(
98
+ "noise/domain-mismatch",
99
+ "Grids and sphere patches are 3D; this graph is 4D",
100
+ {
101
+ hint: "Sample a 4D graph with sampleNoise or sampleOffset and xyzw points."
102
+ }
103
+ );
104
+ }
105
+ const records = origins(program);
106
+ try {
107
+ computeOrigins(program.terms, origin64, records);
108
+ } catch (err) {
109
+ shard(err);
110
+ }
111
+ evalProgram(noiseKernel().state, program, seed, records, pts, 3, count, out);
112
+ }
113
+ function sampleNoiseGradient(graph, seed, points, out, dx, dy, dz, h) {
114
+ const s = stride(graph);
115
+ const count = Math.floor(points.length / s);
116
+ check(count, out);
117
+ let finest = 0;
118
+ for (const a of graph.program.terms.a) finest = Math.max(finest, Math.abs(a));
119
+ const step = h ?? (finest > 0 ? 1e-3 / finest : 1e-3);
120
+ const pts = local(count * s * 7);
121
+ const values = new Float32Array(count * 7);
122
+ for (let i = 0; i < count; i++) {
123
+ for (let v = 0; v < 7; v++) {
124
+ const o = (v * count + i) * s;
125
+ for (let c = 0; c < s; c++) pts[o + c] = points[i * s + c];
126
+ }
127
+ for (let axis = 0; axis < 3; axis++) {
128
+ pts[((1 + axis * 2) * count + i) * s + axis] += step;
129
+ pts[((2 + axis * 2) * count + i) * s + axis] -= step;
130
+ }
131
+ }
132
+ const program = graph.program;
133
+ evalProgram(noiseKernel().state, program, seed, program.zeroOrigins, pts, s, count * 7, values);
134
+ const inv = 1 / (2 * step);
135
+ for (let i = 0; i < count; i++) {
136
+ out[i] = values[i];
137
+ dx[i] = (values[count + i] - values[2 * count + i]) * inv;
138
+ dy[i] = (values[3 * count + i] - values[4 * count + i]) * inv;
139
+ dz[i] = (values[5 * count + i] - values[6 * count + i]) * inv;
140
+ }
141
+ }
142
+ async function sampleNoiseAsync(workers, graph, seed, points, out, options = {}) {
143
+ const program = graph.programFor(options.node);
144
+ const s = stride(graph);
145
+ const count = Math.floor(points.length / s);
146
+ check(count, out);
147
+ const { module } = await loadNoiseKernel();
148
+ const copy = points.slice(0, count * s);
149
+ const job = {
150
+ kind: "points",
151
+ points: copy,
152
+ stride: s,
153
+ ...options.origin ? { origin: Float64Array.from(options.origin) } : {}
154
+ };
155
+ const values = await workers.run(WORKER, "sample", [module, program, seed >>> 0, job], {
156
+ transfer: [copy.buffer],
157
+ priority: options.priority
158
+ }).catch(shard);
159
+ out.set(values);
160
+ }
161
+ const poolTiming = { mainThreadMs: 0 };
162
+ function splits(workers, rows) {
163
+ const lanes = workers.size > 0 ? workers.size : 1;
164
+ return Math.max(1, Math.min(lanes, Math.ceil(rows / 16)));
165
+ }
166
+ async function areaAsync(workers, graph, seed, kind, area, width, rows, out, options) {
167
+ const program = graph.programFor(options.node);
168
+ const { module } = await loadNoiseKernel();
169
+ const t0 = performance.now();
170
+ const jobs = splits(workers, rows);
171
+ const per = Math.ceil(rows / jobs);
172
+ const pending = [];
173
+ for (let row0 = 0; row0 < rows; row0 += per) {
174
+ const row1 = Math.min(rows, row0 + per);
175
+ pending.push(
176
+ workers.run(
177
+ WORKER,
178
+ "sample",
179
+ [module, program, seed >>> 0, { kind, area, row0, row1 }],
180
+ {
181
+ priority: options.priority
182
+ }
183
+ ).then((values) => {
184
+ const t = performance.now();
185
+ out.set(values, row0 * width);
186
+ poolTiming.mainThreadMs += performance.now() - t;
187
+ }, shard)
188
+ );
189
+ }
190
+ poolTiming.mainThreadMs += performance.now() - t0;
191
+ await Promise.all(pending);
192
+ }
193
+ function sampleSpherePatchAsync(workers, graph, seed, patch, out, options = {}) {
194
+ check(patch.resolution * patch.resolution, out);
195
+ const area = {
196
+ face: patch.face,
197
+ x0: patch.x0,
198
+ y0: patch.y0,
199
+ extent: patch.extent,
200
+ resolution: patch.resolution,
201
+ radius: patch.radius
202
+ };
203
+ return areaAsync(
204
+ workers,
205
+ graph,
206
+ seed,
207
+ "patch",
208
+ area,
209
+ patch.resolution,
210
+ patch.resolution,
211
+ out,
212
+ options
213
+ );
214
+ }
215
+ function sampleGrid2dAsync(workers, graph, seed, grid, out, options = {}) {
216
+ const g = normalizeGrid(grid);
217
+ check(g.resolution[0] * g.resolution[1], out);
218
+ return areaAsync(workers, graph, seed, "grid", g, g.resolution[0], g.resolution[1], out, options);
219
+ }
220
+ export {
221
+ noiseOrigins,
222
+ normalizeGrid,
223
+ poolTiming,
224
+ sampleGrid2d,
225
+ sampleGrid2dAsync,
226
+ sampleNoise,
227
+ sampleNoiseAsync,
228
+ sampleNoiseGradient,
229
+ sampleOffset,
230
+ sampleSpherePatch,
231
+ sampleSpherePatchAsync
232
+ };
@@ -0,0 +1,2 @@
1
+ /** `.shard/schemas/noise.schema.json`, generated from the node table. */
2
+ export declare function noiseJsonSchema(): Record<string, unknown>;
package/dist/schema.js ADDED
@@ -0,0 +1,132 @@
1
+ import { NODE_DEFS } from "./nodes.js";
2
+ function paramSchema(p) {
3
+ const base = { description: p.description };
4
+ switch (p.type) {
5
+ case "input":
6
+ return { ...base, $ref: "#/definitions/input" };
7
+ case "number":
8
+ return {
9
+ ...base,
10
+ type: "number",
11
+ ...p.default !== void 0 ? { default: p.default } : {},
12
+ ...p.min !== void 0 ? { minimum: p.min } : {},
13
+ ...p.max !== void 0 ? { maximum: p.max } : {},
14
+ ...p.positive ? { exclusiveMinimum: 0 } : {}
15
+ };
16
+ case "integer":
17
+ return {
18
+ ...base,
19
+ type: "integer",
20
+ ...p.default !== void 0 ? { default: p.default } : {},
21
+ ...p.min !== void 0 ? { minimum: p.min } : {},
22
+ ...p.max !== void 0 ? { maximum: p.max } : {}
23
+ };
24
+ case "boolean":
25
+ return {
26
+ ...base,
27
+ type: "boolean",
28
+ ...p.default !== void 0 ? { default: p.default } : {}
29
+ };
30
+ case "enum":
31
+ return {
32
+ ...base,
33
+ enum: [...p.values],
34
+ ...p.default !== void 0 ? { default: p.default } : {}
35
+ };
36
+ case "pair":
37
+ return {
38
+ ...base,
39
+ type: "array",
40
+ items: { type: "number" },
41
+ minItems: 2,
42
+ maxItems: 2,
43
+ ...p.default ? { default: [...p.default] } : {}
44
+ };
45
+ case "vec":
46
+ return {
47
+ ...base,
48
+ anyOf: [
49
+ { type: "number" },
50
+ { type: "array", items: { type: "number" }, minItems: 2, maxItems: 4 }
51
+ ],
52
+ ...p.default !== void 0 ? { default: p.default } : {}
53
+ };
54
+ case "points":
55
+ return {
56
+ ...base,
57
+ type: "array",
58
+ minItems: 2,
59
+ items: { type: "array", items: { type: "number" }, minItems: 2, maxItems: 2 }
60
+ };
61
+ }
62
+ }
63
+ function noiseJsonSchema() {
64
+ const nodes = [];
65
+ for (const def of NODE_DEFS) {
66
+ let body;
67
+ if (def.form === "value") body = { type: "number" };
68
+ else if (def.form === "list")
69
+ body = { type: "array", minItems: 2, items: { $ref: "#/definitions/input" } };
70
+ else {
71
+ const properties = {};
72
+ const required = [];
73
+ for (const [name, p] of Object.entries(def.params)) {
74
+ properties[name] = paramSchema(p);
75
+ if (p.type === "input" || p.type === "points") required.push(name);
76
+ }
77
+ const object = {
78
+ type: "object",
79
+ properties,
80
+ additionalProperties: false,
81
+ ...required.length ? { required } : {}
82
+ };
83
+ body = def.form === "unary" ? { anyOf: [{ $ref: "#/definitions/input" }, object] } : object;
84
+ }
85
+ nodes.push({
86
+ type: "object",
87
+ description: `${def.description} Range: ${def.range}.`,
88
+ properties: { [def.name]: { ...body, description: def.description } },
89
+ required: [def.name],
90
+ additionalProperties: false,
91
+ examples: [def.example]
92
+ });
93
+ }
94
+ return {
95
+ $schema: "http://json-schema.org/draft-07/schema#",
96
+ title: "Noise graph",
97
+ description: "A noise graph (*.noise.json): named nodes, and `output` naming the one the graph returns. The CPU kernel and the generated WGSL both evaluate it; seeds per source mix with the seed it is sampled with.",
98
+ type: "object",
99
+ properties: {
100
+ $schema: { type: "string" },
101
+ description: { type: "string", description: "What the graph is for." },
102
+ output: { type: "string", description: "The node the graph returns." },
103
+ dimensions: {
104
+ enum: [2, 3, 4],
105
+ default: 3,
106
+ description: "Domain dimensions. 4 adds a w coordinate (animated or looping noise)."
107
+ },
108
+ extent: {
109
+ type: "number",
110
+ exclusiveMinimum: 0,
111
+ description: "Largest distance from the origin the graph is sampled at (a planet radius). The importer rejects frequencies too fine to address at that distance (noise/frequency-too-high)."
112
+ },
113
+ nodes: {
114
+ type: "object",
115
+ minProperties: 1,
116
+ additionalProperties: { $ref: "#/definitions/node" }
117
+ }
118
+ },
119
+ required: ["output", "nodes"],
120
+ additionalProperties: false,
121
+ definitions: {
122
+ input: {
123
+ description: "A node name, an inline node, or a number.",
124
+ anyOf: [{ type: "string" }, { type: "number" }, { $ref: "#/definitions/node" }]
125
+ },
126
+ node: { anyOf: [{ type: "number" }, ...nodes] }
127
+ }
128
+ };
129
+ }
130
+ export {
131
+ noiseJsonSchema
132
+ };
@@ -0,0 +1,58 @@
1
+ import type { PreviewImage } from '@aethervtt/shard-assets';
2
+ import type { NoiseGraph } from './noise-graph.js';
3
+ export interface NoiseDomain {
4
+ /** `plane`: a square on z = 0, centered on the origin. `sphere`: a sphere's surface. */
5
+ readonly kind?: 'plane' | 'sphere';
6
+ /** Plane side length. Default: twice the graph's extent, or 4. */
7
+ readonly size?: number;
8
+ /** Sphere radius. Default: the graph's extent, or 1. */
9
+ readonly radius?: number;
10
+ /** Samples per side (per cube face for spheres). Default 256 (plane) or 105 (six faces). */
11
+ readonly resolution?: number;
12
+ }
13
+ export interface NoiseStats {
14
+ readonly samples: number;
15
+ readonly min: number;
16
+ readonly max: number;
17
+ readonly mean: number;
18
+ readonly stdDev: number;
19
+ /** `bins` equal-width buckets from `from` to `to`, as fractions of the domain. */
20
+ readonly histogram: {
21
+ readonly from: number;
22
+ readonly to: number;
23
+ readonly bins: number[];
24
+ };
25
+ /** For each requested threshold, the fraction of the domain below it. */
26
+ readonly below: {
27
+ readonly threshold: number;
28
+ readonly fraction: number;
29
+ }[];
30
+ }
31
+ /**
32
+ * Samples a graph over a domain and summarizes it: min, max, mean, standard deviation, a histogram,
33
+ * and the fraction below each threshold. Sphere samples are weighted by the area they cover, so
34
+ * "30% below sea level" means 30% of the surface. Deterministic: the same numbers on every host.
35
+ */
36
+ export declare function noiseStats(graph: NoiseGraph, seed: number, domain?: NoiseDomain, options?: {
37
+ node?: string;
38
+ thresholds?: readonly number[];
39
+ }): NoiseStats;
40
+ export interface NoisePreviewOptions {
41
+ /** `plane` (default): the domain's square. `sphere`: an orthographic view of the sphere from +z. */
42
+ readonly domain?: 'plane' | 'sphere';
43
+ readonly seed?: number;
44
+ /** Image side in pixels; overrides width and height. */
45
+ readonly size?: number;
46
+ /** Preview an intermediate node instead of the output. */
47
+ readonly node?: string;
48
+ /** Plane side length or sphere radius; defaults as in `noiseStats`. */
49
+ readonly span?: number;
50
+ }
51
+ /**
52
+ * A grayscale picture of a graph (black at its minimum, white at its maximum). Returns the image and
53
+ * the value range it maps.
54
+ */
55
+ export declare function previewNoise(graph: NoiseGraph, width: number, height: number, options?: NoisePreviewOptions): PreviewImage & {
56
+ min: number;
57
+ max: number;
58
+ };