onejs-sl 0.1.0 → 0.1.2

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,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
+ }