@driftengine/core 4.13.1 → 4.13.3

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 (78) hide show
  1. package/CHANGELOG.json +42 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +4 -1
  4. package/dist/index.js +1 -0
  5. package/dist/render/backend/api.d.ts +63 -8
  6. package/dist/render/backend/webgl2/pointSkinFeedback.d.ts +23 -0
  7. package/dist/render/backend/webgl2/pointSkinFeedback.js +159 -0
  8. package/dist/render/backend/webgl2/renderer.d.ts +33 -0
  9. package/dist/render/backend/webgl2/renderer.js +88 -4
  10. package/dist/render/backend/webgpu/buffers.d.ts +10 -2
  11. package/dist/render/backend/webgpu/buffers.js +66 -16
  12. package/dist/render/backend/webgpu/clothTextures.d.ts +6 -3
  13. package/dist/render/backend/webgpu/clothTextures.js +14 -11
  14. package/dist/render/backend/webgpu/drawCommand.js +3 -3
  15. package/dist/render/backend/webgpu/gpuClothSet.js +1 -0
  16. package/dist/render/backend/webgpu/groupShelf.d.ts +31 -0
  17. package/dist/render/backend/webgpu/groupShelf.js +54 -0
  18. package/dist/render/backend/webgpu/pointSkinPass.d.ts +27 -0
  19. package/dist/render/backend/webgpu/pointSkinPass.js +104 -0
  20. package/dist/render/backend/webgpu/renderer.d.ts +40 -7
  21. package/dist/render/backend/webgpu/renderer.js +151 -48
  22. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +1 -1
  23. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +7 -2
  24. package/dist/render/backend/webgpu/shaders/pointSkin.wgsl.d.ts +13 -0
  25. package/dist/render/backend/webgpu/shaders/pointSkin.wgsl.js +57 -0
  26. package/dist/render/backend/webgpu/vertexFormats.js +1 -1
  27. package/dist/render/lightBudget.d.ts +6 -0
  28. package/dist/render/lightBudget.js +6 -0
  29. package/dist/render/mesh.d.ts +19 -5
  30. package/dist/render/mesh.js +39 -8
  31. package/dist/render/pointSkin.d.ts +55 -0
  32. package/dist/render/pointSkin.js +121 -0
  33. package/dist/render/rendererHarness.js +6 -0
  34. package/dist/render/shader.d.ts +3 -1
  35. package/dist/render/shader.js +6 -1
  36. package/dist/render/shaders/flat/detail.d.ts +21 -0
  37. package/dist/render/shaders/flat/detail.js +74 -0
  38. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  39. package/dist/render/shaders/flat/glassTint.js +1 -0
  40. package/dist/render/shaders/flat/index.d.ts +5 -0
  41. package/dist/render/shaders/flat/index.js +3 -0
  42. package/dist/render/shaders/flat/main.d.ts +1 -1
  43. package/dist/render/shaders/flat/main.js +2 -0
  44. package/dist/render/shaders/generated/flat.wgsl.d.ts +144 -16
  45. package/dist/render/shaders/generated/flat.wgsl.js +183 -55
  46. package/dist/render/surfaceDetail.d.ts +79 -0
  47. package/dist/render/surfaceDetail.js +86 -0
  48. package/dist/render/surfaceTexture.d.ts +9 -0
  49. package/dist/render/vertexPacking.d.ts +16 -4
  50. package/dist/render/vertexPacking.js +10 -3
  51. package/package.json +6 -6
  52. package/src/index.ts +4 -0
  53. package/src/render/backend/api.ts +70 -7
  54. package/src/render/backend/webgl2/pointSkinFeedback.ts +173 -0
  55. package/src/render/backend/webgl2/renderer.ts +121 -4
  56. package/src/render/backend/webgpu/buffers.ts +90 -16
  57. package/src/render/backend/webgpu/clothTextures.ts +14 -11
  58. package/src/render/backend/webgpu/drawCommand.ts +3 -3
  59. package/src/render/backend/webgpu/gpuClothSet.ts +1 -0
  60. package/src/render/backend/webgpu/groupShelf.ts +74 -0
  61. package/src/render/backend/webgpu/pointSkinPass.ts +115 -0
  62. package/src/render/backend/webgpu/renderer.ts +192 -83
  63. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +7 -2
  64. package/src/render/backend/webgpu/shaders/pointSkin.wgsl.ts +58 -0
  65. package/src/render/backend/webgpu/vertexFormats.ts +1 -1
  66. package/src/render/lightBudget.ts +7 -0
  67. package/src/render/mesh.ts +49 -7
  68. package/src/render/pointSkin.ts +178 -0
  69. package/src/render/rendererHarness.ts +6 -0
  70. package/src/render/shader.ts +5 -0
  71. package/src/render/shaders/flat/detail.ts +74 -0
  72. package/src/render/shaders/flat/glassTint.ts +3 -1
  73. package/src/render/shaders/flat/index.ts +8 -0
  74. package/src/render/shaders/flat/main.ts +2 -0
  75. package/src/render/shaders/generated/flat.wgsl.ts +183 -55
  76. package/src/render/surfaceDetail.ts +136 -0
  77. package/src/render/surfaceTexture.ts +9 -0
  78. package/src/render/vertexPacking.ts +25 -5
@@ -60,6 +60,9 @@ import { GpuSceneCaptureTarget } from './sceneCaptureTarget.js';
60
60
  import { sceneCaptureTexels } from '../../sceneCapture.js';
61
61
  import { gpuCompressedFormats } from './compressedUpload.js';
62
62
  import { GpuClothBinding, GpuClothParticles, clothStandIn } from './clothTextures.js';
63
+ import { GroupShelf } from './groupShelf.js';
64
+ import { GpuPointSkin, createPointSkinPipeline } from './pointSkinPass.js';
65
+ import { poseBounds, validatePointSkin, validatePose, } from '../../pointSkin.js';
63
66
  import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel, skinProfileDistance, } from '../../surfaceModel.js';
64
67
  import { OVERLAY_FLOATS, overlayLays, packSurfaceOverlay, } from '../../surfaceOverlay.js';
65
68
  import { PROJECTION_FLOATS, packSurfaceProjection } from '../../surfaceProjection.js';
@@ -67,6 +70,7 @@ import { resolveTransmission } from '../../transmission.js';
67
70
  import { resolveOcclusionFade } from '../../occlusionFade.js';
68
71
  import { orthographicDepthSpan } from '../../lightMatrix.js';
69
72
  import { LAYER_FLOATS, layerMaskMap, layeredProjection, layersFitBeside, packSurfaceLayers, } from '../../surfaceLayers.js';
73
+ import { DETAIL_FLOATS, detailOf, detailRefusal, detailRefusalWarning, packSurfaceDetail, } from '../../surfaceDetail.js';
70
74
  import { LOOK_FLOATS, asksLayerLooks, packLayerLooks } from '../../surfaceLayerLooks.js';
71
75
  import { frameReflectionsRefused, resolveFrameReflections, } from '../../frameReflections.js';
72
76
  import { REFLECTION_SURFACE_FORMAT, ReflectionSurfacePass } from './reflectionSurfacePass.js';
