@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
@@ -0,0 +1,55 @@
1
+ /**
2
+ * A mesh posed on the device from weighted points in its joints' own spaces: the data, its checks,
3
+ * and the bounds a pose is culled by. Each backend's binding poses the vertices.
4
+ *
5
+ * **What it is for.** Some formats store a vertex as up to four points, each in the space of a
6
+ * joint, with a weight each and a normal in the space of the first: posed, the vertex is the
7
+ * weighted sum of each point moved by its joint. That is not ordinary skinning, which moves one
8
+ * bind-pose position by a blend of joints, so a rig of that kind had to be posed on the main thread
9
+ * and its positions uploaded every frame. Reported at 38% of the main thread's drawing time.
10
+ * Posed on the device into a dynamic mesh's own buffers, the main thread hands over the joints.
11
+ *
12
+ * **Into a dynamic mesh rather than in the vertex stage**, because a skinned vertex stage has no
13
+ * attribute left for four more points, and every pass that draws a mesh would need a variant that
14
+ * reads them: lit, cast, glass, motion. Posed once into the mesh's positions and normals, it is an
15
+ * ordinary dynamic mesh to every one of them, motion vectors included. What it costs is the mesh's
16
+ * two buffers, written a pose, and a pass that runs before the draws.
17
+ *
18
+ * **The bounds are a box of spheres, not the posed vertices**, which the CPU never sees. Each joint
19
+ * reaches as far as the farthest point a weighted influence holds in its space, scaled by its
20
+ * largest axis; a weighted sum of points inside those spheres lies inside their box, scaled by the
21
+ * least and greatest sums of a vertex's weights. Conservative, so it culls nothing visible; loose
22
+ * by the joints' reach where a mesh is long and thin, which culls a little less than it could.
23
+ */
24
+ import type { Bounds } from '../math/bounds.ts';
25
+ /** A point skin, one entry a vertex unless said otherwise. */
26
+ export interface PointSkinData {
27
+ /** Four joint indices a vertex, as floats, as `MeshData.joints` is. */
28
+ readonly joints: Float32Array;
29
+ /** Four weights a vertex. An influence of weight 0 is skipped, whatever its joint and point. */
30
+ readonly weights: Float32Array;
31
+ /** Four points a vertex, three floats each: influence `k`'s point in joint `k`'s own space. */
32
+ readonly points: Float32Array;
33
+ /** One normal a vertex, in the space of its first joint. */
34
+ readonly normals: Float32Array;
35
+ }
36
+ /** How far a skin reaches from each joint, and what its weights sum to: what a pose's bounds need. */
37
+ export interface PointSkinReach {
38
+ /** Joints a pose must hand over: one past the highest any vertex names and uses. */
39
+ readonly joints: number;
40
+ /** For each joint, the farthest point a weighted influence holds in its space; -1 where none. */
41
+ readonly reach: Float32Array;
42
+ /** The least and the greatest sum of one vertex's weights. */
43
+ readonly sums: readonly [number, number];
44
+ }
45
+ /** Refuse a skin whose arrays disagree with a mesh of `vertices`, or that names a joint past the rest. */
46
+ export declare function validatePointSkin(data: PointSkinData, vertices: number): void;
47
+ /** Measure a skin's reach once, at creation. */
48
+ export declare function reachOf(data: PointSkinData): PointSkinReach;
49
+ /**
50
+ * The bounds of a pose, into the mesh's own: `matrices` holds each joint's transform into the
51
+ * mesh's model space, sixteen floats, column-major. Allocates nothing.
52
+ */
53
+ export declare function poseBounds(matrices: Float32Array, skin: PointSkinReach, out: Bounds): void;
54
+ /** Refuse a pose that hands over fewer joints than the skin names. */
55
+ export declare function validatePose(matrices: Float32Array, skin: PointSkinReach): void;
@@ -0,0 +1,121 @@
1
+ import { MAX_JOINTS } from './skinPalette.js';
2
+ /** Refuse a skin whose arrays disagree with a mesh of `vertices`, or that names a joint past the rest. */
3
+ export function validatePointSkin(data, vertices) {
4
+ const fail = (what) => {
5
+ throw new Error(`point skin: ${what}`);
6
+ };
7
+ for (const [name, values, width] of [
8
+ ['joints', data.joints, 4],
9
+ ['weights', data.weights, 4],
10
+ ['points', data.points, 12],
11
+ ['normals', data.normals, 3],
12
+ ]) {
13
+ if (values.length !== vertices * width) {
14
+ fail(`${name} holds ${values.length / width} vertices and the mesh has ${vertices}`);
15
+ }
16
+ }
17
+ for (let at = 0; at < data.joints.length; at++) {
18
+ const joint = data.joints[at];
19
+ if (!(Number.isInteger(joint) && joint >= 0 && joint < MAX_JOINTS)) {
20
+ fail(`vertex ${Math.floor(at / 4)} names joint ${joint}; a joint is 0 to ${MAX_JOINTS - 1}`);
21
+ }
22
+ }
23
+ }
24
+ /** Measure a skin's reach once, at creation. */
25
+ export function reachOf(data) {
26
+ const reach = new Float32Array(MAX_JOINTS).fill(-1);
27
+ let joints = 0;
28
+ let least = Infinity;
29
+ let most = -Infinity;
30
+ const vertices = data.weights.length / 4;
31
+ for (let v = 0; v < vertices; v++) {
32
+ /* The first joint carries the normal, so it is named whatever its weight. */
33
+ joints = Math.max(joints, data.joints[v * 4] + 1);
34
+ let sum = 0;
35
+ for (let k = 0; k < 4; k++) {
36
+ const weight = data.weights[v * 4 + k];
37
+ if (weight === 0)
38
+ continue;
39
+ sum += weight;
40
+ const joint = data.joints[v * 4 + k];
41
+ joints = Math.max(joints, joint + 1);
42
+ const at = (v * 4 + k) * 3;
43
+ const x = data.points[at];
44
+ const y = data.points[at + 1];
45
+ const z = data.points[at + 2];
46
+ reach[joint] = Math.max(reach[joint], Math.sqrt(x * x + y * y + z * z));
47
+ }
48
+ least = Math.min(least, sum);
49
+ most = Math.max(most, sum);
50
+ }
51
+ return {
52
+ joints,
53
+ reach: reach.slice(0, joints),
54
+ sums: vertices === 0 ? [1, 1] : [least, most],
55
+ };
56
+ }
57
+ /**
58
+ * The bounds of a pose, into the mesh's own: `matrices` holds each joint's transform into the
59
+ * mesh's model space, sixteen floats, column-major. Allocates nothing.
60
+ */
61
+ export function poseBounds(matrices, skin, out) {
62
+ let minX = Infinity;
63
+ let minY = Infinity;
64
+ let minZ = Infinity;
65
+ let maxX = -Infinity;
66
+ let maxY = -Infinity;
67
+ let maxZ = -Infinity;
68
+ for (let j = 0; j < skin.joints; j++) {
69
+ const reach = skin.reach[j];
70
+ if (reach < 0)
71
+ continue;
72
+ const m = j * 16;
73
+ const scale = Math.sqrt(Math.max(columnLength2(matrices, m), columnLength2(matrices, m + 4), columnLength2(matrices, m + 8)));
74
+ const r = reach * scale;
75
+ const x = matrices[m + 12];
76
+ const y = matrices[m + 13];
77
+ const z = matrices[m + 14];
78
+ minX = Math.min(minX, x - r);
79
+ minY = Math.min(minY, y - r);
80
+ minZ = Math.min(minZ, z - r);
81
+ maxX = Math.max(maxX, x + r);
82
+ maxY = Math.max(maxY, y + r);
83
+ maxZ = Math.max(maxZ, z + r);
84
+ }
85
+ if (minX > maxX) {
86
+ out.min.fill(0);
87
+ out.max.fill(0);
88
+ out.centre.fill(0);
89
+ out.radius = 0;
90
+ return;
91
+ }
92
+ /* A sum of weights other than one scales the box about the origin, by the least and the most. */
93
+ const [least, most] = skin.sums;
94
+ out.min[0] = Math.min(least * minX, most * minX);
95
+ out.min[1] = Math.min(least * minY, most * minY);
96
+ out.min[2] = Math.min(least * minZ, most * minZ);
97
+ out.max[0] = Math.max(least * maxX, most * maxX);
98
+ out.max[1] = Math.max(least * maxY, most * maxY);
99
+ out.max[2] = Math.max(least * maxZ, most * maxZ);
100
+ let radius2 = 0;
101
+ for (let axis = 0; axis < 3; axis++) {
102
+ const centre = (out.min[axis] + out.max[axis]) / 2;
103
+ out.centre[axis] = centre;
104
+ const half = out.max[axis] - centre;
105
+ radius2 += half * half;
106
+ }
107
+ out.radius = Math.sqrt(radius2);
108
+ }
109
+ /** The squared length of one column's first three components. */
110
+ function columnLength2(m, at) {
111
+ const x = m[at];
112
+ const y = m[at + 1];
113
+ const z = m[at + 2];
114
+ return x * x + y * y + z * z;
115
+ }
116
+ /** Refuse a pose that hands over fewer joints than the skin names. */
117
+ export function validatePose(matrices, skin) {
118
+ if (matrices.length % 16 !== 0 || matrices.length / 16 < skin.joints) {
119
+ throw new Error(`point skin: a pose of ${matrices.length / 16} joints for a skin that names ${skin.joints}`);
120
+ }
121
+ }
@@ -77,6 +77,12 @@ export function recordingGl(options = {}) {
77
77
  ONE: 1,
78
78
  ONE_MINUS_SRC_ALPHA: 0x0303,
79
79
  TRIANGLES: 0x0004,
80
+ /* Transform feedback's, so a point skin's pose can be seen to discard, capture and undo both. */
81
+ POINTS: 0x0000,
82
+ RASTERIZER_DISCARD: 0x8c89,
83
+ TRANSFORM_FEEDBACK: 0x8e22,
84
+ TRANSFORM_FEEDBACK_BUFFER: 0x8c8e,
85
+ SEPARATE_ATTRIBS: 0x8c8d,
80
86
  /* The winding a pass names, so a test can tell which one it chose. */
81
87
  CW: 0x0900,
82
88
  CCW: 0x0901,
@@ -1,5 +1,7 @@
1
1
  /** Shader compilation with fail-fast diagnostics (init-time only, never per-frame). */
2
- export declare function compileProgram(gl: WebGL2RenderingContext, vertexSource: string, fragmentSource: string, label: string): WebGLProgram;
2
+ export declare function compileProgram(gl: WebGL2RenderingContext, vertexSource: string, fragmentSource: string, label: string,
3
+ /** Vertex outputs captured by transform feedback, one buffer each; named before the link. */
4
+ feedback?: readonly string[]): WebGLProgram;
3
5
  /**
4
6
  * A program compiled and linked without asking whether that worked, since asking waits for the
5
7
  * driver to finish: for a context offering `KHR_parallel_shader_compile`, whose
@@ -1,5 +1,7 @@
1
1
  /** Shader compilation with fail-fast diagnostics (init-time only, never per-frame). */
2
- export function compileProgram(gl, vertexSource, fragmentSource, label) {
2
+ export function compileProgram(gl, vertexSource, fragmentSource, label,
3
+ /** Vertex outputs captured by transform feedback, one buffer each; named before the link. */
4
+ feedback) {
3
5
  const vs = compileStage(gl, gl.VERTEX_SHADER, vertexSource, `${label}.vert`);
4
6
  const fs = compileStage(gl, gl.FRAGMENT_SHADER, fragmentSource, `${label}.frag`);
5
7
  const program = gl.createProgram();
@@ -7,6 +9,9 @@ export function compileProgram(gl, vertexSource, fragmentSource, label) {
7
9
  throw new Error(`[${label}] createProgram failed`);
8
10
  gl.attachShader(program, vs);
9
11
  gl.attachShader(program, fs);
12
+ if (feedback !== undefined) {
13
+ gl.transformFeedbackVaryings(program, feedback, gl.SEPARATE_ATTRIBS);
14
+ }
10
15
  gl.linkProgram(program);
11
16
  gl.deleteShader(vs);
12
17
  gl.deleteShader(fs);
@@ -22,15 +22,27 @@ import type { MeshData } from '@driftengine/drft';
22
22
  * on the CPU. What would make this wrong is a value outside its range, which the range check sends
23
23
  * to floats rather than clamping, or a shader reading an attribute as something other than the
24
24
  * value it carries.
25
+ *
26
+ * **A mesh whose colours are rewritten keeps them as floats in a buffer of their own** (`stream`),
27
+ * whatever they are: one value every vertex shares has no buffer to rewrite, sixteen bits would
28
+ * have to be packed on the CPU every rewrite, and colours interleaved with the rest of a vertex
29
+ * could only be rewritten by keeping the rows, which a mesh lets go once it is uploaded. It costs
30
+ * twelve bytes a vertex where the colours might have cost none, which is why a mesh asks for it.
31
+ */
32
+ /**
33
+ * Floats; sixteen-bit fixed point, signed or not; one value for every vertex; or floats in a
34
+ * buffer of their own, for an attribute a caller rewrites.
25
35
  */
26
- /** Floats; sixteen-bit fixed point, signed or not; or one value for every vertex. */
27
- export type AttributePacking = 'float' | 'snorm16' | 'unorm16' | 'constant';
36
+ export type AttributePacking = 'float' | 'snorm16' | 'unorm16' | 'constant' | 'stream';
28
37
  /** The attributes this decides for. Every other one travels as floats. */
29
38
  export declare const PACKED_ATTRIBUTES: readonly ['normals', 'tangents', 'colors', 'emissive', 'weights', 'weights2'];
30
39
  export type PackedAttribute = (typeof PACKED_ATTRIBUTES)[number];
31
40
  export type VertexPacking = Readonly<Record<PackedAttribute, AttributePacking>>;
32
- /** The choice for this mesh. `dynamic` is a mesh whose positions and normals are rewritten. */
33
- export declare function vertexPackingOf(data: MeshData, dynamic: boolean): VertexPacking;
41
+ /**
42
+ * The choice for this mesh. `dynamic` is a mesh whose positions and normals are rewritten, and
43
+ * `dynamicColors` one whose colours are.
44
+ */
45
+ export declare function vertexPackingOf(data: MeshData, dynamic: boolean, dynamicColors?: boolean): VertexPacking;
34
46
  /**
35
47
  * What 1 is in each fixed point, the values WebGPU's `snorm16` and `unorm16` and WebGL2's
36
48
  * normalised `SHORT` and `UNSIGNED_SHORT` divide by. A value is stored rounded to nearest, so 1, 0
@@ -7,12 +7,19 @@ export const PACKED_ATTRIBUTES = [
7
7
  'weights',
8
8
  'weights2',
9
9
  ];
10
- /** The choice for this mesh. `dynamic` is a mesh whose positions and normals are rewritten. */
11
- export function vertexPackingOf(data, dynamic) {
10
+ /**
11
+ * The choice for this mesh. `dynamic` is a mesh whose positions and normals are rewritten, and
12
+ * `dynamicColors` one whose colours are.
13
+ */
14
+ export function vertexPackingOf(data, dynamic, dynamicColors = false) {
12
15
  return {
13
16
  normals: dynamic ? 'float' : fixedOrFloat(data.normals, -1),
14
17
  tangents: fixedOrFloat(data.tangents, -1),
15
- colors: shared(data.colors, 3) ? 'constant' : fixedOrFloat(data.colors, 0),
18
+ colors: dynamicColors
19
+ ? 'stream'
20
+ : shared(data.colors, 3)
21
+ ? 'constant'
22
+ : fixedOrFloat(data.colors, 0),
16
23
  emissive: shared(data.emissive, 1) ? 'constant' : 'float',
17
24
  weights: fixedOrFloat(data.weights, 0),
18
25
  weights2: fixedOrFloat(data.weights2, 0),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/core",
3
- "version": "4.13.1",
3
+ "version": "4.13.2",
4
4
  "description": "The runtime of DriftEngine, a WebGPU game engine with a WebGL2 fallback: renderer, loop, input, cameras, scene graph and physics",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -29,8 +29,8 @@
29
29
  ],
30
30
  "sideEffects": false,
31
31
  "dependencies": {
32
- "@driftengine/drft": "4.13.1",
33
- "@driftengine/physics": "4.13.1",
32
+ "@driftengine/drft": "4.13.2",
33
+ "@driftengine/physics": "4.13.2",
34
34
  "gl-matrix": "^3.4.3"
35
35
  },
36
36
  "author": "Drift Technologies",
@@ -67,8 +67,8 @@
67
67
  "prepack": "node ../../scripts/build.mjs @driftengine/core"
68
68
  },
69
69
  "devDependencies": {
70
- "@driftengine/nav": "4.13.1",
71
- "@driftengine/network": "4.13.1",
72
- "@driftengine/texture": "4.13.1"
70
+ "@driftengine/nav": "4.13.2",
71
+ "@driftengine/network": "4.13.2",
72
+ "@driftengine/texture": "4.13.2"
73
73
  }
74
74
  }
package/src/index.ts CHANGED
@@ -483,6 +483,7 @@ export type {
483
483
  BoltHandle,
484
484
  CausticsHandle,
485
485
  ClothBindingHandle,
486
+ PointSkinHandle,
486
487
  ClothParticlesHandle,
487
488
  FlockHandle,
488
489
  IncrementalMeshHandle,
@@ -503,6 +504,7 @@ export type {
503
504
  WindStreakHandle,
504
505
  } from './render/backend/api.ts';
505
506
  export type { ClothBindingData } from './render/clothBindingData.ts';
507
+ export type { PointSkinData } from './render/pointSkin.ts';
506
508
  /*
507
509
  * A material's shading model, for what the standard one is not: brushed metal, hair, skin, an eye.
508
510
  * A frozen descriptor made by a factory and set as `SurfaceMaterial.model`; each is a pipeline of
@@ -12,6 +12,7 @@ import type { CompressedTextureSource, SurfaceSource } from '../compressedSource
12
12
  import type { LightmapPage } from '../lightmap.ts';
13
13
  import type { DeviceParticles } from '../particlePool.ts';
14
14
  import type { ClothBindingData } from '../clothBindingData.ts';
15
+ import type { PointSkinData } from '../pointSkin.ts';
15
16
  import type { TextStyle } from '../textLayout.ts';
16
17
  import type { SdfFont } from '../sdfFont.ts';
17
18
  import type { SdfTextStyle } from '../sdfTextLayout.ts';
@@ -64,6 +65,15 @@ export interface ClothBindingHandle {
64
65
  /* Deliberately empty, for the weak-type reason `MeshHandle` explains. */
65
66
  }
66
67
 
68
+ /**
69
+ * A dynamic mesh's point skin on the device, from `createPointSkin`: its vertices as weighted points
70
+ * in its joints' spaces. Opaque likewise.
71
+ */
72
+ /* eslint-disable-next-line @typescript-eslint/no-empty-object-type -- opaque by design */
73
+ export interface PointSkinHandle {
74
+ /* Deliberately empty, for the weak-type reason `MeshHandle` explains. */
75
+ }
76
+
67
77
  /** One character's cloth particles on the device, from `createClothParticles`. Opaque likewise. */
68
78
  /* eslint-disable-next-line @typescript-eslint/no-empty-object-type -- opaque by design */
69
79
  export interface ClothParticlesHandle {
@@ -249,11 +259,20 @@ export interface MeshOptions {
249
259
  /**
250
260
  * Whether this mesh's positions will be rewritten with `updateMesh`.
251
261
  *
252
- * Costs a `DYNAMIC_DRAW` hint on WebGL2 and a CPU-side copy of the interleaved vertex data on
253
- * WebGPU, which interleaves every attribute into one buffer and so cannot patch a position
254
- * without the rest of the vertex in hand. Both are paid only where a consumer asks.
262
+ * Costs a `DYNAMIC_DRAW` hint on WebGL2, and on WebGPU two buffers of its own for the positions
263
+ * and the normals beside the rest of the vertex, bound with it on every draw, and floats for the
264
+ * normals where they might have been sixteen bits. Paid only where a consumer asks.
255
265
  */
256
266
  readonly dynamic?: boolean;
267
+ /**
268
+ * Whether this mesh's colours will be rewritten with `updateMeshColors`.
269
+ *
270
+ * Costs the colours as floats in a buffer of their own, twelve bytes a vertex where they might
271
+ * have been sixteen bits or, for one colour every vertex shares, nothing at all. Independent of
272
+ * `dynamic`: a surface shaded again as a camera moves rewrites its colours every frame and its
273
+ * positions never, and should pay for neither the positions' buffers nor their motion.
274
+ */
275
+ readonly dynamicColors?: boolean;
257
276
  }
258
277
 
259
278
  export type RendererApi = Omit<
@@ -262,6 +281,7 @@ export type RendererApi = Omit<
262
281
  | 'createMesh'
263
282
  | 'createMeshIncremental'
264
283
  | 'updateMesh'
284
+ | 'updateMeshColors'
265
285
  | 'drawMesh'
266
286
  | 'disposeMesh'
267
287
  | 'drawTranslucentMesh'
@@ -330,6 +350,9 @@ export type RendererApi = Omit<
330
350
  | 'setCloth'
331
351
  | 'disposeClothBinding'
332
352
  | 'disposeClothParticles'
353
+ | 'createPointSkin'
354
+ | 'posePointSkin'
355
+ | 'disposePointSkin'
333
356
  > & {
334
357
  /**
335
358
  * A mesh's cloth binding: which simulation triangle each of its vertices follows, where on it and
@@ -354,6 +377,29 @@ export type RendererApi = Omit<
354
377
  setCloth(binding: ClothBindingHandle | null, particles?: ClothParticlesHandle | null): void;
355
378
  disposeClothBinding(binding: ClothBindingHandle): void;
356
379
  disposeClothParticles(particles: ClothParticlesHandle): void;
380
+ /**
381
+ * A dynamic mesh posed on the device from weighted points in its joints' own spaces
382
+ * (`PointSkinData`): each vertex the weighted sum of up to four points, each moved by its joint,
383
+ * and a normal turned by its first joint. Posed into the mesh's own positions and normals, so
384
+ * every pass draws it as any dynamic mesh, motion vectors included. See `pointSkin.ts`.
385
+ *
386
+ * **For a rig whose vertices are stored that way**, which ordinary skinning cannot pose: it moves
387
+ * one bind-pose position by a blend of joints. Such a rig was posed on the main thread and
388
+ * uploaded every frame, reported at 38% of the main thread's drawing time.
389
+ *
390
+ * The mesh must be `{ dynamic: true }` and have the skin's vertex count; any other is refused in
391
+ * words, as is a joint past 511.
392
+ */
393
+ createPointSkin(mesh: MeshHandle, data: PointSkinData): PointSkinHandle;
394
+ /**
395
+ * Pose a skin: `matrices` holds each joint's transform into the mesh's model space, sixteen floats
396
+ * a joint, column-major, at least as many joints as the skin names. **Once a frame, before the
397
+ * mesh's draws**: a second pose in a frame reaches every draw of it on WebGPU, as a second
398
+ * `updateMesh` does. The mesh's bounds become a box around every joint's reach, so it is culled
399
+ * as it moves without the posed vertices ever coming back to the CPU.
400
+ */
401
+ posePointSkin(skin: PointSkinHandle, matrices: Float32Array): void;
402
+ disposePointSkin(skin: PointSkinHandle): void;
357
403
  /**
358
404
  * What the following draws lay over their surfaces beyond their materials — a rim of light, a
359
405
  * dissolve with a glowing edge, wrinkle normals by region — or null for none, which every pass
@@ -928,10 +974,12 @@ export type RendererApi = Omit<
928
974
  * quads with a matrix each, which caps the resolution at the number of draw calls a consumer
929
975
  * can afford. A consumer found it by trying to hang a banner.
930
976
  *
931
- * **Only positions and normals.** Colour, emissive and the rest describe what a surface *is*
932
- * and do not change because it bent; rewriting them every frame would be uploading unchanged
933
- * bytes. The vertex *count* is fixed: the index buffer, the pipeline and every other attribute
934
- * are sized against it, and an update of a different size is refused naming both.
977
+ * **Only positions and normals.** Emissive and the rest describe what a surface *is* and do not
978
+ * change because it bent; rewriting them every frame would be uploading unchanged bytes. Colour
979
+ * is the exception a consumer found, a surface shaded again every frame without moving, and it
980
+ * has `updateMeshColors` of its own rather than a place here, so a rewrite of either sends only
981
+ * what changed. The vertex *count* is fixed: the index buffer, the pipeline and every other
982
+ * attribute are sized against it, and an update of a different size is refused naming both.
935
983
  *
936
984
  * The mesh must have been created with `{ dynamic: true }`. That is not ceremony — it decides
937
985
  * the buffer hint on one backend and what has to be kept on the other, and a mesh that never
@@ -939,6 +987,21 @@ export type RendererApi = Omit<
939
987
  */
940
988
  updateMesh(mesh: MeshHandle, positions: Float32Array, normals?: Float32Array): void;
941
989
 
990
+ /**
991
+ * Rewrite a mesh's colours, three floats a vertex, linear as `MeshData.colors` is.
992
+ *
993
+ * **For a surface whose colour changes while it stays where it is**: ground shaded with its
994
+ * distance from a moving camera, a heat map, a tile lit by a game's own rules. Without it such a
995
+ * surface is made again as a new mesh every frame, which is a buffer allocated and let go each
996
+ * frame on both backends and every pipeline it draws with looked up again.
997
+ *
998
+ * The mesh must have been created with `{ dynamicColors: true }`, which gives the colours a buffer
999
+ * of their own; any other is refused in words. Nothing else moves: not the positions, not the
1000
+ * bounds, and no motion vector, since a colour does not move. A different vertex count is refused
1001
+ * naming both.
1002
+ */
1003
+ updateMeshColors(mesh: MeshHandle, colors: Float32Array): void;
1004
+
942
1005
  /** Draw a handle this renderer made. Passing one from the other backend is a caller error. */
943
1006
  drawMesh(
944
1007
  mesh: MeshHandle,
@@ -0,0 +1,173 @@
1
+ /**
2
+ * A point skin on WebGL2: posed by transform feedback, a vertex stage run once a vertex with the
3
+ * rasteriser off, its two outputs captured into the dynamic mesh's own position and normal buffers.
4
+ * What a point skin is, and why it poses into a mesh, is `pointSkin.ts`; the same arithmetic in the
5
+ * same order is `backend/webgpu/shaders/pointSkin.wgsl.ts`.
6
+ *
7
+ * **Every binding it makes it undoes**, because it runs between a frame's draws: the program, the
8
+ * vertex array, the joint texture on its own unit, the feedback buffers and the discard. A buffer
9
+ * left bound for feedback while a draw reads it as vertices is an error WebGL2 raises at the draw.
10
+ */
11
+ import { POINT_SKIN_TEXTURE_UNIT } from '../../lightBudget.ts';
12
+ import type { Mesh } from '../../mesh.ts';
13
+ import { reachOf, type PointSkinData, type PointSkinReach } from '../../pointSkin.ts';
14
+ import { compileProgram } from '../../shader.ts';
15
+
16
+ const POSE_VERT = /* glsl */ `#version 300 es
17
+ precision highp float;
18
+ precision highp int;
19
+ layout(location = 0) in vec4 aNamed;
20
+ layout(location = 1) in vec4 aWeights;
21
+ layout(location = 2) in vec3 aPoint0;
22
+ layout(location = 3) in vec3 aPoint1;
23
+ layout(location = 4) in vec3 aPoint2;
24
+ layout(location = 5) in vec3 aPoint3;
25
+ layout(location = 6) in vec3 aNormal;
26
+ uniform highp sampler2D uJoints;
27
+ out vec3 vPosition;
28
+ out vec3 vNormal;
29
+
30
+ mat4 jointAt(float joint) {
31
+ int at = int(joint) * 4;
32
+ return mat4(
33
+ texelFetch(uJoints, ivec2(at, 0), 0),
34
+ texelFetch(uJoints, ivec2(at + 1, 0), 0),
35
+ texelFetch(uJoints, ivec2(at + 2, 0), 0),
36
+ texelFetch(uJoints, ivec2(at + 3, 0), 0)
37
+ );
38
+ }
39
+
40
+ void main() {
41
+ vec3 p = vec3(0.0);
42
+ if (aWeights.x != 0.0) p += aWeights.x * (jointAt(aNamed.x) * vec4(aPoint0, 1.0)).xyz;
43
+ if (aWeights.y != 0.0) p += aWeights.y * (jointAt(aNamed.y) * vec4(aPoint1, 1.0)).xyz;
44
+ if (aWeights.z != 0.0) p += aWeights.z * (jointAt(aNamed.z) * vec4(aPoint2, 1.0)).xyz;
45
+ if (aWeights.w != 0.0) p += aWeights.w * (jointAt(aNamed.w) * vec4(aPoint3, 1.0)).xyz;
46
+ vec3 raw = mat3(jointAt(aNamed.x)) * aNormal;
47
+ float l = length(raw);
48
+ vNormal = l > 0.0 ? raw / l : raw;
49
+ vPosition = p;
50
+ gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
51
+ }
52
+ `;
53
+
54
+ const POSE_FRAG = /* glsl */ `#version 300 es
55
+ precision mediump float;
56
+ out vec4 colour;
57
+ void main() {
58
+ colour = vec4(0.0);
59
+ }
60
+ `;
61
+
62
+ /** The program and feedback object every skin on a context poses with, made at the first skin. */
63
+ export interface PointSkinPose {
64
+ readonly program: WebGLProgram;
65
+ readonly feedback: WebGLTransformFeedback;
66
+ }
67
+
68
+ export function createPointSkinPose(gl: WebGL2RenderingContext): PointSkinPose {
69
+ const program = compileProgram(gl, POSE_VERT, POSE_FRAG, 'pointSkin.pose', [
70
+ 'vPosition',
71
+ 'vNormal',
72
+ ]);
73
+ gl.useProgram(program);
74
+ gl.uniform1i(gl.getUniformLocation(program, 'uJoints'), POINT_SKIN_TEXTURE_UNIT);
75
+ gl.useProgram(null);
76
+ const feedback = gl.createTransformFeedback();
77
+ if (feedback === null) throw new Error('createTransformFeedback failed');
78
+ return { program, feedback };
79
+ }
80
+
81
+ export class GlPointSkin {
82
+ readonly mesh: Mesh;
83
+ readonly reach: PointSkinReach;
84
+ private readonly count: number;
85
+ private readonly vao: WebGLVertexArrayObject;
86
+ private readonly buffers: WebGLBuffer[] = [];
87
+ private readonly joints: WebGLTexture;
88
+ private readonly width: number;
89
+
90
+ constructor(gl: WebGL2RenderingContext, mesh: Mesh, data: PointSkinData) {
91
+ this.mesh = mesh;
92
+ this.reach = reachOf(data);
93
+ this.count = data.weights.length / 4;
94
+ const vao = gl.createVertexArray();
95
+ if (vao === null) throw new Error('createVertexArray failed');
96
+ this.vao = vao;
97
+ gl.bindVertexArray(vao);
98
+ this.attach(gl, data.joints, [[0, 4, 16, 0]]);
99
+ this.attach(gl, data.weights, [[1, 4, 16, 0]]);
100
+ this.attach(gl, data.points, [
101
+ [2, 3, 48, 0],
102
+ [3, 3, 48, 12],
103
+ [4, 3, 48, 24],
104
+ [5, 3, 48, 36],
105
+ ]);
106
+ this.attach(gl, data.normals, [[6, 3, 12, 0]]);
107
+ gl.bindVertexArray(null);
108
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
109
+
110
+ /* One row, four texels a joint, as the skin palette is. */
111
+ this.width = Math.max(1, this.reach.joints * 4);
112
+ const joints = gl.createTexture();
113
+ if (joints === null) throw new Error('createTexture failed');
114
+ this.joints = joints;
115
+ gl.activeTexture(gl.TEXTURE0 + POINT_SKIN_TEXTURE_UNIT);
116
+ gl.bindTexture(gl.TEXTURE_2D, joints);
117
+ gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32F, this.width, 1);
118
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
119
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
120
+ gl.bindTexture(gl.TEXTURE_2D, null);
121
+ }
122
+
123
+ /** One buffer, read as each `[location, size, stride, offset]` says. */
124
+ private attach(
125
+ gl: WebGL2RenderingContext,
126
+ values: Float32Array,
127
+ reads: readonly (readonly [number, number, number, number])[],
128
+ ): void {
129
+ const buffer = gl.createBuffer();
130
+ if (buffer === null) throw new Error('createBuffer failed');
131
+ this.buffers.push(buffer);
132
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
133
+ gl.bufferData(gl.ARRAY_BUFFER, values, gl.STATIC_DRAW);
134
+ for (const [location, size, stride, offset] of reads) {
135
+ gl.enableVertexAttribArray(location);
136
+ gl.vertexAttribPointer(location, size, gl.FLOAT, false, stride, offset);
137
+ }
138
+ }
139
+
140
+ /** Pose every vertex into the mesh's buffers now, and undo every binding it made. */
141
+ pose(gl: WebGL2RenderingContext, pose: PointSkinPose, matrices: Float32Array): void {
142
+ const streams = this.mesh.streams;
143
+ if (streams === null) return;
144
+ gl.useProgram(pose.program);
145
+ gl.bindVertexArray(this.vao);
146
+ gl.activeTexture(gl.TEXTURE0 + POINT_SKIN_TEXTURE_UNIT);
147
+ gl.bindTexture(gl.TEXTURE_2D, this.joints);
148
+ if (this.reach.joints > 0) {
149
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, this.width, 1, gl.RGBA, gl.FLOAT, matrices, 0);
150
+ }
151
+ gl.enable(gl.RASTERIZER_DISCARD);
152
+ gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, pose.feedback);
153
+ gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, streams.positions);
154
+ gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, streams.normals);
155
+ gl.beginTransformFeedback(gl.POINTS);
156
+ gl.drawArrays(gl.POINTS, 0, this.count);
157
+ gl.endTransformFeedback();
158
+ gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);
159
+ gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, null);
160
+ gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
161
+ gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, null);
162
+ gl.disable(gl.RASTERIZER_DISCARD);
163
+ gl.bindTexture(gl.TEXTURE_2D, null);
164
+ gl.bindVertexArray(null);
165
+ gl.useProgram(null);
166
+ }
167
+
168
+ dispose(gl: WebGL2RenderingContext): void {
169
+ for (const buffer of this.buffers) gl.deleteBuffer(buffer);
170
+ gl.deleteTexture(this.joints);
171
+ gl.deleteVertexArray(this.vao);
172
+ }
173
+ }