onejs-sl 0.1.1 → 0.1.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.3
4
+
5
+ `onejs-sl/tables` names every parameter, so an editor can offer `lerp(x, y, s)` rather than an argument count. Nothing a program compiles to changes.
6
+
7
+ - `BUILTIN_PARAMS` names every builtin's parameters
8
+ - `SL_SDF_PARAM_NAMES` names every shape's parameters
9
+ - `LIB_SIGNATURES` gives every library function's parameter names and types
10
+
11
+ ## 0.1.2
12
+
13
+ The helper library has one source, `lib/*.hlsl`, translated at build time into GLSL ES, WGSL and the shared subset and copied into OneJS as its `.cginc` files. The web draws exactly what it drew; the Unity shader's text changes where Metal needed it to, and draws the same.
14
+
15
+ - `emitLibrary` prints the library functions a body calls, in the shared subset
16
+ - `emitBody` prints no scalar swizzles, and types every literal and scalar it passes to an intrinsic
17
+ - `atan2` of vectors is refused; no web backend compiled it
18
+ - `onejs-sl/goldens.json` exports the goldens
19
+ - The tests compile the shared subset as Metal on a Mac with Xcode
20
+
3
21
  ## 0.1.1
4
22
 
5
23
  A host can now supply its own frame for a program's body and get goldens for the pictures it should draw. Nothing an existing program draws changes, and OneJS's generated Unity shader is byte for byte what 0.1.0 printed.
package/README.md CHANGED
@@ -37,12 +37,13 @@ only what it calls.
37
37
  |---|---|
38
38
  | `onejs-sl` | `parse` and `analyze`, the TypeScript form `sl`, the IR types, `SL_IR_VERSION`, `toJSON`/`fromJSON`, `SLParseError` (file, line, column, length) |
39
39
  | `onejs-sl/core` | the same without the parser: what a game needs at run time |
40
- | `onejs-sl/tables` | `BUILTINS`, `SL_HLSL`, `INPUTS`, `SL_SDF_SHAPES`, `SL_SDF_PARAMS`: what completion and highlighting read |
40
+ | `onejs-sl/tables` | `BUILTINS`, `SL_HLSL`, `INPUTS`, `SL_SDF_SHAPES`, `SL_SDF_PARAMS`: what completion and highlighting read. `BUILTIN_PARAMS`, `SL_SDF_PARAM_NAMES` and `LIB_SIGNATURES` name every parameter, so an editor can show `lerp(x, y, s)` |
41
41
  | `onejs-sl/limits` | `vmFit(program)`: whether the VM runs it, and why not, without encoding |
42
42
  | `onejs-sl/vm` | `encode`, `SL_WIRE_VERSION` |
43
- | `onejs-sl/emit/hlsl-body` | `emitBody`: a program as a function body for a host's own frame |
43
+ | `onejs-sl/emit/hlsl-body` | `emitBody`: a program as a function body for a host's own frame; `emitLibrary`: the library functions it calls |
44
44
  | `onejs-sl/emit/unity` | `emitShader`: the `.shader` a Unity editor generates, a frame over `emitBody` |
45
45
  | `onejs-sl/emit/web` | `emitWGSL`, `emitGLSL`: OneJS's web frame |
46
+ | `onejs-sl/goldens.json` | the goldens (below), as data |
46
47
 
47
48
  `src/entries.test.ts` pins every name, since removing one breaks a host.
48
49
 
@@ -50,16 +51,25 @@ only what it calls.
50
51
 
51
52
  `emitBody(program, target)` prints the body only: one local per node, in the
52
53
  HLSL and Metal shared subset (HLSL spelling, `fmod`, no `mul`, no `static`, no
53
- derivatives). The `BodyTarget` says what differs between hosts: an expression
54
+ derivatives, no swizzle of a scalar, and every literal and scalar in an
55
+ intrinsic at its exact type, since Metal overloads where HLSL converts). The `BodyTarget` says what differs between hosts: an expression
54
56
  for each input, the float4 holding a uniform slot, a texture sample, whether
55
57
  `toLinear` is real (`colour: "linear"`) or the identity (`"gamma"`), and
56
58
  optionally a local to assign the result to. It returns the uniform and texture
57
59
  slots the body uses and the library functions it calls. OneJS's Unity shader is
