@driftengine/core 4.9.0 → 4.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/CHANGELOG.json +133 -11
  2. package/README.md +43 -38
  3. package/dist/index.d.ts +7 -4
  4. package/dist/index.js +5 -4
  5. package/dist/render/ambientHarmonics.d.ts +1 -1
  6. package/dist/render/ambientHarmonics.js +1 -1
  7. package/dist/render/areaLights.d.ts +4 -4
  8. package/dist/render/areaLights.js +1 -1
  9. package/dist/render/backend/api.d.ts +10 -3
  10. package/dist/render/backend/select.js +11 -5
  11. package/dist/render/backend/webgl2/boneAnimations.d.ts +29 -0
  12. package/dist/render/backend/webgl2/boneAnimations.js +80 -0
  13. package/dist/render/backend/webgl2/instanced.d.ts +5 -1
  14. package/dist/render/backend/webgl2/instanced.js +13 -1
  15. package/dist/render/backend/webgl2/renderer.d.ts +189 -4
  16. package/dist/render/backend/webgl2/renderer.js +614 -66
  17. package/dist/render/backend/webgl2/skinPaletteTexture.d.ts +5 -3
  18. package/dist/render/backend/webgl2/skinPaletteTexture.js +6 -4
  19. package/dist/render/backend/webgpu/boneAnimations.d.ts +34 -0
  20. package/dist/render/backend/webgpu/boneAnimations.js +76 -0
  21. package/dist/render/backend/webgpu/buffers.d.ts +24 -4
  22. package/dist/render/backend/webgpu/buffers.js +116 -59
  23. package/dist/render/backend/webgpu/clothKernels.d.ts +8 -0
  24. package/dist/render/backend/webgpu/clothKernels.js +76 -0
  25. package/dist/render/backend/webgpu/clothOps.d.ts +39 -0
  26. package/dist/render/backend/webgpu/clothOps.js +78 -0
  27. package/dist/render/backend/webgpu/clothSetLayout.d.ts +25 -0
  28. package/dist/render/backend/webgpu/clothSetLayout.js +84 -0
  29. package/dist/render/backend/webgpu/clothSetPack.d.ts +33 -0
  30. package/dist/render/backend/webgpu/clothSetPack.js +144 -0
  31. package/dist/render/backend/webgpu/compressedUpload.d.ts +0 -1
  32. package/dist/render/backend/webgpu/compressedUpload.js +53 -8
  33. package/dist/render/backend/webgpu/depthPass.d.ts +59 -3
  34. package/dist/render/backend/webgpu/depthPass.js +49 -14
  35. package/dist/render/backend/webgpu/drawCommand.d.ts +20 -4
  36. package/dist/render/backend/webgpu/drawCommand.js +11 -3
  37. package/dist/render/backend/webgpu/flatPass.d.ts +85 -42
  38. package/dist/render/backend/webgpu/flatPass.js +137 -20
  39. package/dist/render/backend/webgpu/gpuClothSet.d.ts +80 -0
  40. package/dist/render/backend/webgpu/gpuClothSet.js +462 -0
  41. package/dist/render/backend/webgpu/instanced.d.ts +14 -1
  42. package/dist/render/backend/webgpu/instanced.js +24 -1
  43. package/dist/render/backend/webgpu/latePass.js +1 -0
  44. package/dist/render/backend/webgpu/materialStore.d.ts +101 -0
  45. package/dist/render/backend/webgpu/materialStore.js +265 -0
  46. package/dist/render/backend/webgpu/motionPass.js +15 -6
  47. package/dist/render/backend/webgpu/pipelineCache.d.ts +28 -1
  48. package/dist/render/backend/webgpu/pipelineCache.js +46 -1
  49. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  50. package/dist/render/backend/webgpu/renderer.d.ts +200 -35
  51. package/dist/render/backend/webgpu/renderer.js +993 -168
  52. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +9 -2
  53. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +121 -47
  54. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +7 -3
  55. package/dist/render/backend/webgpu/skinPaletteRing.js +13 -7
  56. package/dist/render/backend/webgpu/skinPaletteTexture.d.ts +6 -0
  57. package/dist/render/backend/webgpu/skinPaletteTexture.js +7 -1
  58. package/dist/render/backend/webgpu/staticBundles.d.ts +125 -0
  59. package/dist/render/backend/webgpu/staticBundles.js +242 -0
  60. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +10 -3
  61. package/dist/render/backend/webgpu/surfaceTexturePass.js +20 -2
  62. package/dist/render/backend/webgpu/uniformRing.d.ts +21 -0
  63. package/dist/render/backend/webgpu/uniformRing.js +9 -1
  64. package/dist/render/backend/webgpu/vertexFormats.d.ts +22 -0
  65. package/dist/render/backend/webgpu/vertexFormats.js +97 -0
  66. package/dist/render/bloomChain.d.ts +3 -3
  67. package/dist/render/boneAnimation.d.ts +92 -0
  68. package/dist/render/boneAnimation.js +133 -0
  69. package/dist/render/compressedSource.d.ts +33 -13
  70. package/dist/render/compressedSource.js +70 -25
  71. package/dist/render/glCompressed.js +83 -0
  72. package/dist/render/globalMediumPass.d.ts +2 -0
  73. package/dist/render/globalMediumPass.js +1 -0
  74. package/dist/render/indexWidth.d.ts +17 -0
  75. package/dist/render/indexWidth.js +23 -0
  76. package/dist/render/instances.d.ts +26 -0
  77. package/dist/render/lightBudget.d.ts +10 -0
  78. package/dist/render/lightBudget.js +10 -0
  79. package/dist/render/lightMatrix.d.ts +11 -0
  80. package/dist/render/lightMatrix.js +14 -0
  81. package/dist/render/lightmap.d.ts +1 -1
  82. package/dist/render/mesh.d.ts +14 -0
  83. package/dist/render/mesh.js +44 -14
  84. package/dist/render/pointLightSelection.d.ts +4 -4
  85. package/dist/render/renderQuality.d.ts +30 -0
  86. package/dist/render/renderQuality.js +5 -0
  87. package/dist/render/rendererHarness.d.ts +7 -0
  88. package/dist/render/rendererHarness.js +12 -0
  89. package/dist/render/screenSpaceReflection.d.ts +2 -1
  90. package/dist/render/shader.d.ts +15 -0
  91. package/dist/render/shader.js +41 -0
  92. package/dist/render/shaders/boneAnimation.d.ts +19 -0
  93. package/dist/render/shaders/boneAnimation.js +63 -0
  94. package/dist/render/shaders/depth.d.ts +7 -0
  95. package/dist/render/shaders/depth.js +35 -10
  96. package/dist/render/shaders/flat/directionalShadow.js +84 -10
  97. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  98. package/dist/render/shaders/flat/glassTint.js +3 -0
  99. package/dist/render/shaders/flat/index.d.ts +21 -0
  100. package/dist/render/shaders/flat/index.js +42 -7
  101. package/dist/render/shaders/flat/layered.d.ts +19 -0
  102. package/dist/render/shaders/flat/layered.js +115 -0
  103. package/dist/render/shaders/flat/main.d.ts +1 -1
  104. package/dist/render/shaders/flat/main.js +58 -21
  105. package/dist/render/shaders/flat/mapReads.d.ts +10 -0
  106. package/dist/render/shaders/flat/mapReads.js +38 -0
  107. package/dist/render/shaders/flat/models.js +1 -1
  108. package/dist/render/shaders/flat/movingSun.d.ts +21 -0
  109. package/dist/render/shaders/flat/movingSun.js +46 -0
  110. package/dist/render/shaders/flat/normalMap.d.ts +6 -2
  111. package/dist/render/shaders/flat/normalMap.js +14 -3
  112. package/dist/render/shaders/flat/overlay.d.ts +3 -2
  113. package/dist/render/shaders/flat/overlay.js +42 -20
  114. package/dist/render/shaders/flat/pointShadow.js +2 -2
  115. package/dist/render/shaders/flat/preamble.js +59 -27
  116. package/dist/render/shaders/flat/worldUv.d.ts +22 -0
  117. package/dist/render/shaders/flat/worldUv.js +113 -0
  118. package/dist/render/shaders/generated/depth.wgsl.d.ts +93 -0
  119. package/dist/render/shaders/generated/depth.wgsl.js +93 -0
  120. package/dist/render/shaders/generated/flat.wgsl.d.ts +3616 -3042
  121. package/dist/render/shaders/generated/flat.wgsl.js +3688 -3113
  122. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +8 -3
  123. package/dist/render/shaders/generated/globalMedium.wgsl.js +8 -3
  124. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +15 -10
  125. package/dist/render/shaders/generated/lightVolume.wgsl.js +16 -11
  126. package/dist/render/shaders/globalMedium.js +9 -1
  127. package/dist/render/shaders/lightVolume.js +9 -1
  128. package/dist/render/shaders/outputTransform.d.ts +7 -0
  129. package/dist/render/shaders/outputTransform.js +28 -14
  130. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  131. package/dist/render/shaders/tangentFrame.js +16 -3
  132. package/dist/render/shaders/vertexChannel.d.ts +7 -0
  133. package/dist/render/shaders/vertexChannel.js +26 -0
  134. package/dist/render/skinnedClothRun.d.ts +28 -0
  135. package/dist/render/skinnedClothRun.js +103 -35
  136. package/dist/render/staticDraws.d.ts +47 -0
  137. package/dist/render/staticDraws.js +43 -0
  138. package/dist/render/surfaceLayers.d.ts +39 -0
  139. package/dist/render/surfaceLayers.js +26 -0
  140. package/dist/render/surfaceOverlay.d.ts +16 -3
  141. package/dist/render/surfaceOverlay.js +11 -6
  142. package/dist/render/surfaceProjection.d.ts +34 -0
  143. package/dist/render/surfaceProjection.js +30 -0
  144. package/dist/render/surfaceTexture.d.ts +68 -7
  145. package/dist/render/surfaceTexture.js +56 -17
  146. package/dist/render/vertexPacking.d.ts +48 -0
  147. package/dist/render/vertexPacking.js +68 -0
  148. package/package.json +6 -6
  149. package/src/index.ts +12 -4
  150. package/src/render/ambientHarmonics.ts +1 -1
  151. package/src/render/areaLights.ts +4 -4
  152. package/src/render/backend/api.ts +13 -3
  153. package/src/render/backend/select.ts +10 -5
  154. package/src/render/backend/webgl2/boneAnimations.ts +105 -0
  155. package/src/render/backend/webgl2/instanced.ts +20 -1
  156. package/src/render/backend/webgl2/renderer.ts +723 -79
  157. package/src/render/backend/webgl2/skinPaletteTexture.ts +6 -4
  158. package/src/render/backend/webgpu/boneAnimations.ts +97 -0
  159. package/src/render/backend/webgpu/buffers.ts +195 -70
  160. package/src/render/backend/webgpu/clothKernels.ts +86 -0
  161. package/src/render/backend/webgpu/clothOps.ts +86 -0
  162. package/src/render/backend/webgpu/clothSetLayout.ts +102 -0
  163. package/src/render/backend/webgpu/clothSetPack.ts +197 -0
  164. package/src/render/backend/webgpu/compressedUpload.ts +59 -9
  165. package/src/render/backend/webgpu/depthPass.ts +51 -10
  166. package/src/render/backend/webgpu/drawCommand.ts +25 -4
  167. package/src/render/backend/webgpu/flatPass.ts +212 -41
  168. package/src/render/backend/webgpu/gpuClothSet.ts +564 -0
  169. package/src/render/backend/webgpu/instanced.ts +32 -1
  170. package/src/render/backend/webgpu/latePass.ts +1 -0
  171. package/src/render/backend/webgpu/materialStore.ts +276 -0
  172. package/src/render/backend/webgpu/motionPass.ts +18 -7
  173. package/src/render/backend/webgpu/pipelineCache.ts +53 -7
  174. package/src/render/backend/webgpu/renderer.ts +1201 -202
  175. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +121 -47
  176. package/src/render/backend/webgpu/skinPaletteRing.ts +13 -7
  177. package/src/render/backend/webgpu/skinPaletteTexture.ts +7 -1
  178. package/src/render/backend/webgpu/staticBundles.ts +364 -0
  179. package/src/render/backend/webgpu/surfaceTexturePass.ts +29 -3
  180. package/src/render/backend/webgpu/uniformRing.ts +10 -1
  181. package/src/render/backend/webgpu/vertexFormats.ts +113 -0
  182. package/src/render/bloomChain.ts +3 -3
  183. package/src/render/boneAnimation.ts +223 -0
  184. package/src/render/compressedSource.ts +121 -29
  185. package/src/render/glCompressed.ts +83 -0
  186. package/src/render/globalMediumPass.ts +7 -0
  187. package/src/render/indexWidth.ts +27 -0
  188. package/src/render/instances.ts +27 -0
  189. package/src/render/lightBudget.ts +11 -0
  190. package/src/render/lightMatrix.ts +15 -0
  191. package/src/render/lightmap.ts +1 -1
  192. package/src/render/mesh.ts +72 -14
  193. package/src/render/pointLightSelection.ts +4 -4
  194. package/src/render/renderQuality.ts +38 -0
  195. package/src/render/rendererHarness.ts +15 -0
  196. package/src/render/screenSpaceReflection.ts +2 -1
  197. package/src/render/shader.ts +59 -0
  198. package/src/render/shaders/boneAnimation.ts +63 -0
  199. package/src/render/shaders/depth.ts +45 -10
  200. package/src/render/shaders/flat/directionalShadow.ts +84 -10
  201. package/src/render/shaders/flat/glassTint.ts +7 -1
  202. package/src/render/shaders/flat/index.ts +63 -7
  203. package/src/render/shaders/flat/layered.ts +115 -0
  204. package/src/render/shaders/flat/main.ts +58 -21
  205. package/src/render/shaders/flat/mapReads.ts +38 -0
  206. package/src/render/shaders/flat/models.ts +1 -1
  207. package/src/render/shaders/flat/movingSun.ts +46 -0
  208. package/src/render/shaders/flat/normalMap.ts +14 -3
  209. package/src/render/shaders/flat/overlay.ts +42 -20
  210. package/src/render/shaders/flat/pointShadow.ts +2 -2
  211. package/src/render/shaders/flat/preamble.ts +59 -27
  212. package/src/render/shaders/flat/worldUv.ts +113 -0
  213. package/src/render/shaders/generated/depth.wgsl.ts +95 -0
  214. package/src/render/shaders/generated/flat.wgsl.ts +3688 -3113
  215. package/src/render/shaders/generated/globalMedium.wgsl.ts +8 -3
  216. package/src/render/shaders/generated/lightVolume.wgsl.ts +16 -11
  217. package/src/render/shaders/globalMedium.ts +9 -1
  218. package/src/render/shaders/lightVolume.ts +9 -1
  219. package/src/render/shaders/outputTransform.ts +30 -14
  220. package/src/render/shaders/tangentFrame.ts +16 -3
  221. package/src/render/shaders/vertexChannel.ts +27 -0
  222. package/src/render/skinnedClothRun.ts +138 -38
  223. package/src/render/staticDraws.ts +95 -0
  224. package/src/render/surfaceLayers.ts +57 -0
  225. package/src/render/surfaceOverlay.ts +24 -7
  226. package/src/render/surfaceProjection.ts +57 -0
  227. package/src/render/surfaceTexture.ts +124 -27
  228. package/src/render/vertexPacking.ts +109 -0
  229. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +0 -74
  230. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +0 -452
  231. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +0 -502
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Where each of a material's maps is read: a layered material's blend (`layered.ts`), the three
3
+ * planes of a triplanar projection (`worldUv.ts`), or the map itself at the surface's coordinates.
4
+ *
5
+ * **Functions rather than branches in `main`, for the bundle's sake.** The generated WGSL stores an
6
+ * item once however many of the sixteen permutations share it, and `main` differs in every one, so
7
+ * a line there is paid sixteen times and a line here once. Called from the same uniform branches the
8
+ * reads stood in, so their implicit levels are as legal as they were: AGENTS.md 2026-08-07.
9
+ */
10
+ export const MAP_READS_GLSL = /* glsl */ `
11
+ /* The surface's coordinates moved onto the world's horizontal plane where a material projects them,
12
+ with the frame's coordinates and the mesh's tangents, which no longer run along them, set aside. */
13
+ void projectSurface(inout vec3 at, inout vec2 frameAt, inout int frameTangents) {
14
+ if (WORLD_UVS && projected()) {
15
+ at.xy = planarAt();
16
+ frameAt = at.xy;
17
+ frameTangents = 0;
18
+ }
19
+ }
20
+
21
+ vec4 albedoTexel(vec3 at) {
22
+ if (LAYERED && layered()) return layeredAlbedo();
23
+ if (WORLD_UVS && triplanar()) return triplanarAlbedo(at.z);
24
+ return texture(uAlbedo, at);
25
+ }
26
+
27
+ vec3 ormTexel(vec3 at) {
28
+ if (LAYERED && layered()) return layeredOrm();
29
+ if (WORLD_UVS && triplanar()) return triplanarOrm(at.z);
30
+ return texture(uOrmMap, at).rgb;
31
+ }
32
+
33
+ vec3 emissiveTexel(vec3 at) {
34
+ if (LAYERED && layered() && layeredGlows()) return layeredEmissive();
35
+ if (WORLD_UVS && triplanar()) return triplanarEmissive(at.z);
36
+ return texture(uEmissiveMap, at).rgb;
37
+ }
38
+ `;
@@ -59,7 +59,7 @@ const bool PHYSICAL_SPECULAR = PHYSICAL_SPECULAR_ON; // wgsl:override
59
59
  * A model's numbers, two vectors a material — the lit stage's uniform budget has no more room
