@driftengine/core 4.8.3 → 4.8.5

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 (180) hide show
  1. package/CHANGELOG.json +82 -0
  2. package/README.md +1 -1
  3. package/dist/build/shaderComments.js +0 -0
  4. package/dist/geometry/meshConcat.js +2 -0
  5. package/dist/index.d.ts +10 -4
  6. package/dist/index.js +16 -1
  7. package/dist/math/scalar.js +1 -1
  8. package/dist/render/anisotropicLobe.d.ts +7 -0
  9. package/dist/render/anisotropicLobe.js +33 -0
  10. package/dist/render/backend/api.d.ts +41 -4
  11. package/dist/render/backend/select.js +8 -0
  12. package/dist/render/backend/webgl2/clothTextures.d.ts +27 -0
  13. package/dist/render/backend/webgl2/clothTextures.js +93 -0
  14. package/dist/render/backend/webgl2/renderer.d.ts +136 -3
  15. package/dist/render/backend/webgl2/renderer.js +604 -63
  16. package/dist/render/backend/webgl2/skinScatter.d.ts +50 -0
  17. package/dist/render/backend/webgl2/skinScatter.js +256 -0
  18. package/dist/render/backend/webgpu/buffers.d.ts +25 -11
  19. package/dist/render/backend/webgpu/buffers.js +98 -49
  20. package/dist/render/backend/webgpu/clothSolvePack.d.ts +27 -0
  21. package/dist/render/backend/webgpu/clothSolvePack.js +129 -0
  22. package/dist/render/backend/webgpu/clothTextures.d.ts +68 -0
  23. package/dist/render/backend/webgpu/clothTextures.js +145 -0
  24. package/dist/render/backend/webgpu/compressedUpload.d.ts +18 -0
  25. package/dist/render/backend/webgpu/compressedUpload.js +38 -0
  26. package/dist/render/backend/webgpu/computeHost.d.ts +21 -0
  27. package/dist/render/backend/webgpu/computeHost.js +5 -0
  28. package/dist/render/backend/webgpu/depthPass.d.ts +16 -6
  29. package/dist/render/backend/webgpu/depthPass.js +39 -9
  30. package/dist/render/backend/webgpu/drawCommand.js +6 -2
  31. package/dist/render/backend/webgpu/flatPass.d.ts +38 -4
  32. package/dist/render/backend/webgpu/flatPass.js +109 -12
  33. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +74 -0
  34. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +452 -0
  35. package/dist/render/backend/webgpu/motionPass.d.ts +20 -0
  36. package/dist/render/backend/webgpu/motionPass.js +106 -9
  37. package/dist/render/backend/webgpu/postPass.d.ts +3 -1
  38. package/dist/render/backend/webgpu/postPass.js +6 -3
  39. package/dist/render/backend/webgpu/renderer.d.ts +113 -5
  40. package/dist/render/backend/webgpu/renderer.js +498 -48
  41. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +33 -0
  42. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +469 -0
  43. package/dist/render/backend/webgpu/skinScatterPass.d.ts +51 -0
  44. package/dist/render/backend/webgpu/skinScatterPass.js +253 -0
  45. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
  46. package/dist/render/backend/webgpu/surfaceTexturePass.js +37 -12
  47. package/dist/render/clothBindingData.d.ts +41 -0
  48. package/dist/render/clothBindingData.js +72 -0
  49. package/dist/render/clothPlacement.d.ts +25 -0
  50. package/dist/render/clothPlacement.js +85 -0
  51. package/dist/render/compressedSource.d.ts +66 -0
  52. package/dist/render/compressedSource.js +138 -0
  53. package/dist/render/cutoutCaster.d.ts +2 -0
  54. package/dist/render/cutoutCaster.js +2 -0
  55. package/dist/render/cutoutDither.d.ts +51 -0
  56. package/dist/render/cutoutDither.js +90 -0
  57. package/dist/render/eyeRefraction.d.ts +35 -0
  58. package/dist/render/eyeRefraction.js +40 -0
  59. package/dist/render/glCompressed.d.ts +27 -0
  60. package/dist/render/glCompressed.js +53 -0
  61. package/dist/render/hairLobes.d.ts +70 -0
  62. package/dist/render/hairLobes.js +120 -0
  63. package/dist/render/lightBudget.d.ts +22 -0
  64. package/dist/render/lightBudget.js +22 -0
  65. package/dist/render/mesh.d.ts +7 -0
  66. package/dist/render/mesh.js +24 -0
  67. package/dist/render/recon/motionVectors.d.ts +4 -2
  68. package/dist/render/recon/motionVectors.js +6 -3
  69. package/dist/render/renderQuality.d.ts +20 -0
  70. package/dist/render/renderQuality.js +5 -0
  71. package/dist/render/rendererHarness.js +4 -0
  72. package/dist/render/sceneTarget.d.ts +12 -0
  73. package/dist/render/sceneTarget.js +14 -0
  74. package/dist/render/shaders/clothBinding.d.ts +22 -0
  75. package/dist/render/shaders/clothBinding.js +80 -0
  76. package/dist/render/shaders/depth.js +42 -6
  77. package/dist/render/shaders/flat/anisotropic.d.ts +15 -0
  78. package/dist/render/shaders/flat/anisotropic.js +68 -0
  79. package/dist/render/shaders/flat/eye.d.ts +1 -0
  80. package/dist/render/shaders/flat/eye.js +81 -0
  81. package/dist/render/shaders/flat/hair.d.ts +1 -0
  82. package/dist/render/shaders/flat/hair.js +101 -0
  83. package/dist/render/shaders/flat/index.js +36 -2
  84. package/dist/render/shaders/flat/main.d.ts +1 -1
  85. package/dist/render/shaders/flat/main.js +113 -17
  86. package/dist/render/shaders/flat/models.d.ts +14 -0
  87. package/dist/render/shaders/flat/models.js +273 -0
  88. package/dist/render/shaders/flat/preamble.js +26 -24
  89. package/dist/render/shaders/flat/skin.d.ts +1 -0
  90. package/dist/render/shaders/flat/skin.js +114 -0
  91. package/dist/render/shaders/generated/depth.wgsl.d.ts +38 -16
  92. package/dist/render/shaders/generated/depth.wgsl.js +38 -16
  93. package/dist/render/shaders/generated/flat.wgsl.d.ts +1790 -1704
  94. package/dist/render/shaders/generated/flat.wgsl.js +1828 -1742
  95. package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +48 -0
  96. package/dist/render/shaders/generated/skinBlur.wgsl.js +54 -0
  97. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  98. package/dist/render/shaders/recon/motion.wgsl.js +102 -0
  99. package/dist/render/shaders/skinBlur.d.ts +1 -0
  100. package/dist/render/shaders/skinBlur.js +103 -0
  101. package/dist/render/shaders/skinning.d.ts +3 -1
  102. package/dist/render/shaders/skinning.js +31 -4
  103. package/dist/render/shadowCasters.d.ts +13 -1
  104. package/dist/render/skinBlur.d.ts +41 -0
  105. package/dist/render/skinBlur.js +49 -0
  106. package/dist/render/skinScatter.d.ts +67 -0
  107. package/dist/render/skinScatter.js +80 -0
  108. package/dist/render/skinnedClothRun.d.ts +26 -0
  109. package/dist/render/skinnedClothRun.js +84 -0
  110. package/dist/render/surfaceModel.d.ts +129 -0
  111. package/dist/render/surfaceModel.js +152 -0
  112. package/dist/render/surfaceTexture.d.ts +44 -4
  113. package/dist/render/surfaceTexture.js +60 -46
  114. package/dist/render/vertexDefaults.js +7 -0
  115. package/package.json +6 -6
  116. package/src/build/shaderComments.ts +0 -0
  117. package/src/geometry/meshConcat.ts +2 -0
  118. package/src/index.ts +40 -1
  119. package/src/math/scalar.ts +1 -1
  120. package/src/render/anisotropicLobe.ts +41 -0
  121. package/src/render/backend/api.ts +52 -3
  122. package/src/render/backend/select.ts +8 -0
  123. package/src/render/backend/webgl2/clothTextures.ts +120 -0
  124. package/src/render/backend/webgl2/renderer.ts +693 -64
  125. package/src/render/backend/webgl2/skinScatter.ts +309 -0
  126. package/src/render/backend/webgpu/buffers.ts +137 -63
  127. package/src/render/backend/webgpu/clothSolvePack.ts +161 -0
  128. package/src/render/backend/webgpu/clothTextures.ts +194 -0
  129. package/src/render/backend/webgpu/compressedUpload.ts +65 -0
  130. package/src/render/backend/webgpu/computeHost.ts +25 -0
  131. package/src/render/backend/webgpu/depthPass.ts +39 -7
  132. package/src/render/backend/webgpu/drawCommand.ts +6 -2
  133. package/src/render/backend/webgpu/flatPass.ts +145 -7
  134. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +502 -0
  135. package/src/render/backend/webgpu/motionPass.ts +128 -9
  136. package/src/render/backend/webgpu/postPass.ts +5 -1
  137. package/src/render/backend/webgpu/renderer.ts +663 -43
  138. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +470 -0
  139. package/src/render/backend/webgpu/skinScatterPass.ts +309 -0
  140. package/src/render/backend/webgpu/surfaceTexturePass.ts +42 -12
  141. package/src/render/clothBindingData.ts +89 -0
  142. package/src/render/clothPlacement.ts +114 -0
  143. package/src/render/compressedSource.ts +198 -0
  144. package/src/render/cutoutCaster.ts +5 -0
  145. package/src/render/cutoutDither.ts +99 -0
  146. package/src/render/eyeRefraction.ts +72 -0
  147. package/src/render/glCompressed.ts +86 -0
  148. package/src/render/hairLobes.ts +188 -0
  149. package/src/render/lightBudget.ts +25 -0
  150. package/src/render/mesh.ts +23 -0
  151. package/src/render/recon/motionVectors.ts +6 -3
  152. package/src/render/renderQuality.ts +28 -0
  153. package/src/render/rendererHarness.ts +4 -0
  154. package/src/render/sceneTarget.ts +21 -0
  155. package/src/render/shaders/clothBinding.ts +81 -0
  156. package/src/render/shaders/depth.ts +42 -6
  157. package/src/render/shaders/flat/anisotropic.ts +68 -0
  158. package/src/render/shaders/flat/eye.ts +82 -0
  159. package/src/render/shaders/flat/hair.ts +102 -0
  160. package/src/render/shaders/flat/index.ts +36 -2
  161. package/src/render/shaders/flat/main.ts +113 -17
  162. package/src/render/shaders/flat/models.ts +286 -0
  163. package/src/render/shaders/flat/preamble.ts +26 -24
  164. package/src/render/shaders/flat/skin.ts +116 -0
  165. package/src/render/shaders/generated/depth.wgsl.ts +38 -16
  166. package/src/render/shaders/generated/flat.wgsl.ts +1828 -1742
  167. package/src/render/shaders/generated/skinBlur.wgsl.ts +56 -0
  168. package/src/render/shaders/recon/motion.wgsl.ts +102 -0
  169. package/src/render/shaders/skinBlur.ts +105 -0
  170. package/src/render/shaders/skinning.ts +31 -4
  171. package/src/render/shadowCasters.ts +13 -0
  172. package/src/render/skinBlur.ts +53 -0
  173. package/src/render/skinScatter.ts +92 -0
  174. package/src/render/skinnedClothRun.ts +128 -0
  175. package/src/render/surfaceModel.ts +274 -0
  176. package/src/render/surfaceTexture.ts +123 -63
  177. package/src/render/vertexDefaults.ts +7 -0
  178. package/dist/math/exact.d.ts +0 -76
  179. package/dist/math/exact.js +0 -327
  180. package/src/math/exact.ts +0 -340
