@driftengine/core 4.10.0 → 4.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/CHANGELOG.json +107 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +6 -5
  4. package/dist/index.js +2 -2
  5. package/dist/render/ambientOcclusionPass.d.ts +3 -2
  6. package/dist/render/ambientOcclusionPass.js +4 -2
  7. package/dist/render/backend/api.d.ts +21 -1
  8. package/dist/render/backend/webgl2/renderer.d.ts +141 -3
  9. package/dist/render/backend/webgl2/renderer.js +454 -42
  10. package/dist/render/backend/webgpu/buffers.d.ts +5 -0
  11. package/dist/render/backend/webgpu/buffers.js +98 -48
  12. package/dist/render/backend/webgpu/clothKernels.d.ts +8 -0
  13. package/dist/render/backend/webgpu/clothKernels.js +76 -0
  14. package/dist/render/backend/webgpu/clothOps.d.ts +39 -0
  15. package/dist/render/backend/webgpu/clothOps.js +78 -0
  16. package/dist/render/backend/webgpu/clothSetLayout.d.ts +25 -0
  17. package/dist/render/backend/webgpu/clothSetLayout.js +84 -0
  18. package/dist/render/backend/webgpu/clothSetPack.d.ts +33 -0
  19. package/dist/render/backend/webgpu/clothSetPack.js +144 -0
  20. package/dist/render/backend/webgpu/drawCommand.d.ts +2 -0
  21. package/dist/render/backend/webgpu/drawCommand.js +1 -0
  22. package/dist/render/backend/webgpu/flatPass.d.ts +1 -1
  23. package/dist/render/backend/webgpu/flatPass.js +2 -1
  24. package/dist/render/backend/webgpu/gpuClothSet.d.ts +80 -0
  25. package/dist/render/backend/webgpu/gpuClothSet.js +462 -0
  26. package/dist/render/backend/webgpu/latePass.js +1 -0
  27. package/dist/render/backend/webgpu/motionPass.js +15 -6
  28. package/dist/render/backend/webgpu/pipelineCache.d.ts +22 -1
  29. package/dist/render/backend/webgpu/pipelineCache.js +39 -1
  30. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  31. package/dist/render/backend/webgpu/renderer.d.ts +100 -1
  32. package/dist/render/backend/webgpu/renderer.js +423 -95
  33. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +9 -2
  34. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +121 -47
  35. package/dist/render/backend/webgpu/staticBundles.js +2 -1
  36. package/dist/render/backend/webgpu/vertexFormats.d.ts +22 -0
  37. package/dist/render/backend/webgpu/vertexFormats.js +97 -0
  38. package/dist/render/globalMediumPass.d.ts +2 -0
  39. package/dist/render/globalMediumPass.js +1 -0
  40. package/dist/render/indexWidth.d.ts +17 -0
  41. package/dist/render/indexWidth.js +23 -0
  42. package/dist/render/instances.d.ts +13 -1
  43. package/dist/render/instances.js +10 -0
  44. package/dist/render/lightMatrix.d.ts +11 -0
  45. package/dist/render/lightMatrix.js +14 -0
  46. package/dist/render/mesh.d.ts +9 -0
  47. package/dist/render/mesh.js +38 -14
  48. package/dist/render/occlusionFade.d.ts +21 -0
  49. package/dist/render/occlusionFade.js +27 -0
  50. package/dist/render/renderQuality.d.ts +30 -0
  51. package/dist/render/renderQuality.js +5 -0
  52. package/dist/render/rendererHarness.d.ts +7 -0
  53. package/dist/render/rendererHarness.js +12 -0
  54. package/dist/render/sceneTarget.d.ts +2 -0
  55. package/dist/render/sceneTarget.js +1 -1
  56. package/dist/render/shader.d.ts +15 -0
  57. package/dist/render/shader.js +41 -0
  58. package/dist/render/shaders/ambientOcclusion.js +12 -1
  59. package/dist/render/shaders/flat/directionalShadow.js +84 -10
  60. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  61. package/dist/render/shaders/flat/glassTint.js +3 -0
  62. package/dist/render/shaders/flat/index.d.ts +15 -0
  63. package/dist/render/shaders/flat/index.js +24 -4
  64. package/dist/render/shaders/flat/layered.d.ts +27 -0
  65. package/dist/render/shaders/flat/layered.js +198 -0
  66. package/dist/render/shaders/flat/main.d.ts +1 -1
  67. package/dist/render/shaders/flat/main.js +50 -16
  68. package/dist/render/shaders/flat/mapReads.d.ts +10 -0
  69. package/dist/render/shaders/flat/mapReads.js +38 -0
  70. package/dist/render/shaders/flat/movingSun.d.ts +21 -0
  71. package/dist/render/shaders/flat/movingSun.js +46 -0
  72. package/dist/render/shaders/flat/normalMap.d.ts +6 -2
  73. package/dist/render/shaders/flat/normalMap.js +23 -3
  74. package/dist/render/shaders/flat/overlay.d.ts +3 -2
  75. package/dist/render/shaders/flat/overlay.js +41 -19
  76. package/dist/render/shaders/flat/preamble.js +23 -5
  77. package/dist/render/shaders/flat/worldUv.d.ts +22 -0
  78. package/dist/render/shaders/flat/worldUv.js +113 -0
  79. package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +6 -1
  80. package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +6 -1
  81. package/dist/render/shaders/generated/flat.wgsl.d.ts +768 -368
  82. package/dist/render/shaders/generated/flat.wgsl.js +791 -391
  83. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +8 -3
  84. package/dist/render/shaders/generated/globalMedium.wgsl.js +8 -3
  85. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +15 -10
  86. package/dist/render/shaders/generated/lightVolume.wgsl.js +16 -11
  87. package/dist/render/shaders/globalMedium.js +9 -1
  88. package/dist/render/shaders/lightVolume.js +9 -1
  89. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  90. package/dist/render/shaders/tangentFrame.js +16 -3
  91. package/dist/render/skinnedClothRun.d.ts +28 -0
  92. package/dist/render/skinnedClothRun.js +103 -35
  93. package/dist/render/surfaceLayers.d.ts +107 -0
  94. package/dist/render/surfaceLayers.js +82 -0
  95. package/dist/render/surfaceOverlay.d.ts +16 -3
  96. package/dist/render/surfaceOverlay.js +11 -6
  97. package/dist/render/surfaceProjection.d.ts +34 -0
  98. package/dist/render/surfaceProjection.js +30 -0
  99. package/dist/render/surfaceTexture.d.ts +39 -0
  100. package/dist/render/transmission.d.ts +21 -0
  101. package/dist/render/transmission.js +21 -0
  102. package/dist/render/vertexPacking.d.ts +48 -0
  103. package/dist/render/vertexPacking.js +68 -0
  104. package/dist/ui/splash.d.ts +42 -2
  105. package/dist/ui/splash.js +78 -13
  106. package/package.json +6 -6
  107. package/src/index.ts +6 -2
  108. package/src/render/ambientOcclusionPass.ts +4 -1
  109. package/src/render/backend/api.ts +31 -0
  110. package/src/render/backend/webgl2/renderer.ts +519 -56
  111. package/src/render/backend/webgpu/buffers.ts +153 -54
  112. package/src/render/backend/webgpu/clothKernels.ts +86 -0
  113. package/src/render/backend/webgpu/clothOps.ts +86 -0
  114. package/src/render/backend/webgpu/clothSetLayout.ts +102 -0
  115. package/src/render/backend/webgpu/clothSetPack.ts +197 -0
  116. package/src/render/backend/webgpu/drawCommand.ts +3 -0
  117. package/src/render/backend/webgpu/flatPass.ts +26 -28
  118. package/src/render/backend/webgpu/gpuClothSet.ts +564 -0
  119. package/src/render/backend/webgpu/latePass.ts +1 -0
  120. package/src/render/backend/webgpu/motionPass.ts +18 -7
  121. package/src/render/backend/webgpu/pipelineCache.ts +46 -7
  122. package/src/render/backend/webgpu/renderer.ts +622 -164
  123. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +121 -47
  124. package/src/render/backend/webgpu/staticBundles.ts +2 -1
  125. package/src/render/backend/webgpu/vertexFormats.ts +113 -0
  126. package/src/render/globalMediumPass.ts +7 -0
  127. package/src/render/indexWidth.ts +27 -0
  128. package/src/render/instances.ts +18 -1
  129. package/src/render/lightMatrix.ts +15 -0
  130. package/src/render/mesh.ts +66 -14
  131. package/src/render/occlusionFade.ts +29 -0
  132. package/src/render/renderQuality.ts +38 -0
  133. package/src/render/rendererHarness.ts +15 -0
  134. package/src/render/sceneTarget.ts +3 -0
  135. package/src/render/shader.ts +59 -0
  136. package/src/render/shaders/ambientOcclusion.ts +12 -1
  137. package/src/render/shaders/flat/directionalShadow.ts +84 -10
  138. package/src/render/shaders/flat/glassTint.ts +7 -1
  139. package/src/render/shaders/flat/index.ts +39 -4
  140. package/src/render/shaders/flat/layered.ts +198 -0
  141. package/src/render/shaders/flat/main.ts +50 -16
  142. package/src/render/shaders/flat/mapReads.ts +38 -0
  143. package/src/render/shaders/flat/movingSun.ts +46 -0
  144. package/src/render/shaders/flat/normalMap.ts +23 -3
  145. package/src/render/shaders/flat/overlay.ts +41 -19
  146. package/src/render/shaders/flat/preamble.ts +23 -5
  147. package/src/render/shaders/flat/worldUv.ts +113 -0
  148. package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +6 -1
  149. package/src/render/shaders/generated/flat.wgsl.ts +791 -391
  150. package/src/render/shaders/generated/globalMedium.wgsl.ts +8 -3
  151. package/src/render/shaders/generated/lightVolume.wgsl.ts +16 -11
  152. package/src/render/shaders/globalMedium.ts +9 -1
  153. package/src/render/shaders/lightVolume.ts +9 -1
  154. package/src/render/shaders/tangentFrame.ts +16 -3
  155. package/src/render/skinnedClothRun.ts +138 -38
  156. package/src/render/surfaceLayers.ts +176 -0
  157. package/src/render/surfaceOverlay.ts +24 -7
  158. package/src/render/surfaceProjection.ts +57 -0
  159. package/src/render/surfaceTexture.ts +39 -0
  160. package/src/render/transmission.ts +27 -0
  161. package/src/render/vertexPacking.ts +109 -0
  162. package/src/ui/splash.ts +83 -14
  163. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +0 -74
  164. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +0 -452
  165. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +0 -502
