@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
@@ -5,6 +5,7 @@ import { CUTOUT_DITHER_GLSL } from '../cutoutDither.js';
5
5
  import { SKINNING_GLSL } from './skinning.js';
6
6
  import { CLOTH_BINDING_GLSL } from './clothBinding.js';
7
7
  import { CHANNEL_ATTRIBUTE, CHANNEL_BEND } from './vertexChannel.js';
8
+ import { BONE_ANIMATION_GLSL } from './boneAnimation.js';
8
9
  /**
9
10
  * Depth-only shadow pass. Its optional peel mode rejects the first recorded
10
11
  * surface so the next independently fading occluder can be retained.
@@ -77,6 +78,17 @@ uniform vec2 uUvOffset;
77
78
  uniform vec2 uAlphaCutout;
78
79
  #endif
79
80
 
81
+ #if ANIMATED
82
+ /*
83
+ * **A bone animation's, after every other declaration** so no field of the variants before it
84
+ * moves: the grain lane the vertex's bone rides in, the scene's clock, and the chunk the colour
85
+ * pass runs — the same expression, or a crowd's shadow stands still while the crowd moves.
86
+ */
87
+ layout(location = 8) in float aGrain;
88
+ uniform float uSceneTime;
89
+ ${BONE_ANIMATION_GLSL}
90
+ #endif
91
+
80
92
  out vec4 vLightPosition;
81
93
  #if CUTOUT
82
94
  /* The texture coordinate and, in z, the texture-array layer the face wears. */
