@driftengine/core 4.13.1 → 4.13.2

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 (57) hide show
  1. package/CHANGELOG.json +31 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +2 -1
  4. package/dist/render/backend/api.d.ts +63 -8
  5. package/dist/render/backend/webgl2/pointSkinFeedback.d.ts +23 -0
  6. package/dist/render/backend/webgl2/pointSkinFeedback.js +159 -0
  7. package/dist/render/backend/webgl2/renderer.d.ts +21 -0
  8. package/dist/render/backend/webgl2/renderer.js +41 -2
  9. package/dist/render/backend/webgpu/buffers.d.ts +10 -2
  10. package/dist/render/backend/webgpu/buffers.js +66 -16
  11. package/dist/render/backend/webgpu/clothTextures.d.ts +6 -3
  12. package/dist/render/backend/webgpu/clothTextures.js +14 -11
  13. package/dist/render/backend/webgpu/drawCommand.js +3 -3
  14. package/dist/render/backend/webgpu/gpuClothSet.js +1 -0
  15. package/dist/render/backend/webgpu/groupShelf.d.ts +31 -0
  16. package/dist/render/backend/webgpu/groupShelf.js +54 -0
  17. package/dist/render/backend/webgpu/pointSkinPass.d.ts +27 -0
  18. package/dist/render/backend/webgpu/pointSkinPass.js +104 -0
  19. package/dist/render/backend/webgpu/renderer.d.ts +31 -7
  20. package/dist/render/backend/webgpu/renderer.js +118 -47
  21. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +1 -1
  22. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +7 -2
  23. package/dist/render/backend/webgpu/shaders/pointSkin.wgsl.d.ts +13 -0
  24. package/dist/render/backend/webgpu/shaders/pointSkin.wgsl.js +57 -0
  25. package/dist/render/backend/webgpu/vertexFormats.js +1 -1
  26. package/dist/render/lightBudget.d.ts +6 -0
  27. package/dist/render/lightBudget.js +6 -0
  28. package/dist/render/mesh.d.ts +19 -5
  29. package/dist/render/mesh.js +39 -8
  30. package/dist/render/pointSkin.d.ts +55 -0
  31. package/dist/render/pointSkin.js +121 -0
  32. package/dist/render/rendererHarness.js +6 -0
  33. package/dist/render/shader.d.ts +3 -1
  34. package/dist/render/shader.js +6 -1
  35. package/dist/render/vertexPacking.d.ts +16 -4
  36. package/dist/render/vertexPacking.js +10 -3
  37. package/package.json +6 -6
  38. package/src/index.ts +2 -0
  39. package/src/render/backend/api.ts +70 -7
  40. package/src/render/backend/webgl2/pointSkinFeedback.ts +173 -0
  41. package/src/render/backend/webgl2/renderer.ts +63 -2
  42. package/src/render/backend/webgpu/buffers.ts +90 -16
  43. package/src/render/backend/webgpu/clothTextures.ts +14 -11
  44. package/src/render/backend/webgpu/drawCommand.ts +3 -3
  45. package/src/render/backend/webgpu/gpuClothSet.ts +1 -0
  46. package/src/render/backend/webgpu/groupShelf.ts +74 -0
  47. package/src/render/backend/webgpu/pointSkinPass.ts +115 -0
  48. package/src/render/backend/webgpu/renderer.ts +150 -82
  49. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +7 -2
  50. package/src/render/backend/webgpu/shaders/pointSkin.wgsl.ts +58 -0
  51. package/src/render/backend/webgpu/vertexFormats.ts +1 -1
  52. package/src/render/lightBudget.ts +7 -0
  53. package/src/render/mesh.ts +49 -7
  54. package/src/render/pointSkin.ts +178 -0
  55. package/src/render/rendererHarness.ts +6 -0
  56. package/src/render/shader.ts +5 -0
  57. package/src/render/vertexPacking.ts +25 -5
@@ -181,6 +181,14 @@ import { GpuSceneCaptureTarget } from './sceneCaptureTarget.ts';
181
181
  import { sceneCaptureTexels } from '../../sceneCapture.ts';
182
182
  import { gpuCompressedFormats } from './compressedUpload.ts';