58
60
  one frame over it (`hlsl.ts`); Magerie's compute kernel is another, and its
59
61
  target is in `src/body.test.ts` so an opcode cannot change without the text
60
- Magerie compiles changing in front of a test. The library functions' own text
61
- arrives with the translator (`Specs/SL_PACKAGE.md` section 5); until then a host
62
- takes them from OneJS's `SLCommon.cginc`, `Noise2D.cginc` and `SDF2D.cginc`.
62
+ Magerie compiles changing in front of a test.
63
+
64
+ `emitLibrary(body.uses.helpers, colour)` prints the library functions the body
65
+ calls, and everything they call, in the same subset and dependency order, with
66
+ the colour branch asked for: the text a host puts ahead of its frame. It is a
67
+ separate call because OneJS's own frame never needs it (the Unity shader
68
+ includes `SLCommon.cginc`), and a bundle that prints only Unity shaders should
69
+ not carry the library too. On a Mac with Xcode, `src/metal.test.ts` compiles
70
+ the whole library and a body for every corpus program and every builtin at
71
+ every width as Metal, behind the two defines a host's compatibility header
72
+ supplies (`frac`, `lerp`).
63
73
 
64
74
  A uniform declared with a hex default (`uniform float4 tint = #ff8040;`), or
65
75
  with `sl.uniform.colour`, is marked `colour: true`: its value is sRGB as
