@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.
Files changed (66) hide show
  1. package/dist/customShaderWgpuMeshMaterialRenderer.d.ts.map +1 -1
  2. package/dist/customShaderWgpuMeshMaterialRenderer.js +6 -9
  3. package/dist/customShaderWgpuMeshMaterialRenderer.js.map +1 -1
  4. package/dist/drawWgpuScene3D.d.ts.map +1 -1
  5. package/dist/drawWgpuScene3D.js +4 -2
  6. package/dist/drawWgpuScene3D.js.map +1 -1
  7. package/dist/explainWgpuScene3DCoverage.d.ts +2 -2
  8. package/dist/explainWgpuScene3DCoverage.d.ts.map +1 -1
  9. package/dist/explainWgpuScene3DCoverage.js +62 -26
  10. package/dist/explainWgpuScene3DCoverage.js.map +1 -1
  11. package/dist/wgpuClassicPrelude.js +2 -2
  12. package/dist/wgpuClassicPrelude.js.map +1 -1
  13. package/dist/wgpuMeshMaterialRegistry.d.ts.map +1 -1
  14. package/dist/wgpuMeshMaterialRegistry.js +15 -11
  15. package/dist/wgpuMeshMaterialRegistry.js.map +1 -1
  16. package/dist/wgpuMeshPipeline.d.ts +1 -1
  17. package/dist/wgpuMeshPipeline.d.ts.map +1 -1
  18. package/dist/wgpuMeshPipeline.js +39 -12
  19. package/dist/wgpuMeshPipeline.js.map +1 -1
  20. package/dist/wgpuMeshUpload.d.ts +1 -1
  21. package/dist/wgpuMeshUpload.d.ts.map +1 -1
  22. package/dist/wgpuMeshUpload.js +37 -12
  23. package/dist/wgpuMeshUpload.js.map +1 -1
  24. package/dist/wgpuParticleEmitter3D.d.ts.map +1 -1
  25. package/dist/wgpuParticleEmitter3D.js +9 -6
  26. package/dist/wgpuParticleEmitter3D.js.map +1 -1
  27. package/dist/wgpuPbrPipelineCache.d.ts.map +1 -1
  28. package/dist/wgpuPbrPipelineCache.js +2 -2
  29. package/dist/wgpuPbrPipelineCache.js.map +1 -1
  30. package/dist/wgpuScene3DRuntime.d.ts.map +1 -1
  31. package/dist/wgpuScene3DRuntime.js +18 -10
  32. package/dist/wgpuScene3DRuntime.js.map +1 -1
  33. package/dist/wgpuScene3DTestHelper.d.ts.map +1 -1
  34. package/dist/wgpuScene3DTestHelper.js +126 -6
  35. package/dist/wgpuScene3DTestHelper.js.map +1 -1
  36. package/dist/wgpuShadedModifierSnippet.d.ts.map +1 -1
  37. package/dist/wgpuShadedModifierSnippet.js +8 -11
  38. package/dist/wgpuShadedModifierSnippet.js.map +1 -1
  39. package/dist/wgpuShadedPrelude.d.ts +5 -3
  40. package/dist/wgpuShadedPrelude.d.ts.map +1 -1
  41. package/dist/wgpuShadedPrelude.js +34 -13
  42. package/dist/wgpuShadedPrelude.js.map +1 -1
  43. package/dist/wgpuShadowMap.d.ts.map +1 -1
  44. package/dist/wgpuShadowMap.js +22 -6
  45. package/dist/wgpuShadowMap.js.map +1 -1
  46. package/dist/wgpuSkinPalette.d.ts +4 -1
  47. package/dist/wgpuSkinPalette.d.ts.map +1 -1
  48. package/dist/wgpuSkinPalette.js +227 -27
  49. package/dist/wgpuSkinPalette.js.map +1 -1
  50. package/dist/wgpuWireframeUpload.d.ts.map +1 -1
  51. package/dist/wgpuWireframeUpload.js +12 -6
  52. package/dist/wgpuWireframeUpload.js.map +1 -1
  53. package/package.json +20 -15
  54. package/src/customShaderWgpuMeshMaterialRenderer.test.ts +25 -1
  55. package/src/explainWgpuScene3DCoverage.test.ts +44 -10
  56. package/src/wgpuMeshMaterialRegistry.test.ts +22 -0
  57. package/src/wgpuMeshPipeline.test.ts +34 -1
  58. package/src/wgpuMeshUpload.test.ts +65 -3
  59. package/src/wgpuParticleEmitter3D.test.ts +23 -0
  60. package/src/wgpuScene3DRuntime.test.ts +8 -4
  61. package/src/wgpuScene3DTestHelper.test.ts +84 -0
  62. package/src/wgpuShadedModifierSnippet.test.ts +20 -6
  63. package/src/wgpuShadedPrelude.test.ts +6 -6
  64. package/src/wgpuShadowMap.test.ts +30 -0
  65. package/src/wgpuSkinPalette.test.ts +134 -19
  66. 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.skinPaletteCapacity).toBe(0);
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 first = ensureWgpuSkinDrawBindGroup(state, new Float32Array(16));
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
- expect(ensureWgpuSkinDrawBindGroup(state, new Float32Array(16))).toBe(first);
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
- ensureWgpuSkinDrawBindGroup(state, new Float32Array(32));
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 one row', () => {
134
+ it('packs four rgba32float texels per joint into a fixed-width arena row', () => {
63
135
  const { fake, state } = makeWgpuScene3DState();
64
- const joints = new Float32Array(32);
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(view).toBe(getWgpuScene3DRuntime(state).skinPaletteView);
70
- expect(texture?.args[0]).toMatchObject({
71
- format: 'rgba32float',
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('reuses capacity and grows without a joint-count fallback gate', () => {
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 * 80));
81
- uploadWgpuSkinPalette(state, new Float32Array(16 * 40));
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
- uploadWgpuSkinPalette(state, new Float32Array(16 * 160));
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
- expect(getWgpuScene3DRuntime(state).skinPaletteCapacity).toBe(160);
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();