@driftengine/core 4.13.0 → 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 +42 -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,178 @@
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
+ import { MAX_JOINTS } from './skinPalette.ts';
26
+
27
+ /** A point skin, one entry a vertex unless said otherwise. */
28
+ export interface PointSkinData {
29
+ /** Four joint indices a vertex, as floats, as `MeshData.joints` is. */
30
+ readonly joints: Float32Array;
31
+ /** Four weights a vertex. An influence of weight 0 is skipped, whatever its joint and point. */
32
+ readonly weights: Float32Array;
33
+ /** Four points a vertex, three floats each: influence `k`'s point in joint `k`'s own space. */
34
+ readonly points: Float32Array;
35
+ /** One normal a vertex, in the space of its first joint. */
36
+ readonly normals: Float32Array;
37
+ }
38
+
39
+ /** How far a skin reaches from each joint, and what its weights sum to: what a pose's bounds need. */
40
+ export interface PointSkinReach {
41
+ /** Joints a pose must hand over: one past the highest any vertex names and uses. */
42
+ readonly joints: number;
43
+ /** For each joint, the farthest point a weighted influence holds in its space; -1 where none. */
44
+ readonly reach: Float32Array;
45
+ /** The least and the greatest sum of one vertex's weights. */
46
+ readonly sums: readonly [number, number];
47
+ }
48
+
49
+ /** Refuse a skin whose arrays disagree with a mesh of `vertices`, or that names a joint past the rest. */
50
+ export function validatePointSkin(data: PointSkinData, vertices: number): void {
51
+ const fail = (what: string): never => {
52
+ throw new Error(`point skin: ${what}`);
53
+ };
54
+ for (const [name, values, width] of [
55
+ ['joints', data.joints, 4],
56
+ ['weights', data.weights, 4],
57
+ ['points', data.points, 12],
58
+ ['normals', data.normals, 3],
59
+ ] as const) {
60
+ if (values.length !== vertices * width) {
61
+ fail(`${name} holds ${values.length / width} vertices and the mesh has ${vertices}`);
62
+ }
63
+ }
64
+ for (let at = 0; at < data.joints.length; at++) {
65
+ const joint = data.joints[at] as number;
66
+ if (!(Number.isInteger(joint) && joint >= 0 && joint < MAX_JOINTS)) {
67
+ fail(`vertex ${Math.floor(at / 4)} names joint ${joint}; a joint is 0 to ${MAX_JOINTS - 1}`);
68
+ }
69
+ }
70
+ }
71
+
72
+ /** Measure a skin's reach once, at creation. */
73
+ export function reachOf(data: PointSkinData): PointSkinReach {
74
+ const reach = new Float32Array(MAX_JOINTS).fill(-1);
75
+ let joints = 0;
76
+ let least = Infinity;
77
+ let most = -Infinity;
78
+ const vertices = data.weights.length / 4;
79
+ for (let v = 0; v < vertices; v++) {
80
+ /* The first joint carries the normal, so it is named whatever its weight. */
81
+ joints = Math.max(joints, (data.joints[v * 4] as number) + 1);
82
+ let sum = 0;
83
+ for (let k = 0; k < 4; k++) {
84
+ const weight = data.weights[v * 4 + k] as number;
85
+ if (weight === 0) continue;
86
+ sum += weight;
87
+ const joint = data.joints[v * 4 + k] as number;
88
+ joints = Math.max(joints, joint + 1);
89
+ const at = (v * 4 + k) * 3;
90
+ const x = data.points[at] as number;
91
+ const y = data.points[at + 1] as number;
92
+ const z = data.points[at + 2] as number;
93
+ reach[joint] = Math.max(reach[joint] as number, Math.sqrt(x * x + y * y + z * z));
94
+ }
95
+ least = Math.min(least, sum);
96
+ most = Math.max(most, sum);
97
+ }
98
+ return {
99
+ joints,
100
+ reach: reach.slice(0, joints),
101
+ sums: vertices === 0 ? [1, 1] : [least, most],
102
+ };
103
+ }
104
+
105
+ /**
106
+ * The bounds of a pose, into the mesh's own: `matrices` holds each joint's transform into the
107
+ * mesh's model space, sixteen floats, column-major. Allocates nothing.
108
+ */
109
+ export function poseBounds(matrices: Float32Array, skin: PointSkinReach, out: Bounds): void {
110
+ let minX = Infinity;
111
+ let minY = Infinity;
112
+ let minZ = Infinity;
113
+ let maxX = -Infinity;
114
+ let maxY = -Infinity;
115
+ let maxZ = -Infinity;
116
+ for (let j = 0; j < skin.joints; j++) {
117
+ const reach = skin.reach[j] as number;
118
+ if (reach < 0) continue;
119
+ const m = j * 16;
120
+ const scale = Math.sqrt(
121
+ Math.max(
122
+ columnLength2(matrices, m),
123
+ columnLength2(matrices, m + 4),
124
+ columnLength2(matrices, m + 8),
125
+ ),
126
+ );
127
+ const r = reach * scale;
128
+ const x = matrices[m + 12] as number;
129
+ const y = matrices[m + 13] as number;
130
+ const z = matrices[m + 14] as number;
131
+ minX = Math.min(minX, x - r);
132
+ minY = Math.min(minY, y - r);
133
+ minZ = Math.min(minZ, z - r);
134
+ maxX = Math.max(maxX, x + r);
135
+ maxY = Math.max(maxY, y + r);
136
+ maxZ = Math.max(maxZ, z + r);
137
+ }
138
+ if (minX > maxX) {
139
+ out.min.fill(0);
140
+ out.max.fill(0);
141
+ out.centre.fill(0);
142
+ out.radius = 0;
143
+ return;
144
+ }
145
+ /* A sum of weights other than one scales the box about the origin, by the least and the most. */
146
+ const [least, most] = skin.sums;
147
+ out.min[0] = Math.min(least * minX, most * minX);
148
+ out.min[1] = Math.min(least * minY, most * minY);
149
+ out.min[2] = Math.min(least * minZ, most * minZ);
150
+ out.max[0] = Math.max(least * maxX, most * maxX);
151
+ out.max[1] = Math.max(least * maxY, most * maxY);
152
+ out.max[2] = Math.max(least * maxZ, most * maxZ);
153
+ let radius2 = 0;
154
+ for (let axis = 0; axis < 3; axis++) {
155
+ const centre = ((out.min[axis] as number) + (out.max[axis] as number)) / 2;
156
+ out.centre[axis] = centre;
157
+ const half = (out.max[axis] as number) - centre;
158
+ radius2 += half * half;
159
+ }
160
+ out.radius = Math.sqrt(radius2);
161
+ }
162
+
163
+ /** The squared length of one column's first three components. */
164
+ function columnLength2(m: Float32Array, at: number): number {
165
+ const x = m[at] as number;
166
+ const y = m[at + 1] as number;
167
+ const z = m[at + 2] as number;
168
+ return x * x + y * y + z * z;
169
+ }
170
+
171
+ /** Refuse a pose that hands over fewer joints than the skin names. */
172
+ export function validatePose(matrices: Float32Array, skin: PointSkinReach): void {
173
+ if (matrices.length % 16 !== 0 || matrices.length / 16 < skin.joints) {
174
+ throw new Error(
175
+ `point skin: a pose of ${matrices.length / 16} joints for a skin that names ${skin.joints}`,
176
+ );
177
+ }
178
+ }
@@ -125,6 +125,12 @@ export function recordingGl(
125
125
  ONE: 1,
126
126
  ONE_MINUS_SRC_ALPHA: 0x0303,
127
127
  TRIANGLES: 0x0004,
128
+ /* Transform feedback's, so a point skin's pose can be seen to discard, capture and undo both. */
129
+ POINTS: 0x0000,
130
+ RASTERIZER_DISCARD: 0x8c89,
131
+ TRANSFORM_FEEDBACK: 0x8e22,
132
+ TRANSFORM_FEEDBACK_BUFFER: 0x8c8e,
133
+ SEPARATE_ATTRIBS: 0x8c8d,
128
134
  /* The winding a pass names, so a test can tell which one it chose. */
129
135
  CW: 0x0900,
130
136
  CCW: 0x0901,
@@ -5,6 +5,8 @@ export function compileProgram(
5
5
  vertexSource: string,
6
6
  fragmentSource: string,
7
7
  label: string,
8
+ /** Vertex outputs captured by transform feedback, one buffer each; named before the link. */
9
+ feedback?: readonly string[],
8
10
  ): WebGLProgram {
9
11
  const vs = compileStage(gl, gl.VERTEX_SHADER, vertexSource, `${label}.vert`);
10
12
  const fs = compileStage(gl, gl.FRAGMENT_SHADER, fragmentSource, `${label}.frag`);
@@ -14,6 +16,9 @@ export function compileProgram(
14
16
 
15
17
  gl.attachShader(program, vs);
16
18
  gl.attachShader(program, fs);
19
+ if (feedback !== undefined) {
20
+ gl.transformFeedbackVaryings(program, feedback as string[], gl.SEPARATE_ATTRIBS);
21
+ }
17
22
  gl.linkProgram(program);
18
23
  gl.deleteShader(vs);
19
24
  gl.deleteShader(fs);
@@ -23,10 +23,19 @@ import type { MeshData } from '@driftengine/drft';
23
23
  * on the CPU. What would make this wrong is a value outside its range, which the range check sends
24
24
  * to floats rather than clamping, or a shader reading an attribute as something other than the
25
25
  * value it carries.
26
+ *
27
+ * **A mesh whose colours are rewritten keeps them as floats in a buffer of their own** (`stream`),
28
+ * whatever they are: one value every vertex shares has no buffer to rewrite, sixteen bits would
29
+ * have to be packed on the CPU every rewrite, and colours interleaved with the rest of a vertex
30
+ * could only be rewritten by keeping the rows, which a mesh lets go once it is uploaded. It costs
31
+ * twelve bytes a vertex where the colours might have cost none, which is why a mesh asks for it.
26
32
  */
27
33
 
28
- /** Floats; sixteen-bit fixed point, signed or not; or one value for every vertex. */
29
- export type AttributePacking = 'float' | 'snorm16' | 'unorm16' | 'constant';
34
+ /**
35
+ * Floats; sixteen-bit fixed point, signed or not; one value for every vertex; or floats in a
36
+ * buffer of their own, for an attribute a caller rewrites.
37
+ */
38
+ export type AttributePacking = 'float' | 'snorm16' | 'unorm16' | 'constant' | 'stream';
30
39
 
31
40
  /** The attributes this decides for. Every other one travels as floats. */
32
41
  export const PACKED_ATTRIBUTES = [
@@ -42,12 +51,23 @@ export type PackedAttribute = (typeof PACKED_ATTRIBUTES)[number];
42
51
 
43
52
  export type VertexPacking = Readonly<Record<PackedAttribute, AttributePacking>>;
44
53
 
45
- /** The choice for this mesh. `dynamic` is a mesh whose positions and normals are rewritten. */
46
- export function vertexPackingOf(data: MeshData, dynamic: boolean): VertexPacking {
54
+ /**
55
+ * The choice for this mesh. `dynamic` is a mesh whose positions and normals are rewritten, and
56
+ * `dynamicColors` one whose colours are.
57
+ */
58
+ export function vertexPackingOf(
59
+ data: MeshData,
60
+ dynamic: boolean,
61
+ dynamicColors = false,
62
+ ): VertexPacking {
47
63
  return {
48
64
  normals: dynamic ? 'float' : fixedOrFloat(data.normals, -1),
49
65
  tangents: fixedOrFloat(data.tangents, -1),
50
- colors: shared(data.colors, 3) ? 'constant' : fixedOrFloat(data.colors, 0),
66
+ colors: dynamicColors
67
+ ? 'stream'
68
+ : shared(data.colors, 3)
69
+ ? 'constant'
70
+ : fixedOrFloat(data.colors, 0),
51
71
  emissive: shared(data.emissive, 1) ? 'constant' : 'float',
52
72
  weights: fixedOrFloat(data.weights, 0),
53
73
  weights2: fixedOrFloat(data.weights2, 0),