@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.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/compile.d.ts +61 -0
- package/dist/compile.js +375 -0
- package/dist/gpu.d.ts +11 -0
- package/dist/gpu.js +15 -0
- package/dist/graph.d.ts +44 -0
- package/dist/graph.js +370 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +127 -0
- package/dist/kernel.d.ts +99 -0
- package/dist/kernel.js +263 -0
- package/dist/loader.d.ts +25 -0
- package/dist/loader.js +108 -0
- package/dist/methods.d.ts +9 -0
- package/dist/methods.js +119 -0
- package/dist/nodes.d.ts +74 -0
- package/dist/nodes.js +366 -0
- package/dist/noise-graph.d.ts +80 -0
- package/dist/noise-graph.js +258 -0
- package/dist/planet.d.ts +44 -0
- package/dist/planet.js +15 -0
- package/dist/plugin.d.ts +2 -0
- package/dist/plugin.js +18 -0
- package/dist/reference.d.ts +9 -0
- package/dist/reference.js +97 -0
- package/dist/sample.d.ts +67 -0
- package/dist/sample.js +232 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +132 -0
- package/dist/stats.d.ts +58 -0
- package/dist/stats.js +164 -0
- package/dist/wgsl.d.ts +17 -0
- package/dist/wgsl.js +641 -0
- package/dist/worker.js +58 -0
- package/package.json +39 -0
- package/wasm/shard_noise.wasm +0 -0
- package/wasm/shard_noise_simd.wasm +0 -0
package/dist/nodes.js
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
const SOURCE_KINDS = ["value", "perlin", "simplex", "cellular"];
|
|
2
|
+
const CELL_DISTANCES = ["euclidean", "manhattan", "chebyshev"];
|
|
3
|
+
const CELL_RETURNS = ["f1", "f2", "f2-f1", "cell"];
|
|
4
|
+
const MAX_OCTAVES = 16;
|
|
5
|
+
const seed = {
|
|
6
|
+
type: "integer",
|
|
7
|
+
default: 0,
|
|
8
|
+
min: 0,
|
|
9
|
+
max: 4294967295,
|
|
10
|
+
description: "Layer seed, mixed with the seed the graph is sampled with. Changing it changes only this layer."
|
|
11
|
+
};
|
|
12
|
+
const frequency = {
|
|
13
|
+
type: "number",
|
|
14
|
+
default: 1,
|
|
15
|
+
positive: true,
|
|
16
|
+
description: "Features per unit of distance. 1 means one lattice cell per unit."
|
|
17
|
+
};
|
|
18
|
+
const dims = (values) => ({
|
|
19
|
+
type: "enum",
|
|
20
|
+
values,
|
|
21
|
+
default: 3,
|
|
22
|
+
description: `Dimensions sampled: ${values.join(", ")}. 2 reads x and y only; 4 needs a 4D graph.`
|
|
23
|
+
});
|
|
24
|
+
const cellular = {
|
|
25
|
+
distance: {
|
|
26
|
+
type: "enum",
|
|
27
|
+
values: CELL_DISTANCES,
|
|
28
|
+
default: "euclidean",
|
|
29
|
+
description: "Distance metric to feature points."
|
|
30
|
+
},
|
|
31
|
+
return: {
|
|
32
|
+
type: "enum",
|
|
33
|
+
values: CELL_RETURNS,
|
|
34
|
+
default: "f1",
|
|
35
|
+
description: "f1: distance to the nearest point. f2: to the second nearest. f2-f1: cell edges (0 on a border). cell: a random value per cell in [-1, 1]."
|
|
36
|
+
},
|
|
37
|
+
jitter: {
|
|
38
|
+
type: "number",
|
|
39
|
+
default: 1,
|
|
40
|
+
min: 0,
|
|
41
|
+
max: 1,
|
|
42
|
+
description: "How far feature points move from cell centers: 0 is a regular grid."
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const input = (description) => ({ type: "input", description });
|
|
46
|
+
const fractal = (name, description, range, example) => ({
|
|
47
|
+
name,
|
|
48
|
+
category: "fractal",
|
|
49
|
+
description,
|
|
50
|
+
range,
|
|
51
|
+
form: "params",
|
|
52
|
+
params: {
|
|
53
|
+
source: {
|
|
54
|
+
type: "enum",
|
|
55
|
+
values: SOURCE_KINDS,
|
|
56
|
+
default: "simplex",
|
|
57
|
+
description: "The source summed per octave."
|
|
58
|
+
},
|
|
59
|
+
octaves: {
|
|
60
|
+
type: "integer",
|
|
61
|
+
default: 4,
|
|
62
|
+
min: 1,
|
|
63
|
+
max: MAX_OCTAVES,
|
|
64
|
+
description: `Layers summed, 1 to ${MAX_OCTAVES}. Each adds detail at lacunarity \xD7 the previous frequency.`
|
|
65
|
+
},
|
|
66
|
+
frequency: { ...frequency, description: "Frequency of the first octave." },
|
|
67
|
+
lacunarity: {
|
|
68
|
+
type: "number",
|
|
69
|
+
default: 2,
|
|
70
|
+
positive: true,
|
|
71
|
+
description: "Frequency multiplier per octave (usually 2)."
|
|
72
|
+
},
|
|
73
|
+
gain: {
|
|
74
|
+
type: "number",
|
|
75
|
+
default: 0.5,
|
|
76
|
+
positive: true,
|
|
77
|
+
description: "Amplitude multiplier per octave (0.5 halves each; higher is rougher)."
|
|
78
|
+
},
|
|
79
|
+
seed,
|
|
80
|
+
dims: dims([2, 3, 4]),
|
|
81
|
+
...cellular
|
|
82
|
+
},
|
|
83
|
+
example
|
|
84
|
+
});
|
|
85
|
+
const shape = (def) => ({
|
|
86
|
+
category: "shape",
|
|
87
|
+
form: "params",
|
|
88
|
+
...def
|
|
89
|
+
});
|
|
90
|
+
const NODE_DEFS = [
|
|
91
|
+
{
|
|
92
|
+
name: "value",
|
|
93
|
+
category: "source",
|
|
94
|
+
description: "Value noise: random values on a lattice, smoothly interpolated. Blocky, cheap.",
|
|
95
|
+
range: "[-1, 1], mean 0",
|
|
96
|
+
form: "params",
|
|
97
|
+
params: { seed, frequency, dims: dims([2, 3]) },
|
|
98
|
+
example: { value: { frequency: 4, seed: 1 } }
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "perlin",
|
|
102
|
+
category: "source",
|
|
103
|
+
description: "Perlin gradient noise: smooth, with a slight grid alignment.",
|
|
104
|
+
range: "[-1, 1], mean 0",
|
|
105
|
+
form: "params",
|
|
106
|
+
params: { seed, frequency, dims: dims([2, 3]) },
|
|
107
|
+
example: { perlin: { frequency: 2, seed: 1 } }
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "simplex",
|
|
111
|
+
category: "source",
|
|
112
|
+
description: "Simplex noise: smooth and isotropic, the usual choice. 3D uses the OpenSimplex2 lattice; 2D and 4D the classic simplex lattice.",
|
|
113
|
+
range: "[-1, 1], mean 0",
|
|
114
|
+
form: "params",
|
|
115
|
+
params: { seed, frequency, dims: dims([2, 3, 4]) },
|
|
116
|
+
example: { simplex: { frequency: 1, seed: 1 } }
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "cellular",
|
|
120
|
+
category: "source",
|
|
121
|
+
description: "Cellular (Worley) noise: distances to jittered feature points, one per cell. Cracks, cells, pebbles.",
|
|
122
|
+
range: "euclidean f1 [0, 1.25] (mean \u2248 0.43 in 2D, 0.52 in 3D); f2 [0, 1.5] (mean \u2248 0.70); f2-f1 [0, 1.25] (mean \u2248 0.2 in 3D); manhattan distances reach 2 and chebyshev 1; cell [-1, 1], mean 0",
|
|
123
|
+
form: "params",
|
|
124
|
+
params: { seed, frequency, dims: dims([2, 3]), ...cellular },
|
|
125
|
+
example: { cellular: { frequency: 3, return: "f2-f1" } }
|
|
126
|
+
},
|
|
127
|
+
fractal(
|
|
128
|
+
"fbm",
|
|
129
|
+
"Fractal Brownian motion: octaves of a source summed with falling amplitude. Continents, hills, clouds.",
|
|
130
|
+
"[-1, 1] (normalized by the sum of amplitudes), mean 0",
|
|
131
|
+
{ fbm: { source: "simplex", octaves: 5, frequency: 0.8, gain: 0.5, seed: 1 } }
|
|
132
|
+
),
|
|
133
|
+
fractal(
|
|
134
|
+
"ridged",
|
|
135
|
+
"Ridged multifractal: (1 \u2212 |n|)\xB2 per octave, so zero crossings become sharp ridges. Mountains.",
|
|
136
|
+
"[-1, 1], ridges near 1",
|
|
137
|
+
{ ridged: { source: "simplex", octaves: 6, frequency: 3.2, seed: 2 } }
|
|
138
|
+
),
|
|
139
|
+
fractal(
|
|
140
|
+
"billow",
|
|
141
|
+
"Billow: 2|n| \u2212 1 per octave, so valleys become creases. Puffy clouds, rolling hills.",
|
|
142
|
+
"[-1, 1]",
|
|
143
|
+
{ billow: { source: "simplex", octaves: 4, frequency: 2, seed: 3 } }
|
|
144
|
+
),
|
|
145
|
+
{
|
|
146
|
+
name: "add",
|
|
147
|
+
category: "combine",
|
|
148
|
+
description: "Sum of two or more inputs.",
|
|
149
|
+
range: "sum of the input ranges",
|
|
150
|
+
form: "list",
|
|
151
|
+
params: {},
|
|
152
|
+
example: { add: ["continents", { multiply: ["mountains", "mask"] }] }
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "multiply",
|
|
156
|
+
category: "combine",
|
|
157
|
+
description: "Product of two or more inputs. Multiply by a 0\u20131 mask to fade a layer in and out.",
|
|
158
|
+
range: "product of the input ranges",
|
|
159
|
+
form: "list",
|
|
160
|
+
params: {},
|
|
161
|
+
example: { multiply: ["mountains", "mask"] }
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "min",
|
|
165
|
+
category: "combine",
|
|
166
|
+
description: "Smallest of two or more inputs.",
|
|
167
|
+
range: "within the input ranges",
|
|
168
|
+
form: "list",
|
|
169
|
+
params: {},
|
|
170
|
+
example: { min: ["a", "b"] }
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: "max",
|
|
174
|
+
category: "combine",
|
|
175
|
+
description: "Largest of two or more inputs.",
|
|
176
|
+
range: "within the input ranges",
|
|
177
|
+
form: "list",
|
|
178
|
+
params: {},
|
|
179
|
+
example: { max: ["a", 0] }
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: "lerp",
|
|
183
|
+
category: "combine",
|
|
184
|
+
description: "a + t \xD7 (b \u2212 a): blends a toward b by t (0 gives a, 1 gives b).",
|
|
185
|
+
range: "between a and b for t in [0, 1]",
|
|
186
|
+
form: "params",
|
|
187
|
+
params: { a: input("Value at t = 0."), b: input("Value at t = 1."), t: input("Blend factor.") },
|
|
188
|
+
example: { lerp: { a: "plains", b: "mountains", t: "mask" } }
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: "select",
|
|
192
|
+
category: "combine",
|
|
193
|
+
description: "a where control is below threshold, b above it, blended smoothly over \xB1falloff (0 is a hard edge).",
|
|
194
|
+
range: "between a and b",
|
|
195
|
+
form: "params",
|
|
196
|
+
params: {
|
|
197
|
+
a: input("Value below the threshold."),
|
|
198
|
+
b: input("Value above the threshold."),
|
|
199
|
+
control: input("What decides between a and b."),
|
|
200
|
+
threshold: { type: "number", default: 0, description: "Where control switches from a to b." },
|
|
201
|
+
falloff: {
|
|
202
|
+
type: "number",
|
|
203
|
+
default: 0,
|
|
204
|
+
min: 0,
|
|
205
|
+
description: "Half-width of the smooth blend around the threshold."
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
example: {
|
|
209
|
+
select: { a: "ocean", b: "land", control: "continents", threshold: 0.1, falloff: 0.05 }
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
shape({
|
|
213
|
+
name: "remap",
|
|
214
|
+
description: "Linearly maps [from\u2080, from\u2081] to [to\u2080, to\u2081], optionally clamped. Builds masks.",
|
|
215
|
+
range: "to (when clamped)",
|
|
216
|
+
params: {
|
|
217
|
+
input: input("The value to map."),
|
|
218
|
+
from: { type: "pair", default: [-1, 1], description: "Input range." },
|
|
219
|
+
to: { type: "pair", default: [0, 1], description: "Output range." },
|
|
220
|
+
clamp: {
|
|
221
|
+
type: "boolean",
|
|
222
|
+
default: false,
|
|
223
|
+
description: "Clamp the result to the output range."
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
example: { remap: { input: "continents", from: [0.1, 0.4], to: [0, 1], clamp: true } }
|
|
227
|
+
}),
|
|
228
|
+
shape({
|
|
229
|
+
name: "clamp",
|
|
230
|
+
description: "Limits the input to [min, max].",
|
|
231
|
+
range: "[min, max]",
|
|
232
|
+
params: {
|
|
233
|
+
input: input("The value to clamp."),
|
|
234
|
+
min: { type: "number", default: -1, description: "Lower bound." },
|
|
235
|
+
max: { type: "number", default: 1, description: "Upper bound." }
|
|
236
|
+
},
|
|
237
|
+
example: { clamp: { input: "height", min: 0, max: 1 } }
|
|
238
|
+
}),
|
|
239
|
+
shape({
|
|
240
|
+
name: "curve",
|
|
241
|
+
description: "Piecewise-linear curve through control points, flat beyond the first and last. Reshapes a height profile.",
|
|
242
|
+
range: "the points\u2019 y range",
|
|
243
|
+
params: {
|
|
244
|
+
input: input("The value to reshape."),
|
|
245
|
+
points: {
|
|
246
|
+
type: "points",
|
|
247
|
+
description: "Control points [[x, y], \u2026], x increasing, at least two."
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
example: {
|
|
251
|
+
curve: {
|
|
252
|
+
input: "height",
|
|
253
|
+
points: [
|
|
254
|
+
[-1, -1],
|
|
255
|
+
[0, -0.2],
|
|
256
|
+
[0.5, 0.3],
|
|
257
|
+
[1, 1]
|
|
258
|
+
]
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}),
|
|
262
|
+
shape({
|
|
263
|
+
name: "terrace",
|
|
264
|
+
description: "Quantizes the input into steps with sloped risers. Mesas, rice terraces, strata.",
|
|
265
|
+
range: "range",
|
|
266
|
+
params: {
|
|
267
|
+
input: input("The value to terrace."),
|
|
268
|
+
steps: {
|
|
269
|
+
type: "integer",
|
|
270
|
+
default: 4,
|
|
271
|
+
min: 1,
|
|
272
|
+
max: 256,
|
|
273
|
+
description: "Steps across the range."
|
|
274
|
+
},
|
|
275
|
+
range: { type: "pair", default: [-1, 1], description: "The input range the steps span." },
|
|
276
|
+
sharpness: {
|
|
277
|
+
type: "number",
|
|
278
|
+
default: 0.5,
|
|
279
|
+
min: 0,
|
|
280
|
+
max: 1,
|
|
281
|
+
description: "0 leaves the input linear; 1 makes vertical risers."
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
example: { terrace: { input: "hills", steps: 6, sharpness: 0.7 } }
|
|
285
|
+
}),
|
|
286
|
+
shape({
|
|
287
|
+
name: "abs",
|
|
288
|
+
description: "Absolute value.",
|
|
289
|
+
range: "[0, max |input|]",
|
|
290
|
+
form: "unary",
|
|
291
|
+
params: { input: input("The value.") },
|
|
292
|
+
example: { abs: "ridges" }
|
|
293
|
+
}),
|
|
294
|
+
shape({
|
|
295
|
+
name: "power",
|
|
296
|
+
description: "sign(x) \xD7 |x|^exponent. Exponents above 1 flatten low values and sharpen peaks.",
|
|
297
|
+
range: "same sign as the input",
|
|
298
|
+
params: {
|
|
299
|
+
input: input("The base."),
|
|
300
|
+
exponent: { type: "number", default: 2, description: "The exponent." }
|
|
301
|
+
},
|
|
302
|
+
example: { power: { input: "mask", exponent: 3 } }
|
|
303
|
+
}),
|
|
304
|
+
{
|
|
305
|
+
name: "constant",
|
|
306
|
+
category: "constant",
|
|
307
|
+
description: "A fixed value. A bare number anywhere an input goes means the same.",
|
|
308
|
+
range: "the value",
|
|
309
|
+
form: "value",
|
|
310
|
+
params: {},
|
|
311
|
+
example: { constant: 0.5 }
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
name: "warp",
|
|
315
|
+
category: "domain",
|
|
316
|
+
description: "Domain warp: samples input at a position displaced by `by` (read three times with independent seeds, one per axis) \xD7 amount. Makes layers swirl and flow.",
|
|
317
|
+
range: "the input\u2019s",
|
|
318
|
+
form: "params",
|
|
319
|
+
params: {
|
|
320
|
+
input: input("What is sampled at the displaced position."),
|
|
321
|
+
by: input("The displacement source (usually a low-frequency fbm)."),
|
|
322
|
+
amount: {
|
|
323
|
+
type: "number",
|
|
324
|
+
default: 0.1,
|
|
325
|
+
description: "Displacement in domain units at |by| = 1."
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
example: { warp: { input: "mountains", by: "continents", amount: 0.15 } }
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: "scale",
|
|
332
|
+
category: "domain",
|
|
333
|
+
description: "The input sees the position multiplied by `by`: 2 doubles every frequency below it. A vector scales axes separately.",
|
|
334
|
+
range: "the input\u2019s",
|
|
335
|
+
form: "params",
|
|
336
|
+
params: {
|
|
337
|
+
input: input("What sees the scaled position."),
|
|
338
|
+
by: { type: "vec", default: 1, description: "Scale: a number, or [x, y, z] (and w in 4D)." }
|
|
339
|
+
},
|
|
340
|
+
example: { scale: { input: "hills", by: [1, 0.25, 1] } }
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: "translate",
|
|
344
|
+
category: "domain",
|
|
345
|
+
description: "The input sees the position moved by `by`. Offsets two otherwise identical layers.",
|
|
346
|
+
range: "the input\u2019s",
|
|
347
|
+
form: "params",
|
|
348
|
+
params: {
|
|
349
|
+
input: input("What sees the moved position."),
|
|
350
|
+
by: { type: "vec", default: 0, description: "Offset: a number, or [x, y, z] (and w in 4D)." }
|
|
351
|
+
},
|
|
352
|
+
example: { translate: { input: "hills", by: [100, 0, 0] } }
|
|
353
|
+
}
|
|
354
|
+
];
|
|
355
|
+
const byName = new Map(NODE_DEFS.map((d) => [d.name, d]));
|
|
356
|
+
function nodeDef(name) {
|
|
357
|
+
return byName.get(name);
|
|
358
|
+
}
|
|
359
|
+
export {
|
|
360
|
+
CELL_DISTANCES,
|
|
361
|
+
CELL_RETURNS,
|
|
362
|
+
MAX_OCTAVES,
|
|
363
|
+
NODE_DEFS,
|
|
364
|
+
SOURCE_KINDS,
|
|
365
|
+
nodeDef
|
|
366
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { AssetStore } from '@aethervtt/shard-assets';
|
|
2
|
+
import { type JsonValue } from '@aethervtt/shard-core';
|
|
3
|
+
import { type NoiseProgram } from './compile.js';
|
|
4
|
+
import { type ParsedGraph } from './graph.js';
|
|
5
|
+
/** A program as JSON: what the importer writes and loading copies back into typed arrays. */
|
|
6
|
+
export interface ProgramJson {
|
|
7
|
+
code: number[];
|
|
8
|
+
consts: number[];
|
|
9
|
+
registers: number;
|
|
10
|
+
result: number;
|
|
11
|
+
dimensions: 2 | 3 | 4;
|
|
12
|
+
terms: {
|
|
13
|
+
a: number[];
|
|
14
|
+
b: number[];
|
|
15
|
+
skew: number[];
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** The `noise` importer's artifact: the source graph, its program, the WGSL, and a hash. */
|
|
19
|
+
export interface NoiseGraphArtifact {
|
|
20
|
+
format: 1;
|
|
21
|
+
name: string;
|
|
22
|
+
module: string;
|
|
23
|
+
source: JsonValue;
|
|
24
|
+
program: ProgramJson;
|
|
25
|
+
wgsl: string;
|
|
26
|
+
hash: string;
|
|
27
|
+
}
|
|
28
|
+
/** The WGSL function name (`noise_<name>`) and module path (`noise::…`) for an asset path. */
|
|
29
|
+
export declare function noiseNames(path: string): {
|
|
30
|
+
name: string;
|
|
31
|
+
module: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* A noise graph ready to sample: the validated graph, its compiled program (what the CPU kernel
|
|
35
|
+
* runs), and its WGSL module (what shaders import). One JSON source gives both the same function.
|
|
36
|
+
*/
|
|
37
|
+
export declare class NoiseGraph {
|
|
38
|
+
/** Bumps on hot reload. */
|
|
39
|
+
version: number;
|
|
40
|
+
private nodePrograms;
|
|
41
|
+
/** The graph file's JSON (without `$schema`). */
|
|
42
|
+
source: JsonValue;
|
|
43
|
+
graph: ParsedGraph;
|
|
44
|
+
/** What the CPU kernel runs. */
|
|
45
|
+
program: NoiseProgram;
|
|
46
|
+
/** Function suffix: the WGSL exports `noise_<name>` and `noise_<name>_at`. */
|
|
47
|
+
name: string;
|
|
48
|
+
/** WGSL module path, e.g. `noise::assets::noise::planet`. */
|
|
49
|
+
module: string;
|
|
50
|
+
wgsl: string;
|
|
51
|
+
/** Hash of the program: equal hashes sample equal values. */
|
|
52
|
+
hash: string;
|
|
53
|
+
constructor(source: JsonValue, graph: ParsedGraph, program: NoiseProgram, name: string, module: string, wgsl: string, hash: string);
|
|
54
|
+
/** The WGSL function for positions near the origin. */
|
|
55
|
+
get fn(): string;
|
|
56
|
+
/** Parses, validates, and compiles a graph. Throws its first error, with the rest in `details`. */
|
|
57
|
+
static fromJson(json: unknown, options?: {
|
|
58
|
+
name?: string;
|
|
59
|
+
module?: string;
|
|
60
|
+
}): NoiseGraph;
|
|
61
|
+
/** `fromJson` once the kernel is loaded, so the graph can be sampled synchronously. */
|
|
62
|
+
static create(json: unknown, options?: {
|
|
63
|
+
name?: string;
|
|
64
|
+
module?: string;
|
|
65
|
+
}): Promise<NoiseGraph>;
|
|
66
|
+
static fromArtifact(artifact: NoiseGraphArtifact): NoiseGraph;
|
|
67
|
+
toArtifact(): NoiseGraphArtifact;
|
|
68
|
+
/** The program with `node` as its output (the graph's own output by default), for previews. */
|
|
69
|
+
programFor(node?: string): NoiseProgram;
|
|
70
|
+
/** Hot reload: takes `next`'s graph, program, and WGSL, and bumps `version`. */
|
|
71
|
+
copyFrom(next: NoiseGraph): void;
|
|
72
|
+
}
|
|
73
|
+
export declare class NoiseGraphStore extends AssetStore<NoiseGraph, 'NoiseGraph'> {
|
|
74
|
+
constructor();
|
|
75
|
+
}
|
|
76
|
+
export declare const NoiseGraphs: import("@aethervtt/shard-core").ResourceDef<NoiseGraphStore>;
|
|
77
|
+
export declare const NoiseGraphAssetType: import("@aethervtt/shard-assets").AssetTypeDef<NoiseGraph>;
|
|
78
|
+
/** `*.noise.json`: a noise graph, validated with pointers into the file and compiled. */
|
|
79
|
+
export declare const NoiseGraphImporter: import("@aethervtt/shard-assets").ImporterDef;
|
|
80
|
+
export declare const noiseGraphSchema: () => unknown;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AssetStore,
|
|
3
|
+
defineAssetSchema,
|
|
4
|
+
defineAssetType,
|
|
5
|
+
defineImporter
|
|
6
|
+
} from "@aethervtt/shard-assets";
|
|
7
|
+
import { defineResource, defineSchema, ShardError } from "@aethervtt/shard-core";
|
|
8
|
+
import { compileGraph } from "./compile.js";
|
|
9
|
+
import { parseGraph } from "./graph.js";
|
|
10
|
+
import { computeOrigins } from "./kernel.js";
|
|
11
|
+
import { loadNoiseKernel } from "./loader.js";
|
|
12
|
+
import { noiseJsonSchema } from "./schema.js";
|
|
13
|
+
import { generateWgsl } from "./wgsl.js";
|
|
14
|
+
function noiseNames(path) {
|
|
15
|
+
const clean = (s) => {
|
|
16
|
+
const t = s.toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
17
|
+
return /^[a-z_]/.test(t) ? t : `_${t}`;
|
|
18
|
+
};
|
|
19
|
+
const segments = path.replace(/\.noise\.json$/, "").split("/").filter(Boolean).map(clean);
|
|
20
|
+
const name = segments[segments.length - 1] ?? "graph";
|
|
21
|
+
return { name, module: `noise::${segments.join("::") || "graph"}` };
|
|
22
|
+
}
|
|
23
|
+
function throwFirst(errors) {
|
|
24
|
+
const first = errors[0];
|
|
25
|
+
throw new ShardError(
|
|
26
|
+
first.code,
|
|
27
|
+
`${first.message}${errors.length > 1 ? ` (+${errors.length - 1} more)` : ""}`,
|
|
28
|
+
{ path: first.path, hint: first.hint, details: errors }
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
function fnv(h, bytes) {
|
|
32
|
+
for (let i = 0; i < bytes.length; i++) h = Math.imul(h ^ bytes[i], 16777619);
|
|
33
|
+
return h >>> 0;
|
|
34
|
+
}
|
|
35
|
+
function programHash(p) {
|
|
36
|
+
const parts = [
|
|
37
|
+
new Uint8Array(p.code.buffer, p.code.byteOffset, p.code.byteLength),
|
|
38
|
+
new Uint8Array(p.consts.buffer, p.consts.byteOffset, p.consts.byteLength),
|
|
39
|
+
new Uint8Array(p.terms.a.buffer, p.terms.a.byteOffset, p.terms.a.byteLength),
|
|
40
|
+
new Uint8Array(p.terms.b.buffer, p.terms.b.byteOffset, p.terms.b.byteLength),
|
|
41
|
+
p.terms.skew,
|
|
42
|
+
new Uint8Array([
|
|
43
|
+
p.registers & 255,
|
|
44
|
+
p.registers >> 8,
|
|
45
|
+
p.result & 255,
|
|
46
|
+
p.result >> 8,
|
|
47
|
+
p.dimensions
|
|
48
|
+
])
|
|
49
|
+
];
|
|
50
|
+
let a = 2166136261;
|
|
51
|
+
let b = 2538058380;
|
|
52
|
+
for (const part of parts) {
|
|
53
|
+
a = fnv(a, part);
|
|
54
|
+
b = fnv(b ^ 1540483477, part);
|
|
55
|
+
}
|
|
56
|
+
return a.toString(16).padStart(8, "0") + b.toString(16).padStart(8, "0");
|
|
57
|
+
}
|
|
58
|
+
function programFromJson(p) {
|
|
59
|
+
const terms = {
|
|
60
|
+
a: new Float64Array(p.terms.a),
|
|
61
|
+
b: new Float64Array(p.terms.b),
|
|
62
|
+
skew: new Uint8Array(p.terms.skew)
|
|
63
|
+
};
|
|
64
|
+
const zeroOrigins = new Int32Array(terms.skew.length * 8);
|
|
65
|
+
computeOrigins(terms, [0, 0, 0, 0], zeroOrigins);
|
|
66
|
+
const code = new Int32Array(p.code);
|
|
67
|
+
return {
|
|
68
|
+
code,
|
|
69
|
+
consts: new Float32Array(p.consts),
|
|
70
|
+
registers: p.registers,
|
|
71
|
+
result: p.result,
|
|
72
|
+
dimensions: p.dimensions,
|
|
73
|
+
terms,
|
|
74
|
+
zeroOrigins,
|
|
75
|
+
instructions: code.length / 12
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function programToJson(p) {
|
|
79
|
+
return {
|
|
80
|
+
code: [...p.code],
|
|
81
|
+
consts: [...p.consts],
|
|
82
|
+
registers: p.registers,
|
|
83
|
+
result: p.result,
|
|
84
|
+
dimensions: p.dimensions,
|
|
85
|
+
terms: { a: [...p.terms.a], b: [...p.terms.b], skew: [...p.terms.skew] }
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
class NoiseGraph {
|
|
89
|
+
/** Bumps on hot reload. */
|
|
90
|
+
version = 0;
|
|
91
|
+
nodePrograms = /* @__PURE__ */ new Map();
|
|
92
|
+
/** The graph file's JSON (without `$schema`). */
|
|
93
|
+
source;
|
|
94
|
+
graph;
|
|
95
|
+
/** What the CPU kernel runs. */
|
|
96
|
+
program;
|
|
97
|
+
/** Function suffix: the WGSL exports `noise_<name>` and `noise_<name>_at`. */
|
|
98
|
+
name;
|
|
99
|
+
/** WGSL module path, e.g. `noise::assets::noise::planet`. */
|
|
100
|
+
module;
|
|
101
|
+
wgsl;
|
|
102
|
+
/** Hash of the program: equal hashes sample equal values. */
|
|
103
|
+
hash;
|
|
104
|
+
constructor(source, graph, program, name, module, wgsl, hash) {
|
|
105
|
+
this.source = source;
|
|
106
|
+
this.graph = graph;
|
|
107
|
+
this.program = program;
|
|
108
|
+
this.name = name;
|
|
109
|
+
this.module = module;
|
|
110
|
+
this.wgsl = wgsl;
|
|
111
|
+
this.hash = hash;
|
|
112
|
+
}
|
|
113
|
+
/** The WGSL function for positions near the origin. */
|
|
114
|
+
get fn() {
|
|
115
|
+
return `noise_${this.name}`;
|
|
116
|
+
}
|
|
117
|
+
/** Parses, validates, and compiles a graph. Throws its first error, with the rest in `details`. */
|
|
118
|
+
static fromJson(json, options = {}) {
|
|
119
|
+
let source = json;
|
|
120
|
+
if (source && typeof source === "object" && !Array.isArray(source) && "$schema" in source) {
|
|
121
|
+
const { $schema: _, ...rest } = source;
|
|
122
|
+
source = rest;
|
|
123
|
+
}
|
|
124
|
+
const { graph, errors } = parseGraph(source);
|
|
125
|
+
if (!graph) throwFirst(errors);
|
|
126
|
+
const program = compileGraph(graph);
|
|
127
|
+
const names = options.name ? { name: options.name, module: options.module ?? `noise::${options.name}` } : noiseNames("graph");
|
|
128
|
+
const wgsl = generateWgsl(program, { name: names.name });
|
|
129
|
+
return new NoiseGraph(
|
|
130
|
+
source,
|
|
131
|
+
graph,
|
|
132
|
+
program,
|
|
133
|
+
names.name,
|
|
134
|
+
names.module,
|
|
135
|
+
wgsl,
|
|
136
|
+
programHash(program)
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
/** `fromJson` once the kernel is loaded, so the graph can be sampled synchronously. */
|
|
140
|
+
static async create(json, options = {}) {
|
|
141
|
+
await loadNoiseKernel();
|
|
142
|
+
return NoiseGraph.fromJson(json, options);
|
|
143
|
+
}
|
|
144
|
+
static fromArtifact(artifact) {
|
|
145
|
+
const { graph, errors } = parseGraph(artifact.source);
|
|
146
|
+
if (!graph) throwFirst(errors);
|
|
147
|
+
return new NoiseGraph(
|
|
148
|
+
artifact.source,
|
|
149
|
+
graph,
|
|
150
|
+
programFromJson(artifact.program),
|
|
151
|
+
artifact.name,
|
|
152
|
+
artifact.module,
|
|
153
|
+
artifact.wgsl,
|
|
154
|
+
artifact.hash
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
toArtifact() {
|
|
158
|
+
return {
|
|
159
|
+
format: 1,
|
|
160
|
+
name: this.name,
|
|
161
|
+
module: this.module,
|
|
162
|
+
source: this.source,
|
|
163
|
+
program: programToJson(this.program),
|
|
164
|
+
wgsl: this.wgsl,
|
|
165
|
+
hash: this.hash
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** The program with `node` as its output (the graph's own output by default), for previews. */
|
|
169
|
+
programFor(node) {
|
|
170
|
+
if (node === void 0 || node === this.graph.output) return this.program;
|
|
171
|
+
let p = this.nodePrograms.get(node);
|
|
172
|
+
if (!p) {
|
|
173
|
+
p = compileGraph(this.graph, node);
|
|
174
|
+
this.nodePrograms.set(node, p);
|
|
175
|
+
}
|
|
176
|
+
return p;
|
|
177
|
+
}
|
|
178
|
+
/** Hot reload: takes `next`'s graph, program, and WGSL, and bumps `version`. */
|
|
179
|
+
copyFrom(next) {
|
|
180
|
+
this.source = next.source;
|
|
181
|
+
this.graph = next.graph;
|
|
182
|
+
this.program = next.program;
|
|
183
|
+
this.name = next.name;
|
|
184
|
+
this.module = next.module;
|
|
185
|
+
this.wgsl = next.wgsl;
|
|
186
|
+
this.hash = next.hash;
|
|
187
|
+
this.nodePrograms = /* @__PURE__ */ new Map();
|
|
188
|
+
this.version++;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
class NoiseGraphStore extends AssetStore {
|
|
192
|
+
constructor() {
|
|
193
|
+
super("NoiseGraph");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const NoiseGraphs = defineResource("noise/NoiseGraphs", {
|
|
197
|
+
description: "Loaded noise graphs by guid.",
|
|
198
|
+
init: () => new NoiseGraphStore()
|
|
199
|
+
});
|
|
200
|
+
const NoiseGraphAssetType = defineAssetType("NoiseGraph", {
|
|
201
|
+
store: NoiseGraphs,
|
|
202
|
+
// The kernel loads with the first graph, so every loaded graph can be sampled synchronously.
|
|
203
|
+
load: async (artifact) => {
|
|
204
|
+
await loadNoiseKernel();
|
|
205
|
+
return NoiseGraph.fromArtifact(artifact.json);
|
|
206
|
+
},
|
|
207
|
+
update: (existing, next) => existing.copyFrom(next)
|
|
208
|
+
});
|
|
209
|
+
const NoSettings = defineSchema(
|
|
210
|
+
"noise/NoSettings",
|
|
211
|
+
{},
|
|
212
|
+
{ description: "None: the graph is the file." }
|
|
213
|
+
);
|
|
214
|
+
const NoiseGraphImporter = defineImporter({
|
|
215
|
+
name: "noise",
|
|
216
|
+
version: 1,
|
|
217
|
+
extensions: [".noise.json"],
|
|
218
|
+
settings: NoSettings,
|
|
219
|
+
async import(source) {
|
|
220
|
+
let json;
|
|
221
|
+
try {
|
|
222
|
+
json = JSON.parse(source.text());
|
|
223
|
+
} catch (cause) {
|
|
224
|
+
throw new ShardError("assets/import-failed", `${source.path} isn't valid JSON`, {
|
|
225
|
+
path: source.path,
|
|
226
|
+
cause
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const names = noiseNames(source.path);
|
|
230
|
+
const graph = NoiseGraph.fromJson(json, names);
|
|
231
|
+
return {
|
|
232
|
+
assets: [
|
|
233
|
+
{
|
|
234
|
+
label: "",
|
|
235
|
+
type: "NoiseGraph",
|
|
236
|
+
json: graph.toArtifact(),
|
|
237
|
+
info: {
|
|
238
|
+
nodes: graph.graph.nodes.size,
|
|
239
|
+
instructions: graph.program.instructions,
|
|
240
|
+
octaves: graph.program.terms.skew.length,
|
|
241
|
+
module: graph.module,
|
|
242
|
+
function: graph.fn
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
]
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
const noiseGraphSchema = defineAssetSchema("noise.schema.json", noiseJsonSchema);
|
|
250
|
+
export {
|
|
251
|
+
NoiseGraph,
|
|
252
|
+
NoiseGraphAssetType,
|
|
253
|
+
NoiseGraphImporter,
|
|
254
|
+
NoiseGraphStore,
|
|
255
|
+
NoiseGraphs,
|
|
256
|
+
noiseGraphSchema,
|
|
257
|
+
noiseNames
|
|
258
|
+
};
|