183
183
  import { GpuClothBinding, GpuClothParticles, clothStandIn } from './clothTextures.ts';
184
+ import { GroupShelf } from './groupShelf.ts';
185
+ import { GpuPointSkin, createPointSkinPipeline } from './pointSkinPass.ts';
186
+ import {
187
+ poseBounds,
188
+ validatePointSkin,
189
+ validatePose,
190
+ type PointSkinData,
191
+ } from '../../pointSkin.ts';
184
192
  import type { ClothViews } from './clothTextures.ts';
185
193
  import type { ComputeHost } from './computeHost.ts';
186
194
  import {
@@ -1476,15 +1484,6 @@ interface SunTintTarget {
1476
1484
  readonly mips: readonly [SnapshotMips, SnapshotMips];
1477
1485
  }
1478
1486
 
1479
- /** A skinned or morphed flat group, and what it was built with, compared by identity. */
1480
- interface SkinnedTwin {
1481
- readonly group: GPUBindGroup;
1482
- readonly palette: GPUTextureView | null;
1483
- readonly deltas: GPUTextureView | null;
1484
- readonly cloth: GPUTextureView | null;
1485
- readonly clothBinding: GPUTextureView | null;
1486
- }
1487
-
1488
1487
  export class WebGPURenderer implements RendererApi {
1489
1488
  /**
1490
1489
  * Real timestamp queries, where the adapter offers them.
@@ -1602,9 +1601,11 @@ export class WebGPURenderer implements RendererApi {
1602
1601
  * thirty-odd sections, each its own material, built one a section a frame — about ninety a frame
1603
1602
  * for two characters, reported by a consumer. A slot's map holds one entry a material its rigs
1604
1603
  * are drawn in, and is emptied wherever the unskinned groups are, whose identities it is keyed
1605
- * by.
1604
+ * by. **And by what each group was built with, on a shelf a material** (`groupShelf.ts`): a
1605
+ * garment's particles take turns between two textures and several meshes may follow one cloth,
1606
+ * so one group a material was made again draw after draw.
1606
1607
  */
1607
- private readonly skinnedGroups: (Map<GPUBindGroup, SkinnedTwin> | undefined)[] = [];
1608
+ private readonly skinnedGroups: (Map<GPUBindGroup, GroupShelf> | undefined)[] = [];
1608
1609
  /** The cloth the next skinned draws are placed by, or null. See `setCloth`. */
1609
1610
  private cloth: { binding: GpuClothBinding; particles: GpuClothParticles } | null = null;
1610
1611
  private clothStandCache: { texture: GPUTexture; views: ClothViews } | null = null;
@@ -6157,17 +6158,14 @@ export class WebGPURenderer implements RendererApi {
6157
6158
  /** The same layout plus the joint palette. See `createDepthBindGroupLayout`. */
6158
6159
  private readonly shadowSkinnedLayout: GPUBindGroupLayout;
6159
6160
  /**
6160
- * The skinned shadow bind groups, one per palette slot, cached against what can invalidate them.
6161
+ * The skinned shadow bind groups, a shelf per palette slot, each found by what it was built with.
6161
6162
  *
6162
6163
  * The same argument `flatBindGroupFor` makes, and per slot for the same reason: the group it was
6163
6164
  * built beside, and the palette view, which now differs between two characters casting into one
6164
- * bake. Comparing object identity makes a missed invalidation impossible rather than unlikely.
6165
+ * bake, and the cloth's particles and binding, which take turns. Comparing object identity makes
6166
+ * a missed invalidation impossible rather than unlikely. See `groupShelf.ts`.
6165
6167
  */
6166
- private readonly shadowSkinnedGroups: (GPUBindGroup | null)[] = [];
6167
- private readonly shadowSkinnedGroupsBeside: (GPUBindGroup | null)[] = [];
6168
- private readonly shadowSkinnedGroupsPalette: (GPUTextureView | null)[] = [];
6169
- private readonly shadowSkinnedGroupsCloth: (GPUTextureView | null)[] = [];
6170
- private readonly shadowSkinnedGroupsClothBinding: (GPUTextureView | null)[] = [];
6168
+ private readonly shadowSkinnedShelves: (GroupShelf | undefined)[] = [];
6171
6169
  /** Held from construction so the skinned group can be rebuilt without them being threaded. */
6172
6170
  private shadowBlankView: GPUTextureView | null = null;
6173
6171
  private shadowBlankSampler: GPUSampler | null = null;
@@ -6343,30 +6341,26 @@ export class WebGPURenderer implements RendererApi {
6343
6341
  */
6344
6342
  private shadowSkinnedGroup(palette: GPUTextureView, at: number, cloth: ClothViews): GPUBindGroup {
6345
6343
  const beside = this.shadowBindGroup;
6346
- if (
6347
- (this.shadowSkinnedGroups[at] ?? null) === null ||
6348
- this.shadowSkinnedGroupsBeside[at] !== beside ||
6349
- this.shadowSkinnedGroupsPalette[at] !== palette ||
6350
- this.shadowSkinnedGroupsCloth[at] !== cloth.particles ||
6351
- this.shadowSkinnedGroupsClothBinding[at] !== cloth.binding
6352
- ) {
6353
- this.shadowSkinnedGroups[at] = createDepthBindGroup(
6354
- this.surface.device,
6355
- this.shadowSkinnedLayout,
6356
- this.shadowDraws.buffer,
6357
- this.shadowPassUniforms as GPUBuffer,
6358
- this.shadowBlankView as GPUTextureView,
6359
- this.shadowBlankSampler as GPUSampler,
6360
- palette,
6361
- null,
6362
- cloth,
6363
- );
6364
- this.shadowSkinnedGroupsBeside[at] = beside;
6365
- this.shadowSkinnedGroupsPalette[at] = palette;
6366
- this.shadowSkinnedGroupsCloth[at] = cloth.particles;
6367
- this.shadowSkinnedGroupsClothBinding[at] = cloth.binding;
6344
+ let shelf = this.shadowSkinnedShelves[at];
6345
+ if (shelf === undefined) {
6346
+ shelf = new GroupShelf();
6347
+ this.shadowSkinnedShelves[at] = shelf;
6368
6348
  }
6369
- return this.shadowSkinnedGroups[at] as GPUBindGroup;
6349
+ const kept = shelf.find(beside, palette, cloth.particles, cloth.binding);
6350
+ if (kept !== null) return kept;
6351
+ const group = createDepthBindGroup(
6352
+ this.surface.device,
6353
+ this.shadowSkinnedLayout,
6354
+ this.shadowDraws.buffer,
6355
+ this.shadowPassUniforms as GPUBuffer,
6356
+ this.shadowBlankView as GPUTextureView,
6357
+ this.shadowBlankSampler as GPUSampler,
6358
+ palette,
6359
+ null,
6360
+ cloth,
6361
+ );
6362
+ shelf.keep(group, beside, palette, cloth.particles, cloth.binding);
6363
+ return group;
6370
6364
  }
6371
6365
 
6372
6366
  private scatterDepthGroup(): GPUBindGroup {
@@ -8621,7 +8615,7 @@ export class WebGPURenderer implements RendererApi {
8621
8615
  * neither the pipeline set nor the key depends on whether the geometry lands in one call or
8622
8616
  * over six frames.
8623
8617
  */
8624
- private warmFlatPipelines(data: MeshData, dynamic: boolean): string {
8618
+ private warmFlatPipelines(data: MeshData, dynamic: boolean, dynamicColors = false): string {
8625
8619
  /*
8626
8620
  * **Every optional attribute, not the two that come to mind.** `present` decides where
8627
8621
  * each attribute sits in the interleaved buffer, and `createGpuMesh` decides the same
@@ -8658,9 +8652,10 @@ export class WebGPURenderer implements RendererApi {
8658
8652
  }
8659
8653
  /*
8660
8654
  * And how each attribute travels (`vertexPacking.ts`): a field read as sixteen bits rather than
8661
- * thirty-two, or not interleaved at all, moves every field after it, so the key says so.
8655
+ * thirty-two, or not interleaved at all, moves every field after it, so the key says so. A
8656
+ * `dynamicColors` mesh's colours are one of those, as `:colors~stream`.
8662
8657
  */
8663
- const packing = vertexPackingOf(data, dynamic);
8658
+ const packing = vertexPackingOf(data, dynamic, dynamicColors);
8664
8659
  markPacking(present, packing);
8665
8660
  key += packingKey(packing);
8666
8661
  const fullKey = `${this.variant}|${key}`;
@@ -8777,8 +8772,10 @@ export class WebGPURenderer implements RendererApi {
8777
8772
  createMesh(given: MeshData, options: MeshOptions = {}): GpuMesh {
8778
8773
  /* The streams it is drawn with, before anything records which it has. See `withLightmapUvs`. */
8779
8774
  const data = withLightmapUvs(given);
8780
- const fullKey = this.warmFlatPipelines(data, options.dynamic === true);
8781
- return this.keyed(createGpuMesh(this.surface.device, data, options.dynamic === true), fullKey);
8775
+ const dynamic = options.dynamic === true;
8776
+ const dynamicColors = options.dynamicColors === true;
8777
+ const fullKey = this.warmFlatPipelines(data, dynamic, dynamicColors);
8778
+ return this.keyed(createGpuMesh(this.surface.device, data, dynamic, dynamicColors), fullKey);
8782
8779
  }
8783
8780
 
8784
8781
  /**
@@ -8792,11 +8789,14 @@ export class WebGPURenderer implements RendererApi {
8792
8789
  */
8793
8790
  createMeshIncremental(given: MeshData, options: MeshOptions = {}): IncrementalMeshHandle {
8794
8791
  const data = withLightmapUvs(given);
8795
- const fullKey = this.warmFlatPipelines(data, options.dynamic === true);
8792
+ const dynamic = options.dynamic === true;
8793
+ const dynamicColors = options.dynamicColors === true;
8794
+ const fullKey = this.warmFlatPipelines(data, dynamic, dynamicColors);
8796
8795
  const { mesh, upload } = createGpuMeshIncremental(
8797
8796
  this.surface.device,
8798
8797
  data,
8799
- options.dynamic === true,
8798
+ dynamic,
8799
+ dynamicColors,
8800
8800
  );
8801
8801
  const surface = this.surface;
8802
8802
  return {
@@ -8897,6 +8897,81 @@ export class WebGPURenderer implements RendererApi {
8897
8897
  mesh.update(this.surface.device, positions, normals, staged);
8898
8898
  }
8899
8899
 
8900
+ /** The pipeline every point skin poses with, made at the first one. See `pointSkinPass.ts`. */
8901
+ private pointSkinPipeline: GPUComputePipeline | null = null;
8902
+
8903
+ /**
8904
+ * A dynamic mesh posed on the device from weighted points in its joints' spaces. Refused in words
8905
+ * on a mesh that is not dynamic, whose positions share one buffer with the rest of each vertex.
8906
+ */
8907
+ createPointSkin(mesh: GpuMesh, data: PointSkinData): GpuPointSkin {
8908
+ if (mesh.update === null) {
8909
+ throw new Error(
8910
+ 'a point skin poses a mesh created with { dynamic: true }, whose positions and normals ' +
8911
+ 'have buffers of their own for the pose to write. Say so at `createMesh`.',
8912
+ );
8913
+ }
8914
+ validatePointSkin(data, mesh.vertexCount);
8915
+ const device = this.surface.device;
8916
+ this.pointSkinPipeline ??= createPointSkinPipeline(device);
8917
+ return new GpuPointSkin(device, this.pointSkinPipeline, mesh, data);
8918
+ }
8919
+
8920
+ /**
8921
+ * Pose a skin: `matrices` holds each joint's transform into the mesh's model space, sixteen floats,
8922
+ * column-major. Recorded into the encoder the frame's next submit leads with, behind a copy of
8923
+ * last frame's positions where a reconstruction keeps them, as `updateMesh` records its rewrite.
8924
+ */
8925
+ posePointSkin(skin: GpuPointSkin, matrices: Float32Array): void {
8926
+ validatePose(matrices, skin.reach);
8927
+ if (this.surface.lost) return;
8928
+ const mesh = skin.mesh;
8929
+ poseBounds(matrices, skin.reach, mesh.bounds);
8930
+ const encoder = this.rewriteCopies();
8931
+ const motion = mesh.motion;
8932
+ if (motion !== null && this.reconstructionWanted) {
8933
+ const frame = frameOfChange(this.moverFrame, this.inFrame);
8934
+ if (keepsPrevious(motion.changed, frame)) {
8935
+ const size = mesh.vertexBuffers[0]?.size ?? 0;
8936
+ motion.previous ??= this.surface.device.createBuffer({
8937
+ label: 'recon.dynamicPrevious',
8938
+ size,
8939
+ usage: 0x0020 | 0x0008, // VERTEX | COPY_DST
8940
+ });
8941
+ encoder.copyBufferToBuffer(mesh.vertexBuffers[0] as GPUBuffer, 0, motion.previous, 0, size);
8942
+ }
8943
+ motion.changed = frame;
8944
+ }
8945
+ skin.pose(this.surface.device, encoder, matrices);
8946
+ }
8947
+
8948
+ /**
8949
+ * **The pending rewrites go first**, since a pose recorded into them reads the skin's buffers and
8950
+ * a destroyed buffer in a submit loses the whole command buffer. Nothing else reads them, so once
8951
+ * the rewrites are submitted, which only moves them earlier, the buffers can go at once.
8952
+ */
8953
+ disposePointSkin(skin: GpuPointSkin): void {
8954
+ if (this.pendingRewrites !== null && !this.surface.lost) this.submit([]);
8955
+ skin.dispose();
8956
+ }
8957
+
8958
+ /**
8959
+ * Rewrite a mesh's colours. Refused in words on a mesh that did not ask for `dynamicColors`,
8960
+ * whose colours are interleaved with the rest of a vertex or are one value with no buffer at all.
8961
+ * See `GpuMesh.updateColors`.
8962
+ */
8963
+ updateMeshColors(mesh: GpuMesh, colors: Float32Array): void {
8964
+ if (mesh.updateColors === null) {
8965
+ throw new Error(
8966
+ 'this mesh was not created with { dynamicColors: true }, so its colours are packed with ' +
8967
+ 'the rest of each vertex, or are one value with no buffer at all, and cannot be ' +
8968
+ 'rewritten. Say so at `createMesh`: the flag gives the colours a buffer of their own.',
8969
+ );
8970
+ }
8971
+ if (this.surface.lost) return;
8972
+ mesh.updateColors(this.surface.device, colors);
8973
+ }
8974
+
8900
8975
  /**
8901
8976
  * Settle everything the frame shares: the camera, the lights and the fog.
8902
8977
  *
@@ -12391,40 +12466,33 @@ export class WebGPURenderer implements RendererApi {
12391
12466
  byMaterial = new Map();
12392
12467
  this.skinnedGroups[at] = byMaterial;
12393
12468
  }
12394
- let twin = byMaterial.get(base);
12395
- if (
12396
- twin === undefined ||
12397
- twin.palette !== palette ||
12398
- twin.deltas !== deltas ||
12399
- twin.cloth !== (cloth?.particles ?? null) ||
12400
- twin.clothBinding !== (cloth?.binding ?? null)
12401
- ) {
12402
- /* Built from the live material, with the overlay's atlas where `base` is its twin. */
12403
- this.overlayBinding = base !== this.bindGroup;
12404
- const group = createFlatBindGroup(
12405
- this.surface.device,
12406
- this.flatLayoutFor(skinned, morphed),
12407
- this.variant,
12408
- this.perDraw.buffer,
12409
- this.passRing.buffer,
12410
- this.materialStore.buffer,
12411
- this.viewRing.buffer,
12412
- this.flatTextures,
12413
- palette,
12414
- deltas,
12415
- cloth,
12416
- );
12417
- this.overlayBinding = false;
12418
- twin = {
12419
- group,
12420
- palette,
12421
- deltas,
12422
- cloth: cloth?.particles ?? null,
12423
- clothBinding: cloth?.binding ?? null,
12424
- };
12425
- byMaterial.set(base, twin);
12426
- }
12427
- return twin.group;
12469
+ let shelf = byMaterial.get(base);
12470
+ if (shelf === undefined) {
12471
+ shelf = new GroupShelf();
12472
+ byMaterial.set(base, shelf);
12473
+ }
12474
+ const particles = cloth?.particles ?? null;
12475
+ const binding = cloth?.binding ?? null;
12476
+ const kept = shelf.find(palette, deltas, particles, binding);
12477
+ if (kept !== null) return kept;
12478
+ /* Built from the live material, with the overlay's atlas where `base` is its twin. */
12479
+ this.overlayBinding = base !== this.bindGroup;
12480
+ const group = createFlatBindGroup(
12481
+ this.surface.device,
12482
+ this.flatLayoutFor(skinned, morphed),
12483
+ this.variant,
12484
+ this.perDraw.buffer,
12485
+ this.passRing.buffer,
12486
+ this.materialStore.buffer,
12487
+ this.viewRing.buffer,
12488
+ this.flatTextures,
12489
+ palette,
12490
+ deltas,
12491
+ cloth,
12492
+ );
12493
+ this.overlayBinding = false;
12494
+ shelf.keep(group, palette, deltas, particles, binding);
12495
+ return group;
12428
12496
  }
12429
12497
 
12430
12498
  /** The layout matching a pair of vertex flags. A pipeline and its bind group must agree. */
@@ -88,7 +88,8 @@ struct Pace {
88
88
  colliderBase: u32,
89
89
  colliders: u32,
90
90
  width: u32,
91
- pad0: u32,
91
+ /* The share of its offset from its skinned position a particle keeps a substep: the pull's. */
92
+ pullKeep: f32,
92
93
  keep: f32,
93
94
  dragKeep: f32,
94
95
  maxDistanceScale: f32,
@@ -428,7 +429,7 @@ fn pushOut(p: vec3<f32>, centre: vec3<f32>, radius: f32) -> vec3<f32> {
428
429
  return centre + d * (radius / sqrt(d2));
429
430
  }
430
431
 
431
- /* applyMaxDistance, applyStops (back, then front) and applyColliders, in that order. */
432
+ /* applyPull, applyMaxDistance, applyStops (back, then front) and applyColliders, in that order. */
432
433
  @compute @workgroup_size(64)
433
434
  fn clothLimit(@builtin(global_invocation_id) id: vec3<u32>) {
434
435
  let i = id.x;
@@ -444,6 +445,10 @@ fn clothLimit(@builtin(global_invocation_id) id: vec3<u32>) {
444
445
  let at = stateAt(POSITION, i);
445
446
  var p = load(at);
446
447
  let t = skinnedAt(i, g);
448
+ /* Only where there is a pull, as on the CPU: t + (p - t) is not p in floats. */
449
+ if (paces[g].pullKeep < 1.0) {
450
+ p = t + (p - t) * paces[g].pullKeep;
451
+ }
447
452
  /* A max distance below zero is none: the CPU's Infinity, which a device may not be handed. */
448
453
  let maxDistance = statics[n + i];
449
454
  if ((limits & 1u) != 0u && maxDistance >= 0.0) {
@@ -0,0 +1,58 @@
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
+
14
+ export const POINT_SKIN_WGSL = /* wgsl */ `
15
+ struct Params {
16
+ count: u32,
17
+ pad0: u32,
18
+ pad1: u32,
19
+ pad2: u32,
20
+ }
21
+
22
+ @group(0) @binding(0) var<uniform> params: Params;
23
+ @group(0) @binding(1) var<storage, read> joints: array<mat4x4<f32>>;
24
+ @group(0) @binding(2) var<storage, read> points: array<vec4<f32>>;
25
+ @group(0) @binding(3) var<storage, read> named: array<vec4<u32>>;
26
+ @group(0) @binding(4) var<storage, read> normals: array<vec4<f32>>;
27
+ @group(0) @binding(5) var<storage, read_write> posedPositions: array<f32>;
28
+ @group(0) @binding(6) var<storage, read_write> posedNormals: array<f32>;
29
+
30
+ @compute @workgroup_size(${POINT_SKIN_WORKGROUP})
31
+ fn posePoints(@builtin(global_invocation_id) id: vec3<u32>) {
32
+ let v = id.x;
33
+ if (v >= params.count) {
34
+ return;
35
+ }
36
+ let j = named[v];
37
+ var p = vec3<f32>(0.0, 0.0, 0.0);
38
+ for (var k = 0u; k < 4u; k = k + 1u) {
39
+ let q = points[v * 4u + k];
40
+ if (q.w != 0.0) {
41
+ p = p + q.w * (joints[j[k]] * vec4<f32>(q.xyz, 1.0)).xyz;
42
+ }
43
+ }
44
+ let m = joints[j.x];
45
+ let raw = mat3x3<f32>(m[0].xyz, m[1].xyz, m[2].xyz) * normals[v].xyz;
46
+ let l = length(raw);
47
+ var n = raw;
48
+ if (l > 0.0) {
49
+ n = raw / l;
50
+ }
51
+ posedPositions[v * 3u] = p.x;
52
+ posedPositions[v * 3u + 1u] = p.y;
53
+ posedPositions[v * 3u + 2u] = p.z;
54
+ posedNormals[v * 3u] = n.x;
55
+ posedNormals[v * 3u + 1u] = n.y;
56
+ posedNormals[v * 3u + 2u] = n.z;
57
+ }
58
+ `;
@@ -17,7 +17,7 @@ import {
17
17
  */
18
18
 
19
19
  /** The ways an attribute can travel other than as floats, as the key words name them. */
20
- const PACKINGS = ['snorm16', 'unorm16', 'constant'] as const;
20
+ const PACKINGS = ['snorm16', 'unorm16', 'constant', 'stream'] as const;
21
21
 
22
22
  /** The pipeline key's word for one attribute's packing. Not a prefix of any other word. */
23
23
  function word(name: string, packing: AttributePacking): string {
@@ -287,6 +287,13 @@ export const BONE_PLACES_TEXTURE_UNIT = PARTICLE_DEPTH_UNIT + 1;
287
287
  export const BONE_TURNS_TEXTURE_UNIT = BONE_PLACES_TEXTURE_UNIT + 1;
288
288
  export const INSTANCE_CLOCKS_TEXTURE_UNIT = BONE_TURNS_TEXTURE_UNIT + 1;
289
289
 
290
+ /**
291
+ * A point skin's joints (`pointSkin.ts`), read by the transform-feedback program that poses it. Past
292
+ * every unit a draw binds, so a pose taken between two draws leaves nothing a draw expects to find.
293
+ * Twenty-nine.
294
+ */
295
+ export const POINT_SKIN_TEXTURE_UNIT = INSTANCE_CLOCKS_TEXTURE_UNIT + 1;
296
+
290
297
  /**
291
298
  * A cookie's tile, in texels a side.
292
299
  *
@@ -115,6 +115,12 @@ export class Mesh {
115
115
  * this — two references, and only where a consumer asked.
116
116
  */
117
117
  private readonly deformable: { positions: WebGLBuffer; normals: WebGLBuffer } | null = null;
118
+ /** A dynamic mesh's position and normal buffers, for a pose written on the device; else null. */
119
+ get streams(): { readonly positions: WebGLBuffer; readonly normals: WebGLBuffer } | null {
120
+ return this.deformable;
121
+ }
122
+ /** The colour buffer, kept so `updateColors` can rewrite it. Null unless `dynamicColors`. */
123
+ private readonly recolourable: WebGLBuffer | null = null;
118
124
  /** How many floats the position buffer holds, so an update of the wrong size is refused. */
119
125
  private readonly positionFloats: number;
120
126
  /**
@@ -222,8 +228,15 @@ export class Mesh {
222
228
  * and the default is the fast path: filling a buffer with `bufferData` in one call skips the
223
229
  * zero-fill that `bufferData(size)` then `bufferSubData` pays for, and every mesh in the
224
230
  * engine that does not need to be spread should keep skipping it.
231
+ * @param dynamicColors Colours as floats in a buffer kept for `updateColors`.
225
232
  */
226
- constructor(gl: WebGL2RenderingContext, data: MeshData, dynamic = false, spread = false) {
233
+ constructor(
234
+ gl: WebGL2RenderingContext,
235
+ data: MeshData,
236
+ dynamic = false,
237
+ spread = false,
238
+ dynamicColors = false,
239
+ ) {
227
240
  validateMeshData(data);
228
241
  this.spread = spread;
229
242
  this.hasUvs = data.uvs !== undefined;
@@ -243,11 +256,15 @@ export class Mesh {
243
256
  */
244
257
  const usage = dynamic ? gl.DYNAMIC_DRAW : gl.STATIC_DRAW;
245
258
  /* How each attribute travels: sixteen-bit fixed point, or one value as a constant. */
246
- const packing = vertexPackingOf(data, dynamic);
259
+ const packing = vertexPackingOf(data, dynamic, dynamicColors);
247
260
  const positions = this.attachAttribute(gl, ATTR_POSITION, data.positions, 3, usage);
248
261
  const normals = this.attachAttribute(gl, ATTR_NORMAL, data.normals, 3, usage, packing.normals);
249
262
  if (dynamic) this.deformable = { positions, normals };
250
- this.attachShared(gl, ATTR_COLOR, data.colors, 3, packing.colors);
263
+ if (packing.colors === 'stream') {
264
+ this.recolourable = this.attachAttribute(gl, ATTR_COLOR, data.colors, 3, gl.DYNAMIC_DRAW);
265
+ } else {
266
+ this.attachShared(gl, ATTR_COLOR, data.colors, 3, packing.colors);
267
+ }
251
268
  this.attachShared(gl, ATTR_EMISSIVE, data.emissive, 1, packing.emissive);
252
269
  /*
253
270
  * Absent means "not shiny", supplied as a constant rather than as a buffer of
@@ -463,9 +480,9 @@ export class Mesh {
463
480
  /**
464
481
  * Rewrite this mesh's positions, and its normals where the caller has them.
465
482
  *
466
- * **Only the two attributes that move.** Colour, emissive and the rest describe what a surface
467
- * *is* and do not change because it bent; rewriting them would be uploading unchanged bytes
468
- * every frame. A caller with genuinely changing colour wants a second mesh or a tint.
483
+ * **Only the two attributes that move.** Emissive and the rest describe what a surface *is* and
484
+ * do not change because it bent; rewriting them would be uploading unchanged bytes every frame.
485
+ * A caller whose colours change has `updateColors`, on a mesh made `dynamicColors`.
469
486
  *
470
487
  * **The bounds are recomputed**, because everything that decides whether this is on screen
471
488
  * starts from them — a cloth that blew sideways out of its original box would be culled while
@@ -495,6 +512,30 @@ export class Mesh {
495
512
  }
496
513
  }
497
514
 
515
+ /**
516
+ * Rewrite this mesh's colours, three floats a vertex, into the buffer they have to themselves.
517
+ *
518
+ * Nothing else moves, the bounds included: a colour does not change where a vertex is.
519
+ */
520
+ updateColors(gl: WebGL2RenderingContext, colors: Float32Array): void {
521
+ if (this.recolourable === null) {
522
+ throw new Error(
523
+ 'this mesh was not created with { dynamicColors: true }, so its colours are packed or are ' +
524
+ 'one value with no buffer at all, and cannot be rewritten. Say so at `createMesh`: the ' +
525
+ 'flag gives the colours a buffer of their own.',
526
+ );
527
+ }
528
+ if (colors.length !== this.positionFloats) {
529
+ throw new Error(
530
+ `this mesh has ${this.positionFloats / 3} vertices and the colours have ` +
531
+ `${colors.length / 3}. A mesh's vertex count is fixed at creation: the index buffer, ` +
532
+ 'the pipeline and every other attribute are sized against it.',
533
+ );
534
+ }
535
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.recolourable);
536
+ gl.bufferSubData(gl.ARRAY_BUFFER, 0, colors);
537
+ }
538
+
498
539
  /**
499
540
  * One attribute's buffer, as floats or in its sixteen-bit fixed point (`vertexPacking.ts`), which
500
541
  * WebGL2 reads back through a normalised `SHORT` or `UNSIGNED_SHORT`: three lanes wide where
@@ -638,8 +679,9 @@ export function createMeshIncremental(
638
679
  gl: WebGL2RenderingContext,
639
680
  data: MeshData,
640
681
  dynamic = false,
682
+ dynamicColors = false,
641
683
  ): IncrementalMesh {
642
- const mesh = new Mesh(gl, data, dynamic, true);
684
+ const mesh = new Mesh(gl, data, dynamic, true, dynamicColors);
643
685
  return { mesh, upload: mesh.uploads(gl) };
644
686
  }
645
687