@@ -6,6 +6,17 @@ export const MAIN_GLSL = `void main() {
6
6
  * turns a float into an array index. A plain texture is an array of one, so layer 0.
7
7
  */
8
8
  vec3 surfaceAt = vec3(vUv.xy, floor(vUv.z + 0.5));
9
+ /*
10
+ * The coordinates a map's frame and its cutout's level are measured along: the mesh's, or the
11
+ * world's where the material projects its maps (worldUv.ts) — in which case the mesh's tangents
12
+ * no longer run along them either. Triplanar reads its own three planes below; every other
13
+ * reader of surfaceAt takes the horizontal plane.
14
+ */
15
+ vec2 frameAt = vUv.xy;
16
+ int frameTangents = vHasTangents;
17
+ projectSurface(surfaceAt, frameAt, frameTangents);
18
+ /* A layered material's mask, read once for every layer's share below: layered.ts. */
19
+ if (LAYERED && layered()) layeredBegin();
9
20
  if (dot(vec4(vWorldPos, 1.0), uClipPlane) < 0.0) discard;
10
21
  /*
11
22
  * The layer's surface effects, zeros where the albedo array carries no table — see
@@ -25,6 +36,8 @@ export const MAIN_GLSL = `void main() {
25
36
  vec2 fxDx = dFdx(surfaceAt.xy);
26
37
  vec2 fxDy = dFdy(surfaceAt.xy);
27
38
  if (uWriteMode.y != 0.0 && !ditherKeeps(uWriteMode.y)) discard;
39
+ /* An opacity spent on the screen door instead of the blend: preamble.ts. 0 keeps nothing. */
40
+ if (uWriteMode.z != 0.0 && ditherThreshold() >= vAlpha) discard;
28
41
  /* A draw's dissolve cuts the surface away before anything is shaded: overlay.ts. */
29
42
  if (SURFACE_OVERLAY) {
30
43
  if (overlayCuts(surfaceAt.xy)) discard;
@@ -62,14 +75,14 @@ export const MAIN_GLSL = `void main() {
62
75
  */
63
76
  float coverage = 1.0;
64
77
  if (uMaterialFlags.x != 0) {
65
- vec4 texel = texture(uAlbedo, surfaceAt);
78
+ vec4 texel = albedoTexel(surfaceAt);
66
79
  // Discarded before anything else is computed: a thrown-away fragment should not pay
67
80
  // for the lighting it will never contribute to. The test credits alpha for the mip level
68
81
  // it samples, so a needle averaged thin down the chain is not lost; see cutoutCoverage.ts.
69
82
  // Behind a branch on a uniform, so a material with no cutoff pays nothing for it.
70
83
  float kept = texel.a;
71
84
  if (uCutout.x > 0.0) {
72
- float tested = cutoutAlpha(texel.a, vUv.xy * vec2(textureSize(uAlbedo, 0).xy));
85
+ float tested = cutoutAlpha(texel.a, frameAt * vec2(textureSize(uAlbedo, 0).xy));
73
86
  if (uCutout.y == 0.0) {
74
87
  if (tested < uCutout.x) discard;
75
88
  } else {
@@ -88,6 +101,8 @@ export const MAIN_GLSL = `void main() {
88
101
  }
89
102
  }
90
103
  }
