@flighthq/scene3d-wgpu 0.3.0 → 0.3.1-next.1041.35b950c
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/dist/customShaderWgpuMeshMaterialRenderer.d.ts.map +1 -1
- package/dist/customShaderWgpuMeshMaterialRenderer.js +6 -9
- package/dist/customShaderWgpuMeshMaterialRenderer.js.map +1 -1
- package/dist/drawWgpuScene3D.d.ts.map +1 -1
- package/dist/drawWgpuScene3D.js +4 -2
- package/dist/drawWgpuScene3D.js.map +1 -1
- package/dist/explainWgpuScene3DCoverage.d.ts +2 -2
- package/dist/explainWgpuScene3DCoverage.d.ts.map +1 -1
- package/dist/explainWgpuScene3DCoverage.js +62 -26
- package/dist/explainWgpuScene3DCoverage.js.map +1 -1
- package/dist/wgpuClassicPrelude.js +2 -2
- package/dist/wgpuClassicPrelude.js.map +1 -1
- package/dist/wgpuMeshMaterialRegistry.d.ts.map +1 -1
- package/dist/wgpuMeshMaterialRegistry.js +15 -11
- package/dist/wgpuMeshMaterialRegistry.js.map +1 -1
- package/dist/wgpuMeshPipeline.d.ts +1 -1
- package/dist/wgpuMeshPipeline.d.ts.map +1 -1
- package/dist/wgpuMeshPipeline.js +39 -12
- package/dist/wgpuMeshPipeline.js.map +1 -1
- package/dist/wgpuMeshUpload.d.ts +1 -1
- package/dist/wgpuMeshUpload.d.ts.map +1 -1
- package/dist/wgpuMeshUpload.js +37 -12
- package/dist/wgpuMeshUpload.js.map +1 -1
- package/dist/wgpuParticleEmitter3D.d.ts.map +1 -1
- package/dist/wgpuParticleEmitter3D.js +9 -6
- package/dist/wgpuParticleEmitter3D.js.map +1 -1
- package/dist/wgpuPbrPipelineCache.d.ts.map +1 -1
- package/dist/wgpuPbrPipelineCache.js +2 -2
- package/dist/wgpuPbrPipelineCache.js.map +1 -1
- package/dist/wgpuScene3DRuntime.d.ts.map +1 -1
- package/dist/wgpuScene3DRuntime.js +18 -10
- package/dist/wgpuScene3DRuntime.js.map +1 -1
- package/dist/wgpuScene3DTestHelper.d.ts.map +1 -1
- package/dist/wgpuScene3DTestHelper.js +126 -6
- package/dist/wgpuScene3DTestHelper.js.map +1 -1
- package/dist/wgpuShadedModifierSnippet.d.ts.map +1 -1
- package/dist/wgpuShadedModifierSnippet.js +8 -11
- package/dist/wgpuShadedModifierSnippet.js.map +1 -1
- package/dist/wgpuShadedPrelude.d.ts +5 -3
- package/dist/wgpuShadedPrelude.d.ts.map +1 -1
- package/dist/wgpuShadedPrelude.js +34 -13
- package/dist/wgpuShadedPrelude.js.map +1 -1
- package/dist/wgpuShadowMap.d.ts.map +1 -1
- package/dist/wgpuShadowMap.js +22 -6
- package/dist/wgpuShadowMap.js.map +1 -1
- package/dist/wgpuSkinPalette.d.ts +4 -1
- package/dist/wgpuSkinPalette.d.ts.map +1 -1
- package/dist/wgpuSkinPalette.js +227 -27
- package/dist/wgpuSkinPalette.js.map +1 -1
- package/dist/wgpuWireframeUpload.d.ts.map +1 -1
- package/dist/wgpuWireframeUpload.js +12 -6
- package/dist/wgpuWireframeUpload.js.map +1 -1
- package/package.json +20 -15
- package/src/customShaderWgpuMeshMaterialRenderer.test.ts +25 -1
- package/src/explainWgpuScene3DCoverage.test.ts +44 -10
- package/src/wgpuMeshMaterialRegistry.test.ts +22 -0
- package/src/wgpuMeshPipeline.test.ts +34 -1
- package/src/wgpuMeshUpload.test.ts +65 -3
- package/src/wgpuParticleEmitter3D.test.ts +23 -0
- package/src/wgpuScene3DRuntime.test.ts +8 -4
- package/src/wgpuScene3DTestHelper.test.ts +84 -0
- package/src/wgpuShadedModifierSnippet.test.ts +20 -6
- package/src/wgpuShadedPrelude.test.ts +6 -6
- package/src/wgpuShadowMap.test.ts +30 -0
- package/src/wgpuSkinPalette.test.ts +134 -19
- package/src/wgpuWireframeUpload.test.ts +29 -1
|
@@ -6,7 +6,10 @@ import {
|
|
|
6
6
|
destroyWgpuSkinPalette,
|
|
7
7
|
ensureWgpuSkinDrawBindGroup,
|
|
8
8
|
ensureWgpuSkinDrawLayout,
|
|
9
|
+
ensureWgpuSkinMeshDrawBindGroup,
|
|
10
|
+
ensureWgpuSkinMeshDrawLayout,
|
|
9
11
|
registerWgpuGpuSkinning,
|
|
12
|
+
uploadWgpuSkinNormalPalette,
|
|
10
13
|
uploadWgpuSkinPalette,
|
|
11
14
|
} from './wgpuSkinPalette';
|
|
12
15
|
|
|
@@ -21,7 +24,9 @@ describe('destroyWgpuSkinPalette', () => {
|
|
|
21
24
|
|
|
22
25
|
expect(runtime.skinPaletteTexture).toBeNull();
|
|
23
26
|
expect(runtime.skinPaletteView).toBeNull();
|
|
24
|
-
expect(runtime.
|
|
27
|
+
expect(runtime.skinPaletteArenaRows).toBe(0);
|
|
28
|
+
expect(runtime.skinPaletteArenaCursor).toBe(0);
|
|
29
|
+
expect(runtime.skinPaletteArenaBases).toBeNull();
|
|
25
30
|
expect(runtime.skinDrawBindGroup).toBeNull();
|
|
26
31
|
});
|
|
27
32
|
});
|
|
@@ -29,12 +34,15 @@ describe('destroyWgpuSkinPalette', () => {
|
|
|
29
34
|
describe('ensureWgpuSkinDrawBindGroup', () => {
|
|
30
35
|
it('caches the group and rebuilds it only when palette growth replaces its view', () => {
|
|
31
36
|
const { fake, state } = makeWgpuScene3DState();
|
|
32
|
-
const
|
|
37
|
+
const palette = new Float32Array(16);
|
|
38
|
+
const first = ensureWgpuSkinDrawBindGroup(state, palette);
|
|
33
39
|
const groupCount = fake.calls.filter((call) => call.name === 'createBindGroup').length;
|
|
34
|
-
|
|
40
|
+
// The same skeleton keeps its region, so nothing is reallocated and nothing is rebuilt.
|
|
41
|
+
expect(ensureWgpuSkinDrawBindGroup(state, palette)).toBe(first);
|
|
35
42
|
expect(fake.calls.filter((call) => call.name === 'createBindGroup')).toHaveLength(groupCount);
|
|
36
43
|
|
|
37
|
-
|
|
44
|
+
// A second skeleton claims the next row, which outgrows the arena and replaces its view.
|
|
45
|
+
ensureWgpuSkinDrawBindGroup(state, new Float32Array(16));
|
|
38
46
|
expect(fake.calls.filter((call) => call.name === 'createBindGroup')).toHaveLength(groupCount + 1);
|
|
39
47
|
expect(fake.calls.filter((call) => call.name === 'createTexture')).toHaveLength(2);
|
|
40
48
|
});
|
|
@@ -49,6 +57,45 @@ describe('ensureWgpuSkinDrawLayout', () => {
|
|
|
49
57
|
});
|
|
50
58
|
});
|
|
51
59
|
|
|
60
|
+
describe('ensureWgpuSkinMeshDrawBindGroup', () => {
|
|
61
|
+
it('binds both palettes, so no binding its layout declares is left unsupplied', () => {
|
|
62
|
+
const { fake, state } = makeWgpuScene3DState();
|
|
63
|
+
ensureWgpuSkinMeshDrawBindGroup(state, new Float32Array(16 * 2), new Float32Array(12 * 2));
|
|
64
|
+
const group = fake.calls.filter((call) => call.name === 'createBindGroup').at(-1);
|
|
65
|
+
const bindings = (group!.args[0] as { entries: { binding: number }[] }).entries.map((e) => e.binding);
|
|
66
|
+
expect(bindings).toEqual([0, 1, 2]);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('ensureWgpuSkinMeshDrawLayout', () => {
|
|
71
|
+
it('declares the normal palette at binding 2, which the shadow layout does NOT', () => {
|
|
72
|
+
// ★ THE TWO LAYOUTS ARE DELIBERATELY DIFFERENT. The shadow path skins positions only, so making it
|
|
73
|
+
// declare a normal palette would oblige it to SUPPLY one forever — a bind group must satisfy every
|
|
74
|
+
// binding its layout declares. This asserts the split exists rather than trusting the comment.
|
|
75
|
+
const { fake, state } = makeWgpuScene3DState();
|
|
76
|
+
ensureWgpuSkinMeshDrawLayout(state);
|
|
77
|
+
const meshLayout = fake.calls.filter((call) => call.name === 'createBindGroupLayout').at(-1);
|
|
78
|
+
const meshBindings = (meshLayout!.args[0] as { entries: { binding: number }[] }).entries.map((e) => e.binding);
|
|
79
|
+
expect(meshBindings).toEqual([0, 1, 2]);
|
|
80
|
+
|
|
81
|
+
ensureWgpuSkinDrawLayout(state);
|
|
82
|
+
const shadowLayout = fake.calls.filter((call) => call.name === 'createBindGroupLayout').at(-1);
|
|
83
|
+
const shadowBindings = (shadowLayout!.args[0] as { entries: { binding: number }[] }).entries.map((e) => e.binding);
|
|
84
|
+
expect(shadowBindings).toEqual([0, 1]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('caches its own layout rather than rebuilding it per draw', () => {
|
|
88
|
+
// Only the CACHING half is asserted here. The fake device returns one shared stub for every
|
|
89
|
+
// createBindGroupLayout call, so object identity cannot distinguish the mesh layout from the shadow
|
|
90
|
+
// one — an assertion that they differ would be testing the mock, not the code. The binding-shape
|
|
91
|
+
// test above is what proves the two layouts are actually different.
|
|
92
|
+
const { fake, state } = makeWgpuScene3DState();
|
|
93
|
+
ensureWgpuSkinMeshDrawLayout(state);
|
|
94
|
+
ensureWgpuSkinMeshDrawLayout(state);
|
|
95
|
+
expect(fake.calls.filter((call) => call.name === 'createBindGroupLayout')).toHaveLength(1);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
52
99
|
describe('registerWgpuGpuSkinning', () => {
|
|
53
100
|
it('installs the opt-in adapter on only the requested render state', () => {
|
|
54
101
|
const { state } = makeWgpuScene3DState();
|
|
@@ -58,31 +105,99 @@ describe('registerWgpuGpuSkinning', () => {
|
|
|
58
105
|
});
|
|
59
106
|
});
|
|
60
107
|
|
|
108
|
+
describe('uploadWgpuSkinNormalPalette', () => {
|
|
109
|
+
it('packs THREE texels per joint, not four', () => {
|
|
110
|
+
// ★ The joint count comes from /12, not /16: a 3x3 padded to vec4 columns is twelve floats. Reusing
|
|
111
|
+
// the pose divisor would under-count every skeleton by a quarter and upload a truncated row —
|
|
112
|
+
// wrong lighting rather than a failure. Two joints => 6 texels.
|
|
113
|
+
const { fake, state } = makeWgpuScene3DState();
|
|
114
|
+
const base = uploadWgpuSkinNormalPalette(state, new Float32Array(12 * 2));
|
|
115
|
+
const texture = fake.calls.find((call) => call.name === 'createTexture');
|
|
116
|
+
const write = fake.calls.find((call) => call.name === 'writeTexture');
|
|
117
|
+
|
|
118
|
+
expect(base).toBe(0);
|
|
119
|
+
expect(texture?.args[0]).toMatchObject({ format: 'rgba32float', size: [256, 1, 1] });
|
|
120
|
+
expect(write?.args[3]).toEqual([6, 1, 1]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('uses a texture distinct from the pose palette', () => {
|
|
124
|
+
// Aliasing them would upload one over the other every frame.
|
|
125
|
+
const { state } = makeWgpuScene3DState();
|
|
126
|
+
uploadWgpuSkinPalette(state, new Float32Array(16));
|
|
127
|
+
uploadWgpuSkinNormalPalette(state, new Float32Array(12));
|
|
128
|
+
const runtime = getWgpuScene3DRuntime(state);
|
|
129
|
+
expect(runtime.skinNormalPaletteTexture).not.toBe(runtime.skinPaletteTexture);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
61
133
|
describe('uploadWgpuSkinPalette', () => {
|
|
62
|
-
it('packs four rgba32float texels per joint into
|
|
134
|
+
it('packs four rgba32float texels per joint into a fixed-width arena row', () => {
|
|
63
135
|
const { fake, state } = makeWgpuScene3DState();
|
|
64
|
-
const
|
|
65
|
-
const view = uploadWgpuSkinPalette(state, joints);
|
|
136
|
+
const base = uploadWgpuSkinPalette(state, new Float32Array(32));
|
|
66
137
|
const texture = fake.calls.find((call) => call.name === 'createTexture');
|
|
67
138
|
const write = fake.calls.find((call) => call.name === 'writeTexture');
|
|
68
139
|
|
|
69
|
-
expect(
|
|
70
|
-
expect(texture?.args[0]).toMatchObject({
|
|
71
|
-
|
|
72
|
-
size: [8, 1, 1],
|
|
73
|
-
});
|
|
74
|
-
expect(write?.args[2]).toEqual({ bytesPerRow: 128 });
|
|
140
|
+
expect(base).toBe(0);
|
|
141
|
+
expect(texture?.args[0]).toMatchObject({ format: 'rgba32float', size: [256, 1, 1] });
|
|
142
|
+
expect(write?.args[2]).toEqual({ bytesPerRow: 128, offset: 0 });
|
|
75
143
|
expect(write?.args[3]).toEqual([8, 1, 1]);
|
|
76
144
|
});
|
|
77
145
|
|
|
78
|
-
it('
|
|
146
|
+
it('gives two skeletons in one frame DISTINCT regions', () => {
|
|
147
|
+
const { state } = makeWgpuScene3DState();
|
|
148
|
+
const first = uploadWgpuSkinPalette(state, new Float32Array(16 * 2));
|
|
149
|
+
const second = uploadWgpuSkinPalette(state, new Float32Array(16 * 2));
|
|
150
|
+
|
|
151
|
+
// The whole point of the arena: a second skeleton must not land on the first one's texels, or every
|
|
152
|
+
// skinned draw in the frame samples whichever palette was written last.
|
|
153
|
+
expect(second).not.toBe(first);
|
|
154
|
+
expect(second).toBe(256);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('gives one skeleton ONE region however many passes ask for it', () => {
|
|
158
|
+
const { fake, state } = makeWgpuScene3DState();
|
|
159
|
+
const palette = new Float32Array(16 * 2);
|
|
160
|
+
const shadowPass = uploadWgpuSkinPalette(state, palette);
|
|
161
|
+
const writes = fake.calls.filter((call) => call.name === 'writeTexture').length;
|
|
162
|
+
const meshPass = uploadWgpuSkinPalette(state, palette);
|
|
163
|
+
|
|
164
|
+
expect(meshPass).toBe(shadowPass);
|
|
165
|
+
expect(fake.calls.filter((call) => call.name === 'writeTexture')).toHaveLength(writes);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('spans rows for a skeleton wider than one row, as whole rows plus the remainder', () => {
|
|
169
|
+
const { fake, state } = makeWgpuScene3DState();
|
|
170
|
+
// 100 joints is 400 texels against a 256-texel row: one full row and 144 left over. Two rectangles,
|
|
171
|
+
// because a region straddling a row boundary is not expressible as a single copy.
|
|
172
|
+
uploadWgpuSkinPalette(state, new Float32Array(16 * 100));
|
|
173
|
+
const writes = fake.calls.filter((call) => call.name === 'writeTexture');
|
|
174
|
+
|
|
175
|
+
expect(writes).toHaveLength(2);
|
|
176
|
+
expect(writes[0].args[2]).toEqual({ bytesPerRow: 4096, rowsPerImage: 1 });
|
|
177
|
+
expect(writes[0].args[3]).toEqual([256, 1, 1]);
|
|
178
|
+
expect(writes[1].args[0]).toMatchObject({ origin: [0, 1, 0] });
|
|
179
|
+
expect(writes[1].args[2]).toEqual({ bytesPerRow: 2304, offset: 4096 });
|
|
180
|
+
expect(writes[1].args[3]).toEqual([144, 1, 1]);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('starts the next region on a row boundary, so every write stays a rectangle', () => {
|
|
184
|
+
const { state } = makeWgpuScene3DState();
|
|
185
|
+
uploadWgpuSkinPalette(state, new Float32Array(16 * 100));
|
|
186
|
+
// 400 texels rounds up to two whole rows, so the next skeleton starts at 512 rather than 400.
|
|
187
|
+
expect(uploadWgpuSkinPalette(state, new Float32Array(16 * 2))).toBe(512);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('replays regions already handed out when growth replaces the arena texture', () => {
|
|
79
191
|
const { fake, state } = makeWgpuScene3DState();
|
|
80
|
-
uploadWgpuSkinPalette(state, new Float32Array(16 *
|
|
81
|
-
|
|
82
|
-
expect(fake.calls.filter((call) => call.name === 'createTexture')).toHaveLength(1);
|
|
192
|
+
uploadWgpuSkinPalette(state, new Float32Array(16 * 2));
|
|
193
|
+
const before = fake.calls.filter((call) => call.name === 'writeTexture').length;
|
|
83
194
|
|
|
84
|
-
|
|
195
|
+
// Growth is a NEW texture; without the replay the first skeleton's texels would be blank in it.
|
|
196
|
+
uploadWgpuSkinPalette(state, new Float32Array(16 * 2));
|
|
85
197
|
expect(fake.calls.filter((call) => call.name === 'createTexture')).toHaveLength(2);
|
|
86
|
-
|
|
198
|
+
const writes = fake.calls.filter((call) => call.name === 'writeTexture');
|
|
199
|
+
expect(writes.length).toBe(before + 2);
|
|
200
|
+
expect(writes.at(-2)?.args[0]).toMatchObject({ origin: [0, 0, 0] });
|
|
201
|
+
expect(writes.at(-1)?.args[0]).toMatchObject({ origin: [0, 1, 0] });
|
|
87
202
|
});
|
|
88
203
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createBoxMeshGeometry } from '@flighthq/mesh/contract';
|
|
1
|
+
import { createBoxMeshGeometry, createMeshGeometry } from '@flighthq/mesh/contract';
|
|
2
2
|
|
|
3
3
|
import { makeWgpuScene3DState } from './wgpuScene3DTestHelper';
|
|
4
4
|
import { ensureWgpuWireframeUpload } from './wgpuWireframeUpload';
|
|
@@ -24,6 +24,34 @@ describe('ensureWgpuWireframeUpload', () => {
|
|
|
24
24
|
expect(lineBufferCreate).toBeDefined();
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
+
// Non-indexed geometry only became reachable here once the mesh upload stopped refusing it; before
|
|
28
|
+
// that this function bailed early, and buildLineIndices would have dereferenced a null index array.
|
|
29
|
+
// The edges come from a sequential vertex range instead, matching glWireframeUpload.
|
|
30
|
+
it('derives line indices from a sequential range for non-indexed geometry', () => {
|
|
31
|
+
const { fake, state } = makeWgpuScene3DState();
|
|
32
|
+
// Two triangles' worth of positions: 6 vertices x 3 floats.
|
|
33
|
+
const geometry = createMeshGeometry({
|
|
34
|
+
indices: null,
|
|
35
|
+
layout: {
|
|
36
|
+
attributes: [{ byteOffset: 0, format: 'float32x3', semantic: 'position' }],
|
|
37
|
+
stride: 12,
|
|
38
|
+
},
|
|
39
|
+
vertices: new Float32Array(18),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const upload = ensureWgpuWireframeUpload(state, geometry);
|
|
43
|
+
|
|
44
|
+
expect(upload).not.toBeNull();
|
|
45
|
+
// 2 triangles x 6 line indices x 2 bytes.
|
|
46
|
+
const lineBufferCreate = fake.calls.find(
|
|
47
|
+
(c) =>
|
|
48
|
+
c.name === 'createBuffer' &&
|
|
49
|
+
((c.args[0] as { usage: number }).usage & GPUBufferUsage.INDEX) !== 0 &&
|
|
50
|
+
(c.args[0] as { size: number }).size === 2 * 6 * 2,
|
|
51
|
+
);
|
|
52
|
+
expect(lineBufferCreate).toBeDefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
27
55
|
it('returns the cached upload without rebuilding when the version is unchanged', () => {
|
|
28
56
|
const { fake, state } = makeWgpuScene3DState();
|
|
29
57
|
const geometry = createBoxMeshGeometry();
|