@forgeax/engine-vfx-render 0.1.4 → 0.1.6
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/README.md +6 -12
- package/dist/.tsbuildinfo +1 -1
- package/dist/feature/gpu-particle-feature.d.ts +20 -26
- package/dist/feature/gpu-particle-feature.d.ts.map +1 -1
- package/dist/feature/particle-resources.d.ts +0 -3
- package/dist/feature/particle-resources.d.ts.map +1 -1
- package/dist/feature/stage-plan.d.ts +1 -1
- package/dist/feature/stage-plan.d.ts.map +1 -1
- package/dist/host/vfx-runtime-host.d.ts +6 -3
- package/dist/host/vfx-runtime-host.d.ts.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +452 -720
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -8
- package/src/__tests__/billboard-advanced.integration.test.ts +0 -23
- package/src/__tests__/fog-producer.integration.test.ts +4 -3
- package/src/__tests__/gpu-host.integration.test.ts +54 -120
- package/src/__tests__/particle-resources.unit.test.ts +0 -9
- package/src/__tests__/stage-plan.integration.test.ts +132 -0
- package/src/feature/gpu-particle-feature.ts +482 -888
- package/src/feature/particle-resources.ts +0 -8
- package/src/feature/stage-plan.ts +4 -1
- package/src/host/vfx-runtime-host.ts +34 -17
- package/src/index.ts +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgeax/engine-vfx-render",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -22,13 +22,13 @@
|
|
|
22
22
|
"LICENSE"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@forgeax/engine-assets-runtime": "0.1.
|
|
26
|
-
"@forgeax/engine-ecs": "0.1.
|
|
27
|
-
"@forgeax/engine-math": "0.1.
|
|
28
|
-
"@forgeax/engine-render": "0.1.
|
|
29
|
-
"@forgeax/engine-scene": "0.1.
|
|
30
|
-
"@forgeax/engine-types": "0.1.
|
|
31
|
-
"@forgeax/engine-vfx": "0.1.
|
|
25
|
+
"@forgeax/engine-assets-runtime": "0.1.6",
|
|
26
|
+
"@forgeax/engine-ecs": "0.1.6",
|
|
27
|
+
"@forgeax/engine-math": "0.1.6",
|
|
28
|
+
"@forgeax/engine-render": "0.1.6",
|
|
29
|
+
"@forgeax/engine-scene": "0.1.6",
|
|
30
|
+
"@forgeax/engine-types": "0.1.6",
|
|
31
|
+
"@forgeax/engine-vfx": "0.1.6"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {},
|
|
34
34
|
"forgeax": {
|
|
@@ -43,27 +43,4 @@ describe('billboard advanced executable inputs', () => {
|
|
|
43
43
|
error: { code: 'vfx-renderer-depth-missing' },
|
|
44
44
|
});
|
|
45
45
|
});
|
|
46
|
-
|
|
47
|
-
it('changes soft-particle alpha when scene depth changes before Fog mixing', () => {
|
|
48
|
-
const nearDepth = resolveBillboardAdvancedState(renderer, {
|
|
49
|
-
age: 0.1,
|
|
50
|
-
lifetime: 1,
|
|
51
|
-
particleDepth: 0.7,
|
|
52
|
-
sceneDepth: 0.75,
|
|
53
|
-
depthAvailable: true,
|
|
54
|
-
});
|
|
55
|
-
const farDepth = resolveBillboardAdvancedState(renderer, {
|
|
56
|
-
age: 0.1,
|
|
57
|
-
lifetime: 1,
|
|
58
|
-
particleDepth: 0.7,
|
|
59
|
-
sceneDepth: 1.0,
|
|
60
|
-
depthAvailable: true,
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
expect(nearDepth.ok).toBe(true);
|
|
64
|
-
expect(farDepth.ok).toBe(true);
|
|
65
|
-
if (nearDepth.ok && farDepth.ok) {
|
|
66
|
-
expect(farDepth.value.softParticleFade).toBeGreaterThan(nearDepth.value.softParticleFade);
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
46
|
});
|
|
@@ -29,7 +29,7 @@ describe('VFX Fog producer contract', () => {
|
|
|
29
29
|
}
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
it('keeps VFX coverage on the
|
|
32
|
+
it('keeps VFX coverage on the current graphics feature lane', () => {
|
|
33
33
|
const feature = readFileSync(
|
|
34
34
|
fileURLToPath(new URL('../feature/gpu-particle-feature.ts', import.meta.url)),
|
|
35
35
|
'utf8',
|
|
@@ -38,9 +38,10 @@ describe('VFX Fog producer contract', () => {
|
|
|
38
38
|
fileURLToPath(new URL('../feature/camera.ts', import.meta.url)),
|
|
39
39
|
'utf8',
|
|
40
40
|
);
|
|
41
|
-
expect(feature).toContain(
|
|
41
|
+
expect(feature).toContain('requiredCapabilities');
|
|
42
|
+
expect(feature).toContain('identity: IDENTITY');
|
|
42
43
|
expect(feature).not.toMatch(/fog(?:History|Texture|Registry)/i);
|
|
43
|
-
expect(feature).toContain('
|
|
44
|
+
expect(feature).toContain('worldCenter');
|
|
44
45
|
expect(camera).toContain('position');
|
|
45
46
|
expect(camera).toContain('viewProjection');
|
|
46
47
|
});
|
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
import { createAssetRegistry, createCatalogSource } from '@forgeax/engine-assets-runtime';
|
|
2
1
|
import { World } from '@forgeax/engine-ecs';
|
|
3
|
-
import { RenderFeaturePreparationFailedError } from '@forgeax/engine-render';
|
|
4
|
-
import { err, ok } from '@forgeax/engine-types';
|
|
5
2
|
import {
|
|
6
3
|
ParticleEffectPlayer,
|
|
7
4
|
VFX_GPU_RUNTIME_RESOURCE_KEY,
|
|
8
5
|
type VfxGpuRuntime,
|
|
9
6
|
type VfxGpuTickIntent,
|
|
10
|
-
vfxGpuEffectContribution,
|
|
11
7
|
} from '@forgeax/engine-vfx';
|
|
12
|
-
import { describe, expect, it
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
13
9
|
import { gpuParticleRenderFeature } from '../feature/gpu-particle-feature.js';
|
|
14
10
|
import {
|
|
15
11
|
createCameraProvider,
|
|
@@ -21,12 +17,10 @@ import {
|
|
|
21
17
|
describe('GPU VFX public host', () => {
|
|
22
18
|
it('owns loader and FixedUpdate attachment without exposing simulation internals', async () => {
|
|
23
19
|
const world = new World();
|
|
24
|
-
const
|
|
20
|
+
const registered: unknown[] = [];
|
|
25
21
|
const assets = {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return { kind: 'particle-effect', dispose: () => {} };
|
|
29
|
-
},
|
|
22
|
+
loaders: { registerPackLoader: (loader: unknown) => registered.push(loader) },
|
|
23
|
+
lookup: () => undefined,
|
|
30
24
|
};
|
|
31
25
|
const host = createVfxRuntimeHost({
|
|
32
26
|
camera: {
|
|
@@ -41,45 +35,21 @@ describe('GPU VFX public host', () => {
|
|
|
41
35
|
|
|
42
36
|
const attached = await host.attachWorld({ world, assets: assets as never });
|
|
43
37
|
expect(attached).toMatchObject({ ok: true, value: { state: 'attached' } });
|
|
44
|
-
expect(
|
|
38
|
+
expect(registered).toHaveLength(1);
|
|
45
39
|
expect(host.feature.requiredMaterialShaders).toEqual(
|
|
46
40
|
Object.values(PARTICLE_SHADER_IDENTIFIERS),
|
|
47
41
|
);
|
|
48
|
-
expect(host.feature.inspect()).toEqual({
|
|
49
|
-
frameNumber: -1,
|
|
50
|
-
dispatches: 0,
|
|
51
|
-
indirectDraws: 0,
|
|
52
|
-
subjectOutputs: 0,
|
|
53
|
-
});
|
|
54
|
-
expect(await host.detachWorld({ world })).toMatchObject({
|
|
55
|
-
ok: true,
|
|
56
|
-
value: { state: 'detached' },
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('shares an App-selected GPU decoder when the host attaches the same registry', async () => {
|
|
61
|
-
const world = new World();
|
|
62
|
-
const assets = createAssetRegistry({
|
|
63
|
-
catalog: createCatalogSource({ entries: [] }),
|
|
64
|
-
});
|
|
65
|
-
assets.installDecoder(vfxGpuEffectContribution.kind, vfxGpuEffectContribution.decoder);
|
|
66
|
-
const host = createVfxRuntimeHost({ camera: { read: () => undefined } });
|
|
67
|
-
|
|
68
|
-
expect(await host.attachWorld({ world, assets })).toMatchObject({
|
|
69
|
-
ok: true,
|
|
70
|
-
value: { state: 'attached' },
|
|
71
|
-
});
|
|
72
42
|
expect(await host.detachWorld({ world })).toMatchObject({
|
|
73
43
|
ok: true,
|
|
74
44
|
value: { state: 'detached' },
|
|
75
45
|
});
|
|
76
|
-
assets.dispose();
|
|
77
46
|
});
|
|
78
47
|
|
|
79
48
|
it('fences preview controls to one attached host generation', async () => {
|
|
80
49
|
const world = new World();
|
|
81
50
|
const assets = {
|
|
82
|
-
|
|
51
|
+
loaders: { registerPackLoader: () => {} },
|
|
52
|
+
lookup: () => undefined,
|
|
83
53
|
};
|
|
84
54
|
const host = createVfxRuntimeHost({ camera: { read: () => undefined } });
|
|
85
55
|
expect(host.acquireControl(world)).toMatchObject({
|
|
@@ -145,7 +115,7 @@ describe('GPU VFX public host', () => {
|
|
|
145
115
|
expect(host.acquireControl(world)).toMatchObject({ ok: true, value: { generation: 2 } });
|
|
146
116
|
});
|
|
147
117
|
|
|
148
|
-
it('
|
|
118
|
+
it('declares every pending emitter program in one feature plan', () => {
|
|
149
119
|
const world = new World();
|
|
150
120
|
const player = world.spawn().unwrap();
|
|
151
121
|
const intent = (id: string): VfxGpuTickIntent => ({
|
|
@@ -206,45 +176,41 @@ describe('GPU VFX public host', () => {
|
|
|
206
176
|
lastSequence: -1,
|
|
207
177
|
},
|
|
208
178
|
});
|
|
209
|
-
const prepareProgram = vi.fn((name: string) =>
|
|
210
|
-
err(
|
|
211
|
-
new RenderFeaturePreparationFailedError(
|
|
212
|
-
'forgeax.vfx-render.gpu-particles',
|
|
213
|
-
-1,
|
|
214
|
-
'prepare-gpu-program',
|
|
215
|
-
'pipeline',
|
|
216
|
-
name,
|
|
217
|
-
'rhi-not-available:compile pending',
|
|
218
|
-
'next-frame',
|
|
219
|
-
),
|
|
220
|
-
),
|
|
221
|
-
);
|
|
222
179
|
const feature = gpuParticleRenderFeature({ camera: { read: () => undefined } });
|
|
223
|
-
|
|
224
|
-
const prepared = feature.prepare(
|
|
180
|
+
const planned = feature.plan(
|
|
225
181
|
{
|
|
226
182
|
worlds: [
|
|
227
183
|
{
|
|
228
184
|
world,
|
|
229
|
-
runtime: {
|
|
230
|
-
|
|
185
|
+
runtime: {
|
|
186
|
+
isEmitterSessionEnabled: () => true,
|
|
187
|
+
setEmitterCameraVisibility: () => {},
|
|
188
|
+
markEventDispatched: () => {},
|
|
189
|
+
commit: () => {},
|
|
190
|
+
},
|
|
191
|
+
camera: {
|
|
192
|
+
position: new Float32Array(3),
|
|
193
|
+
right: new Float32Array([1, 0, 0]),
|
|
194
|
+
up: new Float32Array([0, 1, 0]),
|
|
195
|
+
viewProjection: new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
|
|
196
|
+
},
|
|
231
197
|
intents: [intent('first'), intent('second')],
|
|
232
198
|
},
|
|
233
199
|
],
|
|
234
200
|
frameNumber: 1,
|
|
235
201
|
} as never,
|
|
236
|
-
{
|
|
202
|
+
{ targets: [], caps: {}, frame: { frameNumber: 1 }, generation: 1 } as never,
|
|
237
203
|
);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
expect(
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
]);
|
|
204
|
+
expect(planned.ok).toBe(true);
|
|
205
|
+
if (!planned.ok) return;
|
|
206
|
+
expect(
|
|
207
|
+
planned.value.resources
|
|
208
|
+
.filter((resource) => resource.kind === 'compute-program')
|
|
209
|
+
.map((resource) => resource.name),
|
|
210
|
+
).toEqual([expect.stringContaining('.first.'), expect.stringContaining('.second.')]);
|
|
245
211
|
});
|
|
246
212
|
|
|
247
|
-
it('
|
|
213
|
+
it('keeps masked bindings warm and sends effect-relative time to WGSL', () => {
|
|
248
214
|
const world = new World();
|
|
249
215
|
const player = world.spawn().unwrap();
|
|
250
216
|
const intent = (tick: number): VfxGpuTickIntent => ({
|
|
@@ -306,38 +272,11 @@ describe('GPU VFX public host', () => {
|
|
|
306
272
|
},
|
|
307
273
|
});
|
|
308
274
|
let sessionEnabled = true;
|
|
309
|
-
let renderConsumptionEnabled = true;
|
|
310
|
-
let generation = 0;
|
|
311
|
-
const liveBindings = new Set<object>();
|
|
312
|
-
const prepareProgram = vi.fn(() => ok({ generation: 1 } as never));
|
|
313
|
-
const prepareBuffer = vi.fn((_name: string, _descriptor: { readonly data?: Uint8Array }) =>
|
|
314
|
-
ok({ generation: 1 } as never),
|
|
315
|
-
);
|
|
316
|
-
const prepareBindings = vi.fn(() => {
|
|
317
|
-
generation += 1;
|
|
318
|
-
const reference = { generation: 1, id: generation };
|
|
319
|
-
liveBindings.add(reference);
|
|
320
|
-
return ok(reference as never);
|
|
321
|
-
});
|
|
322
|
-
const retainBindings = vi.fn((references: readonly object[]) =>
|
|
323
|
-
references.every((reference) => liveBindings.has(reference))
|
|
324
|
-
? ok(undefined)
|
|
325
|
-
: err(
|
|
326
|
-
new RenderFeaturePreparationFailedError(
|
|
327
|
-
'forgeax.vfx-render.gpu-particles',
|
|
328
|
-
-1,
|
|
329
|
-
'retain-gpu-bindings',
|
|
330
|
-
'bindings',
|
|
331
|
-
'persistent-bindings',
|
|
332
|
-
'bindings-unavailable',
|
|
333
|
-
'next-frame',
|
|
334
|
-
),
|
|
335
|
-
),
|
|
336
|
-
);
|
|
337
275
|
const runtime = {
|
|
338
|
-
hasPlayer: () => true,
|
|
339
276
|
isEmitterSessionEnabled: () => sessionEnabled,
|
|
340
277
|
setEmitterCameraVisibility: () => {},
|
|
278
|
+
markEventDispatched: () => {},
|
|
279
|
+
commit: () => {},
|
|
341
280
|
};
|
|
342
281
|
const camera = {
|
|
343
282
|
position: new Float32Array(3),
|
|
@@ -345,45 +284,39 @@ describe('GPU VFX public host', () => {
|
|
|
345
284
|
up: new Float32Array([0, 1, 0]),
|
|
346
285
|
viewProjection: new Float32Array(16),
|
|
347
286
|
};
|
|
348
|
-
const feature = gpuParticleRenderFeature({
|
|
349
|
-
|
|
350
|
-
playerConsumption: { isEnabled: () => renderConsumptionEnabled },
|
|
351
|
-
});
|
|
352
|
-
const context = {
|
|
353
|
-
gpu: { prepareProgram, prepareBuffer, prepareBindings, retainBindings },
|
|
354
|
-
targets: [],
|
|
355
|
-
} as never;
|
|
287
|
+
const feature = gpuParticleRenderFeature({ camera: { read: () => camera } });
|
|
288
|
+
const context = { targets: [], caps: {}, frame: { frameNumber: 1 }, generation: 1 } as never;
|
|
356
289
|
const frame = (intents: readonly VfxGpuTickIntent[], frameNumber: number) =>
|
|
357
290
|
({
|
|
358
291
|
worlds: [{ world, runtime, camera, intents }],
|
|
359
292
|
frameNumber,
|
|
360
293
|
}) as never;
|
|
361
294
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
295
|
+
const active = feature.plan(frame([intent(0)], 1), context);
|
|
296
|
+
expect(active.ok).toBe(true);
|
|
297
|
+
if (!active.ok) return;
|
|
298
|
+
const runtimeWrite = active.value.resources.find(
|
|
299
|
+
(resource) => resource.kind === 'buffer' && resource.name.endsWith('.runtime'),
|
|
300
|
+
);
|
|
301
|
+
expect(runtimeWrite?.kind === 'buffer' ? runtimeWrite.data : undefined).toBeInstanceOf(
|
|
302
|
+
Uint8Array,
|
|
303
|
+
);
|
|
304
|
+
const runtimeBytes = runtimeWrite?.kind === 'buffer' ? runtimeWrite.data : undefined;
|
|
305
|
+
expect(new Uint32Array((runtimeBytes as Uint8Array).buffer)[1]).toBe(7);
|
|
372
306
|
sessionEnabled = false;
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
// can be re-enabled without a fresh simulation intent, so the masked frame
|
|
376
|
-
// must keep its prepared bindings warm while contributing no work.
|
|
377
|
-
expect(retainBindings).toHaveBeenCalledTimes(2);
|
|
307
|
+
const masked = feature.plan(frame([intent(1)], 2), context);
|
|
308
|
+
expect(masked).toMatchObject({ ok: true, value: { resources: [], passes: [] } });
|
|
378
309
|
sessionEnabled = true;
|
|
379
|
-
|
|
380
|
-
expect(
|
|
310
|
+
const resumed = feature.plan(frame([intent(2)], 3), context);
|
|
311
|
+
expect(resumed.ok).toBe(true);
|
|
312
|
+
if (resumed.ok) expect(resumed.value.resources.length).toBeGreaterThan(0);
|
|
381
313
|
});
|
|
382
314
|
|
|
383
315
|
it('rejects a second host without replacing the first World runtime', async () => {
|
|
384
316
|
const world = new World();
|
|
385
317
|
const assets = {
|
|
386
|
-
|
|
318
|
+
loaders: { registerPackLoader: () => {} },
|
|
319
|
+
lookup: () => undefined,
|
|
387
320
|
};
|
|
388
321
|
const options = {
|
|
389
322
|
camera: {
|
|
@@ -426,7 +359,8 @@ describe('GPU VFX public host', () => {
|
|
|
426
359
|
it('returns an empty keyed inspection aggregate before a player is present', async () => {
|
|
427
360
|
const world = new World();
|
|
428
361
|
const assets = {
|
|
429
|
-
|
|
362
|
+
loaders: { registerPackLoader: () => {} },
|
|
363
|
+
lookup: () => undefined,
|
|
430
364
|
};
|
|
431
365
|
const host = createVfxRuntimeHost({ camera: { read: () => undefined } });
|
|
432
366
|
await host.attachWorld({ world, assets: assets as never });
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
canonicalMeshVertices,
|
|
5
5
|
particleMaterialPass,
|
|
6
6
|
particleMaterialUsesBindings,
|
|
7
|
-
particleMaterialUsesSceneDepth,
|
|
8
7
|
} from '../feature/particle-resources.js';
|
|
9
8
|
|
|
10
9
|
describe('particle mesh resources', () => {
|
|
@@ -54,14 +53,6 @@ describe('particle material pass', () => {
|
|
|
54
53
|
});
|
|
55
54
|
});
|
|
56
55
|
|
|
57
|
-
it('uses the renderer-owned shader contract for scene-depth selection', () => {
|
|
58
|
-
expect(particleMaterialUsesSceneDepth('group-0-resource')).toBe(true);
|
|
59
|
-
expect(particleMaterialUsesSceneDepth('view-with-resource')).toBe(true);
|
|
60
|
-
expect(particleMaterialUsesSceneDepth('group-0')).toBe(false);
|
|
61
|
-
expect(particleMaterialUsesSceneDepth('view-only')).toBe(false);
|
|
62
|
-
expect(particleMaterialUsesSceneDepth('render-material')).toBe(false);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
56
|
it('does not mistake an ordinary Forward pass for a particle shader', () => {
|
|
66
57
|
const material: MaterialAsset = {
|
|
67
58
|
kind: 'material',
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
+
import type { RenderFeaturePlan } from '@forgeax/engine-render';
|
|
4
|
+
import type { VfxGpuTickIntent } from '@forgeax/engine-vfx';
|
|
3
5
|
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import { freezeRenderFeaturePlan } from '../../../render/src/features/plan';
|
|
7
|
+
import { gpuParticleRenderFeature } from '../feature/gpu-particle-feature.js';
|
|
4
8
|
|
|
5
9
|
const stagePlanPath = resolve(import.meta.dirname, '../feature/stage-plan.ts');
|
|
6
10
|
|
|
@@ -13,4 +17,132 @@ describe('VFX managed stage execution seam', () => {
|
|
|
13
17
|
expect(source).toContain('forgeax_vfx_stage_');
|
|
14
18
|
expect(source).not.toContain('authorRawDispatch');
|
|
15
19
|
});
|
|
20
|
+
|
|
21
|
+
it('projects a VFX stage into the closed render feature plan surface', () => {
|
|
22
|
+
const source = readFileSync(
|
|
23
|
+
resolve(import.meta.dirname, '../feature/gpu-particle-feature.ts'),
|
|
24
|
+
'utf8',
|
|
25
|
+
);
|
|
26
|
+
const plan: RenderFeaturePlan = { resources: [], passes: [] };
|
|
27
|
+
expect(source).toContain('RenderFeaturePlan');
|
|
28
|
+
expect(source).not.toMatch(/GPUCommandEncoder|queue\.submit|encoder\.finish/);
|
|
29
|
+
expect(plan.passes).toEqual([]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('declares storage-produced indirect work and dependent raster descriptors', () => {
|
|
33
|
+
const intent = {
|
|
34
|
+
sequence: 1,
|
|
35
|
+
player: 1,
|
|
36
|
+
emitter: {
|
|
37
|
+
id: 'spark',
|
|
38
|
+
capacity: 4,
|
|
39
|
+
space: 'world',
|
|
40
|
+
bounds: { kind: 'sphere', center: [0, 0, 0], radius: 1 },
|
|
41
|
+
wgsl: '// cooked VFX program',
|
|
42
|
+
reflection: {
|
|
43
|
+
entryPoints: ['forgeax_vfx_spawn_main', 'forgeax_vfx_billboard_main'],
|
|
44
|
+
bindings: [
|
|
45
|
+
{
|
|
46
|
+
entries: [
|
|
47
|
+
{ binding: 0, visibility: 4, buffer: { type: 'storage' } },
|
|
48
|
+
{ binding: 1, visibility: 4, buffer: { type: 'uniform' } },
|
|
49
|
+
{ binding: 4, visibility: 4, buffer: { type: 'storage' } },
|
|
50
|
+
{ binding: 6, visibility: 4, buffer: { type: 'storage' } },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
stages: [],
|
|
55
|
+
dataInterfaces: [],
|
|
56
|
+
},
|
|
57
|
+
renderers: [
|
|
58
|
+
{
|
|
59
|
+
kind: 'billboard',
|
|
60
|
+
material: 'material-guid',
|
|
61
|
+
blend: 'additive',
|
|
62
|
+
sorting: 'none',
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
programFingerprint: 'spark-program',
|
|
67
|
+
reset: true,
|
|
68
|
+
fixedDelta: 1 / 60,
|
|
69
|
+
phaseTick: 0,
|
|
70
|
+
tick: 0,
|
|
71
|
+
seed: 1,
|
|
72
|
+
playCycle: 0,
|
|
73
|
+
spawnCount: 1,
|
|
74
|
+
firstParticleId: 0,
|
|
75
|
+
instanceGeneration: 1,
|
|
76
|
+
channelInputs: [],
|
|
77
|
+
eventCounters: {},
|
|
78
|
+
} as unknown as VfxGpuTickIntent;
|
|
79
|
+
const runtime = {
|
|
80
|
+
isEmitterSessionEnabled: () => true,
|
|
81
|
+
setEmitterCameraVisibility: () => {},
|
|
82
|
+
markEventDispatched: () => {},
|
|
83
|
+
commit: () => {},
|
|
84
|
+
};
|
|
85
|
+
const feature = gpuParticleRenderFeature({ camera: { read: () => undefined } });
|
|
86
|
+
const planned = feature.plan(
|
|
87
|
+
{
|
|
88
|
+
worlds: [
|
|
89
|
+
{
|
|
90
|
+
world: {},
|
|
91
|
+
runtime,
|
|
92
|
+
camera: {
|
|
93
|
+
position: new Float32Array(3),
|
|
94
|
+
right: new Float32Array([1, 0, 0]),
|
|
95
|
+
up: new Float32Array([0, 1, 0]),
|
|
96
|
+
viewProjection: new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
|
|
97
|
+
},
|
|
98
|
+
intents: [intent],
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
frameNumber: 1,
|
|
102
|
+
} as never,
|
|
103
|
+
{
|
|
104
|
+
caps: {} as never,
|
|
105
|
+
frame: { frameNumber: 1 },
|
|
106
|
+
generation: 1,
|
|
107
|
+
targets: [
|
|
108
|
+
{ name: 'scene-color', kind: 'color', format: 'rgba16float', sampleCount: 1 },
|
|
109
|
+
{ name: 'scene-depth', kind: 'depth', format: 'depth24plus', sampleCount: 1 },
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
expect(planned.ok).toBe(true);
|
|
115
|
+
if (!planned.ok) return;
|
|
116
|
+
expect(
|
|
117
|
+
freezeRenderFeaturePlan(feature.identity, planned.value, [
|
|
118
|
+
{ name: 'scene-color', kind: 'color', format: 'rgba16float', sampleCount: 1 },
|
|
119
|
+
{ name: 'scene-depth', kind: 'depth', format: 'depth24plus', sampleCount: 1 },
|
|
120
|
+
]).ok,
|
|
121
|
+
).toBe(true);
|
|
122
|
+
expect(planned.value.resources).toEqual(
|
|
123
|
+
expect.arrayContaining([
|
|
124
|
+
expect.objectContaining({
|
|
125
|
+
kind: 'buffer',
|
|
126
|
+
name: expect.stringContaining('.indirect'),
|
|
127
|
+
usage: ['storage', 'indirect'],
|
|
128
|
+
}),
|
|
129
|
+
expect.objectContaining({ kind: 'graphics-program' }),
|
|
130
|
+
expect.objectContaining({ kind: 'graphics-bindings' }),
|
|
131
|
+
expect.objectContaining({ kind: 'vertex-data' }),
|
|
132
|
+
]),
|
|
133
|
+
);
|
|
134
|
+
expect(planned.value.passes).toEqual(
|
|
135
|
+
expect.arrayContaining([
|
|
136
|
+
expect.objectContaining({ kind: 'compute' }),
|
|
137
|
+
expect.objectContaining({
|
|
138
|
+
kind: 'raster',
|
|
139
|
+
draws: [
|
|
140
|
+
expect.objectContaining({
|
|
141
|
+
draw: expect.objectContaining({ kind: 'draw-indirect' }),
|
|
142
|
+
}),
|
|
143
|
+
],
|
|
144
|
+
}),
|
|
145
|
+
]),
|
|
146
|
+
);
|
|
147
|
+
});
|
|
16
148
|
});
|