@@ -0,0 +1,50 @@
1
+ import type { SceneTarget } from '../../sceneTarget.ts';
2
+ export declare class GlSkinScatter {
3
+ private readonly gl;
4
+ private readonly program;
5
+ private readonly uniforms;
6
+ private readonly vao;
7
+ /** Each profile's scatter distance per channel, in metres: four floats a profile. */
8
+ private readonly profiles;
9
+ private readonly step;
10
+ private readonly depthToViewZ;
11
+ private diffuse;
12
+ private across;
13
+ private multisampled;
14
+ private drawFramebuffer;
15
+ private resolveFramebuffer;
16
+ private acrossFramebuffer;
17
+ private width;
18
+ private height;
19
+ private samples;
20
+ private sharedDepth;
21
+ /** Refused once and for good: a driver that will not render to half floats. */
22
+ private unavailable;
23
+ private cleared;
24
+ private drawn;
25
+ constructor(gl: WebGL2RenderingContext);
26
+ /** Whether any skin's diffuse was drawn this frame. */
27
+ get pending(): boolean;
28
+ /** What one profile scatters, per channel, in metres: the last material to name it says. */
29
+ setProfile(index: number, r: number, g: number, b: number): void;
30
+ /**
31
+ * Whether a skin can be drawn in two halves now: the target fits the frame and shares its depth.
32
+ * False where this driver will not render to half floats, and then skin is drawn whole.
33
+ */
34
+ ready(scene: SceneTarget): boolean;
35
+ /** Bind the target for one skin's diffuse half, cleared the first time a frame does. */
36
+ begin(): void;
37
+ /**
38
+ * Across into the second target, then down and added into the frame, which `scene` binds. `depth`
39
+ * is the frame's depth as a single-sampled texture, or null where none could be had — and then
40
+ * the diffuse goes back unspread rather than not at all, so the skin keeps its light.
41
+ */
42
+ spread(scene: SceneTarget, depth: WebGLTexture | null, empty: WebGLTexture, projection: ArrayLike<number>, inverse: ArrayLike<number>): void;
43
+ /** Spent at the frame's start: what was drawn into the target was last frame's. */
44
+ reset(): void;
45
+ dispose(): void;
46
+ private allocate;
47
+ private refuse;
48
+ private halfFloatTexture;
49
+ private release;
50
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Skin's screen-space scattering on WebGL2: the target skin's diffuse half is drawn into, and the
3
+ * two passes that spread it. `skinBlur.ts` is what the blur computes and why; the WebGPU backend's
4
+ * `skinScatterPass.ts` is the same thing in that backend's terms.
5
+ *
6
+ * **Drawn as it goes, not kept**: this backend issues a draw when it is asked for, and the state a
7
+ * skin draw reads — its joint palette, its uniforms — is rewritten by the draw after it. So the
8
+ * renderer draws each skin twice in place, the frame's half and then at once its diffuse half into
9
+ * this target, which shares the frame's depth and tests for equality against it.
10
+ *
11
+ * **The units it reads are its own** — `SKIN_BLUR_TEXTURE_UNIT` and the one after — past every
12
+ * unit a lit program binds, because it runs in the middle of a frame and the draws after it must
13
+ * find their textures where they left them.
14
+ */
15
+ import { SKIN_BLUR_DEPTH_UNIT, SKIN_BLUR_TEXTURE_UNIT } from '../../lightBudget.js';
16
+ import { blendKeeping } from '../../sceneCoverage.js';
17
+ import { compileProgram, uniformLocations } from '../../shader.js';
18
+ import { FULLSCREEN_VERT } from '../../shaders/fullscreen.js';
19
+ import { SKIN_BLUR_FRAG } from '../../shaders/skinBlur.js';
20
+ import { SKIN_PROFILES } from '../../skinBlur.js';
21
+ const CLEAR = new Float32Array(4);
22
+ export class GlSkinScatter {
23
+ gl;
24
+ program;
25
+ uniforms;
26
+ vao;
27
+ /** Each profile's scatter distance per channel, in metres: four floats a profile. */
28
+ profiles = new Float32Array(SKIN_PROFILES * 4);
29
+ step = new Float32Array(2);
30
+ depthToViewZ = new Float32Array(4);
31
+ diffuse = null;
32
+ across = null;
33
+ multisampled = null;
34
+ drawFramebuffer = null;
35
+ resolveFramebuffer = null;
36
+ acrossFramebuffer = null;
37
+ width = 0;
38
+ height = 0;
39
+ samples = 1;
40
+ sharedDepth = null;
41
+ /** Refused once and for good: a driver that will not render to half floats. */
42
+ unavailable = false;
43
+ cleared = false;
44
+ drawn = false;
45
+ constructor(gl) {
46
+ this.gl = gl;
47
+ this.program = compileProgram(gl, FULLSCREEN_VERT, SKIN_BLUR_FRAG, 'skinBlur');
48
+ this.uniforms = uniformLocations(gl, this.program, 'skinBlur');
49
+ const vao = gl.createVertexArray();
50
+ if (vao === null)
51
+ throw new Error('GlSkinScatter: createVertexArray failed');
52
+ this.vao = vao;
53
+ /*
54
+ * Rendering to half floats is the extension's, not WebGL2's — the scene target asks for it
55
+ * only when it keeps range, so this asks for itself. Without it, skin is drawn whole.
56
+ */
57
+ if (gl.getExtension('EXT_color_buffer_float') === null)
58
+ this.refuse();
59
+ }
60
+ /** Whether any skin's diffuse was drawn this frame. */
61
+ get pending() {
62
+ return this.drawn;
63
+ }
64
+ /** What one profile scatters, per channel, in metres: the last material to name it says. */
65
+ setProfile(index, r, g, b) {
66
+ const at = Math.min(Math.max(Math.round(index), 0), SKIN_PROFILES - 1) * 4;
67
+ this.profiles[at] = r;
68
+ this.profiles[at + 1] = g;
69
+ this.profiles[at + 2] = b;
70
+ }
71
+ /**
72
+ * Whether a skin can be drawn in two halves now: the target fits the frame and shares its depth.
73
+ * False where this driver will not render to half floats, and then skin is drawn whole.
74
+ */
75
+ ready(scene) {
76
+ if (this.unavailable)
77
+ return false;
78
+ const depth = scene.sharedDepth();
79
+ const attached = depth.renderbuffer ?? depth.texture;
80
+ if (attached === null)
81
+ return false;
82
+ if (this.drawFramebuffer !== null &&
83
+ this.width === depth.width &&
84
+ this.height === depth.height &&
85
+ this.samples === depth.samples &&
86
+ this.sharedDepth === attached) {
87
+ return true;
88
+ }
89
+ const made = this.allocate(depth.width, depth.height, depth.samples, depth.texture, depth.renderbuffer);
90
+ /* Asked in the middle of a frame: the frame's own framebuffer goes back as it was. */
91
+ scene.bind();
92
+ return made;
93
+ }
94
+ /** Bind the target for one skin's diffuse half, cleared the first time a frame does. */
95
+ begin() {
96
+ const { gl } = this;
97
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.drawFramebuffer);
98
+ gl.viewport(0, 0, this.width, this.height);
99
+ if (!this.cleared) {
100
+ gl.clearBufferfv(gl.COLOR, 0, CLEAR);
101
+ this.cleared = true;
102
+ }
103
+ this.drawn = true;
104
+ }
105
+ /**
106
+ * Across into the second target, then down and added into the frame, which `scene` binds. `depth`
107
+ * is the frame's depth as a single-sampled texture, or null where none could be had — and then
108
+ * the diffuse goes back unspread rather than not at all, so the skin keeps its light.
109
+ */
110
+ spread(scene, depth, empty, projection, inverse) {
111
+ const { gl } = this;
112
+ if (this.multisampled !== null) {
113
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.drawFramebuffer);
114
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.resolveFramebuffer);
115
+ gl.blitFramebuffer(0, 0, this.width, this.height, 0, 0, this.width, this.height, gl.COLOR_BUFFER_BIT, gl.NEAREST);
116
+ }
117
+ gl.disable(gl.DEPTH_TEST);
118
+ gl.disable(gl.CULL_FACE);
119
+ gl.disable(gl.BLEND);
120
+ gl.depthMask(false);
121
+ gl.bindVertexArray(this.vao);
122
+ gl.useProgram(this.program);
123
+ const u = this.uniforms;
124
+ gl.uniform1i(u['uSkin'] ?? null, SKIN_BLUR_TEXTURE_UNIT);
125
+ gl.uniform1i(u['uDepth'] ?? null, SKIN_BLUR_DEPTH_UNIT);
126
+ /* The four terms that carry a depth back to view-space metres: the occlusion blur's own. */
127
+ this.depthToViewZ[0] = inverse[10] ?? 0;
128
+ this.depthToViewZ[1] = inverse[14] ?? 0;
129
+ this.depthToViewZ[2] = inverse[11] ?? 0;
130
+ this.depthToViewZ[3] = inverse[15] ?? 1;
131
+ gl.uniform4fv(u['uDepthToViewZ'] ?? null, this.depthToViewZ);
132
+ /* Pixels a metre spans at a metre; nothing to measure against without a depth, so nothing spreads. */
133
+ gl.uniform1f(u['uFocal'] ?? null, depth === null ? 0 : (projection[5] * this.height) / 2);
134
+ gl.uniform4fv(u['uProfiles'] ?? null, this.profiles);
135
+ gl.activeTexture(gl.TEXTURE0 + SKIN_BLUR_DEPTH_UNIT);
136
+ gl.bindTexture(gl.TEXTURE_2D, depth ?? empty);
137
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.acrossFramebuffer);
138
+ gl.viewport(0, 0, this.width, this.height);
139
+ gl.activeTexture(gl.TEXTURE0 + SKIN_BLUR_TEXTURE_UNIT);
140
+ gl.bindTexture(gl.TEXTURE_2D, this.diffuse);
141
+ this.step[0] = 1 / this.width;
142
+ this.step[1] = 0;
143
+ gl.uniform2fv(u['uStep'] ?? null, this.step);
144
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
145
+ scene.bind();
146
+ gl.bindTexture(gl.TEXTURE_2D, this.across);
147
+ this.step[0] = 0;
148
+ this.step[1] = 1 / this.height;
149
+ gl.uniform2fv(u['uStep'] ?? null, this.step);
150
+ /* Added: its colour onto the frame's, the frame's alpha kept. */
151
+ gl.enable(gl.BLEND);
152
+ gl.blendEquation(gl.FUNC_ADD);
153
+ blendKeeping(gl, gl.ONE, gl.ONE);
154
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
155
+ /* The world pass's own state, as the next draw expects to find it. */
156
+ gl.disable(gl.BLEND);
157
+ gl.enable(gl.DEPTH_TEST);
158
+ gl.enable(gl.CULL_FACE);
159
+ gl.depthMask(true);
160
+ gl.bindVertexArray(null);
161
+ }
162
+ /** Spent at the frame's start: what was drawn into the target was last frame's. */
163
+ reset() {
164
+ this.cleared = false;
165
+ this.drawn = false;
166
+ }
167
+ dispose() {
168
+ const { gl } = this;
169
+ this.release();
170
+ gl.deleteProgram(this.program);
171
+ gl.deleteVertexArray(this.vao);
172
+ }
173
+ allocate(width, height, samples, depthTexture, depthRenderbuffer) {
174
+ const { gl } = this;
175
+ this.release();
176
+ this.width = width;
177
+ this.height = height;
178
+ this.samples = samples;
179
+ /* Its own unit, so making its textures unbinds nothing a lit program reads. */
180
+ gl.activeTexture(gl.TEXTURE0 + SKIN_BLUR_TEXTURE_UNIT);
181
+ this.sharedDepth = depthRenderbuffer ?? depthTexture;
182
+ this.diffuse = this.halfFloatTexture(width, height);
183
+ this.across = this.halfFloatTexture(width, height);
184
+ this.drawFramebuffer = gl.createFramebuffer();
185
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.drawFramebuffer);
186
+ if (samples > 1 && depthRenderbuffer !== null) {
187
+ this.multisampled = gl.createRenderbuffer();
188
+ gl.bindRenderbuffer(gl.RENDERBUFFER, this.multisampled);
189
+ gl.renderbufferStorageMultisample(gl.RENDERBUFFER, samples, gl.RGBA16F, width, height);
190
+ gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, this.multisampled);
191
+ gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthRenderbuffer);
192
+ }
193
+ else {
194
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.diffuse, 0);
195
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTexture, 0);
196
+ }
197
+ const drawReady = gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE;
198
+ if (this.multisampled !== null) {
199
+ this.resolveFramebuffer = gl.createFramebuffer();
200
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.resolveFramebuffer);
201
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.diffuse, 0);
202
+ }
203
+ this.acrossFramebuffer = gl.createFramebuffer();
204
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.acrossFramebuffer);
205
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.across, 0);
206
+ const acrossReady = gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE;
207
+ gl.bindRenderbuffer(gl.RENDERBUFFER, null);
208
+ if (drawReady && acrossReady)
209
+ return true;
210
+ this.release();
211
+ this.refuse();
212
+ return false;
213
+ }
214
+ refuse() {
215
+ if (this.unavailable)
216
+ return;
217
+ this.unavailable = true;
218
+ console.warn("WebGL2: this driver will not render skin's diffuse to half floats, so skinScattering " +
219
+ "'screen-space' draws skin pre-integrated. The frame is otherwise unaffected.");
220
+ }
221
+ halfFloatTexture(width, height) {
222
+ const { gl } = this;
223
+ const texture = gl.createTexture();
224
+ gl.bindTexture(gl.TEXTURE_2D, texture);
225
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, width, height, 0, gl.RGBA, gl.HALF_FLOAT, null);
226
+ /* Linear: the blur takes taps between texels, and reads a part-covered one for the part it is. */
227
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
228
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
229
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
230
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
231
+ gl.bindTexture(gl.TEXTURE_2D, null);
232
+ return texture;
233
+ }
234
+ release() {
235
+ const { gl } = this;
236
+ if (this.drawFramebuffer !== null)
237
+ gl.deleteFramebuffer(this.drawFramebuffer);
238
+ if (this.resolveFramebuffer !== null)
239
+ gl.deleteFramebuffer(this.resolveFramebuffer);
240
+ if (this.acrossFramebuffer !== null)
241
+ gl.deleteFramebuffer(this.acrossFramebuffer);
242
+ if (this.multisampled !== null)
243
+ gl.deleteRenderbuffer(this.multisampled);
244
+ if (this.diffuse !== null)
245
+ gl.deleteTexture(this.diffuse);
246
+ if (this.across !== null)
247
+ gl.deleteTexture(this.across);
248
+ this.drawFramebuffer = null;
249
+ this.resolveFramebuffer = null;
250
+ this.acrossFramebuffer = null;
251
+ this.multisampled = null;
252
+ this.diffuse = null;
253
+ this.across = null;
254
+ this.sharedDepth = null;
255
+ }
256
+ }
@@ -42,14 +42,16 @@ export interface GpuMesh {
42
42
  *
43
43
  * **Two, and the first is interleaved** — every attribute this mesh actually supplied, woven
44
44
  * into one stride — while the second holds one value per *absent* optional attribute at a stride
45
- * of zero. It is emphatically not one buffer per attribute, which this comment said until
45
+ * of zero. **A dynamic mesh has four**: its positions and its normals, each a buffer of its own so
46
+ * `update` writes them alone, then the interleaved rest and the constants. It is emphatically not one buffer per attribute, which this comment said until
46
47
  * 2026-09-17 and which cost the motion pass an afternoon: a pipeline built to that description
47
48
  * reads a position out of every twelve bytes of an interleaved vertex, draws geometry that is
48
49
  * not the mesh, and fails no validation at all.
49
50
  */
50
51
  readonly vertexBuffers: readonly GPUBuffer[];
51
52
  /**
52
- * Bytes a vertex takes in the first of those buffers.
53
+ * Bytes a vertex takes in the first of those buffers: the interleaved stride, or twelve for a
54
+ * dynamic mesh, whose first buffer is its positions.
53
55
  *
54
56
  * Kept because it is a function of which *optional* attributes this mesh supplied, so it differs
55
57
  * between two meshes drawn by the same pipeline — and a pass that reads only the position still
@@ -62,10 +64,17 @@ export interface GpuMesh {
62
64
  * Byte offsets of the joint indices and weights in the interleaved vertex, or null for a mesh
63
65
  * that supplied neither. For a pass that skins through a shader of its own — the motion pass —
64
66
  * which has to find them without the `present` map the generated pipelines are keyed by.
67
+ *
68
+ * **Null on a dynamic mesh too**, whose first buffer holds positions alone: the motion pass reads
69
+ * joints from that buffer, so a dynamic skinned mesh's motion between rewrites is its model's
70
+ * rather than its pose's. A rewrite carries its own motion, from last frame's positions.
65
71
  */
66
72
  readonly skinOffsets: {
67
73
  readonly joints: number;
68
74
  readonly weights: number;
75
+ /** The second four influences' offsets, or -1 for a mesh with four. */
76
+ readonly joints2: number;
77
+ readonly weights2: number;
69
78
  } | null;
70
79
  readonly indexBuffer: GPUBuffer;
71
80
  /**
@@ -114,6 +123,10 @@ export interface GpuMesh {
114
123
  * the key is built before the present map is consulted.
115
124
  */
116
125
  readonly isSkinned: boolean;
126
+ /** Whether its rig moves a vertex by eight influences, which keys a pipeline with `SKIN_EIGHT`. */
127
+ readonly isSkinnedEight: boolean;
128
+ /** How many vertices it has, which a cloth binding is checked against. */
129
+ readonly vertexCount: number;
117
130
  /** Whether it has texture coordinates, without which a material's maps read one texel. */
118
131
  readonly hasUvs: boolean;
119
132
  /**
@@ -135,18 +148,19 @@ export interface GpuMesh {
135
148
  /**
136
149
  * Rewrite this mesh's positions, and its normals where the caller has them.
137
150
  *
138
- * Absent unless the mesh was created `dynamic`. **This backend interleaves every attribute into
139
- * one buffer**, so a position is not contiguous with the next one — writing them individually
140
- * would be one `writeBuffer` per vertex. So a dynamic mesh keeps the interleaved array it was
141
- * built from, patches the two fields that move, and uploads the lot in one call. The cost is
142
- * one CPU-side copy of the vertex data per dynamic mesh, which is why the flag exists rather
143
- * than every mesh paying it.
151
+ * Absent unless the mesh was created `dynamic`. **A dynamic mesh's positions and normals are
152
+ * buffers of their own**, so this is one `writeBuffer` of the caller's positions and one of its
153
+ * normals, and nothing else moves. Until 4.8.4 every attribute was interleaved into one buffer,
154
+ * and a dynamic mesh kept a CPU copy of it to patch and upload whole — a garment's colours,
155
+ * coordinates, tangents and joints re-sent every frame for the twenty-four bytes a vertex that
156
+ * changed, reported at 2.8 MB a frame for one character. What the split costs is a pipeline per
157
+ * layout, keyed by `:dynamic`, and two more buffers bound per draw of such a mesh.
144
158
  */
145
159
  readonly update: ((device: GPUDevice, positions: Float32Array, normals?: Float32Array, previous?: GPUBuffer | null) => void) | null;
146
160
  /**
147
- * A dynamic mesh's motion state, which the renderer owns: a buffer for last frame's interleaved
148
- * rows, made the first time a reconstructing renderer rewrites the mesh, and the frame of the
149
- * last rewrite. Null on a mesh created without `{ dynamic: true }`, which never moves a vertex.
161
+ * A dynamic mesh's motion state, which the renderer owns: a buffer for last frame's positions,
162
+ * made the first time a reconstructing renderer rewrites the mesh, and the frame of the last
163
+ * rewrite. Null on a mesh created without `{ dynamic: true }`, which never moves a vertex.
150
164
  */
151
165
  readonly motion: {
152
166
  previous: GPUBuffer | null;
@@ -20,6 +20,7 @@ import { INSTANCE_STRIDE } from '../../instances.js';
20
20
  * without one.
21
21
  */
22
22
  const USAGE = {
23
+ COPY_SRC: 0x0004,
23
24
  COPY_DST: 0x0008,
24
25
  INDEX: 0x0010,
25
26
  VERTEX: 0x0020,
@@ -83,6 +84,13 @@ export const VERTEX_LAYOUT = [
83
84
  * three lanes and a spare live here instead of three attributes that do not fit.
84
85
  */
85
86
  { name: 'channel', shaderLocation: 13, format: 'float32x4', components: 4, optional: true },
87
+ /*
88
+ * The fifth to eighth influences, at the last two locations. Only a skinned variant reads them,
89
+ * and only with `SKIN_EIGHT` on; every other mesh is fed zeros by the constants buffer. An
90
+ * instanced pipeline reclaims 14 and 15 with the rest of 11 to 15, as `INSTANCE_LOCATIONS` says.
91
+ */
92
+ { name: 'joints2', shaderLocation: 14, format: 'float32x4', components: 4, optional: true },
93
+ { name: 'weights2', shaderLocation: 15, format: 'float32x4', components: 4, optional: true },
86
94
  ];
87
95
  /** Geometry on the device, with the buffers bound in layout order. */
88
96
  /**
@@ -152,8 +160,10 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
152
160
  const bounds = boundsOfPositions(data.positions, createBounds());
153
161
  const vertexCount = data.positions.length / 3;
154
162
  const layered = data.layers !== undefined;
155
- const supplied = VERTEX_LAYOUT.filter((attribute) => !attribute.optional || data[attribute.name] !== undefined).map((attribute) => laidOut(attribute, layered));
156
- /* Interleaved: one stride holding every attribute the mesh actually supplied. */
163
+ const supplied = VERTEX_LAYOUT.filter((attribute) => (!attribute.optional || data[attribute.name] !== undefined) &&
164
+ !(dynamic && isStreamed(attribute))).map((attribute) => laidOut(attribute, layered));
165
+ /* Interleaved: one stride holding every attribute the mesh actually supplied — less a dynamic
166
+ mesh's positions and normals, which are buffers of their own. */
157
167
  const stride = supplied.reduce((sum, attribute) => sum + attribute.components * 4, 0);
158
168
  const step = stride / 4;
159
169
  const interleaved = new Float32Array(step * vertexCount);
@@ -212,27 +222,27 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
212
222
  size: align4(data.indices.byteLength),
213
223
  usage: USAGE.INDEX | USAGE.COPY_DST,
214
224
  });
215
- /*
216
- * Where the position and normal fields sit inside one vertex, in floats. Found from the same
217
- * `supplied` list the interleave walks, so the two cannot disagree — reading them off
218
- * `VERTEX_LAYOUT` instead would be right until a mesh omits an optional attribute before them,
219
- * which is the bug `present` already records once in this file.
220
- */
221
- let positionField = -1;
222
- let normalField = -1;
223
- let walked = 0;
224
- for (const attribute of supplied) {
225
- if (attribute.name === 'positions')
226
- positionField = walked;
227
- if (attribute.name === 'normals')
228
- normalField = walked;
229
- walked += attribute.components;
230
- }
225
+ /* A dynamic mesh's two moving streams: copied from as well as written, for last frame's positions. */
226
+ const streamBytes = align4(vertexCount * 12);
227
+ const streams = dynamic
228
+ ? {
229
+ positions: device.createBuffer({
230
+ label: 'mesh.positions',
231
+ size: streamBytes,
232
+ usage: USAGE.VERTEX | USAGE.COPY_DST | USAGE.COPY_SRC,
233
+ }),
234
+ normals: device.createBuffer({
235
+ label: 'mesh.normals',
236
+ size: streamBytes,
237
+ usage: USAGE.VERTEX | USAGE.COPY_DST,
238
+ }),
239
+ }
240
+ : null;
231
241
  /*
232
242
  * The chunks, in whole vertices and whole indices: a step never writes half of either, so the
233
243
  * buffer never holds a torn one even for the frame between two steps.
234
244
  */
235
- const verticesPerStep = Math.max(1, Math.floor(UPLOAD_BYTES_PER_STEP / stride));
245
+ const verticesPerStep = Math.max(1, Math.floor(UPLOAD_BYTES_PER_STEP / (stride + (dynamic ? 24 : 0))));
236
246
  const indexWidth = data.indices.BYTES_PER_ELEMENT;
237
247
  const indicesPerStep = Math.max(1, Math.floor(UPLOAD_BYTES_PER_STEP / indexWidth));
238
248
  const totalSteps = Math.max(1, Math.ceil(vertexCount / verticesPerStep)) +
@@ -250,28 +260,32 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
250
260
  const mesh = gpuMeshOf({
251
261
  vertices,
252
262
  constants,
263
+ streams,
253
264
  indexBuffer,
254
- stride,
265
+ stride: streams === null ? stride : 12,
255
266
  indexCount: data.indices.length,
256
267
  bounds,
257
268
  progress,
258
269
  hasTangents: data.tangents !== undefined,
259
270
  isSkinned: data.joints !== undefined,
271
+ isSkinnedEight: data.joints !== undefined && data.joints2 !== undefined,
272
+ vertexCount,
260
273
  hasUvs: data.uvs !== undefined,
261
- skinOffsets: skinOffsetsOf(supplied),
274
+ skinOffsets: streams === null ? skinOffsetsOf(supplied) : null,
262
275
  hasChannel: data.channel !== undefined,
263
276
  morph: data.morphTargets === undefined || data.morphTargetCount === undefined
264
277
  ? null
265
278
  : new MorphTexture(device, data.morphTargets, data.positions.length / 3, data.morphTargetCount),
266
- update: !dynamic
279
+ update: streams === null
267
280
  ? null
268
- : dynamicUpdate(interleaved, vertices, bounds, vertexCount, step, positionField, normalField),
281
+ : streamUpdate(streams.positions, streams.normals, streamBytes, bounds, vertexCount),
269
282
  });
270
283
  const job = {
271
284
  data,
272
285
  interleaved,
273
286
  device,
274
287
  supplied,
288
+ streams,
275
289
  vertices,
276
290
  indexBuffer,
277
291
  vertexCount,
@@ -305,6 +319,11 @@ function* uploadSteps(job) {
305
319
  fieldOffset += attribute.components;
306
320
  }
307
321
  device.queue.writeBuffer(vertices, from * stride, interleaved, from * step, (to - from) * step);
322
+ if (job.streams !== null) {
323
+ const count = (to - from) * 3;
324
+ device.queue.writeBuffer(job.streams.positions, from * 12, data.positions, from * 3, count);
325
+ device.queue.writeBuffer(job.streams.normals, from * 12, data.normals, from * 3, count);
326
+ }
308
327
  taken += 1;
309
328
  if (taken < job.totalSteps)
310
329
  yield;
@@ -368,21 +387,33 @@ function skinOffsetsOf(supplied) {
368
387
  let floats = 0;
369
388
  let joints = -1;
370
389
  let weights = -1;
390
+ let joints2 = -1;
391
+ let weights2 = -1;
371
392
  for (const attribute of supplied) {
372
393
  if (attribute.name === 'joints')
373
394
  joints = floats * 4;
374
395
  if (attribute.name === 'weights')
375
396
  weights = floats * 4;
397
+ if (attribute.name === 'joints2')
398
+ joints2 = floats * 4;
399
+ if (attribute.name === 'weights2')
400
+ weights2 = floats * 4;
376
401
  floats += attribute.components;
377
402
  }
378
- return joints >= 0 && weights >= 0 ? { joints, weights } : null;
403
+ if (joints < 0 || weights < 0)
404
+ return null;
405
+ return joints2 >= 0 && weights2 >= 0
406
+ ? { joints, weights, joints2, weights2 }
407
+ : { joints, weights, joints2: -1, weights2: -1 };
379
408
  }
380
409
  /** The handle, made where it can see only what it hands out. */
381
410
  function gpuMeshOf(parts) {
382
411
  const motion = parts.update === null ? null : { previous: null, changed: -1 };
383
- const { vertices, constants, indexBuffer, progress } = parts;
412
+ const { vertices, constants, streams, indexBuffer, progress } = parts;
384
413
  return {
385
- vertexBuffers: [vertices, constants],
414
+ vertexBuffers: streams === null
415
+ ? [vertices, constants]
416
+ : [streams.positions, streams.normals, vertices, constants],
386
417
  vertexStride: parts.stride,
387
418
  skinOffsets: parts.skinOffsets,
388
419
  indexBuffer,
@@ -393,6 +424,8 @@ function gpuMeshOf(parts) {
393
424
  },
394
425
  hasTangents: parts.hasTangents,
395
426
  isSkinned: parts.isSkinned,
427
+ isSkinnedEight: parts.isSkinnedEight,
428
+ vertexCount: parts.vertexCount,
396
429
  hasUvs: parts.hasUvs,
397
430
  hasChannel: parts.hasChannel,
398
431
  morph: parts.morph,
@@ -400,43 +433,47 @@ function gpuMeshOf(parts) {
400
433
  motion,
401
434
  dispose() {
402
435
  motion?.previous?.destroy();
436
+ streams?.positions.destroy();
437
+ streams?.normals.destroy();
403
438
  vertices.destroy();
404
439
  constants.destroy();
405
440
  indexBuffer.destroy();
406
441
  },
407
442
  };
408
443
  }
444
+ /** Whether an attribute is one a dynamic mesh keeps in a buffer of its own. */
445
+ function isStreamed(attribute) {
446
+ return attribute.name === 'positions' || attribute.name === 'normals';
447
+ }
409
448
  /**
410
- * A dynamic mesh's `update`, which keeps the interleaved rows and nothing else: it rewrites the
411
- * positions and normals in them and uploads the whole buffer, so the other attributes it does not
412
- * receive have to still be there.
449
+ * A dynamic mesh's `update`: the caller's positions, and its normals where it has them, each
450
+ * written to its own buffer as it came. Last frame's positions, where a reconstruction wants them,
451
+ * are copied on the device before the write — a copy submitted ahead of a `writeBuffer` runs ahead
452
+ * of it on the queue — so no CPU copy of anything is kept.
413
453
  */
414
- function dynamicUpdate(interleaved, vertices, bounds, vertexCount, step, positionField, normalField) {
454
+ function streamUpdate(positionsBuffer, normalsBuffer, streamBytes, bounds, vertexCount) {
415
455
  return (target, positions, normals, previous = null) => {
416
- if (positions.length !== vertexCount * 3) {
417
- throw new Error(`this mesh has ${vertexCount} vertices and the update has ${positions.length / 3}. ` +
456
+ for (const [name, values] of [
457
+ ['update', positions],
458
+ ['normals', normals],
459
+ ]) {
460
+ if (values === undefined || values.length === vertexCount * 3)
461
+ continue;
462
+ throw new Error(`this mesh has ${vertexCount} vertices and the ${name} has ${values.length / 3}. ` +
418
463
  "A mesh's vertex count is fixed at creation: the index buffer, the pipeline and " +
419
464
  'every other attribute are sized against it.');
420
465
  }
421
- /* The rows as they stand are last frame's, handed over before a position of them changes.
422
- `writeBuffer` copies at the call, so patching the array afterwards cannot reach this copy. */
423
- if (previous !== null)
424
- target.queue.writeBuffer(previous, 0, interleaved);
466
+ if (previous !== null) {
467
+ const encoder = target.createCommandEncoder({ label: 'mesh.previousPositions' });
468
+ encoder.copyBufferToBuffer(positionsBuffer, 0, previous, 0, streamBytes);
469
+ target.queue.submit([encoder.finish()]);
470
+ }
425
471
  /* Everything that decides whether this is on screen starts from the bounds, so a cloth
426
472
  that blew sideways out of its original box would be culled while still visible. */
427
473
  boundsOfPositions(positions, bounds);
428
- for (let vertex = 0; vertex < vertexCount; vertex++) {
429
- const at = vertex * step;
430
- interleaved[at + positionField] = positions[vertex * 3] ?? 0;
431
- interleaved[at + positionField + 1] = positions[vertex * 3 + 1] ?? 0;
432
- interleaved[at + positionField + 2] = positions[vertex * 3 + 2] ?? 0;
433
- if (normals === undefined)
434
- continue;
435
- interleaved[at + normalField] = normals[vertex * 3] ?? 0;
436
- interleaved[at + normalField + 1] = normals[vertex * 3 + 1] ?? 0;
437
- interleaved[at + normalField + 2] = normals[vertex * 3 + 2] ?? 0;
438
- }
439
- target.queue.writeBuffer(vertices, 0, interleaved);
474
+ target.queue.writeBuffer(positionsBuffer, 0, positions);
475
+ if (normals !== undefined)
476
+ target.queue.writeBuffer(normalsBuffer, 0, normals);
440
477
  };
441
478
  }
442
479
  /**
@@ -481,8 +518,20 @@ instanced = false) {
481
518
  const constants = [];
482
519
  let offset = 0;
483
520
  const layered = present['layers'] === true;
521
+ /* A dynamic mesh's positions and normals: buffers of their own, ahead of the rest. */
522
+ const dynamic = present['dynamic'] === true;
523
+ const layouts = [];
484
524
  VERTEX_LAYOUT.forEach((declared, index) => {
485
525
  const attribute = laidOut(declared, layered);
526
+ if (dynamic && isStreamed(attribute)) {
527
+ layouts.push({
528
+ arrayStride: attribute.components * 4,
529
+ attributes: [
530
+ { shaderLocation: attribute.shaderLocation, offset: 0, format: attribute.format },
531
+ ],
532
+ });
533
+ return;
534
+ }
486
535
  /*
487
536
  * **The instanced pipeline reclaims the two skinning locations, and it is allowed to because
488
537
  * it cannot skin.** Locations 11 and 12 are the joint indices and weights; the INSTANCED
@@ -530,7 +579,7 @@ instanced = false) {
530
579
  format: attribute.format,
531
580
  });
532
581
  });
533
- const layouts = [{ arrayStride: offset, attributes: interleaved }];
582
+ layouts.push({ arrayStride: offset, attributes: interleaved });
534
583
  /*
535
584
  * The constants buffer is always bound, even with nothing in it: the pipeline layout has
536
585
  * to match the buffers a draw sets, and a mesh supplying every attribute would otherwise