@forgeax/engine-vfx 0.1.27 → 0.1.29
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 +62 -14
- package/dist/assets/particle-effect-decoder.d.ts.map +1 -1
- package/dist/authoring-descriptor.d.ts +4 -4
- package/dist/authoring-descriptor.d.ts.map +1 -1
- package/dist/code-source-v3.d.ts +101 -0
- package/dist/code-source-v3.d.ts.map +1 -0
- package/dist/code-source.d.ts +12 -11
- package/dist/code-source.d.ts.map +1 -1
- package/dist/data-interface.d.ts +17 -1
- package/dist/data-interface.d.ts.map +1 -1
- package/dist/effect-contract.d.ts +11 -1
- package/dist/effect-contract.d.ts.map +1 -1
- package/dist/gpu-loader.d.ts +5 -5
- package/dist/gpu-loader.d.ts.map +1 -1
- package/dist/gpu-program.d.ts +52 -27
- package/dist/gpu-program.d.ts.map +1 -1
- package/dist/gpu-runtime.d.ts +32 -7
- package/dist/gpu-runtime.d.ts.map +1 -1
- package/dist/index.d.ts +10 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +647 -117
- package/dist/index.mjs.map +1 -1
- package/dist/particle-layout.d.ts +62 -0
- package/dist/particle-layout.d.ts.map +1 -0
- package/package.json +5 -5
- package/src/__tests__/authoring-descriptor.unit.test.ts +18 -8
- package/src/__tests__/channel-input.unit.test.ts +4 -1
- package/src/__tests__/channel-overflow-policy-owner.test-d.ts +2 -2
- package/src/__tests__/code-source-v2.unit.test.ts +15 -14
- package/src/__tests__/data-interface-contract.unit.test.ts +41 -2
- package/src/__tests__/effect-contract.unit.test.ts +6 -3
- package/src/__tests__/gpu-program-simulation-culling-owner.test-d.ts +6 -6
- package/src/__tests__/gpu-reflection-vocabulary-owner.test-d.ts +2 -2
- package/src/__tests__/gpu-runtime.integration.test.ts +244 -11
- package/src/__tests__/instance-public-api.unit.test.ts +4 -1
- package/src/__tests__/instance.unit.test.ts +4 -1
- package/src/__tests__/loader-v1-rejection.unit.test.ts +14 -14
- package/src/__tests__/player-reflection.unit.test.ts +2 -2
- package/src/__tests__/player-type.test-d.ts +4 -1
- package/src/__tests__/renderer-source.unit.test.ts +6 -6
- package/src/__tests__/replay-canonical.unit.test.ts +4 -1
- package/src/__tests__/vfx-data-interface-vocabulary-owner.test-d.ts +3 -8
- package/src/assets/particle-effect-decoder.ts +4 -3
- package/src/authoring-descriptor.ts +26 -28
- package/src/code-source-v3.ts +460 -0
- package/src/code-source.ts +23 -27
- package/src/data-interface.ts +66 -1
- package/src/effect-contract.ts +73 -20
- package/src/gpu-loader.ts +61 -35
- package/src/gpu-program.ts +57 -35
- package/src/gpu-runtime.ts +115 -42
- package/src/index.ts +53 -9
- package/src/particle-layout.ts +214 -0
package/src/gpu-program.ts
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
BindGroupLayoutDescriptor,
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
MaterialParticleInput,
|
|
4
|
+
ParticleEffectAssetV3,
|
|
5
5
|
ParticleEffectProgramEmitter,
|
|
6
|
+
ParticleEffectProgramV3,
|
|
6
7
|
} from '@forgeax/engine-types';
|
|
7
8
|
import type {
|
|
8
9
|
ParticleChannelSource,
|
|
9
|
-
ParticleEmitterSourceV2,
|
|
10
10
|
ParticleEventSource,
|
|
11
|
-
ParticleRendererOverflowPolicy,
|
|
12
|
-
ParticleRendererSorting,
|
|
13
|
-
ParticleRendererSource,
|
|
14
11
|
ParticleStageDomain,
|
|
15
12
|
ParticleStageResourceAccess,
|
|
16
13
|
} from './code-source.js';
|
|
14
|
+
import type {
|
|
15
|
+
ParticleEmitterSourceV3,
|
|
16
|
+
ParticleRendererSemanticMap,
|
|
17
|
+
ParticleRendererSortingV3,
|
|
18
|
+
ParticleRendererSourceV3,
|
|
19
|
+
} from './code-source-v3.js';
|
|
17
20
|
import type { VfxDataInterfaceRequirement } from './data-interface.js';
|
|
18
21
|
import type { VfxEffectReflection } from './effect-contract.js';
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
/** The only cooked GPU program ABI exposed by the runtime. */
|
|
24
|
+
export const VFX_GPU_PROGRAM_FORMAT = 'forgeax-vfx-program-3' as const;
|
|
25
|
+
export const VFX_GPU_PROGRAM_FORMAT_V3 = VFX_GPU_PROGRAM_FORMAT;
|
|
21
26
|
export const VFX_GPU_PROGRAM_ARTIFACT_KEY = 'particle-effect/program.json' as const;
|
|
22
27
|
|
|
23
28
|
export interface VfxGpuStageReflection {
|
|
@@ -33,13 +38,17 @@ export interface VfxGpuStageReflection {
|
|
|
33
38
|
readonly iterationBudget: number;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
/** Reflection of one executable renderer projection. */
|
|
42
|
+
export interface VfxGpuRendererReflectionV3 {
|
|
43
|
+
readonly topology: ParticleRendererSourceV3['kind'];
|
|
38
44
|
readonly resource: string;
|
|
39
45
|
readonly capacity: number;
|
|
40
|
-
readonly overflow:
|
|
46
|
+
readonly overflow: 'drop-newest' | 'drop-oldest';
|
|
41
47
|
readonly enabled: boolean;
|
|
42
48
|
readonly shaderInputs: readonly string[];
|
|
49
|
+
readonly attributes: ParticleRendererSemanticMap;
|
|
50
|
+
readonly materialInputs: readonly string[];
|
|
51
|
+
readonly materialInputDefinitions?: readonly MaterialParticleInput[];
|
|
43
52
|
readonly textureSheet?: {
|
|
44
53
|
readonly columns: number;
|
|
45
54
|
readonly rows: number;
|
|
@@ -48,54 +57,67 @@ export interface VfxGpuRendererReflection {
|
|
|
48
57
|
};
|
|
49
58
|
readonly pivot?: readonly [number, number];
|
|
50
59
|
readonly softParticle?: { readonly fadeDistance: number; readonly requiresDepth: true };
|
|
51
|
-
readonly sorting?:
|
|
60
|
+
readonly sorting?: ParticleRendererSortingV3;
|
|
52
61
|
readonly stripKey?: 'alive-index';
|
|
53
62
|
readonly historyLength?: number;
|
|
54
63
|
readonly endpointField?: 'velocity';
|
|
64
|
+
readonly lighting?: 'unlit' | 'standard';
|
|
65
|
+
readonly castShadows: boolean;
|
|
66
|
+
readonly receiveShadows: boolean;
|
|
55
67
|
}
|
|
56
68
|
|
|
57
|
-
export interface
|
|
69
|
+
export interface VfxGpuProgramReflectionV3 {
|
|
58
70
|
readonly hooks: readonly ['vfx_spawn', 'vfx_update'];
|
|
59
71
|
readonly imports: readonly string[];
|
|
60
72
|
readonly resources: readonly string[];
|
|
61
73
|
readonly entryPoints: readonly string[];
|
|
62
74
|
readonly bindings: readonly BindGroupLayoutDescriptor[];
|
|
63
|
-
readonly layout
|
|
64
|
-
readonly dataInterfaces
|
|
65
|
-
readonly eventChannels
|
|
66
|
-
readonly events
|
|
67
|
-
readonly eventEntryPoint
|
|
68
|
-
readonly stages
|
|
69
|
-
readonly renderers
|
|
75
|
+
readonly layout: VfxEffectReflection;
|
|
76
|
+
readonly dataInterfaces: readonly VfxDataInterfaceRequirement[];
|
|
77
|
+
readonly eventChannels: readonly ParticleChannelSource[];
|
|
78
|
+
readonly events: readonly ParticleEventSource[];
|
|
79
|
+
readonly eventEntryPoint: 'forgeax_vfx_event_main';
|
|
80
|
+
readonly stages: readonly VfxGpuStageReflection[];
|
|
81
|
+
readonly renderers: readonly VfxGpuRendererReflectionV3[];
|
|
70
82
|
}
|
|
71
83
|
|
|
72
|
-
export interface
|
|
73
|
-
extends Omit<
|
|
74
|
-
ParticleEffectProgramEmitter,
|
|
75
|
-
'schedule' | 'bounds' | 'renderers' | 'reflection' | 'channels' | 'events'
|
|
76
|
-
> {
|
|
84
|
+
export interface VfxGpuEmitterProgramV3
|
|
85
|
+
extends Omit<ParticleEffectProgramEmitter, 'renderers' | 'reflection'> {
|
|
77
86
|
readonly id: string;
|
|
78
87
|
readonly module: string;
|
|
79
88
|
readonly capacity: number;
|
|
80
|
-
readonly backend:
|
|
81
|
-
readonly space:
|
|
82
|
-
readonly schedule:
|
|
83
|
-
readonly bounds:
|
|
84
|
-
readonly renderers:
|
|
89
|
+
readonly backend: ParticleEmitterSourceV3['backend'];
|
|
90
|
+
readonly space: ParticleEmitterSourceV3['space'];
|
|
91
|
+
readonly schedule: ParticleEmitterSourceV3['schedule'];
|
|
92
|
+
readonly bounds: ParticleEmitterSourceV3['bounds'];
|
|
93
|
+
readonly renderers: readonly ParticleRendererSourceV3[];
|
|
85
94
|
readonly channels?: readonly ParticleChannelSource[];
|
|
86
95
|
readonly events?: readonly ParticleEventSource[];
|
|
87
|
-
readonly simulationWhenCulled: NonNullable<
|
|
96
|
+
readonly simulationWhenCulled: NonNullable<ParticleEmitterSourceV3['simulationWhenCulled']>;
|
|
88
97
|
readonly wgsl: string;
|
|
89
|
-
readonly reflection:
|
|
98
|
+
readonly reflection: VfxGpuProgramReflectionV3;
|
|
90
99
|
}
|
|
91
100
|
|
|
92
|
-
export interface
|
|
101
|
+
export interface VfxGpuProgramV3 extends Omit<ParticleEffectProgramV3, 'emitters'> {
|
|
93
102
|
readonly format: typeof VFX_GPU_PROGRAM_FORMAT;
|
|
94
103
|
readonly fingerprint: string;
|
|
95
|
-
readonly emitters: readonly
|
|
104
|
+
readonly emitters: readonly VfxGpuEmitterProgramV3[];
|
|
96
105
|
}
|
|
97
106
|
|
|
98
|
-
export interface
|
|
107
|
+
export interface VfxGpuEffectAssetV3 extends Omit<ParticleEffectAssetV3, 'program'> {
|
|
99
108
|
readonly guid: string;
|
|
100
|
-
readonly
|
|
109
|
+
readonly kind: 'particle-effect';
|
|
110
|
+
readonly schemaVersion: 3;
|
|
111
|
+
readonly programFingerprint: string;
|
|
112
|
+
readonly emitters: readonly { readonly id: string; readonly capacity: number }[];
|
|
113
|
+
readonly program: VfxGpuProgramV3;
|
|
101
114
|
}
|
|
115
|
+
|
|
116
|
+
/** Public names point at the same one-cut Program v3 shapes. */
|
|
117
|
+
export type VfxGpuRendererReflection = VfxGpuRendererReflectionV3;
|
|
118
|
+
export type VfxGpuProgramReflection = VfxGpuProgramReflectionV3;
|
|
119
|
+
export type VfxGpuEmitterProgram = VfxGpuEmitterProgramV3;
|
|
120
|
+
export type VfxGpuProgram = VfxGpuProgramV3;
|
|
121
|
+
export type VfxGpuEffectAsset = VfxGpuEffectAssetV3;
|
|
122
|
+
export type VfxGpuEmitterProgramAny = VfxGpuEmitterProgramV3;
|
|
123
|
+
export type VfxGpuEffectAssetAny = VfxGpuEffectAssetV3;
|
package/src/gpu-runtime.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { type Handle, toShared } from '@forgeax/engine-types';
|
|
|
4
4
|
import type { ParticleEventSource } from './code-source.js';
|
|
5
5
|
import type { VfxValueMap } from './effect-contract.js';
|
|
6
6
|
import { createVfxEffectContract, type VfxEffectReflection } from './effect-contract.js';
|
|
7
|
-
import type {
|
|
7
|
+
import type { VfxGpuEffectAssetAny, VfxGpuEmitterProgramAny } from './gpu-program.js';
|
|
8
8
|
import {
|
|
9
9
|
ParticleEffectInstance,
|
|
10
10
|
type VfxChannelCounters,
|
|
@@ -49,7 +49,7 @@ export function createVfxInspectSnapshot(input: VfxInspectSnapshotInput) {
|
|
|
49
49
|
export interface VfxGpuTickIntent {
|
|
50
50
|
readonly sequence: number;
|
|
51
51
|
readonly player: EntityHandle;
|
|
52
|
-
readonly emitter:
|
|
52
|
+
readonly emitter: VfxGpuEmitterProgramAny;
|
|
53
53
|
readonly programFingerprint: string;
|
|
54
54
|
readonly reset: boolean;
|
|
55
55
|
readonly fixedDelta: number;
|
|
@@ -84,18 +84,24 @@ export interface VfxGpuEmitterInspectSnapshot {
|
|
|
84
84
|
readonly spawnCount: number;
|
|
85
85
|
readonly firstParticleId: number;
|
|
86
86
|
readonly reset: boolean;
|
|
87
|
-
readonly schedule:
|
|
88
|
-
readonly bounds:
|
|
89
|
-
readonly simulationWhenCulled:
|
|
87
|
+
readonly schedule: VfxGpuEmitterProgramAny['schedule'];
|
|
88
|
+
readonly bounds: VfxGpuEmitterProgramAny['bounds'];
|
|
89
|
+
readonly simulationWhenCulled: VfxGpuEmitterProgramAny['simulationWhenCulled'];
|
|
90
90
|
readonly renderers: readonly {
|
|
91
91
|
readonly index: number;
|
|
92
|
-
readonly kind:
|
|
92
|
+
readonly kind: VfxGpuEmitterProgramAny['renderers'][number]['kind'];
|
|
93
93
|
readonly enabled: boolean;
|
|
94
94
|
}[];
|
|
95
95
|
readonly stages: readonly string[];
|
|
96
96
|
readonly dataInterfaces: readonly string[];
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/** Renderer-facing source view used to refresh culling without a queued tick. */
|
|
100
|
+
export interface VfxGpuEmitterSource {
|
|
101
|
+
readonly player: EntityHandle;
|
|
102
|
+
readonly emitter: VfxGpuEmitterProgramAny;
|
|
103
|
+
}
|
|
104
|
+
|
|
99
105
|
export interface VfxGpuCommittedInspectSnapshot {
|
|
100
106
|
readonly sequence: number;
|
|
101
107
|
readonly tick: number;
|
|
@@ -143,7 +149,7 @@ interface PlayerState {
|
|
|
143
149
|
effect: Handle<'ParticleEffectAsset', 'shared'>;
|
|
144
150
|
assetGuid: string;
|
|
145
151
|
programFingerprint: string;
|
|
146
|
-
emitters: readonly
|
|
152
|
+
emitters: readonly VfxGpuEmitterProgramAny[];
|
|
147
153
|
seed: number;
|
|
148
154
|
playing: boolean;
|
|
149
155
|
playCycle: number;
|
|
@@ -154,6 +160,7 @@ interface PlayerState {
|
|
|
154
160
|
cameraVisible: boolean[];
|
|
155
161
|
phaseTicks: number[];
|
|
156
162
|
hasCommitted: boolean;
|
|
163
|
+
pendingResets: boolean[];
|
|
157
164
|
}
|
|
158
165
|
|
|
159
166
|
function eventCounters(
|
|
@@ -216,10 +223,30 @@ export class VfxGpuRuntime {
|
|
|
216
223
|
return this.#diagnostics;
|
|
217
224
|
}
|
|
218
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Visit every live emitter source. The renderer uses this on render frames
|
|
228
|
+
* where pause/restart culling has no pending simulation intent, so a camera
|
|
229
|
+
* move back into the bounds can refresh visibility before the next tick.
|
|
230
|
+
*/
|
|
231
|
+
forEachEmitterSource(visitor: (source: VfxGpuEmitterSource) => void): void {
|
|
232
|
+
for (const [player, state] of this.#players) {
|
|
233
|
+
for (const emitter of state.emitters) visitor({ player, emitter });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
219
237
|
lastCommitted(player: EntityHandle): VfxGpuTickIntent | undefined {
|
|
220
238
|
return this.#lastCommitted.get(player);
|
|
221
239
|
}
|
|
222
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Return the last submitted fixed-tick state for one emitter. Renderers
|
|
243
|
+
* retain this projection between fixed ticks; simulation intents are a
|
|
244
|
+
* queue, while a drawable emitter remains live until the player is reset.
|
|
245
|
+
*/
|
|
246
|
+
lastCommittedEmitter(player: EntityHandle, emitterId: string): VfxGpuTickIntent | undefined {
|
|
247
|
+
return this.#lastCommittedByEmitter.get(player)?.get(emitterId);
|
|
248
|
+
}
|
|
249
|
+
|
|
223
250
|
inspectPlayers(): readonly VfxGpuPlayerInspectSnapshot[] {
|
|
224
251
|
return Object.freeze(
|
|
225
252
|
[...this.#players.keys()]
|
|
@@ -399,24 +426,49 @@ export class VfxGpuRuntime {
|
|
|
399
426
|
this.#replayRequests.add(player);
|
|
400
427
|
}
|
|
401
428
|
|
|
402
|
-
|
|
429
|
+
/**
|
|
430
|
+
* Acknowledge queued intents. A scalar acknowledges the contiguous prefix;
|
|
431
|
+
* an array acknowledges only those terminal sequences, which lets unrelated
|
|
432
|
+
* player/emitter streams progress while another stream is deferred. When the
|
|
433
|
+
* renderer supplies `publishedSequences`, only intents that reached GPU
|
|
434
|
+
* submission update the retained last-committed projection; skipped intents
|
|
435
|
+
* are queue acknowledgements, not new GPU state.
|
|
436
|
+
*/
|
|
437
|
+
commit(sequence: number | readonly number[], publishedSequences?: readonly number[]): void {
|
|
438
|
+
const prefix = typeof sequence === 'number' ? sequence : undefined;
|
|
439
|
+
const acknowledged = typeof sequence === 'number' ? undefined : new Set(sequence);
|
|
440
|
+
const published = publishedSequences === undefined ? undefined : new Set(publishedSequences);
|
|
403
441
|
const committedPlayers = new Set<EntityHandle>();
|
|
442
|
+
const retained: VfxGpuTickIntent[] = [];
|
|
404
443
|
for (const intent of this.#intents) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
this.#lastCommittedByEmitter.set(intent.player, emitters);
|
|
444
|
+
const shouldAcknowledge =
|
|
445
|
+
prefix === undefined
|
|
446
|
+
? acknowledged?.has(intent.sequence) === true
|
|
447
|
+
: intent.sequence <= prefix;
|
|
448
|
+
if (!shouldAcknowledge) {
|
|
449
|
+
retained.push(intent);
|
|
450
|
+
continue;
|
|
413
451
|
}
|
|
414
|
-
|
|
452
|
+
const state = this.#players.get(intent.player);
|
|
415
453
|
committedPlayers.add(intent.player);
|
|
454
|
+
if (published === undefined || published.has(intent.sequence)) {
|
|
455
|
+
if (state !== undefined) state.hasCommitted = true;
|
|
456
|
+
const priorCommitted = this.#lastCommitted.get(intent.player);
|
|
457
|
+
if (priorCommitted === undefined || priorCommitted.sequence < intent.sequence) {
|
|
458
|
+
this.#lastCommitted.set(intent.player, intent);
|
|
459
|
+
}
|
|
460
|
+
let emitters = this.#lastCommittedByEmitter.get(intent.player);
|
|
461
|
+
if (emitters === undefined) {
|
|
462
|
+
emitters = new Map();
|
|
463
|
+
this.#lastCommittedByEmitter.set(intent.player, emitters);
|
|
464
|
+
}
|
|
465
|
+
const priorEmitter = emitters.get(intent.emitter.id);
|
|
466
|
+
if (priorEmitter === undefined || priorEmitter.sequence < intent.sequence) {
|
|
467
|
+
emitters.set(intent.emitter.id, intent);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
416
470
|
}
|
|
417
|
-
|
|
418
|
-
if (retained < 0) this.#intents.length = 0;
|
|
419
|
-
else if (retained > 0) this.#intents.splice(0, retained);
|
|
471
|
+
this.#intents.splice(0, this.#intents.length, ...retained);
|
|
420
472
|
for (const player of committedPlayers) {
|
|
421
473
|
this.#clearDiagnostics(player, 'vfx-intent-queue-overflow');
|
|
422
474
|
}
|
|
@@ -519,14 +571,14 @@ export class VfxGpuRuntime {
|
|
|
519
571
|
continue;
|
|
520
572
|
}
|
|
521
573
|
this.#clearDiagnostics(input.player, 'vfx-player-invalid');
|
|
522
|
-
const resolved = world.sharedRefs.resolve<'ParticleEffectAsset',
|
|
574
|
+
const resolved = world.sharedRefs.resolve<'ParticleEffectAsset', VfxGpuEffectAssetAny>(
|
|
523
575
|
input.effect,
|
|
524
576
|
);
|
|
525
|
-
if (!resolved.ok || resolved.value.schemaVersion !==
|
|
577
|
+
if (!resolved.ok || resolved.value.schemaVersion !== 3) {
|
|
526
578
|
this.#report({
|
|
527
579
|
code: 'vfx-effect-unavailable',
|
|
528
|
-
expected: 'a loaded schemaVersion
|
|
529
|
-
hint: '
|
|
580
|
+
expected: 'a loaded schemaVersion 3 GPU particle effect',
|
|
581
|
+
hint: 'cold-cook the legacy effect as Program v3 before the first FixedUpdate',
|
|
530
582
|
detail: { player: input.player },
|
|
531
583
|
});
|
|
532
584
|
continue;
|
|
@@ -534,13 +586,14 @@ export class VfxGpuRuntime {
|
|
|
534
586
|
this.#clearDiagnostics(input.player, 'vfx-effect-unavailable');
|
|
535
587
|
const previous = this.#players.get(input.player);
|
|
536
588
|
const replayRequested = this.#replayRequests.delete(input.player);
|
|
537
|
-
const
|
|
589
|
+
const restartRequested =
|
|
538
590
|
previous === undefined ||
|
|
539
591
|
previous.effect !== input.effect ||
|
|
540
592
|
previous.seed !== input.seed ||
|
|
541
593
|
replayRequested ||
|
|
542
594
|
(!previous.playing && input.playing);
|
|
543
|
-
const
|
|
595
|
+
const restart = restartRequested;
|
|
596
|
+
const state = restartRequested
|
|
544
597
|
? {
|
|
545
598
|
effect: input.effect,
|
|
546
599
|
assetGuid: resolved.value.guid,
|
|
@@ -556,6 +609,7 @@ export class VfxGpuRuntime {
|
|
|
556
609
|
cameraVisible: resolved.value.program.emitters.map(() => true),
|
|
557
610
|
phaseTicks: resolved.value.program.emitters.map(() => 0),
|
|
558
611
|
hasCommitted: false,
|
|
612
|
+
pendingResets: resolved.value.program.emitters.map(() => false),
|
|
559
613
|
}
|
|
560
614
|
: previous;
|
|
561
615
|
this.#players.set(input.player, state);
|
|
@@ -563,7 +617,10 @@ export class VfxGpuRuntime {
|
|
|
563
617
|
// authority. Inspection always returns the authored player state after
|
|
564
618
|
// the requested reset intent has been emitted.
|
|
565
619
|
state.playing = input.playing;
|
|
566
|
-
if (!input.playing && !replayRequested)
|
|
620
|
+
if (!input.playing && !replayRequested) {
|
|
621
|
+
if (restart) state.pendingResets.fill(true);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
567
624
|
const queuedForPlayer = this.#intents.reduce(
|
|
568
625
|
(count, intent) => count + (intent.player === input.player ? 1 : 0),
|
|
569
626
|
0,
|
|
@@ -572,6 +629,7 @@ export class VfxGpuRuntime {
|
|
|
572
629
|
queuedForPlayer >=
|
|
573
630
|
this.#maxQueuedTicks * Math.max(1, resolved.value.program.emitters.length)
|
|
574
631
|
) {
|
|
632
|
+
if (restart) state.pendingResets.fill(true);
|
|
575
633
|
if (state.hasCommitted) {
|
|
576
634
|
this.#report({
|
|
577
635
|
code: 'vfx-intent-queue-overflow',
|
|
@@ -584,13 +642,17 @@ export class VfxGpuRuntime {
|
|
|
584
642
|
}
|
|
585
643
|
const instance =
|
|
586
644
|
this.#instances.get(input.player) ?? this.#createInstance(input.player, resolved.value);
|
|
587
|
-
if (instance === undefined)
|
|
645
|
+
if (instance === undefined) {
|
|
646
|
+
if (restart) state.pendingResets.fill(true);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
588
649
|
const hasActiveEmitter = resolved.value.program.emitters.some((emitter) => {
|
|
589
650
|
if (!this.isEmitterSessionEnabled(input.player, emitter.id)) return false;
|
|
590
651
|
const cameraVisible = this.#cameraVisibility.get(`${input.player}:${emitter.id}`) ?? true;
|
|
591
652
|
return cameraVisible || emitter.simulationWhenCulled === 'continue';
|
|
592
653
|
});
|
|
593
654
|
if (!hasActiveEmitter) {
|
|
655
|
+
if (restart) state.pendingResets.fill(true);
|
|
594
656
|
for (const [index, emitter] of resolved.value.program.emitters.entries()) {
|
|
595
657
|
state.cameraVisible[index] =
|
|
596
658
|
this.#cameraVisibility.get(`${input.player}:${emitter.id}`) ?? true;
|
|
@@ -604,6 +666,7 @@ export class VfxGpuRuntime {
|
|
|
604
666
|
? instance.commit({ seed: input.seed, tick })
|
|
605
667
|
: instance.replay(replayInput);
|
|
606
668
|
if (!committed.ok) {
|
|
669
|
+
if (restart) state.pendingResets.fill(true);
|
|
607
670
|
this.#report({
|
|
608
671
|
code: 'vfx-instance-commit-failed',
|
|
609
672
|
expected: 'the current typed instance values to pack into the reflected GPU block',
|
|
@@ -618,8 +681,15 @@ export class VfxGpuRuntime {
|
|
|
618
681
|
const cameraVisible = this.#cameraVisibility.get(`${input.player}:${emitter.id}`) ?? true;
|
|
619
682
|
const becameVisible = cameraVisible && state.cameraVisible[index] === false;
|
|
620
683
|
state.cameraVisible[index] = cameraVisible;
|
|
621
|
-
|
|
622
|
-
if (!
|
|
684
|
+
const emitterRestart = restart || state.pendingResets[index] === true;
|
|
685
|
+
if (!this.isEmitterSessionEnabled(input.player, emitter.id)) {
|
|
686
|
+
if (emitterRestart) state.pendingResets[index] = true;
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (!cameraVisible && emitter.simulationWhenCulled !== 'continue') {
|
|
690
|
+
if (emitterRestart) state.pendingResets[index] = true;
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
623
693
|
const visibilityRestart =
|
|
624
694
|
becameVisible && emitter.simulationWhenCulled === 'restart-on-visible';
|
|
625
695
|
if (visibilityRestart) {
|
|
@@ -634,7 +704,7 @@ export class VfxGpuRuntime {
|
|
|
634
704
|
emitter,
|
|
635
705
|
previousElapsed,
|
|
636
706
|
previousElapsed + delta,
|
|
637
|
-
|
|
707
|
+
emitterRestart || visibilityRestart,
|
|
638
708
|
state.rateRemainders[index] ?? 0,
|
|
639
709
|
);
|
|
640
710
|
state.rateRemainders[index] = scheduled.remainder;
|
|
@@ -655,7 +725,7 @@ export class VfxGpuRuntime {
|
|
|
655
725
|
player: input.player,
|
|
656
726
|
emitter,
|
|
657
727
|
programFingerprint: resolved.value.program.fingerprint,
|
|
658
|
-
reset:
|
|
728
|
+
reset: emitterRestart || visibilityRestart,
|
|
659
729
|
fixedDelta: delta,
|
|
660
730
|
phaseTick,
|
|
661
731
|
tick,
|
|
@@ -678,6 +748,7 @@ export class VfxGpuRuntime {
|
|
|
678
748
|
);
|
|
679
749
|
state.elapsed[index] = previousElapsed + delta;
|
|
680
750
|
state.phaseTicks[index] = phaseTick + 1;
|
|
751
|
+
state.pendingResets[index] = false;
|
|
681
752
|
}
|
|
682
753
|
}
|
|
683
754
|
for (const player of this.#players.keys()) {
|
|
@@ -687,19 +758,21 @@ export class VfxGpuRuntime {
|
|
|
687
758
|
|
|
688
759
|
#createInstance(
|
|
689
760
|
player: EntityHandle,
|
|
690
|
-
effect:
|
|
761
|
+
effect: VfxGpuEffectAssetAny,
|
|
691
762
|
): ParticleEffectInstance | undefined {
|
|
692
763
|
const layout = effect.program.emitters.find(
|
|
693
764
|
(emitter) => emitter.reflection.layout !== undefined,
|
|
694
765
|
)?.reflection.layout;
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
766
|
+
if (layout === undefined) {
|
|
767
|
+
this.#report({
|
|
768
|
+
code: 'vfx-effect-unavailable',
|
|
769
|
+
expected: 'a Program v3 emitter reflection layout',
|
|
770
|
+
hint: 'recook the effect with Program v3 reflection before starting the player',
|
|
771
|
+
detail: { player },
|
|
772
|
+
});
|
|
773
|
+
return undefined;
|
|
774
|
+
}
|
|
775
|
+
const reflection: VfxEffectReflection = layout;
|
|
703
776
|
try {
|
|
704
777
|
const instance = new ParticleEffectInstance(createVfxEffectContract(reflection), {
|
|
705
778
|
channels: effect.program.emitters.flatMap((emitter) => emitter.channels ?? []),
|
|
@@ -730,7 +803,7 @@ export class VfxGpuRuntime {
|
|
|
730
803
|
}
|
|
731
804
|
|
|
732
805
|
function spawnCount(
|
|
733
|
-
emitter:
|
|
806
|
+
emitter: VfxGpuEmitterProgramAny,
|
|
734
807
|
previous: number,
|
|
735
808
|
next: number,
|
|
736
809
|
firstTick: boolean,
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
// @forgeax/engine-vfx - runtime-safe code-first GPU VFX contract.
|
|
2
2
|
|
|
3
|
-
export type {
|
|
3
|
+
export type {
|
|
4
|
+
ParticleEffectAsset,
|
|
5
|
+
ParticleEffectAssetV3,
|
|
6
|
+
ParticleEffectProgramV3,
|
|
7
|
+
ParticleEmitterDefinition,
|
|
8
|
+
} from '@forgeax/engine-types';
|
|
4
9
|
export { particleEffectContribution } from './assets/particle-effect-decoder';
|
|
5
10
|
export type {
|
|
6
11
|
VfxAuthoringCapabilityDescriptor,
|
|
@@ -20,25 +25,34 @@ export type {
|
|
|
20
25
|
ParticleChannelSource,
|
|
21
26
|
ParticleCodeSourceError,
|
|
22
27
|
ParticleCodeSourceInvalidDetail,
|
|
23
|
-
ParticleEffectRootSourceV2,
|
|
24
|
-
ParticleEffectSourceV2,
|
|
25
|
-
ParticleEmitterSourceV2,
|
|
26
28
|
ParticleEventSource,
|
|
27
29
|
ParticleRendererOverflowPolicy,
|
|
28
|
-
ParticleRendererSorting,
|
|
29
|
-
ParticleRendererSource,
|
|
30
30
|
ParticleStageDomain,
|
|
31
31
|
ParticleStageResourceAccess,
|
|
32
32
|
ParticleStageResourceSource,
|
|
33
33
|
ParticleStageSource,
|
|
34
34
|
} from './code-source.js';
|
|
35
35
|
export {
|
|
36
|
-
defineParticleEffectSourceV2,
|
|
37
36
|
PARTICLE_CODE_DEFAULT_MODULE_ID,
|
|
38
37
|
PARTICLE_STAGE_RESOURCE_NAMES,
|
|
39
|
-
parseParticleEffectSourceV2,
|
|
40
38
|
parseVfxStageDeclarations,
|
|
41
39
|
} from './code-source.js';
|
|
40
|
+
export type {
|
|
41
|
+
ParticleAttributeRef,
|
|
42
|
+
ParticleEffectRootSourceV3,
|
|
43
|
+
ParticleEffectSourceV3,
|
|
44
|
+
ParticleEmitterSourceV3,
|
|
45
|
+
ParticleRendererSemantic,
|
|
46
|
+
ParticleRendererSemanticMap,
|
|
47
|
+
ParticleRendererSortingV3,
|
|
48
|
+
ParticleRendererSourceV3,
|
|
49
|
+
} from './code-source-v3.js';
|
|
50
|
+
export {
|
|
51
|
+
defaultParticleRendererAttributes,
|
|
52
|
+
defineParticleEffectSourceV3,
|
|
53
|
+
PARTICLE_RENDERER_SEMANTICS,
|
|
54
|
+
parseParticleEffectSourceV3,
|
|
55
|
+
} from './code-source-v3.js';
|
|
42
56
|
export type {
|
|
43
57
|
VfxDataInterfaceBindingType,
|
|
44
58
|
VfxDataInterfaceError,
|
|
@@ -72,14 +86,26 @@ export {
|
|
|
72
86
|
} from './gpu-loader.js';
|
|
73
87
|
export type {
|
|
74
88
|
VfxGpuEffectAsset,
|
|
89
|
+
VfxGpuEffectAssetAny,
|
|
90
|
+
VfxGpuEffectAssetV3,
|
|
75
91
|
VfxGpuEmitterProgram,
|
|
92
|
+
VfxGpuEmitterProgramAny,
|
|
93
|
+
VfxGpuEmitterProgramV3,
|
|
76
94
|
VfxGpuProgram,
|
|
77
95
|
VfxGpuProgramReflection,
|
|
96
|
+
VfxGpuProgramReflectionV3,
|
|
97
|
+
VfxGpuProgramV3,
|
|
98
|
+
VfxGpuRendererReflectionV3,
|
|
78
99
|
VfxGpuStageReflection,
|
|
79
100
|
} from './gpu-program.js';
|
|
80
|
-
export {
|
|
101
|
+
export {
|
|
102
|
+
VFX_GPU_PROGRAM_ARTIFACT_KEY,
|
|
103
|
+
VFX_GPU_PROGRAM_FORMAT,
|
|
104
|
+
VFX_GPU_PROGRAM_FORMAT_V3,
|
|
105
|
+
} from './gpu-program.js';
|
|
81
106
|
export type {
|
|
82
107
|
VfxGpuEmitterInspectSnapshot,
|
|
108
|
+
VfxGpuEmitterSource,
|
|
83
109
|
VfxGpuPlayerInspectSnapshot,
|
|
84
110
|
VfxGpuRuntimeDiagnostic,
|
|
85
111
|
VfxGpuRuntimeOptions,
|
|
@@ -106,5 +132,23 @@ export {
|
|
|
106
132
|
createParticleEffectInstance,
|
|
107
133
|
ParticleEffectInstance,
|
|
108
134
|
} from './instance.js';
|
|
135
|
+
export type {
|
|
136
|
+
VfxCustomLayout,
|
|
137
|
+
VfxParametersLayout,
|
|
138
|
+
VfxParticleCoreAttribute,
|
|
139
|
+
VfxParticleCoreField,
|
|
140
|
+
VfxParticleCoreLayout,
|
|
141
|
+
VfxParticleCoreValue,
|
|
142
|
+
VfxParticleCoreValues,
|
|
143
|
+
} from './particle-layout.js';
|
|
144
|
+
export {
|
|
145
|
+
deriveVfxCustomLayout,
|
|
146
|
+
encodeVfxParticleCore,
|
|
147
|
+
normalizedVfxParticleAge,
|
|
148
|
+
VFX_PARTICLE_CORE_LAYOUT,
|
|
149
|
+
VFX_PARTICLE_CORE_STRIDE,
|
|
150
|
+
vfxParticleCoreField,
|
|
151
|
+
vfxParticleCoreWgsl,
|
|
152
|
+
} from './particle-layout.js';
|
|
109
153
|
export type { ParticleEffectPlayerData } from './player.js';
|
|
110
154
|
export { ParticleEffectPlayer } from './player.js';
|