@forgeax/engine-vfx-render 0.1.6 → 0.1.7
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/.tsbuildinfo +1 -1
- package/dist/host/vfx-runtime-host.d.ts +18 -5
- package/dist/host/vfx-runtime-host.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +11 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -8
- package/src/__tests__/gpu-host.integration.test.ts +84 -0
- package/src/host/vfx-runtime-host.ts +56 -16
- package/src/index.ts +1 -0
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.7",
|
|
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.7",
|
|
26
|
+
"@forgeax/engine-ecs": "0.1.7",
|
|
27
|
+
"@forgeax/engine-math": "0.1.7",
|
|
28
|
+
"@forgeax/engine-render": "0.1.7",
|
|
29
|
+
"@forgeax/engine-scene": "0.1.7",
|
|
30
|
+
"@forgeax/engine-types": "0.1.7",
|
|
31
|
+
"@forgeax/engine-vfx": "0.1.7"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {},
|
|
34
34
|
"forgeax": {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { World } from '@forgeax/engine-ecs';
|
|
2
|
+
import type { Asset, MaterialAsset } from '@forgeax/engine-types';
|
|
2
3
|
import {
|
|
3
4
|
ParticleEffectPlayer,
|
|
4
5
|
VFX_GPU_RUNTIME_RESOURCE_KEY,
|
|
@@ -45,6 +46,89 @@ describe('GPU VFX public host', () => {
|
|
|
45
46
|
});
|
|
46
47
|
});
|
|
47
48
|
|
|
49
|
+
it('preserves the asset registry receiver while planning a material projection', async () => {
|
|
50
|
+
const world = new World();
|
|
51
|
+
const lookups: string[] = [];
|
|
52
|
+
class StatefulAssets {
|
|
53
|
+
readonly material: MaterialAsset = { kind: 'material', values: {} };
|
|
54
|
+
readonly loaders = { registerPackLoader: () => {} };
|
|
55
|
+
|
|
56
|
+
lookup<T extends Asset = Asset>(guid: string): T | undefined {
|
|
57
|
+
if (this !== assets) throw new Error('asset registry receiver was lost');
|
|
58
|
+
lookups.push(guid);
|
|
59
|
+
return guid === 'material-guid' ? (this.material as T) : undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const assets = new StatefulAssets();
|
|
63
|
+
const host = createVfxRuntimeHost({
|
|
64
|
+
camera: {
|
|
65
|
+
read: () => ({
|
|
66
|
+
position: new Float32Array(3),
|
|
67
|
+
right: new Float32Array([1, 0, 0]),
|
|
68
|
+
up: new Float32Array([0, 1, 0]),
|
|
69
|
+
viewProjection: new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
|
|
70
|
+
}),
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
expect(await host.attachWorld({ world, assets })).toMatchObject({ ok: true });
|
|
74
|
+
|
|
75
|
+
const intent = {
|
|
76
|
+
player: world.spawn().unwrap(),
|
|
77
|
+
fixedDelta: 1 / 60,
|
|
78
|
+
phaseTick: 0,
|
|
79
|
+
seed: 1,
|
|
80
|
+
playCycle: 0,
|
|
81
|
+
spawnCount: 1,
|
|
82
|
+
firstParticleId: 0,
|
|
83
|
+
reset: true,
|
|
84
|
+
channelInputs: [],
|
|
85
|
+
emitter: {
|
|
86
|
+
id: 'receiver-check',
|
|
87
|
+
capacity: 4,
|
|
88
|
+
backend: { required: 'gpu' },
|
|
89
|
+
space: 'world',
|
|
90
|
+
simulationWhenCulled: 'continue',
|
|
91
|
+
schedule: { rate: 0, bursts: [] },
|
|
92
|
+
bounds: { kind: 'sphere', center: [0, 0, 0], radius: 1 },
|
|
93
|
+
wgsl: '// receiver check',
|
|
94
|
+
reflection: { entryPoints: [], bindings: [] },
|
|
95
|
+
renderers: [{ kind: 'billboard', material: 'material-guid' }],
|
|
96
|
+
},
|
|
97
|
+
} as unknown as VfxGpuTickIntent;
|
|
98
|
+
const planned = host.feature.plan(
|
|
99
|
+
{
|
|
100
|
+
worlds: [
|
|
101
|
+
{
|
|
102
|
+
world,
|
|
103
|
+
runtime: {
|
|
104
|
+
isEmitterSessionEnabled: () => true,
|
|
105
|
+
setEmitterCameraVisibility: () => {},
|
|
106
|
+
markEventDispatched: () => {},
|
|
107
|
+
commit: () => {},
|
|
108
|
+
},
|
|
109
|
+
camera: {
|
|
110
|
+
position: new Float32Array(3),
|
|
111
|
+
right: new Float32Array([1, 0, 0]),
|
|
112
|
+
up: new Float32Array([0, 1, 0]),
|
|
113
|
+
viewProjection: new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
|
|
114
|
+
},
|
|
115
|
+
intents: [intent],
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
frameNumber: 1,
|
|
119
|
+
} as never,
|
|
120
|
+
{
|
|
121
|
+
targets: [],
|
|
122
|
+
caps: {},
|
|
123
|
+
frame: { frameNumber: 1 },
|
|
124
|
+
generation: 1,
|
|
125
|
+
} as never,
|
|
126
|
+
);
|
|
127
|
+
expect(planned.ok).toBe(true);
|
|
128
|
+
expect(lookups).toEqual(['material-guid']);
|
|
129
|
+
await host.detachWorld({ world });
|
|
130
|
+
});
|
|
131
|
+
|
|
48
132
|
it('fences preview controls to one attached host generation', async () => {
|
|
49
133
|
const world = new World();
|
|
50
134
|
const assets = {
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from '@forgeax/engine-assets-runtime
|
|
1
|
+
import type {
|
|
2
|
+
AssetRegistryResolver,
|
|
3
|
+
AssetRegistry as LegacyAssetRegistry,
|
|
4
|
+
RuntimeAssetRegistry,
|
|
5
|
+
} from '@forgeax/engine-assets-runtime';
|
|
6
|
+
import { getAssetRegistryResolver } from '@forgeax/engine-assets-runtime';
|
|
7
7
|
import { createWorldContext, type EntityHandle, type World } from '@forgeax/engine-ecs';
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
Asset,
|
|
10
|
+
AssetDecoderLease,
|
|
11
|
+
MaterialAsset,
|
|
12
|
+
MeshAsset,
|
|
13
|
+
Result,
|
|
14
|
+
} from '@forgeax/engine-types';
|
|
9
15
|
import { err, ok } from '@forgeax/engine-types';
|
|
10
16
|
import type {
|
|
11
17
|
VfxDataInterfaceError,
|
|
@@ -32,7 +38,22 @@ import {
|
|
|
32
38
|
type VfxDataInterfaceRegistry,
|
|
33
39
|
} from './data-interface-providers.js';
|
|
34
40
|
|
|
35
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Public authoring projection accepted by the VFX host. Editor owns the
|
|
43
|
+
* projection, while the Engine App still owns bytes/decoders/cache. Keeping
|
|
44
|
+
* this small structural seam public lets the Editor pass that projection
|
|
45
|
+
* without importing the runtime registry's internal module or fabricating a
|
|
46
|
+
* second registry identity.
|
|
47
|
+
*/
|
|
48
|
+
export interface VfxAuthoringAssetRegistry {
|
|
49
|
+
readonly loaders: {
|
|
50
|
+
registerPackLoader(loader: unknown): void;
|
|
51
|
+
};
|
|
52
|
+
lookup<T extends Asset = Asset>(guid: string): T | undefined;
|
|
53
|
+
readonly load?: (guid: string, kind: string) => Promise<Result<unknown, unknown>>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type AssetRegistry = LegacyAssetRegistry | RuntimeAssetRegistry | VfxAuthoringAssetRegistry;
|
|
36
57
|
|
|
37
58
|
export interface VfxRuntimeHostOptions {
|
|
38
59
|
readonly camera: ParticleRenderCameraSource;
|
|
@@ -113,7 +134,7 @@ export interface VfxRuntimeHost {
|
|
|
113
134
|
}
|
|
114
135
|
|
|
115
136
|
/** Install the VFX owner decoder into the core registry. */
|
|
116
|
-
export function installVfxRuntimeDecoder(registry:
|
|
137
|
+
export function installVfxRuntimeDecoder(registry: RuntimeAssetRegistry): AssetDecoderLease {
|
|
117
138
|
return registry.installDecoder(vfxGpuEffectContribution.kind, vfxGpuEffectContribution.decoder);
|
|
118
139
|
}
|
|
119
140
|
|
|
@@ -174,7 +195,12 @@ export function createVfxRuntimeHost(options: VfxRuntimeHostOptions): VfxRuntime
|
|
|
174
195
|
const cached = attached.renderAssets.get(key);
|
|
175
196
|
if (cached?.kind === kind) return cached as Kind extends 'material' ? MaterialAsset : MeshAsset;
|
|
176
197
|
if ('lookup' in attached.assets) {
|
|
177
|
-
const
|
|
198
|
+
const lookup = attached.assets.lookup as (guid: string) => Asset | undefined;
|
|
199
|
+
// Preserve the registry receiver: the Engine-owned legacy class uses
|
|
200
|
+
// `this` to read its load-state/catalogue maps. The Editor facade may
|
|
201
|
+
// expose an arrow function, but calling through the owner is safe for
|
|
202
|
+
// both shapes and avoids turning a lookup into a swallowed plan error.
|
|
203
|
+
const legacy = lookup.call(attached.assets, guid);
|
|
178
204
|
if (legacy?.kind === kind) {
|
|
179
205
|
attached.renderAssets.set(key, legacy);
|
|
180
206
|
return legacy as Kind extends 'material' ? MaterialAsset : MeshAsset;
|
|
@@ -185,10 +211,17 @@ export function createVfxRuntimeHost(options: VfxRuntimeHostOptions): VfxRuntime
|
|
|
185
211
|
attached.renderAssets.set(key, resolved);
|
|
186
212
|
return resolved as Kind extends 'material' ? MaterialAsset : MeshAsset;
|
|
187
213
|
}
|
|
188
|
-
if (
|
|
214
|
+
if (
|
|
215
|
+
'load' in attached.assets &&
|
|
216
|
+
typeof attached.assets.load === 'function' &&
|
|
217
|
+
!attached.pendingRenderAssets.has(key)
|
|
218
|
+
) {
|
|
189
219
|
const epoch = attached.catalogEpoch;
|
|
190
|
-
const
|
|
191
|
-
|
|
220
|
+
const load = attached.assets.load as (
|
|
221
|
+
guid: string,
|
|
222
|
+
kind: string,
|
|
223
|
+
) => Promise<Result<MaterialAsset | MeshAsset, unknown>>;
|
|
224
|
+
const request = load(guid, kind)
|
|
192
225
|
.then((result) => {
|
|
193
226
|
if (!result.ok || worlds.get(world) !== attached || attached.catalogEpoch !== epoch)
|
|
194
227
|
return;
|
|
@@ -327,10 +360,17 @@ export function createVfxRuntimeHost(options: VfxRuntimeHostOptions): VfxRuntime
|
|
|
327
360
|
if (worlds.has(world)) return ok({ state: 'already-attached' });
|
|
328
361
|
if (!registries.has(assets)) {
|
|
329
362
|
try {
|
|
330
|
-
|
|
363
|
+
// The Editor facade deliberately exposes the legacy LoaderRegistry
|
|
364
|
+
// while projecting the Engine-owned App.assets instance. Prefer
|
|
365
|
+
// that public authoring seam before looking for the five-action
|
|
366
|
+
// runtime registry; otherwise a facade that happens to expose
|
|
367
|
+
// `load` would be misclassified as a decoder owner.
|
|
368
|
+
if ('loaders' in assets) {
|
|
369
|
+
assets.loaders.registerPackLoader(vfxGpuEffectPackLoader);
|
|
370
|
+
} else if ('installDecoder' in assets) {
|
|
331
371
|
assets.installDecoder(vfxGpuEffectContribution.kind, vfxGpuEffectContribution.decoder);
|
|
332
372
|
} else {
|
|
333
|
-
assets
|
|
373
|
+
throw new TypeError('VFX assets registry exposes neither loaders nor installDecoder');
|
|
334
374
|
}
|
|
335
375
|
registries.add(assets);
|
|
336
376
|
} catch (cause) {
|
|
@@ -362,7 +402,7 @@ export function createVfxRuntimeHost(options: VfxRuntimeHostOptions): VfxRuntime
|
|
|
362
402
|
);
|
|
363
403
|
}
|
|
364
404
|
let resolver: AssetRegistryResolver | undefined;
|
|
365
|
-
if ('installDecoder' in assets) {
|
|
405
|
+
if (!('loaders' in assets) && 'installDecoder' in assets) {
|
|
366
406
|
try {
|
|
367
407
|
resolver = getAssetRegistryResolver(assets);
|
|
368
408
|
} catch {
|