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