@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,128 @@
1
+ /**
2
+ * A skinned cloth for a renderer: `@driftengine/physics`' solver, run as compute on WebGPU — the
3
+ * particles never leave the device — and as `SkinnedCloth` on the CPU under WebGL2, which has no
4
+ * compute. One schedule, `ClothControl`, and the same kernels either way; what a frame draws is
5
+ * `particles`, through `setCloth(binding, cloth.particles)`.
6
+ *
7
+ * **A function rather than renderer methods, so a game pays for it only by importing it.** See
8
+ * `backend/webgpu/computeHost.ts` for the door it reaches the device through, and why.
9
+ *
10
+ * **The CPU fallback's cost**, measured on a desktop part: a few hundred particles are a
11
+ * millisecond a frame on the main thread, and 4,500 are twenty — a garment that size wants the
12
+ * device. The device solver ran 4,500 particles in 0.65 ms a frame, most of it the dispatches
13
+ * rather than the particles (`demo/dev/skinnedCloth.html?bench=1`).
14
+ */
15
+ import { SkinnedCloth } from '@driftengine/physics';
16
+ import type { SkinnedClothSetup } from '@driftengine/physics';
17
+
18
+ import type { ClothParticlesHandle, RendererApi } from './backend/api.ts';
19
+ import { computeHostOf } from './backend/webgpu/computeHost.ts';
20
+ import type { ComputeHost } from './backend/webgpu/computeHost.ts';
21
+ import { GpuSkinnedCloth } from './backend/webgpu/gpuSkinnedCloth.ts';
22
+
23
+ /** A garment a renderer solves, from `createSkinnedCloth`. */
24
+ export interface SkinnedClothSolver {
25
+ /** What `setCloth(binding, particles)` draws the garment's bound meshes by. */
26
+ readonly particles: ClothParticlesHandle;
27
+ /** Where the solver runs: on the device, or on the CPU beneath a backend without compute. */
28
+ readonly runsOn: 'device' | 'cpu';
29
+ /**
30
+ * One frame: the rig's pose — each joint's global matrix, model space, sixteen floats a joint,
31
+ * and the model matrix — and `dt` seconds, run as whole fixed steps; the particles at the frame's
32
+ * `alpha` are then what the frame draws. **Once a frame, before the draws**, as
33
+ * `updateClothParticles` is: a second in one frame would reach every draw of it on WebGPU.
34
+ */
35
+ step(globals: Float32Array, model: Float32Array, dt: number): void;
36
+ /**
37
+ * The air the cloth drags toward from the next step, world space, metres a second: the frame's
38
+ * sample of the scene's one `WindField`, so a garment moves with everything else in the air.
39
+ */
40
+ setWind(x: number, y: number, z: number): void;
41
+ /** Back to the skinned pose, settled, as a teleport resets it. */
42
+ reset(): void;
43
+ dispose(): void;
44
+ }
45
+
46
+ /** A garment for `renderer`, from a set-up checked by name. */
47
+ export function createSkinnedCloth(
48
+ renderer: RendererApi,
49
+ setup: SkinnedClothSetup,
50
+ ): SkinnedClothSolver {
51
+ const host = computeHostOf(renderer);
52
+ return host === null ? new CpuCloth(renderer, setup) : new DeviceCloth(host, setup);
53
+ }
54
+
55
+ class DeviceCloth implements SkinnedClothSolver {
56
+ readonly runsOn = 'device';
57
+ readonly particles: ClothParticlesHandle;
58
+ private readonly cloth: GpuSkinnedCloth;
59
+
60
+ constructor(
61
+ private readonly host: ComputeHost,
62
+ setup: SkinnedClothSetup,
63
+ ) {
64
+ this.cloth = new GpuSkinnedCloth(host.device(), setup);
65
+ this.particles = this.cloth.particles;
66
+ }
67
+
68
+ step(globals: Float32Array, model: Float32Array, dt: number): void {
69
+ if (this.host.lost()) return;
70
+ this.cloth.advance(globals, model, dt, this.host.changeFrame());
71
+ }
72
+
73
+ setWind(x: number, y: number, z: number): void {
74
+ this.cloth.setWind(x, y, z);
75
+ }
76
+
77
+ reset(): void {
78
+ if (this.host.lost()) return;
79
+ this.cloth.reset();
80
+ }
81
+
82
+ /** Its buffers at once — its work is submitted — and its textures with the frame's. */
83
+ dispose(): void {
84
+ this.cloth.dispose();
85
+ this.host.retire(this.cloth.particles.textures);
86
+ }
87
+
88
+ /** The particles where the last step left them: the parity check's door, waits on the device. */
89
+ read(): Promise<Float32Array> {
90
+ return this.cloth.read();
91
+ }
92
+ }
93
+
94
+ class CpuCloth implements SkinnedClothSolver {
95
+ readonly runsOn = 'cpu';
96
+ readonly particles: ClothParticlesHandle;
97
+ private readonly cloth: SkinnedCloth;
98
+ /** The frame's particles at `alpha`, before they go up. */
99
+ private readonly staging: Float32Array;
100
+
101
+ constructor(
102
+ private readonly renderer: RendererApi,
103
+ setup: SkinnedClothSetup,
104
+ ) {
105
+ this.cloth = new SkinnedCloth(setup);
106
+ this.particles = renderer.createClothParticles(this.cloth.count);
107
+ this.staging = new Float32Array(this.cloth.count * 3);
108
+ }
109
+
110
+ step(globals: Float32Array, model: Float32Array, dt: number): void {
111
+ this.cloth.setPose(globals, model);
112
+ this.cloth.advance(dt);
113
+ this.cloth.interpolate(this.cloth.alpha, this.staging);
114
+ this.renderer.updateClothParticles(this.particles, this.staging);
115
+ }
116
+
117
+ setWind(x: number, y: number, z: number): void {
118
+ this.cloth.setWind(x, y, z);
119
+ }
120
+
121
+ reset(): void {
122
+ this.cloth.reset();
123
+ }
124
+
125
+ dispose(): void {
126
+ this.renderer.disposeClothParticles(this.particles);
127
+ }
128
+ }
@@ -8,6 +8,7 @@ import { compileProgram, uniformLocations } from './shader.ts';
8
8
  import { FULLSCREEN_VERT } from './shaders/fullscreen.ts';
