onejs-sl 0.9.1 → 0.9.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,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.3
4
+
5
+ A hex colour is a float3 wherever a float3 is wanted, and `lookRay(eye, target, lens)` gives the ray through this pixel from a camera. Every program that compiled before keeps its hash.
6
+
7
+ - `lookRay(eye, target, lens)` returns the ray through this pixel from a y up camera at `eye` looking at `target`, as Unity's camera casts it; `lens` is `0.5 / tan(fov / 2)`
8
+ - A hex colour is a float3 wherever a float3 is wanted: returned, passed, in `float3(...)`, or beside a float3 in an op, a choice or a builtin
9
+ - A program that compiled before keeps its hash
10
+
11
+ ## 0.9.2
12
+
13
+ A fractional `tex2Dlod` level now weighs each level's colour by its alpha, so a transparent level no longer darkens the other. Every program keeps its hash; a host's generated shaders take the new text on their next build.
14
+
15
+ - A fractional `tex2Dlod` level blends the two levels premultiplied, so a transparent level fades the other out instead of pulling it toward black
16
+ - Opaque textures draw as they did, and so does colour under zero alpha
17
+ - Every program keeps its hash; one with a fractional `tex2Dlod` compiles to new shader text, which a host's generated shaders pick up on their next build
18
+ - The goldens anchor `lod-alpha.sl` to arithmetic, through a texture with alpha bound to a slot declared `edge`
19
+
3
20
  ## 0.9.1
4
21
 
5
22
  `?:` now takes one side instead of blending the two, so a huge, infinite or NaN value on the side not taken no longer spoils the result. Every program keeps its hash; a host's generated shaders take the new text on their next build.
package/README.md CHANGED
@@ -73,7 +73,10 @@ filtering and wrapping left to the host, which OneJS's hosts take from the
73
73
  bound texture's own settings. Between mip levels the body decides instead: a
74
74
  fractional `tex2Dlod` level blends the two levels either side by the fraction,
75
75
  whatever the texture's filter mode, since the body asks for both whole levels
76
- and blends them itself. A level it can prove whole (a whole constant, a
76
+ and blends them itself. The blend weighs each level's colour by its alpha, as
77
+ trilinear filtering of premultiplied texels does, so a level that reads
78
+ transparent fades the other out instead of darkening it; where both levels are
79
+ opaque, or both transparent, it is the plain blend. A level it can prove whole (a whole constant, a
77
80
  `floor`, an int) costs one sample, and a level past the smallest reads the
78
81
  smallest. A texture whose program sets its own sampler (`[Wrap(Repeat)]`,
79
82
  `[Filter(Point)]`) goes to `sampleWith` instead, with both halves filled in,
@@ -162,7 +165,10 @@ over every pixel, and the anchors must match arithmetic, not each other:
162
165
  stores as written), `texture.sl` (a texture's orientation and sRGB decode),
163
166
  `lod.sl` (each mip level `tex2Dlod` reads, the texture's smaller levels being
164
167
  solid colours), `lod-frac.sl` (a fractional level blends the two levels
165
- either side, on a texture sampled nearest between levels), `wrap-repeat.sl`
168
+ either side, on a texture sampled nearest between levels), `lod-alpha.sl` (the
169
+ same blend weighs each level's colour by its alpha, through a second texture,
170
+ bound to a slot declared `edge`, whose levels are transparent, opaque and half
171
+ transparent), `wrap-repeat.sl`
166
172
  and `wrap-mirror.sl` (a program's own wrap over a texture the host binds
167
173
  clamped, the filter's footprint wrapping with it), `filter-point.sl` (its own
168
174
  point filter, to the byte, through `tex2D` and `tex2Dlod`) and `fetch.sl`
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "generatedBy": "onejs-sl goldens/run.mjs",
3
- "package": "0.9.1",
3
+ "package": "0.9.3",
4
4
  "ir": 6,
5
5
  "target": "rgba8unorm-srgb: straight alpha, 8 bit, sRGB encoded, the stored bytes of a Linear OneJS game's element",
6
6
  "size": [
@@ -19,7 +19,7 @@
19
19
  ],
20
20
  "historyExact": "a stepped fixture marked historyExact has to hold its history samples to the bit",
21
21
  "samples": "a 16 x 16 grid at x = 2 + 4i, y = 2 + 4j, rows from the top, i fastest, RGBA each",