60
60
  * (surfaceModel.ts) — and whether a model map is bound, in the last component.
61
61
  */
62
- uniform vec4 uModelParams[2];
62
+ uniform vec4 uModelParams[2]; // wgsl:material
63
63
  /* The model's own channels, in the albedo's layout: what each means is each model's to say. */
64
64
  uniform highp sampler2DArray uModelMap;
65
65
 
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The sun's moving layer read through a matrix of its own: the one the frame's dynamic shadow pass
3
+ * was drawn with, where that is not the static layer's.
4
+ *
5
+ * **Why a matrix of its own.** The static layer wants a square over the whole of what stands still,
6
+ * so distant roofs keep their shadows; what moves wants a tight one, so a character's shadow is
7
+ * sharp. One matrix for both is one of the two given up. A caller fits each as it likes and draws
8
+ * each pass with its own (`beginShadowPass(matrix, 'dynamic')`); the renderer keeps the dynamic
9
+ * pass's and the lit stage reads that layer through it, the receiver's position projected from the
10
+ * world here rather than carried in a second varying.
11
+ *
12
+ * **A lit switch, `MOVING_SUN`**, off until a frame's dynamic pass is drawn with a matrix the static
13
+ * layer does not share, then on for good. Declared beside the other lit switches, ahead of the
14
+ * shadow lookup that calls it; compiled out, the moving layer is read at the static layer's place,
15
+ * exactly as it always was, and its matrix and depth span are not declared at all.
16
+ *
17
+ * **What it gives up**: a receiver outside the static layer's square takes no sun shadow at all,
18
+ * moving casters included, since everything is faded by that square first; so the tight square
19
+ * belongs inside the wide one.
20
+ */
21
+ export function movingSunGlsl(on: boolean): string {
22
+ if (!on) {
23
+ return /* glsl */ `
24
+ const bool MOVING_SUN = false; // wgsl:override
25
+ vec3 movingSunAt() { return vec3(0.0); }
26
+ float movingSunSpan() { return 1.0; }
27
+ `;
28
+ }
29
+ return /* glsl */ `
30
+ /* Whether the moving layer has a matrix of its own. Off until a frame draws it with one: movingSun.ts. */
31
+ const bool MOVING_SUN = true; // wgsl:override
32
+ /* The dynamic shadow pass's world-to-light matrix, and the depth it spans in metres. */
33
+ uniform mat4 uMovingLightViewProj;
34
+ uniform float uMovingShadowDepthSpan;
35
+
36
+ /* The receiver in the moving layer's map: x and y across it, z its depth, all 0 to 1. */
37
+ vec3 movingSunAt() {
38
+ vec4 l = uMovingLightViewProj * vec4(vWorldPos, 1.0);
39
+ return l.xyz / l.w * 0.5 + 0.5;
40
+ }
41
+
42
+ float movingSunSpan() {
43
+ return uMovingShadowDepthSpan;
44
+ }
45
+ `;
46
+ }
@@ -3,12 +3,23 @@
3
3
  * lit block for every light and by an unlit refracting draw for the direction it bends, so a lit
