@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.
- package/CHANGELOG.json +31 -0
- package/README.md +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/render/backend/api.d.ts +63 -8
- package/dist/render/backend/webgl2/pointSkinFeedback.d.ts +23 -0
- package/dist/render/backend/webgl2/pointSkinFeedback.js +159 -0
- package/dist/render/backend/webgl2/renderer.d.ts +21 -0
- package/dist/render/backend/webgl2/renderer.js +41 -2
- package/dist/render/backend/webgpu/buffers.d.ts +10 -2
- package/dist/render/backend/webgpu/buffers.js +66 -16
- package/dist/render/backend/webgpu/clothTextures.d.ts +6 -3
- package/dist/render/backend/webgpu/clothTextures.js +14 -11
- package/dist/render/backend/webgpu/drawCommand.js +3 -3
- package/dist/render/backend/webgpu/gpuClothSet.js +1 -0
- package/dist/render/backend/webgpu/groupShelf.d.ts +31 -0
- package/dist/render/backend/webgpu/groupShelf.js +54 -0
- package/dist/render/backend/webgpu/pointSkinPass.d.ts +27 -0
- package/dist/render/backend/webgpu/pointSkinPass.js +104 -0
- package/dist/render/backend/webgpu/renderer.d.ts +31 -7
- package/dist/render/backend/webgpu/renderer.js +118 -47
- package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +1 -1
- package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +7 -2
- package/dist/render/backend/webgpu/shaders/pointSkin.wgsl.d.ts +13 -0
- package/dist/render/backend/webgpu/shaders/pointSkin.wgsl.js +57 -0
- package/dist/render/backend/webgpu/vertexFormats.js +1 -1
- package/dist/render/lightBudget.d.ts +6 -0
- package/dist/render/lightBudget.js +6 -0
- package/dist/render/mesh.d.ts +19 -5
- package/dist/render/mesh.js +39 -8
- package/dist/render/pointSkin.d.ts +55 -0
- package/dist/render/pointSkin.js +121 -0
- package/dist/render/rendererHarness.js +6 -0
- package/dist/render/shader.d.ts +3 -1
- package/dist/render/shader.js +6 -1
- package/dist/render/vertexPacking.d.ts +16 -4
- package/dist/render/vertexPacking.js +10 -3
- package/package.json +6 -6
- package/src/index.ts +2 -0
- package/src/render/backend/api.ts +70 -7
- package/src/render/backend/webgl2/pointSkinFeedback.ts +173 -0
- package/src/render/backend/webgl2/renderer.ts +63 -2
- package/src/render/backend/webgpu/buffers.ts +90 -16
- package/src/render/backend/webgpu/clothTextures.ts +14 -11
- package/src/render/backend/webgpu/drawCommand.ts +3 -3
- package/src/render/backend/webgpu/gpuClothSet.ts +1 -0
- package/src/render/backend/webgpu/groupShelf.ts +74 -0
- package/src/render/backend/webgpu/pointSkinPass.ts +115 -0
- package/src/render/backend/webgpu/renderer.ts +150 -82
- package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +7 -2
- package/src/render/backend/webgpu/shaders/pointSkin.wgsl.ts +58 -0
- package/src/render/backend/webgpu/vertexFormats.ts +1 -1
- package/src/render/lightBudget.ts +7 -0
- package/src/render/mesh.ts +49 -7
- package/src/render/pointSkin.ts +178 -0
- package/src/render/rendererHarness.ts +6 -0
- package/src/render/shader.ts +5 -0
- 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,
|
package/dist/render/shader.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/render/shader.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
33
|
-
|
|
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
|
-
/**
|
|
11
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
33
|
-
"@driftengine/physics": "4.13.
|
|
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.
|
|
71
|
-
"@driftengine/network": "4.13.
|
|
72
|
-
"@driftengine/texture": "4.13.
|
|
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
|
|
253
|
-
*
|
|
254
|
-
*
|
|
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.**
|
|
932
|
-
*
|
|
933
|
-
*
|
|
934
|
-
*
|
|
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
|
+
}
|