104
+ /* A layered material whose mask is the vertex colour takes no tint from it: layered.ts. */
105
+ if (LAYERED && layered()) albedo = layeredColor(albedo);
91
106
  albedo *= texel.rgb;
92
107
  coverage = kept;
93
108
  }
@@ -114,7 +129,7 @@ export const MAIN_GLSL = `void main() {
114
129
  float ormRoughness = vRoughness;
115
130
  float metal = 0.0;
116
131
  if (uMaterialFlags.y != 0) {
117
- vec3 t = texture(uOrmMap, surfaceAt).rgb;
132
+ vec3 t = ormTexel(surfaceAt);
118
133
  ormOcclusion = mix(1.0, t.r, uOrmScale.r);
119
134
  ormRoughness = clamp(t.g * uOrmScale.g, 0.0, 1.0);
120
135
  metal = clamp(t.b * uOrmScale.b, 0.0, 1.0);
@@ -155,6 +170,15 @@ export const MAIN_GLSL = `void main() {
155
170
  vec3 glassGlow = vec3(0.0);
156
171
  float glassTransmission = uSeeThrough.z;
157
172
  float glassFrost = uSeeThrough.w;
173
+ /*
174
+ * **A thin surface lets light from behind through, coloured by itself**: a banner, a leaf, a
175
+ * lampshade, an opaque material's \`diffuseTransmission\`, in the glass tint's spare lane. It
176
+ * gathers the light behind it in glassGlow, as a pane does, and adds it through its own colour,
177
+ * or through the \`transmissionColor\` it names in the tint's colour lanes (transmission.ts).
178
+ * None on glass, which has its own way through.
179
+ */
180
+ float thin = glassTransmission > 0.0 ? 0.0 : uGlassTint.w;
181
+ bool seesBehind = glassTransmission > 0.0 || thin > 0.0;
158
182
 
159
183
  /*
160
184
  * The normal a refracting draw bends by: the shading normal, a normal map's tilt and every relief
@@ -196,7 +220,7 @@ export const MAIN_GLSL = `void main() {
196
220
  * below. The rule is about non-uniform branches, and this branch is on a uniform.
197
221
  */
198
222
  vec3 unmapped = n;
199
- if (uNormalStrength > 0.0) n = normalMapped(n, surfaceAt);
223
+ if (uNormalStrength > 0.0) n = normalMapped(n, surfaceAt, frameAt, frameTangents);
200
224
  /* A draw's wrinkles, by region, over whatever the map made: overlay.ts. */
201
225
  if (SURFACE_OVERLAY) n = overlayWrinkle(n, unmapped, surfaceAt.xy);
202
226
 
@@ -367,6 +391,8 @@ export const MAIN_GLSL = `void main() {
367
391
  */
368
392
  shadingNormal = n;
369
393
  if (backFace) n = -n;
394
+ /* A blending rim, on the base colour every light below reads: overlay.ts. */
395
+ if (SURFACE_OVERLAY) albedo = overlayRimBase(albedo, surfaceAt.xy, n);
370
396
  /*
371
397
  * A model's surface, gathered once, on a branch on the switches alone (models.ts) — and whether
372
398
  * there is one, asked once: every site below is a branch on these two, which a pipeline with no
@@ -449,7 +475,7 @@ export const MAIN_GLSL = `void main() {
449
475
  float sunFacing = smoothstep(0.0, 0.02, surfaceNdl);
450
476
  float direct = ndl * sunShade * sunFacing;
451
477
  /* Glass: the sun on the far side of a pane lights it through the glass. See glassGlow. */
452
- if (glassTransmission > 0.0) glassGlow += sunColor * max(-surfaceNdl, 0.0) * sunShade;
478
+ if (seesBehind) glassGlow += sunColor * max(-surfaceNdl, 0.0) * sunShade;
453
479
 
454
480
  /*
455
481
  * Hemispheric: sky above, ground below, mixed by which way the surface looks. An
@@ -1301,8 +1327,7 @@ export const MAIN_GLSL = `void main() {
1301
1327
  * the glass, shaped and shadowed like any lamp, and is added to glassGlow once its shape is
1302
1328
  * known below. For everything else a lamp behind the surface is skipped here, as always.
1303
1329
  */
1304
- float backNdl =
1305
- glassTransmission > 0.0 ? max(-dot(n, toLight / max(dist, 1e-4)), 0.0) : 0.0;
1330
+ float backNdl = seesBehind ? max(-dot(n, toLight / max(dist, 1e-4)), 0.0) : 0.0;
1306
1331
  /* A model that sees light from behind keeps the lamps behind the surface. See models.ts. */
1307
1332
  if (ndl <= 0.0 && backNdl <= 0.0 && !modelledBehind) continue;
1308
1333
 
@@ -1322,7 +1347,9 @@ export const MAIN_GLSL = `void main() {
1322
1347
  #if POINT_SHADOWS
1323
1348
  float grazing = 1.0 - ndl;
1324
1349
  float normalOffset = (0.06 + dist * 0.05) * grazing * grazing + 0.02;
1325
- vec3 receiverPoint = vWorldPos + n * normalOffset;
1350
+ /* Toward a lamp behind a thin surface, which is in the map itself: away from it, the surface
1351
+ would stand in its own shadow. A pane is in no map, so glass keeps the usual side. */
1352
+ vec3 receiverPoint = vWorldPos + (thin > 0.0 && ndl <= 0.0 ? -n : n) * normalOffset;
1326
1353
 
1327
1354
  /*
1328
1355
  * uPointShadowWeight is how present the cubemap is, and it is the difference
@@ -1698,7 +1725,7 @@ export const MAIN_GLSL = `void main() {
1698
1725
  float signedForm = areaSignedForm(n, seenCentre, right, up, seenHalf);
1699
1726
  float form = mix(max(0.0, signedForm), abs(signedForm), uAreaLightTwoSided[a]);
1700
1727
  /* The form factor is linear in the normal, so the one seen from behind is its negation. */
1701
- float backForm = glassTransmission > 0.0
1728
+ float backForm = seesBehind
1702
1729
  ? mix(max(0.0, -signedForm), abs(signedForm), uAreaLightTwoSided[a])
1703
1730
  : 0.0;
1704
1731
  if (form <= 0.0 && backForm <= 0.0) continue;
@@ -1872,7 +1899,7 @@ export const MAIN_GLSL = `void main() {
1872
1899
  * \`driftLight.ts\`.
1873
1900
  */
1874
1901
  if (driftShare > 0.0) {
1875
- if (glassTransmission > 0.0) {
1902
+ if (seesBehind) {
1876
1903
  glassGlow += driftLightIrradiance(vWorldPos, -n) * (driftShare * uDriftLight.w);
1877
1904
  }
1878
1905
  /* The standard expression in its own order — albedo times the field, then the share — so a
@@ -1932,6 +1959,12 @@ export const MAIN_GLSL = `void main() {
1932
1959
  * underneath: a ceiling emitting a dull warm haze over a pale panel cannot be expressed
1933
1960
  * by scaling the panel, and scaling it gets the brightness right and the hue wrong.
1934
1961
  */
1962
+ /* What a thin surface lets through from behind, in its own colour or the one it names (below
1963
+ zero: none named, transmission.ts); a metal lets none. */
1964
+ if (thin > 0.0) {
1965
+ vec3 through = uGlassTint.r < 0.0 ? albedo : uGlassTint.rgb;
1966
+ lit += through * glassGlow * (thin * (1.0 - metal));
1967
+ }
1935
1968
  diffuseAlone = lit;
1936
1969
  vec3 emissiveTint = vEmissiveColor.r < 0.0 ? albedo : vEmissiveColor;
1937
1970
  /*
@@ -1952,7 +1985,7 @@ export const MAIN_GLSL = `void main() {
1952
1985
  */
1953
1986
  vec3 emissiveMapped = vec3(1.0);
1954
1987
  if (uMaterialFlags.z != 0) {
1955
- emissiveMapped = texture(uEmissiveMap, surfaceAt).rgb * uEmissiveScale;
1988
+ emissiveMapped = emissiveTexel(surfaceAt) * uEmissiveScale;
1956
1989
  }
1957
1990
  /* Pulse, flicker and fade from the layer's effects; exactly 1 where it names none. */
1958
1991
  float fxGlow = fx4.z + fx5.x + fx5.w > 0.0 ? fxEmission(surfaceAt.z) : 1.0;
@@ -2096,7 +2129,7 @@ export const MAIN_GLSL = `void main() {
2096
2129
  * map here, on a branch on uniforms alone, which keeps its derivatives legal.
2097
2130
  */
2098
2131
  vec3 refractN = shadingNormal;
2099
- if (uLightingEnabled == 0 && uNormalStrength > 0.0) refractN = normalMapped(refractN, surfaceAt);
2132
+ if (uLightingEnabled == 0 && uNormalStrength > 0.0) refractN = normalMapped(refractN, surfaceAt, frameAt, frameTangents);
2100
2133
  vec3 refractV = normalize(uCameraPos - vWorldPos);
2101
2134
  ivec2 snapSize = textureSize(uRefractScene, 0);
2102
2135
  vec2 screenUv = gl_FragCoord.xy / vec2(snapSize);
@@ -2133,8 +2166,8 @@ export const MAIN_GLSL = `void main() {
2133
2166
  * The glow of the lights behind it is added last, by how frosted it is.
2134
2167
  */
2135
2168
  float fresnel = 0.04 + 0.96 * pow(1.0 - cosView, 5.0);
2136
- shaded = mix(shaded, seen * uGlassTint, glassTransmission * (1.0 - fresnel)) +
2137
- applyOutputTransform(glassGlow * uGlassTint * (glassFrost * glassTransmission) * (1.0 - fog));
2169
+ shaded = mix(shaded, seen * uGlassTint.rgb, glassTransmission * (1.0 - fresnel)) +
2170
+ applyOutputTransform(glassGlow * uGlassTint.rgb * (glassFrost * glassTransmission) * (1.0 - fog));
2138
2171
  } else {
2139
2172
  shaded = seen;
2140
2173
  }
@@ -2146,9 +2179,10 @@ export const MAIN_GLSL = `void main() {
2146
2179
  * **It did not need MeshData.colors widened to four floats**, which is how the consumer who
2147
2180
  * asked for it had costed the change -- widening colors moves every mesh producer, the
2148
2181
  * container layout and every baked asset. Alpha is a factor on the draw's opacity, not a
2149
- * component of its albedo, so it rides a lane instead and nothing existing moves.
2182
+ * component of its albedo, so it rides a lane instead and nothing existing moves. Where the draw
2183
+ * spends it on the screen door instead (\`uWriteMode.z\`), it was spent at the top and is 1 here.
2150
2184
  */
2151
- float alpha = uOpacity * coverage * vAlpha;
2185
+ float alpha = uOpacity * coverage * (uWriteMode.z != 0.0 ? 1.0 : vAlpha);
2152
2186
 
2153
2187
  /*
2154
2188
  * **Order-independent transparency, when this draw is accumulating into it.**
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Where each of a material's maps is read: a layered material's blend (`layered.ts`), the three
3
+ * planes of a triplanar projection (`worldUv.ts`), or the map itself at the surface's coordinates.
4
+ *
5
+ * **Functions rather than branches in `main`, for the bundle's sake.** The generated WGSL stores an
6
+ * item once however many of the sixteen permutations share it, and `main` differs in every one, so
7
+ * a line there is paid sixteen times and a line here once. Called from the same uniform branches the
8
+ * reads stood in, so their implicit levels are as legal as they were: AGENTS.md 2026-08-07.
9
+ */
10
+ export declare const MAP_READS_GLSL = "\n/* The surface's coordinates moved onto the world's horizontal plane where a material projects them,\n with the frame's coordinates and the mesh's tangents, which no longer run along them, set aside. */\nvoid projectSurface(inout vec3 at, inout vec2 frameAt, inout int frameTangents) {\n if (WORLD_UVS && projected()) {\n at.xy = planarAt();\n frameAt = at.xy;\n frameTangents = 0;\n }\n}\n\nvec4 albedoTexel(vec3 at) {\n if (LAYERED && layered()) return layeredAlbedo();\n if (WORLD_UVS && triplanar()) return triplanarAlbedo(at.z);\n return texture(uAlbedo, at);\n}\n\nvec3 ormTexel(vec3 at) {\n if (LAYERED && layered()) return layeredOrm();\n if (WORLD_UVS && triplanar()) return triplanarOrm(at.z);\n return texture(uOrmMap, at).rgb;\n}\n\nvec3 emissiveTexel(vec3 at) {\n if (LAYERED && layered() && layeredGlows()) return layeredEmissive();\n if (WORLD_UVS && triplanar()) return triplanarEmissive(at.z);\n return texture(uEmissiveMap, at).rgb;\n}\n";
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Where each of a material's maps is read: a layered material's blend (`layered.ts`), the three
3
+ * planes of a triplanar projection (`worldUv.ts`), or the map itself at the surface's coordinates.
4
+ *
5
+ * **Functions rather than branches in `main`, for the bundle's sake.** The generated WGSL stores an
6
+ * item once however many of the sixteen permutations share it, and `main` differs in every one, so
7
+ * a line there is paid sixteen times and a line here once. Called from the same uniform branches the
8
+ * reads stood in, so their implicit levels are as legal as they were: AGENTS.md 2026-08-07.
9
+ */
10
+ export const MAP_READS_GLSL = /* glsl */ `
11
+ /* The surface's coordinates moved onto the world's horizontal plane where a material projects them,
12
+ with the frame's coordinates and the mesh's tangents, which no longer run along them, set aside. */
13
+ void projectSurface(inout vec3 at, inout vec2 frameAt, inout int frameTangents) {
14
+ if (WORLD_UVS && projected()) {
15
+ at.xy = planarAt();
16
+ frameAt = at.xy;
17
+ frameTangents = 0;
18
+ }
19
+ }
20
+
21
+ vec4 albedoTexel(vec3 at) {
22
+ if (LAYERED && layered()) return layeredAlbedo();
23
+ if (WORLD_UVS && triplanar()) return triplanarAlbedo(at.z);
24
+ return texture(uAlbedo, at);
25
+ }
26
+
27
+ vec3 ormTexel(vec3 at) {
28
+ if (LAYERED && layered()) return layeredOrm();
29
+ if (WORLD_UVS && triplanar()) return triplanarOrm(at.z);
30
+ return texture(uOrmMap, at).rgb;
31
+ }
32
+
33
+ vec3 emissiveTexel(vec3 at) {
34
+ if (LAYERED && layered() && layeredGlows()) return layeredEmissive();
35
+ if (WORLD_UVS && triplanar()) return triplanarEmissive(at.z);
36
+ return texture(uEmissiveMap, at).rgb;
37
+ }
38
+ `;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The sun's moving layer read through a matrix of its own: the one the frame's dynamic shadow pass
3
+ * was drawn with, where that is not the static layer's.
4
+ *
5
+ * **Why a matrix of its own.** The static layer wants a square over the whole of what stands still,
6
+ * so distant roofs keep their shadows; what moves wants a tight one, so a character's shadow is
7
+ * sharp. One matrix for both is one of the two given up. A caller fits each as it likes and draws
8
+ * each pass with its own (`beginShadowPass(matrix, 'dynamic')`); the renderer keeps the dynamic
9
+ * pass's and the lit stage reads that layer through it, the receiver's position projected from the
10
+ * world here rather than carried in a second varying.
11
+ *
12
+ * **A lit switch, `MOVING_SUN`**, off until a frame's dynamic pass is drawn with a matrix the static
13
+ * layer does not share, then on for good. Declared beside the other lit switches, ahead of the
14
+ * shadow lookup that calls it; compiled out, the moving layer is read at the static layer's place,
15
+ * exactly as it always was, and its matrix and depth span are not declared at all.
16
+ *
17
+ * **What it gives up**: a receiver outside the static layer's square takes no sun shadow at all,
18
+ * moving casters included, since everything is faded by that square first; so the tight square
19
+ * belongs inside the wide one.
20
+ */
21
+ export declare function movingSunGlsl(on: boolean): string;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The sun's moving layer read through a matrix of its own: the one the frame's dynamic shadow pass
3
+ * was drawn with, where that is not the static layer's.
4
+ *
5
+ * **Why a matrix of its own.** The static layer wants a square over the whole of what stands still,
6
+ * so distant roofs keep their shadows; what moves wants a tight one, so a character's shadow is
7
+ * sharp. One matrix for both is one of the two given up. A caller fits each as it likes and draws
8
+ * each pass with its own (`beginShadowPass(matrix, 'dynamic')`); the renderer keeps the dynamic
9
+ * pass's and the lit stage reads that layer through it, the receiver's position projected from the
10
+ * world here rather than carried in a second varying.
11
+ *
12
+ * **A lit switch, `MOVING_SUN`**, off until a frame's dynamic pass is drawn with a matrix the static
13
+ * layer does not share, then on for good. Declared beside the other lit switches, ahead of the
14
+ * shadow lookup that calls it; compiled out, the moving layer is read at the static layer's place,
15
+ * exactly as it always was, and its matrix and depth span are not declared at all.
16
+ *
17
+ * **What it gives up**: a receiver outside the static layer's square takes no sun shadow at all,
18
+ * moving casters included, since everything is faded by that square first; so the tight square
19
+ * belongs inside the wide one.
20
+ */
21
+ export function movingSunGlsl(on) {
22
+ if (!on) {
23
+ return /* glsl */ `
24
+ const bool MOVING_SUN = false; // wgsl:override
25
+ vec3 movingSunAt() { return vec3(0.0); }
26
+ float movingSunSpan() { return 1.0; }
27
+ `;
28
+ }
29
+ return /* glsl */ `
30
+ /* Whether the moving layer has a matrix of its own. Off until a frame draws it with one: movingSun.ts. */
31
+ const bool MOVING_SUN = true; // wgsl:override
32
+ /* The dynamic shadow pass's world-to-light matrix, and the depth it spans in metres. */
33
+ uniform mat4 uMovingLightViewProj;
34
+ uniform float uMovingShadowDepthSpan;
35
+
36
+ /* The receiver in the moving layer's map: x and y across it, z its depth, all 0 to 1. */
37
+ vec3 movingSunAt() {
38
+ vec4 l = uMovingLightViewProj * vec4(vWorldPos, 1.0);
39
+ return l.xyz / l.w * 0.5 + 0.5;
40
+ }
41
+
42
+ float movingSunSpan() {
43
+ return uMovingShadowDepthSpan;
44
+ }
45
+ `;
46
+ }
@@ -3,8 +3,12 @@
3
3
  * lit block for every light and by an unlit refracting draw for the direction it bends, so a lit
4
4
  * pane and an unlit distortion bend by one tilt rather than two transcriptions of it.
5
5
  *
6
- * Depth zero, after the tangent frame it calls; see `flatFrag`. **Only ever called under a branch
6
+ * The frame is built from `frameAt`, the coordinates the map was laid by — the mesh's, or the
7
+ * world's where a material projects them (`worldUv.ts`), with the mesh's tangents then set aside
8
+ * by `hasTangents`, since they run along coordinates the map no longer follows.
9
+ *
10
+ * Depth zero, after the tangent frame and the world's projection it calls; see `flatFrag`. **Only ever called under a branch
7
11
  * on a uniform** — `uNormalStrength` and whatever gates its caller — which is what makes the
8
12
  * derivatives inside `tangentFrame` and the implicit level of the read legal: AGENTS.md 2026-08-07.
9
13
  */
10
- export declare const NORMAL_MAP_GLSL = "\nvec3 normalMapped(vec3 n, vec3 at) {\n vec3 mapped = texture(uNormalMap, at).xyz * 2.0 - 1.0;\n /*\n * z rebuilt where none is stored. A tangent-space normal points out of its surface, so a\n * stored z at or below zero is never a real one: it is a two-channel map, BC5, whose blue a\n * device samples as zero and which arrives here as -1. Every map baked from BC5 to PNG before\n * 4.8.4 carried the same zero. A select rather than a flag: no uniform to bind, no material\n * that has to say what format it holds, and continuous at the boundary, since a unit texel\n * lying flat has its z near zero either way. A texel with z above zero is left as it was.\n */\n mapped.z = mapped.z > 0.0 ? mapped.z : sqrt(max(0.0, 1.0 - dot(mapped.xy, mapped.xy)));\n mat3 tbn = tangentFrame(n, vWorldPos, vUv.xy, vTangent, vHasTangents);\n return normalize(mix(n, normalize(tbn * mapped), uNormalStrength));\n}\n";
14
+ export declare const NORMAL_MAP_GLSL = "\nvec3 normalMapped(vec3 n, vec3 at, vec2 frameAt, int hasTangents) {\n /* On three planes where the material projects them so: worldUv.ts. */\n if (WORLD_UVS && triplanar()) return triplanarNormal(n, at.z);\n /*\n * A layered material's texels, blended by its mask before the frame turns them, around a normal\n * read at the mesh's own coordinates and tangents where the material lays one under them, which is\n * the surface's large shape the layers' detail rides on: layered.ts.\n */\n if (LAYERED && layered()) {\n vec3 under = n;\n if (layeredMeshNormal()) {\n mat3 meshFrame = tangentFrame(n, vWorldPos, vUv.xy, vTangent, vHasTangents);\n under = normalize(meshFrame * layeredMeshTexel());\n }\n mat3 layeredFrame = tangentFrame(under, vWorldPos, frameAt, vTangent, hasTangents);\n return normalize(mix(n, normalize(layeredFrame * layeredTexel()), uNormalStrength));\n }\n vec3 mapped = texture(uNormalMap, at).xyz * 2.0 - 1.0;\n /*\n * z rebuilt where none is stored. A tangent-space normal points out of its surface, so a\n * stored z at or below zero is never a real one: it is a two-channel map, BC5, whose blue a\n * device samples as zero and which arrives here as -1. Every map baked from BC5 to PNG before\n * 4.8.4 carried the same zero. A select rather than a flag: no uniform to bind, no material\n * that has to say what format it holds, and continuous at the boundary, since a unit texel\n * lying flat has its z near zero either way. A texel with z above zero is left as it was.\n */\n mapped.z = mapped.z > 0.0 ? mapped.z : sqrt(max(0.0, 1.0 - dot(mapped.xy, mapped.xy)));\n mat3 tbn = tangentFrame(n, vWorldPos, frameAt, vTangent, hasTangents);\n return normalize(mix(n, normalize(tbn * mapped), uNormalStrength));\n}\n";
@@ -3,12 +3,32 @@
3
3
  * lit block for every light and by an unlit refracting draw for the direction it bends, so a lit
4
4
  * pane and an unlit distortion bend by one tilt rather than two transcriptions of it.
5
5
  *
6
- * Depth zero, after the tangent frame it calls; see `flatFrag`. **Only ever called under a branch
6
+ * The frame is built from `frameAt`, the coordinates the map was laid by — the mesh's, or the
7
+ * world's where a material projects them (`worldUv.ts`), with the mesh's tangents then set aside
8
+ * by `hasTangents`, since they run along coordinates the map no longer follows.
9
+ *
10
+ * Depth zero, after the tangent frame and the world's projection it calls; see `flatFrag`. **Only ever called under a branch
7
11
  * on a uniform** — `uNormalStrength` and whatever gates its caller — which is what makes the
8
12
  * derivatives inside `tangentFrame` and the implicit level of the read legal: AGENTS.md 2026-08-07.
9
13
  */
10
14
  export const NORMAL_MAP_GLSL = /* glsl */ `
11
- vec3 normalMapped(vec3 n, vec3 at) {
15
+ vec3 normalMapped(vec3 n, vec3 at, vec2 frameAt, int hasTangents) {
16
+ /* On three planes where the material projects them so: worldUv.ts. */
17
+ if (WORLD_UVS && triplanar()) return triplanarNormal(n, at.z);
18
+ /*
19
+ * A layered material's texels, blended by its mask before the frame turns them, around a normal
20
+ * read at the mesh's own coordinates and tangents where the material lays one under them, which is
21
+ * the surface's large shape the layers' detail rides on: layered.ts.
22
+ */
23
+ if (LAYERED && layered()) {
24
+ vec3 under = n;
25
+ if (layeredMeshNormal()) {
26
+ mat3 meshFrame = tangentFrame(n, vWorldPos, vUv.xy, vTangent, vHasTangents);
27
+ under = normalize(meshFrame * layeredMeshTexel());
28
+ }
29
+ mat3 layeredFrame = tangentFrame(under, vWorldPos, frameAt, vTangent, hasTangents);
30
+ return normalize(mix(n, normalize(layeredFrame * layeredTexel()), uNormalStrength));
31
+ }
12
32
  vec3 mapped = texture(uNormalMap, at).xyz * 2.0 - 1.0;
13
33
  /*
14
34
  * z rebuilt where none is stored. A tangent-space normal points out of its surface, so a
@@ -19,7 +39,7 @@ vec3 normalMapped(vec3 n, vec3 at) {
19
39
  * lying flat has its z near zero either way. A texel with z above zero is left as it was.
20
40
  */
21
41
  mapped.z = mapped.z > 0.0 ? mapped.z : sqrt(max(0.0, 1.0 - dot(mapped.xy, mapped.xy)));
22
- mat3 tbn = tangentFrame(n, vWorldPos, vUv.xy, vTangent, vHasTangents);
42
+ mat3 tbn = tangentFrame(n, vWorldPos, frameAt, vTangent, hasTangents);
23
43
  return normalize(mix(n, normalize(tbn * mapped), uNormalStrength));
24
44
  }
25
45
  `;
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * A draw's surface overlay in the lit stage: the rim, the dissolve and the wrinkles
3
- * `surfaceOverlay.ts` packs into `uOverlay`. Called from `main` at three places — the dissolve's cut
4
- * before anything is shaded, the wrinkles after the normal map, and the glow beside emission.
3
+ * `surfaceOverlay.ts` packs into `uOverlay`. Called from `main` at four places — the dissolve's cut
4
+ * before anything is shaded, the wrinkles after the normal map, a blending rim on the base colour
5
+ * once the normal is final and before any light reads the colour, and the glow beside emission.
5
6
  *
6
7
  * **A lit switch, `SURFACE_OVERLAY`**, off until a draw first sets an overlay and then on for good,
7
8
  * as `litSwitchesGlsl`'s are; declared after the models so every switch before it keeps its id.
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * A draw's surface overlay in the lit stage: the rim, the dissolve and the wrinkles
3
- * `surfaceOverlay.ts` packs into `uOverlay`. Called from `main` at three places — the dissolve's cut
4
- * before anything is shaded, the wrinkles after the normal map, and the glow beside emission.
3
+ * `surfaceOverlay.ts` packs into `uOverlay`. Called from `main` at four places — the dissolve's cut
4
+ * before anything is shaded, the wrinkles after the normal map, a blending rim on the base colour
5
+ * once the normal is final and before any light reads the colour, and the glow beside emission.
5
6
  *
6
7
  * **A lit switch, `SURFACE_OVERLAY`**, off until a draw first sets an overlay and then on for good,
7
8
  * as `litSwitchesGlsl`'s are; declared after the models so every switch before it keeps its id.
@@ -19,6 +20,7 @@ export function overlayGlsl(on) {
19
20
  const bool SURFACE_OVERLAY = false; // wgsl:override
20
21
  bool overlayCuts(vec2 uv) { return false; }
21
22
  vec3 overlayWrinkle(vec3 mapped, vec3 unmapped, vec2 uv) { return mapped; }
23
+ vec3 overlayRimBase(vec3 base, vec2 uv, vec3 n) { return base; }
22
24
  vec3 overlayGlow(vec2 uv, vec3 n) { return vec3(0.0); }
23
25
  `;
24
26
  }
@@ -84,8 +86,42 @@ vec2 overlayScreen() {
84
86
  return vec2(eye.x / depth + uClusterFrustum.w, eye.y / depth + 1.0) * 0.5;
85
87
  }
86
88
 
89
+ /*
90
+ * The rim's weight here, 0 to 1 before the pulse and the mask: the edge leaning toward normals that
91
+ * face up, raised to the contrast, times the noise laid in screen space. Its strength is the
92
+ * caller's: an added rim takes it times its light, a blending one times its colour.
93
+ */
94
+ float overlayRimWeight(vec2 uv, vec3 n) {
95
+ vec3 toEye = normalize(uCameraPos - vWorldPos);
96
+ float edge = pow(1.0 - clamp(dot(n, toEye), 0.0, 1.0), max(uOverlay[1].x, 1e-4));
97
+ float up = pow(clamp(n.y * 0.5 + 0.5, 0.0, 1.0), max(uOverlay[1].y, 1e-4));
98
+ float rim = pow(edge * up, max(uOverlay[1].z, 1e-4));
99
+ float time = uSurfaceScene.x;
100
+ if (uOverlay[2].x > 0.0 && overlayImagesBound()) {
101
+ vec2 at = overlayScreen() * uOverlay[3].z + uOverlay[3].xy * time;
102
+ rim *= overlayImage(uOverlay[2], at).r;
103
+ }
104
+ rim = min(rim, 1.0);
105
+ rim *= mix(uOverlay[4].x, uOverlay[4].y, 0.5 + 0.5 * sin(uOverlay[3].w * time));
106
+ if (uOverlay[5].x > 0.0 && overlayImagesBound()) {
107
+ rim *= 1.0 - uOverlay[1].w * overlayImage(uOverlay[5], uv).r;
108
+ }
109
+ return rim;
110
+ }
111
+
112
+ /*
113
+ * A blending rim: the base colour pulled toward the rim's by the weight times the alpha, held at
114
+ * one, before any light reads it, so the edge is lit as the surface is. The colour arrives already
115
+ * held at one a channel. Nothing where the rim adds instead, or where there is none.
116
+ */
117
+ vec3 overlayRimBase(vec3 base, vec2 uv, vec3 n) {
118
+ if (uOverlay[0].w <= 0.0 || uOverlay[4].z < 0.5) return base;
119
+ float weight = clamp(uOverlay[0].w * overlayRimWeight(uv, n), 0.0, 1.0);
120
+ return mix(base, uOverlay[0].rgb, weight);
121
+ }
122
+
87
123
  /* What the overlay adds to the light the surface gives off: the dissolve's band, the colour laid
88
- over through its noise, and the rim. Zero where the draw sets none of them. */
124
+ over through its noise, and an adding rim. Zero where the draw sets none of them. */
89
125
  vec3 overlayGlow(vec2 uv, vec3 n) {
90
126
  vec3 glow = vec3(0.0);
91
127
  float noise = overlayDissolveNoise(uv);
@@ -94,22 +130,8 @@ vec3 overlayGlow(vec2 uv, vec3 n) {
94
130
  glow += uOverlay[8].rgb * uOverlay[8].w * band + uOverlay[9].rgb * uOverlay[9].w * noise;
95
131
  }
96
132
  float strength = uOverlay[0].w;
97
- if (strength > 0.0) {
98
- vec3 toEye = normalize(uCameraPos - vWorldPos);
99
- float edge = pow(1.0 - clamp(dot(n, toEye), 0.0, 1.0), max(uOverlay[1].x, 1e-4));
100
- float up = pow(clamp(n.y * 0.5 + 0.5, 0.0, 1.0), max(uOverlay[1].y, 1e-4));
101
- float rim = pow(edge * up, max(uOverlay[1].z, 1e-4));
102
- float time = uSurfaceScene.x;
103
- if (uOverlay[2].x > 0.0 && overlayImagesBound()) {
104
- vec2 at = overlayScreen() * uOverlay[3].z + uOverlay[3].xy * time;
105
- rim *= overlayImage(uOverlay[2], at).r;
106
- }
107
- rim = min(rim, 1.0);
108
- rim *= mix(uOverlay[4].x, uOverlay[4].y, 0.5 + 0.5 * sin(uOverlay[3].w * time));
109
- if (uOverlay[5].x > 0.0 && overlayImagesBound()) {
110
- rim *= 1.0 - uOverlay[1].w * overlayImage(uOverlay[5], uv).r;
111
- }
112
- glow += uOverlay[0].rgb * strength * rim;
133
+ if (strength > 0.0 && uOverlay[4].z < 0.5) {
134
+ glow += uOverlay[0].rgb * strength * overlayRimWeight(uv, n);
113
135
  }
114
136
  return glow;
115
137
  }
@@ -116,7 +116,12 @@ uniform vec4 uSeeThrough; // wgsl:material
116
116
  * the rows a screen coordinate counts. A vec4 where a vec3 already spent the row.
117
117
  */
118
118
  uniform vec4 uRefractTint; // wgsl:material
119
- uniform vec3 uGlassTint; // wgsl:material
119
+ /*
120
+ * A pane's colour (rgb), and in w how much light from behind an opaque thin surface lets through:
121
+ * \`SurfaceMaterial.diffuseTransmission\`, read where the draw is not glass. A vec4 where a vec3
122
+ * already spent the row.
123
+ */
124
+ uniform vec4 uGlassTint; // wgsl:material
120
125
  flat in int vHasTangents;
121
126
 
122
127
  /**
@@ -428,17 +433,30 @@ uniform float uOpacity; // wgsl:material
428
433
  * the grain is mirrored between backends and nothing else about it differs. What it gives up: a
429
434
  * crossfade reads as grain for as long as it lasts, which is why it should be short; and a blended
430
435
  * draw that asks for it is dithered too, since the test is a discard.
436
+ *
437
+ * **\`.z\` — whether this draw spends its own opacity on the same screen door**: 1 keeps the cells
438
+ * under \`vAlpha\` (an instance's \`MeshInstances.alphas\`, a vertex's channel alpha) and blends none
439
+ * of it, so an opaque or cut-out draw can fade one instance at a time. 0 is off. **The same pattern
440
+ * as the fade, and that is the point**: a pixel is kept where its one threshold sits under both, so
441
+ * a crossfade at \`t\` and \`-t\` of an instance at opacity \`a\` still covers each pixel of the
442
+ * instance's share \`a\` once. Two independent patterns would leave holes where they disagree.
431
443
  */
432
- uniform vec2 uWriteMode; // wgsl:material
444
+ uniform vec3 uWriteMode; // wgsl:material
433
445
 
434
- /** Whether this fragment is one \`uWriteMode.y\` keeps. Integer arithmetic, per the device rule. */
435
- bool ditherKeeps(float amount) {
446
+ /** Where this fragment's cell of the 8×8 Bayer pattern falls, in (0, 1). Integer arithmetic,
447
+ per the device rule. */
448
+ float ditherThreshold() {
436
449
  uvec2 p = uvec2(gl_FragCoord.xy) & 7u;
437
450
  uint a = p.x ^ p.y;
438
451
  uint b = p.y;
439
452
  uint cell = ((a & 1u) << 5) | ((b & 1u) << 4) | ((a & 2u) << 2) | ((b & 2u) << 1) |
440
453
  ((a & 4u) >> 1) | ((b & 4u) >> 2);
441
- float threshold = (float(cell) + 0.5) / 64.0;
454
+ return (float(cell) + 0.5) / 64.0;
455
+ }
456
+
457
+ /** Whether this fragment is one \`uWriteMode.y\` keeps. */
458
+ bool ditherKeeps(float amount) {
459
+ float threshold = ditherThreshold();
442
460
  return amount > 0.0 ? threshold < amount : threshold >= -amount;
443
461
  }
444
462
 
@@ -0,0 +1,22 @@
1
+ /**
2
+ * A material's maps placed by where a point is in the world rather than by the mesh's texture
3
+ * coordinates: `SurfaceMaterial.projection`, packed into `uWorldUv` by `surfaceProjection.ts`.
4
+ *
5
+ * **Planar** takes the two horizontal axes, `x` and `z`, at so many repeats a metre: a floor whose
6
+ * mesh carries no useful coordinates, or one coordinate for the whole of it, shows its texture at
7
+ * the size the material asks. **Triplanar** reads every map three times, on the planes across `x`,
8
+ * `y` and `z`, and blends them by how squarely the surface faces each — the geometric normal raised
9
+ * to a sharpness — so a wall, a cliff and a rock wear it as a floor does. A normal map on three
10
+ * planes is turned into the world by the whiteout blend, each plane's tangent-space normal added to
11
+ * the surface's own swizzled into that plane, which keeps the detail of each without a tangent
12
+ * frame per plane.
13
+ *
14
+ * **A lit switch, `WORLD_UVS`**, off until a material first asks and then on for good, declared
15
+ * last so every switch before it keeps its id. Compiled out, its vector is not declared and its
16
+ * calls are constants, so a scene that never asks pays nothing.
17
+ *
18
+ * **What it gives up**: a projection is the world's, so a mesh that moves slides under its own
19
+ * texture, which is right for ground and wrong for a crate; triplanar reads each map three times;
20
+ * and a map that is an array reads one layer for the whole surface, the one the face names.
21
+ */
22
+ export declare function worldUvGlsl(on: boolean): string;