@@ -98,6 +110,12 @@ void main() {
98
110
  #else
99
111
  vec4 local = vec4(aPosition, 1.0);
100
112
  #endif
113
+ #if ANIMATED
114
+ /* Turned and placed as the colour pass turns and places it; its normal and tangent unused. */
115
+ vec3 animatedNormal = vec3(0.0, 1.0, 0.0);
116
+ vec3 animatedTangent = vec3(1.0, 0.0, 0.0);
117
+ local = vec4(boneAnimate(local.xyz, animatedNormal, animatedTangent), 1.0);
118
+ #endif
101
119
  #if INSTANCED
102
120
  mat4 model = mat4(aInstanceModel0, aInstanceModel1, aInstanceModel2, aInstanceModel3);
103
121
  /* The bottom row is the instance's texture cell, rebuilt as the row it stands for: see flat. */
@@ -144,9 +162,9 @@ void main() {
144
162
  }
145
163
  `;
146
164
  /** The rigid variant: the world, a prop, a mesh with no rig behind it. */
147
- export const DEPTH_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: false, CUTOUT: false, GLASS: false }, 'depth');
165
+ export const DEPTH_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: false, CUTOUT: false, GLASS: false, ANIMATED: false }, 'depth');
148
166
  /** The skinned variant, which reads a joint palette and moves the vertex by it. */
149
- export const DEPTH_SKINNED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: true, INSTANCED: false, CUTOUT: false, GLASS: false }, 'depth-skinned');
167
+ export const DEPTH_SKINNED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: true, INSTANCED: false, CUTOUT: false, GLASS: false, ANIMATED: false }, 'depth-skinned');
150
168
  /**
151
169
  * The variant an instanced draw casts its shadow through.
152
170
  *
@@ -155,7 +173,7 @@ export const DEPTH_SKINNED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINN
155
173
  * right place and the rest stacked underneath it, which reads as a lighting fault rather than as
156
174
  * a missing variant, and is the shape the per-draw ring exists to prevent one level up.
157
175
  */
158
- export const DEPTH_INSTANCED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: true, CUTOUT: false, GLASS: false }, 'depth-instanced');
176
+ export const DEPTH_INSTANCED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: true, CUTOUT: false, GLASS: false, ANIMATED: false }, 'depth-instanced');
159
177
  /**
160
178
  * The variants a cutout caster casts through: a leaf card, a chain link, a fence, whose shape is in
161
179
  * its texture's alpha rather than in its geometry.
@@ -166,8 +184,15 @@ export const DEPTH_INSTANCED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKI
166
184
  * leaves every opaque caster byte for byte as it was. What it costs is one more program each, and
167
185
  * a skinned cutout, which nothing has asked for, is not among them.
168
186
  */
169
- export const DEPTH_CUTOUT_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: false, CUTOUT: true, GLASS: false }, 'depth-cutout');
170
- export const DEPTH_INSTANCED_CUTOUT_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: true, CUTOUT: true, GLASS: false }, 'depth-instanced-cutout');
187
+ export const DEPTH_CUTOUT_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: false, CUTOUT: true, GLASS: false, ANIMATED: false }, 'depth-cutout');
188
+ export const DEPTH_INSTANCED_CUTOUT_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: true, CUTOUT: true, GLASS: false, ANIMATED: false }, 'depth-instanced-cutout');
189
+ /**
190
+ * The variants a crowd casts through: an instanced batch playing a bone animation, its vertices
191
+ * where the clip puts them at each instance's moment. See `shaders/boneAnimation.ts`. No glass
192
+ * variant: a crowd is not glass, and a glass batch that animates casts its tint from rest.
193
+ */
194
+ export const DEPTH_INSTANCED_ANIMATED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: true, CUTOUT: false, GLASS: false, ANIMATED: true }, 'depth-instanced-animated');
195
+ export const DEPTH_INSTANCED_ANIMATED_CUTOUT_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: true, CUTOUT: true, GLASS: false, ANIMATED: true }, 'depth-instanced-animated-cutout');
171
196
  export const DEPTH_FRAG = `#version 300 es
172
197
  precision highp float;
173
198
  in vec4 vLightPosition;
@@ -238,11 +263,11 @@ void main() {
238
263
  * exactly where it casts its depth, deformed and placed the same way, or the coloured patch parts
239
264
  * from the pane's own shadow edge. The only addition is `vGlassWorld`.
240
265
  */
241
- export const GLASS_TINT_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: false, CUTOUT: false, GLASS: true }, 'glass-tint');
242
- export const GLASS_TINT_SKINNED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: true, INSTANCED: false, CUTOUT: false, GLASS: true }, 'glass-tint-skinned');
243
- export const GLASS_TINT_INSTANCED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: true, CUTOUT: false, GLASS: true }, 'glass-tint-instanced');
244
- export const GLASS_TINT_CUTOUT_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: false, CUTOUT: true, GLASS: true }, 'glass-tint-cutout');
245
- export const GLASS_TINT_INSTANCED_CUTOUT_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: true, CUTOUT: true, GLASS: true }, 'glass-tint-instanced-cutout');
266
+ export const GLASS_TINT_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: false, CUTOUT: false, GLASS: true, ANIMATED: false }, 'glass-tint');
267
+ export const GLASS_TINT_SKINNED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: true, INSTANCED: false, CUTOUT: false, GLASS: true, ANIMATED: false }, 'glass-tint-skinned');
268
+ export const GLASS_TINT_INSTANCED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: true, CUTOUT: false, GLASS: true, ANIMATED: false }, 'glass-tint-instanced');
269
+ export const GLASS_TINT_CUTOUT_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: false, CUTOUT: true, GLASS: true, ANIMATED: false }, 'glass-tint-cutout');
270
+ export const GLASS_TINT_INSTANCED_CUTOUT_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: true, CUTOUT: true, GLASS: true, ANIMATED: false }, 'glass-tint-instanced-cutout');
246
271
  /**
247
272
  * What a pane lets through: its transmission and colour, less what it reflects toward the light,
248
273
  * and how clear it is — `glassShadow.ts`'s `paneTexel`, stated for the device. Drawn with a
@@ -29,13 +29,56 @@ export const DIRECTIONALSHADOW_GLSL = `vec2 receiverPlaneDepthGradient(vec3 p) {
29
29
  );
30
30
  }
31
31
 
32
- float directionalVisibility(float receiverDepth, float compareDepth, float storedDepth) {
32
+ /* One stored depth against a receiver, in a map spanning \`span\` metres of depth. */
33
+ float directionalVisibilityAt(float receiverDepth, float compareDepth, float storedDepth, float span) {
33
34
  if (compareDepth <= storedDepth) return 1.0;
34
- float rayDistance = max(receiverDepth - storedDepth, 0.0) * uShadowDepthSpan;
35
+ float rayDistance = max(receiverDepth - storedDepth, 0.0) * span;
35
36
  float groundDistance = rayDistance * length(uDirectionalDir.xz);
36
37
  return 1.0 - shadowReach(groundDistance, uShadowMaxDistance);
37
38
  }