9
9
  import { SSR_RESOLVE_FRAG } from './shaders/ssrResolve.ts';
10
10
  import { SSR_TRACE_FRAG } from './shaders/ssrTrace.ts';
11
+ import { blendKeeping } from './sceneCoverage.ts';
11
12
 
12
13
  /**
13
14
  * Screen-space reflection on WebGL2: a scissored trace per surface, then one composite.
@@ -202,7 +203,7 @@ export class SsrPass {
202
203
  gl.disable(gl.CULL_FACE);
203
204
  gl.enable(gl.BLEND);
204
205
  /* Premultiplied `over`: the trace already multiplied its colour by the coverage it found. */
205
- gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
206
+ blendKeeping(gl, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
206
207
 
207
208
  gl.useProgram(this.resolve);
208
209
  gl.activeTexture(gl.TEXTURE0);
@@ -0,0 +1,274 @@
1
+ /**
2
+ * A material's shading model: how its surface answers light, when the standard model is not what
3
+ * it is made of — brushed metal, hair, skin, an eye.
4
+ *
5
+ * **A frozen descriptor, made once by a factory, carrying a kind and that model's numbers.** The
6
+ * standard model is `null`, exactly today's shader, and a scene that never names a model compiles
7
+ * nothing of any of them: each is a pipeline constant of its own (`MODEL_ANISOTROPIC` and the rest
8
+ * in `shaders/flat/models.ts`), off in every pipeline but the ones a material asks for.
9
+ *
10
+ * **Two vectors of numbers a material, and no more.** The lit stage's eight-light build is 254 of
11
+ * the 256 uniform vectors an Adreno 740 offers, so a model's numbers are packed into
12
+ * `uModelParams[2]` — the last component of the second says whether a model map is bound — and
13
+ * every model fits. See `packModel` for each layout.
14
+ *
15
+ * **The eye's axis is the one number that moves.** It is the eye's forward direction in the world,
16
+ * which a turning head or a glance changes every frame; the lit stage has no room left for another
17
+ * varying (sixteen, WebGPU's ceiling), so the renderer turns the descriptor's mesh-local axis by
18
+ * the draw's model matrix and the eye's joint each draw, with `eyeAxisInWorld`.
19
+ */
20
+ import type { Vec3 } from '../math/color.ts';
21
+
22
+ /** Strands of brushed metal, satin or hair-like fibre: a highlight stretched across them. */
23
+ export interface AnisotropicModel {
24
+ readonly kind: 'anisotropic';
25
+ /** 0 to 1: how far the highlight is stretched along the tangent. */
26
+ readonly strength: number;
27
+ /** Radians, turning the stretch's direction from the tangent toward the bitangent. */
28
+ readonly rotation: number;
29
+ }
30
+
31
+ /**
32
+ * Hair: three lobes along the strand, which is the mesh's tangent, running from root to tip
33
+ * (`hairLobes.ts`). A model map's green turns each strand's tilt by up to half of it either way and
34
+ * its blue is occlusion.
35
+ */
36
+ export interface HairModel {
37
+ readonly kind: 'hair';
38
+ /**
39
+ * Radians the cuticle's scales tilt the strand's surface: positive moves the white highlight
40
+ * toward the root and the coloured one toward the tip. A card whose tangent runs tip to root
41
+ * takes it negative.
42
+ */
43
+ readonly shift: number;
44
+ /** 0 to 1: the light scattered through many strands, tinted by their colour. */
45
+ readonly scatter: number;
46
+ /** How strongly light from behind glows through: the transmitted lobe. */
47
+ readonly backlit: number;
48
+ }
49
+
50
+ /** Skin: light scattered beneath the surface, reddening its terminator and its thin parts. */
51
+ export interface SkinModel {
52
+ readonly kind: 'skin';
53
+ /** The colour light keeps travelling beneath the surface — the mean free path, as a colour. */
54
+ readonly scatterColor: Vec3;
55
+ /** Metres: how far light travels beneath it before it leaves. */
56
+ readonly radius: number;
57
+ /** 0 to 1: how much a light behind a thin part shows through it. */
58
+ readonly transmission: number;
59
+ /** Which of up to eight profiles the screen-space scattering blurs this skin by. */
60
+ readonly profile: number;
61
+ }
62
+
63
+ /** An eye: an iris seen through a cornea, which refracts it, under a mirror-smooth highlight. */
64
+ export interface EyeModel {
65
+ readonly kind: 'eye';
66
+ /** The iris's radius, in texture coordinates from the centre of the eye's map. */
67
+ readonly irisRadius: number;
68
+ /** Metres from the cornea down to the iris. */
69
+ readonly irisDepth: number;
70
+ /** The cornea's index of refraction. */
71
+ readonly ior: number;
72
+ /** How smooth the cornea's highlight is. */
73
+ readonly corneaRoughness: number;
74
+ /** The eye's forward direction, in the mesh's own space at its bind pose. */
75
+ readonly axis: Vec3;
76
+ /** The joint the eye turns with, or -1 for an eye placed by its model matrix alone. */
77
+ readonly joint: number;
78
+ }
79
+
80
+ /** Every model a material can name, by its `kind`: see `SurfaceMaterial.model`. */
81
+ export type SurfaceModel = AnisotropicModel | HairModel | SkinModel | EyeModel;
82
+ /** A model's name, which also picks its pipeline switch: `SURFACE_MODEL_SWITCH`. */
83
+ export type SurfaceModelKind = SurfaceModel['kind'];
84
+
85
+ /** Every kind, in the order the lit stage declares their switches. */
86
+ export const SURFACE_MODEL_KINDS: readonly SurfaceModelKind[] = [
87
+ 'anisotropic',
88
+ 'hair',
89
+ 'skin',
90
+ 'eye',
91
+ ];
92
+
93
+ /** The lit stage's switch for each kind. */
94
+ export const SURFACE_MODEL_SWITCH: Readonly<Record<SurfaceModelKind, string>> = {
95
+ anisotropic: 'MODEL_ANISOTROPIC',
96
+ hair: 'MODEL_HAIR',
97
+ skin: 'MODEL_SKIN',
98
+ eye: 'MODEL_EYE',
99
+ };
100
+
101
+ /** Floats a material's model takes: `uModelParams[2]`. */
102
+ export const MODEL_PARAM_FLOATS = 8;
103
+
104
+ function check(model: string, name: string, value: number, low: number, high: number): number {
105
+ if (!Number.isFinite(value) || value < low || value > high) {
106
+ throw new Error(`${model}: ${name} must be from ${low} to ${high}, not ${String(value)}`);
107
+ }
108
+ return value;
109
+ }
110
+
111
+ function unit(model: string, name: string, v: Vec3): Vec3 {
112
+ const length = Math.hypot(v[0], v[1], v[2]);
113
+ if (!(length > 1e-6))
114
+ throw new Error(`${model}: ${name} must be a direction, not [${v.join(', ')}]`);
115
+ return [v[0] / length, v[1] / length, v[2] / length];
116
+ }
117
+
118
+ /** Brushed metal and the like, after glTF's `KHR_materials_anisotropy`: a strength and a turn. */
119
+ export function anisotropicModel(
120
+ options: { strength?: number; rotation?: number } = {},
121
+ ): AnisotropicModel {
122
+ return Object.freeze({
123
+ kind: 'anisotropic',
124
+ strength: check('anisotropicModel', 'strength', options.strength ?? 1, 0, 1),
125
+ rotation: check(
126
+ 'anisotropicModel',
127
+ 'rotation',
128
+ options.rotation ?? 0,
129
+ -2 * Math.PI,
130
+ 2 * Math.PI,
131
+ ),
132
+ });
133
+ }
134
+
135
+ /** Hair, along the mesh's tangent from root to tip. A tilt of 3°, within the 2.5° to 5° measured. */
136
+ export function hairModel(
137
+ options: { shift?: number; scatter?: number; backlit?: number } = {},
138
+ ): HairModel {
139
+ return Object.freeze({
140
+ kind: 'hair',
141
+ shift: check('hairModel', 'shift', options.shift ?? (3 * Math.PI) / 180, -0.5, 0.5),
142
+ scatter: check('hairModel', 'scatter', options.scatter ?? 0.7, 0, 1),
143
+ backlit: check('hairModel', 'backlit', options.backlit ?? 1, 0, 4),
144
+ });
145
+ }
146
+
147
+ /** Skin. The defaults are a fair skin's: a red mean free path a little over a centimetre long. */
148
+ export function skinModel(
149
+ options: { scatterColor?: Vec3; radius?: number; transmission?: number; profile?: number } = {},
150
+ ): SkinModel {
151
+ const color = options.scatterColor ?? [0.85, 0.35, 0.22];
152
+ for (let c = 0; c < 3; c++) check('skinModel', 'scatterColor', color[c] as number, 0, 1);
153
+ const profile = check('skinModel', 'profile', options.profile ?? 0, 0, 7);
154
+ if (!Number.isInteger(profile))
155
+ throw new Error(`skinModel: profile must be a whole number, not ${profile}`);
156
+ return Object.freeze({
157
+ kind: 'skin',
158
+ scatterColor: Object.freeze([color[0], color[1], color[2]]) as unknown as Vec3,
159
+ radius: check('skinModel', 'radius', options.radius ?? 0.012, 0, 0.1),
160
+ transmission: check('skinModel', 'transmission', options.transmission ?? 0.5, 0, 1),
161
+ profile,
162
+ });
163
+ }
164
+
165
+ /** An eye. The defaults are a human eye's, its axis along the mesh's +Z. */
166
+ export function eyeModel(
167
+ options: {
168
+ irisRadius?: number;
169
+ irisDepth?: number;
170
+ ior?: number;
171
+ corneaRoughness?: number;
172
+ axis?: Vec3;
173
+ joint?: number;
174
+ } = {},
175
+ ): EyeModel {
176
+ const joint = options.joint ?? -1;
177
+ if (!Number.isInteger(joint) || joint < -1) {
178
+ throw new Error(`eyeModel: joint must be a joint index or -1, not ${String(joint)}`);
179
+ }
180
+ return Object.freeze({
181
+ kind: 'eye',
182
+ irisRadius: check('eyeModel', 'irisRadius', options.irisRadius ?? 0.15, 0.001, 0.5),
183
+ irisDepth: check('eyeModel', 'irisDepth', options.irisDepth ?? 0.003, 0, 0.02),
184
+ ior: check('eyeModel', 'ior', options.ior ?? 1.336, 1, 2),
185
+ corneaRoughness: check('eyeModel', 'corneaRoughness', options.corneaRoughness ?? 0.02, 0, 1),
186
+ axis: Object.freeze(unit('eyeModel', 'axis', options.axis ?? [0, 0, 1])) as unknown as Vec3,
187
+ joint,
188
+ });
189
+ }
190
+
191
+ /**
192
+ * A model's numbers into `out` — eight floats, `uModelParams[0]` then `[1]` — and whether a model
193
+ * map is bound into the last. Zeros for the standard model. An eye's axis goes in with the
194
+ * descriptor's own until the draw turns it: see `eyeAxisInWorld`.
195
+ *
196
+ * - anisotropic: strength, cos and sin of the rotation · −, −, −, map
197
+ * - hair: shift, scatter, backlit, − · −, −, −, map
198
+ * - skin: the scatter colour, radius · transmission, profile, −, map
199
+ * - eye: iris radius, iris depth, ior, cornea roughness · the axis, map
200
+ */
201
+ export function packModel(model: SurfaceModel | null, mapped: boolean, out: Float32Array): void {
202
+ out.fill(0);
203
+ if (model === null) return;
204
+ out[7] = mapped ? 1 : 0;
205
+ switch (model.kind) {
206
+ case 'anisotropic':
207
+ out[0] = model.strength;
208
+ out[1] = Math.cos(model.rotation);
209
+ out[2] = Math.sin(model.rotation);
210
+ return;
211
+ case 'hair':
212
+ out[0] = model.shift;
213
+ out[1] = model.scatter;
214
+ out[2] = model.backlit;
215
+ return;
216
+ case 'skin':
217
+ out[0] = model.scatterColor[0];
218
+ out[1] = model.scatterColor[1];
219
+ out[2] = model.scatterColor[2];
220
+ out[3] = model.radius;
221
+ out[4] = model.transmission;
222
+ out[5] = model.profile;
223
+ return;
224
+ case 'eye':
225
+ out[0] = model.irisRadius;
226
+ out[1] = model.irisDepth;
227
+ out[2] = model.ior;
228
+ out[3] = model.corneaRoughness;
229
+ out[4] = model.axis[0];
230
+ out[5] = model.axis[1];
231
+ out[6] = model.axis[2];
232
+ return;
233
+ }
234
+ }
235
+
236
+ /**
237
+ * An eye's axis in the world for one draw, into `out[at…at+2]`: the descriptor's mesh-local axis
238
+ * turned by its joint's palette entry, where it names one and a palette is set, and then by the
239
+ * draw's model matrix — the turn its vertices take, without the translation. Normalised, because
240
+ * a palette carries a joint's scale.
241
+ */
242
+ export function eyeAxisInWorld(
243
+ model: EyeModel,
244
+ placement: ArrayLike<number>,
245
+ palette: ArrayLike<number> | null,
246
+ out: Float32Array,
247
+ at: number,
248
+ ): void {
249
+ let [x, y, z] = model.axis;
250
+ const j = model.joint * 16;
251
+ if (palette !== null && model.joint >= 0 && j + 16 <= palette.length) {
252
+ const px =
253
+ (palette[j] as number) * x + (palette[j + 4] as number) * y + (palette[j + 8] as number) * z;
254
+ const py =
255
+ (palette[j + 1] as number) * x +
256
+ (palette[j + 5] as number) * y +
257
+ (palette[j + 9] as number) * z;
258
+ const pz =
259
+ (palette[j + 2] as number) * x +
260
+ (palette[j + 6] as number) * y +
261
+ (palette[j + 10] as number) * z;
262
+ x = px;
263
+ y = py;
264
+ z = pz;
265
+ }
266
+ const m = placement;
267
+ const wx = (m[0] as number) * x + (m[4] as number) * y + (m[8] as number) * z;
268
+ const wy = (m[1] as number) * x + (m[5] as number) * y + (m[9] as number) * z;
269
+ const wz = (m[2] as number) * x + (m[6] as number) * y + (m[10] as number) * z;
270
+ const length = Math.hypot(wx, wy, wz) || 1;
271
+ out[at] = wx / length;
272
+ out[at + 1] = wy / length;
273
+ out[at + 2] = wz / length;
274
+ }
@@ -8,9 +8,19 @@
8
8
  * array samples exactly as a 2D texture does. A mesh names its layer per vertex (`MeshData.layers`),
9
9
  * so a merged block wearing forty facades is one draw.
10
10
  */
11
- import { isSourceList, layerSize, refuseArrayUpdate, sourceSize } from './textureSource.ts';
11
+ import type { SurfaceModel } from './surfaceModel.ts';
12
+ import { layerSize, refuseArrayUpdate, sourceSize } from './textureSource.ts';
13
+ import { compressedLayers, planBlocks, refuseBlockUpdate } from './compressedSource.ts';
14
+ import type {
15
+ BlockFormat,
16
+ CompressedTextureFormat,
17
+ CompressedTextureSource,
18
+ SurfaceSource,
19
+ } from './compressedSource.ts';
20
+ import { uploadCompressedArray } from './glCompressed.ts';
12
21
  import { SURFACE_EFFECT_TEXELS, packSurfaceEffects } from './surfaceEffects.ts';
13
22
  import type { SurfaceLayerEffect } from './surfaceEffects.ts';
23
+ import type { CutoutMode } from './cutoutDither.ts';
14
24
 
15
25
  /**
16
26
  * How a texture behaves past its edges and between its texels.
@@ -70,8 +80,31 @@ export interface SurfaceMaterial<Texture = SurfaceTexture> {
70
80
  /** Repeats across the mesh's own UV range, per axis. Applies to every map the material holds. */
71
81
  uScale?: number;
72
82
  vScale?: number;
73
- /** Alpha below which a fragment is discarded. See `uAlbedoCutout`. */
83
+ /**
84
+ * Where the texture starts, per axis, added after the scale: a surface samples
85
+ * `uv · scale + offset`. Applies to every map, and to a cutout's shadow and depth as well.
86
+ *
87
+ * **What picks a cell of a flipbook or an atlas through the material**, so one quad draws any
88
+ * cell: a strip of four frames is `uScale: 0.25` and `uOffset: frame * 0.25`. Without it a
89
+ * consumer built one quad mesh per cell — 106 meshes for four strips — because the only way to
90
+ * move a texture was to move the geometry's own coordinates.
91
+ */
92
+ uOffset?: number;
93
+ vOffset?: number;
94
+ /** Alpha below which a fragment is discarded. See `uCutout`. */
74
95
  cutout?: number;
96
+ /**
97
+ * How the cutout's edge is drawn: `'hard'`, the default and every cutout before 4.8.4, or
98
+ * `'dithered'`, which keeps a pixel by the share of it the texture covers so a strand of hair, a
99
+ * lash or a fringe of leaves has a soft edge instead of a stair of pixels.
100
+ *
101
+ * **The frame decides how a dithered edge is resolved**, because the material cannot know: under a
102
+ * temporal resolve (TAA or DriftTR) the pattern moves every frame and the resolve averages it;
103
+ * multisampled, the share goes to the GPU as alpha-to-coverage; with neither, the test is hard,
104
+ * since a dither nothing averages is grain. A translucent draw tests hard whatever this says. See
105
+ * `cutoutDither.ts`.
106
+ */
107
+ cutoutMode?: CutoutMode;
75
108
  /**
76
109
  * Whether both faces are seen: glTF's `doubleSided`, what a curtain or a leaf card is. Drawn
77
110
  * without culling, and a back face is lit as its front, with the normal turned to the viewer.
@@ -112,6 +145,19 @@ export interface SurfaceMaterial<Texture = SurfaceTexture> {
112
145
  * consumer who wants a bound map twice as bright asks here rather than rebuilding the mesh.
113
146
  */
114
147
  emissiveScale?: readonly [number, number, number];
148
+ /**
149
+ * How the surface answers light, when the standard model is not what it is made of: brushed
150
+ * metal, hair, skin, an eye — `anisotropicModel`, `hairModel`, `skinModel`, `eyeModel`. Null or
151
+ * absent is the standard model, exactly as before. **A pipeline of its own**, compiled the first
152
+ * time a draw asks, so a scene that names no model pays for none. See `surfaceModel.ts`.
153
+ */
154
+ model?: SurfaceModel | null;
155
+ /**
156
+ * The model's own channels, in the albedo's coordinates and layers; what each channel means is
157
+ * the model's to say. Absent, each model takes its own neutral. Sampled linear, like any map
158
+ * that is not a colour.
159
+ */
160
+ modelMap?: Texture | null;
115
161
  }
