@driftengine/core 4.8.2 → 4.8.4

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 (259) hide show
  1. package/CHANGELOG.json +127 -0
  2. package/README.md +1 -1
  3. package/dist/geometry/meshConcat.js +2 -0
  4. package/dist/index.d.ts +11 -4
  5. package/dist/index.js +22 -1
  6. package/dist/math/scalar.js +1 -1
  7. package/dist/render/anisotropicLobe.d.ts +7 -0
  8. package/dist/render/anisotropicLobe.js +33 -0
  9. package/dist/render/backend/api.d.ts +41 -4
  10. package/dist/render/backend/materialChanges.d.ts +7 -2
  11. package/dist/render/backend/materialChanges.js +8 -3
  12. package/dist/render/backend/select.js +8 -0
  13. package/dist/render/backend/webgl2/clothTextures.d.ts +27 -0
  14. package/dist/render/backend/webgl2/clothTextures.js +93 -0
  15. package/dist/render/backend/webgl2/renderer.d.ts +159 -3
  16. package/dist/render/backend/webgl2/renderer.js +662 -80
  17. package/dist/render/backend/webgl2/skinScatter.d.ts +50 -0
  18. package/dist/render/backend/webgl2/skinScatter.js +256 -0
  19. package/dist/render/backend/webgpu/buffers.d.ts +25 -11
  20. package/dist/render/backend/webgpu/buffers.js +98 -49
  21. package/dist/render/backend/webgpu/causticsPass.js +2 -1
  22. package/dist/render/backend/webgpu/clothSolvePack.d.ts +27 -0
  23. package/dist/render/backend/webgpu/clothSolvePack.js +129 -0
  24. package/dist/render/backend/webgpu/clothTextures.d.ts +68 -0
  25. package/dist/render/backend/webgpu/clothTextures.js +145 -0
  26. package/dist/render/backend/webgpu/compressedUpload.d.ts +18 -0
  27. package/dist/render/backend/webgpu/compressedUpload.js +38 -0
  28. package/dist/render/backend/webgpu/computeHost.d.ts +21 -0
  29. package/dist/render/backend/webgpu/computeHost.js +5 -0
  30. package/dist/render/backend/webgpu/depthPass.d.ts +29 -9
  31. package/dist/render/backend/webgpu/depthPass.js +43 -12
  32. package/dist/render/backend/webgpu/drawCommand.js +6 -2
  33. package/dist/render/backend/webgpu/filmPass.js +2 -1
  34. package/dist/render/backend/webgpu/flatPass.d.ts +43 -4
  35. package/dist/render/backend/webgpu/flatPass.js +111 -13
  36. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +74 -0
  37. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +452 -0
  38. package/dist/render/backend/webgpu/linePass.js +2 -1
  39. package/dist/render/backend/webgpu/motionPass.d.ts +20 -0
  40. package/dist/render/backend/webgpu/motionPass.js +106 -9
  41. package/dist/render/backend/webgpu/panelPass.js +2 -1
  42. package/dist/render/backend/webgpu/particlePass.js +2 -1
  43. package/dist/render/backend/webgpu/plumePass.js +2 -1
  44. package/dist/render/backend/webgpu/postPass.d.ts +15 -3
  45. package/dist/render/backend/webgpu/postPass.js +25 -8
  46. package/dist/render/backend/webgpu/renderer.d.ts +123 -6
  47. package/dist/render/backend/webgpu/renderer.js +534 -58
  48. package/dist/render/backend/webgpu/sdfTextPass.js +2 -1
  49. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +33 -0
  50. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +469 -0
  51. package/dist/render/backend/webgpu/skinScatterPass.d.ts +51 -0
  52. package/dist/render/backend/webgpu/skinScatterPass.js +253 -0
  53. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
  54. package/dist/render/backend/webgpu/surfaceTexturePass.js +37 -12
  55. package/dist/render/backend/webgpu/textPass.js +2 -1
  56. package/dist/render/backend/webgpu/waterPass.js +2 -1
  57. package/dist/render/backend/webgpu/windStreakPass.js +2 -1
  58. package/dist/render/boltBatch.js +2 -1
  59. package/dist/render/causticsRenderer.js +2 -1
  60. package/dist/render/clothBindingData.d.ts +41 -0
  61. package/dist/render/clothBindingData.js +72 -0
  62. package/dist/render/clothPlacement.d.ts +25 -0
  63. package/dist/render/clothPlacement.js +85 -0
  64. package/dist/render/compressedSource.d.ts +66 -0
  65. package/dist/render/compressedSource.js +138 -0
  66. package/dist/render/cutoutCaster.d.ts +5 -1
  67. package/dist/render/cutoutCaster.js +6 -0
  68. package/dist/render/cutoutDither.d.ts +51 -0
  69. package/dist/render/cutoutDither.js +90 -0
  70. package/dist/render/decalPass.js +2 -1
  71. package/dist/render/eyeRefraction.d.ts +35 -0
  72. package/dist/render/eyeRefraction.js +40 -0
  73. package/dist/render/glCompressed.d.ts +27 -0
  74. package/dist/render/glCompressed.js +53 -0
  75. package/dist/render/globalMediumPass.js +3 -2
  76. package/dist/render/hairLobes.d.ts +70 -0
  77. package/dist/render/hairLobes.js +120 -0
  78. package/dist/render/lightBudget.d.ts +22 -0
  79. package/dist/render/lightBudget.js +22 -0
  80. package/dist/render/lineBatch.js +5 -1
  81. package/dist/render/mesh.d.ts +7 -0
  82. package/dist/render/mesh.js +24 -0
  83. package/dist/render/oitPass.js +2 -1
  84. package/dist/render/particleBatch.js +3 -2
  85. package/dist/render/particlePool.d.ts +6 -2
  86. package/dist/render/plumeRenderer.js +3 -2
  87. package/dist/render/recon/motionVectors.d.ts +4 -2
  88. package/dist/render/recon/motionVectors.js +6 -3
  89. package/dist/render/renderQuality.d.ts +29 -2
  90. package/dist/render/renderQuality.js +5 -0
  91. package/dist/render/rendererHarness.js +4 -0
  92. package/dist/render/sceneCoverage.d.ts +54 -0
  93. package/dist/render/sceneCoverage.js +72 -0
  94. package/dist/render/sceneTarget.d.ts +12 -0
  95. package/dist/render/sceneTarget.js +14 -0
  96. package/dist/render/sdfTextRenderer.js +2 -1
  97. package/dist/render/shaders/clothBinding.d.ts +22 -0
  98. package/dist/render/shaders/clothBinding.js +80 -0
  99. package/dist/render/shaders/depth.js +45 -8
  100. package/dist/render/shaders/flat/anisotropic.d.ts +15 -0
  101. package/dist/render/shaders/flat/anisotropic.js +68 -0
  102. package/dist/render/shaders/flat/eye.d.ts +1 -0
  103. package/dist/render/shaders/flat/eye.js +81 -0
  104. package/dist/render/shaders/flat/hair.d.ts +1 -0
  105. package/dist/render/shaders/flat/hair.js +101 -0
  106. package/dist/render/shaders/flat/index.js +42 -3
  107. package/dist/render/shaders/flat/main.d.ts +1 -1
  108. package/dist/render/shaders/flat/main.js +113 -17
  109. package/dist/render/shaders/flat/models.d.ts +14 -0
  110. package/dist/render/shaders/flat/models.js +273 -0
  111. package/dist/render/shaders/flat/preamble.js +26 -24
  112. package/dist/render/shaders/flat/skin.d.ts +1 -0
  113. package/dist/render/shaders/flat/skin.js +114 -0
  114. package/dist/render/shaders/generated/depth.wgsl.d.ts +66 -24
  115. package/dist/render/shaders/generated/depth.wgsl.js +66 -24
  116. package/dist/render/shaders/generated/flat.wgsl.d.ts +1815 -1704
  117. package/dist/render/shaders/generated/flat.wgsl.js +1853 -1742
  118. package/dist/render/shaders/generated/line.wgsl.d.ts +1 -1
  119. package/dist/render/shaders/generated/line.wgsl.js +1 -1
  120. package/dist/render/shaders/generated/particle.wgsl.d.ts +3 -3
  121. package/dist/render/shaders/generated/particle.wgsl.js +3 -3
  122. package/dist/render/shaders/generated/rush.wgsl.d.ts +1 -1
  123. package/dist/render/shaders/generated/rush.wgsl.js +1 -1
  124. package/dist/render/shaders/generated/sdfText.wgsl.d.ts +1 -1
  125. package/dist/render/shaders/generated/sdfText.wgsl.js +1 -1
  126. package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +48 -0
  127. package/dist/render/shaders/generated/skinBlur.wgsl.js +54 -0
  128. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
  129. package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
  130. package/dist/render/shaders/line.d.ts +1 -1
  131. package/dist/render/shaders/outputTransform.d.ts +14 -1
  132. package/dist/render/shaders/outputTransform.js +23 -1
  133. package/dist/render/shaders/particle.d.ts +3 -3
  134. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  135. package/dist/render/shaders/recon/motion.wgsl.js +102 -0
  136. package/dist/render/shaders/recon/resolve.wgsl.js +15 -1
  137. package/dist/render/shaders/rush.js +13 -2
  138. package/dist/render/shaders/sdfText.d.ts +1 -1
  139. package/dist/render/shaders/skinBlur.d.ts +1 -0
  140. package/dist/render/shaders/skinBlur.js +103 -0
  141. package/dist/render/shaders/skinning.d.ts +3 -1
  142. package/dist/render/shaders/skinning.js +31 -4
  143. package/dist/render/shaders/temporalResolve.js +12 -5
  144. package/dist/render/shadowCasters.d.ts +13 -1
  145. package/dist/render/skinBlur.d.ts +41 -0
  146. package/dist/render/skinBlur.js +49 -0
  147. package/dist/render/skinScatter.d.ts +67 -0
  148. package/dist/render/skinScatter.js +80 -0
  149. package/dist/render/skinnedClothRun.d.ts +26 -0
  150. package/dist/render/skinnedClothRun.js +84 -0
  151. package/dist/render/ssrPass.js +2 -1
  152. package/dist/render/surfaceModel.d.ts +129 -0
  153. package/dist/render/surfaceModel.js +152 -0
  154. package/dist/render/surfaceTexture.d.ts +55 -4
  155. package/dist/render/surfaceTexture.js +60 -46
  156. package/dist/render/textRenderer.js +2 -1
  157. package/dist/render/vertexDefaults.js +8 -0
  158. package/dist/render/waterRenderer.js +2 -1
  159. package/dist/render/windStreakRenderer.js +2 -1
  160. package/package.json +6 -6
  161. package/src/geometry/meshConcat.ts +2 -0
  162. package/src/index.ts +53 -1
  163. package/src/math/scalar.ts +1 -1
  164. package/src/render/anisotropicLobe.ts +41 -0
  165. package/src/render/backend/api.ts +52 -3
  166. package/src/render/backend/materialChanges.ts +8 -3
  167. package/src/render/backend/select.ts +8 -0
  168. package/src/render/backend/webgl2/clothTextures.ts +120 -0
  169. package/src/render/backend/webgl2/renderer.ts +750 -80
  170. package/src/render/backend/webgl2/skinScatter.ts +309 -0
  171. package/src/render/backend/webgpu/buffers.ts +137 -63
  172. package/src/render/backend/webgpu/causticsPass.ts +2 -1
  173. package/src/render/backend/webgpu/clothSolvePack.ts +161 -0
  174. package/src/render/backend/webgpu/clothTextures.ts +194 -0
  175. package/src/render/backend/webgpu/compressedUpload.ts +65 -0
  176. package/src/render/backend/webgpu/computeHost.ts +25 -0
  177. package/src/render/backend/webgpu/depthPass.ts +43 -10
  178. package/src/render/backend/webgpu/drawCommand.ts +6 -2
  179. package/src/render/backend/webgpu/filmPass.ts +2 -1
  180. package/src/render/backend/webgpu/flatPass.ts +147 -8
  181. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +502 -0
  182. package/src/render/backend/webgpu/linePass.ts +2 -1
  183. package/src/render/backend/webgpu/motionPass.ts +128 -9
  184. package/src/render/backend/webgpu/panelPass.ts +2 -1
  185. package/src/render/backend/webgpu/particlePass.ts +2 -1
  186. package/src/render/backend/webgpu/plumePass.ts +2 -1
  187. package/src/render/backend/webgpu/postPass.ts +25 -6
  188. package/src/render/backend/webgpu/renderer.ts +701 -52
  189. package/src/render/backend/webgpu/sdfTextPass.ts +2 -1
  190. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +470 -0
  191. package/src/render/backend/webgpu/skinScatterPass.ts +309 -0
  192. package/src/render/backend/webgpu/surfaceTexturePass.ts +42 -12
  193. package/src/render/backend/webgpu/textPass.ts +2 -1
  194. package/src/render/backend/webgpu/waterPass.ts +2 -1
  195. package/src/render/backend/webgpu/windStreakPass.ts +2 -1
  196. package/src/render/boltBatch.ts +2 -1
  197. package/src/render/causticsRenderer.ts +2 -1
  198. package/src/render/clothBindingData.ts +89 -0
  199. package/src/render/clothPlacement.ts +114 -0
  200. package/src/render/compressedSource.ts +198 -0
  201. package/src/render/cutoutCaster.ts +20 -2
  202. package/src/render/cutoutDither.ts +99 -0
  203. package/src/render/decalPass.ts +2 -1
  204. package/src/render/eyeRefraction.ts +72 -0
  205. package/src/render/glCompressed.ts +86 -0
  206. package/src/render/globalMediumPass.ts +3 -2
  207. package/src/render/hairLobes.ts +188 -0
  208. package/src/render/lightBudget.ts +25 -0
  209. package/src/render/lineBatch.ts +3 -1
  210. package/src/render/mesh.ts +23 -0
  211. package/src/render/oitPass.ts +2 -1
  212. package/src/render/particleBatch.ts +3 -2
  213. package/src/render/particlePool.ts +6 -2
  214. package/src/render/plumeRenderer.ts +3 -2
  215. package/src/render/recon/motionVectors.ts +6 -3
  216. package/src/render/renderQuality.ts +37 -2
  217. package/src/render/rendererHarness.ts +4 -0
  218. package/src/render/sceneCoverage.ts +78 -0
  219. package/src/render/sceneTarget.ts +21 -0
  220. package/src/render/sdfTextRenderer.ts +2 -1
  221. package/src/render/shaders/clothBinding.ts +81 -0
  222. package/src/render/shaders/depth.ts +45 -8
  223. package/src/render/shaders/flat/anisotropic.ts +68 -0
  224. package/src/render/shaders/flat/eye.ts +82 -0
  225. package/src/render/shaders/flat/hair.ts +102 -0
  226. package/src/render/shaders/flat/index.ts +42 -3
  227. package/src/render/shaders/flat/main.ts +113 -17
  228. package/src/render/shaders/flat/models.ts +286 -0
  229. package/src/render/shaders/flat/preamble.ts +26 -24
  230. package/src/render/shaders/flat/skin.ts +116 -0
  231. package/src/render/shaders/generated/depth.wgsl.ts +66 -24
  232. package/src/render/shaders/generated/flat.wgsl.ts +1853 -1742
  233. package/src/render/shaders/generated/line.wgsl.ts +1 -1
  234. package/src/render/shaders/generated/particle.wgsl.ts +3 -3
  235. package/src/render/shaders/generated/rush.wgsl.ts +1 -1
  236. package/src/render/shaders/generated/sdfText.wgsl.ts +1 -1
  237. package/src/render/shaders/generated/skinBlur.wgsl.ts +56 -0
  238. package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
  239. package/src/render/shaders/outputTransform.ts +24 -1
  240. package/src/render/shaders/recon/motion.wgsl.ts +102 -0
  241. package/src/render/shaders/recon/resolve.wgsl.ts +15 -1
  242. package/src/render/shaders/rush.ts +13 -2
  243. package/src/render/shaders/skinBlur.ts +105 -0
  244. package/src/render/shaders/skinning.ts +31 -4
  245. package/src/render/shaders/temporalResolve.ts +12 -5
  246. package/src/render/shadowCasters.ts +13 -0
  247. package/src/render/skinBlur.ts +53 -0
  248. package/src/render/skinScatter.ts +92 -0
  249. package/src/render/skinnedClothRun.ts +128 -0
  250. package/src/render/ssrPass.ts +2 -1
  251. package/src/render/surfaceModel.ts +274 -0
  252. package/src/render/surfaceTexture.ts +134 -63
  253. package/src/render/textRenderer.ts +2 -1
  254. package/src/render/vertexDefaults.ts +8 -0
  255. package/src/render/waterRenderer.ts +2 -1
  256. package/src/render/windStreakRenderer.ts +2 -1
  257. package/dist/math/exact.d.ts +0 -76
  258. package/dist/math/exact.js +0 -327
  259. package/src/math/exact.ts +0 -340