38
39
 
40
+ float directionalVisibility(float receiverDepth, float compareDepth, float storedDepth) {
41
+ return directionalVisibilityAt(receiverDepth, compareDepth, storedDepth, uShadowDepthSpan);
42
+ }
43
+
44
+ /* How far inside its square a receiver stands in a layer's map: 1 well inside, 0 at its border. */
45
+ float sunMapFade(vec3 p) {
46
+ if (p.z > 1.0 || p.x < 0.0 || p.x > 1.0 || p.y < 0.0 || p.y > 1.0) return 0.0;
47
+ vec2 fromCentre = abs(p.xy - 0.5) * 2.0;
48
+ return (1.0 - smoothstep(0.72, 0.98, max(fromCentre.x, fromCentre.y))) *
49
+ (1.0 - smoothstep(0.90, 1.0, p.z));
50
+ }
51
+
52
+ /*
53
+ * The moving layer at its own place, where it has a matrix of its own (movingSun.ts): the coarse
54
+ * test of shadowFactor, and its filter, each against the moving map's own depth span, and lit by
55
+ * how far inside the moving map's square the receiver stands rather than cut at its border.
56
+ */
57
+ float movingSunCoarse(vec3 pm, float span) {
58
+ float inside = sunMapFade(pm);
59
+ if (inside <= 0.0) return 1.0;
60
+ float stored = textureLod(uSunShadows, vec3(pm.xy, ${SUN_DYNAMIC_LAYER}.0), 0.0).r;
61
+ return mix(1.0, directionalVisibilityAt(pm.z, pm.z - 1.0 / span, stored, span), inside);
62
+ }
63
+
64
+ float movingSunFiltered(vec3 pm, vec2 gradient, float span) {
65
+ float inside = sunMapFade(pm);
66
+ if (inside <= 0.0) return 1.0;
67
+ float texel = 1.35 / uShadowMapSize;
68
+ float slopeLimit = 9.1 / uShadowMapSize;
69
+ float lit = 0.0;
70
+ for (int i = 0; i < ${MAX_SHADOW_FILTER_TAPS}; i++) {
71
+ if (i >= uShadowFilterTaps) break;
72
+ vec2 sampleUv = pm.xy + DIRECTIONAL_PCF_OFFSETS[i] * texel;
73
+ vec2 texelCentre = (floor(sampleUv * uShadowMapSize) + 0.5) / uShadowMapSize;
74
+ float slopeOffset = clamp(dot(gradient, texelCentre - pm.xy), -slopeLimit, slopeLimit);
75
+ float tapReceiverDepth = pm.z + slopeOffset;
76
+ float stored = textureLod(uSunShadows, vec3(sampleUv, ${SUN_DYNAMIC_LAYER}.0), 0.0).r;
77
+ lit += directionalVisibilityAt(tapReceiverDepth, tapReceiverDepth - 0.14 / span, stored, span);
78
+ }
79
+ return mix(1.0, lit / float(max(uShadowFilterTaps, 1)), inside);
80
+ }
81
+
39
82
  float shadowFactor(float directionalNdl) {
40
83
  if (uShadowStrength <= 0.0) return 1.0;
41
84
 
@@ -65,6 +108,14 @@ float shadowFactor(float directionalNdl) {
65
108
  vec3 p = vLightPos.xyz / vLightPos.w;
66
109
  p = p * 0.5 + 0.5;
67
110
  vec2 depthGradient = receiverPlaneDepthGradient(p);
111
+ /* The moving layer's own place and plane where it has a matrix of its own (movingSun.ts), taken
112
+ here beside the static layer's for the same reason: above every varying-dependent return. */
113
+ vec3 pm = p;
114
+ vec2 movingGradient = depthGradient;
115
+ if (MOVING_SUN) {
116
+ pm = movingSunAt();
117
+ movingGradient = receiverPlaneDepthGradient(pm);
118
+ }
68
119
 
69
120
  // Near a surface's own light terminator its direct-light term is already
70
121
  // approaching zero, while the orthographic projection of that surface
@@ -129,7 +180,8 @@ float shadowFactor(float directionalNdl) {
129
180
  float coarseCompare = p.z - 1.0 / uShadowDepthSpan;
130
181
  float coarse =
131
182
  directionalVisibility(p.z, coarseCompare, textureLod(uSunShadows, vec3(p.xy, ${SUN_STATIC_LAYER}.0), 0.0).r) *
132
- directionalVisibility(p.z, coarseCompare, textureLod(uSunShadows, vec3(p.xy, ${SUN_DYNAMIC_LAYER}.0), 0.0).r);
183
+ (MOVING_SUN ? movingSunCoarse(pm, movingSunSpan()) :
184
+ directionalVisibility(p.z, coarseCompare, textureLod(uSunShadows, vec3(p.xy, ${SUN_DYNAMIC_LAYER}.0), 0.0).r));
133
185
  if (uPeeledShadowEnabled != 0) {
134
186
  coarse *= directionalVisibility(p.z, coarseCompare, textureLod(uSunShadows, vec3(p.xy, ${SUN_PEELED_LAYER}.0), 0.0).r);
135
187
  }
@@ -193,7 +245,6 @@ float shadowFactor(float directionalNdl) {
193
245
  * there is no mip to select and level zero is the only thing \`texture\` could have read.
194
246
  */
195
247
  float staticDepth = textureLod(uSunShadows, vec3(sampleUv, ${SUN_STATIC_LAYER}.0), 0.0).r;
196
- float dynamicDepth = textureLod(uSunShadows, vec3(sampleUv, ${SUN_DYNAMIC_LAYER}.0), 0.0).r;
197
248
  staticLit += directionalVisibility(tapReceiverDepth, compare, staticDepth);
198
249
  if (uPeeledShadowEnabled != 0) {
199
250
  float peeledDepth = textureLod(uSunShadows, vec3(sampleUv, ${SUN_PEELED_LAYER}.0), 0.0).r;
@@ -201,12 +252,18 @@ float shadowFactor(float directionalNdl) {
201
252
  } else {
202
253
  peeledLit += 1.0;
203
254
  }
204
- dynamicLit += directionalVisibility(tapReceiverDepth, compare, dynamicDepth);
255
+ /* The moving layer here where it shares the static one's place; at its own below otherwise.
256
+ Branched on the switch itself, never its negation: see overridableConstants in transform.mjs. */
257
+ if (MOVING_SUN) {
258
+ } else {
259
+ float dynamicDepth = textureLod(uSunShadows, vec3(sampleUv, ${SUN_DYNAMIC_LAYER}.0), 0.0).r;
260
+ dynamicLit += directionalVisibility(tapReceiverDepth, compare, dynamicDepth);
261
+ }
205
262
  }
206
263
  float tapCount = float(max(uShadowFilterTaps, 1));
207
264
  staticLit /= tapCount;
208
265
  peeledLit /= tapCount;
209
- dynamicLit /= tapCount;
266
+ dynamicLit = MOVING_SUN ? movingSunFiltered(pm, movingGradient, movingSunSpan()) : dynamicLit / tapCount;
210
267
 
211
268
  // Independent layers preserve every recorded fact at an overlap: each
212
269
  // caster keeps its own distance fade, and their transmissions compose
@@ -227,11 +284,11 @@ float shadowFactor(float directionalNdl) {
227
284
  * behind the receiver lending it colour within the spread, which only a receiver between two panes
228
285
  * meets. One function for both layers, because two copies were most of the lit shaders' growth.
229
286
  */
230
- vec3 sunGlassLayer(vec3 p, float paneLayer, float tintLayer, float bias, float texel, float uvPerMetre) {
287
+ vec3 sunGlassLayer(vec3 p, float paneLayer, float tintLayer, float bias, float texel, float uvPerMetre, float span) {
231
288
  float tintSize = float(textureSize(uSunGlassTints, 0).x);
232
289
  float clarity = textureLod(uSunGlassTints, vec3(p.xy, tintLayer), 0.0).a;
233
290
  float behind =
234
- max(p.z - textureLod(uSunShadows, vec3(p.xy, paneLayer), 0.0).r, 0.0) * uShadowDepthSpan;
291
+ max(p.z - textureLod(uSunShadows, vec3(p.xy, paneLayer), 0.0).r, 0.0) * span;
235
292
  float radius = clamp(
236
293
  (1.0 - clarity) * ${FROST_SPREAD.toFixed(6)} * behind * uvPerMetre,
237
294
  texel,
@@ -271,6 +328,13 @@ vec3 sunGlassLookup(float directionalNdl) {
271
328
  /* Map units per metre along the receiver, from derivatives taken first in uniform control flow,
272
329
  so a frost spread stated in metres lands the same whatever size of light matrix a consumer built. */
273
330
  float uvPerMetre = length(dFdx(p.xy)) / max(length(dFdx(vWorldPos)), 1e-6);
331
+ /* The moving glass at its own place where its layer has a matrix of its own: movingSun.ts. */
332
+ vec3 pm = p;
333
+ float movingPerMetre = uvPerMetre;
334
+ if (MOVING_SUN) {
335
+ pm = movingSunAt();
336
+ movingPerMetre = length(dFdx(pm.xy)) / max(length(dFdx(vWorldPos)), 1e-6);
337
+ }
274
338
  if (uShadowStrength <= 0.0 || textureSize(uSunGlassTints, 0).x <= 1) return vec3(1.0);
275
339
  if (directionalNdl <= 0.0) return vec3(1.0);
276
340
  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);
@@ -288,8 +352,18 @@ vec3 sunGlassLookup(float directionalNdl) {
288
352
 
289
353
  float bias = 0.14 / uShadowDepthSpan;
290
354
  float texel = 1.35 / uShadowMapSize;
291
- vec3 staticTint = sunGlassLayer(p, ${SUN_GLASS_STATIC_LAYER}.0, 0.0, bias, texel, uvPerMetre);
292
- vec3 movingTint = sunGlassLayer(p, ${SUN_GLASS_MOVING_LAYER}.0, 1.0, bias, texel, uvPerMetre);
355
+ vec3 staticTint = sunGlassLayer(p, ${SUN_GLASS_STATIC_LAYER}.0, 0.0, bias, texel, uvPerMetre, uShadowDepthSpan);
356
+ vec3 movingTint = MOVING_SUN && sunMapFade(pm) <= 0.0
357
+ ? vec3(1.0)
358
+ : sunGlassLayer(
359
+ pm,
360
+ ${SUN_GLASS_MOVING_LAYER}.0,
361
+ 1.0,
362
+ MOVING_SUN ? 0.14 / movingSunSpan() : bias,
363
+ texel,
364
+ movingPerMetre,
365
+ MOVING_SUN ? movingSunSpan() : uShadowDepthSpan
366
+ );
293
367
  return mix(vec3(1.0), staticTint * movingTint, strength);
294
368
  }
295
369
 
@@ -8,7 +8,7 @@ export declare function glassTintGlsl(glassShadows: boolean): string;
8
8
  * The lit stage's switches, by the name of the constant each is: the decision both backends make
9
9
  * about which optional code a lit program carries. See `litSwitchesGlsl`.
10
10
  */
11
- export type LitSwitch = 'GLASS_SHADOWS' | 'CLUSTERED_LIGHTS' | 'LIGHT_FIXTURES' | 'SURFACE_EFFECTS' | 'DRIFT_LIGHT' | 'PHYSICAL_SPECULAR' | 'SURFACE_OVERLAY' | 'REFLECTION_MAPS';
11
+ export type LitSwitch = 'GLASS_SHADOWS' | 'CLUSTERED_LIGHTS' | 'LIGHT_FIXTURES' | 'SURFACE_EFFECTS' | 'DRIFT_LIGHT' | 'PHYSICAL_SPECULAR' | 'SURFACE_OVERLAY' | 'REFLECTION_MAPS' | 'WORLD_UVS' | 'MOVING_SUN' | 'LAYERED';
12
12
  /** Every switch off: what a renderer starts from before its profile and its content say otherwise. */
13
13
  export declare function noLitSwitches(): Record<LitSwitch, boolean>;
14
14
  /** Which of the lit stage's optional features a build reads. See `litSwitchesGlsl`. */
@@ -34,6 +34,9 @@ export function noLitSwitches() {
34
34
  PHYSICAL_SPECULAR: false,
35
35
  SURFACE_OVERLAY: false,
36
36
  REFLECTION_MAPS: false,
37
+ WORLD_UVS: false,
38
+ MOVING_SUN: false,
39
+ LAYERED: false,
37
40
  };
38
41
  }
39
42
  /**
@@ -38,6 +38,12 @@ export interface FlatVertexOptions {
38
38
  * Excludes `skinned`: the joint attributes and the instance matrix want the same locations.
39
39
  */
40
40
  readonly instanced: boolean;
41
+ /**
42
+ * Whether an instanced variant follows a bone animation: \`shaders/boneAnimation.ts\`. A
43
+ * variant rather than a pipeline constant, because it declares three textures the other
44
+ * instanced pipelines would otherwise have to bind stand-ins for. Instanced only.
45
+ */
46
+ readonly animated?: boolean;
41
47
  }
42
48
  /**
43
49
  * The flat vertex shader, built for one variant.
@@ -119,6 +125,21 @@ export interface FlatShaderOptions {
119
125
  * the frame's materials' reflections. True when absent. See `reflectionSurfaceGlsl`.
120
126
  */
121
127
  readonly reflectionMaps?: boolean;
128
+ /**
129
+ * Whether it places a material's maps by the world, `uWorldUv` and all (`SurfaceMaterial
130
+ * .projection`). True when absent. See `worldUvGlsl`.
131
+ */
132
+ readonly worldUvs?: boolean;
133
+ /**
134
+ * Whether it reads the sun's moving layer through a matrix of its own, \`uMovingLightViewProj\`
135
+ * and all. True when absent. See \`movingSunGlsl\`.
136
+ */
137
+ readonly movingSun?: boolean;
138
+ /**
139
+ * Whether it blends a material's layers by a mask, \`uLayers\` and all (\`SurfaceMaterial.layers\`).
140
+ * True when absent. See \`layeredGlsl\`.
141
+ */
142
+ readonly layered?: boolean;
122
143
  /**
123
144
  * How many point lights this build declares room for. `MAX_POINT_LIGHTS` when absent.
124
145
  *
@@ -21,7 +21,12 @@ import { DRIFT_LIGHT_GLSL } from './driftLight.js';
21
21
  import { SURFACE_EFFECTS_GLSL } from './surfaceEffects.js';
22
22
  import { overlayGlsl } from './overlay.js';
23
23
  import { reflectionSurfaceGlsl } from './reflectionSurface.js';
24
- import { CHANNEL_ATTRIBUTE, CHANNEL_BEND } from '../vertexChannel.js';
24
+ import { worldUvGlsl } from './worldUv.js';
25
+ import { movingSunGlsl } from './movingSun.js';
26
+ import { layeredGlsl } from './layered.js';
27
+ import { MAP_READS_GLSL } from './mapReads.js';
28
+ import { CHANNEL_BEND, LIT_CHANNEL_ATTRIBUTE } from '../vertexChannel.js';
29
+ import { BONE_ANIMATION_GLSL } from '../boneAnimation.js';
25
30
  import { FULL_LIGHT_BUDGET } from '../../uniformVectorBudget.js';
26
31
  /**
27
32
  * Flat-shaded vertex colours with one dominant directional source, ambient,
@@ -55,10 +60,16 @@ layout(location = 10) in vec4 aTangent;
55
60
  ${SKINNING_GLSL}
56
61
  #endif
57
62
 
58
- uniform mat4 uViewProj;
63
+ uniform mat4 uViewProj; // wgsl:view
59
64
  /** 1 when the mesh carries a tangent frame. See vHasTangents. */
60
65
  uniform int uHasTangents;
61
- uniform mat4 uLightViewProj;
66
+ uniform mat4 uLightViewProj; // wgsl:view
67
+ /**
68
+ * The scene's clock, what \`setAnimationTime\` set: what a bone animation's instances read
69
+ * their moment from. In the view block, before the wind, so every variant's view block is the
70
+ * plain one's or a prefix of it — the instanced variant declares no wind and keeps this.
71
+ */
72
+ uniform float uSceneTime; // wgsl:view
62
73
  /**
63
74
  * How often the surface texture repeats across this mesh's UV range, per axis.
64
75
  *
@@ -121,6 +132,9 @@ layout(location = 15) in vec4 aInstanceTint;
121
132
  * opacity of one. See lightmap.ts.
122
133
  */
123
134
  const bool LIGHTMAP_REGIONS = false; // wgsl:override
135
+ #if ANIMATED
136
+ ${BONE_ANIMATION_GLSL}
137
+ #endif
124
138
  #else
125
139
  uniform mat4 uModel;
126
140
  /**
@@ -146,7 +160,7 @@ uniform vec3 uTint;
146
160
  * agreeing about where the shared uniforms sit: the instanced variant drops these five along with
147
161
  * the two above it, and a dropped field moves nothing. flatPass.ts asserts exactly that.
148
162
  */
149
- ${CHANNEL_ATTRIBUTE}
163
+ ${LIT_CHANNEL_ATTRIBUTE}
150
164
  ${CHANNEL_BEND}
151
165
  #endif
152
166
 
@@ -244,6 +258,10 @@ void main() {
244
258
  vec3 localNormal = aNormal;
245
259
  vec3 localTangent = aTangent.xyz;
246
260
  #endif
261
+ #if ANIMATED
262
+ /* The clip's turn and place, in the mesh's own space, before the instance places it. */
263
+ local = vec4(boneAnimate(local.xyz, localNormal, localTangent), 1.0);
264
+ #endif
247
265
  #if INSTANCED
248
266
  mat4 model = mat4(aInstanceModel0, aInstanceModel1, aInstanceModel2, aInstanceModel3);
249
267
  /*
@@ -353,7 +371,15 @@ export function flatVert(options) {
353
371
  throw new Error('flatVert: a variant cannot be both morphed and instanced — a morph weight is per draw, ' +
354
372
  'so every instance of the batch would wear one expression between them.');
355
373
  }
356
- return resolveConditionals(FLAT_VERT_SOURCE, { SKINNED: options.skinned, MORPHED: options.morphed, INSTANCED: options.instanced }, 'flatVert');
374
+ if (options.animated === true && !options.instanced) {
375
+ throw new Error('flatVert: a bone animation is played by an instanced variant alone.');
376
+ }
377
+ return resolveConditionals(FLAT_VERT_SOURCE, {
378
+ SKINNED: options.skinned,
379
+ MORPHED: options.morphed,
380
+ INSTANCED: options.instanced,
381
+ ANIMATED: options.animated === true,
382
+ }, 'flatVert');
357
383
  }
358
384
  /**
359
385
  * The flat fragment shader, built for one quality profile.
@@ -439,6 +465,8 @@ export function flatFrag(options) {
439
465
  surfaceEffects: options.surfaceEffects ?? true,
440
466
  driftLight: options.driftLight ?? true,
441
467
  }),
468
+ /* Depth zero and ahead of the sun's lookup, which calls it; its switch takes the next id. */
469
+ movingSunGlsl(options.movingSun ?? true),
442
470
  LOBES_GLSL,
443
471
  POINTSHADOW_GLSL,
444
472
  DIRECTIONALSHADOW_GLSL,
@@ -464,8 +492,6 @@ export function flatFrag(options) {
464
492
  * and `main` is unconditional by construction.
465
493
  */
466
494
  TANGENT_FRAME_GLSL,
467
- /* After the frame it calls, unconditional for the same reason. See normalMap.ts. */
468
- NORMAL_MAP_GLSL,
469
495
  /* Unconditional for the same reason, and called only where a cutoff was asked for. */
470
496
  CUTOUT_COVERAGE_GLSL,
471
497
  CUTOUT_DITHER_GLSL,
@@ -477,6 +503,15 @@ export function flatFrag(options) {
477
503
  overlayGlsl(options.surfaceOverlay ?? true),
478
504
  /* After the overlay, so its switch takes the next id. See reflectionSurface.ts. */
479
505
  reflectionSurfaceGlsl(options.reflectionMaps ?? true),
506
+ /* After the reflection maps, so its switch takes the next id; main calls it. worldUv.ts. */
507
+ worldUvGlsl(options.worldUvs ?? true),
508
+ /* After the projection, so its switch takes the next id; the normal map below calls it. */
509
+ layeredGlsl(options.layered ?? true),
510
+ /* Where each map is read, after both of the ways it can be read otherwise. See mapReads.ts. */
511
+ MAP_READS_GLSL,
512
+ /* After the frame and the projection it calls, unconditional for the same reason as the
513
+ frame. See normalMap.ts. */
514
+ NORMAL_MAP_GLSL,
480
515
  /* After the models, whose answer to a rectangle it asks for. See areaLight.ts. */
481
516
  AREA_LIGHT_GLSL,
482
517
  MAIN_GLSL,
@@ -0,0 +1,19 @@
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 declare function layeredGlsl(on: boolean): string;
@@ -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) {
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
+ }