116
162
 
117
163
  export interface SurfaceTextureOptions {
@@ -211,79 +257,45 @@ export class SurfaceTexture {
211
257
  readonly layers: number;
212
258
  /** The per-layer effects table, or null for a texture given none. See `surfaceEffects.ts`. */
213
259
  private effectsTable: WebGLTexture | null = null;
260
+ /** The block format a compressed texture holds, or null for one uploaded from images. */
261
+ private readonly blockFormat: BlockFormat | null;
214
262
 
215
263
  /**
216
264
  * One image, or an array of images that must share one size (`layerSize` refuses otherwise).
217
- * Uploaded unflipped either way; see the constructor body for why.
265
+ * Uploaded unflipped either way; see the constructor body for why. Or blocks, uploaded as they are
266
+ * where `compressed` — the formats this context samples — has theirs; see `compressedSource.ts`.
218
267
  */
219
268
  constructor(
220
269
  gl: WebGL2RenderingContext,
221
- source: TexImageSource | readonly TexImageSource[],
270
+ source: SurfaceSource | readonly SurfaceSource[],
222
271
  options: SurfaceTextureOptions = {},
272
+ compressed: readonly CompressedTextureFormat[] = [],
223
273
  ) {
224
- const sources: readonly TexImageSource[] = isSourceList(source) ? source : [source];
225
- const { width, height } = layerSize(sources);
226
- this.layers = sources.length;
274
+ const listed = (Array.isArray(source) ? source : [source]) as readonly SurfaceSource[];
275
+ const blocks = compressedLayers(listed);
276
+ this.srgb = (options.colorSpace ?? 'linear') === 'srgb';
277
+ /* Refused before anything is allocated, so a refusal leaves no texture behind. */
278
+ const plan = blocks === null ? null : planBlocks(blocks, this.srgb, compressed);
227
279
  const texture = gl.createTexture();
228
280
  if (texture === null) throw new Error('SurfaceTexture: createTexture failed');
229
281
  this.texture = texture;
230
- this.mipmapped = options.mipmap ?? true;
231
- this.srgb = (options.colorSpace ?? 'linear') === 'srgb';
232
-
233
282
  const wrap = (options.wrap ?? 'repeat') === 'repeat' ? gl.REPEAT : gl.CLAMP_TO_EDGE;
234
-
235
283
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, texture);
236
- /*
237
- * **Not flipped, and this used to be, which is the bug it is written down for.**
238
- *
239
- * The flip was here with a comment saying every source is a 2D drawing surface whose Y
240
- * runs down from the top-left, so without it "every texture arrives mirrored". That is
241
- * true of a canvas and **WebGL ignores `UNPACK_FLIP_Y_WEBGL` entirely for an
242
- * `ImageBitmap`**, which carries its own orientation. So the two source types the same
243
- * method accepts arrived the opposite way up from each other, and nothing caught it
244
- * because every canvas-sourced texture in this repository was symmetric under a vertical
245
- * flip: eroded noise, a radial halo, a sleeve blurred to 24 pixels and back. One consumer
246
- * painted lettering to a canvas beside a sleeve decoded as a bitmap and the text was
247
- * upside down.
248
- *
249
- * **Made to agree by dropping the flip rather than by extending it**, and the direction
250
- * is a deliberate choice about blast radius rather than a coin toss: `drftLoader` builds
251
- * every model texture from an `ImageBitmap`, so flipping bitmaps to match canvases would
252
- * turn over every painted surface on every loaded model. Dropping it changes only
253
- * canvas-sourced textures, and every one of those that exists today is symmetric.
254
- *
255
- * A caller that was mirroring its own canvas to cancel this must stop.
256
- */
257
- /* Allocated mutable rather than with texStorage3D, so `update` can hand a one-layer texture a
258
- new size, as texImage2D always could. Each layer is then written in place. */
259
- gl.texImage3D(
260
- gl.TEXTURE_2D_ARRAY,
261
- 0,
262
- this.srgb ? gl.SRGB8_ALPHA8 : gl.RGBA,
263
- width,
264
- height,
265
- sources.length,
266
- 0,
267
- gl.RGBA,
268
- gl.UNSIGNED_BYTE,
269
- null,
270
- );
271
- for (let layer = 0; layer < sources.length; layer++) {
272
- gl.texSubImage3D(
273
- gl.TEXTURE_2D_ARRAY,
274
- 0,
275
- 0,
276
- 0,
277
- layer,
278
- width,
279
- height,
280
- 1,
281
- gl.RGBA,
282
- gl.UNSIGNED_BYTE,
283
- sources[layer] as TexImageSource,
284
- );
285
- }
286
284
 
285
+ if (blocks !== null && plan !== null) {
286
+ /* The stored chain, or level 0 alone where no chain was asked for. Nothing is generated:
287
+ `generateMipmap` cannot write a compressed format. */
288
+ this.layers = blocks.length;
289
+ this.blockFormat = (blocks[0] as CompressedTextureSource).format;
290
+ this.mipmapped = (options.mipmap ?? true) && plan.levels > 1;
291
+ uploadCompressedArray(gl, plan.name, blocks, this.mipmapped ? plan.levels : 1);
292
+ } else {
293
+ const sources = listed as readonly TexImageSource[];
294
+ this.layers = sources.length;
295
+ this.blockFormat = null;
296
+ this.mipmapped = options.mipmap ?? true;
297
+ uploadImages(gl, sources, this.srgb);
298
+ }
287
299
  gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, wrap);
288
300
  gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, wrap);
