@forgeax/engine-vfx-render 0.1.2
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/LICENSE +202 -0
- package/README.md +163 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/billboard-advanced.integration.test.d.ts +2 -0
- package/dist/__tests__/billboard-advanced.integration.test.d.ts.map +1 -0
- package/dist/__tests__/data-interface-providers.unit.test.d.ts +2 -0
- package/dist/__tests__/data-interface-providers.unit.test.d.ts.map +1 -0
- package/dist/__tests__/data-interface-public-api.test-d.d.ts +2 -0
- package/dist/__tests__/data-interface-public-api.test-d.d.ts.map +1 -0
- package/dist/__tests__/event-resources.unit.test.d.ts +2 -0
- package/dist/__tests__/event-resources.unit.test.d.ts.map +1 -0
- package/dist/__tests__/fog-producer.integration.test.d.ts +2 -0
- package/dist/__tests__/fog-producer.integration.test.d.ts.map +1 -0
- package/dist/__tests__/gpu-host.integration.test.d.ts +2 -0
- package/dist/__tests__/gpu-host.integration.test.d.ts.map +1 -0
- package/dist/__tests__/inspect-snapshot.integration.test.d.ts +2 -0
- package/dist/__tests__/inspect-snapshot.integration.test.d.ts.map +1 -0
- package/dist/__tests__/particle-resources.unit.test.d.ts +2 -0
- package/dist/__tests__/particle-resources.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-vocabulary-owner.test-d.d.ts +2 -0
- package/dist/__tests__/render-vocabulary-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/stage-plan.integration.test.d.ts +2 -0
- package/dist/__tests__/stage-plan.integration.test.d.ts.map +1 -0
- package/dist/__tests__/stage-recovery.integration.test.d.ts +2 -0
- package/dist/__tests__/stage-recovery.integration.test.d.ts.map +1 -0
- package/dist/__tests__/stage-resource-access-owner.test-d.d.ts +2 -0
- package/dist/__tests__/stage-resource-access-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/stage-resource-layout.unit.test.d.ts +2 -0
- package/dist/__tests__/stage-resource-layout.unit.test.d.ts.map +1 -0
- package/dist/__tests__/topology-capacity.unit.test.d.ts +2 -0
- package/dist/__tests__/topology-capacity.unit.test.d.ts.map +1 -0
- package/dist/__tests__/topology-resources.unit.test.d.ts +2 -0
- package/dist/__tests__/topology-resources.unit.test.d.ts.map +1 -0
- package/dist/feature/camera.d.ts +11 -0
- package/dist/feature/camera.d.ts.map +1 -0
- package/dist/feature/event-resources.d.ts +9 -0
- package/dist/feature/event-resources.d.ts.map +1 -0
- package/dist/feature/gpu-particle-feature.d.ts +96 -0
- package/dist/feature/gpu-particle-feature.d.ts.map +1 -0
- package/dist/feature/particle-resources.d.ts +66 -0
- package/dist/feature/particle-resources.d.ts.map +1 -0
- package/dist/feature/stage-plan.d.ts +54 -0
- package/dist/feature/stage-plan.d.ts.map +1 -0
- package/dist/feature/topologies/beam.d.ts +16 -0
- package/dist/feature/topologies/beam.d.ts.map +1 -0
- package/dist/feature/topologies/ribbon.d.ts +15 -0
- package/dist/feature/topologies/ribbon.d.ts.map +1 -0
- package/dist/feature/topologies/trail.d.ts +19 -0
- package/dist/feature/topologies/trail.d.ts.map +1 -0
- package/dist/host/data-interface-providers.d.ts +19 -0
- package/dist/host/data-interface-providers.d.ts.map +1 -0
- package/dist/host/vfx-runtime-host.d.ts +87 -0
- package/dist/host/vfx-runtime-host.d.ts.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +1572 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +63 -0
- package/src/__tests__/billboard-advanced.integration.test.ts +69 -0
- package/src/__tests__/data-interface-providers.unit.test.ts +55 -0
- package/src/__tests__/data-interface-public-api.test-d.ts +58 -0
- package/src/__tests__/event-resources.unit.test.ts +71 -0
- package/src/__tests__/fog-producer.integration.test.ts +59 -0
- package/src/__tests__/gpu-host.integration.test.ts +441 -0
- package/src/__tests__/inspect-snapshot.integration.test.ts +26 -0
- package/src/__tests__/particle-resources.unit.test.ts +75 -0
- package/src/__tests__/render-vocabulary-owner.test-d.ts +81 -0
- package/src/__tests__/stage-plan.integration.test.ts +16 -0
- package/src/__tests__/stage-recovery.integration.test.ts +24 -0
- package/src/__tests__/stage-resource-access-owner.test-d.ts +26 -0
- package/src/__tests__/stage-resource-layout.unit.test.ts +40 -0
- package/src/__tests__/topology-capacity.unit.test.ts +59 -0
- package/src/__tests__/topology-resources.unit.test.ts +29 -0
- package/src/feature/camera.ts +12 -0
- package/src/feature/event-resources.ts +62 -0
- package/src/feature/gpu-particle-feature.ts +1225 -0
- package/src/feature/particle-resources.ts +203 -0
- package/src/feature/stage-plan.ts +282 -0
- package/src/feature/topologies/beam.ts +41 -0
- package/src/feature/topologies/ribbon.ts +31 -0
- package/src/feature/topologies/trail.ts +43 -0
- package/src/host/data-interface-providers.ts +119 -0
- package/src/host/vfx-runtime-host.ts +402 -0
- package/src/index.ts +57 -0
- package/src/shaders/beam.wgsl +69 -0
- package/src/shaders/billboard.wgsl +132 -0
- package/src/shaders/event.wgsl +11 -0
- package/src/shaders/mesh.wgsl +98 -0
- package/src/shaders/ribbon.wgsl +69 -0
- package/src/shaders/stage.wgsl +5 -0
- package/src/shaders/trail.wgsl +77 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
2
|
+
import {
|
|
3
|
+
resolveVfxDataInterfaces,
|
|
4
|
+
type VfxDataInterfaceError,
|
|
5
|
+
type VfxDataInterfaceKind,
|
|
6
|
+
type VfxDataInterfaceProvider,
|
|
7
|
+
type VfxDataInterfaceRequirement,
|
|
8
|
+
type VfxDataInterfaceResolution,
|
|
9
|
+
type VfxDataInterfaceResource,
|
|
10
|
+
type VfxDataInterfaceToken,
|
|
11
|
+
} from '@forgeax/engine-vfx';
|
|
12
|
+
|
|
13
|
+
export type { VfxDataInterfaceProvider } from '@forgeax/engine-vfx';
|
|
14
|
+
|
|
15
|
+
export interface VfxDataInterfaceAvailabilitySource {
|
|
16
|
+
readonly available: (generation: number) => boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface VfxDataInterfaceRegistry {
|
|
20
|
+
readonly providers: readonly VfxDataInterfaceProvider[];
|
|
21
|
+
readonly snapshot:
|
|
22
|
+
| {
|
|
23
|
+
readonly generation: number;
|
|
24
|
+
readonly result: Result<VfxDataInterfaceResolution, VfxDataInterfaceError>;
|
|
25
|
+
}
|
|
26
|
+
| undefined;
|
|
27
|
+
register(provider: VfxDataInterfaceProvider): Result<void, VfxDataInterfaceError>;
|
|
28
|
+
resolve(
|
|
29
|
+
requirements: readonly VfxDataInterfaceRequirement[],
|
|
30
|
+
generation: number,
|
|
31
|
+
): Result<VfxDataInterfaceResolution, VfxDataInterfaceError>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function duplicate(token: VfxDataInterfaceToken, providerId: string): VfxDataInterfaceError {
|
|
35
|
+
return {
|
|
36
|
+
code: 'vfx-data-interface-duplicate',
|
|
37
|
+
expected: 'at most one provider for each reflected Data Interface token',
|
|
38
|
+
hint: `remove provider ${providerId} or the existing provider for ${token} and retry`,
|
|
39
|
+
detail: { token, providerId },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function availableProvider<K extends VfxDataInterfaceKind>(
|
|
44
|
+
token: VfxDataInterfaceProvider<K>['token'],
|
|
45
|
+
kind: K,
|
|
46
|
+
bindingType: VfxDataInterfaceProvider<K>['bindingType'],
|
|
47
|
+
source: VfxDataInterfaceAvailabilitySource,
|
|
48
|
+
): VfxDataInterfaceProvider<K> {
|
|
49
|
+
return {
|
|
50
|
+
id: `${token}-provider`,
|
|
51
|
+
token,
|
|
52
|
+
kind,
|
|
53
|
+
bindingType,
|
|
54
|
+
provide: (generation) => {
|
|
55
|
+
if (!source.available(generation)) {
|
|
56
|
+
return err({
|
|
57
|
+
code: 'vfx-data-interface-missing',
|
|
58
|
+
expected: `an available ${kind} provider for ${token}`,
|
|
59
|
+
hint: `make ${token} available for generation ${generation} and retry rendering`,
|
|
60
|
+
detail: { token, providerId: `${token}-provider` },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const resource: VfxDataInterfaceResource = {
|
|
64
|
+
token,
|
|
65
|
+
kind,
|
|
66
|
+
bindingType,
|
|
67
|
+
generation,
|
|
68
|
+
};
|
|
69
|
+
return ok(resource);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createCameraProvider(
|
|
75
|
+
source: VfxDataInterfaceAvailabilitySource,
|
|
76
|
+
): VfxDataInterfaceProvider<'camera'> {
|
|
77
|
+
return availableProvider('vfx:camera', 'camera', 'uniform', source);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createSceneDepthProvider(
|
|
81
|
+
source: VfxDataInterfaceAvailabilitySource,
|
|
82
|
+
): VfxDataInterfaceProvider<'scene-depth'> {
|
|
83
|
+
return availableProvider('vfx:scene-depth', 'scene-depth', 'sampled-depth', source);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createVfxDataInterfaceRegistry(
|
|
87
|
+
initialProviders: readonly VfxDataInterfaceProvider[] = [],
|
|
88
|
+
): VfxDataInterfaceRegistry {
|
|
89
|
+
const entries = new Map<VfxDataInterfaceToken, VfxDataInterfaceProvider>();
|
|
90
|
+
let registrationError: VfxDataInterfaceError | undefined;
|
|
91
|
+
let lastSnapshot: VfxDataInterfaceRegistry['snapshot'];
|
|
92
|
+
const registry: VfxDataInterfaceRegistry = {
|
|
93
|
+
get providers() {
|
|
94
|
+
return Object.freeze([...entries.values()]);
|
|
95
|
+
},
|
|
96
|
+
get snapshot() {
|
|
97
|
+
return lastSnapshot;
|
|
98
|
+
},
|
|
99
|
+
register(provider) {
|
|
100
|
+
if (entries.has(provider.token)) {
|
|
101
|
+
const error = duplicate(provider.token, provider.id);
|
|
102
|
+
registrationError ??= error;
|
|
103
|
+
return err(error);
|
|
104
|
+
}
|
|
105
|
+
entries.set(provider.token, provider);
|
|
106
|
+
return ok(undefined);
|
|
107
|
+
},
|
|
108
|
+
resolve(requirements, generation) {
|
|
109
|
+
const result =
|
|
110
|
+
registrationError === undefined
|
|
111
|
+
? resolveVfxDataInterfaces(requirements, [...entries.values()], generation)
|
|
112
|
+
: err(registrationError);
|
|
113
|
+
lastSnapshot = Object.freeze({ generation, result });
|
|
114
|
+
return result;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
for (const provider of initialProviders) registry.register(provider);
|
|
118
|
+
return registry;
|
|
119
|
+
}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import type { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
2
|
+
import {
|
|
3
|
+
type AssetRegistryResolver,
|
|
4
|
+
getAssetRegistryResolver,
|
|
5
|
+
} from '@forgeax/engine-assets-runtime/internal';
|
|
6
|
+
import { createWorldContext, type EntityHandle, type World } from '@forgeax/engine-ecs';
|
|
7
|
+
import type { AssetDecoderLease, MaterialAsset, MeshAsset, Result } from '@forgeax/engine-types';
|
|
8
|
+
import { err, ok } from '@forgeax/engine-types';
|
|
9
|
+
import type {
|
|
10
|
+
VfxDataInterfaceError,
|
|
11
|
+
VfxDataInterfaceProvider,
|
|
12
|
+
VfxDataInterfaceRequirement,
|
|
13
|
+
VfxDataInterfaceResolution,
|
|
14
|
+
VfxGpuPlayerInspectSnapshot,
|
|
15
|
+
VfxGpuRuntimeDiagnostic,
|
|
16
|
+
VfxReplayInput,
|
|
17
|
+
VfxValueMap,
|
|
18
|
+
} from '@forgeax/engine-vfx';
|
|
19
|
+
import {
|
|
20
|
+
ParticleEffectPlayer,
|
|
21
|
+
VFX_GPU_RUNTIME_RESOURCE_KEY,
|
|
22
|
+
type VfxGpuRuntime,
|
|
23
|
+
vfxGpuEffectContribution,
|
|
24
|
+
vfxGpuRuntimePlugin,
|
|
25
|
+
} from '@forgeax/engine-vfx';
|
|
26
|
+
import type { ParticleRenderCameraSource } from '../feature/camera.js';
|
|
27
|
+
import { gpuParticleRenderFeature } from '../feature/gpu-particle-feature.js';
|
|
28
|
+
import {
|
|
29
|
+
createVfxDataInterfaceRegistry,
|
|
30
|
+
type VfxDataInterfaceRegistry,
|
|
31
|
+
} from './data-interface-providers.js';
|
|
32
|
+
|
|
33
|
+
export interface VfxRuntimeHostOptions {
|
|
34
|
+
readonly camera: ParticleRenderCameraSource;
|
|
35
|
+
readonly maxQueuedTicks?: number;
|
|
36
|
+
readonly providers?: readonly VfxDataInterfaceProvider[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface VfxRuntimeHostError {
|
|
40
|
+
readonly code:
|
|
41
|
+
| 'vfx-host-loader-install-failed'
|
|
42
|
+
| 'vfx-host-world-attach-failed'
|
|
43
|
+
| 'vfx-host-world-detach-failed';
|
|
44
|
+
readonly expected: string;
|
|
45
|
+
readonly hint: string;
|
|
46
|
+
readonly detail: { readonly cause: unknown };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface VfxRuntimeHostControlError {
|
|
50
|
+
readonly code:
|
|
51
|
+
| 'vfx-host-control-world-detached'
|
|
52
|
+
| 'vfx-host-control-stale-generation'
|
|
53
|
+
| 'vfx-host-control-runtime-unavailable'
|
|
54
|
+
| 'vfx-host-control-player-unavailable';
|
|
55
|
+
readonly expected: string;
|
|
56
|
+
readonly hint: string;
|
|
57
|
+
readonly detail: {
|
|
58
|
+
readonly requestedGeneration?: number;
|
|
59
|
+
readonly currentGeneration?: number;
|
|
60
|
+
readonly player?: EntityHandle;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface VfxRuntimeHostControl {
|
|
65
|
+
readonly generation: number;
|
|
66
|
+
replay(input: {
|
|
67
|
+
readonly player: EntityHandle;
|
|
68
|
+
readonly replayInput?: VfxReplayInput<VfxValueMap>;
|
|
69
|
+
}): Result<{ readonly state: 'queued'; readonly generation: number }, VfxRuntimeHostControlError>;
|
|
70
|
+
setEmitterSessionEnabled(input: {
|
|
71
|
+
readonly player: EntityHandle;
|
|
72
|
+
readonly emitterId: string;
|
|
73
|
+
readonly enabled: boolean;
|
|
74
|
+
}): Result<
|
|
75
|
+
{
|
|
76
|
+
readonly state: 'enabled' | 'disabled';
|
|
77
|
+
readonly generation: number;
|
|
78
|
+
},
|
|
79
|
+
VfxRuntimeHostControlError
|
|
80
|
+
>;
|
|
81
|
+
setPlayerRenderConsumption(input: {
|
|
82
|
+
readonly player: EntityHandle;
|
|
83
|
+
readonly enabled: boolean;
|
|
84
|
+
}): Result<
|
|
85
|
+
{
|
|
86
|
+
readonly state: 'enabled' | 'paused';
|
|
87
|
+
readonly generation: number;
|
|
88
|
+
},
|
|
89
|
+
VfxRuntimeHostControlError
|
|
90
|
+
>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface VfxRuntimeHost {
|
|
94
|
+
readonly feature: ReturnType<typeof gpuParticleRenderFeature>;
|
|
95
|
+
readonly dataInterfaces: VfxDataInterfaceRegistry;
|
|
96
|
+
inspect(world: World): VfxRuntimeHostInspectSnapshot | undefined;
|
|
97
|
+
acquireControl(world: World): Result<VfxRuntimeHostControl, VfxRuntimeHostControlError>;
|
|
98
|
+
resolveDataInterfaces(input: {
|
|
99
|
+
readonly requirements: readonly VfxDataInterfaceRequirement[];
|
|
100
|
+
readonly generation: number;
|
|
101
|
+
}): Result<VfxDataInterfaceResolution, VfxDataInterfaceError>;
|
|
102
|
+
attachWorld(input: {
|
|
103
|
+
readonly world: World;
|
|
104
|
+
readonly assets: AssetRegistry;
|
|
105
|
+
}): Promise<Result<{ readonly state: 'attached' | 'already-attached' }, VfxRuntimeHostError>>;
|
|
106
|
+
detachWorld(input: {
|
|
107
|
+
readonly world: World;
|
|
108
|
+
}): Promise<Result<{ readonly state: 'detached' | 'not-attached' }, VfxRuntimeHostError>>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Install the VFX owner decoder into the one host registry. */
|
|
112
|
+
export function installVfxRuntimeDecoder(registry: AssetRegistry): AssetDecoderLease {
|
|
113
|
+
return registry.installDecoder(vfxGpuEffectContribution.kind, vfxGpuEffectContribution.decoder);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface VfxRuntimeHostInspectSnapshot {
|
|
117
|
+
readonly generation: number;
|
|
118
|
+
/** VFX GPU resource generation; distinct from host attachment generation. */
|
|
119
|
+
readonly renderGeneration: number;
|
|
120
|
+
readonly players: readonly VfxGpuPlayerInspectSnapshot[];
|
|
121
|
+
readonly diagnostics: readonly VfxGpuRuntimeDiagnostic[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
type PluginContext = Awaited<ReturnType<typeof createWorldContext>>;
|
|
125
|
+
|
|
126
|
+
function failure(
|
|
127
|
+
code: VfxRuntimeHostError['code'],
|
|
128
|
+
expected: string,
|
|
129
|
+
hint: string,
|
|
130
|
+
cause: unknown,
|
|
131
|
+
): VfxRuntimeHostError {
|
|
132
|
+
return { code, expected, hint, detail: { cause } };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function controlFailure(
|
|
136
|
+
code: VfxRuntimeHostControlError['code'],
|
|
137
|
+
expected: string,
|
|
138
|
+
hint: string,
|
|
139
|
+
detail: VfxRuntimeHostControlError['detail'] = {},
|
|
140
|
+
): VfxRuntimeHostControlError {
|
|
141
|
+
return { code, expected, hint, detail };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function createVfxRuntimeHost(options: VfxRuntimeHostOptions): VfxRuntimeHost {
|
|
145
|
+
const decoderLeases = new WeakMap<AssetRegistry, AssetDecoderLease>();
|
|
146
|
+
const worlds = new WeakMap<
|
|
147
|
+
World,
|
|
148
|
+
{
|
|
149
|
+
readonly assets: AssetRegistry;
|
|
150
|
+
readonly resolver: AssetRegistryResolver | undefined;
|
|
151
|
+
readonly generation: number;
|
|
152
|
+
readonly pluginContext: PluginContext;
|
|
153
|
+
readonly renderAssets: Map<string, MaterialAsset | MeshAsset>;
|
|
154
|
+
readonly pendingRenderAssets: Map<string, Promise<void>>;
|
|
155
|
+
readonly unsubscribeAssets: () => void;
|
|
156
|
+
catalogEpoch: number;
|
|
157
|
+
}
|
|
158
|
+
>();
|
|
159
|
+
const pausedPlayers = new WeakMap<World, Set<EntityHandle>>();
|
|
160
|
+
let nextGeneration = 1;
|
|
161
|
+
const dataInterfaces = createVfxDataInterfaceRegistry(options.providers);
|
|
162
|
+
const readRenderAsset = <Kind extends 'material' | 'mesh'>(
|
|
163
|
+
world: World,
|
|
164
|
+
guid: string,
|
|
165
|
+
kind: Kind,
|
|
166
|
+
): (Kind extends 'material' ? MaterialAsset : MeshAsset) | undefined => {
|
|
167
|
+
const attached = worlds.get(world);
|
|
168
|
+
if (attached === undefined) return undefined;
|
|
169
|
+
const key = `${kind}:${guid.toLowerCase()}`;
|
|
170
|
+
const cached = attached.renderAssets.get(key);
|
|
171
|
+
if (cached?.kind === kind) return cached as Kind extends 'material' ? MaterialAsset : MeshAsset;
|
|
172
|
+
const resolved = attached.resolver?.lookup<MaterialAsset | MeshAsset>(guid);
|
|
173
|
+
if (resolved?.kind === kind) {
|
|
174
|
+
attached.renderAssets.set(key, resolved);
|
|
175
|
+
return resolved as Kind extends 'material' ? MaterialAsset : MeshAsset;
|
|
176
|
+
}
|
|
177
|
+
if (!attached.pendingRenderAssets.has(key)) {
|
|
178
|
+
const epoch = attached.catalogEpoch;
|
|
179
|
+
const request = attached.assets
|
|
180
|
+
.load(guid, kind)
|
|
181
|
+
.then((result) => {
|
|
182
|
+
if (!result.ok || worlds.get(world) !== attached || attached.catalogEpoch !== epoch)
|
|
183
|
+
return;
|
|
184
|
+
attached.renderAssets.set(key, result.value);
|
|
185
|
+
})
|
|
186
|
+
.catch(() => undefined);
|
|
187
|
+
attached.pendingRenderAssets.set(key, request);
|
|
188
|
+
void request.finally(() => {
|
|
189
|
+
if (attached.pendingRenderAssets.get(key) === request) {
|
|
190
|
+
attached.pendingRenderAssets.delete(key);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return undefined;
|
|
195
|
+
};
|
|
196
|
+
const feature = gpuParticleRenderFeature({
|
|
197
|
+
camera: options.camera,
|
|
198
|
+
dataInterfaces,
|
|
199
|
+
material: { read: (world, guid) => readRenderAsset(world, guid, 'material') },
|
|
200
|
+
mesh: { read: (world, guid) => readRenderAsset(world, guid, 'mesh') },
|
|
201
|
+
playerConsumption: {
|
|
202
|
+
isEnabled: (world, player) => !pausedPlayers.get(world)?.has(player),
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
return {
|
|
206
|
+
feature,
|
|
207
|
+
dataInterfaces,
|
|
208
|
+
inspect: (world) => {
|
|
209
|
+
const attached = worlds.get(world);
|
|
210
|
+
if (attached === undefined || !world.hasResource(VFX_GPU_RUNTIME_RESOURCE_KEY))
|
|
211
|
+
return undefined;
|
|
212
|
+
const runtime = world.getResource<VfxGpuRuntime>(VFX_GPU_RUNTIME_RESOURCE_KEY);
|
|
213
|
+
return Object.freeze({
|
|
214
|
+
generation: attached.generation,
|
|
215
|
+
renderGeneration: runtime.renderGeneration,
|
|
216
|
+
players: runtime.inspectPlayers(),
|
|
217
|
+
diagnostics: runtime.diagnostics(),
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
acquireControl: (world) => {
|
|
221
|
+
const attached = worlds.get(world);
|
|
222
|
+
if (attached === undefined) {
|
|
223
|
+
return err(
|
|
224
|
+
controlFailure(
|
|
225
|
+
'vfx-host-control-world-detached',
|
|
226
|
+
'an attached VFX Runtime World',
|
|
227
|
+
'attach the World to this VfxRuntimeHost before acquiring controls',
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
const requestedGeneration = attached.generation;
|
|
232
|
+
const withRuntime = <T>(
|
|
233
|
+
player: EntityHandle,
|
|
234
|
+
action: (runtime: VfxGpuRuntime) => T,
|
|
235
|
+
): Result<T, VfxRuntimeHostControlError> => {
|
|
236
|
+
const current = worlds.get(world);
|
|
237
|
+
if (current === undefined) {
|
|
238
|
+
return err(
|
|
239
|
+
controlFailure(
|
|
240
|
+
'vfx-host-control-world-detached',
|
|
241
|
+
`VFX host generation ${requestedGeneration} to remain attached`,
|
|
242
|
+
'reacquire controls after the World is attached again',
|
|
243
|
+
{ requestedGeneration },
|
|
244
|
+
),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
if (current.generation !== requestedGeneration) {
|
|
248
|
+
return err(
|
|
249
|
+
controlFailure(
|
|
250
|
+
'vfx-host-control-stale-generation',
|
|
251
|
+
`VFX host generation ${requestedGeneration}`,
|
|
252
|
+
'discard this stale control lease and acquire one from the current host generation',
|
|
253
|
+
{ requestedGeneration, currentGeneration: current.generation },
|
|
254
|
+
),
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
if (!world.hasResource(VFX_GPU_RUNTIME_RESOURCE_KEY)) {
|
|
258
|
+
return err(
|
|
259
|
+
controlFailure(
|
|
260
|
+
'vfx-host-control-runtime-unavailable',
|
|
261
|
+
'the attached World to own its VFX GPU runtime resource',
|
|
262
|
+
'repair the host attachment before retrying the preview command',
|
|
263
|
+
{ requestedGeneration, currentGeneration: current.generation },
|
|
264
|
+
),
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
if (!world.get(player, ParticleEffectPlayer).ok) {
|
|
268
|
+
return err(
|
|
269
|
+
controlFailure(
|
|
270
|
+
'vfx-host-control-player-unavailable',
|
|
271
|
+
'a live entity with ParticleEffectPlayer in the attached World',
|
|
272
|
+
'discard the stale player handle or target a VFX player owned by this World',
|
|
273
|
+
{ requestedGeneration, currentGeneration: current.generation, player },
|
|
274
|
+
),
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
return ok(action(world.getResource<VfxGpuRuntime>(VFX_GPU_RUNTIME_RESOURCE_KEY)));
|
|
278
|
+
};
|
|
279
|
+
const control: VfxRuntimeHostControl = {
|
|
280
|
+
generation: requestedGeneration,
|
|
281
|
+
replay: ({ player, replayInput }) =>
|
|
282
|
+
withRuntime(player, (runtime) => {
|
|
283
|
+
runtime.replay(player, replayInput);
|
|
284
|
+
return Object.freeze({ state: 'queued' as const, generation: requestedGeneration });
|
|
285
|
+
}),
|
|
286
|
+
setEmitterSessionEnabled: ({ player, emitterId, enabled }) =>
|
|
287
|
+
withRuntime(player, (runtime) => {
|
|
288
|
+
runtime.setEmitterSessionEnabled(player, emitterId, enabled);
|
|
289
|
+
return Object.freeze({
|
|
290
|
+
state: enabled ? ('enabled' as const) : ('disabled' as const),
|
|
291
|
+
generation: requestedGeneration,
|
|
292
|
+
});
|
|
293
|
+
}),
|
|
294
|
+
setPlayerRenderConsumption: ({ player, enabled }) =>
|
|
295
|
+
withRuntime(player, () => {
|
|
296
|
+
let paused = pausedPlayers.get(world);
|
|
297
|
+
if (enabled) {
|
|
298
|
+
paused?.delete(player);
|
|
299
|
+
if (paused !== undefined && paused.size === 0) pausedPlayers.delete(world);
|
|
300
|
+
} else {
|
|
301
|
+
paused ??= new Set();
|
|
302
|
+
paused.add(player);
|
|
303
|
+
pausedPlayers.set(world, paused);
|
|
304
|
+
}
|
|
305
|
+
return Object.freeze({
|
|
306
|
+
state: enabled ? ('enabled' as const) : ('paused' as const),
|
|
307
|
+
generation: requestedGeneration,
|
|
308
|
+
});
|
|
309
|
+
}),
|
|
310
|
+
};
|
|
311
|
+
return ok(Object.freeze(control));
|
|
312
|
+
},
|
|
313
|
+
resolveDataInterfaces: ({ requirements, generation }) =>
|
|
314
|
+
dataInterfaces.resolve(requirements, generation),
|
|
315
|
+
attachWorld: async ({ world, assets }) => {
|
|
316
|
+
if (worlds.has(world)) return ok({ state: 'already-attached' });
|
|
317
|
+
if (!decoderLeases.has(assets)) {
|
|
318
|
+
try {
|
|
319
|
+
decoderLeases.set(assets, installVfxRuntimeDecoder(assets));
|
|
320
|
+
} catch (cause) {
|
|
321
|
+
return err(
|
|
322
|
+
failure(
|
|
323
|
+
'vfx-host-loader-install-failed',
|
|
324
|
+
'the VFX owner decoder to be installed once in the runtime core',
|
|
325
|
+
'remove a conflicting particle-effect decoder and retry attachWorld',
|
|
326
|
+
cause,
|
|
327
|
+
),
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
let pluginContext: PluginContext;
|
|
332
|
+
try {
|
|
333
|
+
pluginContext = await createWorldContext(world, [
|
|
334
|
+
vfxGpuRuntimePlugin(
|
|
335
|
+
options.maxQueuedTicks === undefined ? {} : { maxQueuedTicks: options.maxQueuedTicks },
|
|
336
|
+
),
|
|
337
|
+
]);
|
|
338
|
+
} catch (cause) {
|
|
339
|
+
return err(
|
|
340
|
+
failure(
|
|
341
|
+
'vfx-host-world-attach-failed',
|
|
342
|
+
'the World FixedUpdate VFX intent producer to install',
|
|
343
|
+
'repair the reported World registration conflict and retry',
|
|
344
|
+
cause,
|
|
345
|
+
),
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
let resolver: AssetRegistryResolver | undefined;
|
|
349
|
+
try {
|
|
350
|
+
resolver = getAssetRegistryResolver(assets);
|
|
351
|
+
} catch {
|
|
352
|
+
resolver = undefined;
|
|
353
|
+
}
|
|
354
|
+
const attached = {
|
|
355
|
+
assets,
|
|
356
|
+
resolver,
|
|
357
|
+
pluginContext,
|
|
358
|
+
generation: nextGeneration++,
|
|
359
|
+
renderAssets: new Map<string, MaterialAsset | MeshAsset>(),
|
|
360
|
+
pendingRenderAssets: new Map<string, Promise<void>>(),
|
|
361
|
+
catalogEpoch:
|
|
362
|
+
typeof (assets as { readonly snapshot?: unknown }).snapshot === 'function'
|
|
363
|
+
? assets.snapshot().epoch
|
|
364
|
+
: 0,
|
|
365
|
+
unsubscribeAssets: () => {},
|
|
366
|
+
};
|
|
367
|
+
const subscribe = (assets as { readonly subscribe?: AssetRegistry['subscribe'] }).subscribe;
|
|
368
|
+
if (subscribe !== undefined) {
|
|
369
|
+
attached.unsubscribeAssets = subscribe((snapshot) => {
|
|
370
|
+
if (snapshot.epoch === attached.catalogEpoch) return;
|
|
371
|
+
attached.catalogEpoch = snapshot.epoch;
|
|
372
|
+
attached.renderAssets.clear();
|
|
373
|
+
attached.pendingRenderAssets.clear();
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
worlds.set(world, attached);
|
|
377
|
+
return ok({ state: 'attached' });
|
|
378
|
+
},
|
|
379
|
+
detachWorld: async ({ world }) => {
|
|
380
|
+
const attached = worlds.get(world);
|
|
381
|
+
if (attached === undefined) return ok({ state: 'not-attached' });
|
|
382
|
+
try {
|
|
383
|
+
await attached.pluginContext.fiber.dispose();
|
|
384
|
+
} catch (cause) {
|
|
385
|
+
return err(
|
|
386
|
+
failure(
|
|
387
|
+
'vfx-host-world-detach-failed',
|
|
388
|
+
'the VFX FixedUpdate producer to detach exactly once',
|
|
389
|
+
'inspect the World schedule and retry detachWorld',
|
|
390
|
+
cause,
|
|
391
|
+
),
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
pausedPlayers.delete(world);
|
|
395
|
+
attached.unsubscribeAssets();
|
|
396
|
+
attached.renderAssets.clear();
|
|
397
|
+
attached.pendingRenderAssets.clear();
|
|
398
|
+
worlds.delete(world);
|
|
399
|
+
return ok({ state: 'detached' });
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// @forgeax/engine-vfx-render - GPU particle execution and rendering.
|
|
2
|
+
|
|
3
|
+
export type { ParticleRenderCamera, ParticleRenderCameraSource } from './feature/camera.js';
|
|
4
|
+
export {
|
|
5
|
+
encodeEventInputs,
|
|
6
|
+
eventCapacity,
|
|
7
|
+
eventCounterData,
|
|
8
|
+
eventInputCapacity,
|
|
9
|
+
VFX_EVENT_BYTES,
|
|
10
|
+
VFX_EVENT_COUNTER_BYTES,
|
|
11
|
+
VFX_EVENT_INPUT_BYTES,
|
|
12
|
+
} from './feature/event-resources.js';
|
|
13
|
+
export type { VfxGpuRenderObservation } from './feature/gpu-particle-feature.js';
|
|
14
|
+
export {
|
|
15
|
+
createVfxRenderInspectSnapshot,
|
|
16
|
+
gpuParticleRenderFeature,
|
|
17
|
+
resolveBillboardAdvancedState,
|
|
18
|
+
topologyRecoveryHint,
|
|
19
|
+
} from './feature/gpu-particle-feature.js';
|
|
20
|
+
export {
|
|
21
|
+
createTopologyResourcePlan,
|
|
22
|
+
PARTICLE_SHADER_IDENTIFIERS,
|
|
23
|
+
topologyCapacitySnapshot,
|
|
24
|
+
} from './feature/particle-resources.js';
|
|
25
|
+
export type {
|
|
26
|
+
VfxStagePlanError,
|
|
27
|
+
VfxStagePlanObservation,
|
|
28
|
+
VfxStageReadiness,
|
|
29
|
+
VfxStageRecovery,
|
|
30
|
+
VfxValidatedStage,
|
|
31
|
+
VfxValidatedStagePlan,
|
|
32
|
+
} from './feature/stage-plan.js';
|
|
33
|
+
export {
|
|
34
|
+
observeStagePlan,
|
|
35
|
+
stageDispatches,
|
|
36
|
+
stageRecoveryReadiness,
|
|
37
|
+
validatedStagePlan,
|
|
38
|
+
} from './feature/stage-plan.js';
|
|
39
|
+
export type {
|
|
40
|
+
VfxDataInterfaceAvailabilitySource,
|
|
41
|
+
VfxDataInterfaceProvider,
|
|
42
|
+
VfxDataInterfaceRegistry,
|
|
43
|
+
} from './host/data-interface-providers.js';
|
|
44
|
+
export {
|
|
45
|
+
createCameraProvider,
|
|
46
|
+
createSceneDepthProvider,
|
|
47
|
+
createVfxDataInterfaceRegistry,
|
|
48
|
+
} from './host/data-interface-providers.js';
|
|
49
|
+
export type {
|
|
50
|
+
VfxRuntimeHost,
|
|
51
|
+
VfxRuntimeHostControl,
|
|
52
|
+
VfxRuntimeHostControlError,
|
|
53
|
+
VfxRuntimeHostError,
|
|
54
|
+
VfxRuntimeHostInspectSnapshot,
|
|
55
|
+
VfxRuntimeHostOptions,
|
|
56
|
+
} from './host/vfx-runtime-host.js';
|
|
57
|
+
export { createVfxRuntimeHost, installVfxRuntimeDecoder } from './host/vfx-runtime-host.js';
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#define_import_path forgeax::vfx-render.particles.beam
|
|
2
|
+
#import forgeax_view::common::{View, FogViewParams, FogRay, view}
|
|
3
|
+
#import forgeax_view::fog::{apply_fog}
|
|
4
|
+
|
|
5
|
+
struct BeamInput {
|
|
6
|
+
@location(0) start: vec3<f32>,
|
|
7
|
+
@location(1) endpoint: vec3<f32>,
|
|
8
|
+
@location(2) color: vec4<f32>,
|
|
9
|
+
@location(3) properties: vec2<f32>,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
struct VertexOutput {
|
|
13
|
+
@builtin(position) position: vec4<f32>,
|
|
14
|
+
@location(0) color: vec4<f32>,
|
|
15
|
+
@location(1) clip_position: vec3<f32>,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fn fogWorldPoint(ndc: vec3<f32>) -> vec3<f32> {
|
|
19
|
+
let homogeneous = view.inverseViewProj * vec4<f32>(ndc, 1.0);
|
|
20
|
+
let divisor = select(1.0, homogeneous.w, abs(homogeneous.w) > 0.000001);
|
|
21
|
+
return homogeneous.xyz / divisor;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fn fogRayFromNdc(ndc: vec3<f32>) -> FogRay {
|
|
25
|
+
let worldPosition = fogWorldPoint(ndc);
|
|
26
|
+
let nearPosition = fogWorldPoint(vec3<f32>(ndc.xy, 0.0));
|
|
27
|
+
let farPosition = fogWorldPoint(vec3<f32>(ndc.xy, 1.0));
|
|
28
|
+
let perspective = view.temporalProjection.z < 0.5;
|
|
29
|
+
let perspectiveVector = worldPosition - view.cameraPos;
|
|
30
|
+
let orthographicVector = farPosition - nearPosition;
|
|
31
|
+
let direction = normalize(select(orthographicVector, perspectiveVector, perspective));
|
|
32
|
+
let origin = select(nearPosition, view.cameraPos, perspective);
|
|
33
|
+
let ray_distance = select(
|
|
34
|
+
max(dot(worldPosition - nearPosition, direction), 0.0),
|
|
35
|
+
length(perspectiveVector),
|
|
36
|
+
perspective,
|
|
37
|
+
);
|
|
38
|
+
return FogRay(origin, direction, ray_distance);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@vertex
|
|
42
|
+
fn vs_main(input: BeamInput, @builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
43
|
+
let corners = array<vec2<f32>, 6>(
|
|
44
|
+
vec2<f32>(0.0, -1.0), vec2<f32>(1.0, -1.0), vec2<f32>(1.0, 1.0),
|
|
45
|
+
vec2<f32>(0.0, -1.0), vec2<f32>(1.0, 1.0), vec2<f32>(0.0, 1.0)
|
|
46
|
+
);
|
|
47
|
+
let corner = corners[vertexIndex];
|
|
48
|
+
let delta = input.endpoint.xy - input.start.xy;
|
|
49
|
+
let normal = normalize(vec2<f32>(-delta.y, delta.x) + vec2<f32>(0.000001, 0.0));
|
|
50
|
+
let point = mix(input.start, input.endpoint, corner.x);
|
|
51
|
+
let clipPosition = vec3<f32>(point.xy + normal * corner.y * input.properties.x, point.z);
|
|
52
|
+
var output: VertexOutput;
|
|
53
|
+
output.position = vec4<f32>(clipPosition, 1.0);
|
|
54
|
+
output.color = input.color;
|
|
55
|
+
output.clip_position = clipPosition;
|
|
56
|
+
return output;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@fragment
|
|
60
|
+
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
61
|
+
let base = vec4<f32>(0.7, 0.2, 1.0, 1.0);
|
|
62
|
+
let alpha = base.a * input.color.a;
|
|
63
|
+
let fogged = apply_fog(
|
|
64
|
+
view.fog,
|
|
65
|
+
fogRayFromNdc(input.clip_position),
|
|
66
|
+
vec4<f32>(base.rgb * input.color.rgb, alpha),
|
|
67
|
+
);
|
|
68
|
+
return vec4<f32>(fogged.rgb * fogged.a, fogged.a);
|
|
69
|
+
}
|