4
4
  * pane and an unlit distortion bend by one tilt rather than two transcriptions of it.
5
5
  *
6
- * Depth zero, after the tangent frame it calls; see `flatFrag`. **Only ever called under a branch
6
+ * The frame is built from `frameAt`, the coordinates the map was laid by — the mesh's, or the
7
+ * world's where a material projects them (`worldUv.ts`), with the mesh's tangents then set aside
8
+ * by `hasTangents`, since they run along coordinates the map no longer follows.
9
+ *
10
+ * Depth zero, after the tangent frame and the world's projection it calls; see `flatFrag`. **Only ever called under a branch
7
11
  * on a uniform** — `uNormalStrength` and whatever gates its caller — which is what makes the
8
12
  * derivatives inside `tangentFrame` and the implicit level of the read legal: AGENTS.md 2026-08-07.
9
13
  */
10
14
  export const NORMAL_MAP_GLSL = /* glsl */ `
11
- vec3 normalMapped(vec3 n, vec3 at) {
15
+ vec3 normalMapped(vec3 n, vec3 at, vec2 frameAt, int hasTangents) {
16
+ /* On three planes where the material projects them so: worldUv.ts. */
17
+ if (WORLD_UVS && triplanar()) return triplanarNormal(n, at.z);
18
+ /* A layered material's texels, blended by its mask before the frame turns them: layered.ts. */
19
+ if (LAYERED && layered()) {
20
+ mat3 layeredFrame = tangentFrame(n, vWorldPos, frameAt, vTangent, hasTangents);
21
+ return normalize(mix(n, normalize(layeredFrame * layeredTexel()), uNormalStrength));
22
+ }
12
23
  vec3 mapped = texture(uNormalMap, at).xyz * 2.0 - 1.0;
13
24
  /*
14
25
  * z rebuilt where none is stored. A tangent-space normal points out of its surface, so a
@@ -19,7 +30,7 @@ vec3 normalMapped(vec3 n, vec3 at) {
19
30
  * lying flat has its z near zero either way. A texel with z above zero is left as it was.
20
31
  */
21
32
  mapped.z = mapped.z > 0.0 ? mapped.z : sqrt(max(0.0, 1.0 - dot(mapped.xy, mapped.xy)));
22
- mat3 tbn = tangentFrame(n, vWorldPos, vUv.xy, vTangent, vHasTangents);
33
+ mat3 tbn = tangentFrame(n, vWorldPos, frameAt, vTangent, hasTangents);
23
34
  return normalize(mix(n, normalize(tbn * mapped), uNormalStrength));
24
35
  }
25
36
  `;
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * A draw's surface overlay in the lit stage: the rim, the dissolve and the wrinkles
3
- * `surfaceOverlay.ts` packs into `uOverlay`. Called from `main` at three places — the dissolve's cut
4
- * before anything is shaded, the wrinkles after the normal map, and the glow beside emission.
3
+ * `surfaceOverlay.ts` packs into `uOverlay`. Called from `main` at four places — the dissolve's cut
4
+ * before anything is shaded, the wrinkles after the normal map, a blending rim on the base colour
5
+ * once the normal is final and before any light reads the colour, and the glow beside emission.
5
6
  *