289
301
  /* Nearest where the caller said their pixels are the subject; see `filter`. The mip chain is
@@ -303,7 +315,7 @@ export class SurfaceTexture {
303
315
  : gl.LINEAR,
304
316
  );
305
317
 
306
- if (this.mipmapped) gl.generateMipmap(gl.TEXTURE_2D_ARRAY);
318
+ if (this.mipmapped && this.blockFormat === null) gl.generateMipmap(gl.TEXTURE_2D_ARRAY);
307
319
  applyAnisotropy(gl, options.anisotropy ?? DEFAULT_ANISOTROPY);
308
320
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
309
321
 
@@ -334,7 +346,6 @@ export class SurfaceTexture {
334
346
  }
335
347
  }
336
348
 
337
- /** Bind the effects table to `unit`, or `fallback` for a texture given none. */
338
349
  /** Whether this texture carries an effects table, which the lit program reads only once one exists. */
339
350
  get hasEffects(): boolean {
340
351
  return this.effectsTable !== null;
@@ -359,6 +370,7 @@ export class SurfaceTexture {
359
370
  update(gl: WebGL2RenderingContext, source: TexImageSource): void {
360
371
  if (this.texture === null) return;
361
372
  refuseArrayUpdate(this.layers);
373
+ refuseBlockUpdate(this.blockFormat);
362
374
  const { width, height } = sourceSize(source);
363
375
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.texture);
364
376
  /* Unflipped, matching the constructor. See it for why, and for what it cost. */
@@ -393,6 +405,65 @@ export class SurfaceTexture {
393
405
  }
394
406
  }
395
407
 
408
+ /** Images into the bound array, level 0 of each layer; the chain, if any, is generated after. */
409
+ function uploadImages(
410
+ gl: WebGL2RenderingContext,
411
+ sources: readonly TexImageSource[],
412
+ srgb: boolean,
413
+ ): void {
414
+ const { width, height } = layerSize(sources);
415
+ /*
416
+ * **Not flipped, and this used to be, which is the bug it is written down for.**
417
+ *
418
+ * The flip was here with a comment saying every source is a 2D drawing surface whose Y
419
+ * runs down from the top-left, so without it "every texture arrives mirrored". That is
420
+ * true of a canvas and **WebGL ignores `UNPACK_FLIP_Y_WEBGL` entirely for an
421
+ * `ImageBitmap`**, which carries its own orientation. So the two source types the same
422
+ * method accepts arrived the opposite way up from each other, and nothing caught it
423
+ * because every canvas-sourced texture in this repository was symmetric under a vertical
424
+ * flip: eroded noise, a radial halo, a sleeve blurred to 24 pixels and back. One consumer
425
+ * painted lettering to a canvas beside a sleeve decoded as a bitmap and the text was
426
+ * upside down.
427
+ *
428
+ * **Made to agree by dropping the flip rather than by extending it**, and the direction
429
+ * is a deliberate choice about blast radius rather than a coin toss: `drftLoader` builds
430
+ * every model texture from an `ImageBitmap`, so flipping bitmaps to match canvases would
431
+ * turn over every painted surface on every loaded model. Dropping it changes only
432
+ * canvas-sourced textures, and every one of those that exists today is symmetric.
433
+ *
434
+ * A caller that was mirroring its own canvas to cancel this must stop.
435
+ */
436
+ /* Allocated mutable rather than with texStorage3D, so `update` can hand a one-layer texture a
437
+ new size, as texImage2D always could. Each layer is then written in place. */
438
+ gl.texImage3D(
439
+ gl.TEXTURE_2D_ARRAY,
440
+ 0,
441
+ srgb ? gl.SRGB8_ALPHA8 : gl.RGBA,
442
+ width,
443
+ height,
444
+ sources.length,
445
+ 0,
446
+ gl.RGBA,
447
+ gl.UNSIGNED_BYTE,
448
+ null,
449
+ );
450
+ for (let layer = 0; layer < sources.length; layer++) {
451
+ gl.texSubImage3D(
452
+ gl.TEXTURE_2D_ARRAY,
453
+ 0,
454
+ 0,
455
+ 0,
456
+ layer,
457
+ width,
458
+ height,
459
+ 1,
460
+ gl.RGBA,
461
+ gl.UNSIGNED_BYTE,
462
+ sources[layer] as TexImageSource,
463
+ );
464
+ }
465
+ }
466
+
396
467
  /**
397
468
  * Set anisotropic filtering when the driver offers it, and stay silent when it does not.
398
469
  *