@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
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Where a cloth-bound vertex goes: the reference for what the vertex stage computes.
3
+ *
4
+ * A render vertex bound to a simulation triangle (particles a, b, c) sits at its barycentric point
5
+ * — `a·(1 − u − v) + b·u + c·v` — raised `offset` along the triangle's normal; its normal turns as
6
+ * the triangle turned since rest. The turn is the rotation from the triangle's rest frame
7
+ * to its frame now, each frame built from its first edge and its normal, so a vertex beside the
8
+ * triangle, a fold's width away, moves as the cloth moves rather than as its skeleton does.
9
+ *
10
+ * **A vertex bound to one particle** — all three indices equal — sits on it and keeps its normal:
11
+ * a single point has no frame to turn by. That is how a binding says "follow this particle".
12
+ *
13
+ * **A reference, as `recon/motionVectors.ts` is**: nothing at run time calls it. The vertex stage in
14
+ * `shaders/clothBinding.ts` and the cloth motion stage in `shaders/recon/motion.wgsl.ts` are the
15
+ * same arithmetic for the device, and nothing but a device holds them to this — the source tests
16
+ * pin where the shader moves a vertex, not the numbers it moves it by. `demo/dev/skinnedCloth.html`
17
+ * is where the two backends were compared, and they agree to two levels in 255.
18
+ */
19
+ const NOW = new Float64Array(9);
20
+ const REST = new Float64Array(9);
21
+ /** A triangle's frame — first edge, normal × edge, normal — as three columns, into `out`. */
22
+ function frame(particles, a, b, c, out) {
23
+ const ex = particles[b * 3] - particles[a * 3];
24
+ const ey = particles[b * 3 + 1] - particles[a * 3 + 1];
25
+ const ez = particles[b * 3 + 2] - particles[a * 3 + 2];
26
+ const fx = particles[c * 3] - particles[a * 3];
27
+ const fy = particles[c * 3 + 1] - particles[a * 3 + 1];
28
+ const fz = particles[c * 3 + 2] - particles[a * 3 + 2];
29
+ let nx = ey * fz - ez * fy;
30
+ let ny = ez * fx - ex * fz;
31
+ let nz = ex * fy - ey * fx;
32
+ const nl = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
33
+ nx /= nl;
34
+ ny /= nl;
35
+ nz /= nl;
36
+ const el = Math.sqrt(ex * ex + ey * ey + ez * ez) || 1;
37
+ const tx = ex / el;
38
+ const ty = ey / el;
39
+ const tz = ez / el;
40
+ out[0] = tx;
41
+ out[1] = ty;
42
+ out[2] = tz;
43
+ out[3] = ny * tz - nz * ty;
44
+ out[4] = nz * tx - nx * tz;
45
+ out[5] = nx * ty - ny * tx;
46
+ out[6] = nx;
47
+ out[7] = ny;
48
+ out[8] = nz;
49
+ }
50
+ /** Place one vertex: `now` and `rest` are particle positions, three floats each. */
51
+ export function clothPlace(now, rest, a, b, c, u, v, offset, restNormal, out) {
52
+ if (a === b && b === c) {
53
+ out.position[0] = now[a * 3];
54
+ out.position[1] = now[a * 3 + 1];
55
+ out.position[2] = now[a * 3 + 2];
56
+ out.normal[0] = restNormal[0];
57
+ out.normal[1] = restNormal[1];
58
+ out.normal[2] = restNormal[2];
59
+ return;
60
+ }
61
+ frame(now, a, b, c, NOW);
62
+ frame(rest, a, b, c, REST);
63
+ const w = 1 - u - v;
64
+ for (let k = 0; k < 3; k++) {
65
+ out.position[k] =
66
+ now[a * 3 + k] * w +
67
+ now[b * 3 + k] * u +
68
+ now[c * 3 + k] * v +
69
+ NOW[6 + k] * offset;
70
+ }
71
+ /* Turned by now × restᵀ: the rest normal in the rest frame's terms, rebuilt in the frame now. */
72
+ const t = REST[0] * restNormal[0] +
73
+ REST[1] * restNormal[1] +
74
+ REST[2] * restNormal[2];
75
+ const s = REST[3] * restNormal[0] +
76
+ REST[4] * restNormal[1] +
77
+ REST[5] * restNormal[2];
78
+ const n = REST[6] * restNormal[0] +
79
+ REST[7] * restNormal[1] +
80
+ REST[8] * restNormal[2];
81
+ for (let k = 0; k < 3; k++) {
82
+ out.normal[k] =
83
+ NOW[k] * t + NOW[3 + k] * s + NOW[6 + k] * n;
84
+ }
85
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * A block-compressed image a caller hands to `createSurfaceTexture`, and the one decision about it
3
+ * both backends share: whether this device takes it as blocks.
4
+ *
5
+ * **Uploaded as it is, or not at all.** A BC texture costs a byte a texel on the GPU (BC7) or half
6
+ * of one (BC1) against four for RGBA, and keeps the mip chain its author built rather than one a box
7
+ * filter makes. A device that cannot sample the format — most phones, which have ASTC and ETC2 —
8
+ * gets nothing from the blocks, and the engine does not decode them here: core ships no decoder.
9
+ * The loader in `@driftengine/assets` asks `uploadsCompressed` first and decodes at load where the
10
+ * answer is no; a caller handing a source this device cannot take is refused by name.
11
+ *
12
+ * Backend-neutral, because both backends must agree on what they accept before either touches a
13
+ * device. `BlockFormat` is the same union `@driftengine/drft` names `BcFormat`; core imports nothing
14
+ * from that package, and a format added there without a twin here fails where the loader passes one
15
+ * to the other, at compile time.
16
+ */
17
+ /** BC1 to BC5 and BC7. BC6H and the signed forms are not among them. */
18
+ export type BlockFormat = 'bc1' | 'bc2' | 'bc3' | 'bc4' | 'bc5' | 'bc7';
19
+ /** A format as a device offers it: the colour space is part of the name, as WebGPU's are. */
20
+ export type CompressedTextureFormat = 'bc1' | 'bc1-srgb' | 'bc2' | 'bc2-srgb' | 'bc3' | 'bc3-srgb' | 'bc4' | 'bc5' | 'bc7' | 'bc7-srgb';
21
+ /** Block-compressed texels with their stored mip chain, level 0 first. */
22
+ export interface CompressedTextureSource {
23
+ readonly format: BlockFormat;
24
+ readonly width: number;
25
+ readonly height: number;
26
+ readonly levels: readonly Uint8Array[];
27
+ }
28
+ /** What `createSurfaceTexture` takes: an image, or blocks. */
29
+ export type SurfaceSource = TexImageSource | CompressedTextureSource;
30
+ /**
31
+ * The layers as blocks, or null where every one is an image. An array mixing the two is refused:
32
+ * one GPU texture has one format.
33
+ */
34
+ export declare function compressedLayers(sources: readonly SurfaceSource[]): readonly CompressedTextureSource[] | null;
35
+ /** Bytes per 4x4 block. */
36
+ export declare const BLOCK_BYTES: Readonly<Record<BlockFormat, number>>;
37
+ /** Whether `source` is a compressed source rather than an image. */
38
+ export declare function isCompressedSource(source: unknown): source is CompressedTextureSource;
39
+ /** The format a device would have to offer, or null for one with no sRGB form. */
40
+ export declare function compressedFormatName(format: BlockFormat, srgb: boolean): CompressedTextureFormat | null;
41
+ /**
42
+ * Whether a `width` by `height` source in `format` uploads as blocks on a device offering
43
+ * `available`. Level 0 must be whole blocks each way: WebGPU refuses any other size for a BC
44
+ * texture, and WebGL2's S3TC extension says the same, so a 6x6 image is decoded rather than
45
+ * refused. What would make that wrong is a device that pads; none is relied on here.
46
+ */
47
+ export declare function uploadsCompressed(format: BlockFormat, srgb: boolean, width: number, height: number, available: readonly CompressedTextureFormat[]): boolean;
48
+ /**
49
+ * How many levels the layers carry, after checking each is the size its place requires and every
50
+ * layer matches the first. Refuses by name: a short level uploaded anyway is a device error at
51
+ * submit, which on WebGPU is a frame that draws nothing and says so somewhere else.
52
+ */
53
+ export declare function compressedLevels(layers: readonly CompressedTextureSource[]): number;
54
+ /** What a refusal names: the format, its colour space, and what the device offers instead. */
55
+ export declare function compressedRefusal(format: BlockFormat, srgb: boolean, width: number, height: number, available: readonly CompressedTextureFormat[]): string;
56
+ /** The format name and level count blocks upload as here, or a refusal naming why they cannot. */
57
+ export declare function planBlocks(blocks: readonly CompressedTextureSource[], srgb: boolean, compressed: readonly CompressedTextureFormat[]): {
58
+ readonly name: CompressedTextureFormat;
59
+ readonly levels: number;
60
+ };
61
+ /**
62
+ * `update` replaces pixels with an image, and a texture holding blocks has no pixels to replace:
63
+ * its format is the blocks', and an image would be a different texture. Refused rather than
64
+ * quietly reallocated, so the handle a draw holds keeps meaning what it meant.
65
+ */
66
+ export declare function refuseBlockUpdate(format: BlockFormat | null): void;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * A block-compressed image a caller hands to `createSurfaceTexture`, and the one decision about it
3
+ * both backends share: whether this device takes it as blocks.
4
+ *
5
+ * **Uploaded as it is, or not at all.** A BC texture costs a byte a texel on the GPU (BC7) or half
6
+ * of one (BC1) against four for RGBA, and keeps the mip chain its author built rather than one a box
7
+ * filter makes. A device that cannot sample the format — most phones, which have ASTC and ETC2 —
8
+ * gets nothing from the blocks, and the engine does not decode them here: core ships no decoder.
9
+ * The loader in `@driftengine/assets` asks `uploadsCompressed` first and decodes at load where the
10
+ * answer is no; a caller handing a source this device cannot take is refused by name.
11
+ *
12
+ * Backend-neutral, because both backends must agree on what they accept before either touches a
13
+ * device. `BlockFormat` is the same union `@driftengine/drft` names `BcFormat`; core imports nothing
14
+ * from that package, and a format added there without a twin here fails where the loader passes one
15
+ * to the other, at compile time.
16
+ */
17
+ /**
18
+ * The layers as blocks, or null where every one is an image. An array mixing the two is refused:
19
+ * one GPU texture has one format.
20
+ */
21
+ export function compressedLayers(sources) {
22
+ let count = 0;
23
+ for (const source of sources)
24
+ if (isCompressedSource(source))
25
+ count++;
26
+ if (count === 0)
27
+ return null;
28
+ if (count !== sources.length) {
29
+ throw new Error('createSurfaceTextureArray: some layers are blocks and some are images, and one texture has ' +
30
+ 'one format. Decode the blocks, or build the array from blocks alone.');
31
+ }
32
+ return sources;
33
+ }
34
+ /** Bytes per 4x4 block. */
35
+ export const BLOCK_BYTES = {
36
+ bc1: 8,
37
+ bc2: 16,
38
+ bc3: 16,
39
+ bc4: 8,
40
+ bc5: 16,
41
+ bc7: 16,
42
+ };
43
+ /** Whether `source` is a compressed source rather than an image. */
44
+ export function isCompressedSource(source) {
45
+ const candidate = source;
46
+ return (typeof candidate === 'object' &&
47
+ candidate !== null &&
48
+ typeof candidate.format === 'string' &&
49
+ Array.isArray(candidate.levels));
50
+ }
51
+ /** The format a device would have to offer, or null for one with no sRGB form. */
52
+ export function compressedFormatName(format, srgb) {
53
+ if (!srgb)
54
+ return format;
55
+ return format === 'bc4' || format === 'bc5' ? null : `${format}-srgb`;
56
+ }
57
+ /**
58
+ * Whether a `width` by `height` source in `format` uploads as blocks on a device offering
59
+ * `available`. Level 0 must be whole blocks each way: WebGPU refuses any other size for a BC
60
+ * texture, and WebGL2's S3TC extension says the same, so a 6x6 image is decoded rather than
61
+ * refused. What would make that wrong is a device that pads; none is relied on here.
62
+ */
63
+ export function uploadsCompressed(format, srgb, width, height, available) {
64
+ const name = compressedFormatName(format, srgb);
65
+ return name !== null && width % 4 === 0 && height % 4 === 0 && available.includes(name);
66
+ }
67
+ /** The bytes level `level` of a `width` by `height` image takes: whole blocks, one at least. */
68
+ function levelBytes(format, width, height, level) {
69
+ const w = Math.max(1, width >> level);
70
+ const h = Math.max(1, height >> level);
71
+ return Math.ceil(w / 4) * Math.ceil(h / 4) * BLOCK_BYTES[format];
72
+ }
73
+ /**
74
+ * How many levels the layers carry, after checking each is the size its place requires and every
75
+ * layer matches the first. Refuses by name: a short level uploaded anyway is a device error at
76
+ * submit, which on WebGPU is a frame that draws nothing and says so somewhere else.
77
+ */
78
+ export function compressedLevels(layers) {
79
+ const first = layers[0];
80
+ if (first === undefined)
81
+ throw new Error('createSurfaceTexture: an array needs at least one image');
82
+ const { format, width, height } = first;
83
+ const count = first.levels.length;
84
+ const most = Math.floor(Math.log2(Math.max(1, width, height))) + 1;
85
+ if (count < 1 || count > most) {
86
+ throw new Error(`createSurfaceTexture: ${count} levels for a ${width}x${height} image, which has at least 1 ` +
87
+ `and at most ${most}`);
88
+ }
89
+ for (let layer = 0; layer < layers.length; layer++) {
90
+ const source = layers[layer];
91
+ if (source.format !== format ||
92
+ source.width !== width ||
93
+ source.height !== height ||
94
+ source.levels.length !== count) {
95
+ throw new Error(`createSurfaceTexture: layer ${layer} is ${source.width}x${source.height} ` +
96
+ `${source.format.toUpperCase()} with ${source.levels.length} levels, and layer 0 is ` +
97
+ `${width}x${height} ${format.toUpperCase()} with ${count}. Every layer of an array is one ` +
98
+ 'format, size and chain on the device.');
99
+ }
100
+ for (let level = 0; level < count; level++) {
101
+ const want = levelBytes(format, width, height, level);
102
+ const got = source.levels[level].length;
103
+ if (got !== want) {
104
+ throw new Error(`createSurfaceTexture: level ${level} of a ${width}x${height} ${format.toUpperCase()} ` +
105
+ `image is ${want} bytes, and ${got} were given`);
106
+ }
107
+ }
108
+ }
109
+ return count;
110
+ }
111
+ /** What a refusal names: the format, its colour space, and what the device offers instead. */
112
+ export function compressedRefusal(format, srgb, width, height, available) {
113
+ const asked = `${format.toUpperCase()}${srgb ? ' sRGB' : ''} at ${width}x${height}`;
114
+ const offered = available.length === 0 ? 'no BC format' : available.join(', ');
115
+ return (`createSurfaceTexture: this device cannot take ${asked} as blocks — it offers ${offered}, and ` +
116
+ 'a BC texture must be whole 4x4 blocks at level 0. Decode it first (decodeBc in ' +
117
+ '@driftengine/assets), or ask renderer.compressedFormats before choosing.');
118
+ }
119
+ /** The format name and level count blocks upload as here, or a refusal naming why they cannot. */
120
+ export function planBlocks(blocks, srgb, compressed) {
121
+ const { format, width, height } = blocks[0];
122
+ const name = compressedFormatName(format, srgb);
123
+ if (name === null || !uploadsCompressed(format, srgb, width, height, compressed)) {
124
+ throw new Error(compressedRefusal(format, srgb, width, height, compressed));
125
+ }
126
+ return { name, levels: compressedLevels(blocks) };
127
+ }
128
+ /**
129
+ * `update` replaces pixels with an image, and a texture holding blocks has no pixels to replace:
130
+ * its format is the blocks', and an image would be a different texture. Refused rather than
131
+ * quietly reallocated, so the handle a draw holds keeps meaning what it meant.
132
+ */
133
+ export function refuseBlockUpdate(format) {
134
+ if (format === null)
135
+ return;
136
+ throw new Error(`updateSurfaceTexture: this texture holds ${format.toUpperCase()} blocks, and update replaces ` +
137
+ 'an image. Build a new texture for new contents.');
138
+ }
@@ -12,12 +12,16 @@
12
12
  * reads it and binds before it asks again.
13
13
  */
14
14
  import type { SurfaceMaterial } from './surfaceTexture.ts';
15
- /** A cutout caster's texture, its cutoff and its UV scale. */
15
+ /** A cutout caster's texture, its cutoff and its UV scale and offset. */
16
16
  export interface CutoutCaster<Texture> {
17
17
  readonly albedo: Texture;
18
18
  readonly cutoff: number;
19
+ /** 1 for a dithered edge, which a caster draws with a pattern that does not move. */
20
+ readonly dithered: number;
19
21
  readonly u: number;
20
22
  readonly v: number;
23
+ readonly uOffset: number;
24
+ readonly vOffset: number;
21
25
  }
22
26
  /** The cutout `material` casts, or null for an ordinary caster. */
23
27
  export declare function cutoutOf<Texture>(material: SurfaceMaterial<Texture> | null | undefined): CutoutCaster<Texture> | null;
@@ -1,8 +1,11 @@
1
1
  const answer = {
2
2
  albedo: null,
3
3
  cutoff: 0,
4
+ dithered: 0,
4
5
  u: 1,
5
6
  v: 1,
7
+ uOffset: 0,
8
+ vOffset: 0,
6
9
  };
7
10
  /** The cutout `material` casts, or null for an ordinary caster. */
8
11
  export function cutoutOf(material) {
@@ -14,7 +17,10 @@ export function cutoutOf(material) {
14
17
  return null;
15
18
  answer.albedo = albedo;
16
19
  answer.cutoff = cutoff;
20
+ answer.dithered = material.cutoutMode === 'dithered' ? 1 : 0;
17
21
  answer.u = material.uScale ?? 1;
18
22
  answer.v = material.vScale ?? 1;
23
+ answer.uOffset = material.uOffset ?? 0;
24
+ answer.vOffset = material.vOffset ?? 0;
19
25
  return answer;
20
26
  }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * A cutout whose edge is a share of the pixel rather than a line through it.
3
+ *
4
+ * **Why.** A hard alpha test draws a strand of hair, an eyelash or a fringe of leaves as a stair of
5
+ * whole pixels: a card's alpha falls off over a few texels and the test turns that into on and off.
6
+ * A dithered cutout keeps each pixel with a probability, the share of it the texture covers, and
7
+ * something downstream then averages the pattern back into coverage. What can average it depends on
8
+ * the frame, so the material asks for the smooth edge and the renderer decides, per frame, how:
9
+ *
10
+ * - **A temporal resolve (TAA or DriftTR)**: the pixel is kept against a threshold that moves every
11
+ * frame, and the resolve integrates the pattern over its history into coverage.
12
+ * - **A multisampled frame**: the share goes to the GPU as alpha-to-coverage, which turns it into
13
+ * that many of the pixel's samples.
14
+ * - **Neither**: the hard test, because a dither nothing resolves is grain on the screen.
15
+ *
16
+ * **What it gives up**: a dithered edge under a temporal resolve is soft by one history's worth of
17
+ * blur and ghosts slightly behind a fast card, which is the resolve's own trade; and under
18
+ * multisampling the share is quantised to the sample count, five levels at four samples. **What
19
+ * would make it wrong** is a pass that tests depth for equality against the main pass and discards
20
+ * differently from it; the caster and the lit pass take their keep from the same two functions here.
21
+ */
22
+ /** What a material asks for. `'hard'` is every cutout that shipped before this. */
23
+ export type CutoutMode = 'hard' | 'dithered';
24
+ /** What the frame does with it: 0, 1 and 2 in the shader's `uCutout.y`. */
25
+ export type CutoutResolve = 'hard' | 'dither' | 'coverage';
26
+ /** The shader's number for a resolve. */
27
+ export declare const CUTOUT_RESOLVE_CODE: Readonly<Record<CutoutResolve, number>>;
28
+ /**
29
+ * How a cutout is drawn this frame. A temporal resolve is preferred over multisampling when a frame
30
+ * has both, because it resolves every share and multisampling only the sample count's.
31
+ */
32
+ export declare function resolveCutout(mode: CutoutMode, temporal: boolean, samples: number): CutoutResolve;
33
+ /**
34
+ * The share of a pixel a cutout keeps, from its alpha: half at the cutoff, ramping over a band
35
+ * twice the distance to the nearer end. At a cutoff of a half that is alpha itself.
36
+ */
37
+ export declare function cutoutShare(alpha: number, cutoff: number): number;
38
+ /**
39
+ * The threshold a pixel's share is compared against: interleaved gradient noise (Jimenez,
40
+ * "Next Generation Post Processing in Call of Duty: Advanced Warfare", 2014), offset by the frame so
41
+ * the pattern moves. `x` and `y` are pixel centres, as `gl_FragCoord.xy` is.
42
+ */
43
+ export declare function ditherThreshold(x: number, y: number, frame: number): number;
44
+ /**
45
+ * The two functions in GLSL, for the lit pass and every caster that keeps the same pixels.
46
+ *
47
+ * `cutoutKeeps(share, pixel, frame)` is the dithered test; `cutoutShare(alpha, cutoff)` is also what
48
+ * a coverage frame writes as alpha. Frame is a float the renderer counts and wraps, so the noise's
49
+ * argument stays small enough for a half-precision device.
50
+ */
51
+ export declare const CUTOUT_DITHER_GLSL: string;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * A cutout whose edge is a share of the pixel rather than a line through it.
3
+ *
4
+ * **Why.** A hard alpha test draws a strand of hair, an eyelash or a fringe of leaves as a stair of
5
+ * whole pixels: a card's alpha falls off over a few texels and the test turns that into on and off.
6
+ * A dithered cutout keeps each pixel with a probability, the share of it the texture covers, and
7
+ * something downstream then averages the pattern back into coverage. What can average it depends on
8
+ * the frame, so the material asks for the smooth edge and the renderer decides, per frame, how:
9
+ *
10
+ * - **A temporal resolve (TAA or DriftTR)**: the pixel is kept against a threshold that moves every
11
+ * frame, and the resolve integrates the pattern over its history into coverage.
12
+ * - **A multisampled frame**: the share goes to the GPU as alpha-to-coverage, which turns it into
13
+ * that many of the pixel's samples.
14
+ * - **Neither**: the hard test, because a dither nothing resolves is grain on the screen.
15
+ *
16
+ * **What it gives up**: a dithered edge under a temporal resolve is soft by one history's worth of
17
+ * blur and ghosts slightly behind a fast card, which is the resolve's own trade; and under
18
+ * multisampling the share is quantised to the sample count, five levels at four samples. **What
19
+ * would make it wrong** is a pass that tests depth for equality against the main pass and discards
20
+ * differently from it; the caster and the lit pass take their keep from the same two functions here.
21
+ */
22
+ /** The shader's number for a resolve. */
23
+ export const CUTOUT_RESOLVE_CODE = Object.freeze({
24
+ hard: 0,
25
+ dither: 1,
26
+ coverage: 2,
27
+ });
28
+ /**
29
+ * How a cutout is drawn this frame. A temporal resolve is preferred over multisampling when a frame
30
+ * has both, because it resolves every share and multisampling only the sample count's.
31
+ */
32
+ export function resolveCutout(mode, temporal, samples) {
33
+ if (mode !== 'dithered')
34
+ return 'hard';
35
+ if (temporal)
36
+ return 'dither';
37
+ if (samples > 1)
38
+ return 'coverage';
39
+ return 'hard';
40
+ }
41
+ /**
42
+ * The share of a pixel a cutout keeps, from its alpha: half at the cutoff, ramping over a band
43
+ * twice the distance to the nearer end. At a cutoff of a half that is alpha itself.
44
+ */
45
+ export function cutoutShare(alpha, cutoff) {
46
+ const band = Math.max(2 * Math.min(cutoff, 1 - cutoff), 1e-4);
47
+ return Math.min(Math.max((alpha - cutoff) / band + 0.5, 0), 1);
48
+ }
49
+ /**
50
+ * The temporal step of the noise below, per frame: Jimenez's constant (2014), which moves the
51
+ * pattern across the tile rather than stepping each pixel's threshold.
52
+ *
53
+ * **A golden-ratio step per pixel was tried and measured, and it is not better.** It spreads a still
54
+ * pixel's thresholds evenly over time, which reads like the cure for grain; on a card of strands
55
+ * under TAA it left 12.9 levels of grain against this one's 11.7. The floor is the resolve's own: it
56
+ * keeps nine tenths of its history, so the newest frame's kept-or-not pattern is a tenth of the
57
+ * picture, about ten levels of grain at a half share whatever sequence chose it.
58
+ */
59
+ const NOISE_FRAME_STEP = 5.588238;
60
+ /**
61
+ * The threshold a pixel's share is compared against: interleaved gradient noise (Jimenez,
62
+ * "Next Generation Post Processing in Call of Duty: Advanced Warfare", 2014), offset by the frame so
63
+ * the pattern moves. `x` and `y` are pixel centres, as `gl_FragCoord.xy` is.
64
+ */
65
+ export function ditherThreshold(x, y, frame) {
66
+ const px = x + NOISE_FRAME_STEP * frame;
67
+ const py = y + NOISE_FRAME_STEP * frame;
68
+ const inner = 0.06711056 * px + 0.00583715 * py;
69
+ const f = 52.9829189 * (inner - Math.floor(inner));
70
+ return f - Math.floor(f);
71
+ }
72
+ /**
73
+ * The two functions in GLSL, for the lit pass and every caster that keeps the same pixels.
74
+ *
75
+ * `cutoutKeeps(share, pixel, frame)` is the dithered test; `cutoutShare(alpha, cutoff)` is also what
76
+ * a coverage frame writes as alpha. Frame is a float the renderer counts and wraps, so the noise's
77
+ * argument stays small enough for a half-precision device.
78
+ */
79
+ export const CUTOUT_DITHER_GLSL = `
80
+ float cutoutShare(float alpha, float cutoff) {
81
+ float band = max(2.0 * min(cutoff, 1.0 - cutoff), 1e-4);
82
+ return clamp((alpha - cutoff) / band + 0.5, 0.0, 1.0);
83
+ }
84
+
85
+ bool cutoutKeeps(float share, vec2 pixel, float frame) {
86
+ vec2 p = pixel + vec2(${NOISE_FRAME_STEP.toFixed(6)} * frame);
87
+ float noise = fract(52.9829189 * fract(dot(p, vec2(0.06711056, 0.00583715))));
88
+ return share > noise;
89
+ }
90
+ `;
@@ -4,6 +4,7 @@ import { DEPTH_01_TO_CLIP } from './lightVolumeDraw.js';
4
4
  import { compileProgram, uniformLocations } from './shader.js';
5
5
  import { DECAL_PROJECT_FRAG } from './shaders/decalProject.js';
6
6
  import { FULLSCREEN_VERT } from './shaders/fullscreen.js';
7
+ import { blendKeeping } from './sceneCoverage.js';
7
8
  /**
8
9
  * Drawn decals on WebGL2: one scissored triangle per mark, multiplied into the finished scene.
9
10
  *
@@ -54,7 +55,7 @@ export class DecalPass {
54
55
  gl.depthMask(false);
55
56
  gl.disable(gl.CULL_FACE);
56
57
  gl.enable(gl.BLEND);
57
- gl.blendFunc(gl.ZERO, gl.SRC_COLOR);
58
+ blendKeeping(gl, gl.ZERO, gl.SRC_COLOR);
58
59
  gl.enable(gl.SCISSOR_TEST);
59
60
  /* Unit 0, chosen rather than inherited: `bindTexture` binds to whichever unit is active, and
60
61
  the renderer leaves that wherever its last pass finished. `OitPass` records what leaving it
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The eye model's refraction, as `shaders/flat/eye.ts` computes it: the reference its tests hold the
3
+ * arithmetic to, and nothing at run time calls it.
4
+ *
5
+ * **An iris is seen through a cornea**, a few millimetres of clear fluid above it with an index near
6
+ * water's, so it is not where it is painted: the eye's ray bends at the cornea toward the axis and
7
+ * meets the iris plane somewhere else, and the iris seems to lie deeper and to shift as the eye
8
+ * turns away. The lit stage moves the iris's texture coordinate by that shift before any map of
9
+ * the material is read, so the whole iris — colour, relief, its own channels — moves as one.
10
+ *
11
+ * **The shift**: refract the view at the cornea's surface, follow the refracted ray down until it
12
+ * has descended the cornea's height above the iris plane along the eye's axis, and take what it
13
+ * travelled across the plane. Head-on it is nothing; at an angle θ to a flat cornea it is
14
+ * `height · tan(asin(sin θ / ior))`. The plane is perpendicular to the axis, so a cornea seen
15
+ * almost edge-on sends the ray along it: the descent is floored, which bounds the shift at about a
16
+ * thousand times the height rather than letting it run to infinity at the limb.
17
+ */
18
+ import type { Vec3 } from '../math/color.ts';
19
+ /** Schlick's base for the cornea's highlight, at the model's default index 1.336. */
20
+ export declare const CORNEA_F0: number;
21
+ /** How slowly a refracted ray may descend toward the iris plane before it is held there. */
22
+ export declare const EYE_MIN_DESCENT = 0.001;
23
+ /** GLSL's `refract`, into `out`: the transmitted direction, or zeros past total internal reflection. */
24
+ export declare function refract(incident: Vec3, normal: Vec3, eta: number, out: Float64Array): void;
25
+ /**
26
+ * How far across the iris plane the eye's ray lands from where it entered the cornea, in metres,
27
+ * into `out`: `toEye` and `normal` at the cornea, `axis` the eye's, all unit vectors; `height` the
28
+ * cornea's height above the iris plane there.
29
+ */
30
+ export declare function eyeRefractionShift(toEye: Vec3, normal: Vec3, axis: Vec3, ior: number, height: number, out: Float64Array): void;
31
+ /**
32
+ * Where the iris is, without a map: 1 inside `irisRadius` of the texture's centre, 0 outside, across
33
+ * an edge `edge` wide — a limbus, not a cut.
34
+ */
35
+ export declare function irisMask(u: number, v: number, irisRadius: number, edge: number): number;
@@ -0,0 +1,40 @@
1
+ /** Schlick's base for the cornea's highlight, at the model's default index 1.336. */
2
+ export const CORNEA_F0 = ((1 - 1.336) / (1 + 1.336)) ** 2;
3
+ /** How slowly a refracted ray may descend toward the iris plane before it is held there. */
4
+ export const EYE_MIN_DESCENT = 1e-3;
5
+ /** GLSL's `refract`, into `out`: the transmitted direction, or zeros past total internal reflection. */
6
+ export function refract(incident, normal, eta, out) {
7
+ const d = normal[0] * incident[0] + normal[1] * incident[1] + normal[2] * incident[2];
8
+ const k = 1 - eta * eta * (1 - d * d);
9
+ if (k < 0) {
10
+ out.fill(0);
11
+ return;
12
+ }
13
+ const s = eta * d + Math.sqrt(k);
14
+ for (let c = 0; c < 3; c++)
15
+ out[c] = eta * incident[c] - s * normal[c];
16
+ }
17
+ /**
18
+ * How far across the iris plane the eye's ray lands from where it entered the cornea, in metres,
19
+ * into `out`: `toEye` and `normal` at the cornea, `axis` the eye's, all unit vectors; `height` the
20
+ * cornea's height above the iris plane there.
21
+ */
22
+ export function eyeRefractionShift(toEye, normal, axis, ior, height, out) {
23
+ const ray = new Float64Array(3);
24
+ refract([-toEye[0], -toEye[1], -toEye[2]], normal, 1 / ior, ray);
25
+ const descent = Math.max(-(ray[0] * axis[0] + ray[1] * axis[1] + ray[2] * axis[2]), EYE_MIN_DESCENT);
26
+ const reach = height / descent;
27
+ const along = (ray[0] * axis[0] + ray[1] * axis[1] + ray[2] * axis[2]) *
28
+ reach;
29
+ for (let c = 0; c < 3; c++)
30
+ out[c] = ray[c] * reach - axis[c] * along;
31
+ }
32
+ /**
33
+ * Where the iris is, without a map: 1 inside `irisRadius` of the texture's centre, 0 outside, across
34
+ * an edge `edge` wide — a limbus, not a cut.
35
+ */
36
+ export function irisMask(u, v, irisRadius, edge) {
37
+ const r = Math.hypot(u - 0.5, v - 0.5);
38
+ const t = Math.min(Math.max((r - (irisRadius - edge)) / (2 * edge), 0), 1);
39
+ return 1 - t * t * (3 - 2 * t);
40
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * BC on WebGL2: which formats this context samples, and the upload of a stored chain.
3
+ *
4
+ * **Four extensions, each a different subset**, and the names a device offers are read off them:
5
+ * `WEBGL_compressed_texture_s3tc` is BC1 to BC3, its `_srgb` twin their sRGB forms,
6
+ * `EXT_texture_compression_rgtc` BC4 and BC5, and `EXT_texture_compression_bptc` BC7 in both. A
7
+ * desktop GPU usually has all four and a phone none, which is why the loader decodes there.
8
+ *
9
+ * BC1 is uploaded as `COMPRESSED_RGBA_S3TC_DXT1_EXT` rather than the RGB one, so a three-colour
10
+ * block's fourth index is transparent here as it is on WebGPU, whose only BC1 is `bc1-rgba-unorm`.
11
+ *
12
+ * The stored levels are uploaded as they are and no chain is generated: `generateMipmap` cannot
13
+ * write a compressed format, and the chain its author stored is the better one anyway. A chain that
14
+ * stops before 1x1 sets `TEXTURE_MAX_LEVEL` to its last level, or the texture is incomplete and
15
+ * samples black.
16
+ */
17
+ import type { CompressedTextureFormat, CompressedTextureSource } from './compressedSource.ts';
18
+ /**
19
+ * The BC formats this context samples, enabling each extension that offers some. Enabling is what
20
+ * `getExtension` does, and a format is only legal in an upload once it has been called.
21
+ */
22
+ export declare function glCompressedFormats(gl: WebGL2RenderingContext): CompressedTextureFormat[];
23
+ /**
24
+ * Upload `layers`, every one of `levels` levels, to the bound `TEXTURE_2D_ARRAY` as `name`, and cap
25
+ * the chain at what was stored. The caller has checked the sources and the device.
26
+ */
27
+ export declare function uploadCompressedArray(gl: WebGL2RenderingContext, name: CompressedTextureFormat, layers: readonly CompressedTextureSource[], levels: number): void;
@@ -0,0 +1,53 @@
1
+ /** The constants each extension defines, written out so a missing extension cannot be read for one. */
2
+ const INTERNAL = {
3
+ bc1: 0x83f1, // COMPRESSED_RGBA_S3TC_DXT1_EXT
4
+ bc2: 0x83f2, // COMPRESSED_RGBA_S3TC_DXT3_EXT
5
+ bc3: 0x83f3, // COMPRESSED_RGBA_S3TC_DXT5_EXT
6
+ 'bc1-srgb': 0x8c4d, // COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT
7
+ 'bc2-srgb': 0x8c4e, // COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT
8
+ 'bc3-srgb': 0x8c4f, // COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT
9
+ bc4: 0x8dbb, // COMPRESSED_RED_RGTC1_EXT
10
+ bc5: 0x8dbd, // COMPRESSED_RED_GREEN_RGTC2_EXT
11
+ bc7: 0x8e8c, // COMPRESSED_RGBA_BPTC_UNORM_EXT
12
+ 'bc7-srgb': 0x8e8d, // COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT
13
+ };
14
+ const BY_EXTENSION = [
15
+ ['WEBGL_compressed_texture_s3tc', ['bc1', 'bc2', 'bc3']],
16
+ ['WEBGL_compressed_texture_s3tc_srgb', ['bc1-srgb', 'bc2-srgb', 'bc3-srgb']],
17
+ ['EXT_texture_compression_rgtc', ['bc4', 'bc5']],
18
+ ['EXT_texture_compression_bptc', ['bc7', 'bc7-srgb']],
19
+ ];
20
+ /**
21
+ * The BC formats this context samples, enabling each extension that offers some. Enabling is what
22
+ * `getExtension` does, and a format is only legal in an upload once it has been called.
23
+ */
24
+ export function glCompressedFormats(gl) {
25
+ const out = [];
26
+ for (const [name, formats] of BY_EXTENSION) {
27
+ if (gl.getExtension(name) !== null)
28
+ out.push(...formats);
29
+ }
30
+ return out;
31
+ }
32
+ /**
33
+ * Upload `layers`, every one of `levels` levels, to the bound `TEXTURE_2D_ARRAY` as `name`, and cap
34
+ * the chain at what was stored. The caller has checked the sources and the device.
35
+ */
36
+ export function uploadCompressedArray(gl, name, layers, levels) {
37
+ const first = layers[0];
38
+ for (let level = 0; level < levels; level++) {
39
+ let size = 0;
40
+ for (const layer of layers)
41
+ size += layer.levels[level].length;
42
+ const data = new Uint8Array(size);
43
+ let at = 0;
44
+ for (const layer of layers) {
45
+ const bytes = layer.levels[level];
46
+ data.set(bytes, at);
47
+ at += bytes.length;
48
+ }
49
+ gl.compressedTexImage3D(gl.TEXTURE_2D_ARRAY, level, INTERNAL[name], Math.max(1, first.width >> level), Math.max(1, first.height >> level), layers.length, 0, data);
50
+ }
51
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_BASE_LEVEL, 0);
52
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAX_LEVEL, levels - 1);
53
+ }