onejs-sl 0.9.1 → 0.9.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.2
4
+
5
+ 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.
6
+
7
+ - A fractional `tex2Dlod` level blends the two levels premultiplied, so a transparent level fades the other out instead of pulling it toward black
8
+ - Opaque textures draw as they did, and so does colour under zero alpha
9
+ - 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
10
+ - The goldens anchor `lod-alpha.sl` to arithmetic, through a texture with alpha bound to a slot declared `edge`
11
+
3
12
  ## 0.9.1
4
13
 
5
14
  `?:` 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.2",
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onejs-sl",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
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)};`)
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