22
- "texture": "every sampled slot: 8 x 8, rgba8 sRGB, linear filter, clamp to edge; texel (x, y) with y from the top = (32x + 16, 32y + 16, (x + y) even ? 200 : 40, 255); uv (0, 0) is the image's bottom left. Mip levels 1 to 3 (4 x 4, 2 x 2, 1 x 1) are each one colour, (224, 64, 32, 255), (32, 224, 64, 255), (64, 32, 224, 255), sampled nearest between levels",
22
+ "texture": "every sampled slot: 8 x 8, rgba8 sRGB, linear filter, clamp to edge; texel (x, y) with y from the top = (32x + 16, 32y + 16, (x + y) even ? 200 : 40, 255); uv (0, 0) is the image's bottom left. Mip levels 1 to 3 (4 x 4, 2 x 2, 1 x 1) are each one colour, (224, 64, 32, 255), (32, 224, 64, 255), (64, 32, 224, 255), sampled nearest between levels. A slot declared edge gets instead an 8 x 8 sampled the same way whose levels 0 to 3 are each one colour with straight alpha, (0, 0, 0, 0), (224, 64, 32, 255), (32, 224, 64, 128), (255, 255, 255, 0)",
23
23
  "uniforms": "each program's declared defaults; a colour uniform's default is sRGB as written",
24
24
  "drawnOn": {
25
25
  "webgpu": "apple metal-3",
@@ -215,6 +215,14 @@
215
215
  "1.25": [0,255,0,255,0,255,0,255,255,0,0,255,255,0,0,255,0,0,0,255,0,0,0,255,255,255,0,255,255,255,0,255,0,0,0,255,0,0,0,255,255,255,0,255,255,255,0,255,0,0,0,255,0,0,0,255,255,0,0,255,255,0,0,255,0,255,0,255,0,255,0,255,255,0,0,255,255,0,0,255,0,0,0,255,0,0,0,255,255,255,0,255,255,255,0,255,0,0,0,255,0,0,0,255,255,255,0,255,255,255,0,255,0,0,0,255,0,0,0,255,255,0,0,255,255,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,255,255,0,255,255,255,0,255,255,0,0,255,255,0,0,255,0,255,0,255,0,255,0,255,255,255,0,255,255,255,0,255,255,0,0,255,255,0,0,255,255,0,0,255,255,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,255,255,0,255,255,255,0,255,255,0,0,255,255,0,0,255,0,255,0,255,0,255,0,255,255,255,0,255,255,255,0,255,255,0,0,255,255,0,0,255,255,0,0,255,255,0,0,255,255,255,255,255,255,255,255,255,255,255,0,255,255,255,0,255,0,255,0,255,0,255,0,255,255,255,0,255,255,255,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,255,0,255,0,255,0,255,255,255,0,255,255,255,0,255,255,255,255,255,255,255,255,255,255,255,0,255,255,255,0,255,0,255,0,255,0,255,0,255,255,255,0,255,255,255,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,255,0,255,0,255,0,255,255,255,0,255,255,255,0,255,255,0,0,255,255,0,0,255,255,0,0,255,255,0,0,255,255,255,255,255,255,255,255,255,255,0,255,255,255,0,255,255,255,0,0,255,255,0,0,255,255,0,255,255,255,0,255,255,255,255,0,255,255,255,0,255,255,0,0,255,255,0,0,255,255,0,0,255,255,0,0,255,255,0,0,255,255,0,0,255,255,255,255,255,255,255,255,255,255,0,255,255,255,0,255,255,255,0,0,255,255,0,0,255,255,0,255,255,255,0,255,255,255,255,0,255,255,255,0,255,255,0,0,255,255,0,0,255,255,0,255,255,255,0,255,255,255,0,255,255,255,0,255,255,255,0,255,255,255,0,255,255,255,255,255,255,255,255,255,255,255,0,255,255,255,0,255,255,255,0,255,255,255,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,255,255,255,0,255,255,255,0,255,255,255,0,255,255,255,0,255,255,255,0,255,255,255,255,255,255,255,255,255,255,255,0,255,255,255,0,255,255,255,0,255,255,255,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,255,255,0,0,255,255,255,0,255,255,255,0,255,255,0,255,255,255,0,255,255,255,0,255,255,255,0,255,255,255,0,255,255,255,0,255,255,255,255,255,255,255,255,255,255,255,0,0,255,255,0,0,255,255,255,255,255,255,255,255,255,255,0,0,255,255,0,0,255,255,255,0,255,255,255,0,255,255,0,255,255,255,0,255,255,255,0,255,255,255,0,255,255,255,0,255,255,255,0,255,255,255,255,255,255,255,255,255,255,255,0,0,255,255,0,0,255,255,255,255,255,255,255,255,255,255,0,255,255,255,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,255,255,255,0,255,255,0,255,255,255,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,255,255,255,0,255,255,255,0,255,255,255,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,255,255,255,0,255,255,0,255,255,255,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,255,255,255,0,255,255,255,0,255,0,255,0,255,0,255,255,255,255,255,255,255,255,255,0,255,0,255,0,255,0,255,255,0,0,255,255,0,0,255,0,0,255,255,0,0,255,255,255,255,0,255,255,255,0,255,0,255,255,255,0,255,255,255,255,255,255,255,255,255,255,255,0,255,0,255,0,255,0,255,255,255,255,255,255,255,255,255,0,255,0,255,0,255,0,255,255,0,0,255,255,0,0,255,0,0,255,255,0,0,255,255,255,255,0,255,255,255,0,255,0,255,255,255,0,255,255,255,255,255,255,255,255,255,255,255]
216
216
  }
