@driftengine/core 4.9.0 → 4.11.0

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 (231) hide show
  1. package/CHANGELOG.json +133 -11
  2. package/README.md +43 -38
  3. package/dist/index.d.ts +7 -4
  4. package/dist/index.js +5 -4
  5. package/dist/render/ambientHarmonics.d.ts +1 -1
  6. package/dist/render/ambientHarmonics.js +1 -1
  7. package/dist/render/areaLights.d.ts +4 -4
  8. package/dist/render/areaLights.js +1 -1
  9. package/dist/render/backend/api.d.ts +10 -3
  10. package/dist/render/backend/select.js +11 -5
  11. package/dist/render/backend/webgl2/boneAnimations.d.ts +29 -0
  12. package/dist/render/backend/webgl2/boneAnimations.js +80 -0
  13. package/dist/render/backend/webgl2/instanced.d.ts +5 -1
  14. package/dist/render/backend/webgl2/instanced.js +13 -1
  15. package/dist/render/backend/webgl2/renderer.d.ts +189 -4
  16. package/dist/render/backend/webgl2/renderer.js +614 -66
  17. package/dist/render/backend/webgl2/skinPaletteTexture.d.ts +5 -3
  18. package/dist/render/backend/webgl2/skinPaletteTexture.js +6 -4
  19. package/dist/render/backend/webgpu/boneAnimations.d.ts +34 -0
  20. package/dist/render/backend/webgpu/boneAnimations.js +76 -0
  21. package/dist/render/backend/webgpu/buffers.d.ts +24 -4
  22. package/dist/render/backend/webgpu/buffers.js +116 -59
  23. package/dist/render/backend/webgpu/clothKernels.d.ts +8 -0
  24. package/dist/render/backend/webgpu/clothKernels.js +76 -0
  25. package/dist/render/backend/webgpu/clothOps.d.ts +39 -0
  26. package/dist/render/backend/webgpu/clothOps.js +78 -0
  27. package/dist/render/backend/webgpu/clothSetLayout.d.ts +25 -0
  28. package/dist/render/backend/webgpu/clothSetLayout.js +84 -0
  29. package/dist/render/backend/webgpu/clothSetPack.d.ts +33 -0
  30. package/dist/render/backend/webgpu/clothSetPack.js +144 -0
  31. package/dist/render/backend/webgpu/compressedUpload.d.ts +0 -1
  32. package/dist/render/backend/webgpu/compressedUpload.js +53 -8
  33. package/dist/render/backend/webgpu/depthPass.d.ts +59 -3
  34. package/dist/render/backend/webgpu/depthPass.js +49 -14
  35. package/dist/render/backend/webgpu/drawCommand.d.ts +20 -4
  36. package/dist/render/backend/webgpu/drawCommand.js +11 -3
  37. package/dist/render/backend/webgpu/flatPass.d.ts +85 -42
  38. package/dist/render/backend/webgpu/flatPass.js +137 -20
  39. package/dist/render/backend/webgpu/gpuClothSet.d.ts +80 -0
  40. package/dist/render/backend/webgpu/gpuClothSet.js +462 -0
  41. package/dist/render/backend/webgpu/instanced.d.ts +14 -1
  42. package/dist/render/backend/webgpu/instanced.js +24 -1
  43. package/dist/render/backend/webgpu/latePass.js +1 -0
  44. package/dist/render/backend/webgpu/materialStore.d.ts +101 -0
  45. package/dist/render/backend/webgpu/materialStore.js +265 -0
  46. package/dist/render/backend/webgpu/motionPass.js +15 -6
  47. package/dist/render/backend/webgpu/pipelineCache.d.ts +28 -1
  48. package/dist/render/backend/webgpu/pipelineCache.js +46 -1
  49. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  50. package/dist/render/backend/webgpu/renderer.d.ts +200 -35
  51. package/dist/render/backend/webgpu/renderer.js +993 -168
  52. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +9 -2
  53. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +121 -47
  54. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +7 -3
  55. package/dist/render/backend/webgpu/skinPaletteRing.js +13 -7
  56. package/dist/render/backend/webgpu/skinPaletteTexture.d.ts +6 -0
  57. package/dist/render/backend/webgpu/skinPaletteTexture.js +7 -1
  58. package/dist/render/backend/webgpu/staticBundles.d.ts +125 -0
  59. package/dist/render/backend/webgpu/staticBundles.js +242 -0
  60. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +10 -3
  61. package/dist/render/backend/webgpu/surfaceTexturePass.js +20 -2
  62. package/dist/render/backend/webgpu/uniformRing.d.ts +21 -0
  63. package/dist/render/backend/webgpu/uniformRing.js +9 -1
  64. package/dist/render/backend/webgpu/vertexFormats.d.ts +22 -0
  65. package/dist/render/backend/webgpu/vertexFormats.js +97 -0
  66. package/dist/render/bloomChain.d.ts +3 -3
  67. package/dist/render/boneAnimation.d.ts +92 -0
  68. package/dist/render/boneAnimation.js +133 -0
  69. package/dist/render/compressedSource.d.ts +33 -13
  70. package/dist/render/compressedSource.js +70 -25
  71. package/dist/render/glCompressed.js +83 -0
  72. package/dist/render/globalMediumPass.d.ts +2 -0
  73. package/dist/render/globalMediumPass.js +1 -0
  74. package/dist/render/indexWidth.d.ts +17 -0
  75. package/dist/render/indexWidth.js +23 -0
  76. package/dist/render/instances.d.ts +26 -0
  77. package/dist/render/lightBudget.d.ts +10 -0
  78. package/dist/render/lightBudget.js +10 -0
  79. package/dist/render/lightMatrix.d.ts +11 -0
  80. package/dist/render/lightMatrix.js +14 -0
  81. package/dist/render/lightmap.d.ts +1 -1
  82. package/dist/render/mesh.d.ts +14 -0
  83. package/dist/render/mesh.js +44 -14
  84. package/dist/render/pointLightSelection.d.ts +4 -4
  85. package/dist/render/renderQuality.d.ts +30 -0
  86. package/dist/render/renderQuality.js +5 -0
  87. package/dist/render/rendererHarness.d.ts +7 -0
  88. package/dist/render/rendererHarness.js +12 -0
  89. package/dist/render/screenSpaceReflection.d.ts +2 -1
  90. package/dist/render/shader.d.ts +15 -0
  91. package/dist/render/shader.js +41 -0
  92. package/dist/render/shaders/boneAnimation.d.ts +19 -0
  93. package/dist/render/shaders/boneAnimation.js +63 -0
  94. package/dist/render/shaders/depth.d.ts +7 -0
  95. package/dist/render/shaders/depth.js +35 -10
  96. package/dist/render/shaders/flat/directionalShadow.js +84 -10
  97. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  98. package/dist/render/shaders/flat/glassTint.js +3 -0
  99. package/dist/render/shaders/flat/index.d.ts +21 -0
  100. package/dist/render/shaders/flat/index.js +42 -7
  101. package/dist/render/shaders/flat/layered.d.ts +19 -0
  102. package/dist/render/shaders/flat/layered.js +115 -0
  103. package/dist/render/shaders/flat/main.d.ts +1 -1
  104. package/dist/render/shaders/flat/main.js +58 -21
  105. package/dist/render/shaders/flat/mapReads.d.ts +10 -0
  106. package/dist/render/shaders/flat/mapReads.js +38 -0
  107. package/dist/render/shaders/flat/models.js +1 -1
  108. package/dist/render/shaders/flat/movingSun.d.ts +21 -0
  109. package/dist/render/shaders/flat/movingSun.js +46 -0
  110. package/dist/render/shaders/flat/normalMap.d.ts +6 -2
  111. package/dist/render/shaders/flat/normalMap.js +14 -3
  112. package/dist/render/shaders/flat/overlay.d.ts +3 -2
  113. package/dist/render/shaders/flat/overlay.js +42 -20
  114. package/dist/render/shaders/flat/pointShadow.js +2 -2
  115. package/dist/render/shaders/flat/preamble.js +59 -27
  116. package/dist/render/shaders/flat/worldUv.d.ts +22 -0
  117. package/dist/render/shaders/flat/worldUv.js +113 -0
  118. package/dist/render/shaders/generated/depth.wgsl.d.ts +93 -0
  119. package/dist/render/shaders/generated/depth.wgsl.js +93 -0
  120. package/dist/render/shaders/generated/flat.wgsl.d.ts +3616 -3042
  121. package/dist/render/shaders/generated/flat.wgsl.js +3688 -3113
  122. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +8 -3
  123. package/dist/render/shaders/generated/globalMedium.wgsl.js +8 -3
  124. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +15 -10
  125. package/dist/render/shaders/generated/lightVolume.wgsl.js +16 -11
  126. package/dist/render/shaders/globalMedium.js +9 -1
  127. package/dist/render/shaders/lightVolume.js +9 -1
  128. package/dist/render/shaders/outputTransform.d.ts +7 -0
  129. package/dist/render/shaders/outputTransform.js +28 -14
  130. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  131. package/dist/render/shaders/tangentFrame.js +16 -3
  132. package/dist/render/shaders/vertexChannel.d.ts +7 -0
  133. package/dist/render/shaders/vertexChannel.js +26 -0
  134. package/dist/render/skinnedClothRun.d.ts +28 -0
  135. package/dist/render/skinnedClothRun.js +103 -35
  136. package/dist/render/staticDraws.d.ts +47 -0
  137. package/dist/render/staticDraws.js +43 -0
  138. package/dist/render/surfaceLayers.d.ts +39 -0
  139. package/dist/render/surfaceLayers.js +26 -0
  140. package/dist/render/surfaceOverlay.d.ts +16 -3
  141. package/dist/render/surfaceOverlay.js +11 -6
  142. package/dist/render/surfaceProjection.d.ts +34 -0
  143. package/dist/render/surfaceProjection.js +30 -0
  144. package/dist/render/surfaceTexture.d.ts +68 -7
  145. package/dist/render/surfaceTexture.js +56 -17
  146. package/dist/render/vertexPacking.d.ts +48 -0
  147. package/dist/render/vertexPacking.js +68 -0
  148. package/package.json +6 -6
  149. package/src/index.ts +12 -4
  150. package/src/render/ambientHarmonics.ts +1 -1
  151. package/src/render/areaLights.ts +4 -4
  152. package/src/render/backend/api.ts +13 -3
  153. package/src/render/backend/select.ts +10 -5
  154. package/src/render/backend/webgl2/boneAnimations.ts +105 -0
  155. package/src/render/backend/webgl2/instanced.ts +20 -1
  156. package/src/render/backend/webgl2/renderer.ts +723 -79
  157. package/src/render/backend/webgl2/skinPaletteTexture.ts +6 -4
  158. package/src/render/backend/webgpu/boneAnimations.ts +97 -0
  159. package/src/render/backend/webgpu/buffers.ts +195 -70
  160. package/src/render/backend/webgpu/clothKernels.ts +86 -0
  161. package/src/render/backend/webgpu/clothOps.ts +86 -0
  162. package/src/render/backend/webgpu/clothSetLayout.ts +102 -0
  163. package/src/render/backend/webgpu/clothSetPack.ts +197 -0
  164. package/src/render/backend/webgpu/compressedUpload.ts +59 -9
  165. package/src/render/backend/webgpu/depthPass.ts +51 -10
  166. package/src/render/backend/webgpu/drawCommand.ts +25 -4
  167. package/src/render/backend/webgpu/flatPass.ts +212 -41
  168. package/src/render/backend/webgpu/gpuClothSet.ts +564 -0
  169. package/src/render/backend/webgpu/instanced.ts +32 -1
  170. package/src/render/backend/webgpu/latePass.ts +1 -0
  171. package/src/render/backend/webgpu/materialStore.ts +276 -0
  172. package/src/render/backend/webgpu/motionPass.ts +18 -7
  173. package/src/render/backend/webgpu/pipelineCache.ts +53 -7
  174. package/src/render/backend/webgpu/renderer.ts +1201 -202
  175. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +121 -47
  176. package/src/render/backend/webgpu/skinPaletteRing.ts +13 -7
  177. package/src/render/backend/webgpu/skinPaletteTexture.ts +7 -1
  178. package/src/render/backend/webgpu/staticBundles.ts +364 -0
  179. package/src/render/backend/webgpu/surfaceTexturePass.ts +29 -3
  180. package/src/render/backend/webgpu/uniformRing.ts +10 -1
  181. package/src/render/backend/webgpu/vertexFormats.ts +113 -0
  182. package/src/render/bloomChain.ts +3 -3
  183. package/src/render/boneAnimation.ts +223 -0
  184. package/src/render/compressedSource.ts +121 -29
  185. package/src/render/glCompressed.ts +83 -0
  186. package/src/render/globalMediumPass.ts +7 -0
  187. package/src/render/indexWidth.ts +27 -0
  188. package/src/render/instances.ts +27 -0
  189. package/src/render/lightBudget.ts +11 -0
  190. package/src/render/lightMatrix.ts +15 -0
  191. package/src/render/lightmap.ts +1 -1
  192. package/src/render/mesh.ts +72 -14
  193. package/src/render/pointLightSelection.ts +4 -4
  194. package/src/render/renderQuality.ts +38 -0
  195. package/src/render/rendererHarness.ts +15 -0
  196. package/src/render/screenSpaceReflection.ts +2 -1
  197. package/src/render/shader.ts +59 -0
  198. package/src/render/shaders/boneAnimation.ts +63 -0
  199. package/src/render/shaders/depth.ts +45 -10
  200. package/src/render/shaders/flat/directionalShadow.ts +84 -10
  201. package/src/render/shaders/flat/glassTint.ts +7 -1
  202. package/src/render/shaders/flat/index.ts +63 -7
  203. package/src/render/shaders/flat/layered.ts +115 -0
  204. package/src/render/shaders/flat/main.ts +58 -21
  205. package/src/render/shaders/flat/mapReads.ts +38 -0
  206. package/src/render/shaders/flat/models.ts +1 -1
  207. package/src/render/shaders/flat/movingSun.ts +46 -0
  208. package/src/render/shaders/flat/normalMap.ts +14 -3
  209. package/src/render/shaders/flat/overlay.ts +42 -20
  210. package/src/render/shaders/flat/pointShadow.ts +2 -2
  211. package/src/render/shaders/flat/preamble.ts +59 -27
  212. package/src/render/shaders/flat/worldUv.ts +113 -0
  213. package/src/render/shaders/generated/depth.wgsl.ts +95 -0
  214. package/src/render/shaders/generated/flat.wgsl.ts +3688 -3113
  215. package/src/render/shaders/generated/globalMedium.wgsl.ts +8 -3
  216. package/src/render/shaders/generated/lightVolume.wgsl.ts +16 -11
  217. package/src/render/shaders/globalMedium.ts +9 -1
  218. package/src/render/shaders/lightVolume.ts +9 -1
  219. package/src/render/shaders/outputTransform.ts +30 -14
  220. package/src/render/shaders/tangentFrame.ts +16 -3
  221. package/src/render/shaders/vertexChannel.ts +27 -0
  222. package/src/render/skinnedClothRun.ts +138 -38
  223. package/src/render/staticDraws.ts +95 -0
  224. package/src/render/surfaceLayers.ts +57 -0
  225. package/src/render/surfaceOverlay.ts +24 -7
  226. package/src/render/surfaceProjection.ts +57 -0
  227. package/src/render/surfaceTexture.ts +124 -27
  228. package/src/render/vertexPacking.ts +109 -0
  229. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +0 -74
  230. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +0 -452
  231. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +0 -502