6
7
  * **A lit switch, `SURFACE_OVERLAY`**, off until a draw first sets an overlay and then on for good,
7
8
  * as `litSwitchesGlsl`'s are; declared after the models so every switch before it keeps its id.
@@ -19,6 +20,7 @@ export function overlayGlsl(on: boolean): string {
19
20
  const bool SURFACE_OVERLAY = false; // wgsl:override
20
21
  bool overlayCuts(vec2 uv) { return false; }
21
22
  vec3 overlayWrinkle(vec3 mapped, vec3 unmapped, vec2 uv) { return mapped; }
23
+ vec3 overlayRimBase(vec3 base, vec2 uv, vec3 n) { return base; }
22
24
  vec3 overlayGlow(vec2 uv, vec3 n) { return vec3(0.0); }
23
25
  `;
24
26
  }
@@ -26,7 +28,7 @@ vec3 overlayGlow(vec2 uv, vec3 n) { return vec3(0.0); }
26
28
  /* Whether this program carries a draw's overlay. Off but where one has been set: overlay.ts. */
27
29
  const bool SURFACE_OVERLAY = true; // wgsl:override
28
30
  /* The overlay's fifteen vectors; packSurfaceOverlay in surfaceOverlay.ts says what each holds. */
29
- uniform vec4 uOverlay[15];
31
+ uniform vec4 uOverlay[15]; // wgsl:material
30
32
 
31
33
  /* Whether the atlas is what the refraction slot holds: not where this draw refracts or is glass. */
32
34
  bool overlayImagesBound() {
@@ -84,8 +86,42 @@ vec2 overlayScreen() {
84
86
  return vec2(eye.x / depth + uClusterFrustum.w, eye.y / depth + 1.0) * 0.5;
85
87
  }
86
88
 
89
+ /*
90
+ * The rim's weight here, 0 to 1 before the pulse and the mask: the edge leaning toward normals that
91
+ * face up, raised to the contrast, times the noise laid in screen space. Its strength is the
92
+ * caller's: an added rim takes it times its light, a blending one times its colour.
93
+ */
94
+ float overlayRimWeight(vec2 uv, vec3 n) {
95
+ vec3 toEye = normalize(uCameraPos - vWorldPos);
96
+ float edge = pow(1.0 - clamp(dot(n, toEye), 0.0, 1.0), max(uOverlay[1].x, 1e-4));
97
+ float up = pow(clamp(n.y * 0.5 + 0.5, 0.0, 1.0), max(uOverlay[1].y, 1e-4));
98
+ float rim = pow(edge * up, max(uOverlay[1].z, 1e-4));
99
+ float time = uSurfaceScene.x;
100
+ if (uOverlay[2].x > 0.0 && overlayImagesBound()) {
101
+ vec2 at = overlayScreen() * uOverlay[3].z + uOverlay[3].xy * time;
102
+ rim *= overlayImage(uOverlay[2], at).r;
103
+ }
104
+ rim = min(rim, 1.0);
105
+ rim *= mix(uOverlay[4].x, uOverlay[4].y, 0.5 + 0.5 * sin(uOverlay[3].w * time));
106
+ if (uOverlay[5].x > 0.0 && overlayImagesBound()) {
107
+ rim *= 1.0 - uOverlay[1].w * overlayImage(uOverlay[5], uv).r;
108
+ }
109
+ return rim;
110
+ }
111
+
112
+ /*
113
+ * A blending rim: the base colour pulled toward the rim's by the weight times the alpha, held at
114
+ * one, before any light reads it, so the edge is lit as the surface is. The colour arrives already
115
+ * held at one a channel. Nothing where the rim adds instead, or where there is none.
116
+ */
117
+ vec3 overlayRimBase(vec3 base, vec2 uv, vec3 n) {
118
+ if (uOverlay[0].w <= 0.0 || uOverlay[4].z < 0.5) return base;
119
+ float weight = clamp(uOverlay[0].w * overlayRimWeight(uv, n), 0.0, 1.0);
120
+ return mix(base, uOverlay[0].rgb, weight);
121
+ }
122
+
87
123
  /* What the overlay adds to the light the surface gives off: the dissolve's band, the colour laid
88
- over through its noise, and the rim. Zero where the draw sets none of them. */
124
+ over through its noise, and an adding rim. Zero where the draw sets none of them. */
89
125
  vec3 overlayGlow(vec2 uv, vec3 n) {
90
126
  vec3 glow = vec3(0.0);
91
127
  float noise = overlayDissolveNoise(uv);
@@ -94,22 +130,8 @@ vec3 overlayGlow(vec2 uv, vec3 n) {
94
130
  glow += uOverlay[8].rgb * uOverlay[8].w * band + uOverlay[9].rgb * uOverlay[9].w * noise;
95
131
  }
96
132
  float strength = uOverlay[0].w;
97
- if (strength > 0.0) {
98
- vec3 toEye = normalize(uCameraPos - vWorldPos);
99
- float edge = pow(1.0 - clamp(dot(n, toEye), 0.0, 1.0), max(uOverlay[1].x, 1e-4));
100
- float up = pow(clamp(n.y * 0.5 + 0.5, 0.0, 1.0), max(uOverlay[1].y, 1e-4));
101
- float rim = pow(edge * up, max(uOverlay[1].z, 1e-4));
102
- float time = uSurfaceScene.x;
103
- if (uOverlay[2].x > 0.0 && overlayImagesBound()) {
104
- vec2 at = overlayScreen() * uOverlay[3].z + uOverlay[3].xy * time;
105
- rim *= overlayImage(uOverlay[2], at).r;
106
- }
107
- rim = min(rim, 1.0);
108
- rim *= mix(uOverlay[4].x, uOverlay[4].y, 0.5 + 0.5 * sin(uOverlay[3].w * time));
109
- if (uOverlay[5].x > 0.0 && overlayImagesBound()) {
110
- rim *= 1.0 - uOverlay[1].w * overlayImage(uOverlay[5], uv).r;
111
- }
112
- glow += uOverlay[0].rgb * strength * rim;
133
+ if (strength > 0.0 && uOverlay[4].z < 0.5) {
134
+ glow += uOverlay[0].rgb * strength * overlayRimWeight(uv, n);
113
135
  }
114
136
  return glow;
115
137
  }
@@ -13,7 +13,7 @@
13
13
  * picture and is its own change.
14
14
  */
15
15
  import { MAX_SHADOW_FILTER_TAPS } from '../../renderQuality.ts';
16
- import { OUTPUT_TRANSFORM_GLSL } from '../outputTransform.ts';
16
+ import { LIT_OUTPUT_TRANSFORM_GLSL } from '../outputTransform.ts';
17
17
  import { LAMP_GLASS_GLSL } from './lampGlass.ts';
18
18
 
19
19
  export const POINTSHADOW_GLSL = `/**
@@ -344,7 +344,7 @@ ${LAMP_GLASS_GLSL}
344
344
 
345
345
  layout(location = 0) out vec4 outColor;
346
346
 
347
- ${OUTPUT_TRANSFORM_GLSL}
347
+ ${LIT_OUTPUT_TRANSFORM_GLSL}
348
348
 
349
349
  /**
350
350
  * A fixed Poisson disk avoids the grid-shaped edge left by square 3x3 PCF.
@@ -121,9 +121,20 @@ uniform sampler2D uRefractScene;
121
121
  * gives up, and four separate floats cost half the shadowed lights such a device can have. The tints
122
122
  * stay colours of their own. Zero transmission is not glass — see glass.ts.
123
123
  */
124
- uniform vec4 uSeeThrough;
125
- uniform vec3 uRefractTint;
126
- uniform vec3 uGlassTint;
124
+ uniform vec4 uSeeThrough; // wgsl:material
125
+ /*
126
+ * What a metre of the medium leaves of each channel (rgb), and in w which way this frame's rows run
127
+ * up the screen: +1 where the first row is the bottom (WebGL2), -1 where it is the top (WebGPU). The
128
+ * bend is taken in view space, whose y is up whatever the framebuffer says, so it is turned here to
129
+ * the rows a screen coordinate counts. A vec4 where a vec3 already spent the row.
130
+ */
131
+ uniform vec4 uRefractTint; // wgsl:material
132
+ /*
133
+ * A pane's colour (rgb), and in w how much light from behind an opaque thin surface lets through:
134
+ * \`SurfaceMaterial.diffuseTransmission\`, read where the draw is not glass. A vec4 where a vec3
135
+ * already spent the row.
136
+ */
137
+ uniform vec4 uGlassTint; // wgsl:material
127
138
  flat in int vHasTangents;
128
139
 
129
140
  /**
@@ -154,7 +165,7 @@ uniform mediump sampler2DArray uAlbedo;
154
165
  * card. Its draw culls nothing, and a back face is lit as a front, with the shading normal turned
155
166
  * toward the viewer.
156
167
  */
157
- uniform ivec4 uMaterialFlags;
168
+ uniform ivec4 uMaterialFlags; // wgsl:material
158
169
  /**
159
170
  * Alpha below which a textured fragment is thrown away entirely, 0 to disable.
160
171
  *
@@ -168,13 +179,17 @@ uniform ivec4 uMaterialFlags;
168
179
  * else changes.
169
180
  *
170
181
  * **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 the material's lighting channels — a mask a light
173
- * shades it only where its own shares a bit with (\`SurfaceMaterial.lightChannels\`, 1 unless a
174
- * material names others). One vector rather than a float and three more, because every loose
175
- * scalar is a row of WebGL2's 224 that the light budget gives up.
182
+ * 1 a dither the temporal resolve averages, 2 alpha-to-coverage; see \`cutoutDither.ts\`), z unused,
183
+ * w the material's lighting channels — a mask a light shades it only where its own shares a bit
184
+ * with (\`SurfaceMaterial.lightChannels\`, 1 unless a material names others). One vector rather than
185
+ * a float and three more, because every loose scalar is a row of WebGL2's 224 that the light budget
186
+ * gives up.
187
+ *
188
+ * **z was the frame the dither's pattern is offset by until 4.10.0**, and that made every material
189
+ * new every frame: a material is kept on WebGPU by its numbers, and one of them changed with the
190
+ * clock. The frame is the pass's, in \`uHighlightMin.w\`.
176
191
  */
177
- uniform vec4 uCutout;
192
+ uniform vec4 uCutout; // wgsl:material
178
193
  /**
179
194
  * A draw's own ambient, as nine spherical-harmonic coefficients of incoming radiance, red green blue
180
195
  * in turn, twenty-seven floats, and in the last of the twenty-eight a 1 where they are on — 0 is the
@@ -182,7 +197,7 @@ uniform vec4 uCutout;
182
197
  * and the basis, and its tests hold this to it. Seven rows of the material block, which is per draw
183
198
  * as far as anything here is: \`setAmbientSH\` moves it between two draws as \`setMaterial\` does.
184
199
  */
185
- uniform vec4 uAmbientSH[7];
200
+ uniform vec4 uAmbientSH[7]; // wgsl:material
186
201
 
187
202
  /** The ambient a surface facing \`n\` receives from \`uAmbientSH\`: \`ambientFromSH\`'s arithmetic. */
188
203
  vec3 ambientHarmonics(vec3 n) {
@@ -229,7 +244,7 @@ uniform highp sampler2DArray uNormalMap;
229
244
  * shadow samplers, where a profile with shadows off should declare none of them; it is the wrong
230
245
  * tool for one image on a shader that already declares one.
231
246
  */
232
- uniform float uNormalStrength;
247
+ uniform float uNormalStrength; // wgsl:material
233
248
 
234
249
  /**
235
250
  * Occlusion, roughness and metallic in one image. glTF's packing, because that is what an import
@@ -250,7 +265,7 @@ uniform highp sampler2DArray uOrmMap;
250
265
  * occlusionTexture.strength is: 1 + strength * (texel - 1). A multiply at 0 means fully occluded,
251
266
  * which is the opposite of what a caller asking for no occlusion means.
252
267
  */
253
- uniform vec3 uOrmScale;
268
+ uniform vec3 uOrmScale; // wgsl:material
254
269
  /**
255
270
  * 1 when \`uEnvironment\` holds the GGX-prefiltered cube, 0 when it holds the capture's box chain.
256
271
  *
@@ -266,7 +281,7 @@ uniform vec3 uOrmScale;
266
281
  * (\`uniformVectorBudget.ts\`). y is how much of the environment the material mirrors, z how
267
282
  * bright what it mirrors is.
268
283
  */
269
- uniform vec3 uEnvironmentDials;
284
+ uniform vec3 uEnvironmentDials; // wgsl:material
270
285
 
271
286
  /**
272
287
  * Where a surface glows, and in what colour.
@@ -288,7 +303,7 @@ uniform highp sampler2DArray uEmissiveMap;
288
303
  * The counterpart of \`uOrmScale\`: a factor that multiplies a texture is not a value, and this is
289
304
  * where the multiplier lives once an image supplies the shape.
290
305
  */
291
- uniform vec3 uEmissiveScale;
306
+ uniform vec3 uEmissiveScale; // wgsl:material
292
307
 
293
308
  #if DIRECTIONAL_SHADOWS
294
309
  /*
@@ -375,7 +390,7 @@ uniform vec4 uSurfaceScene;
375
390
  * draw moves it), y the night factor emission is gated on. One row where two floats were two
376
391
  * (\`uniformVectorBudget.ts\`).
377
392
  */
378
- uniform vec2 uEmission;
393
+ uniform vec2 uEmission; // wgsl:material
379
394
  #if NIGHT_EMISSIVE
380
395
  /**
381
396
  * Emission that only shows where the dominant light does not reach. 0 is off and is the default.
@@ -400,7 +415,7 @@ uniform float uNightEmissive;
400
415
  * of that to change one channel is how a renderer ends up with two shaders that
401
416
  * disagree about the sun. The caller owns the blend state; this owns the number.
402
417
  */
403
- uniform float uOpacity;
418
+ uniform float uOpacity; // wgsl:material
404
419
 
405
420
  /**
406
421
  * Two per-draw switches on how a fragment is *written* rather than what colour it is, sharing one
@@ -431,17 +446,30 @@ uniform float uOpacity;
431
446
  * the grain is mirrored between backends and nothing else about it differs. What it gives up: a
432
447
  * crossfade reads as grain for as long as it lasts, which is why it should be short; and a blended
433
448
  * draw that asks for it is dithered too, since the test is a discard.
449
+ *
450
+ * **\`.z\` — whether this draw spends its own opacity on the same screen door**: 1 keeps the cells
451
+ * under \`vAlpha\` (an instance's \`MeshInstances.alphas\`, a vertex's channel alpha) and blends none
452
+ * of it, so an opaque or cut-out draw can fade one instance at a time. 0 is off. **The same pattern
453
+ * as the fade, and that is the point**: a pixel is kept where its one threshold sits under both, so
454
+ * a crossfade at \`t\` and \`-t\` of an instance at opacity \`a\` still covers each pixel of the
455
+ * instance's share \`a\` once. Two independent patterns would leave holes where they disagree.
434
456
  */
435
- uniform vec2 uWriteMode;
457
+ uniform vec3 uWriteMode; // wgsl:material
436
458
 
437
- /** Whether this fragment is one \`uWriteMode.y\` keeps. Integer arithmetic, per the device rule. */
438
- bool ditherKeeps(float amount) {
459
+ /** Where this fragment's cell of the 8×8 Bayer pattern falls, in (0, 1). Integer arithmetic,
460
+ per the device rule. */
461
+ float ditherThreshold() {
439
462
  uvec2 p = uvec2(gl_FragCoord.xy) & 7u;
440
463
  uint a = p.x ^ p.y;
441
464
  uint b = p.y;
442
465
  uint cell = ((a & 1u) << 5) | ((b & 1u) << 4) | ((a & 2u) << 2) | ((b & 2u) << 1) |
443
466
  ((a & 4u) >> 1) | ((b & 4u) >> 2);
444
- float threshold = (float(cell) + 0.5) / 64.0;
467
+ return (float(cell) + 0.5) / 64.0;
468
+ }
469
+
470
+ /** Whether this fragment is one \`uWriteMode.y\` keeps. */
471
+ bool ditherKeeps(float amount) {
472
+ float threshold = ditherThreshold();
445
473
  return amount > 0.0 ? threshold < amount : threshold >= -amount;
446
474
  }
447
475
 
@@ -465,7 +493,7 @@ bool ditherKeeps(float amount) {
465
493
  * one uniform skips the untaken side rather than computing both and discarding one, which is
466
494
  * only true because nothing here reads it through a *varying*-dependent branch.
467
495
  */
468
- uniform int uLightingEnabled;
496
+ uniform int uLightingEnabled; // wgsl:material
469
497
  /**
470
498
  * How this draw meets the medium: the atmospheric haze and the underwater tint both, folded
471
499
  * together because both are the *camera's* medium rather than a property of the surface — see
@@ -487,7 +515,7 @@ uniform int uLightingEnabled;
487
515
  * is what "exactly as its colour says" has to mean for it; the cost is that such a draw will
488
516
  * not tint if the camera goes under water, unlike everything else in the frame.
489
517
  */
490
- uniform int uFogEnabled;
518
+ uniform int uFogEnabled; // wgsl:material
491
519
 
492
520
  /**
493
521
  * A pass-level scale over the grain the geometry declared, 0 to 1.
@@ -497,7 +525,7 @@ uniform int uFogEnabled;
497
525
  * imported model that declares nothing is drawn with the pass's own answer, and a scene
498
526
  * can dial a whole draw down without rebuilding its geometry.
499
527
  */
500
- uniform float uGrain;
528
+ uniform float uGrain; // wgsl:material
501
529
  /*
502
530
  * The material's own relief: how strong, and how many bumps to a metre.
503
531
  *
@@ -506,7 +534,7 @@ uniform float uGrain;
506
534
  * between them, and the geometry carrying them is identical in all three cases. See
507
535
  * Renderer.setSurfaceRelief. x is the strength and y the bumps to a metre, in one row.
508
536
  */
509
- uniform vec2 uRelief;
537
+ uniform vec2 uRelief; // wgsl:material
510
538
  /**
511
539
  * How hard the bound surface texture's own luminance turns the shading normal, 0 for not at all.
512
540
  *
@@ -529,7 +557,7 @@ uniform vec2 uRelief;
529
557
  * 0..1 like its neighbours for the same reason: this is an amplitude against a luminance
530
558
  * gradient, not a fraction of anything.
531
559
  */
532
- uniform float uTextureRelief;
560
+ uniform float uTextureRelief; // wgsl:material
533
561
 
534
562
  /**
535
563
  * How much of the environment this material mirrors, 0 to 1.
@@ -664,8 +692,12 @@ uniform vec3 uProbeGridSets;
664
692
  /**
665
693
  * A world-space box that lights up regardless of time of day: its corners, and in the far corner's
666
694
  * w the gain — one row where a loose float was another (\`uniformVectorBudget.ts\`).
695
+ *
696
+ * **And in the near corner's w the frame the cutout dither's pattern is offset by**, for the same
697
+ * reason: a row the lit stage already pays for, holding a number the pass sets and no material does.
698
+ * See \`uCutout\`.
667
699
  */
668
- uniform vec3 uHighlightMin;
700
+ uniform vec4 uHighlightMin;
669
701
  uniform vec4 uHighlightMax;
670
702
 
671
703
  /*
@@ -0,0 +1,113 @@
1
+ /**
2
+ * A material's maps placed by where a point is in the world rather than by the mesh's texture
3
+ * coordinates: `SurfaceMaterial.projection`, packed into `uWorldUv` by `surfaceProjection.ts`.
4
+ *
5
+ * **Planar** takes the two horizontal axes, `x` and `z`, at so many repeats a metre: a floor whose
6
+ * mesh carries no useful coordinates, or one coordinate for the whole of it, shows its texture at
7
+ * the size the material asks. **Triplanar** reads every map three times, on the planes across `x`,
8
+ * `y` and `z`, and blends them by how squarely the surface faces each — the geometric normal raised
9
+ * to a sharpness — so a wall, a cliff and a rock wear it as a floor does. A normal map on three
10
+ * planes is turned into the world by the whiteout blend, each plane's tangent-space normal added to
11
+ * the surface's own swizzled into that plane, which keeps the detail of each without a tangent
12
+ * frame per plane.
13
+ *
14
+ * **A lit switch, `WORLD_UVS`**, off until a material first asks and then on for good, declared
15
+ * last so every switch before it keeps its id. Compiled out, its vector is not declared and its
16
+ * calls are constants, so a scene that never asks pays nothing.
17
+ *
18
+ * **What it gives up**: a projection is the world's, so a mesh that moves slides under its own
19
+ * texture, which is right for ground and wrong for a crate; triplanar reads each map three times;
20
+ * and a map that is an array reads one layer for the whole surface, the one the face names.
21
+ */
22
+ export function worldUvGlsl(on: boolean): string {
23
+ if (!on) {
24
+ return /* glsl */ `
25
+ const bool WORLD_UVS = false; // wgsl:override
26
+ bool projected() { return false; }
27
+ bool triplanar() { return false; }
28
+ vec2 planarAt() { return vec2(0.0); }
29
+ vec4 triplanarAlbedo(float layer) { return vec4(0.0); }
30
+ vec3 triplanarOrm(float layer) { return vec3(0.0); }
31
+ vec3 triplanarEmissive(float layer) { return vec3(0.0); }
32
+ vec3 triplanarNormal(vec3 n, float layer) { return n; }
33
+ `;
34
+ }
35
+ return /* glsl */ `
36
+ /* Whether this program places maps by the world. Off but where a material has asked: worldUv.ts. */
37
+ const bool WORLD_UVS = true; // wgsl:override
38
+ /* x: 0 the mesh's coordinates, 1 planar, 2 triplanar; y: repeats a metre; z: the blend's sharpness. */
39
+ uniform vec4 uWorldUv; // wgsl:material
40
+
41
+ bool projected() {
42
+ return uWorldUv.x > 0.5;
43
+ }
44
+
45
+ bool triplanar() {
46
+ return uWorldUv.x > 1.5;
47
+ }
48
+
49
+ /* The horizontal axes, scaled: u along +x, v along +z. */
50
+ vec2 planarAt() {
51
+ return vWorldPos.xz * uWorldUv.y;
52
+ }
53
+
54
+ /* How much each plane counts: the geometric normal's components, sharpened, summing to one. */
55
+ vec3 triplanarWeights() {
56
+ vec3 w = pow(abs(normalize(vNormal)), vec3(uWorldUv.z));
57
+ return w / max(w.x + w.y + w.z, 1e-5);
58
+ }
59
+
60
+ /*
61
+ * Each map read on the three planes and blended: a function a map rather than one taking the
62
+ * sampler, whose precision differs between them. Under a branch on a uniform, as every map read
63
+ * is, so the implicit level each read takes is legal: AGENTS.md 2026-08-07.
64
+ */
65
+ vec4 triplanarAlbedo(float layer) {
66
+ vec3 w = triplanarWeights();
67
+ vec3 p = vWorldPos * uWorldUv.y;
68
+ return texture(uAlbedo, vec3(p.zy, layer)) * w.x + texture(uAlbedo, vec3(p.xz, layer)) * w.y +
69
+ texture(uAlbedo, vec3(p.xy, layer)) * w.z;
70
+ }
71
+
72
+ vec3 triplanarOrm(float layer) {
73
+ vec3 w = triplanarWeights();
74
+ vec3 p = vWorldPos * uWorldUv.y;
75
+ return texture(uOrmMap, vec3(p.zy, layer)).rgb * w.x +
76
+ texture(uOrmMap, vec3(p.xz, layer)).rgb * w.y + texture(uOrmMap, vec3(p.xy, layer)).rgb * w.z;
77
+ }
78
+
79
+ vec3 triplanarEmissive(float layer) {
80
+ vec3 w = triplanarWeights();
81
+ vec3 p = vWorldPos * uWorldUv.y;
82
+ return texture(uEmissiveMap, vec3(p.zy, layer)).rgb * w.x +
83
+ texture(uEmissiveMap, vec3(p.xz, layer)).rgb * w.y +
84
+ texture(uEmissiveMap, vec3(p.xy, layer)).rgb * w.z;
85
+ }
86
+
87
+ /* A tangent-space normal off the map, its z rebuilt where a two-channel map stored none. */
88
+ vec3 triplanarTexel(vec2 at, float layer) {
89
+ vec3 t = texture(uNormalMap, vec3(at, layer)).xyz * 2.0 - 1.0;
90
+ t.z = t.z > 0.0 ? t.z : sqrt(max(0.0, 1.0 - dot(t.xy, t.xy)));
91
+ return t;
92
+ }
93
+
94
+ /*
95
+ * The normal map on three planes, by the whiteout blend: each plane's texel added to the surface
96
+ * normal swizzled into that plane, its z scaled by the normal's own, and swizzled back. Each plane's
97
+ * coordinates run along the same world axes from both sides, so a texel's axes are the world's and
98
+ * nothing is flipped; what that gives up is a texture mirrored on a plane's negative side.
99
+ */
100
+ vec3 triplanarNormal(vec3 n, float layer) {
101
+ vec3 w = triplanarWeights();
102
+ vec3 p = vWorldPos * uWorldUv.y;
103
+ vec3 tx = triplanarTexel(p.zy, layer);
104
+ vec3 ty = triplanarTexel(p.xz, layer);
105
+ vec3 tz = triplanarTexel(p.xy, layer);
106
+ tx = vec3(tx.xy + n.zy, abs(tx.z) * n.x);
107
+ ty = vec3(ty.xy + n.xz, abs(ty.z) * n.y);
108
+ tz = vec3(tz.xy + n.xy, abs(tz.z) * n.z);
109
+ vec3 blended = normalize(tx.zyx * w.x + ty.xzy * w.y + tz.xyz * w.z);
110
+ return normalize(mix(n, blended, uNormalStrength));
111
+ }
112
+ `;
113
+ }