217
217
  },
218
+ "lod-alpha.sl": {
219
+ "hash": "aaf5be77",
220
+ "source": "// An anchor for a fractional tex2Dlod level on a texture with alpha: the level\n// runs from 0 at the left edge to 3.5 at the right, through the goldens' edge\n// texture, whose levels are transparent black, opaque, half transparent and\n// transparent white. A fraction weighs each level's colour by its alpha, so a\n// transparent level fades the other out instead of pulling it toward the\n// colour it stores.\ntexture2D edge;\nfloat4 main() { return tex2Dlod(edge, uv, uv.x * 3.5); }\n",
221
+ "samples": {
222
+ "0": [224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0],
223
+ "1.25": [224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0,224,64,32,35,224,64,32,91,224,64,32,146,224,64,32,202,223,67,32,254,211,105,38,226,193,138,44,198,165,170,51,170,112,204,59,142,32,224,64,115,32,224,64,87,32,224,64,59,32,224,64,31,32,224,64,3,255,255,255,0,255,255,255,0]
224
+ }
225
+ },
218
226
  "lod-frac.sl": {
219
227
  "hash": "fd3f2852",
220
228
  "source": "// An anchor for a fractional tex2Dlod level: the level runs from 0 at the left\n// edge to 4.5 at the right. A fraction blends the level below and the level\n// above by that fraction, whatever the texture's own filter between levels,\n// and past level 3, the smallest, both are level 3.\ntexture2D t;\nfloat4 main() { return tex2Dlod(t, uv, uv.x * 4.5); }\n",
package/lib/noise2d.hlsl CHANGED
@@ -9,7 +9,7 @@
9
9
  // is the kind of thing nobody notices until two effects that should match do
10
10
  // not.
11
11
 
12
- // MARK: hash
12
+ // hash
13
13
  //
14
14
  // The lattice hash is integer arithmetic, and has to stay integer arithmetic.
15
15
  // A float hash, frac of a large product as most shader noise is written, is
@@ -108,7 +108,7 @@ float onejsFbm(float2 p, float seed, int octaves)
108
108
  return onejsFbm(p, seed, octaves, 2.0, 0.5);
109
109
  }
110
110
 
111
- // MARK: simplex
111
+ // simplex
112
112
  //
113
113
  // Ashima/McEwan 2D simplex. Worth carrying next to the value noise because the
114
114
  // two fail differently: value noise interpolates a square grid, so at high
@@ -167,7 +167,7 @@ float onejsFbmSimplex(float2 p, float seed, int octaves, float lacunarity, float
167
167
  return sum / max(norm, 1e-4);
168
168
  }
169
169
 
170
- // MARK: turbulence and ridged
170
+ // turbulence and ridged
171
171
  //
172
172
  // fBm above sums signed octaves, so octaves cancel as often as they add and the
173
173
  // field reads as cloud. Turbulence sums the ABSOLUTE value of each octave: the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onejs-sl",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "The OneJS shader language: a small typed language for per pixel programs, compiled to a portable IR and emitted as HLSL, WGSL or GLSL ES",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