@@ -36,13 +36,56 @@ export const DIRECTIONALSHADOW_GLSL = `vec2 receiverPlaneDepthGradient(vec3 p) {
36
36
  );
37
37
  }
38
38
 
39
- float directionalVisibility(float receiverDepth, float compareDepth, float storedDepth) {
39
+ /* One stored depth against a receiver, in a map spanning \`span\` metres of depth. */
40
+ float directionalVisibilityAt(float receiverDepth, float compareDepth, float storedDepth, float span) {
40
41
  if (compareDepth <= storedDepth) return 1.0;
41
- float rayDistance = max(receiverDepth - storedDepth, 0.0) * uShadowDepthSpan;
42
+ float rayDistance = max(receiverDepth - storedDepth, 0.0) * span;
42
43
  float groundDistance = rayDistance * length(uDirectionalDir.xz);
43
44
  return 1.0 - shadowReach(groundDistance, uShadowMaxDistance);
44
45
  }
45
46
 
47
+ float directionalVisibility(float receiverDepth, float compareDepth, float storedDepth) {
48
+ return directionalVisibilityAt(receiverDepth, compareDepth, storedDepth, uShadowDepthSpan);
49
+ }
50
+
51
+ /* How far inside its square a receiver stands in a layer's map: 1 well inside, 0 at its border. */
52
+ float sunMapFade(vec3 p) {
53
+ if (p.z > 1.0 || p.x < 0.0 || p.x > 1.0 || p.y < 0.0 || p.y > 1.0) return 0.0;
54
+ vec2 fromCentre = abs(p.xy - 0.5) * 2.0;
55
+ return (1.0 - smoothstep(0.72, 0.98, max(fromCentre.x, fromCentre.y))) *
56
+ (1.0 - smoothstep(0.90, 1.0, p.z));
57
+ }
58
+
59
+ /*
60
+ * The moving layer at its own place, where it has a matrix of its own (movingSun.ts): the coarse
61
+ * test of shadowFactor, and its filter, each against the moving map's own depth span, and lit by
62
+ * how far inside the moving map's square the receiver stands rather than cut at its border.
63
+ */
64
+ float movingSunCoarse(vec3 pm, float span) {
65
+ float inside = sunMapFade(pm);
66
+ if (inside <= 0.0) return 1.0;
67
+ float stored = textureLod(uSunShadows, vec3(pm.xy, ${SUN_DYNAMIC_LAYER}.0), 0.0).r;
68
+ return mix(1.0, directionalVisibilityAt(pm.z, pm.z - 1.0 / span, stored, span), inside);
69
+ }
70
+
71
+ float movingSunFiltered(vec3 pm, vec2 gradient, float span) {
72
+ float inside = sunMapFade(pm);
73
+ if (inside <= 0.0) return 1.0;
74
+ float texel = 1.35 / uShadowMapSize;
75
+ float slopeLimit = 9.1 / uShadowMapSize;
76
+ float lit = 0.0;
77
+ for (int i = 0; i < ${MAX_SHADOW_FILTER_TAPS}; i++) {
78
+ if (i >= uShadowFilterTaps) break;
79
+ vec2 sampleUv = pm.xy + DIRECTIONAL_PCF_OFFSETS[i] * texel;
80
+ vec2 texelCentre = (floor(sampleUv * uShadowMapSize) + 0.5) / uShadowMapSize;
81
+ float slopeOffset = clamp(dot(gradient, texelCentre - pm.xy), -slopeLimit, slopeLimit);
82
+ float tapReceiverDepth = pm.z + slopeOffset;
83
+ float stored = textureLod(uSunShadows, vec3(sampleUv, ${SUN_DYNAMIC_LAYER}.0), 0.0).r;
84
+ lit += directionalVisibilityAt(tapReceiverDepth, tapReceiverDepth - 0.14 / span, stored, span);
85
+ }
86
+ return mix(1.0, lit / float(max(uShadowFilterTaps, 1)), inside);
87
+ }
88
+
46
89
  float shadowFactor(float directionalNdl) {
47
90
  if (uShadowStrength <= 0.0) return 1.0;
48
91
 
@@ -72,6 +115,14 @@ float shadowFactor(float directionalNdl) {
72
115
  vec3 p = vLightPos.xyz / vLightPos.w;
73
116
  p = p * 0.5 + 0.5;
74
117
  vec2 depthGradient = receiverPlaneDepthGradient(p);
118
+ /* The moving layer's own place and plane where it has a matrix of its own (movingSun.ts), taken
119
+ here beside the static layer's for the same reason: above every varying-dependent return. */
120
+ vec3 pm = p;
121
+ vec2 movingGradient = depthGradient;
122
+ if (MOVING_SUN) {
123
+ pm = movingSunAt();
124
+ movingGradient = receiverPlaneDepthGradient(pm);
125
+ }
75
126
 
76
127
  // Near a surface's own light terminator its direct-light term is already
77
128
  // approaching zero, while the orthographic projection of that surface
@@ -136,7 +187,8 @@ float shadowFactor(float directionalNdl) {
136
187
  float coarseCompare = p.z - 1.0 / uShadowDepthSpan;
137
188
  float coarse =
138
189
  directionalVisibility(p.z, coarseCompare, textureLod(uSunShadows, vec3(p.xy, ${SUN_STATIC_LAYER}.0), 0.0).r) *
139
- directionalVisibility(p.z, coarseCompare, textureLod(uSunShadows, vec3(p.xy, ${SUN_DYNAMIC_LAYER}.0), 0.0).r);
190
+ (MOVING_SUN ? movingSunCoarse(pm, movingSunSpan()) :
191
+ directionalVisibility(p.z, coarseCompare, textureLod(uSunShadows, vec3(p.xy, ${SUN_DYNAMIC_LAYER}.0), 0.0).r));
140
192
  if (uPeeledShadowEnabled != 0) {
141
193
  coarse *= directionalVisibility(p.z, coarseCompare, textureLod(uSunShadows, vec3(p.xy, ${SUN_PEELED_LAYER}.0), 0.0).r);
142
194
  }
@@ -200,7 +252,6 @@ float shadowFactor(float directionalNdl) {
200
252
  * there is no mip to select and level zero is the only thing \`texture\` could have read.
201
253
  */
202
254
  float staticDepth = textureLod(uSunShadows, vec3(sampleUv, ${SUN_STATIC_LAYER}.0), 0.0).r;
203
- float dynamicDepth = textureLod(uSunShadows, vec3(sampleUv, ${SUN_DYNAMIC_LAYER}.0), 0.0).r;
204
255
  staticLit += directionalVisibility(tapReceiverDepth, compare, staticDepth);
205
256
  if (uPeeledShadowEnabled != 0) {
206
257
  float peeledDepth = textureLod(uSunShadows, vec3(sampleUv, ${SUN_PEELED_LAYER}.0), 0.0).r;
@@ -208,12 +259,18 @@ float shadowFactor(float directionalNdl) {
208
259
  } else {
209
260
  peeledLit += 1.0;
210
261
  }
211
- dynamicLit += directionalVisibility(tapReceiverDepth, compare, dynamicDepth);
262
+ /* The moving layer here where it shares the static one's place; at its own below otherwise.
263
+ Branched on the switch itself, never its negation: see overridableConstants in transform.mjs. */
264
+ if (MOVING_SUN) {
265
+ } else {
266
+ float dynamicDepth = textureLod(uSunShadows, vec3(sampleUv, ${SUN_DYNAMIC_LAYER}.0), 0.0).r;
267
+ dynamicLit += directionalVisibility(tapReceiverDepth, compare, dynamicDepth);
268
+ }
212
269
  }
213
270
  float tapCount = float(max(uShadowFilterTaps, 1));
214
271
  staticLit /= tapCount;
215
272
  peeledLit /= tapCount;
216
- dynamicLit /= tapCount;
273
+ dynamicLit = MOVING_SUN ? movingSunFiltered(pm, movingGradient, movingSunSpan()) : dynamicLit / tapCount;
217
274
 
218
275
  // Independent layers preserve every recorded fact at an overlap: each
219
276
  // caster keeps its own distance fade, and their transmissions compose
@@ -234,11 +291,11 @@ float shadowFactor(float directionalNdl) {
234
291
  * behind the receiver lending it colour within the spread, which only a receiver between two panes
235
292
  * meets. One function for both layers, because two copies were most of the lit shaders' growth.
236
293
  */
237
- vec3 sunGlassLayer(vec3 p, float paneLayer, float tintLayer, float bias, float texel, float uvPerMetre) {
294
+ vec3 sunGlassLayer(vec3 p, float paneLayer, float tintLayer, float bias, float texel, float uvPerMetre, float span) {
238
295
  float tintSize = float(textureSize(uSunGlassTints, 0).x);
239
296
  float clarity = textureLod(uSunGlassTints, vec3(p.xy, tintLayer), 0.0).a;
240
297
  float behind =
241
- max(p.z - textureLod(uSunShadows, vec3(p.xy, paneLayer), 0.0).r, 0.0) * uShadowDepthSpan;
298
+ max(p.z - textureLod(uSunShadows, vec3(p.xy, paneLayer), 0.0).r, 0.0) * span;
242
299
  float radius = clamp(
243
300
  (1.0 - clarity) * ${FROST_SPREAD.toFixed(6)} * behind * uvPerMetre,
244
301
  texel,
@@ -278,6 +335,13 @@ vec3 sunGlassLookup(float directionalNdl) {
278
335
  /* Map units per metre along the receiver, from derivatives taken first in uniform control flow,
279
336
  so a frost spread stated in metres lands the same whatever size of light matrix a consumer built. */
280
337
  float uvPerMetre = length(dFdx(p.xy)) / max(length(dFdx(vWorldPos)), 1e-6);
338
+ /* The moving glass at its own place where its layer has a matrix of its own: movingSun.ts. */
339
+ vec3 pm = p;
340
+ float movingPerMetre = uvPerMetre;
341
+ if (MOVING_SUN) {
342
+ pm = movingSunAt();
343
+ movingPerMetre = length(dFdx(pm.xy)) / max(length(dFdx(vWorldPos)), 1e-6);
344
+ }
281
345
  if (uShadowStrength <= 0.0 || textureSize(uSunGlassTints, 0).x <= 1) return vec3(1.0);
282
346
  if (directionalNdl <= 0.0) return vec3(1.0);
283
347
  if (p.z > 1.0 || p.x < 0.0 || p.x > 1.0 || p.y < 0.0 || p.y > 1.0) return vec3(1.0);
@@ -295,8 +359,18 @@ vec3 sunGlassLookup(float directionalNdl) {
295
359
 
296
360
  float bias = 0.14 / uShadowDepthSpan;
297
361
  float texel = 1.35 / uShadowMapSize;
298
- vec3 staticTint = sunGlassLayer(p, ${SUN_GLASS_STATIC_LAYER}.0, 0.0, bias, texel, uvPerMetre);
299
- vec3 movingTint = sunGlassLayer(p, ${SUN_GLASS_MOVING_LAYER}.0, 1.0, bias, texel, uvPerMetre);
362
+ vec3 staticTint = sunGlassLayer(p, ${SUN_GLASS_STATIC_LAYER}.0, 0.0, bias, texel, uvPerMetre, uShadowDepthSpan);
363
+ vec3 movingTint = MOVING_SUN && sunMapFade(pm) <= 0.0
364
+ ? vec3(1.0)
365
+ : sunGlassLayer(
366
+ pm,
367
+ ${SUN_GLASS_MOVING_LAYER}.0,
368
+ 1.0,
369
+ MOVING_SUN ? 0.14 / movingSunSpan() : bias,
370
+ texel,
371
+ movingPerMetre,
372
+ MOVING_SUN ? movingSunSpan() : uShadowDepthSpan
373
+ );
300
374
  return mix(vec3(1.0), staticTint * movingTint, strength);
301
375
  }
302
376
 
@@ -36,7 +36,10 @@ export type LitSwitch =
36
36
  | 'DRIFT_LIGHT'
37
37
  | 'PHYSICAL_SPECULAR'
38
38
  | 'SURFACE_OVERLAY'
39
- | 'REFLECTION_MAPS';
39
+ | 'REFLECTION_MAPS'
40
+ | 'WORLD_UVS'
41
+ | 'MOVING_SUN'
42
+ | 'LAYERED';
40
43
 
41
44
  /** Every switch off: what a renderer starts from before its profile and its content say otherwise. */
42
45
  export function noLitSwitches(): Record<LitSwitch, boolean> {
@@ -49,6 +52,9 @@ export function noLitSwitches(): Record<LitSwitch, boolean> {
49
52
  PHYSICAL_SPECULAR: false,
50
53
  SURFACE_OVERLAY: false,
51
54
  REFLECTION_MAPS: false,
55
+ WORLD_UVS: false,
56
+ MOVING_SUN: false,
57
+ LAYERED: false,
52
58
  };
53
59
  }
54
60
 
@@ -21,7 +21,12 @@ import { DRIFT_LIGHT_GLSL } from './driftLight.ts';
21
21
  import { SURFACE_EFFECTS_GLSL } from './surfaceEffects.ts';
22
22
  import { overlayGlsl } from './overlay.ts';
23
23
  import { reflectionSurfaceGlsl } from './reflectionSurface.ts';
24
- import { CHANNEL_ATTRIBUTE, CHANNEL_BEND } from '../vertexChannel.ts';
24
+ import { worldUvGlsl } from './worldUv.ts';
25
+ import { movingSunGlsl } from './movingSun.ts';
26
+ import { layeredGlsl } from './layered.ts';
27
+ import { MAP_READS_GLSL } from './mapReads.ts';
28
+ import { CHANNEL_BEND, LIT_CHANNEL_ATTRIBUTE } from '../vertexChannel.ts';
29
+ import { BONE_ANIMATION_GLSL } from '../boneAnimation.ts';
25
30
  import { FULL_LIGHT_BUDGET, type LightBudget } from '../../uniformVectorBudget.ts';
26
31
 
27
32
  /**
@@ -56,10 +61,16 @@ layout(location = 10) in vec4 aTangent;
56
61
  ${SKINNING_GLSL}
57
62
  #endif
58
63
 
59
- uniform mat4 uViewProj;
64
+ uniform mat4 uViewProj; // wgsl:view
60
65
  /** 1 when the mesh carries a tangent frame. See vHasTangents. */
61
66
  uniform int uHasTangents;
62
- uniform mat4 uLightViewProj;
67
+ uniform mat4 uLightViewProj; // wgsl:view
68
+ /**
69
+ * The scene's clock, what \`setAnimationTime\` set: what a bone animation's instances read
70
+ * their moment from. In the view block, before the wind, so every variant's view block is the
71
+ * plain one's or a prefix of it — the instanced variant declares no wind and keeps this.
72
+ */
73
+ uniform float uSceneTime; // wgsl:view
63
74
  /**
64
75
  * How often the surface texture repeats across this mesh's UV range, per axis.
65
76
  *
@@ -122,6 +133,9 @@ layout(location = 15) in vec4 aInstanceTint;
122
133
  * opacity of one. See lightmap.ts.
123
134
  */
124
135
  const bool LIGHTMAP_REGIONS = false; // wgsl:override
136
+ #if ANIMATED
137
+ ${BONE_ANIMATION_GLSL}
138
+ #endif
125
139
  #else
126
140
  uniform mat4 uModel;
127
141
  /**
@@ -147,7 +161,7 @@ uniform vec3 uTint;
147
161
  * agreeing about where the shared uniforms sit: the instanced variant drops these five along with
148
162
  * the two above it, and a dropped field moves nothing. flatPass.ts asserts exactly that.
149
163
  */
150
- ${CHANNEL_ATTRIBUTE}
164
+ ${LIT_CHANNEL_ATTRIBUTE}
151
165
  ${CHANNEL_BEND}
152
166
  #endif
153
167
 
@@ -245,6 +259,10 @@ void main() {
245
259
  vec3 localNormal = aNormal;
246
260
  vec3 localTangent = aTangent.xyz;
247
261
  #endif
262
+ #if ANIMATED
263
+ /* The clip's turn and place, in the mesh's own space, before the instance places it. */
264
+ local = vec4(boneAnimate(local.xyz, localNormal, localTangent), 1.0);
265
+ #endif
248
266
  #if INSTANCED
249
267
  mat4 model = mat4(aInstanceModel0, aInstanceModel1, aInstanceModel2, aInstanceModel3);
250
268
  /*
@@ -379,6 +397,12 @@ export interface FlatVertexOptions {
379
397
  * Excludes `skinned`: the joint attributes and the instance matrix want the same locations.
380
398
  */
381
399
  readonly instanced: boolean;
400
+ /**
401
+ * Whether an instanced variant follows a bone animation: \`shaders/boneAnimation.ts\`. A
402
+ * variant rather than a pipeline constant, because it declares three textures the other
403
+ * instanced pipelines would otherwise have to bind stand-ins for. Instanced only.
404
+ */
405
+ readonly animated?: boolean;
382
406
  }
383
407
 
384
408
  /**
@@ -401,9 +425,17 @@ export function flatVert(options: FlatVertexOptions): string {
401
425
  'so every instance of the batch would wear one expression between them.',
402
426
  );
403
427
  }
428
+ if (options.animated === true && !options.instanced) {
429
+ throw new Error('flatVert: a bone animation is played by an instanced variant alone.');
430
+ }
404
431
  return resolveConditionals(
405
432
  FLAT_VERT_SOURCE,
406
- { SKINNED: options.skinned, MORPHED: options.morphed, INSTANCED: options.instanced },
433
+ {
434
+ SKINNED: options.skinned,
435
+ MORPHED: options.morphed,
436
+ INSTANCED: options.instanced,
437
+ ANIMATED: options.animated === true,
438
+ },
407
439
  'flatVert',
408
440
  );
409
441
  }
@@ -480,6 +512,21 @@ export interface FlatShaderOptions {
480
512
  * the frame's materials' reflections. True when absent. See `reflectionSurfaceGlsl`.
481
513
  */
482
514
  readonly reflectionMaps?: boolean;
515
+ /**
516
+ * Whether it places a material's maps by the world, `uWorldUv` and all (`SurfaceMaterial
517
+ * .projection`). True when absent. See `worldUvGlsl`.
518
+ */
519
+ readonly worldUvs?: boolean;
520
+ /**
521
+ * Whether it reads the sun's moving layer through a matrix of its own, \`uMovingLightViewProj\`
522
+ * and all. True when absent. See \`movingSunGlsl\`.
523
+ */
524
+ readonly movingSun?: boolean;
525
+ /**
526
+ * Whether it blends a material's layers by a mask, \`uLayers\` and all (\`SurfaceMaterial.layers\`).
527
+ * True when absent. See \`layeredGlsl\`.
528
+ */
529
+ readonly layered?: boolean;
483
530
  /**
484
531
  * How many point lights this build declares room for. `MAX_POINT_LIGHTS` when absent.
485
532
  *
@@ -591,6 +638,8 @@ export function flatFrag(options: FlatShaderOptions): string {
591
638
  surfaceEffects: options.surfaceEffects ?? true,
592
639
  driftLight: options.driftLight ?? true,
593
640
  }),
641
+ /* Depth zero and ahead of the sun's lookup, which calls it; its switch takes the next id. */
642
+ movingSunGlsl(options.movingSun ?? true),
594
643
  LOBES_GLSL,
595
644
  POINTSHADOW_GLSL,
596
645
  DIRECTIONALSHADOW_GLSL,
@@ -616,8 +665,6 @@ export function flatFrag(options: FlatShaderOptions): string {
616
665
  * and `main` is unconditional by construction.
617
666
  */
618
667
  TANGENT_FRAME_GLSL,
619
- /* After the frame it calls, unconditional for the same reason. See normalMap.ts. */
620
- NORMAL_MAP_GLSL,
621
668
  /* Unconditional for the same reason, and called only where a cutoff was asked for. */
622
669
  CUTOUT_COVERAGE_GLSL,
623
670
  CUTOUT_DITHER_GLSL,
@@ -629,6 +676,15 @@ export function flatFrag(options: FlatShaderOptions): string {
629
676
  overlayGlsl(options.surfaceOverlay ?? true),
630
677
  /* After the overlay, so its switch takes the next id. See reflectionSurface.ts. */
631
678
  reflectionSurfaceGlsl(options.reflectionMaps ?? true),
679
+ /* After the reflection maps, so its switch takes the next id; main calls it. worldUv.ts. */
680
+ worldUvGlsl(options.worldUvs ?? true),
681
+ /* After the projection, so its switch takes the next id; the normal map below calls it. */
682
+ layeredGlsl(options.layered ?? true),
683
+ /* Where each map is read, after both of the ways it can be read otherwise. See mapReads.ts. */
684
+ MAP_READS_GLSL,
685
+ /* After the frame and the projection it calls, unconditional for the same reason as the
686
+ frame. See normalMap.ts. */
687
+ NORMAL_MAP_GLSL,
632
688
  /* After the models, whose answer to a rectangle it asks for. See areaLight.ts. */
633
689
  AREA_LIGHT_GLSL,
634
690
  MAIN_GLSL,
@@ -0,0 +1,115 @@
1
+ /**
2
+ * A material blended from layers by a mask: `SurfaceMaterial.layers`, packed into `uLayers` by
3
+ * `surfaceLayers.ts`.
4
+ *
5
+ * **Layer `i` is layer `i` of the material's arrays**, each read at the mesh's coordinates times its
6
+ * own repeat; the mask is layer 0 of the map a shading model would read, at the mesh's coordinates.
7
+ * Each channel lays a layer over everything before it — red layer 1, green 2, blue 3, alpha 4 — so a
8
+ * layer's share is its channel times one less each later channel, and the base keeps what none of
9
+ * them took. Colour, roughness and occlusion are blended by those shares, and the normal map's
10
+ * tangent-space texels too, before the frame turns them into the world.
11
+ *
12
+ * **Every layer present is read, whatever its share.** A read whose level is implicit is legal only
13
+ * in uniform control flow (AGENTS.md 2026-08-07), so the loop is bounded by the layer count, a
14
+ * uniform, and never skips on a share, which is the pixel's own.
15
+ *
16
+ * **A lit switch, `LAYERED`**, off until a material first asks, declared last so every switch before
17
+ * it keeps its id. Compiled out, its two vectors are not declared and its calls are constants.
18
+ */
19
+ export function layeredGlsl(on: boolean): string {
20
+ if (!on) {
21
+ return /* glsl */ `
22
+ const bool LAYERED = false; // wgsl:override
23
+ bool layered() { return false; }
24
+ void layeredBegin() {}
25
+ vec4 layeredAlbedo() { return vec4(1.0); }
26
+ vec3 layeredOrm() { return vec3(1.0); }
27
+ vec3 layeredTexel() { return vec3(0.0, 0.0, 1.0); }
28
+ bool layeredGlows() { return false; }
29
+ vec3 layeredEmissive() { return vec3(0.0); }
30
+ `;
31
+ }
32
+ return /* glsl */ `
33
+ /* Whether this program blends a material's layers. Off but where one has asked: layered.ts. */
34
+ const bool LAYERED = true; // wgsl:override
35
+ /* The five repeats, base first (x y z w, then x), the count (y) and the glowing layer, or -1 (z). */
36
+ uniform vec4 uLayers[2]; // wgsl:material
37
+
38
+ /* The mask's channels that lay a layer, zero past the count: read once, in layeredBegin. */
39
+ vec4 lMask;
40
+
41
+ bool layered() {
42
+ return uLayers[1].y > 0.5;
43
+ }
44
+
45
+ /* The mask, at the mesh's coordinates, where a model's map is. Under a branch on uniforms. */
46
+ void layeredBegin() {
47
+ vec4 m = texture(uModelMap, vec3(vUv.xy, 0.0));
48
+ float n = uLayers[1].y;
49
+ lMask = vec4(n > 1.5 ? m.r : 0.0, n > 2.5 ? m.g : 0.0, n > 3.5 ? m.b : 0.0, n > 4.5 ? m.a : 0.0);
50
+ }
51
+
52
+ /* Layer i's share of this point: its channel, less every later layer's. */
53
+ float layerShare(int i) {
54
+ float keep = 1.0;
55
+ for (int j = 3; j >= 0; j--) {
56
+ if (j + 1 == i) return lMask[j] * keep;
57
+ keep *= 1.0 - lMask[j];
58
+ }
59
+ return keep;
60
+ }
61
+
62
+ float layerRepeat(int i) {
63
+ return i < 4 ? uLayers[0][i] : uLayers[1].x;
64
+ }
65
+
66
+ vec3 layerAt(int i) {
67
+ return vec3(vUv.xy * layerRepeat(i), float(i));
68
+ }
69
+
70
+ /* Colour by the shares, and the base layer's coverage, which a cutout reads. */
71
+ vec4 layeredAlbedo() {
72
+ vec4 sum = vec4(0.0);
73
+ for (int i = 0; i < 5; i++) {
74
+ if (float(i) >= uLayers[1].y) break;
75
+ vec4 t = texture(uAlbedo, layerAt(i));
76
+ sum.rgb += t.rgb * layerShare(i);
77
+ if (i == 0) sum.a = t.a;
78
+ }
79
+ return sum;
80
+ }
81
+
82
+ vec3 layeredOrm() {
83
+ vec3 sum = vec3(0.0);
84
+ for (int i = 0; i < 5; i++) {
85
+ if (float(i) >= uLayers[1].y) break;
86
+ sum += texture(uOrmMap, layerAt(i)).rgb * layerShare(i);
87
+ }
88
+ return sum;
89
+ }
90
+
91
+ /* The tangent-space texels blended, each with its z rebuilt where a two-channel map stored none. */
92
+ vec3 layeredTexel() {
93
+ vec3 sum = vec3(0.0);
94
+ for (int i = 0; i < 5; i++) {
95
+ if (float(i) >= uLayers[1].y) break;
96
+ vec3 t = texture(uNormalMap, layerAt(i)).xyz * 2.0 - 1.0;
97
+ t.z = t.z > 0.0 ? t.z : sqrt(max(0.0, 1.0 - dot(t.xy, t.xy)));
98
+ sum += t * layerShare(i);
99
+ }
100
+ float len = length(sum);
101
+ return len > 1e-5 ? sum / len : vec3(0.0, 0.0, 1.0);
102
+ }
103
+
104
+ /* Whether the emissive map belongs to one layer rather than to the whole surface. */
105
+ bool layeredGlows() {
106
+ return uLayers[1].z > -0.5;
107
+ }
108
+
109
+ /* The emissive map at its layer's place, glowing by that layer's share. */
110
+ vec3 layeredEmissive() {
111
+ int glow = int(uLayers[1].z + 0.5);
112
+ return texture(uEmissiveMap, layerAt(glow)).rgb * layerShare(glow);
113
+ }
114
+ `;
115
+ }
@@ -7,6 +7,17 @@ export const MAIN_GLSL = `void main() {
7
7
  * turns a float into an array index. A plain texture is an array of one, so layer 0.
8
8
  */
9
9
  vec3 surfaceAt = vec3(vUv.xy, floor(vUv.z + 0.5));
10
+ /*
11
+ * The coordinates a map's frame and its cutout's level are measured along: the mesh's, or the
12
+ * world's where the material projects its maps (worldUv.ts) — in which case the mesh's tangents
13
+ * no longer run along them either. Triplanar reads its own three planes below; every other
14
+ * reader of surfaceAt takes the horizontal plane.
15
+ */
16
+ vec2 frameAt = vUv.xy;
17
+ int frameTangents = vHasTangents;
18
+ projectSurface(surfaceAt, frameAt, frameTangents);
19
+ /* A layered material's mask, read once for every layer's share below: layered.ts. */
20
+ if (LAYERED && layered()) layeredBegin();
10
21
  if (dot(vec4(vWorldPos, 1.0), uClipPlane) < 0.0) discard;
11
22
  /*
12
23
  * The layer's surface effects, zeros where the albedo array carries no table — see
@@ -26,6 +37,8 @@ export const MAIN_GLSL = `void main() {
26
37
  vec2 fxDx = dFdx(surfaceAt.xy);
27
38
  vec2 fxDy = dFdy(surfaceAt.xy);
28
39
  if (uWriteMode.y != 0.0 && !ditherKeeps(uWriteMode.y)) discard;
40
+ /* An opacity spent on the screen door instead of the blend: preamble.ts. 0 keeps nothing. */
41
+ if (uWriteMode.z != 0.0 && ditherThreshold() >= vAlpha) discard;
29
42
  /* A draw's dissolve cuts the surface away before anything is shaded: overlay.ts. */
30
43
  if (SURFACE_OVERLAY) {
31
44
  if (overlayCuts(surfaceAt.xy)) discard;
@@ -63,14 +76,14 @@ export const MAIN_GLSL = `void main() {
63
76
  */
64
77
  float coverage = 1.0;
65
78
  if (uMaterialFlags.x != 0) {
66
- vec4 texel = texture(uAlbedo, surfaceAt);
79
+ vec4 texel = albedoTexel(surfaceAt);
67
80
  // Discarded before anything else is computed: a thrown-away fragment should not pay
68
81
  // for the lighting it will never contribute to. The test credits alpha for the mip level
69
82
  // it samples, so a needle averaged thin down the chain is not lost; see cutoutCoverage.ts.
70
83
  // Behind a branch on a uniform, so a material with no cutoff pays nothing for it.
71
84
  float kept = texel.a;
72
85
  if (uCutout.x > 0.0) {
73
- float tested = cutoutAlpha(texel.a, vUv.xy * vec2(textureSize(uAlbedo, 0).xy));
86
+ float tested = cutoutAlpha(texel.a, frameAt * vec2(textureSize(uAlbedo, 0).xy));
74
87
  if (uCutout.y == 0.0) {
75
88
  if (tested < uCutout.x) discard;
76
89
  } else {
@@ -82,7 +95,7 @@ export const MAIN_GLSL = `void main() {
82
95
  */
83
96
  float share = cutoutShare(tested, uCutout.x);
84
97
  if (uCutout.y == 1.0) {
85
- if (!cutoutKeeps(share, gl_FragCoord.xy, uCutout.z)) discard;
98
+ if (!cutoutKeeps(share, gl_FragCoord.xy, uHighlightMin.w)) discard;
86
99
  kept = 1.0;
87
100
  } else {
88
101
  kept = share;
@@ -115,7 +128,7 @@ export const MAIN_GLSL = `void main() {
115
128
  float ormRoughness = vRoughness;
116
129
  float metal = 0.0;
117
130
  if (uMaterialFlags.y != 0) {
118
- vec3 t = texture(uOrmMap, surfaceAt).rgb;
131
+ vec3 t = ormTexel(surfaceAt);
119
132
  ormOcclusion = mix(1.0, t.r, uOrmScale.r);
120
133
  ormRoughness = clamp(t.g * uOrmScale.g, 0.0, 1.0);
121
134
  metal = clamp(t.b * uOrmScale.b, 0.0, 1.0);
@@ -156,6 +169,14 @@ export const MAIN_GLSL = `void main() {
156
169
  vec3 glassGlow = vec3(0.0);
157
170
  float glassTransmission = uSeeThrough.z;
158
171
  float glassFrost = uSeeThrough.w;
172
+ /*
173
+ * **A thin surface lets light from behind through, coloured by itself**: a banner, a leaf, a
174
+ * lampshade, an opaque material's \`diffuseTransmission\`, in the glass tint's spare lane. It
175
+ * gathers the light behind it in glassGlow, as a pane does, and adds it through its own colour.
176
+ * None on glass, which has its own way through.
177
+ */
178
+ float thin = glassTransmission > 0.0 ? 0.0 : uGlassTint.w;
179
+ bool seesBehind = glassTransmission > 0.0 || thin > 0.0;
159
180
 
160
181
  /*
161
182
  * The normal a refracting draw bends by: the shading normal, a normal map's tilt and every relief
@@ -197,7 +218,7 @@ export const MAIN_GLSL = `void main() {
197
218
  * below. The rule is about non-uniform branches, and this branch is on a uniform.
198
219
  */
199
220
  vec3 unmapped = n;
200
- if (uNormalStrength > 0.0) n = normalMapped(n, surfaceAt);
221
+ if (uNormalStrength > 0.0) n = normalMapped(n, surfaceAt, frameAt, frameTangents);
201
222
  /* A draw's wrinkles, by region, over whatever the map made: overlay.ts. */
202
223
  if (SURFACE_OVERLAY) n = overlayWrinkle(n, unmapped, surfaceAt.xy);
203
224
 
@@ -368,6 +389,8 @@ export const MAIN_GLSL = `void main() {
368
389
  */
369
390
  shadingNormal = n;
370
391
  if (backFace) n = -n;
392
+ /* A blending rim, on the base colour every light below reads: overlay.ts. */
393
+ if (SURFACE_OVERLAY) albedo = overlayRimBase(albedo, surfaceAt.xy, n);
371
394
  /*
372
395
  * A model's surface, gathered once, on a branch on the switches alone (models.ts) — and whether
373
396
  * there is one, asked once: every site below is a branch on these two, which a pipeline with no
@@ -450,7 +473,7 @@ export const MAIN_GLSL = `void main() {
450
473
  float sunFacing = smoothstep(0.0, 0.02, surfaceNdl);
451
474
  float direct = ndl * sunShade * sunFacing;
452
475
  /* Glass: the sun on the far side of a pane lights it through the glass. See glassGlow. */
453
- if (glassTransmission > 0.0) glassGlow += sunColor * max(-surfaceNdl, 0.0) * sunShade;
476
+ if (seesBehind) glassGlow += sunColor * max(-surfaceNdl, 0.0) * sunShade;
454
477
 
455
478
  /*
456
479
  * Hemispheric: sky above, ground below, mixed by which way the surface looks. An
@@ -1281,7 +1304,7 @@ export const MAIN_GLSL = `void main() {
1281
1304
  */
1282
1305
  float falloff;
1283
1306
  /*
1284
- * **A light may name its own falloff, and then it is Unreal's for a light without
1307
+ * **A light may name its own falloff, and then it is the usual one for a light without
1285
1308
  * inverse-square falloff**: \`(1 - (d/R)^2)^n\` with no distance term, so a fill authored to
1286
1309
  * light a whole hall across a hundred metres does, where the inverse square would have it
1287
1310
  * gone a few metres out. Zero is the frame's own rule below, which every light was before.
@@ -1302,8 +1325,7 @@ export const MAIN_GLSL = `void main() {
1302
1325
  * the glass, shaped and shadowed like any lamp, and is added to glassGlow once its shape is
1303
1326
  * known below. For everything else a lamp behind the surface is skipped here, as always.
1304
1327
  */
1305
- float backNdl =
1306
- glassTransmission > 0.0 ? max(-dot(n, toLight / max(dist, 1e-4)), 0.0) : 0.0;
1328
+ float backNdl = seesBehind ? max(-dot(n, toLight / max(dist, 1e-4)), 0.0) : 0.0;
1307
1329
  /* A model that sees light from behind keeps the lamps behind the surface. See models.ts. */
1308
1330
  if (ndl <= 0.0 && backNdl <= 0.0 && !modelledBehind) continue;
1309
1331
 
@@ -1323,7 +1345,9 @@ export const MAIN_GLSL = `void main() {
1323
1345
  #if POINT_SHADOWS
1324
1346
  float grazing = 1.0 - ndl;
1325
1347
  float normalOffset = (0.06 + dist * 0.05) * grazing * grazing + 0.02;
1326
- vec3 receiverPoint = vWorldPos + n * normalOffset;
1348
+ /* Toward a lamp behind a thin surface, which is in the map itself: away from it, the surface
1349
+ would stand in its own shadow. A pane is in no map, so glass keeps the usual side. */
1350
+ vec3 receiverPoint = vWorldPos + (thin > 0.0 && ndl <= 0.0 ? -n : n) * normalOffset;
1327
1351
 
1328
1352
  /*
1329
1353
  * uPointShadowWeight is how present the cubemap is, and it is the difference
@@ -1699,7 +1723,7 @@ export const MAIN_GLSL = `void main() {
1699
1723
  float signedForm = areaSignedForm(n, seenCentre, right, up, seenHalf);
1700
1724
  float form = mix(max(0.0, signedForm), abs(signedForm), uAreaLightTwoSided[a]);
1701
1725
  /* The form factor is linear in the normal, so the one seen from behind is its negation. */
1702
- float backForm = glassTransmission > 0.0
1726
+ float backForm = seesBehind
1703
1727
  ? mix(max(0.0, -signedForm), abs(signedForm), uAreaLightTwoSided[a])
1704
1728
  : 0.0;
1705
1729
  if (form <= 0.0 && backForm <= 0.0) continue;
@@ -1873,7 +1897,7 @@ export const MAIN_GLSL = `void main() {
1873
1897
  * \`driftLight.ts\`.
1874
1898
  */
1875
1899
  if (driftShare > 0.0) {
1876
- if (glassTransmission > 0.0) {
1900
+ if (seesBehind) {
1877
1901
  glassGlow += driftLightIrradiance(vWorldPos, -n) * (driftShare * uDriftLight.w);
1878
1902
  }
1879
1903
  /* The standard expression in its own order — albedo times the field, then the share — so a
@@ -1933,6 +1957,8 @@ export const MAIN_GLSL = `void main() {
1933
1957
  * underneath: a ceiling emitting a dull warm haze over a pale panel cannot be expressed
1934
1958
  * by scaling the panel, and scaling it gets the brightness right and the hue wrong.
1935
1959
  */
1960
+ /* What a thin surface lets through from behind, through its own colour; a metal lets none. */
1961
+ if (thin > 0.0) lit += albedo * glassGlow * (thin * (1.0 - metal));
1936
1962
  diffuseAlone = lit;
1937
1963
  vec3 emissiveTint = vEmissiveColor.r < 0.0 ? albedo : vEmissiveColor;
1938
1964
  /*
@@ -1953,7 +1979,7 @@ export const MAIN_GLSL = `void main() {
1953
1979
  */
1954
1980
  vec3 emissiveMapped = vec3(1.0);
1955
1981
  if (uMaterialFlags.z != 0) {
1956
- emissiveMapped = texture(uEmissiveMap, surfaceAt).rgb * uEmissiveScale;
1982
+ emissiveMapped = emissiveTexel(surfaceAt) * uEmissiveScale;
1957
1983
  }
1958
1984
  /* Pulse, flicker and fade from the layer's effects; exactly 1 where it names none. */
1959
1985
  float fxGlow = fx4.z + fx5.x + fx5.w > 0.0 ? fxEmission(surfaceAt.z) : 1.0;
@@ -1997,7 +2023,7 @@ export const MAIN_GLSL = `void main() {
1997
2023
  // finishing a run reads the same at noon and at midnight. Costs one compare
1998
2024
  // per fragment and no extra draw call — the box is a uniform, not geometry.
1999
2025
  if (uHighlightMax.w > 0.0) {
2000
- vec3 inside = step(uHighlightMin, vWorldPos) * step(vWorldPos, uHighlightMax.xyz);
2026
+ vec3 inside = step(uHighlightMin.xyz, vWorldPos) * step(vWorldPos, uHighlightMax.xyz);
2001
2027
  lit += albedo * uHighlightMax.w * inside.x * inside.y * inside.z;
2002
2028
  }
2003
2029
  }
@@ -2097,11 +2123,21 @@ export const MAIN_GLSL = `void main() {
2097
2123
  * map here, on a branch on uniforms alone, which keeps its derivatives legal.
2098
2124
  */
2099
2125
  vec3 refractN = shadingNormal;
2100
- if (uLightingEnabled == 0 && uNormalStrength > 0.0) refractN = normalMapped(refractN, surfaceAt);
2126
+ if (uLightingEnabled == 0 && uNormalStrength > 0.0) refractN = normalMapped(refractN, surfaceAt, frameAt, frameTangents);
2101
2127
  vec3 refractV = normalize(uCameraPos - vWorldPos);
2102
2128
  ivec2 snapSize = textureSize(uRefractScene, 0);
2103
2129
  vec2 screenUv = gl_FragCoord.xy / vec2(snapSize);
2104
- vec2 bent = clamp(screenUv + refractN.xy * uSeeThrough.x, vec2(0.0), vec2(1.0));
2130
+ /*
2131
+ * **The normal's sideways part as the eye sees it**: the shading normal in view space, its x
2132
+ * and y across the screen, y turned to the way this frame's rows run (w of the tint). It was the
2133
+ * normal's world x and y until 4.10.0, which on a quad facing the camera is the quad's own lean
2134
+ * against the world's axes: one offset over the whole quad, a lens, with a normal map's tilt a
2135
+ * small change on top. In view space a quad facing the eye bends by nothing but its map, which
2136
+ * is a shimmer, and a pane bends by how it leans toward the eye wherever the eye stands.
2137
+ */
2138
+ vec3 seenN = mat3(uView) * refractN;
2139
+ vec2 bend = vec2(seenN.x, seenN.y * uRefractTint.w);
2140
+ vec2 bent = clamp(screenUv + bend * uSeeThrough.x, vec2(0.0), vec2(1.0));
2105
2141
  /*
2106
2142
  * **An explicit level, and frost is what picks it** — the 2026-08-07 rule, and the reason a
2107
2143
  * frosted pane blurs rather than fades: the copy carries a mip chain on a frame that draws glass,
@@ -2115,7 +2151,7 @@ export const MAIN_GLSL = `void main() {
2115
2151
  /* Unsigned: a pane passes the same light seen from either side, and so does its Fresnel. */
2116
2152
  float cosView = max(abs(dot(refractN, refractV)), 0.05);
2117
2153
  float pathLength = uSeeThrough.y * vThickness / cosView;
2118
- vec3 seen = behind * pow(uRefractTint, vec3(pathLength));
2154
+ vec3 seen = behind * pow(uRefractTint.rgb, vec3(pathLength));
2119
2155
  if (glassTransmission > 0.0) {
2120
2156
  /*
2121
2157
  * **Glass keeps its own surface and shows what is behind over it**, by how much it lets
@@ -2124,8 +2160,8 @@ export const MAIN_GLSL = `void main() {
2124
2160
  * The glow of the lights behind it is added last, by how frosted it is.
2125
2161
  */
2126
2162
  float fresnel = 0.04 + 0.96 * pow(1.0 - cosView, 5.0);
2127
- shaded = mix(shaded, seen * uGlassTint, glassTransmission * (1.0 - fresnel)) +
2128
- applyOutputTransform(glassGlow * uGlassTint * (glassFrost * glassTransmission) * (1.0 - fog));
2163
+ shaded = mix(shaded, seen * uGlassTint.rgb, glassTransmission * (1.0 - fresnel)) +
2164
+ applyOutputTransform(glassGlow * uGlassTint.rgb * (glassFrost * glassTransmission) * (1.0 - fog));
2129
2165
  } else {
2130
2166
  shaded = seen;
2131
2167
  }
@@ -2137,9 +2173,10 @@ export const MAIN_GLSL = `void main() {
2137
2173
  * **It did not need MeshData.colors widened to four floats**, which is how the consumer who
2138
2174
  * asked for it had costed the change -- widening colors moves every mesh producer, the
2139
2175
  * container layout and every baked asset. Alpha is a factor on the draw's opacity, not a
2140
- * component of its albedo, so it rides a lane instead and nothing existing moves.
2176
+ * component of its albedo, so it rides a lane instead and nothing existing moves. Where the draw
2177
+ * spends it on the screen door instead (\`uWriteMode.z\`), it was spent at the top and is 1 here.
2141
2178
  */
2142
- float alpha = uOpacity * coverage * vAlpha;
2179
+ float alpha = uOpacity * coverage * (uWriteMode.z != 0.0 ? 1.0 : vAlpha);
2143
2180
 
2144
2181
  /*
2145
2182
  * **Order-independent transparency, when this draw is accumulating into it.**