@@ -920,7 +924,9 @@ export class WebGPURenderer {
920
924
  * thirty-odd sections, each its own material, built one a section a frame — about ninety a frame
921
925
  * for two characters, reported by a consumer. A slot's map holds one entry a material its rigs
922
926
  * are drawn in, and is emptied wherever the unskinned groups are, whose identities it is keyed
923
- * by.
927
+ * by. **And by what each group was built with, on a shelf a material** (`groupShelf.ts`): a
928
+ * garment's particles take turns between two textures and several meshes may follow one cloth,
929
+ * so one group a material was made again draw after draw.
924
930
  */
925
931
  skinnedGroups = [];
926
932
  /** The cloth the next skinned draws are placed by, or null. See `setCloth`. */
@@ -2279,6 +2285,10 @@ export class WebGPURenderer {
2279
2285
  */
2280
2286
  transmission = new Float32Array([-1, -1, -1, 0]);
2281
2287
  warnedLayersModelMap = false;
2288
+ /** `uDetail`, the material's tiled detail packed by `packSurfaceDetail`. */
2289
+ detailFloats = new Float32Array(DETAIL_FLOATS);
2290
+ /** The detail refusals already said, each once. See `detailOfMaterial`. */
2291
+ warnedDetail = new Set();
2282
2292
  warnedLayersTriplanar = false;
2283
2293
  /** Its numbers, `uModelParams`, packed by `packModel`. */
2284
2294
  modelParams = new Float32Array(MODEL_PARAM_FLOATS);
@@ -2683,7 +2693,10 @@ export class WebGPURenderer {
2683
2693
  const ownModelMap = (material?.modelMap ?? null);
2684
2694
  /* A layered material's mask, where it is a map, goes where a model's would: surfaceLayers.ts. */
2685
2695
  const layers = this.layersOf(material);
2686
- const modelMap = ownModelMap ?? layerMaskMap(layers);
2696
+ const taken = ownModelMap ?? layerMaskMap(layers);
2697
+ /* And its detail's array, where nothing else is read there: surfaceDetail.ts. */
2698
+ const detail = this.detailOfMaterial(material, taken);
2699
+ const modelMap = taken ?? detail?.maps ?? null;
2687
2700
  this.materialModel = material?.model ?? null;
2688
2701
  const reflectivity = material?.reflectivity;
2689
2702
  const environmentGain = material?.environmentGain;
@@ -2767,6 +2780,11 @@ export class WebGPURenderer {
2767
2780
  f.set(this.lookFloats, this.materialField('uLayerLooks'));
2768
2781
  if (asksLayerLooks(layers))
2769
2782
  this.enableLit('LAYER_LOOKS');
2783
+ /* And its tiled detail, the first time a material lays any: detail.ts. */
2784
+ packSurfaceDetail(detail, orm !== null, this.detailFloats);
2785
+ f.set(this.detailFloats, this.materialField('uDetail'));
2786
+ if (detail !== null)
2787
+ this.enableLit('SURFACE_DETAIL');
2770
2788
  f.set(this.modelParams, this.materialField('uModelParams'));
2771
2789
  });
2772
2790
  /*
@@ -4931,17 +4949,14 @@ export class WebGPURenderer {
4931
4949
  /** The same layout plus the joint palette. See `createDepthBindGroupLayout`. */
4932
4950
  shadowSkinnedLayout;
4933
4951
  /**
4934
- * The skinned shadow bind groups, one per palette slot, cached against what can invalidate them.
4952
+ * The skinned shadow bind groups, a shelf per palette slot, each found by what it was built with.
4935
4953
  *
4936
4954
  * The same argument `flatBindGroupFor` makes, and per slot for the same reason: the group it was
4937
4955
  * built beside, and the palette view, which now differs between two characters casting into one
4938
- * bake. Comparing object identity makes a missed invalidation impossible rather than unlikely.
4956
+ * bake, and the cloth's particles and binding, which take turns. Comparing object identity makes
4957
+ * a missed invalidation impossible rather than unlikely. See `groupShelf.ts`.
4939
4958
  */
4940
- shadowSkinnedGroups = [];
4941
- shadowSkinnedGroupsBeside = [];
4942
- shadowSkinnedGroupsPalette = [];
4943
- shadowSkinnedGroupsCloth = [];
4944
- shadowSkinnedGroupsClothBinding = [];
4959
+ shadowSkinnedShelves = [];
4945
4960
  /** Held from construction so the skinned group can be rebuilt without them being threaded. */
4946
4961
  shadowBlankView = null;
4947
4962
  shadowBlankSampler = null;
@@ -5098,18 +5113,17 @@ export class WebGPURenderer {
5098
5113
  */
5099
5114
  shadowSkinnedGroup(palette, at, cloth) {
5100
5115
  const beside = this.shadowBindGroup;
5101
- if ((this.shadowSkinnedGroups[at] ?? null) === null ||
5102
- this.shadowSkinnedGroupsBeside[at] !== beside ||
5103
- this.shadowSkinnedGroupsPalette[at] !== palette ||
5104
- this.shadowSkinnedGroupsCloth[at] !== cloth.particles ||
5105
- this.shadowSkinnedGroupsClothBinding[at] !== cloth.binding) {
5106
- this.shadowSkinnedGroups[at] = createDepthBindGroup(this.surface.device, this.shadowSkinnedLayout, this.shadowDraws.buffer, this.shadowPassUniforms, this.shadowBlankView, this.shadowBlankSampler, palette, null, cloth);
5107
- this.shadowSkinnedGroupsBeside[at] = beside;
5108
- this.shadowSkinnedGroupsPalette[at] = palette;
5109
- this.shadowSkinnedGroupsCloth[at] = cloth.particles;
5110
- this.shadowSkinnedGroupsClothBinding[at] = cloth.binding;
5111
- }
5112
- return this.shadowSkinnedGroups[at];
5116
+ let shelf = this.shadowSkinnedShelves[at];
5117
+ if (shelf === undefined) {
5118
+ shelf = new GroupShelf();
5119
+ this.shadowSkinnedShelves[at] = shelf;
5120
+ }
5121
+ const kept = shelf.find(beside, palette, cloth.particles, cloth.binding);
5122
+ if (kept !== null)
5123
+ return kept;
5124
+ const group = createDepthBindGroup(this.surface.device, this.shadowSkinnedLayout, this.shadowDraws.buffer, this.shadowPassUniforms, this.shadowBlankView, this.shadowBlankSampler, palette, null, cloth);
5125
+ shelf.keep(group, beside, palette, cloth.particles, cloth.binding);
5126
+ return group;
5113
5127
  }
5114
5128
  scatterDepthGroup() {
5115
5129
  return this.shadowLayerIsPeel && this.scatterDepthPeelBindGroup !== null
@@ -5141,6 +5155,23 @@ export class WebGPURenderer {
5141
5155
  * A material's layers where they can be drawn: none asked, or a model's map in the mask's place,
5142
5156
  * said once, leave one layer.
5143
5157
  */
5158
+ /**
5159
+ * A material's detail, or null where it is refused: its slot holds another array, or it is placed
5160
+ * by the world. Each said once. This backend has room for its vectors wherever it runs.
5161
+ */
5162
+ detailOfMaterial(material, taken) {
5163
+ const detail = detailOf(material);
5164
+ if (detail === null)
5165
+ return null;
5166
+ const refusal = detailRefusal(detail, taken, (material?.projection ?? null) !== null);
5167
+ if (refusal === null)
5168
+ return detail;
5169
+ if (!this.warnedDetail.has(refusal)) {
5170
+ this.warnedDetail.add(refusal);
5171
+ console.warn(detailRefusalWarning(refusal));
5172
+ }
5173
+ return null;
5174
+ }
5144
5175
  layersOf(material) {
5145
5176
  const layers = material?.layers ?? null;
5146
5177
  if (layers === null || layers.repeats.length === 0)
@@ -6863,7 +6894,7 @@ export class WebGPURenderer {
6863
6894
  * neither the pipeline set nor the key depends on whether the geometry lands in one call or
6864
6895
  * over six frames.
6865
6896
  */
6866
- warmFlatPipelines(data, dynamic) {
6897
+ warmFlatPipelines(data, dynamic, dynamicColors = false) {
6867
6898
  /*
6868
6899
  * **Every optional attribute, not the two that come to mind.** `present` decides where
6869
6900
  * each attribute sits in the interleaved buffer, and `createGpuMesh` decides the same
@@ -6901,9 +6932,10 @@ export class WebGPURenderer {
6901
6932
  }
6902
6933
  /*
6903
6934
  * And how each attribute travels (`vertexPacking.ts`): a field read as sixteen bits rather than
6904
- * thirty-two, or not interleaved at all, moves every field after it, so the key says so.
6935
+ * thirty-two, or not interleaved at all, moves every field after it, so the key says so. A
6936
+ * `dynamicColors` mesh's colours are one of those, as `:colors~stream`.
6905
6937
  */
6906
- const packing = vertexPackingOf(data, dynamic);
6938
+ const packing = vertexPackingOf(data, dynamic, dynamicColors);
6907
6939
  markPacking(present, packing);
6908
6940
  key += packingKey(packing);
6909
6941
  const fullKey = `${this.variant}|${key}`;
@@ -6993,8 +7025,10 @@ export class WebGPURenderer {
6993
7025
  createMesh(given, options = {}) {
6994
7026
  /* The streams it is drawn with, before anything records which it has. See `withLightmapUvs`. */
6995
7027
  const data = withLightmapUvs(given);
6996
- const fullKey = this.warmFlatPipelines(data, options.dynamic === true);
6997
- return this.keyed(createGpuMesh(this.surface.device, data, options.dynamic === true), fullKey);
7028
+ const dynamic = options.dynamic === true;
7029
+ const dynamicColors = options.dynamicColors === true;
7030
+ const fullKey = this.warmFlatPipelines(data, dynamic, dynamicColors);
7031
+ return this.keyed(createGpuMesh(this.surface.device, data, dynamic, dynamicColors), fullKey);
6998
7032
  }
6999
7033
  /**
7000
7034
  * Geometry in, handle out — but the geometry lands over as many frames as the caller gives it.
@@ -7007,8 +7041,10 @@ export class WebGPURenderer {
7007
7041
  */
7008
7042
  createMeshIncremental(given, options = {}) {
7009
7043
  const data = withLightmapUvs(given);
7010
- const fullKey = this.warmFlatPipelines(data, options.dynamic === true);
7011
- const { mesh, upload } = createGpuMeshIncremental(this.surface.device, data, options.dynamic === true);
7044
+ const dynamic = options.dynamic === true;
7045
+ const dynamicColors = options.dynamicColors === true;
7046
+ const fullKey = this.warmFlatPipelines(data, dynamic, dynamicColors);
7047
+ const { mesh, upload } = createGpuMeshIncremental(this.surface.device, data, dynamic, dynamicColors);
7012
7048
  const surface = this.surface;
7013
7049
  return {
7014
7050
  mesh: this.keyed(mesh, fullKey),
@@ -7102,6 +7138,75 @@ export class WebGPURenderer {
7102
7138
  }
7103
7139
  mesh.update(this.surface.device, positions, normals, staged);
7104
7140
  }
7141
+ /** The pipeline every point skin poses with, made at the first one. See `pointSkinPass.ts`. */
7142
+ pointSkinPipeline = null;
7143
+ /**
7144
+ * A dynamic mesh posed on the device from weighted points in its joints' spaces. Refused in words
7145
+ * on a mesh that is not dynamic, whose positions share one buffer with the rest of each vertex.
7146
+ */
7147
+ createPointSkin(mesh, data) {
7148
+ if (mesh.update === null) {
7149
+ throw new Error('a point skin poses a mesh created with { dynamic: true }, whose positions and normals ' +
7150
+ 'have buffers of their own for the pose to write. Say so at `createMesh`.');
7151
+ }
7152
+ validatePointSkin(data, mesh.vertexCount);
7153
+ const device = this.surface.device;
7154
+ this.pointSkinPipeline ??= createPointSkinPipeline(device);
7155
+ return new GpuPointSkin(device, this.pointSkinPipeline, mesh, data);
7156
+ }
7157
+ /**
7158
+ * Pose a skin: `matrices` holds each joint's transform into the mesh's model space, sixteen floats,
7159
+ * column-major. Recorded into the encoder the frame's next submit leads with, behind a copy of
7160
+ * last frame's positions where a reconstruction keeps them, as `updateMesh` records its rewrite.
7161
+ */
7162
+ posePointSkin(skin, matrices) {
7163
+ validatePose(matrices, skin.reach);
7164
+ if (this.surface.lost)
7165
+ return;
7166
+ const mesh = skin.mesh;
7167
+ poseBounds(matrices, skin.reach, mesh.bounds);
7168
+ const encoder = this.rewriteCopies();
7169
+ const motion = mesh.motion;
7170
+ if (motion !== null && this.reconstructionWanted) {
7171
+ const frame = frameOfChange(this.moverFrame, this.inFrame);
7172
+ if (keepsPrevious(motion.changed, frame)) {
7173
+ const size = mesh.vertexBuffers[0]?.size ?? 0;
7174
+ motion.previous ??= this.surface.device.createBuffer({
7175
+ label: 'recon.dynamicPrevious',
7176
+ size,
7177
+ usage: 0x0020 | 0x0008, // VERTEX | COPY_DST
7178
+ });
7179
+ encoder.copyBufferToBuffer(mesh.vertexBuffers[0], 0, motion.previous, 0, size);
7180
+ }
7181
+ motion.changed = frame;
7182
+ }
7183
+ skin.pose(this.surface.device, encoder, matrices);
7184
+ }
7185
+ /**
7186
+ * **The pending rewrites go first**, since a pose recorded into them reads the skin's buffers and
7187
+ * a destroyed buffer in a submit loses the whole command buffer. Nothing else reads them, so once
7188
+ * the rewrites are submitted, which only moves them earlier, the buffers can go at once.
7189
+ */
7190
+ disposePointSkin(skin) {
7191
+ if (this.pendingRewrites !== null && !this.surface.lost)
7192
+ this.submit([]);
7193
+ skin.dispose();
7194
+ }
7195
+ /**
7196
+ * Rewrite a mesh's colours. Refused in words on a mesh that did not ask for `dynamicColors`,
7197
+ * whose colours are interleaved with the rest of a vertex or are one value with no buffer at all.
7198
+ * See `GpuMesh.updateColors`.
7199
+ */
7200
+ updateMeshColors(mesh, colors) {
7201
+ if (mesh.updateColors === null) {
7202
+ throw new Error('this mesh was not created with { dynamicColors: true }, so its colours are packed with ' +
7203
+ 'the rest of each vertex, or are one value with no buffer at all, and cannot be ' +
7204
+ 'rewritten. Say so at `createMesh`: the flag gives the colours a buffer of their own.');
7205
+ }
7206
+ if (this.surface.lost)
7207
+ return;
7208
+ mesh.updateColors(this.surface.device, colors);
7209
+ }
7105
7210
  /**
7106
7211
  * Settle everything the frame shares: the camera, the lights and the fog.
7107
7212
  *
@@ -7237,6 +7342,8 @@ export class WebGPURenderer {
7237
7342
  /* Nor layers: one layer until a material asks for more. */
7238
7343
  f.fill(0, at('uLayers'), at('uLayers') + LAYER_FLOATS);
7239
7344
  f.fill(0, at('uLayerLooks'), at('uLayerLooks') + LOOK_FLOATS);
7345
+ /* Nor detail. */
7346
+ f.fill(0, at('uDetail'), at('uDetail') + DETAIL_FLOATS);
7240
7347
  f[at('uCutout')] = 0;
7241
7348
  f[at('uCutout') + 1] = 0;
7242
7349
  this.cutoutResolveStaged = 'hard';
@@ -10067,26 +10174,22 @@ export class WebGPURenderer {
10067
10174
  byMaterial = new Map();
10068
10175
  this.skinnedGroups[at] = byMaterial;
10069
10176
  }
10070
- let twin = byMaterial.get(base);
10071
- if (twin === undefined ||
10072
- twin.palette !== palette ||
10073
- twin.deltas !== deltas ||
10074
- twin.cloth !== (cloth?.particles ?? null) ||
10075
- twin.clothBinding !== (cloth?.binding ?? null)) {
10076
- /* Built from the live material, with the overlay's atlas where `base` is its twin. */
10077
- this.overlayBinding = base !== this.bindGroup;
10078
- const group = createFlatBindGroup(this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, this.perDraw.buffer, this.passRing.buffer, this.materialStore.buffer, this.viewRing.buffer, this.flatTextures, palette, deltas, cloth);
10079
- this.overlayBinding = false;
10080
- twin = {
10081
- group,
10082
- palette,
10083
- deltas,
10084
- cloth: cloth?.particles ?? null,
10085
- clothBinding: cloth?.binding ?? null,
10086
- };
10087
- byMaterial.set(base, twin);
10088
- }
10089
- return twin.group;
10177
+ let shelf = byMaterial.get(base);
10178
+ if (shelf === undefined) {
10179
+ shelf = new GroupShelf();
10180
+ byMaterial.set(base, shelf);
10181
+ }
10182
+ const particles = cloth?.particles ?? null;
10183
+ const binding = cloth?.binding ?? null;
10184
+ const kept = shelf.find(palette, deltas, particles, binding);
10185
+ if (kept !== null)
10186
+ return kept;
10187
+ /* Built from the live material, with the overlay's atlas where `base` is its twin. */
10188
+ this.overlayBinding = base !== this.bindGroup;
10189
+ const group = createFlatBindGroup(this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, this.perDraw.buffer, this.passRing.buffer, this.materialStore.buffer, this.viewRing.buffer, this.flatTextures, palette, deltas, cloth);
10190
+ this.overlayBinding = false;
10191
+ shelf.keep(group, palette, deltas, particles, binding);
10192
+ return group;
10090
10193
  }
10091
10194
  /** The layout matching a pair of vertex flags. A pipeline and its bind group must agree. */
10092
10195
  flatLayoutFor(skinned, morphed) {
@@ -37,4 +37,4 @@
37
37
  * a reset, sits the round out while the rest of the set runs it.
38
38
  */
39
39
  export declare const CLOTH_WORKGROUP = 64;
40
- export declare const CLOTH_SOLVE_WGSL = "\n/* The set's: every garment's particles, records, skin matrices and colliders, and the step length\n they all share. */\nstruct Constants {\n count: u32,\n distances: u32,\n bendings: u32,\n joints: u32,\n colliders: u32,\n pad0: u32,\n pad1: u32,\n pad2: u32,\n h: f32,\n invH: f32,\n invH2: f32,\n pad3: f32,\n}\n\n/* Where this dispatch's round of records starts in paces, at a dynamic offset. */\nstruct Round {\n at: u32,\n pad0: u32,\n pad1: u32,\n pad2: u32,\n}\n\n/* One garment's numbers for one round: its step's, and its own that never change. */\nstruct Pace {\n fraction: f32,\n blend: f32,\n alpha: f32,\n carry: u32,\n /* Where the pose a step starts from and the one it moves toward sit in pose, in floats. */\n fromAt: u32,\n toAt: u32,\n /* 0 where the garment sits this round out. Not active, which WGSL reserves. */\n live: u32,\n /* 1 a max distance, 2 a backstop, 4 a frontstop: which limits the garment has at all. */\n limits: u32,\n /* The character's turn since the last step, by columns; origin.w is the share carried. */\n turn0: vec4<f32>,\n turn1: vec4<f32>,\n turn2: vec4<f32>,\n origin: vec4<f32>,\n shift: vec4<f32>,\n /* The frame's wind: one wind, sampled once a frame, so a step's rather than the set-up's. */\n wind: vec4<f32>,\n colliderBase: u32,\n colliders: u32,\n width: u32,\n pad0: u32,\n keep: f32,\n dragKeep: f32,\n maxDistanceScale: f32,\n margin: f32,\n gravity: vec4<f32>,\n}\n\n/* One colour of one kind of constraint across the set, at a dynamic offset; first resets the\n multipliers. Publishing reads one garment's particles as begin to end, and first is the garment. */\nstruct Batch {\n begin: u32,\n end: u32,\n first: u32,\n pad: u32,\n}\n\n@group(0) @binding(0) var<uniform> c: Constants;\n@group(0) @binding(1) var<uniform> round: Round;\n@group(0) @binding(2) var<uniform> batch: Batch;\n@group(0) @binding(3) var<storage, read_write> state: array<f32>;\n@group(0) @binding(4) var<storage, read_write> pose: array<f32>;\n@group(0) @binding(5) var<storage, read> statics: array<f32>;\n@group(0) @binding(6) var<storage, read> constraints: array<u32>;\n@group(0) @binding(7) var<storage, read_write> lambda: array<f32>;\n@group(0) @binding(8) var<storage, read> frame: array<f32>;\n@group(0) @binding(9) var<storage, read> paces: array<Pace>;\n\n/* The record of particle i's garment this round: which garment a particle is, in statics. */\nfn paceAt(i: u32) -> u32 {\n return round.at + u32(statics[29u * c.count + i]);\n}\n\nfn stateAt(section: u32, i: u32) -> u32 {\n return section * 3u * c.count + i * 3u;\n}\n\nfn load(at: u32) -> vec3<f32> {\n return vec3<f32>(state[at], state[at + 1u], state[at + 2u]);\n}\n\nfn store(at: u32, v: vec3<f32>) {\n state[at] = v.x;\n state[at + 1u] = v.y;\n state[at + 2u] = v.z;\n}\n\nfn poseOf(at: u32) -> vec3<f32> {\n return vec3<f32>(pose[at], pose[at + 1u], pose[at + 2u]);\n}\n\n/* Sections of state. */\nconst POSITION = 0u;\nconst PREVIOUS = 1u;\nconst VELOCITY = 2u;\nconst START = 3u;\n\nfn inverseMass(i: u32) -> f32 {\n return statics[i];\n}\n\n/* Where skinning puts particle i, fraction of the way from the last pose to the latest. */\nfn skinnedAt(i: u32, g: u32) -> vec3<f32> {\n let a = poseOf(paces[g].fromAt + i * 3u);\n let b = poseOf(paces[g].toAt + i * 3u);\n return a + (b - a) * paces[g].fraction;\n}\n\nfn skinMatrix(j: u32) -> mat4x4<f32> {\n let at = j * 16u;\n return mat4x4<f32>(\n vec4<f32>(frame[at], frame[at + 1u], frame[at + 2u], frame[at + 3u]),\n vec4<f32>(frame[at + 4u], frame[at + 5u], frame[at + 6u], frame[at + 7u]),\n vec4<f32>(frame[at + 8u], frame[at + 9u], frame[at + 10u], frame[at + 11u]),\n vec4<f32>(frame[at + 12u], frame[at + 13u], frame[at + 14u], frame[at + 15u]));\n}\n\n/* skinParticles: each particle's skinned position and normal by up to eight influences. */\n@compute @workgroup_size(64)\nfn clothPose(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let n = c.count;\n let restAt = 7u * n + i * 3u;\n let normalAt = 10u * n + i * 3u;\n let x = vec4<f32>(statics[restAt], statics[restAt + 1u], statics[restAt + 2u], 1.0);\n let q0 = vec3<f32>(statics[normalAt], statics[normalAt + 1u], statics[normalAt + 2u]);\n var p = vec3<f32>(0.0);\n var q = vec3<f32>(0.0);\n for (var half = 0u; half < 2u; half++) {\n for (var k = 0u; k < 4u; k++) {\n let at = 13u * n + i * 16u + half * 8u;\n let w = statics[at + 4u + k];\n if (w == 0.0) {\n continue;\n }\n let m = skinMatrix(u32(statics[at + k]));\n p += w * (m * x).xyz;\n q += w * (mat3x3<f32>(m[0].xyz, m[1].xyz, m[2].xyz) * q0);\n }\n }\n let to = paces[g].toAt + i * 3u;\n pose[to] = p.x;\n pose[to + 1u] = p.y;\n pose[to + 2u] = p.z;\n let len = sqrt(dot(q, q));\n let inv = select(0.0, 1.0 / len, len > 1e-12);\n let normal = 6u * n + i * 3u;\n pose[normal] = q.x * inv;\n pose[normal + 1u] = q.y * inv;\n pose[normal + 2u] = q.z * inv;\n}\n\n/* SkinnedCloth.rest: every particle at the latest skinned position, at rest. */\n@compute @workgroup_size(64)\nfn clothRest(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let p = poseOf(paces[g].toAt + i * 3u);\n store(stateAt(POSITION, i), p);\n store(stateAt(START, i), p);\n store(stateAt(VELOCITY, i), vec3<f32>(0.0));\n}\n\n/* The step's start: the step start kept, a free particle carried, a kinematic one placed. */\n@compute @workgroup_size(64)\nfn clothBegin(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let at = stateAt(POSITION, i);\n let p = load(at);\n store(stateAt(START, i), p);\n if (inverseMass(i) == 0.0) {\n store(at, skinnedAt(i, g));\n return;\n }\n if (paces[g].carry == 0u) {\n return;\n }\n /* carryParticles: the share of the character's turn about its old origin, and of its move. */\n let turn = mat3x3<f32>(paces[g].turn0.xyz, paces[g].turn1.xyz, paces[g].turn2.xyz);\n let share = paces[g].origin.w;\n let x = p - paces[g].origin.xyz;\n store(at, p + (turn * x - x) * share + paces[g].shift.xyz);\n let vAt = stateAt(VELOCITY, i);\n let v = load(vAt);\n store(vAt, v + (turn * v - v) * share);\n}\n\n/* predictParticles. */\n@compute @workgroup_size(64)\nfn clothPredict(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let at = stateAt(POSITION, i);\n let p = load(at);\n store(stateAt(PREVIOUS, i), p);\n if (inverseMass(i) == 0.0) {\n return;\n }\n let vAt = stateAt(VELOCITY, i);\n let air = paces[g].wind.xyz;\n let v = air + (load(vAt) * paces[g].keep - air) * paces[g].dragKeep + paces[g].gravity.xyz * c.h;\n store(vAt, v);\n store(at, p + v * c.h);\n}\n\n/* solveDistanceBatch: one invocation a constraint of the batch, constraints sorted by batch. */\n@compute @workgroup_size(64)\nfn clothDistance(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n let a = constraints[k * 4u];\n let b = constraints[k * 4u + 1u];\n /* A garment sitting the round out solves nothing, and its next step's first pass resets. */\n if (paces[paceAt(a)].live == 0u) {\n return;\n }\n /* The first pass of a substep starts the multiplier at nothing, as the CPU's fill(0) does \u2014\n written before any early return, so a skipped constraint does not carry the last one. */\n if (batch.first == 1u) {\n lambda[k] = 0.0;\n }\n let wa = inverseMass(a);\n let wb = inverseMass(b);\n let w = wa + wb;\n if (w == 0.0) {\n return;\n }\n let aAt = stateAt(POSITION, a);\n let bAt = stateAt(POSITION, b);\n let d = load(bAt) - load(aAt);\n let len = sqrt(dot(d, d));\n if (len < 1e-9) {\n return;\n }\n let alpha = bitcast<f32>(constraints[k * 4u + 3u]) * c.invH2;\n let old = lambda[k];\n let delta = (-(len - bitcast<f32>(constraints[k * 4u + 2u])) - alpha * old) / (w + alpha);\n lambda[k] = old + delta;\n let s = delta / len;\n /* A pinned particle is shared across a batch, so it is never written, even with itself. */\n if (wa > 0.0) {\n store(aAt, load(aAt) - d * s * wa);\n }\n if (wb > 0.0) {\n store(bAt, load(bAt) + d * s * wb);\n }\n}\n\n/* solveBendingBatch, with the angle from atan2 (see the header). */\n@compute @workgroup_size(64)\nfn clothBending(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n let at = c.distances * 4u + k * 6u;\n let slot = c.distances + k;\n if (paces[paceAt(constraints[at])].live == 0u) {\n return;\n }\n if (batch.first == 1u) {\n lambda[slot] = 0.0;\n }\n let ids = vec4<u32>(constraints[at], constraints[at + 1u], constraints[at + 2u], constraints[at + 3u]);\n let w = vec4<f32>(inverseMass(ids.x), inverseMass(ids.y), inverseMass(ids.z), inverseMass(ids.w));\n if (w.x + w.y + w.z + w.w == 0.0) {\n return;\n }\n let origin = load(stateAt(POSITION, ids.x));\n let e = load(stateAt(POSITION, ids.y)) - origin;\n let p3 = load(stateAt(POSITION, ids.z)) - origin;\n let p4 = load(stateAt(POSITION, ids.w)) - origin;\n var n1 = cross(e, p3);\n var n2 = cross(e, p4);\n let l1 = sqrt(dot(n1, n1));\n let l2 = sqrt(dot(n2, n2));\n if (l1 < 1e-12 || l2 < 1e-12) {\n return;\n }\n n1 = n1 / l1;\n n2 = n2 / l2;\n let d = clamp(dot(n1, n2), -1.0, 1.0);\n let sine = sqrt(dot(cross(n1, n2), cross(n1, n2)));\n if (sine < 1e-6) {\n return;\n }\n /* The q vectors: the negative of the derivative of n1 \u00B7 n2. See solveBendingBatch. */\n let q3 = (cross(e, n2) + cross(n1, e) * d) / l1;\n let q4 = (cross(e, n1) + cross(n2, e) * d) / l2;\n let q2 = -((cross(p3, n2) + cross(n1, p3) * d) / l1 + (cross(p4, n1) + cross(n2, p4) * d) / l2);\n let q1 = -(q2 + q3 + q4);\n var sum = w.x * dot(q1, q1) + w.y * dot(q2, q2) + w.z * dot(q3, q3) + w.w * dot(q4, q4);\n sum = sum / (sine * sine);\n if (sum < 1e-12) {\n return;\n }\n let alpha = bitcast<f32>(constraints[at + 5u]) * c.invH2;\n let old = lambda[slot];\n let angle = atan2(sine, d);\n let delta = (-(angle - bitcast<f32>(constraints[at + 4u])) - alpha * old) / (sum + alpha);\n lambda[slot] = old + delta;\n let scale = delta / sine;\n if (w.x > 0.0) {\n let p = stateAt(POSITION, ids.x);\n store(p, load(p) + w.x * scale * q1);\n }\n if (w.y > 0.0) {\n let p = stateAt(POSITION, ids.y);\n store(p, load(p) + w.y * scale * q2);\n }\n if (w.z > 0.0) {\n let p = stateAt(POSITION, ids.z);\n store(p, load(p) + w.z * scale * q3);\n }\n if (w.w > 0.0) {\n let p = stateAt(POSITION, ids.w);\n store(p, load(p) + w.w * scale * q4);\n }\n}\n\n/* solveTethers, one invocation a tether of the batch: a batch holds a particle once. */\n@compute @workgroup_size(64)\nfn clothTether(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n let at = c.distances * 4u + c.bendings * 6u + k * 3u;\n if (paces[paceAt(constraints[at])].live == 0u) {\n return;\n }\n let pAt = stateAt(POSITION, constraints[at]);\n let anchor = load(stateAt(POSITION, constraints[at + 1u]));\n let d = load(pAt) - anchor;\n let dist = sqrt(dot(d, d));\n let reach = bitcast<f32>(constraints[at + 2u]);\n if (dist <= reach || dist < 1e-9) {\n return;\n }\n store(pAt, anchor + d * (reach / dist));\n}\n\n/* pushOut: the point onto the sphere's surface if it is inside. A point at the centre stays. */\nfn pushOut(p: vec3<f32>, centre: vec3<f32>, radius: f32) -> vec3<f32> {\n let d = p - centre;\n let d2 = dot(d, d);\n if (d2 >= radius * radius || d2 < 1e-18) {\n return p;\n }\n return centre + d * (radius / sqrt(d2));\n}\n\n/* applyMaxDistance, applyStops (back, then front) and applyColliders, in that order. */\n@compute @workgroup_size(64)\nfn clothLimit(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count || inverseMass(i) == 0.0) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let limits = paces[g].limits;\n let n = c.count;\n let at = stateAt(POSITION, i);\n var p = load(at);\n let t = skinnedAt(i, g);\n /* A max distance below zero is none: the CPU's Infinity, which a device may not be handed. */\n let maxDistance = statics[n + i];\n if ((limits & 1u) != 0u && maxDistance >= 0.0) {\n let limit = maxDistance * paces[g].maxDistanceScale;\n let d = p - t;\n let d2 = dot(d, d);\n if (d2 > limit * limit) {\n p = t + d * (limit / sqrt(d2));\n }\n }\n let normal = poseOf(6u * n + i * 3u);\n if ((limits & 2u) != 0u) {\n let radius = statics[2u * n + i * 2u + 1u];\n if (radius > 0.0) {\n p = pushOut(p, t - normal * (statics[2u * n + i * 2u] + radius), radius);\n }\n }\n if ((limits & 4u) != 0u) {\n let radius = statics[4u * n + i * 2u + 1u];\n if (radius > 0.0) {\n p = pushOut(p, t + normal * (statics[4u * n + i * 2u] + radius), radius);\n }\n }\n let pad = statics[6u * n + i] + paces[g].margin;\n let ends = c.joints * 16u;\n let radii = ends + c.colliders * 6u;\n let first = paces[g].colliderBase;\n for (var k = first; k < first + paces[g].colliders; k++) {\n let a = vec3<f32>(frame[ends + k * 6u], frame[ends + k * 6u + 1u], frame[ends + k * 6u + 2u]);\n let e = vec3<f32>(frame[ends + k * 6u + 3u], frame[ends + k * 6u + 4u], frame[ends + k * 6u + 5u]) - a;\n let e2 = dot(e, e);\n var s = 0.0;\n if (e2 > 1e-18) {\n s = clamp(dot(p - a, e) / e2, 0.0, 1.0);\n }\n let r0 = frame[radii + k * 2u];\n let r1 = frame[radii + k * 2u + 1u];\n p = pushOut(p, a + e * s, r0 + (r1 - r0) * s + pad);\n }\n store(at, p);\n}\n\n/* finishParticles. */\n@compute @workgroup_size(64)\nfn clothFinish(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n if (paces[paceAt(i)].live == 0u) {\n return;\n }\n let vAt = stateAt(VELOCITY, i);\n if (inverseMass(i) == 0.0) {\n store(vAt, vec3<f32>(0.0));\n return;\n }\n store(vAt, (load(stateAt(POSITION, i)) - load(stateAt(PREVIOUS, i))) * c.invH);\n}\n\n/* SkinnedCloth.blend: after a reset, the simulation's share grows over the blend's steps. */\n@compute @workgroup_size(64)\nfn clothBlend(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n /* A blend of 0 is none, not a last step of one: see ClothDevice.step. */\n let g = paceAt(i);\n let blend = paces[g].blend;\n if (paces[g].live == 0u || blend <= 0.0) {\n return;\n }\n let t = skinnedAt(i, g);\n let at = stateAt(POSITION, i);\n store(at, t + (load(at) - t) * blend);\n let vAt = stateAt(VELOCITY, i);\n store(vAt, load(vAt) * blend);\n}\n\n@group(1) @binding(0) var output: texture_storage_2d<rgba32float, write>;\n\n/* SkinnedCloth.interpolate, into the particle texture the vertex stage reads: one garment's, whose\n particles are batch.begin to batch.end and whose record is batch.first's. */\n@compute @workgroup_size(64)\nfn clothPublish(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = batch.begin + id.x;\n if (i >= batch.end) {\n return;\n }\n let g = round.at + batch.first;\n let a = load(stateAt(START, i));\n let p = a + (load(stateAt(POSITION, i)) - a) * paces[g].alpha;\n let local = id.x;\n let width = paces[g].width;\n textureStore(output, vec2<u32>(local % width, local / width), vec4<f32>(p, 0.0));\n}\n";
40
+ export declare const CLOTH_SOLVE_WGSL = "\n/* The set's: every garment's particles, records, skin matrices and colliders, and the step length\n they all share. */\nstruct Constants {\n count: u32,\n distances: u32,\n bendings: u32,\n joints: u32,\n colliders: u32,\n pad0: u32,\n pad1: u32,\n pad2: u32,\n h: f32,\n invH: f32,\n invH2: f32,\n pad3: f32,\n}\n\n/* Where this dispatch's round of records starts in paces, at a dynamic offset. */\nstruct Round {\n at: u32,\n pad0: u32,\n pad1: u32,\n pad2: u32,\n}\n\n/* One garment's numbers for one round: its step's, and its own that never change. */\nstruct Pace {\n fraction: f32,\n blend: f32,\n alpha: f32,\n carry: u32,\n /* Where the pose a step starts from and the one it moves toward sit in pose, in floats. */\n fromAt: u32,\n toAt: u32,\n /* 0 where the garment sits this round out. Not active, which WGSL reserves. */\n live: u32,\n /* 1 a max distance, 2 a backstop, 4 a frontstop: which limits the garment has at all. */\n limits: u32,\n /* The character's turn since the last step, by columns; origin.w is the share carried. */\n turn0: vec4<f32>,\n turn1: vec4<f32>,\n turn2: vec4<f32>,\n origin: vec4<f32>,\n shift: vec4<f32>,\n /* The frame's wind: one wind, sampled once a frame, so a step's rather than the set-up's. */\n wind: vec4<f32>,\n colliderBase: u32,\n colliders: u32,\n width: u32,\n /* The share of its offset from its skinned position a particle keeps a substep: the pull's. */\n pullKeep: f32,\n keep: f32,\n dragKeep: f32,\n maxDistanceScale: f32,\n margin: f32,\n gravity: vec4<f32>,\n}\n\n/* One colour of one kind of constraint across the set, at a dynamic offset; first resets the\n multipliers. Publishing reads one garment's particles as begin to end, and first is the garment. */\nstruct Batch {\n begin: u32,\n end: u32,\n first: u32,\n pad: u32,\n}\n\n@group(0) @binding(0) var<uniform> c: Constants;\n@group(0) @binding(1) var<uniform> round: Round;\n@group(0) @binding(2) var<uniform> batch: Batch;\n@group(0) @binding(3) var<storage, read_write> state: array<f32>;\n@group(0) @binding(4) var<storage, read_write> pose: array<f32>;\n@group(0) @binding(5) var<storage, read> statics: array<f32>;\n@group(0) @binding(6) var<storage, read> constraints: array<u32>;\n@group(0) @binding(7) var<storage, read_write> lambda: array<f32>;\n@group(0) @binding(8) var<storage, read> frame: array<f32>;\n@group(0) @binding(9) var<storage, read> paces: array<Pace>;\n\n/* The record of particle i's garment this round: which garment a particle is, in statics. */\nfn paceAt(i: u32) -> u32 {\n return round.at + u32(statics[29u * c.count + i]);\n}\n\nfn stateAt(section: u32, i: u32) -> u32 {\n return section * 3u * c.count + i * 3u;\n}\n\nfn load(at: u32) -> vec3<f32> {\n return vec3<f32>(state[at], state[at + 1u], state[at + 2u]);\n}\n\nfn store(at: u32, v: vec3<f32>) {\n state[at] = v.x;\n state[at + 1u] = v.y;\n state[at + 2u] = v.z;\n}\n\nfn poseOf(at: u32) -> vec3<f32> {\n return vec3<f32>(pose[at], pose[at + 1u], pose[at + 2u]);\n}\n\n/* Sections of state. */\nconst POSITION = 0u;\nconst PREVIOUS = 1u;\nconst VELOCITY = 2u;\nconst START = 3u;\n\nfn inverseMass(i: u32) -> f32 {\n return statics[i];\n}\n\n/* Where skinning puts particle i, fraction of the way from the last pose to the latest. */\nfn skinnedAt(i: u32, g: u32) -> vec3<f32> {\n let a = poseOf(paces[g].fromAt + i * 3u);\n let b = poseOf(paces[g].toAt + i * 3u);\n return a + (b - a) * paces[g].fraction;\n}\n\nfn skinMatrix(j: u32) -> mat4x4<f32> {\n let at = j * 16u;\n return mat4x4<f32>(\n vec4<f32>(frame[at], frame[at + 1u], frame[at + 2u], frame[at + 3u]),\n vec4<f32>(frame[at + 4u], frame[at + 5u], frame[at + 6u], frame[at + 7u]),\n vec4<f32>(frame[at + 8u], frame[at + 9u], frame[at + 10u], frame[at + 11u]),\n vec4<f32>(frame[at + 12u], frame[at + 13u], frame[at + 14u], frame[at + 15u]));\n}\n\n/* skinParticles: each particle's skinned position and normal by up to eight influences. */\n@compute @workgroup_size(64)\nfn clothPose(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let n = c.count;\n let restAt = 7u * n + i * 3u;\n let normalAt = 10u * n + i * 3u;\n let x = vec4<f32>(statics[restAt], statics[restAt + 1u], statics[restAt + 2u], 1.0);\n let q0 = vec3<f32>(statics[normalAt], statics[normalAt + 1u], statics[normalAt + 2u]);\n var p = vec3<f32>(0.0);\n var q = vec3<f32>(0.0);\n for (var half = 0u; half < 2u; half++) {\n for (var k = 0u; k < 4u; k++) {\n let at = 13u * n + i * 16u + half * 8u;\n let w = statics[at + 4u + k];\n if (w == 0.0) {\n continue;\n }\n let m = skinMatrix(u32(statics[at + k]));\n p += w * (m * x).xyz;\n q += w * (mat3x3<f32>(m[0].xyz, m[1].xyz, m[2].xyz) * q0);\n }\n }\n let to = paces[g].toAt + i * 3u;\n pose[to] = p.x;\n pose[to + 1u] = p.y;\n pose[to + 2u] = p.z;\n let len = sqrt(dot(q, q));\n let inv = select(0.0, 1.0 / len, len > 1e-12);\n let normal = 6u * n + i * 3u;\n pose[normal] = q.x * inv;\n pose[normal + 1u] = q.y * inv;\n pose[normal + 2u] = q.z * inv;\n}\n\n/* SkinnedCloth.rest: every particle at the latest skinned position, at rest. */\n@compute @workgroup_size(64)\nfn clothRest(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let p = poseOf(paces[g].toAt + i * 3u);\n store(stateAt(POSITION, i), p);\n store(stateAt(START, i), p);\n store(stateAt(VELOCITY, i), vec3<f32>(0.0));\n}\n\n/* The step's start: the step start kept, a free particle carried, a kinematic one placed. */\n@compute @workgroup_size(64)\nfn clothBegin(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let at = stateAt(POSITION, i);\n let p = load(at);\n store(stateAt(START, i), p);\n if (inverseMass(i) == 0.0) {\n store(at, skinnedAt(i, g));\n return;\n }\n if (paces[g].carry == 0u) {\n return;\n }\n /* carryParticles: the share of the character's turn about its old origin, and of its move. */\n let turn = mat3x3<f32>(paces[g].turn0.xyz, paces[g].turn1.xyz, paces[g].turn2.xyz);\n let share = paces[g].origin.w;\n let x = p - paces[g].origin.xyz;\n store(at, p + (turn * x - x) * share + paces[g].shift.xyz);\n let vAt = stateAt(VELOCITY, i);\n let v = load(vAt);\n store(vAt, v + (turn * v - v) * share);\n}\n\n/* predictParticles. */\n@compute @workgroup_size(64)\nfn clothPredict(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let at = stateAt(POSITION, i);\n let p = load(at);\n store(stateAt(PREVIOUS, i), p);\n if (inverseMass(i) == 0.0) {\n return;\n }\n let vAt = stateAt(VELOCITY, i);\n let air = paces[g].wind.xyz;\n let v = air + (load(vAt) * paces[g].keep - air) * paces[g].dragKeep + paces[g].gravity.xyz * c.h;\n store(vAt, v);\n store(at, p + v * c.h);\n}\n\n/* solveDistanceBatch: one invocation a constraint of the batch, constraints sorted by batch. */\n@compute @workgroup_size(64)\nfn clothDistance(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n let a = constraints[k * 4u];\n let b = constraints[k * 4u + 1u];\n /* A garment sitting the round out solves nothing, and its next step's first pass resets. */\n if (paces[paceAt(a)].live == 0u) {\n return;\n }\n /* The first pass of a substep starts the multiplier at nothing, as the CPU's fill(0) does \u2014\n written before any early return, so a skipped constraint does not carry the last one. */\n if (batch.first == 1u) {\n lambda[k] = 0.0;\n }\n let wa = inverseMass(a);\n let wb = inverseMass(b);\n let w = wa + wb;\n if (w == 0.0) {\n return;\n }\n let aAt = stateAt(POSITION, a);\n let bAt = stateAt(POSITION, b);\n let d = load(bAt) - load(aAt);\n let len = sqrt(dot(d, d));\n if (len < 1e-9) {\n return;\n }\n let alpha = bitcast<f32>(constraints[k * 4u + 3u]) * c.invH2;\n let old = lambda[k];\n let delta = (-(len - bitcast<f32>(constraints[k * 4u + 2u])) - alpha * old) / (w + alpha);\n lambda[k] = old + delta;\n let s = delta / len;\n /* A pinned particle is shared across a batch, so it is never written, even with itself. */\n if (wa > 0.0) {\n store(aAt, load(aAt) - d * s * wa);\n }\n if (wb > 0.0) {\n store(bAt, load(bAt) + d * s * wb);\n }\n}\n\n/* solveBendingBatch, with the angle from atan2 (see the header). */\n@compute @workgroup_size(64)\nfn clothBending(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n let at = c.distances * 4u + k * 6u;\n let slot = c.distances + k;\n if (paces[paceAt(constraints[at])].live == 0u) {\n return;\n }\n if (batch.first == 1u) {\n lambda[slot] = 0.0;\n }\n let ids = vec4<u32>(constraints[at], constraints[at + 1u], constraints[at + 2u], constraints[at + 3u]);\n let w = vec4<f32>(inverseMass(ids.x), inverseMass(ids.y), inverseMass(ids.z), inverseMass(ids.w));\n if (w.x + w.y + w.z + w.w == 0.0) {\n return;\n }\n let origin = load(stateAt(POSITION, ids.x));\n let e = load(stateAt(POSITION, ids.y)) - origin;\n let p3 = load(stateAt(POSITION, ids.z)) - origin;\n let p4 = load(stateAt(POSITION, ids.w)) - origin;\n var n1 = cross(e, p3);\n var n2 = cross(e, p4);\n let l1 = sqrt(dot(n1, n1));\n let l2 = sqrt(dot(n2, n2));\n if (l1 < 1e-12 || l2 < 1e-12) {\n return;\n }\n n1 = n1 / l1;\n n2 = n2 / l2;\n let d = clamp(dot(n1, n2), -1.0, 1.0);\n let sine = sqrt(dot(cross(n1, n2), cross(n1, n2)));\n if (sine < 1e-6) {\n return;\n }\n /* The q vectors: the negative of the derivative of n1 \u00B7 n2. See solveBendingBatch. */\n let q3 = (cross(e, n2) + cross(n1, e) * d) / l1;\n let q4 = (cross(e, n1) + cross(n2, e) * d) / l2;\n let q2 = -((cross(p3, n2) + cross(n1, p3) * d) / l1 + (cross(p4, n1) + cross(n2, p4) * d) / l2);\n let q1 = -(q2 + q3 + q4);\n var sum = w.x * dot(q1, q1) + w.y * dot(q2, q2) + w.z * dot(q3, q3) + w.w * dot(q4, q4);\n sum = sum / (sine * sine);\n if (sum < 1e-12) {\n return;\n }\n let alpha = bitcast<f32>(constraints[at + 5u]) * c.invH2;\n let old = lambda[slot];\n let angle = atan2(sine, d);\n let delta = (-(angle - bitcast<f32>(constraints[at + 4u])) - alpha * old) / (sum + alpha);\n lambda[slot] = old + delta;\n let scale = delta / sine;\n if (w.x > 0.0) {\n let p = stateAt(POSITION, ids.x);\n store(p, load(p) + w.x * scale * q1);\n }\n if (w.y > 0.0) {\n let p = stateAt(POSITION, ids.y);\n store(p, load(p) + w.y * scale * q2);\n }\n if (w.z > 0.0) {\n let p = stateAt(POSITION, ids.z);\n store(p, load(p) + w.z * scale * q3);\n }\n if (w.w > 0.0) {\n let p = stateAt(POSITION, ids.w);\n store(p, load(p) + w.w * scale * q4);\n }\n}\n\n/* solveTethers, one invocation a tether of the batch: a batch holds a particle once. */\n@compute @workgroup_size(64)\nfn clothTether(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n let at = c.distances * 4u + c.bendings * 6u + k * 3u;\n if (paces[paceAt(constraints[at])].live == 0u) {\n return;\n }\n let pAt = stateAt(POSITION, constraints[at]);\n let anchor = load(stateAt(POSITION, constraints[at + 1u]));\n let d = load(pAt) - anchor;\n let dist = sqrt(dot(d, d));\n let reach = bitcast<f32>(constraints[at + 2u]);\n if (dist <= reach || dist < 1e-9) {\n return;\n }\n store(pAt, anchor + d * (reach / dist));\n}\n\n/* pushOut: the point onto the sphere's surface if it is inside. A point at the centre stays. */\nfn pushOut(p: vec3<f32>, centre: vec3<f32>, radius: f32) -> vec3<f32> {\n let d = p - centre;\n let d2 = dot(d, d);\n if (d2 >= radius * radius || d2 < 1e-18) {\n return p;\n }\n return centre + d * (radius / sqrt(d2));\n}\n\n/* applyPull, applyMaxDistance, applyStops (back, then front) and applyColliders, in that order. */\n@compute @workgroup_size(64)\nfn clothLimit(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count || inverseMass(i) == 0.0) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let limits = paces[g].limits;\n let n = c.count;\n let at = stateAt(POSITION, i);\n var p = load(at);\n let t = skinnedAt(i, g);\n /* Only where there is a pull, as on the CPU: t + (p - t) is not p in floats. */\n if (paces[g].pullKeep < 1.0) {\n p = t + (p - t) * paces[g].pullKeep;\n }\n /* A max distance below zero is none: the CPU's Infinity, which a device may not be handed. */\n let maxDistance = statics[n + i];\n if ((limits & 1u) != 0u && maxDistance >= 0.0) {\n let limit = maxDistance * paces[g].maxDistanceScale;\n let d = p - t;\n let d2 = dot(d, d);\n if (d2 > limit * limit) {\n p = t + d * (limit / sqrt(d2));\n }\n }\n let normal = poseOf(6u * n + i * 3u);\n if ((limits & 2u) != 0u) {\n let radius = statics[2u * n + i * 2u + 1u];\n if (radius > 0.0) {\n p = pushOut(p, t - normal * (statics[2u * n + i * 2u] + radius), radius);\n }\n }\n if ((limits & 4u) != 0u) {\n let radius = statics[4u * n + i * 2u + 1u];\n if (radius > 0.0) {\n p = pushOut(p, t + normal * (statics[4u * n + i * 2u] + radius), radius);\n }\n }\n let pad = statics[6u * n + i] + paces[g].margin;\n let ends = c.joints * 16u;\n let radii = ends + c.colliders * 6u;\n let first = paces[g].colliderBase;\n for (var k = first; k < first + paces[g].colliders; k++) {\n let a = vec3<f32>(frame[ends + k * 6u], frame[ends + k * 6u + 1u], frame[ends + k * 6u + 2u]);\n let e = vec3<f32>(frame[ends + k * 6u + 3u], frame[ends + k * 6u + 4u], frame[ends + k * 6u + 5u]) - a;\n let e2 = dot(e, e);\n var s = 0.0;\n if (e2 > 1e-18) {\n s = clamp(dot(p - a, e) / e2, 0.0, 1.0);\n }\n let r0 = frame[radii + k * 2u];\n let r1 = frame[radii + k * 2u + 1u];\n p = pushOut(p, a + e * s, r0 + (r1 - r0) * s + pad);\n }\n store(at, p);\n}\n\n/* finishParticles. */\n@compute @workgroup_size(64)\nfn clothFinish(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n if (paces[paceAt(i)].live == 0u) {\n return;\n }\n let vAt = stateAt(VELOCITY, i);\n if (inverseMass(i) == 0.0) {\n store(vAt, vec3<f32>(0.0));\n return;\n }\n store(vAt, (load(stateAt(POSITION, i)) - load(stateAt(PREVIOUS, i))) * c.invH);\n}\n\n/* SkinnedCloth.blend: after a reset, the simulation's share grows over the blend's steps. */\n@compute @workgroup_size(64)\nfn clothBlend(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n /* A blend of 0 is none, not a last step of one: see ClothDevice.step. */\n let g = paceAt(i);\n let blend = paces[g].blend;\n if (paces[g].live == 0u || blend <= 0.0) {\n return;\n }\n let t = skinnedAt(i, g);\n let at = stateAt(POSITION, i);\n store(at, t + (load(at) - t) * blend);\n let vAt = stateAt(VELOCITY, i);\n store(vAt, load(vAt) * blend);\n}\n\n@group(1) @binding(0) var output: texture_storage_2d<rgba32float, write>;\n\n/* SkinnedCloth.interpolate, into the particle texture the vertex stage reads: one garment's, whose\n particles are batch.begin to batch.end and whose record is batch.first's. */\n@compute @workgroup_size(64)\nfn clothPublish(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = batch.begin + id.x;\n if (i >= batch.end) {\n return;\n }\n let g = round.at + batch.first;\n let a = load(stateAt(START, i));\n let p = a + (load(stateAt(POSITION, i)) - a) * paces[g].alpha;\n let local = id.x;\n let width = paces[g].width;\n textureStore(output, vec2<u32>(local % width, local / width), vec4<f32>(p, 0.0));\n}\n";
@@ -87,7 +87,8 @@ struct Pace {
87
87
  colliderBase: u32,
88
88
  colliders: u32,
89
89
  width: u32,
90
- pad0: u32,
90
+ /* The share of its offset from its skinned position a particle keeps a substep: the pull's. */
91
+ pullKeep: f32,
91
92
  keep: f32,
92
93
  dragKeep: f32,
93
94
  maxDistanceScale: f32,
@@ -427,7 +428,7 @@ fn pushOut(p: vec3<f32>, centre: vec3<f32>, radius: f32) -> vec3<f32> {
427
428
  return centre + d * (radius / sqrt(d2));
428
429
  }
429
430
 
430
- /* applyMaxDistance, applyStops (back, then front) and applyColliders, in that order. */
431
+ /* applyPull, applyMaxDistance, applyStops (back, then front) and applyColliders, in that order. */
431
432
  @compute @workgroup_size(64)
432
433
  fn clothLimit(@builtin(global_invocation_id) id: vec3<u32>) {
433
434
  let i = id.x;
@@ -443,6 +444,10 @@ fn clothLimit(@builtin(global_invocation_id) id: vec3<u32>) {
443
444
  let at = stateAt(POSITION, i);
444
445
  var p = load(at);
445
446
  let t = skinnedAt(i, g);
447
+ /* Only where there is a pull, as on the CPU: t + (p - t) is not p in floats. */
448
+ if (paces[g].pullKeep < 1.0) {
449
+ p = t + (p - t) * paces[g].pullKeep;
450
+ }
446
451
  /* A max distance below zero is none: the CPU's Infinity, which a device may not be handed. */
447
452
  let maxDistance = statics[n + i];
448
453
  if ((limits & 1u) != 0u && maxDistance >= 0.0) {
@@ -0,0 +1,13 @@
1
+ /**
2
+ * A point skin posed on the device, in WGSL, hand-authored under the compute exception `AGENTS.md`
3
+ * records (2026-08-24): a compute shader has no WebGL2 twin to generate from, and `npm run wgsl`
4
+ * neither writes nor polices this file. WebGL2 poses the same skin by transform feedback, in
5
+ * `backend/webgl2/pointSkinFeedback.ts`, by the same arithmetic in the same order.
6
+ *
7
+ * One invocation a vertex: its position the sum of each weighted point moved by its joint, an
8
+ * influence of weight 0 skipped, and its normal the first joint's turn of the normal it was given,
9
+ * made unit length where it has any. Written into the mesh's own position and normal buffers, three
10
+ * floats a vertex, which every pass then draws as it draws any dynamic mesh.
11
+ */
12
+ export declare const POINT_SKIN_WORKGROUP = 64;
13
+ export declare const POINT_SKIN_WGSL = "\nstruct Params {\n count: u32,\n pad0: u32,\n pad1: u32,\n pad2: u32,\n}\n\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage, read> joints: array<mat4x4<f32>>;\n@group(0) @binding(2) var<storage, read> points: array<vec4<f32>>;\n@group(0) @binding(3) var<storage, read> named: array<vec4<u32>>;\n@group(0) @binding(4) var<storage, read> normals: array<vec4<f32>>;\n@group(0) @binding(5) var<storage, read_write> posedPositions: array<f32>;\n@group(0) @binding(6) var<storage, read_write> posedNormals: array<f32>;\n\n@compute @workgroup_size(64)\nfn posePoints(@builtin(global_invocation_id) id: vec3<u32>) {\n let v = id.x;\n if (v >= params.count) {\n return;\n }\n let j = named[v];\n var p = vec3<f32>(0.0, 0.0, 0.0);\n for (var k = 0u; k < 4u; k = k + 1u) {\n let q = points[v * 4u + k];\n if (q.w != 0.0) {\n p = p + q.w * (joints[j[k]] * vec4<f32>(q.xyz, 1.0)).xyz;\n }\n }\n let m = joints[j.x];\n let raw = mat3x3<f32>(m[0].xyz, m[1].xyz, m[2].xyz) * normals[v].xyz;\n let l = length(raw);\n var n = raw;\n if (l > 0.0) {\n n = raw / l;\n }\n posedPositions[v * 3u] = p.x;\n posedPositions[v * 3u + 1u] = p.y;\n posedPositions[v * 3u + 2u] = p.z;\n posedNormals[v * 3u] = n.x;\n posedNormals[v * 3u + 1u] = n.y;\n posedNormals[v * 3u + 2u] = n.z;\n}\n";
@@ -0,0 +1,57 @@
1
+ /**
2
+ * A point skin posed on the device, in WGSL, hand-authored under the compute exception `AGENTS.md`
3
+ * records (2026-08-24): a compute shader has no WebGL2 twin to generate from, and `npm run wgsl`
4
+ * neither writes nor polices this file. WebGL2 poses the same skin by transform feedback, in
5
+ * `backend/webgl2/pointSkinFeedback.ts`, by the same arithmetic in the same order.
6
+ *
7
+ * One invocation a vertex: its position the sum of each weighted point moved by its joint, an
8
+ * influence of weight 0 skipped, and its normal the first joint's turn of the normal it was given,
9
+ * made unit length where it has any. Written into the mesh's own position and normal buffers, three
10
+ * floats a vertex, which every pass then draws as it draws any dynamic mesh.
11
+ */
12
+ export const POINT_SKIN_WORKGROUP = 64;
13
+ export const POINT_SKIN_WGSL = /* wgsl */ `
14
+ struct Params {
15
+ count: u32,
16
+ pad0: u32,
17
+ pad1: u32,
18
+ pad2: u32,
19
+ }
20
+
21
+ @group(0) @binding(0) var<uniform> params: Params;
22
+ @group(0) @binding(1) var<storage, read> joints: array<mat4x4<f32>>;
23
+ @group(0) @binding(2) var<storage, read> points: array<vec4<f32>>;
24
+ @group(0) @binding(3) var<storage, read> named: array<vec4<u32>>;
25
+ @group(0) @binding(4) var<storage, read> normals: array<vec4<f32>>;
26
+ @group(0) @binding(5) var<storage, read_write> posedPositions: array<f32>;
27
+ @group(0) @binding(6) var<storage, read_write> posedNormals: array<f32>;
28
+
29
+ @compute @workgroup_size(${POINT_SKIN_WORKGROUP})
30
+ fn posePoints(@builtin(global_invocation_id) id: vec3<u32>) {
31
+ let v = id.x;
32
+ if (v >= params.count) {
33
+ return;
34
+ }
35
+ let j = named[v];
36
+ var p = vec3<f32>(0.0, 0.0, 0.0);
37
+ for (var k = 0u; k < 4u; k = k + 1u) {
38
+ let q = points[v * 4u + k];
39
+ if (q.w != 0.0) {
40
+ p = p + q.w * (joints[j[k]] * vec4<f32>(q.xyz, 1.0)).xyz;
41
+ }
42
+ }
43
+ let m = joints[j.x];
44
+ let raw = mat3x3<f32>(m[0].xyz, m[1].xyz, m[2].xyz) * normals[v].xyz;
45
+ let l = length(raw);
46
+ var n = raw;
47
+ if (l > 0.0) {
48
+ n = raw / l;
49
+ }
50
+ posedPositions[v * 3u] = p.x;
51
+ posedPositions[v * 3u + 1u] = p.y;
52
+ posedPositions[v * 3u + 2u] = p.z;
53
+ posedNormals[v * 3u] = n.x;
54
+ posedNormals[v * 3u + 1u] = n.y;
55
+ posedNormals[v * 3u + 2u] = n.z;
56
+ }
57
+ `;
@@ -10,7 +10,7 @@ import { PACKED_ATTRIBUTES, toFixed16, } from '../../vertexPacking.js';
10
10
  * which reads three. Every field stays a whole number of four-byte words, so a vertex does too.
11
11
  */
12
12
  /** The ways an attribute can travel other than as floats, as the key words name them. */
13
- const PACKINGS = ['snorm16', 'unorm16', 'constant'];
13
+ const PACKINGS = ['snorm16', 'unorm16', 'constant', 'stream'];
14
14
  /** The pipeline key's word for one attribute's packing. Not a prefix of any other word. */
15
15
  function word(name, packing) {
16
16
  return `${name}~${packing}`;
@@ -253,6 +253,12 @@ export declare const PARTICLE_DEPTH_UNIT: number;
253
253
  export declare const BONE_PLACES_TEXTURE_UNIT: number;
254
254
  export declare const BONE_TURNS_TEXTURE_UNIT: number;
255
255
  export declare const INSTANCE_CLOCKS_TEXTURE_UNIT: number;
256
+ /**
257
+ * A point skin's joints (`pointSkin.ts`), read by the transform-feedback program that poses it. Past
258
+ * every unit a draw binds, so a pose taken between two draws leaves nothing a draw expects to find.
259
+ * Twenty-nine.
260
+ */
261
+ export declare const POINT_SKIN_TEXTURE_UNIT: number;
256
262
  /**
257
263
  * A cookie's tile, in texels a side.
258
264
  *
@@ -254,6 +254,12 @@ export const PARTICLE_DEPTH_UNIT = PARTICLE_SPRITE_UNIT + 1;
254
254
  export const BONE_PLACES_TEXTURE_UNIT = PARTICLE_DEPTH_UNIT + 1;
255
255
  export const BONE_TURNS_TEXTURE_UNIT = BONE_PLACES_TEXTURE_UNIT + 1;
256
256
  export const INSTANCE_CLOCKS_TEXTURE_UNIT = BONE_TURNS_TEXTURE_UNIT + 1;
257
+ /**
258
+ * A point skin's joints (`pointSkin.ts`), read by the transform-feedback program that poses it. Past
259
+ * every unit a draw binds, so a pose taken between two draws leaves nothing a draw expects to find.
260
+ * Twenty-nine.
261
+ */
262
+ export const POINT_SKIN_TEXTURE_UNIT = INSTANCE_CLOCKS_TEXTURE_UNIT + 1;
257
263
  /**
258
264
  * A cookie's tile, in texels a side.
259
265
  *
@@ -17,6 +17,13 @@ export declare class Mesh {
17
17
  * this — two references, and only where a consumer asked.
18
18
  */
19
19
  private readonly deformable;
20
+ /** A dynamic mesh's position and normal buffers, for a pose written on the device; else null. */
21
+ get streams(): {
22
+ readonly positions: WebGLBuffer;
23
+ readonly normals: WebGLBuffer;
24
+ } | null;
25
+ /** The colour buffer, kept so `updateColors` can rewrite it. Null unless `dynamicColors`. */
26
+ private readonly recolourable;
20
27
  /** How many floats the position buffer holds, so an update of the wrong size is refused. */
21
28
  private readonly positionFloats;
22
29
  /**
@@ -114,8 +121,9 @@ export declare class Mesh {
114
121
  * and the default is the fast path: filling a buffer with `bufferData` in one call skips the
115
122
  * zero-fill that `bufferData(size)` then `bufferSubData` pays for, and every mesh in the
116
123
  * engine that does not need to be spread should keep skipping it.
124
+ * @param dynamicColors Colours as floats in a buffer kept for `updateColors`.
117
125
  */
118
- constructor(gl: WebGL2RenderingContext, data: MeshData, dynamic?: boolean, spread?: boolean);
126
+ constructor(gl: WebGL2RenderingContext, data: MeshData, dynamic?: boolean, spread?: boolean, dynamicColors?: boolean);
119
127
  /** Caller is responsible for having the flat program + uniforms bound. */
120
128
  draw(gl: WebGL2RenderingContext): void;
121
129
  /**
@@ -135,15 +143,21 @@ export declare class Mesh {
135
143
  /**
136
144
  * Rewrite this mesh's positions, and its normals where the caller has them.
137
145
  *
138
- * **Only the two attributes that move.** Colour, emissive and the rest describe what a surface
139
- * *is* and do not change because it bent; rewriting them would be uploading unchanged bytes
140
- * every frame. A caller with genuinely changing colour wants a second mesh or a tint.
146
+ * **Only the two attributes that move.** Emissive and the rest describe what a surface *is* and
147
+ * do not change because it bent; rewriting them would be uploading unchanged bytes every frame.
148
+ * A caller whose colours change has `updateColors`, on a mesh made `dynamicColors`.
141
149
  *
142
150
  * **The bounds are recomputed**, because everything that decides whether this is on screen
143
151
  * starts from them — a cloth that blew sideways out of its original box would be culled while
144
152
  * still visible, which is the kind of bug that only shows up at the edge of the frame.
145
153
  */
146
154
  update(gl: WebGL2RenderingContext, positions: Float32Array, normals?: Float32Array): void;
155
+ /**
156
+ * Rewrite this mesh's colours, three floats a vertex, into the buffer they have to themselves.
157
+ *
158
+ * Nothing else moves, the bounds included: a colour does not change where a vertex is.
159
+ */
160
+ updateColors(gl: WebGL2RenderingContext, colors: Float32Array): void;
147
161
  /**
148
162
  * One attribute's buffer, as floats or in its sixteen-bit fixed point (`vertexPacking.ts`), which
149
163
  * WebGL2 reads back through a normalised `SHORT` or `UNSIGNED_SHORT`: three lanes wide where
@@ -198,6 +212,6 @@ export interface IncrementalMesh {
198
212
  * the allocations. The bounds are the deliberate one — a streamer places and culls a region from
199
213
  * them before a byte of it has landed.
200
214
  */
201
- export declare function createMeshIncremental(gl: WebGL2RenderingContext, data: MeshData, dynamic?: boolean): IncrementalMesh;
215
+ export declare function createMeshIncremental(gl: WebGL2RenderingContext, data: MeshData, dynamic?: boolean, dynamicColors?: boolean): IncrementalMesh;
202
216
  /** Texture coordinates and layers interleaved as (u, v, layer), one row a vertex. */
203
217
  export declare function packUvLayers(uvs: Float32Array, layers: Float32Array): Float32Array;