@@ -67,6 +77,44 @@ written, which is what a colour picker shows, and its reads convert. `inputsUsed
67
77
  says which inputs the result depends on, dead nodes aside, so a host knows
68
78
  whether a program animates.
69
79
 
80
+ ## The helper library: one source
81
+
82
+ Value and simplex noise, the octave kinds, voronoi, hsv2rgb, `toLinear` and the
83
+ 42 distance shapes are written once, in HLSL, in `lib/sdf2d.hlsl`,
84
+ `lib/noise2d.hlsl` and `lib/common.hlsl`. Everything else is printed from them
85
+ by `npm run lib`, and nobody edits the copies:
86
+
87
+ - **OneJS's `.cginc` files** (`SDF2D`, `Noise2D`, `SLCommon` in
88
+ `Resources/OneJS/`) are the source with an include guard and a generated
89
+ header around it. The VM, the generated Unity shaders and `fx` include them.
90
+ `npm run lib` writes them when the package sits in the OneJS container.
91
+ - **`src/lib/`**: `table.ts` (every function, its signature, what it calls, and
92
+ the shape table read from `sl_sdfDistance`'s switch), and the text of each
93
+ function as GLSL ES (`glsl.ts`), WGSL (`wgsl.ts`) and the shared subset
94
+ (`hlsl.ts`). The web emitters and `emitLibrary` take only what a program calls.
95
+ `names.ts` holds each function's parameter names, which only
96
+ `onejs-sl/tables` reads.
97
+
98
+ `lib/translate.ts` does the printing, at build time only. It reads a subset of
99
+ HLSL, listed at its top (functions, locals, `if`, bounded `for`, `switch`,
100
+ intrinsics, `mul(v, float2x2(...))`, and one preprocessor switch,
101
+ `UNITY_COLORSPACE_GAMMA`, which becomes the target's colour), and anything
102
+ outside it is an error with a file and line rather than a guess. HLSL converts
103
+ where GLSL, WGSL and Metal refuse, so it makes every conversion explicit first;
104
+ the printers then differ in spelling, plus what WGSL lacks (overloading,
105
+ ternaries, swizzle assignment, assignable parameters). On the web the colour
106
+ switch stays a runtime branch on the frame's `opt.x`, since a web build does not
107
+ know the project's colour space when it prints.
108
+
109
+ `src/lib/lib.test.ts` regenerates everything and fails on any byte of
110
+ difference, the `.cginc` copies included in the container, and OneJS's
111
+ `SLSharedLibraryTests` checks them from the other side. What proves the
112
+ translations draw the same picture: the goldens compile the whole library on
113
+ WebGPU and WebGL2 and draw every fixture; OneJS's parity harness holds the web
114
+ output to the VM; `SLSharedLibraryTests` draws every corpus program in Unity
115
+ through `SLCommon.cginc` and through the translated shared subset and requires
116
+ them to agree; and the Metal check above compiles the shared subset.
117
+
70
118
  ## Goldens
71
119
 
72
120
  `goldens/goldens.json` is every corpus fixture drawn as a Linear OneJS game
@@ -81,8 +129,10 @@ profile (set `CHROME` to choose one). The two backends must agree within 1/255
81
129
  over every pixel, and three anchors must match arithmetic, not each other:
82
130
  `orient.sl` (orientation, and the linear to sRGB store), `hex.sl` (a hex colour
83
131
  stores as written) and `texture.sl` (a texture's orientation and sRGB decode).
84
- The file describes the sampling grid, the times and the texture every sampled
85
- slot gets. No CI runner here has a GPU, so `src/goldens.test.ts` checks instead
132
+ The whole translated library must also compile on both backends, including
133
+ the functions no fixture reaches. The file describes the sampling grid, the
134
+ times and the texture every sampled slot gets. A host imports it as
135
+ `onejs-sl/goldens.json`. No CI runner here has a GPU, so `src/goldens.test.ts` checks instead
86
136
  that the file still covers the corpus at today's hashes; a change that moves a
87
137
  hash fails there until the goldens are drawn again.
88
138
 
@@ -131,8 +181,8 @@ would rather write its programs out at build time.
131
181
 
132
182
  **A third and fourth backend, for the browser.** A player cannot compile a
133
183
  shader, but the page it runs in can. `web.ts` prints every program as WGSL and
134
- as GLSL ES 3.00 (`weblib.ts` holds the noise, colour and 42 distance functions
135
- ported from `SLCommon.cginc`, `Noise2D.cginc` and `SDF2D.cginc`), and OneJS's
184
+ as GLSL ES 3.00 (carrying the library functions it calls, translated from
185
+ `lib/*.hlsl`), and OneJS's
136
186
  `Plugins/WebGL/OneJSSLWeb.jslib` compiles whichever one Unity's device speaks
137
187
  and draws it into the element's target in place of the VM. A `.sl` import
138
188
  carries both strings, printed at build time; an `encode()` result has them as
@@ -294,13 +344,13 @@ linear light. `sl.ramp` mixes its stops in sRGB, which is what reads as an even
294
344
  ramp, and converts the result once through `TO_LINEAR`; `sl.color("#hex")` is
295
345
  `parseColor` plus that conversion, and `sl.toLinear` is the conversion on its
296
346
  own for a vec4 built from raw components. Both backends implement it gamma
297
- aware (`sl_toLinear` in `SLCommon.cginc`), so a Gamma project gets the value as
298
- written. Alpha is coverage and is never converted. Same rule as `fx`.
347
+ aware (`sl_toLinear` in `lib/common.hlsl`), so a Gamma project gets the value
348
+ as written. Alpha is coverage and is never converted. Same rule as `fx`.
299
349
 
300
350
  ## Noise
301
351
 
302
352
  `sl.noise`, `sl.simplex`, `sl.fbm(p, octaves, base)`, `sl.turbulence` and
303
- `sl.ridged` are the fields `fx.noise` draws, from the same `Noise2D.cginc`, so
353
+ `sl.ridged` are the fields `fx.noise` draws, from the same `lib/noise2d.hlsl`, so
304
354
  a simplex here is the simplex there. Octaves are 1 to 4. A program has no seed;
305
355
  offset the input for a different field. `sl.simplex` used to be value noise on
306
356
  a rotated lattice, and `sl.fbm` had its own value noise; both changed on
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "generatedBy": "onejs-sl goldens/run.mjs",
3
- "package": "0.1.0",
3
+ "package": "0.1.2",
4
4
  "ir": 2,
5
5
  "target": "rgba8unorm-srgb: straight alpha, 8 bit, sRGB encoded, the stored bytes of a Linear OneJS game's element",
6
6
  "size": [
@@ -0,0 +1,155 @@
1
+ // Helpers shared by EVERY shader language backend.
2
+ //
3
+ // The VM (OneJS/FxProgram.shader) includes this as SLCommon.cginc, and every
4
+ // shader generated from a program by the HLSL emitter includes it too. The web
5
+ // emitters and a host's own frame (`emitBody`) get it translated, by
6
+ // `lib/translate.ts`, from this one text. That sharing is the whole reason this
7
+ // file exists: the backends have to agree, and the fastest way to make them
8
+ // disagree is to write `noise` twice.
9
+ //
10
+ // Anything an opcode needs that is more than one expression belongs here rather
11
+ // than in either backend. If it lives in only one, the golden image comparison
12
+ // fails on every program that touches it, and the failure looks like a bug in
13
+ // the program.
14
+
15
+ // Noise is the same code fx draws with (noise2d.hlsl), so `sl.simplex` and
16
+ // `fx.noise({ type: "simplex" })` mean one thing, and turbulence and ridged
17
+ // exist here for the same reason they exist there. A program has no seed of
18
+ // its own: offset the input to get a different field. The four kinds and
19
+ // their numbers are the contract with `onejsFbmKind`: 0 value fBm, 1 simplex
20
+ // fBm, 2 turbulence, 3 ridged, with the classic lacunarity 2 and gain 0.5.
21
+ // Voronoi keeps its own hash: the fx side has no voronoi to share yet.
22
+ float sl_hash21(float2 p)
23
+ {
24
+ p = frac(p * float2(123.34, 456.21));
25
+ p += dot(p, p + 45.32);
26
+ return frac(p.x * p.y);
27
+ }
28
+
29
+ float sl_valueNoise(float2 p) { return onejsVNoise(p, 0.0); }
30
+ float sl_simplex(float2 p) { return onejsSimplex(p, 0.0); }
31
+ float sl_fbm(float2 p, int octaves, int kind)
32
+ {
33
+ return onejsFbmKind(kind, p, 0.0, clamp(octaves, 1, 4), 2.0, 0.5);
34
+ }
35
+
36
+ float3 sl_hsv2rgb(float3 c)
37
+ {
38
+ float4 K = float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
39
+ float3 p = abs(frac(c.xxx + K.xyz) * 6.0 - K.www);
40
+ return c.z * lerp(K.xxx, saturate(p - K.xxx), c.y);
41
+ }
42
+
43
+ /**
44
+ * Signed distance to a shape, positive outside.
45
+ *
46
+ * The id table is SDF_SHAPES in onejs-unity/src/fx/sdf.ts, shared with the fx
47
+ * pipeline so a shape means the same thing in both. A contract test compares
48
+ * them, because this dispatcher is a second copy of the one in FxSources and a
49
+ * copy is exactly the thing that drifts.
50
+ *
51
+ * The shader language passes four parameters where fx passes six. Position and
52
+ * rotation are deliberately absent: a program transforms the point before
53
+ * calling this, which is both how signed distance code is normally written and
54
+ * more composable than baking a transform into every shape.
55
+ */
56
+ float sl_sdfDistance(int id, float2 p, float4 a, float2 b)
57
+ {
58
+ switch (id)
59
+ {
60
+ case 0: return sdCircle(p, a.x);
61
+ case 1: return sdRoundedBox(p, a.xy, float4(a.z, a.w, b.x, b.y));
62
+ case 2: return sdBox(p, a.xy);
63
+ case 3: return sdOrientedBox(p, a.xy, a.zw, b.x);
64
+ case 4: return sdSegment(p, a.xy, a.zw);
65
+ case 5: return sdRhombus(p, a.xy);
66
+ case 6: return sdTrapezoid(p, a.x, a.y, a.z);
67
+ case 7: return sdParallelogram(p, a.x, a.y, a.z);
68
+ case 8: return sdEquilateralTriangle(p, a.x);
69
+ case 9: return sdTriangleIsosceles(p, a.xy);
70
+ case 10: return sdTriangle(p, a.xy, a.zw, b.xy);
71
+ case 11: return sdUnevenCapsule(p, a.x, a.y, a.z);
72
+ case 12: return sdPentagon(p, a.x);
73
+ case 13: return sdHexagon(p, a.x);
74
+ case 14: return sdOctogon(p, a.x);
75
+ case 15: return sdHexagram(p, a.x);
76
+ case 16: return sdStar5(p, a.x, a.y);
77
+ case 17: return sdStar(p, a.x, (int)a.y, a.z);
78
+ case 18: return sdPie(p, a.xy, a.z);
79
+ case 19: return sdCutDisk(p, a.x, a.y);
80
+ case 20: return sdArc(p, a.xy, a.z, a.w);
81
+ case 21: return sdRing(p, a.xy, a.z, a.w);
82
+ case 22: return sdHorseshoe(p, a.xy, a.z, float2(a.w, b.x));
83
+ case 23: return sdVesica(p, a.x, a.y);
84
+ case 24: return sdOrientedVesica(p, a.xy, a.zw, b.x);
85
+ case 25: return sdMoon(p, a.x, a.y, a.z);
86
+ case 26: return sdRoundedCross(p, a.x);
87
+ case 27: return sdEgg(p, a.x, a.y, a.z, a.w);
88
+ case 28: return sdHeart(p);
89
+ case 29: return sdCross(p, a.xy, a.z);
90
+ case 30: return sdRoundedX(p, a.x, a.y);
91
+ case 31: return sdEllipse(p, a.xy);
92
+ case 32: return sdParabola(p, a.x);
93
+ case 33: return sdParabolaSegment(p, a.x, a.y);
94
+ case 34: return sdBezier(p, a.xy, a.zw, b.xy);
95
+ case 35: return sdBlobbyCross(p, a.x);
96
+ case 36: return sdTunnel(p, a.xy);
97
+ case 37: return sdStairs(p, a.xy, a.z);
98
+ case 38: return sdQuadraticCircle(p);
99
+ case 39: return sdHyberbola(p, a.x, a.y);
100
+ case 40: return sdCoolS(p);
101
+ case 41: return sdCircleWave(p, a.x, a.y);
102
+ default: return 1e6;
103
+ }
104
+ }
105
+
106
+ float sl_voronoi(float2 p)
107
+ {
108
+ // Distance to the nearest of a jittered lattice of points. Written here
109
+ // rather than in either backend, for the same reason as the noise above.
110
+ float2 cell = floor(p);
111
+ float2 f = frac(p);
112
+ float best = 8.0;
113
+ [unroll]
114
+ for (int y = -1; y <= 1; y++)
115
+ {
116
+ [unroll]
117
+ for (int x = -1; x <= 1; x++)
118
+ {
119
+ float2 o = float2(x, y);
120
+ float2 jitter = float2(sl_hash21(cell + o), sl_hash21(cell + o + 37.7));
121
+ best = min(best, length(o + jitter - f));
122
+ }
123
+ }
124
+ return best;
125
+ }
126
+
127
+ float sl_luminance(float3 c) { return dot(c, float3(0.2126, 0.7152, 0.0722)); }
128
+
129
+ // A colour as written (sRGB, like CSS) to the working space the target holds.
130
+ // Fx targets are linear float textures that a Linear colour space project
131
+ // encodes on display, so a stop stored as written drew lighter than its swatch.
132
+ // In a Gamma project the written value is already right. The float4 form keeps
133
+ // alpha, which is coverage rather than light. Mirrors fxColorToWorking.
134
+ //
135
+ // Every width uses Unity's cubic approximation, lane by lane. The VM has one
136
+ // TO_LINEAR for all widths (its registers are float4, so it converts .rgb),
137
+ // and a scalar or float2 on the exact curve here drew up to 5/255 apart from
138
+ // it in the dark range: the editor and a player disagreed.
139
+ //
140
+ // The curve is Unity's GammaToLinearSpace, written out here so that a target
141
+ // without UnityCG.cginc has it too. The switch below is the one piece of
142
+ // preprocessor the translator reads: it becomes the target's colour setting.
143
+ float3 sl_gammaToLinear(float3 c) { return c * (c * (c * 0.305306011 + 0.682171111) + 0.012522878); }
144
+
145
+ #ifdef UNITY_COLORSPACE_GAMMA
146
+ float sl_toLinear(float c) { return c; }
147
+ float2 sl_toLinear(float2 c) { return c; }
148
+ float3 sl_toLinear(float3 c) { return c; }
149
+ float4 sl_toLinear(float4 c) { return c; }
150
+ #else
151
+ float sl_toLinear(float c) { return sl_gammaToLinear(float3(c, 0, 0)).x; }
152
+ float2 sl_toLinear(float2 c) { return sl_gammaToLinear(float3(c, 0)).xy; }
153
+ float3 sl_toLinear(float3 c) { return sl_gammaToLinear(c); }
154
+ float4 sl_toLinear(float4 c) { return float4(sl_gammaToLinear(c.rgb), c.a); }
155
+ #endif
@@ -0,0 +1,167 @@
1
+ // Scrolling fBm value noise, shared by OneJS/TextureFX and OneJS/FxSources.
2
+ //
3
+ // Computed from a seed rather than sampled, so an effect needs no texture, any
4
+ // seed works without shipping art for it, and the field is infinite: scrolling
5
+ // never repeats and never has to tile.
6
+ //
7
+ // This lives in its own include because two shaders want it. Keeping a second
8
+ // copy in each would let them drift, and a noise field that differs by shader
9
+ // is the kind of thing nobody notices until two effects that should match do
10
+ // not.
11
+
12
+ float onejsHash21(float2 p, float seed)
13
+ {
14
+ p = frac(p * float2(123.34, 456.21) + seed * 0.1731);
15
+ p += dot(p, p + 45.32);
16
+ return frac(p.x * p.y);
17
+ }
18
+
19
+ float onejsVNoise(float2 p, float seed)
20
+ {
21
+ float2 i = floor(p), f = frac(p);
22
+ f = f * f * (3.0 - 2.0 * f);
23
+ float a = onejsHash21(i, seed);
24
+ float b = onejsHash21(i + float2(1, 0), seed);
25
+ float c = onejsHash21(i + float2(0, 1), seed);
26
+ float d = onejsHash21(i + float2(1, 1), seed);
27
+ return lerp(lerp(a, b, f.x), lerp(c, d, f.x), f.y);
28
+ }
29
+
30
+ // Octaves are capped at 4 and the loop is unrolled to that, because a dynamic
31
+ // trip count here costs more than the octaves it saves.
32
+ /// lacunarity is how much finer each octave gets, gain how much quieter.
33
+ /// The classic pair is 2 and 0.5; pushing lacunarity up and gain toward 1 gives
34
+ /// the stringy, turbulent look that reads as fire or smoke rather than cloud.
35
+ float onejsFbm(float2 p, float seed, int octaves, float lacunarity, float gain)
36
+ {
37
+ float sum = 0, amp = 0.5, norm = 0;
38
+ [unroll(4)]
39
+ for (int o = 0; o < 4; o++)
40
+ {
41
+ if (o >= octaves) break;
42
+ sum += onejsVNoise(p, seed + o * 19.0) * amp;
43
+ norm += amp;
44
+ p *= lacunarity;
45
+ amp *= gain;
46
+ }
47
+ return sum / max(norm, 1e-4);
48
+ }
49
+
50
+ /// The classic parameters, for callers that do not care.
51
+ float onejsFbm(float2 p, float seed, int octaves)
52
+ {
53
+ return onejsFbm(p, seed, octaves, 2.0, 0.5);
54
+ }
55
+
56
+ // MARK: simplex
57
+ //
58
+ // Ashima/McEwan 2D simplex. Worth carrying next to the value noise because the
59
+ // two fail differently: value noise interpolates a square grid, so at high
60
+ // octave gain its cells show through as blocks, which is exactly what spoiled
61
+ // the first pass at the fire sample. Simplex is built on triangles and has no
62
+ // axis-aligned structure to leak.
63
+
64
+ float2 onejsMod289(float2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
65
+ float3 onejsMod289(float3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
66
+ float3 onejsPermute(float3 x) { return onejsMod289(((x * 34.0) + 1.0) * x); }
67
+
68
+ /// Raw simplex, roughly -1..1.
69
+ float onejsSimplexRaw(float2 v)
70
+ {
71
+ const float4 C = float4(0.211324865405187, 0.366025403784439,
72
+ -0.577350269189626, 0.024390243902439);
73
+ float2 i = floor(v + dot(v, C.yy));
74
+ float2 x0 = v - i + dot(i, C.xx);
75
+ float2 i1 = (x0.x > x0.y) ? float2(1.0, 0.0) : float2(0.0, 1.0);
76
+ float4 x12 = x0.xyxy + C.xxzz;
77
+ x12.xy -= i1;
78
+ i = onejsMod289(i);
79
+ float3 p = onejsPermute(onejsPermute(i.y + float3(0.0, i1.y, 1.0))
80
+ + i.x + float3(0.0, i1.x, 1.0));
81
+ float3 m = max(0.5 - float3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0);
82
+ m = m * m; m = m * m;
83
+ float3 x = 2.0 * frac(p * C.www) - 1.0;
84
+ float3 h = abs(x) - 0.5;
85
+ float3 ox = floor(x + 0.5);
86
+ float3 a0 = x - ox;
87
+ m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h);
88
+ float3 g;
89
+ g.x = a0.x * x0.x + h.x * x0.y;
90
+ g.yz = a0.yz * x12.xz + h.yz * x12.yw;
91
+ return 130.0 * dot(m, g);
92
+ }
93
+
94
+ /// 0..1, and seeded by displacing the input: simplex has no seed of its own.
95
+ float onejsSimplex(float2 p, float seed)
96
+ {
97
+ return onejsSimplexRaw(p + seed * 137.13) * 0.5 + 0.5;
98
+ }
99
+
100
+ float onejsFbmSimplex(float2 p, float seed, int octaves, float lacunarity, float gain)
101
+ {
102
+ float sum = 0, amp = 0.5, norm = 0;
103
+ [unroll(4)]
104
+ for (int o = 0; o < 4; o++)
105
+ {
106
+ if (o >= octaves) break;
107
+ sum += onejsSimplex(p, seed + o * 19.0) * amp;
108
+ norm += amp;
109
+ p *= lacunarity;
110
+ amp *= gain;
111
+ }
112
+ return sum / max(norm, 1e-4);
113
+ }
114
+
115
+ // MARK: turbulence and ridged
116
+ //
117
+ // fBm above sums signed octaves, so octaves cancel as often as they add and the
118
+ // field reads as cloud. Turbulence sums the ABSOLUTE value of each octave: the
119
+ // zero crossings become creases, and the creases of every octave stack into
120
+ // the veins and licks that read as fire, smoke and marble. Ridged inverts the
121
+ // same crease so it is the bright line rather than the dark one, and squares
122
+ // it so the ridges stay sharp under the finer octaves. Both are built on
123
+ // simplex; a value noise crease follows the grid and shows as blocks.
124
+
125
+ /// 0..1. Perlin's turbulence: sum over octaves of |signed simplex|.
126
+ float onejsTurbulence(float2 p, float seed, int octaves, float lacunarity, float gain)
127
+ {
128
+ float sum = 0, amp = 0.5, norm = 0;
129
+ [unroll(4)]
130
+ for (int o = 0; o < 4; o++)
131
+ {
132
+ if (o >= octaves) break;
133
+ sum += abs(onejsSimplexRaw(p + (seed + o * 19.0) * 137.13)) * amp;
134
+ norm += amp;
135
+ p *= lacunarity;
136
+ amp *= gain;
137
+ }
138
+ return sum / max(norm, 1e-4);
139
+ }
140
+
141
+ /// 0..1. Musgrave's ridged multifractal, without the feedback term: the
142
+ /// crease of |signed simplex| turned into a bright ridge and squared.
143
+ float onejsRidged(float2 p, float seed, int octaves, float lacunarity, float gain)
144
+ {
145
+ float sum = 0, amp = 0.5, norm = 0;
146
+ [unroll(4)]
147
+ for (int o = 0; o < 4; o++)
148
+ {
149
+ if (o >= octaves) break;
150
+ float r = 1.0 - abs(onejsSimplexRaw(p + (seed + o * 19.0) * 137.13));
151
+ sum += r * r * amp;
152
+ norm += amp;
153
+ p *= lacunarity;
154
+ amp *= gain;
155
+ }
156
+ return sum / max(norm, 1e-4);
157
+ }
158
+
159
+ /// Dispatches on the noise kind: 0 value fBm, 1 simplex fBm, 2 turbulence,
160
+ /// 3 ridged. The numbers are the contract with onejs-unity/src/fx/image.ts.
161
+ float onejsFbmKind(int kind, float2 p, float seed, int octaves, float lacunarity, float gain)
162
+ {
163
+ if (kind == 2) return onejsTurbulence(p, seed, octaves, lacunarity, gain);
164
+ if (kind == 3) return onejsRidged(p, seed, octaves, lacunarity, gain);
165
+ return kind == 1 ? onejsFbmSimplex(p, seed, octaves, lacunarity, gain)
166
+ : onejsFbm(p, seed, octaves, lacunarity, gain);
167
+ }