@@ -21,6 +21,8 @@ export const MAIN_GLSL = `void main() {
21
21
  fxLoad(int(surfaceAt.z));
22
22
  surfaceAt = fxAnimate(surfaceAt);
23
23
  }
24
+ /* An eye's iris, moved by its cornea before any map reads it: models.ts. */
25
+ surfaceAt = modelSurfaceAt(surfaceAt);
24
26
  vec2 fxDx = dFdx(surfaceAt.xy);
25
27
  vec2 fxDy = dFdy(surfaceAt.xy);
26
28
  if (uWriteMode.y != 0.0 && !ditherKeeps(uWriteMode.y)) discard;
@@ -56,18 +58,35 @@ export const MAIN_GLSL = `void main() {
56
58
  * rectangle of haze with a picture in it, rather than a picture.
57
59
  */
58
60
  float coverage = 1.0;
59
- if (uAlbedoEnabled != 0) {
61
+ if (uMaterialFlags.x != 0) {
60
62
  vec4 texel = texture(uAlbedo, surfaceAt);
61
63
  // Discarded before anything else is computed: a thrown-away fragment should not pay
62
64
  // for the lighting it will never contribute to. The test credits alpha for the mip level
63
65
  // it samples, so a needle averaged thin down the chain is not lost; see cutoutCoverage.ts.
64
66
  // Behind a branch on a uniform, so a material with no cutoff pays nothing for it.
65
- if (uAlbedoCutout > 0.0) {
67
+ float kept = texel.a;
68
+ if (uCutout.x > 0.0) {
66
69
  float tested = cutoutAlpha(texel.a, vUv.xy * vec2(textureSize(uAlbedo, 0).xy));
67
- if (tested < uAlbedoCutout) discard;
70
+ if (uCutout.y == 0.0) {
71
+ if (tested < uCutout.x) discard;
72
+ } else {
73
+ /*
74
+ * A dithered edge: the pixel is kept by the share of it the texture covers. Under a
75
+ * temporal resolve the test is against noise that moves every frame, and a kept pixel is
76
+ * whole; multisampled, the share is the alpha the GPU turns into coverage. See
77
+ * cutoutDither.ts.
78
+ */
79
+ float share = cutoutShare(tested, uCutout.x);
80
+ if (uCutout.y == 1.0) {
81
+ if (!cutoutKeeps(share, gl_FragCoord.xy, uCutout.z)) discard;
82
+ kept = 1.0;
83
+ } else {
84
+ kept = share;
85
+ }
86
+ }
68
87
  }
69
88
  albedo *= texel.rgb;
70
- coverage = texel.a;
89
+ coverage = kept;
71
90
  }
72
91
 
73
92
  /*
@@ -91,7 +110,7 @@ export const MAIN_GLSL = `void main() {
91
110
  float ormOcclusion = 1.0;
92
111
  float ormRoughness = vRoughness;
93
112
  float metal = 0.0;
94
- if (uOrmEnabled != 0) {
113
+ if (uMaterialFlags.y != 0) {
95
114
  vec3 t = texture(uOrmMap, surfaceAt).rgb;
96
115
  ormOcclusion = mix(1.0, t.r, uOrmScale.r);
97
116
  ormRoughness = clamp(t.g * uOrmScale.g, 0.0, 1.0);
@@ -121,6 +140,8 @@ export const MAIN_GLSL = `void main() {
121
140
  }
122
141
 
123
142
  vec3 lit = albedo;
143
+ /* Skin's diffuse, held before emission for the screen-space blur's own pass: models.ts. */
144
+ vec3 diffuseAlone = vec3(0.0);
124
145
  /* The light arriving at a glass pane from behind it, gathered in each light loop below and added
125
146
  over the see-through at the end. Stays zero for anything that is not glass. See glass.ts. */
126
147
  vec3 glassGlow = vec3(0.0);
@@ -143,7 +164,7 @@ export const MAIN_GLSL = `void main() {
143
164
  * Taken from the stored normal alone, a pane seen from behind glowed with the light on the
144
165
  * eye's side and not with the sun shining through it.
145
166
  */
146
- bool backFace = (uDoubleSided != 0 || glassTransmission > 0.0) && dot(n, uCameraPos - vWorldPos) < 0.0;
167
+ bool backFace = (uMaterialFlags.w != 0 || glassTransmission > 0.0) && dot(n, uCameraPos - vWorldPos) < 0.0;
147
168
  /*
148
169
  * The authored normal, before either relief perturbs it.
149
170
  *
@@ -162,6 +183,15 @@ export const MAIN_GLSL = `void main() {
162
183
  */
163
184
  if (uNormalStrength > 0.0) {
164
185
  vec3 mapped = texture(uNormalMap, surfaceAt).xyz * 2.0 - 1.0;
186
+ /*
187
+ * z rebuilt where none is stored. A tangent-space normal points out of its surface, so a
188
+ * stored z at or below zero is never a real one: it is a two-channel map, BC5, whose blue a
189
+ * device samples as zero and which arrives here as -1. Every map baked from BC5 to PNG before
190
+ * 4.8.4 carried the same zero. A select rather than a flag: no uniform to bind, no material
191
+ * that has to say what format it holds, and continuous at the boundary, since a unit texel
192
+ * lying flat has its z near zero either way. A texel with z above zero is left as it was.
193
+ */
194
+ mapped.z = mapped.z > 0.0 ? mapped.z : sqrt(max(0.0, 1.0 - dot(mapped.xy, mapped.xy)));
165
195
  mat3 tbn = tangentFrame(n, vWorldPos, vUv.xy, vTangent, vHasTangents);
166
196
  n = normalize(mix(n, normalize(tbn * mapped), uNormalStrength));
167
197
  }
@@ -268,7 +298,7 @@ export const MAIN_GLSL = `void main() {
268
298
  * and normalized: dividing the gradient by \`det\` instead would blow up on a fragment where
269
299
  * the surface is edge-on and the quad's footprint collapses.
270
300
  */
271
- if (uAlbedoEnabled != 0 && uTextureRelief != 0.0) {
301
+ if (uMaterialFlags.x != 0 && uTextureRelief != 0.0) {
272
302
  /*
273
303
  * **A centred difference, not three.js's forward one, and that is what makes the two
274
304
  * backends agree.** WebGL2's framebuffer counts y upward and WebGPU's counts it downward,
@@ -331,6 +361,14 @@ export const MAIN_GLSL = `void main() {
331
361
  * across the fold of every curtain.
332
362
  */
333
363
  if (backFace) n = -n;
364
+ /*
365
+ * A model's surface, gathered once, on a branch on the switches alone (models.ts) — and whether
366
+ * there is one, asked once: every site below is a branch on these two, which a pipeline with no
367
+ * model folds away, and each extra call was a copy of its plumbing in all sixteen permutations.
368
+ */
369
+ bool modelled = modelOn();
370
+ bool modelledBehind = modelSeesBehind();
371
+ if (modelled) modelSurface(n, albedo, surfaceRoughness, metal, surfaceAt);
334
372
 
335
373
  /*
336
374
  * One dot product, used twice: ndl is the lit side and its negative is the night side below.
@@ -444,7 +482,19 @@ export const MAIN_GLSL = `void main() {
444
482
  * smallest change available in a shader with a documented history of shared subexpressions
445
483
  * rounding differently when their neighbours move. See the night-emissive term.
446
484
  */
447
- lit = albedo * (ambient + sunColor * direct * (1.0 - metal));
485
+ /*
486
+ * A model answers the sun itself, and is shadowed as it says (models.ts). The standard line
487
+ * stands as it was, so a pipeline with no model compiles to exactly the arithmetic before.
488
+ */
489
+ vec3 sunModelSpecular = vec3(0.0);
490
+ if (modelled) {
491
+ modelLight(uDirectionalDir, 0.0, 0.0);
492
+ float sunModelFacing = modelledBehind ? 1.0 : sunFacing;
493
+ lit = modelAmbient(ambient) + sunColor * mDiffuse * modelShade(sunShade) * sunModelFacing;
494
+ sunModelSpecular = mSpecular * sunColor * sunShade * sunModelFacing;
495
+ } else {
496
+ lit = albedo * (ambient + sunColor * direct * (1.0 - metal));
497
+ }
448
498
 
449
499
  /*
450
500
  * A sun highlight, for the few surfaces that ask for one.
@@ -507,6 +557,7 @@ export const MAIN_GLSL = `void main() {
507
557
  */
508
558
  vec3 sunHighlight =
509
559
  sunColor * specularLobe(max(dot(n, halfway), 0.0), surfaceRoughness) * sunSpec * sunShade * sunFacing;
560
+ if (modelled) sunHighlight = sunModelSpecular;
510
561
  /*
511
562
  * The dielectric's share goes in here, where it has always gone, so the environment blend
512
563
  * below dims it by exactly the reflectance it always dimmed it by. The metal's share is added
@@ -578,18 +629,20 @@ export const MAIN_GLSL = `void main() {
578
629
  * whole block was skipped for a fully metallic surface and max(uReflectivity, metal) inside it
579
630
  * was never reached. A chromed subject came out near black with a baked probe sitting unread.
580
631
  *
581
- * **Gated on uOrmEnabled rather than on metal, and that is required rather than tidy.** The
632
+ * **Gated on the ORM flag rather than on metal, and that is required rather than tidy.** The
582
633
  * block takes fwidth of the mirror direction, and a derivative may only be taken under uniform
583
- * control flow. uOrmEnabled is a uniform; metal is a texture fetch, so branching on it would
634
+ * control flow. The flag is a uniform; metal is a texture fetch, so branching on it would
584
635
  * put that derivative in non-uniform control flow and WGSL refuses the module outright. See
585
636
  * AGENTS.md, 2026-08-07. The per-texel decision still happens, one line down, in the amount.
586
637
  *
587
- * At uOrmEnabled 0 this is uReflectivity > 0.0 exactly, so nothing that binds no map takes a
638
+ * With the flag at 0 this is uReflectivity > 0.0 exactly, so nothing that binds no map takes a
588
639
  * branch it did not take before.
589
640
  */
590
- if (uReflectivity > 0.0 || uOrmEnabled != 0) {
641
+ /* A model reflects its environment whatever the pass asked of the standard model: see
642
+ modelReflectance. On the switches alone, so the derivatives below stay in uniform flow. */
643
+ if (uReflectivity > 0.0 || uMaterialFlags.y != 0 || modelled) {
591
644
  vec3 toEye = normalize(uCameraPos - vWorldPos);
592
- vec3 mirrored = reflect(-toEye, n);
645
+ vec3 mirrored = reflect(-toEye, modelled ? modelReflectNormal(n) : n);
593
646
  vec3 environment = mix(uAmbientGround, uAmbient, mirrored.y * 0.5 + 0.5);
594
647
  #if ENVIRONMENT_PROBE
595
648
  /*
@@ -845,6 +898,8 @@ export const MAIN_GLSL = `void main() {
845
898
  #else
846
899
  float weight = clamp(fresnel * reflectAmount, 0.0, 1.0);
847
900
  #endif
901
+ if (modelled) weight = modelReflectance(weight);
902
+ if (modelled) environment = modelEnvironment(environment);
848
903
  lit = mix(
849
904
  lit,
850
905
  environment * mix(vec3(1.0), albedo, metal),
@@ -1151,7 +1206,8 @@ export const MAIN_GLSL = `void main() {
1151
1206
  */
1152
1207
  float backNdl =
1153
1208
  glassTransmission > 0.0 ? max(-dot(n, toLight / max(dist, 1e-4)), 0.0) : 0.0;
1154
- if (ndl <= 0.0 && backNdl <= 0.0) continue;
1209
+ /* A model that sees light from behind keeps the lamps behind the surface. See models.ts. */
1210
+ if (ndl <= 0.0 && backNdl <= 0.0 && !modelledBehind) continue;
1155
1211
 
1156
1212
  /*
1157
1213
  * Normal-offset bias.
@@ -1400,13 +1456,25 @@ export const MAIN_GLSL = `void main() {
1400
1456
 
1401
1457
  float shape = (uLightFalloff == 1 ? falloff : falloff * falloff) * coneFalloff * photometric;
1402
1458
  glassGlow += lightColor * backNdl * shape * lightWeight * shaded * lampGlass;
1403
- if (ndl <= 0.0) continue;
1459
+ if (ndl <= 0.0 && !modelledBehind) continue;
1404
1460
  /*
1405
1461
  * The cookie tints rather than scales, because a mask may be coloured — a stained window is
1406
1462
  * the case that makes the difference — and a greyscale one is exactly a scale. Applied to
1407
1463
  * the light's colour rather than to \`shape\`, which is a scalar and could not carry it.
1408
1464
  */
1409
1465
  lightColor *= cookieTint;
1466
+ /*
1467
+ * A model's answer to this lamp, held back and shadowed exactly as the standard terms below
1468
+ * are — the shadowed sum through \`modelShade\`, which is where skin's penumbra reddens.
1469
+ */
1470
+ if (modelled) {
1471
+ modelLight(toLight / max(dist, 1e-4), lightSourceRadius, dist);
1472
+ vec3 lampModelLight = lightColor * shape * lightWeight;
1473
+ lampOpen += (mDiffuse + mSpecular) * lampModelLight;
1474
+ lampShadowed += (mDiffuse * modelShade(shaded) + mSpecular * shaded) * lampModelLight * lampGlass;
1475
+ lightShade = min(lightShade, mix(1.0, shaded, lightWeight));
1476
+ continue;
1477
+ }
1410
1478
  /*
1411
1479
  * **A metal has no diffuse term, and this lamp kept giving it one.**
1412
1480
  *
@@ -1648,6 +1716,22 @@ export const MAIN_GLSL = `void main() {
1648
1716
 
1649
1717
  glassGlow += uAreaLightColor[a] * backForm * areaOccl * areaGlass;
1650
1718
  if (form <= 0.0) continue;
1719
+ /*
1720
+ * A model answers a rectangle as it answers a lamp at its centre, carried at the rectangle's
1721
+ * own energy: the diffuse by the exact form factor over the centre's cosine, the specular by
1722
+ * the form factor. **An approximation, stated**: a model's lobe is not integrated over the
1723
+ * rectangle as the standard lobe is, so a glossy model under a large softbox shows a lobe
1724
+ * from its centre rather than its shape.
1725
+ */
1726
+ if (modelled) {
1727
+ vec3 areaL = normalize(centre - vWorldPos);
1728
+ modelLight(areaL, 0.0, 0.0);
1729
+ vec3 areaModel = (mDiffuse * (form / max(dot(n, areaL), 1e-3)) + mSpecular * form)
1730
+ * uAreaLightColor[a];
1731
+ lampOpen += areaModel;
1732
+ lampShadowed += areaModel * areaOccl * areaGlass;
1733
+ continue;
1734
+ }
1651
1735
  /* Held back with the point lamps', and shadowed by the same term it always was. */
1652
1736
  vec3 areaDiffuse = albedo * uAreaLightColor[a] * form * (1.0 - metal);
1653
1737
  lampOpen += areaDiffuse;
@@ -1741,7 +1825,12 @@ export const MAIN_GLSL = `void main() {
1741
1825
  if (glassTransmission > 0.0) {
1742
1826
  glassGlow += driftLightIrradiance(vWorldPos, -n) * (driftShare * uDriftLight.w);
1743
1827
  }
1744
- lit += albedo * driftLightIrradiance(vWorldPos, n) * (driftShare * uDriftLight.w) * (1.0 - metal);
1828
+ /* The standard expression in its own order — albedo times the field, then the share — so a
1829
+ pipeline with no model rounds exactly as it did; a model is handed the light alone. */
1830
+ vec3 driftField = driftLightIrradiance(vWorldPos, n);
1831
+ lit += modelled
1832
+ ? modelIrradiance(driftField * (driftShare * uDriftLight.w))
1833
+ : albedo * driftField * (driftShare * uDriftLight.w) * (1.0 - metal);
1745
1834
  }
1746
1835
 
1747
1836
  /*
@@ -1793,6 +1882,7 @@ export const MAIN_GLSL = `void main() {
1793
1882
  * underneath: a ceiling emitting a dull warm haze over a pale panel cannot be expressed
1794
1883
  * by scaling the panel, and scaling it gets the brightness right and the hue wrong.
1795
1884
  */
1885
+ diffuseAlone = lit;
1796
1886
  vec3 emissiveTint = vEmissiveColor.r < 0.0 ? albedo : vEmissiveColor;
1797
1887
  /*
1798
1888
  * Where the surface glows, from an image, or everywhere if it binds none.
@@ -1811,7 +1901,7 @@ export const MAIN_GLSL = `void main() {
1811
1901
  * explicit level — the 2026-08-07 rule is satisfied by the gate rather than by a \`textureLod\`.
1812
1902
  */
1813
1903
  vec3 emissiveMapped = vec3(1.0);
1814
- if (uEmissiveMapEnabled != 0) {
1904
+ if (uMaterialFlags.z != 0) {
1815
1905
  emissiveMapped = texture(uEmissiveMap, surfaceAt).rgb * uEmissiveScale;
1816
1906
  }
1817
1907
  /* Pulse, flicker and fade from the layer's effects; exactly 1 where it names none. */
@@ -1890,8 +1980,14 @@ export const MAIN_GLSL = `void main() {
1890
1980
  // Water is a camera medium, not a property of individual objects.
1891
1981
  vec3 waterTransmission = vec3(0.42) + uUnderwaterColor * 2.0;
1892
1982
  lit = mix(lit, lit * waterTransmission, uUnderwaterFactor * 0.55);
1983
+ diffuseAlone = mix(diffuseAlone, diffuseAlone * waterTransmission, uUnderwaterFactor * 0.55);
1893
1984
  fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);
1894
1985
  }
1986
+ /* The diffuse pass leaves here, with skin's diffuse and the profile it is spread by. */
1987
+ if (modelDiffuseAlone()) {
1988
+ outColor = vec4(modelDiffuseOut(diffuseAlone * ormOcclusion, fog), skinProfileCode());
1989
+ return;
1990
+ }
1895
1991
 
1896
1992
  /*
1897
1993
  * A surface recedes into the medium; light added to the frame fades in it (\`uFogEnabled\` at 2,
@@ -0,0 +1,286 @@
1
+ /**
2
+ * The lit stage's shading models: how a material that is not the standard one answers light.
3
+ *
4
+ * **One switch a model, each a pipeline constant, and a branch only ever on the switch itself.**
5
+ * `MODEL_ANISOTROPIC`, `MODEL_HAIR`, `MODEL_SKIN` and `MODEL_EYE` are off in every pipeline but the
6
+ * ones a material with that model asks for, so the device compiles every model out of the standard
7
+ * pipelines and a scene that names none carries none — the arrangement `litSwitchesGlsl` has, for
8
+ * its reason. Not one constant numbering the models: a comparison against an override is a
9
+ * specialisation-constant operation, which the WGSL generator refuses (`overridableConstants`), so
10
+ * every way in is a function that branches on the switches one at a time.
11
+ *
12
+ * **Where the models plug in.** `main` gathers the surface once (`modelSurface`), then asks at each
13
+ * place light arrives: `modelLight` for the sun, each lamp and each rectangle, `modelAmbient` for
14
+ * the sky and the probe's irradiance, `modelIrradiance` for DriftLight, `modelReflectNormal` and
15
+ * `modelReflectance` for the environment's reflection. Each returns today's term for the standard
16
+ * model, and each model's file is where its own answers are.
17
+ *
18
+ * **What a model returns from a light** is the two halves the standard terms are: a diffuse already
19
+ * weighted by the angle of arrival, and a specular lobe in the engine's peak-normalised units (see
20
+ * `specularLobe`), both before any shadow — the caller shadows the specular, and the diffuse through
21
+ * `modelShade`, which is the identity but for skin, whose penumbra reddens.
22
+ */
23
+ import { SURFACE_MODEL_SWITCH } from '../../surfaceModel.ts';
24
+ import type { SurfaceModelKind } from '../../surfaceModel.ts';
25
+ import { ANISOTROPIC_GLSL } from './anisotropic.ts';
26
+ import { HAIR_GLSL } from './hair.ts';
27
+ import { SKIN_GLSL } from './skin.ts';
28
+ import { EYE_GLSL } from './eye.ts';
29
+
30
+ /** The switches, the numbers, the surface every model reads, and the standard answer it falls back on. */
31
+ const MODELS_HEAD = /* glsl */ `
32
+ /* Which model a pipeline shades by. Each off but where a material asks for it: see models.ts. */
33
+ const bool MODEL_ANISOTROPIC = false; // wgsl:override
34
+ const bool MODEL_HAIR = false; // wgsl:override
35
+ const bool MODEL_SKIN = false; // wgsl:override
36
+ const bool MODEL_EYE = false; // wgsl:override
37
+ /*
38
+ * Skin's screen-space scattering, as two halves of one surface: the scene pass without its diffuse,
39
+ * and a pass that is nothing but it, which the blur spreads before it is added back. Both off but
40
+ * where \`skinScattering\` is \`'screen-space'\`; see \`skinBlur.ts\`.
41
+ */
42
+ const bool SKIN_SCREEN = false; // wgsl:override
43
+ const bool SKIN_DIFFUSE = false; // wgsl:override
44
+
45
+ /*
46
+ * A model's numbers, two vectors a material — the lit stage's uniform budget has no more room
47
+ * (surfaceModel.ts) — and whether a model map is bound, in the last component.
48
+ */
49
+ uniform vec4 uModelParams[2];
50
+ /* The model's own channels, in the albedo's layout: what each means is each model's to say. */
51
+ uniform highp sampler2DArray uModelMap;
52
+
53
+ /** Whether this pipeline shades by any model but the standard one. */
54
+ bool modelOn() {
55
+ if (MODEL_ANISOTROPIC) return true;
56
+ if (MODEL_HAIR) return true;
57
+ if (MODEL_SKIN) return true;
58
+ if (MODEL_EYE) return true;
59
+ return false;
60
+ }
61
+
62
+ /** Whether this pipeline draws skin's diffuse alone, for the screen-space blur. */
63
+ bool modelDiffuseAlone() {
64
+ if (MODEL_SKIN) {
65
+ if (SKIN_DIFFUSE) return true;
66
+ }
67
+ return false;
68
+ }
69
+
70
+ /** Whether light from behind the surface reaches the eye through it: hair, and skin's thin parts. */
71
+ bool modelSeesBehind() {
72
+ if (MODEL_HAIR) return true;
73
+ if (MODEL_SKIN) return true;
74
+ return false;
75
+ }
76
+
77
+ /* The surface a model shades, gathered once in main before any light reaches it. */
78
+ vec3 mNormal;
79
+ vec3 mToEye;
80
+ vec3 mTangent;
81
+ vec3 mBitangent;
82
+ vec3 mAlbedo;
83
+ vec3 mSpecColor;
84
+ float mRoughness;
85
+ float mMetal;
86
+ vec4 mMap;
87
+ /*
88
+ * What \`modelLight\` leaves: the diffuse weighted by its angle and the lobe, neither shadowed.
89
+ * Globals rather than out parameters, because every parameter a call passes is a local, a store and
90
+ * a pointer at each call site in \`main\` — sixteen copies of it, one a permutation.
91
+ */
92
+ vec3 mDiffuse;
93
+ vec3 mSpecular;
94
+
95
+ /**
96
+ * Normalised GGX — the distribution itself, not the engine's peak-normalised look — for the models
97
+ * whose highlight is physical: skin's and the cornea's.
98
+ */
99
+ float ggxLobe(float ndh, float alpha) {
100
+ float a2 = alpha * alpha;
101
+ float d = ndh * ndh * (a2 - 1.0) + 1.0;
102
+ return a2 / (3.14159265 * d * d);
103
+ }
104
+
105
+ /** Smith's height-correlated masking with the BRDF's 1 / 4 N·L N·V folded in, for the same. */
106
+ float smithMasking(float ndl, float ndv, float alpha) {
107
+ float a2 = alpha * alpha;
108
+ return 0.5 / max(
109
+ ndl * sqrt(ndv * ndv * (1.0 - a2) + a2) + ndv * sqrt(ndl * ndl * (1.0 - a2) + a2),
110
+ 1e-5
111
+ );
112
+ }
113
+
114
+ /** The standard model's answer to one light, which a model falls back to for what it does not change. */
115
+ void standardLight(vec3 l, float sourceRadius, float dist) {
116
+ float ndl = max(dot(mNormal, l), 0.0);
117
+ vec3 h = normalize(l + mToEye);
118
+ float ndh = max(dot(mNormal, h), 0.0);
119
+ float lobe = sourceRadius > 0.0
120
+ ? sphereLobe(ndh, mRoughness, sourceRadius, dist)
121
+ : specularLobe(ndh, mRoughness);
122
+ vec3 tint = mix(mSpecColor, vec3(1.0), pow(1.0 - max(dot(mToEye, h), 0.0), 5.0) * mMetal);
123
+ mDiffuse = mAlbedo * ndl * (1.0 - mMetal);
124
+ mSpecular = ndl > 0.0 ? tint * lobe : vec3(0.0);
125
+ }
126
+
127
+ `;
128
+
129
+ /** Where `main` asks: each model's answer, and the standard's where a model keeps it. */
130
+ const MODELS_DISPATCH = /* glsl */ `
131
+ /**
132
+ * Where the material's maps are read: the surface's own coordinate, but for an eye, whose iris is
133
+ * seen through its cornea and moved by it (\`eyeRefraction.ts\`). Before any map is read, and on a
134
+ * branch on the switches alone, so its derivatives are in uniform control flow.
135
+ */
136
+ vec3 modelSurfaceAt(vec3 at) {
137
+ if (MODEL_EYE) return eyeRefract(at);
138
+ return at;
139
+ }
140
+
141
+ /**
142
+ * Gather the surface. The frame and the map read derivatives, so this is called under a branch on
143
+ * the switches alone, which is uniform control flow by construction.
144
+ */
145
+ void modelSurface(vec3 n, vec3 albedo, float roughness, float metal, vec3 at) {
146
+ mNormal = n;
147
+ mToEye = normalize(uCameraPos - vWorldPos);
148
+ mat3 frame = tangentFrame(n, vWorldPos, vUv.xy, vTangent, vHasTangents);
149
+ mTangent = frame[0];
150
+ mBitangent = frame[1];
151
+ mAlbedo = albedo;
152
+ mSpecColor = mix(vec3(vSpecular), albedo, metal);
153
+ mRoughness = roughness;
154
+ mMetal = metal;
155
+ mMap = vec4(0.5, 0.5, 1.0, 1.0);
156
+ if (uModelParams[1].w > 0.0) mMap = texture(uModelMap, at);
157
+ if (MODEL_ANISOTROPIC) anisotropicSurface();
158
+ if (MODEL_HAIR) hairSurface();
159
+ if (MODEL_SKIN) skinSurface();
160
+ if (MODEL_EYE) eyeSurface(at);
161
+ }
162
+
163
+ /**
164
+ * One light, arriving along \`l\` (unit, toward the light), from a source \`sourceRadius\` across at
165
+ * \`dist\` — 0 for the sun. Into \`mDiffuse\`, weighted by its angle, and \`mSpecular\`, in lobe
166
+ * units, neither shadowed.
167
+ */
168
+ void modelLight(vec3 l, float sourceRadius, float dist) {
169
+ if (MODEL_ANISOTROPIC) {
170
+ anisotropicLight(l, sourceRadius, dist);
171
+ return;
172
+ }
173
+ if (MODEL_HAIR) {
174
+ hairLight(l, sourceRadius, dist);
175
+ return;
176
+ }
177
+ if (MODEL_SKIN) {
178
+ skinLight(l, sourceRadius, dist);
179
+ return;
180
+ }
181
+ if (MODEL_EYE) {
182
+ eyeLight(l, sourceRadius, dist);
183
+ return;
184
+ }
185
+ standardLight(l, sourceRadius, dist);
186
+ }
187
+
188
+ /** How a shadow of \`shadow\` darkens this model's diffuse: the identity, but for hair and skin. */
189
+ vec3 modelShade(float shadow) {
190
+ if (MODEL_HAIR) return hairShade(shadow);
191
+ if (MODEL_SKIN) return skinShade(shadow);
192
+ return vec3(shadow);
193
+ }
194
+
195
+ /** The sky's or the probe's light on this surface, before any reflection. */
196
+ vec3 modelAmbient(vec3 ambient) {
197
+ if (MODEL_HAIR) return hairAround(ambient);
198
+ if (MODEL_SKIN) {
199
+ if (SKIN_SCREEN) return vec3(0.0);
200
+ }
201
+ return mAlbedo * ambient;
202
+ }
203
+
204
+ /** Light arriving from every side at once — DriftLight's — as this surface returns it. */
205
+ vec3 modelIrradiance(vec3 irradiance) {
206
+ if (MODEL_HAIR) return hairAround(irradiance);
207
+ if (MODEL_SKIN) {
208
+ if (SKIN_SCREEN) return vec3(0.0);
209
+ }
210
+ return mAlbedo * irradiance * (1.0 - mMetal);
211
+ }
212
+
213
+ /** The normal the environment is reflected about: the surface's own, but for a stretched lobe. */
214
+ vec3 modelReflectNormal(vec3 n) {
215
+ if (MODEL_ANISOTROPIC) return anisotropicReflectNormal(n);
216
+ if (MODEL_HAIR) return hairAcross();
217
+ return n;
218
+ }
219
+
220
+ /**
221
+ * The environment the reflection blends toward: black in skin's diffuse pass, so the blend dims
222
+ * the light before it by the reflection's share — as the whole surface's does, and only that light,
223
+ * the lamps arriving after it — and adds none of the reflection, which is the scene pass's half.
224
+ */
225
+ vec3 modelEnvironment(vec3 environment) {
226
+ if (MODEL_SKIN) {
227
+ if (SKIN_DIFFUSE) return vec3(0.0);
228
+ }
229
+ return environment;
230
+ }
231
+
232
+ /** Skin's diffuse as the diffuse pass writes it, from the lit total before emission, fogged. */
233
+ vec3 modelDiffuseOut(vec3 diffuse, float fog) {
234
+ return diffuse * (1.0 - fog);
235
+ }
236
+
237
+ /**
238
+ * How much of the environment the surface reflects — the blend's weight, Fresnel and all — from
239
+ * what the standard model would.
240
+ */
241
+ float modelReflectance(float standard) {
242
+ if (MODEL_HAIR) return hairReflectance();
243
+ if (MODEL_SKIN) return skinReflectance();
244
+ if (MODEL_EYE) return eyeReflectance();
245
+ return standard;
246
+ }
247
+ `;
248
+
249
+ /** The models' whole chunk, at depth zero before `main`: see `flatFrag`. */
250
+ export const MODELS_GLSL = [
251
+ MODELS_HEAD,
252
+ ANISOTROPIC_GLSL,
253
+ HAIR_GLSL,
254
+ SKIN_GLSL,
255
+ EYE_GLSL,
256
+ MODELS_DISPATCH,
257
+ ].join('\n');
258
+
259
+ /**
260
+ * Which half of a skin a lit pipeline draws under `skinScattering: 'screen-space'`: the whole
261
+ * surface (every other pipeline), the frame's half without its diffuse, or the diffuse alone into
262
+ * the blur's target — that last one depth-tested for equality against what the frame's half wrote
263
+ * and writing none, since it is the same surface drawn again. See `skinBlur.ts`.
264
+ */
265
+ export type SkinHalf = 'whole' | 'scene' | 'diffuse';
266
+
267
+ /** A skin lit stage with one half's switch on, WebGL2's way: see `SkinHalf`. */
268
+ export function skinHalfBound(source: string, half: SkinHalf): string {
269
+ if (half === 'whole') return source;
270
+ const name = half === 'scene' ? 'SKIN_SCREEN' : 'SKIN_DIFFUSE';
271
+ const off = `const bool ${name} = false;`;
272
+ if (!source.includes(off)) {
273
+ throw new Error(`skinHalfBound: this lit stage declares no ${name} constant`);
274
+ }
275
+ return source.replace(off, `const bool ${name} = true;`);
276
+ }
277
+
278
+ /** A lit stage with one model's switch on: the same source, the constant flipped — WebGL2's way. */
279
+ export function modelBound(source: string, kind: SurfaceModelKind): string {
280
+ const name = SURFACE_MODEL_SWITCH[kind];
281
+ const off = `const bool ${name} = false;`;
282
+ if (!source.includes(off)) {
283
+ throw new Error(`modelBound: this lit stage declares no ${name} constant`);
284
+ }
285
+ return source.replace(off, `const bool ${name} = true;`);
286
+ }
@@ -139,8 +139,22 @@ flat in int vHasTangents;
139
139
  * white shades identically to one with no texture at all.
140
140
  */
141
141
  uniform mediump sampler2DArray uAlbedo;
142
- /** 0 leaves every textured path unevaluated, for the majority of geometry with no image. */
143
- uniform int uAlbedoEnabled;
142
+ /**
143
+ * **The material's four switches in one row**, because every loose scalar is a row of the 256 an
144
+ * Adreno 740 offers and the eight-light build had none to spare once a shading model asked for two
145
+ * (\`models.ts\`): four ints were four rows, and this is one.
146
+ *
147
+ * - **x, the albedo**: 0 leaves every textured path unevaluated, for the majority of geometry with
148
+ * no image.
149
+ * - **y, the ORM map**: 1 when one is bound, gating its whole block. A uniform rather than a
150
+ * permutation flag, because a fifth flag takes the sixteen permutations flatSource.test.ts sweeps
151
+ * to thirty-two and roughly doubles the generated WGSL every consumer bundles.
152
+ * - **z, the emissive map**: 1 when one is bound, gating its sample, for the same reason.
153
+ * - **w, two-sided**: 1 for a surface seen from both faces — glTF's doubleSided, a curtain, a leaf
154
+ * card. Its draw culls nothing, and a back face is lit as a front, with the shading normal turned
155
+ * toward the viewer.
156
+ */
157
+ uniform ivec4 uMaterialFlags;
144
158
  /**
145
159
  * Alpha below which a textured fragment is thrown away entirely, 0 to disable.
146
160
  *
@@ -152,8 +166,13 @@ uniform int uAlbedoEnabled;
152
166
  *
153
167
  * Zero by default, so a texture with no alpha to speak of costs one compare and nothing
154
168
  * else changes.
169
+ *
170
+ * **Four numbers in one row**: x is the cutoff, y how this frame resolves the edge (0 the hard test,
171
+ * 1 a dither the temporal resolve averages, 2 alpha-to-coverage; see \`cutoutDither.ts\`), z the
172
+ * frame the dither's pattern is offset by, w spare. One vector rather than a float and two more,
173
+ * because every loose scalar is a row of WebGL2's 224 that the light budget gives up.
155
174
  */
156
- uniform float uAlbedoCutout;
175
+ uniform vec4 uCutout;
157
176
  /**
158
177
  * The albedo array's per-layer effects table: six texels a layer, read with \`texelFetch\` at the
159
178
  * layer a vertex carries. A 1×1 stand-in where the array has none, which the size test below reads
@@ -161,11 +180,6 @@ uniform float uAlbedoCutout;
161
180
  */
162
181
  uniform mediump sampler2D uSurfaceEffects;
163
182
 
164
- /**
165
- * 1 for a surface seen from both faces: glTF's doubleSided, a curtain or a leaf card. Its draw culls
166
- * nothing, and a back face is lit as a front, with the shading normal turned toward the viewer.
167
- */
168
- uniform int uDoubleSided;
169
183
 
170
184
  /**
171
185
  * A normal map, in the surface's own space.
@@ -181,7 +195,7 @@ uniform highp sampler2DArray uNormalMap;
181
195
  /**
182
196
  * How hard the map turns the shading normal. 0 is no map at all, and gates the whole block.
183
197
  *
184
- * A uniform rather than a permutation flag, following uAlbedoEnabled: a fifth boolean would take
198
+ * A uniform rather than a permutation flag, following the albedo's flag: a fifth boolean would take
185
199
  * the exhaustive permutation sweep from sixteen shaders to thirty-two, and the generated WGSL that
186
200
  * consumers bundle as source from 766 KB to about 1.5 MB. A permutation is the right tool for the
187
201
  * shadow samplers, where a profile with shadows off should declare none of them; it is the wrong
@@ -200,16 +214,6 @@ uniform float uNormalStrength;
200
214
  * decoding them as display values would bend every one of them toward its floor.
201
215
  */
202
216
  uniform highp sampler2DArray uOrmMap;
203
- /**
204
- * 1 when a map is bound. Gates the whole block, following uAlbedoEnabled.
205
- *
206
- * A uniform rather than a fifth permutation flag: FlatShaderOptions has four booleans and
207
- * flatSource.test.ts sweeps all sixteen exhaustively, so a fifth takes that to thirty-two and
208
- * roughly doubles the generated WGSL that consumers bundle as source, this engine having no build
209
- * step. A permutation is the right tool for the shadow samplers, where a profile with shadows off
210
- * should declare none of them. It is the wrong tool for one more image.
211
- */
212
- uniform int uOrmEnabled;
213
217
  /**
214
218
  * What each channel is scaled by, component-aligned with the map: r occlusion, g roughness,
215
219
  * b metallic. The alignment is the documentation.
@@ -227,7 +231,7 @@ uniform vec3 uOrmScale;
227
231
  * wants a roughness combined in quadrature. Likewise the weight: the split sum is the correct share
228
232
  * of a *prefiltered* environment and asserts an integral that was never performed over a box chain.
229
233
  *
230
- * A uniform rather than a permutation flag, following \`uOrmEnabled\`: a fifth boolean doubles the
234
+ * A uniform rather than a permutation flag, following the ORM map's in \`uMaterialFlags\`: a fifth boolean doubles the
231
235
  * generated WGSL every consumer bundles as source, and this is a few multiply-adds.
232
236
  */
233
237
  uniform float uEnvironmentPrefiltered;
@@ -241,13 +245,11 @@ uniform float uEnvironmentPrefiltered;
241
245
  * where it should be saturated — but it is silent, because a glow that is too dim reads as a
242
246
  * material choice rather than as a decode.
243
247
  *
244
- * A uniform gate rather than a permutation flag, for exactly the reasons \`uOrmEnabled\` gives
248
+ * A uniform gate rather than a permutation flag, for exactly the reasons \`uMaterialFlags\` gives
245
249
  * above, and now with a number behind them: a fifth flag was built and measured at 49% of the
246
250
  * bundle. See \`ARCHITECTURE.md\` §1.
247
251
  */
248
252
  uniform highp sampler2DArray uEmissiveMap;
249
- /** 1 when a map is bound. Gates the sample, following uOrmEnabled. */
250
- uniform int uEmissiveMapEnabled;
251
253
  /**
252
254
  * What the map is scaled by, per channel. 1 is the identity and the default.
253
255
  *
@@ -423,7 +425,7 @@ bool ditherKeeps(float amount) {
423
425
  * block member the shader does not already carry the size of; it is one \`int\` beside
424
426
  * \`uOpacity\`, read once. And the branch it drives is on a value that is the same for every
425
427
  * fragment in one draw call, which is exactly the guarantee \`uReflectivity\` and
426
- * \`uAlbedoEnabled\` already lean on above: a compiler asked to predicate a whole wavefront on
428
+ * \`uMaterialFlags\` already lean on above: a compiler asked to predicate a whole wavefront on
427
429
  * one uniform skips the untaken side rather than computing both and discarding one, which is
428
430
  * only true because nothing here reads it through a *varying*-dependent branch.
429
431
  */