@forgeax/engine-vfx-render 0.1.6 → 0.1.19
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/feature/gpu-particle-feature.d.ts.map +1 -1
- package/dist/feature/particle-resources.d.ts +11 -0
- package/dist/feature/particle-resources.d.ts.map +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 +27 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -8
- package/src/__tests__/fog-producer.integration.test.ts +5 -3
- package/src/__tests__/gpu-host.integration.test.ts +102 -0
- package/src/__tests__/particle-resources.unit.test.ts +9 -0
- package/src/__tests__/stage-plan.integration.test.ts +7 -1
- package/src/feature/gpu-particle-feature.ts +17 -3
- package/src/feature/particle-resources.ts +18 -0
- package/src/host/vfx-runtime-host.ts +56 -16
- package/src/index.ts +1 -0
- package/src/shaders/beam.wgsl +1 -24
- package/src/shaders/billboard.wgsl +1 -24
- package/src/shaders/mesh.wgsl +1 -24
- package/src/shaders/ribbon.wgsl +1 -24
- package/src/shaders/trail.wgsl +1 -24
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.19",
|
|
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.19",
|
|
26
|
+
"@forgeax/engine-ecs": "0.1.19",
|
|
27
|
+
"@forgeax/engine-math": "0.1.19",
|
|
28
|
+
"@forgeax/engine-render": "0.1.19",
|
|
29
|
+
"@forgeax/engine-scene": "0.1.19",
|
|
30
|
+
"@forgeax/engine-types": "0.1.19",
|
|
31
|
+
"@forgeax/engine-vfx": "0.1.19"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {},
|
|
34
34
|
"forgeax": {
|
|
@@ -21,9 +21,11 @@ describe('VFX Fog producer contract', () => {
|
|
|
21
21
|
);
|
|
22
22
|
expect(source, name).toMatch(/#import\s+forgeax_view::fog::\{[^}]*\bapply_fog\b[^}]*\}/);
|
|
23
23
|
expect(count(source, /\bapply_fog\s*\(/g), name).toBe(1);
|
|
24
|
-
expect(count(source, /\bFogRay\s*\(/g), name).toBe(
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
expect(count(source, /\bFogRay\s*\(/g) + count(source, /\bfogRayFromNdc\s*\(/g), name).toBe(
|
|
25
|
+
1,
|
|
26
|
+
);
|
|
27
|
+
expect(source, name).toMatch(/world[_A-Za-z]*position|worldPosition|fogRayFromNdc/);
|
|
28
|
+
expect(source, name).toMatch(/ray[_A-Za-z]*distance|distance|fogRayFromNdc/);
|
|
27
29
|
expect(source, name).toMatch(/\.a\b|alpha/);
|
|
28
30
|
expect(source, name).not.toMatch(/texture_3d|raymarch/i);
|
|
29
31
|
}
|
|
@@ -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,107 @@ 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 = {
|
|
54
|
+
kind: 'material',
|
|
55
|
+
passes: [{ name: 'particle-billboard', program: { module: 'custom-depth' } }],
|
|
56
|
+
};
|
|
57
|
+
readonly loaders = { registerPackLoader: () => {} };
|
|
58
|
+
|
|
59
|
+
lookup<T extends Asset = Asset>(guid: string): T | undefined {
|
|
60
|
+
if (this !== assets) throw new Error('asset registry receiver was lost');
|
|
61
|
+
lookups.push(guid);
|
|
62
|
+
return guid === 'material-guid' ? (this.material as T) : undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const assets = new StatefulAssets();
|
|
66
|
+
const host = createVfxRuntimeHost({
|
|
67
|
+
camera: {
|
|
68
|
+
read: () => ({
|
|
69
|
+
position: new Float32Array(3),
|
|
70
|
+
right: new Float32Array([1, 0, 0]),
|
|
71
|
+
up: new Float32Array([0, 1, 0]),
|
|
72
|
+
viewProjection: new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
|
|
73
|
+
}),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
expect(await host.attachWorld({ world, assets })).toMatchObject({ ok: true });
|
|
77
|
+
|
|
78
|
+
const intent = {
|
|
79
|
+
player: world.spawn().unwrap(),
|
|
80
|
+
fixedDelta: 1 / 60,
|
|
81
|
+
phaseTick: 0,
|
|
82
|
+
seed: 1,
|
|
83
|
+
playCycle: 0,
|
|
84
|
+
spawnCount: 1,
|
|
85
|
+
firstParticleId: 0,
|
|
86
|
+
reset: true,
|
|
87
|
+
channelInputs: [],
|
|
88
|
+
emitter: {
|
|
89
|
+
id: 'receiver-check',
|
|
90
|
+
capacity: 4,
|
|
91
|
+
backend: { required: 'gpu' },
|
|
92
|
+
space: 'world',
|
|
93
|
+
simulationWhenCulled: 'continue',
|
|
94
|
+
schedule: { rate: 0, bursts: [] },
|
|
95
|
+
bounds: { kind: 'sphere', center: [0, 0, 0], radius: 1 },
|
|
96
|
+
wgsl: '// receiver check',
|
|
97
|
+
reflection: { entryPoints: [], bindings: [] },
|
|
98
|
+
renderers: [{ kind: 'billboard', material: 'material-guid' }],
|
|
99
|
+
},
|
|
100
|
+
} as unknown as VfxGpuTickIntent;
|
|
101
|
+
const planned = host.feature.plan(
|
|
102
|
+
{
|
|
103
|
+
worlds: [
|
|
104
|
+
{
|
|
105
|
+
world,
|
|
106
|
+
runtime: {
|
|
107
|
+
isEmitterSessionEnabled: () => true,
|
|
108
|
+
setEmitterCameraVisibility: () => {},
|
|
109
|
+
markEventDispatched: () => {},
|
|
110
|
+
commit: () => {},
|
|
111
|
+
},
|
|
112
|
+
camera: {
|
|
113
|
+
position: new Float32Array(3),
|
|
114
|
+
right: new Float32Array([1, 0, 0]),
|
|
115
|
+
up: new Float32Array([0, 1, 0]),
|
|
116
|
+
viewProjection: new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
|
|
117
|
+
},
|
|
118
|
+
intents: [intent],
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
frameNumber: 1,
|
|
122
|
+
} as never,
|
|
123
|
+
{
|
|
124
|
+
targets: [
|
|
125
|
+
{ name: 'scene-color', kind: 'color', format: 'rgba8unorm-srgb', sampleCount: 1 },
|
|
126
|
+
{ name: 'scene-depth', kind: 'depth', format: 'depth24plus', sampleCount: 1 },
|
|
127
|
+
],
|
|
128
|
+
caps: {},
|
|
129
|
+
frame: { frameNumber: 1 },
|
|
130
|
+
generation: 1,
|
|
131
|
+
materialShaderBindingContract: () => 'group-0-resource',
|
|
132
|
+
} as never,
|
|
133
|
+
);
|
|
134
|
+
expect(planned.ok).toBe(true);
|
|
135
|
+
expect(lookups).toEqual(['material-guid']);
|
|
136
|
+
if (planned.ok) {
|
|
137
|
+
expect(planned.value.resources).toEqual(
|
|
138
|
+
expect.arrayContaining([
|
|
139
|
+
expect.objectContaining({
|
|
140
|
+
kind: 'graphics-bindings',
|
|
141
|
+
values: expect.objectContaining({ sceneDepthBinding: 0 }),
|
|
142
|
+
logicalTargets: { sceneDepth: 'scene-depth' },
|
|
143
|
+
}),
|
|
144
|
+
]),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
await host.detachWorld({ world });
|
|
148
|
+
});
|
|
149
|
+
|
|
48
150
|
it('fences preview controls to one attached host generation', async () => {
|
|
49
151
|
const world = new World();
|
|
50
152
|
const assets = {
|
|
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
3
3
|
import {
|
|
4
4
|
canonicalMeshVertices,
|
|
5
5
|
particleMaterialPass,
|
|
6
|
+
particleMaterialSceneDepthBinding,
|
|
6
7
|
particleMaterialUsesBindings,
|
|
7
8
|
} from '../feature/particle-resources.js';
|
|
8
9
|
|
|
@@ -53,6 +54,14 @@ describe('particle material pass', () => {
|
|
|
53
54
|
});
|
|
54
55
|
});
|
|
55
56
|
|
|
57
|
+
it('maps renderer shader contracts to their scene-depth binding slots', () => {
|
|
58
|
+
expect(particleMaterialSceneDepthBinding('group-0-resource')).toBe(0);
|
|
59
|
+
expect(particleMaterialSceneDepthBinding('view-and-scene-depth')).toBe(1);
|
|
60
|
+
expect(particleMaterialSceneDepthBinding('group-0')).toBeUndefined();
|
|
61
|
+
expect(particleMaterialSceneDepthBinding('view-only')).toBeUndefined();
|
|
62
|
+
expect(particleMaterialSceneDepthBinding('render-material')).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
56
65
|
it('does not mistake an ordinary Forward pass for a particle shader', () => {
|
|
57
66
|
const material: MaterialAsset = {
|
|
58
67
|
kind: 'material',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
-
import type
|
|
3
|
+
import { createSceneDataCatalog, type RenderFeaturePlan } from '@forgeax/engine-render';
|
|
4
4
|
import type { VfxGpuTickIntent } from '@forgeax/engine-vfx';
|
|
5
5
|
import { describe, expect, it } from 'vitest';
|
|
6
6
|
import { freezeRenderFeaturePlan } from '../../../render/src/features/plan';
|
|
@@ -108,6 +108,12 @@ describe('VFX managed stage execution seam', () => {
|
|
|
108
108
|
{ name: 'scene-color', kind: 'color', format: 'rgba16float', sampleCount: 1 },
|
|
109
109
|
{ name: 'scene-depth', kind: 'depth', format: 'depth24plus', sampleCount: 1 },
|
|
110
110
|
],
|
|
111
|
+
sceneData: createSceneDataCatalog({
|
|
112
|
+
featureIdentity: feature.identity,
|
|
113
|
+
generation: 1,
|
|
114
|
+
planIdentity: `${feature.identity}:1`,
|
|
115
|
+
rgba16floatRenderable: true,
|
|
116
|
+
}),
|
|
111
117
|
},
|
|
112
118
|
);
|
|
113
119
|
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
createTopologyResourcePlan,
|
|
26
26
|
PARTICLE_SHADER_IDENTIFIERS,
|
|
27
27
|
particleMaterialPass,
|
|
28
|
+
particleMaterialSceneDepthBinding,
|
|
28
29
|
particleMaterialUsesBindings,
|
|
29
30
|
} from './particle-resources.js';
|
|
30
31
|
import type { VfxStagePlanObservation, VfxValidatedStagePlan } from './stage-plan.js';
|
|
@@ -346,6 +347,11 @@ export function gpuParticleRenderFeature(
|
|
|
346
347
|
return {
|
|
347
348
|
identity: IDENTITY,
|
|
348
349
|
requiredCapabilities: ['compute', 'indirectDrawing'],
|
|
350
|
+
// Generated emitter programs are first-use assets. They must hand a
|
|
351
|
+
// WebGPU module to the prepared feature pipeline without waiting for the
|
|
352
|
+
// diagnostic-only getCompilationInfo() round trip; pipeline creation still
|
|
353
|
+
// validates the module before the pass is submitted.
|
|
354
|
+
shaderModuleMode: 'immediate',
|
|
349
355
|
requiredMaterialShaders: Object.values(PARTICLE_SHADER_IDENTIFIERS),
|
|
350
356
|
extract: (context) => {
|
|
351
357
|
const extracted: ExtractedWorld[] = [];
|
|
@@ -552,6 +558,14 @@ export function gpuParticleRenderFeature(
|
|
|
552
558
|
const submesh =
|
|
553
559
|
renderer.kind === 'mesh' ? mesh?.submeshes[renderer.submesh ?? 0] : undefined;
|
|
554
560
|
const indexFormat = mesh?.indices instanceof Uint32Array ? 'uint32' : 'uint16';
|
|
561
|
+
const sceneDepthBinding = isBillboard
|
|
562
|
+
? particleMaterialSceneDepthBinding(
|
|
563
|
+
context.materialShaderBindingContract?.(materialPass.shader) ??
|
|
564
|
+
(materialPass.shader === PARTICLE_SHADER_IDENTIFIERS.billboard
|
|
565
|
+
? 'view-and-scene-depth'
|
|
566
|
+
: undefined),
|
|
567
|
+
)
|
|
568
|
+
: undefined;
|
|
555
569
|
const instances = `${rendererPrefix}.instances`;
|
|
556
570
|
const history = `${rendererPrefix}.history`;
|
|
557
571
|
const projectionRuntime = `${rendererPrefix}.runtime`;
|
|
@@ -690,9 +704,9 @@ export function gpuParticleRenderFeature(
|
|
|
690
704
|
group: 0,
|
|
691
705
|
runtime: projectionRuntime,
|
|
692
706
|
instances,
|
|
693
|
-
...(
|
|
707
|
+
...(sceneDepthBinding === undefined ? {} : { sceneDepthBinding }),
|
|
694
708
|
},
|
|
695
|
-
...(
|
|
709
|
+
...(sceneDepthBinding !== undefined && depthTarget !== undefined
|
|
696
710
|
? { logicalTargets: { sceneDepth: depthTarget.name } }
|
|
697
711
|
: {}),
|
|
698
712
|
},
|
|
@@ -784,7 +798,7 @@ export function gpuParticleRenderFeature(
|
|
|
784
798
|
depthStoreOp: 'store',
|
|
785
799
|
},
|
|
786
800
|
}),
|
|
787
|
-
...(
|
|
801
|
+
...(sceneDepthBinding !== undefined && depthTarget !== undefined
|
|
788
802
|
? { sampledTargets: [depthTarget.name] }
|
|
789
803
|
: {}),
|
|
790
804
|
draws: [
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RenderFeatureMaterialShaderBindingContract } from '@forgeax/engine-render';
|
|
1
2
|
import {
|
|
2
3
|
err,
|
|
3
4
|
type MaterialAsset,
|
|
@@ -161,6 +162,23 @@ export function particleMaterialUsesBindings(material: MaterialAsset | undefined
|
|
|
161
162
|
return (material?.parameters?.length ?? 0) > 0;
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Return the scene-depth binding slot required by a particle material shader.
|
|
167
|
+
*
|
|
168
|
+
* Custom particle shaders historically bind only the sampled depth resource at
|
|
169
|
+
* group(0)/binding(0), while the renderer-owned billboard shader also consumes
|
|
170
|
+
* the view UBO at binding(0) and therefore uses depth at binding(1). Keep this
|
|
171
|
+
* choice derived from the renderer contract instead of assuming every
|
|
172
|
+
* billboard has the built-in layout.
|
|
173
|
+
*/
|
|
174
|
+
export function particleMaterialSceneDepthBinding(
|
|
175
|
+
contract: RenderFeatureMaterialShaderBindingContract | undefined,
|
|
176
|
+
): 0 | 1 | undefined {
|
|
177
|
+
if (contract === 'group-0-resource') return 0;
|
|
178
|
+
if (contract === 'view-and-scene-depth') return 1;
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
164
182
|
function floatAttribute(value: ArrayBuffer | Float32Array | Uint16Array | undefined): Float32Array {
|
|
165
183
|
if (value instanceof Float32Array) return value;
|
|
166
184
|
if (value instanceof Uint16Array) return Float32Array.from(value);
|
|
@@ -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 {
|
package/src/index.ts
CHANGED
package/src/shaders/beam.wgsl
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#define_import_path forgeax::vfx-render.particles.beam
|
|
2
|
-
#import forgeax_view::common::{View, FogViewParams, FogRay, view}
|
|
2
|
+
#import forgeax_view::common::{View, FogViewParams, FogRay, view, fogRayFromNdc}
|
|
3
3
|
#import forgeax_view::fog::{apply_fog}
|
|
4
4
|
|
|
5
5
|
struct BeamInput {
|
|
@@ -15,29 +15,6 @@ struct VertexOutput {
|
|
|
15
15
|
@location(1) clip_position: vec3<f32>,
|
|
16
16
|
}
|
|
17
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
18
|
@vertex
|
|
42
19
|
fn vs_main(input: BeamInput, @builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
43
20
|
let corners = array<vec2<f32>, 6>(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#define_import_path forgeax::vfx-render.particles.billboard
|
|
2
|
-
#import forgeax_view::common::{View, FogViewParams, FogRay, view}
|
|
2
|
+
#import forgeax_view::common::{View, FogViewParams, FogRay, view, fogRayFromNdc}
|
|
3
3
|
#import forgeax_view::fog::{apply_fog}
|
|
4
4
|
|
|
5
5
|
@group(0) @binding(1) var scene_depth: texture_depth_2d;
|
|
@@ -51,29 +51,6 @@ fn softParticle(position: vec4<f32>, alpha: f32, fadeDistance: f32) -> f32 {
|
|
|
51
51
|
return alpha * softParticleFactor(position.z, sceneDepth, fadeDistance);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
fn fogWorldPoint(ndc: vec3<f32>) -> vec3<f32> {
|
|
55
|
-
let homogeneous = view.inverseViewProj * vec4<f32>(ndc, 1.0);
|
|
56
|
-
let divisor = select(1.0, homogeneous.w, abs(homogeneous.w) > 0.000001);
|
|
57
|
-
return homogeneous.xyz / divisor;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
fn fogRayFromNdc(ndc: vec3<f32>) -> FogRay {
|
|
61
|
-
let worldPosition = fogWorldPoint(ndc);
|
|
62
|
-
let nearPosition = fogWorldPoint(vec3<f32>(ndc.xy, 0.0));
|
|
63
|
-
let farPosition = fogWorldPoint(vec3<f32>(ndc.xy, 1.0));
|
|
64
|
-
let perspective = view.temporalProjection.z < 0.5;
|
|
65
|
-
let perspectiveVector = worldPosition - view.cameraPos;
|
|
66
|
-
let orthographicVector = farPosition - nearPosition;
|
|
67
|
-
let direction = normalize(select(orthographicVector, perspectiveVector, perspective));
|
|
68
|
-
let origin = select(nearPosition, view.cameraPos, perspective);
|
|
69
|
-
let ray_distance = select(
|
|
70
|
-
max(dot(worldPosition - nearPosition, direction), 0.0),
|
|
71
|
-
length(perspectiveVector),
|
|
72
|
-
perspective,
|
|
73
|
-
);
|
|
74
|
-
return FogRay(origin, direction, ray_distance);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
54
|
struct VertexInput {
|
|
78
55
|
@location(0) position: vec3<f32>,
|
|
79
56
|
@location(1) right: vec2<f32>,
|
package/src/shaders/mesh.wgsl
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#define_import_path forgeax::vfx-render.particles.mesh
|
|
2
|
-
#import forgeax_view::common::{View, FogViewParams, FogRay, view}
|
|
2
|
+
#import forgeax_view::common::{View, FogViewParams, FogRay, view, fogRayFromNdc}
|
|
3
3
|
#import forgeax_view::fog::{apply_fog}
|
|
4
4
|
|
|
5
5
|
struct VertexOutput {
|
|
@@ -26,29 +26,6 @@ struct VertexInput {
|
|
|
26
26
|
@location(11) surface: vec4<f32>,
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
fn fogWorldPoint(ndc: vec3<f32>) -> vec3<f32> {
|
|
30
|
-
let homogeneous = view.inverseViewProj * vec4<f32>(ndc, 1.0);
|
|
31
|
-
let divisor = select(1.0, homogeneous.w, abs(homogeneous.w) > 0.000001);
|
|
32
|
-
return homogeneous.xyz / divisor;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
fn fogRayFromNdc(ndc: vec3<f32>) -> FogRay {
|
|
36
|
-
let worldPosition = fogWorldPoint(ndc);
|
|
37
|
-
let nearPosition = fogWorldPoint(vec3<f32>(ndc.xy, 0.0));
|
|
38
|
-
let farPosition = fogWorldPoint(vec3<f32>(ndc.xy, 1.0));
|
|
39
|
-
let perspective = view.temporalProjection.z < 0.5;
|
|
40
|
-
let perspectiveVector = worldPosition - view.cameraPos;
|
|
41
|
-
let orthographicVector = farPosition - nearPosition;
|
|
42
|
-
let direction = normalize(select(orthographicVector, perspectiveVector, perspective));
|
|
43
|
-
let origin = select(nearPosition, view.cameraPos, perspective);
|
|
44
|
-
let ray_distance = select(
|
|
45
|
-
max(dot(worldPosition - nearPosition, direction), 0.0),
|
|
46
|
-
length(perspectiveVector),
|
|
47
|
-
perspective,
|
|
48
|
-
);
|
|
49
|
-
return FogRay(origin, direction, ray_distance);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
29
|
@vertex
|
|
53
30
|
fn vs_main(input: VertexInput) -> VertexOutput {
|
|
54
31
|
var output: VertexOutput;
|
package/src/shaders/ribbon.wgsl
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#define_import_path forgeax::vfx-render.particles.ribbon
|
|
2
|
-
#import forgeax_view::common::{View, FogViewParams, FogRay, view}
|
|
2
|
+
#import forgeax_view::common::{View, FogViewParams, FogRay, view, fogRayFromNdc}
|
|
3
3
|
#import forgeax_view::fog::{apply_fog}
|
|
4
4
|
|
|
5
5
|
struct SegmentInput {
|
|
@@ -15,29 +15,6 @@ struct VertexOutput {
|
|
|
15
15
|
@location(1) clip_position: vec3<f32>,
|
|
16
16
|
}
|
|
17
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
18
|
@vertex
|
|
42
19
|
fn vs_main(input: SegmentInput, @builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
43
20
|
let corners = array<vec2<f32>, 6>(
|
package/src/shaders/trail.wgsl
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#define_import_path forgeax::vfx-render.particles.trail
|
|
2
|
-
#import forgeax_view::common::{View, FogViewParams, FogRay, view}
|
|
2
|
+
#import forgeax_view::common::{View, FogViewParams, FogRay, view, fogRayFromNdc}
|
|
3
3
|
#import forgeax_view::fog::{apply_fog}
|
|
4
4
|
|
|
5
5
|
struct SegmentInput {
|
|
@@ -15,29 +15,6 @@ struct VertexOutput {
|
|
|
15
15
|
@location(1) clip_position: vec3<f32>,
|
|
16
16
|
}
|
|
17
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
18
|
@vertex
|
|
42
19
|
fn vs_main(input: SegmentInput, @builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
43
20
|
let corners = array<vec2<f32>, 6>(
|