package/src/body.ts CHANGED
@@ -48,7 +48,8 @@ export interface BodyTarget {
48
48
  * do with the GPU's sampler:
49
49
  *
50
50
  * - **Straight alpha**, not premultiplied, in and out: the body never
51
- * multiplies or divides by alpha, and its result is straight too.
51
+ * multiplies or divides by alpha, except to blend two mip levels
52
+ * (`sampleLevel`), and its result is straight too.
52
53
  * - **In the space `colour` names.** With `linear`, rgb is linear light, so
53
54
  * an sRGB texture is decoded before it is filtered, as an sRGB sampler
54
55
  * does; with `gamma`, rgb is as stored.
@@ -68,7 +69,11 @@ export interface BodyTarget {
68
69
  * asks for whole levels, or for a level it cannot prove whole, floored and
69
70
  * floored plus one, which it blends by the fraction itself; so a fraction
70
71
  * draws the same whatever the texture's filter between levels, and a host
71
- * has no blending to do. A level past the smallest reads the smallest.
72
+ * has no blending to do. The blend weighs each level's colour by its alpha,
73
+ * as trilinear filtering of premultiplied texels does, and divides alpha
74
+ * back out, so a level that reads transparent adds no colour; where both
75
+ * read transparent it is the plain blend. A level past the smallest reads
76
+ * the smallest.
72
77
  * Otherwise the same contract as `sample`. `tex2Dlod` in a program; HLSL's
73
78
  * `tex2Dlod(s, float4(uv, 0, lod))` and Metal's `sample(s, uv, level(lod))`.
74
79
  */
@@ -198,6 +203,8 @@ export function emitBody(p: Program, target: BodyTarget): Body {
198
203
  const textures = new Set<number>()
199
204
  const helpers = new Set<string>()
200
205
  const helper = (fn: string) => { helpers.add(fn); return fn }
206
+ /** The statements a node prints ahead of its own line (`printBody`'s `before`). */
207
+ const ahead = new Map<NodeRef, string[]>()
201
208
  /** A node at width `w`: a scalar repeated into a constructor, since HLSL refuses float3(x) and Metal x.xxx. */
202
209
  /** A constant node's value, or null for anything computed. */
203
210
  const constantOf = (ref: number): number[] | null => {
@@ -404,10 +411,22 @@ export function emitBody(p: Program, target: BodyTarget): Body {
404
411
  // A fraction blends the level below and the level above,
405
412
  // whatever the texture's filter between levels, as the web
406
413
  // emitters do (`isWhole`). Past the smallest level both read the
407
- // smallest, as a whole level past it does.
414
+ // smallest, as a whole level past it does. The blend weighs
415
+ // each level's colour by its alpha, as trilinear filtering of
416
+ // premultiplied texels does: a level that reads transparent
417
+ // adds no colour, rather than pulling the other toward black.
418
+ // Where both are opaque it is the plain lerp, and where both
419
+ // are transparent too, so colour under zero alpha survives.
408
420
  if (isWhole(p.nodes, n.args[1]!)) return level(slot, a[0]!, a[1]!)
409
421
  const below = `floor(${a[1]})`
410
- return `lerp(${level(slot, a[0]!, below)}, ${level(slot, a[0]!, `${below} + 1.0`)}, frac(${a[1]}))`
422
+ const [lo, hi, mixed] = ["lo", "hi", "pm"].map((s) => `${name(ref)}_${s}`)
423
+ const pm = (v: string) => `float4(${v}.rgb * ${v}.a, ${v}.a)`
424
+ ahead.set(ref, [
425
+ `float4 ${lo} = ${level(slot, a[0]!, below)};`,
426
+ `float4 ${hi} = ${level(slot, a[0]!, `${below} + 1.0`)};`,
427
+ `float4 ${mixed} = lerp(${pm(lo)}, ${pm(hi)}, frac(${a[1]}));`,
428
+ ])
429
+ return `float4(${mixed}.a > 0.0 ? ${mixed}.rgb / ${mixed}.a : lerp(${lo}.rgb, ${hi}.rgb, frac(${a[1]})), ${mixed}.a)`
411
430
  }
412
431
  case SLOP.TEXTURE_SIZE: {
413
432
  const slot = Math.round(imm[0] ?? 0)
@@ -449,7 +468,7 @@ export function emitBody(p: Program, target: BodyTarget): Body {
449
468
  loopOpen: `${target.loopAttribute === undefined ? "" : target.loopAttribute + " "}for (;;) {`,
450
469
  breakUnless: (c, turns, max) => `if (!(${c}) || ${turns} >= ${max}) break;`,
451
470
  }
452
- const lines = printBody(p, shape, syntax, expr, indent)
471
+ const lines = printBody(p, shape, syntax, expr, indent, (ref) => ahead.get(ref) ?? [])
453
472
  lines.push(target.result === undefined
454
473
  ? `${indent}return ${name(p.result)};`
455
474
  : `${indent}${target.result} = ${name(p.result)};`)
@@ -125,6 +125,14 @@ constant 7 before the IR sees it. `sin(0.5)` is two instructions, because
125
125
  folding it would mean a second implementation of `sin` in JavaScript, and a
126
126
  second implementation is somewhere for the two backends to disagree.
127
127
 
128
+ **A hex colour takes the width its place wants.** Alone, `#ff8040` is a
129
+ float4. Where a float3 is wanted (a float3's declaration, a float3 function's
130
+ return, a float3 parameter, `float3(#ff8040)`, the other side of an op or a
131
+ choice, or a builtin's other arguments) it is that float3, its alpha dropped.
132
+ A choice between two hexes counts as one. A computed float4 does not fit a
133
+ float3 this way: that is still an error, since its alpha may be meant
134
+ (`writtenHex` and `asWidth` in `lower.ts`; `hex-width.test.ts`).
135
+
128
136
  **A hex default makes a uniform a colour.** `uniform float4 tint = #ff8040;`
129
137
  stores the sRGB components as its default, which is what the host sets and what
130
138
  the generated shader's Properties block shows, and every read of `tint` goes
package/src/lang/check.ts CHANGED
@@ -243,7 +243,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
243
243
  }
244
244
  })
245
245
 
246
- // MARK: bodies
246
+ // bodies
247
247
 
248
248
  // The prelude's own bodies are checked once, where it is built, against a
249
249
  // unit that has no uniforms of its own. Re-checking them here would let a
@@ -492,7 +492,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
492
492
  return `this can never run: every way through the ${above} above ends in a return, break or continue`
493
493
  }
494
494
 
495
- // MARK: expressions
495
+ // expressions
496
496
 
497
497
  function checkExpr(fn: FuncDecl, e: Expr, scope: Set<string>): void {
498
498
  switch (e.k) {
package/src/lang/lower.ts CHANGED
@@ -317,7 +317,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
317
317
  })
318
318
  }
319
319
 
320
- // MARK: declarations
320
+ // declarations
321
321
 
322
322
  function declareUniform(name: string, type: TypeName, c: number[], control: UniformControl): Val {
323
323
  switch (type) {
@@ -429,7 +429,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
429
429
  }
430
430
  }
431
431
 
432
- // MARK: functions
432
+ // functions
433
433
 
434
434
  /** A function's body run to its return, in `scope`, which holds its parameters. */
435
435
  function callBody(fn: FuncDecl, scope: Scope): LV {
@@ -442,7 +442,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
442
442
  return frame.retval
443
443
  }
444
444
 
445
- // MARK: statements
445
+ // statements
446
446
 
447
447
  /**
448
448
  * Runs a body. What it does to the locals is in their bindings and what it
@@ -570,7 +570,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
570
570
  case "switch": exec([switchAsIf(s, scope)], scope, frame); break
571
571
  case "block": exec(s.body, new Scope(scope), frame); break
572
572
  case "return": {
573
- const v = lowerExpr(s.value, scope, want(frame.ret))
573
+ const v = asWidth(lowerExpr(s.value, scope, want(frame.ret)), s.value, frame.ret.width, s.pos)
574
574
  frame.retval = returned(v, frame, s.pos)
575
575
  frame.returned = true
576
576
  break
@@ -606,6 +606,25 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
606
606
  b.value = fit(v, b, name, s.pos)
607
607
  }
608
608
 
609
+ /**
610
+ * A colour written as a hex, `#ff8000`, or a choice between two. It has
611
+ * no width of its own: alone it is a float4, and where a float3 is wanted
612
+ * (a float3's declaration, return or argument, the other side of an op or
613
+ * a choice, a builtin's other arguments) it is that float3, its alpha
614
+ * dropped. Before, only a declaration dropped it, and returning one from a
615
+ * float3 function, passing one, or mixing one with a float3 was an error
616
+ * (learn Mac, 6 Nov 2026, lessons 7 and 8).
617
+ */
618
+ function writtenHex(e: Expr): boolean {
619
+ return e.k === "hex" || (e.k === "cond" && writtenHex(e.then) && writtenHex(e.else))
620
+ }
621
+
622
+ /** `v`, lowered from `e`, as the float3 a place wants when `e` is a written hex; otherwise as it is. */
623
+ function asWidth(v: LV, e: Expr, width: number, pos: Pos): LV {
624
+ if (width !== 3 || widthOf(v) !== 4 || !writtenHex(e)) return v
625
+ return at(pos, () => asVal(v).swz("xyz") as unknown as Val)
626
+ }
627
+
609
628
  /** A return's value as the function's type. Only the kind converts; a width has to match. */
610
629
  function returned(v: LV, frame: Frame, pos: Pos): LV {
611
630
  const fn = frame.fn
@@ -621,7 +640,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
621
640
  return fit(v, t, `${fn.name}'s result`, pos)
622
641
  }
623
642
 
624
- // MARK: loops
643
+ // loops
625
644
 
626
645
  function runLoop(s: Extract<Stmt, { k: "for" | "while" }>, scope: Scope, frame: Frame): void {
627
646
  const loopScope = new Scope(scope)
@@ -765,7 +784,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
765
784
  return r === undefined ? null : { min: r.min, max: r.max }
766
785
  }
767
786
 
768
- // MARK: switch
787
+ // switch
769
788
 
770
789
  /**
771
790
  * A switch as the if chain it means: `x == 1 || x == 2` per case, in order,
@@ -801,7 +820,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
801
820
  return out[0] ?? { k: "if", cond: { k: "bool", value: true, pos: s.pos }, then: [], else: [], pos: s.pos }
802
821
  }
803
822
 
804
- // MARK: conversions
823
+ // conversions
805
824
 
806
825
  /**
807
826
  * A value as a declared type, as a declaration, an assignment and a
@@ -919,7 +938,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
919
938
  return b.width === 1 ? parts[0]! : at(pos, () => compose(b.width, parts))
920
939
  }
921
940
 
922
- // MARK: expressions
941
+ // expressions
923
942
 
924
943
  /**
925
944
  * `want` is the kind the value is going into, an int or a uint, which makes
@@ -993,6 +1012,8 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
993
1012
 
994
1013
  /** A binary op on operands already lowered from `ea` and `eb`. */
995
1014
  function combine(op: BinaryOp, a: LV, b: LV, ea: Expr, eb: Expr, scope: Scope, pos: Pos): LV {
1015
+ a = asWidth(a, ea, widthOf(b), pos)
1016
+ b = asWidth(b, eb, widthOf(a), pos)
996
1017
  // A whole number beside an int is an int: `i < 10`, `i % 3`.
997
1018
  if (isIntLike(a) && typeof b === "number" && wholeExpr(eb)) b = lowerExpr(eb, scope, kindWant(a))
998
1019
  if (isIntLike(b) && typeof a === "number" && wholeExpr(ea)) a = lowerExpr(ea, scope, kindWant(b))
@@ -1134,6 +1155,8 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
1134
1155
  if (cond instanceof Test) void cond.float
1135
1156
  let t = lowerExpr(e.then, scope, want)
1136
1157
  let f = lowerExpr(e.else, scope, want)
1158
+ t = asWidth(t, e.then, widthOf(f), e.pos)
1159
+ f = asWidth(f, e.else, widthOf(t), e.pos)
1137
1160
  if (isIntLike(t) && typeof f === "number" && wholeExpr(e.else)) f = lowerExpr(e.else, scope, kindWant(t))
1138
1161
  if (isIntLike(f) && typeof t === "number" && wholeExpr(e.then)) t = lowerExpr(e.then, scope, kindWant(f))
1139
1162
  const kinded = [t, f].some((v) => typeof v === "bigint" || v instanceof Uint || typeof v === "boolean" || (v instanceof Val && v.kind !== undefined))
@@ -1163,7 +1186,11 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
1163
1186
  const n = (callee as Extract<Expr, { k: "ident" }>).name
1164
1187
 
1165
1188
  if (n === "int" || n === "uint" || n === "bool") return convert(n, e, scope)
1166
- if (n in TYPE_WIDTH) return construct(n as TypeName, floats(e.args), e.pos)
1189
+ if (n in TYPE_WIDTH) {
1190
+ // float3(#ff8000) is the colour's rgb, as a float3 declared from it is.
1191
+ const one = e.args.length === 1 ? asWidth(toFloat(lowerExpr(e.args[0]!, scope), e.args[0]!.pos), e.args[0]!, TYPE_WIDTH[n as TypeName], e.pos) : null
1192
+ return construct(n as TypeName, one !== null ? [one as number | Val] : floats(e.args), e.pos)
1193
+ }
1167
1194
 
1168
1195
  if (n === "textureSize" || n === "tex2Dfetch") {
1169
1196
  const tex = samplers.get((e.args[0] as Extract<Expr, { k: "ident" }>).name)!
@@ -1210,6 +1237,9 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
1210
1237
  const builtin = BUILTINS[n]
1211
1238
  if (builtin !== undefined) {
1212
1239
  let args = e.args.map((a) => lowerExpr(a, scope))
1240
+ // A hex among float3s is a float3: lerp(#ff8000, sky, t).
1241
+ const others = Math.max(0, ...args.filter((_, i) => !writtenHex(e.args[i]!)).map(widthOf))
1242
+ args = args.map((v, i) => asWidth(v, e.args[i]!, others, e.args[i]!.pos))
1213
1243
  // min, max, abs and clamp keep ints ints; everything else takes floats.
1214
1244
  if (INT_BUILTINS.has(n) && args.some(isIntLike) && args.every((v, i) => isIntLike(v) || (typeof v === "number" && wholeExpr(e.args[i]!)))) {
1215
1245
  const k = kindWant(args.find(isIntLike)!)
@@ -1264,7 +1294,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
1264
1294
  const arg = e.args[i]!
1265
1295
  // In the caller's scope, where the argument is written; as the
1266
1296
  // parameter's kind, so a whole number passed to an int is one.
1267
- const v = lowerExpr(arg, caller, want(t))
1297
+ const v = asWidth(lowerExpr(arg, caller, want(t)), arg, t.width, arg.pos)
1268
1298
  const got = widthOf(v)
1269
1299
  if (got !== t.width) {
1270
1300
  fail(
@@ -1307,7 +1337,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
1307
1337
  }
1308
1338
  }
1309
1339
 
1310
- // MARK: tables and small helpers
1340
+ // tables and small helpers
1311
1341
 
1312
1342
  type ArithOp = "+" | "-" | "*" | "/" | "%"
1313
1343
  type BitOp = "&" | "|" | "^" | "<<" | ">>"
@@ -82,7 +82,7 @@ class Parser {
82
82
  private readonly errors?: SLParseError[],
83
83
  ) {}
84
84
 
85
- // MARK: token plumbing
85
+ // token plumbing
86
86
 
87
87
  private peek(n = 0): Token { return this.tokens[Math.min(this.i + n, this.tokens.length - 1)]! }
88
88
  private next(): Token { return this.tokens[this.i++]! }
@@ -107,7 +107,7 @@ class Parser {
107
107
  this.errors.push(e)
108
108
  }
109
109
 
110
- // MARK: recovery
110
+ // recovery
111
111
 
112
112
  /**
113
113
  * Runs one statement's or one declaration's parse. Collecting, an error is
@@ -184,7 +184,7 @@ class Parser {
184
184
  this.fail(`"${t.text}" is not a type; the types are float, float2, float3, float4, int, uint and bool`, t)
185
185
  }
186
186
 
187
- // MARK: the file
187
+ // the file
188
188
 
189
189
  parseUnit(): Unit {
190
190
  const unit: Unit = {
@@ -328,7 +328,7 @@ class Parser {
328
328
  return { name: name.text, ret, params, body, pos: start, prelude: false }
329
329
  }
330
330
 
331
- // MARK: statements
331
+ // statements
332
332
 
333
333
  private parseBlock(): Stmt[] {
334
334
  this.expect("{", "to open a body")
@@ -538,7 +538,7 @@ class Parser {
538
538
  return { k: "switch", value, cases, pos: kw }
539
539
  }
540
540
 
541
- // MARK: expressions
541
+ // expressions
542
542
 
543
543
  parseExpr(): Expr { return this.parseBinary(0) }
544
544
 
@@ -40,6 +40,14 @@ float2 tile(float2 p, float n) {
40
40
  float3 palette(float t, float3 a, float3 b, float3 c, float3 d) {
41
41
  return a + b * cos(6.28318530718 * (c * t + d));
42
42
  }
43
+
44
+ // This pixel's ray from eye toward target, as Unity's camera casts it. lens is 0.5 / tan(fov / 2).
45
+ float3 lookRay(float3 eye, float3 target, float lens) {
46
+ float3 forward = normalize(target - eye);
47
+ float3 right = normalize(cross(float3(0, 1, 0), forward));
48
+ float3 up = cross(forward, right);
49
+ return normalize(forward * lens + right * centered.x + up * centered.y);
50
+ }
43
51
  `
44
52
 
45
53
  /** Each prelude function's name, in the order the source declares them. */
package/src/sl.ts CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  import { SLOP, SL_HLSL, SL_NAME, type SLOpCode } from "./ops"
32
32
  import { SL_SDF_PARAMS, SL_SDF_SHAPES, type SlSdfKind } from "./shapes"
33
33
 
34
- // MARK: values
34
+ // values
35
35
 
36
36
  type Swz = "x" | "y" | "z" | "w" | "r" | "g" | "b" | "a"
37
37
  const CHAN: Record<string, number> = { x: 0, y: 1, z: 2, w: 3, r: 0, g: 1, b: 2, a: 3 }
@@ -202,7 +202,7 @@ function withKind<T extends SLNode>(n: T, kind: SLKind | undefined): T {
202
202
  return kind === undefined ? n : { ...n, kind }
203
203
  }
204
204
 
205
- // MARK: recording context
205
+ // recording context
206
206
 
207
207
  let current: Builder | null = null
208
208
 
@@ -288,7 +288,7 @@ function un(op: SLOpCode, a: Val): Val {
288
288
  return mk(a.owner, a.owner.call(op, a.width, [a.ref]), a.width)
289
289
  }
290
290
 
291
- // MARK: kinds (IR 4)
291
+ // kinds (IR 4)
292
292
 
293
293
  /** The ops an int or a uint keeps its kind through. Anything else takes it as a float. */
294
294
  const INT_OPS = new Set<number>([SLOP.ADD, SLOP.SUB, SLOP.MUL, SLOP.DIV, SLOP.MOD, SLOP.MIN, SLOP.MAX, SLOP.CLAMP])
@@ -486,7 +486,7 @@ function typed(b: Builder, v: Num, t: ValType): NodeRef {
486
486
  return v.ref
487
487
  }
488
488
 
489
- // MARK: the public surface
489
+ // the public surface
490
490
 
491
491
  export interface ProgramInputs {
492
492
  uv: Vec2
package/src/structure.ts CHANGED
@@ -224,8 +224,15 @@ export const local = (ref: NodeRef): string => `n${ref}`
224
224
  * Prints the body: every placed node in its region, each `if` and `loop` with
225
225
  * its regions inside it. `expr` prints a value node's expression; the result is
226
226
  * `local(p.result)` for the caller to return.
227
+ *
228
+ * `before` gives the statements a value node needs ahead of its own line,
229
+ * asked after `expr` has printed that node: locals of its own, named after
230
+ * `local(ref)`, for an expression that would otherwise repeat a texture read.
227
231
  */
228
- export function printBody(p: Program, s: Structure, syntax: Syntax, expr: (ref: NodeRef) => string, indent: string): string[] {
232
+ export function printBody(
233
+ p: Program, s: Structure, syntax: Syntax, expr: (ref: NodeRef) => string, indent: string,
234
+ before: (ref: NodeRef) => readonly string[] = () => [],
235
+ ): string[] {
229
236
  const lines: string[] = []
230
237
  const loopRef = new Map<number, NodeRef>()
231
238
  for (const [ref, n] of p.nodes.entries()) if (n.k === "loop") loopRef.set(n.id, ref)
@@ -270,8 +277,11 @@ export function printBody(p: Program, s: Structure, syntax: Syntax, expr: (ref:
270
277
  lines.push(ind + syntax.value(ref, src.k === "loop" ? carried(n.src, n.index) : result(n.src, n.index)))
271
278
  break
272
279
  }
273
- default:
274
- lines.push(ind + syntax.value(ref, expr(ref)))
280
+ default: {
281
+ const e = expr(ref)
282
+ for (const line of before(ref)) lines.push(ind + line)
283
+ lines.push(ind + syntax.value(ref, e))
284
+ }
275
285
  }
276
286
  }
277
287
  }
package/src/web.ts CHANGED
@@ -114,6 +114,8 @@ function emitWeb(p: Program, lang: WebLanguage): string {
114
114
  const splat = (ref: number, t: SLType) => (typeOf(ref) === 1 && t > 1 ? `${T(t)}(n${ref})` : `n${ref}`)
115
115
  /** A literal at width `t`. */
116
116
  const k = (v: number, t: SLType) => (t > 1 ? `${T(t)}(${lit(v)})` : lit(v))
117
+ /** The statements a node prints ahead of its own line (`printBody`'s `before`). */
118
+ const ahead = new Map<NodeRef, string[]>()
117
119
 
118
120
  const expr = (ref: NodeRef): string => {
119
121
  const n = valueAt(p.nodes, ref)
@@ -297,11 +299,25 @@ function emitWeb(p: Program, lang: WebLanguage): string {
297
299
  : `textureLod(sl_Tex${slot}, ${a[0]}, ${level})`
298
300
  // A fraction blends the level below and the level above,
299
301
  // whatever the texture's filter between levels, as the HLSL
300
- // emitter does (`isWhole`). Past the smallest level both read
301
- // the smallest, as a whole level past it does.
302
+ // emitter does (`isWhole`), each level's colour weighed by its
303
+ // alpha, the plain blend where both are transparent. Past the
304
+ // smallest level both read the smallest, as a whole level past
305
+ // it does.
302
306
  if (isWhole(p.nodes, n.args[1]!)) return at(a[1]!)
303
307
  const below = `floor(${a[1]})`
304
- return `mix(${at(below)}, ${at(`${below} + 1.0`)}, fract(${a[1]}))`
308
+ const [lo, hi, mixed] = ["lo", "hi", "pm"].map((s) => `${local(ref)}_${s}`)
309
+ const pm = (v: string) => `${T(4)}(${v}.rgb * ${v}.a, ${v}.a)`
310
+ const decl = (v: string, e: string) => (W ? `let ${v}: ${T(4)} = ${e};` : `${T(4)} ${v} = ${e};`)
311
+ ahead.set(ref, [
312
+ decl(lo, at(below)),
313
+ decl(hi, at(`${below} + 1.0`)),
314
+ decl(mixed, `mix(${pm(lo)}, ${pm(hi)}, fract(${a[1]}))`),
315
+ ])
316
+ const plain = `mix(${lo}.rgb, ${hi}.rgb, fract(${a[1]}))`
317
+ const rgb = W
318
+ ? `select(${plain}, ${mixed}.rgb / ${mixed}.a, ${mixed}.a > 0.0)`
319
+ : `(${mixed}.a > 0.0 ? ${mixed}.rgb / ${mixed}.a : ${plain})`
320
+ return `${T(4)}(${rgb}, ${mixed}.a)`
305
321
  }
306
322
  case SLOP.TEXTURE_SIZE: {
307
323
  const slot = Math.round(imm[0] ?? 0)
@@ -405,7 +421,7 @@ function emitWeb(p: Program, lang: WebLanguage): string {
405
421
  loopOpen: "for (;;) {",
406
422
  breakUnless: (c, turns, max) => `if (!(${c}) || ${turns} >= ${max}) break;`,
407
423
  }
408
- const body = printBody(p, shape, syntax, expr, " ").join("\n")
424
+ const body = printBody(p, shape, syntax, expr, " ", (ref) => ahead.get(ref) ?? []).join("\n")
409
425
  const library = [...[...smoothsteps].sort().map((w) => smoothstep(w, lang)), ...librarySource(need, lang)].join("\n")
410
426
  const header = `// GENERATED from a shader language program (${p.hash}). Do not edit.`
411
427
  // Only the textures the program reads, and in WGSL only the samplers it