@forgeax/engine-vfx 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 +214 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/authoring-descriptor.unit.test.d.ts +2 -0
- package/dist/__tests__/authoring-descriptor.unit.test.d.ts.map +1 -0
- package/dist/__tests__/channel-input.unit.test.d.ts +2 -0
- package/dist/__tests__/channel-input.unit.test.d.ts.map +1 -0
- package/dist/__tests__/channel-inspect.unit.test.d.ts +2 -0
- package/dist/__tests__/channel-inspect.unit.test.d.ts.map +1 -0
- package/dist/__tests__/code-source-v2.unit.test.d.ts +2 -0
- package/dist/__tests__/code-source-v2.unit.test.d.ts.map +1 -0
- package/dist/__tests__/data-interface-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/data-interface-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/effect-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/effect-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/gpu-program-simulation-culling-owner.test-d.d.ts +2 -0
- package/dist/__tests__/gpu-program-simulation-culling-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/gpu-reflection-vocabulary-owner.test-d.d.ts +2 -0
- package/dist/__tests__/gpu-reflection-vocabulary-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/gpu-runtime.integration.test.d.ts +2 -0
- package/dist/__tests__/gpu-runtime.integration.test.d.ts.map +1 -0
- package/dist/__tests__/inspect-snapshot.unit.test.d.ts +2 -0
- package/dist/__tests__/inspect-snapshot.unit.test.d.ts.map +1 -0
- package/dist/__tests__/instance-public-api.unit.test.d.ts +2 -0
- package/dist/__tests__/instance-public-api.unit.test.d.ts.map +1 -0
- package/dist/__tests__/instance.unit.test.d.ts +2 -0
- package/dist/__tests__/instance.unit.test.d.ts.map +1 -0
- package/dist/__tests__/loader-v1-rejection.unit.test.d.ts +2 -0
- package/dist/__tests__/loader-v1-rejection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/player-reflection.unit.test.d.ts +2 -0
- package/dist/__tests__/player-reflection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/player-type.test-d.d.ts +2 -0
- package/dist/__tests__/player-type.test-d.d.ts.map +1 -0
- package/dist/__tests__/renderer-source.unit.test.d.ts +2 -0
- package/dist/__tests__/renderer-source.unit.test.d.ts.map +1 -0
- package/dist/__tests__/replay-canonical.unit.test.d.ts +2 -0
- package/dist/__tests__/replay-canonical.unit.test.d.ts.map +1 -0
- package/dist/__tests__/runtime-dist-isolation.test.d.ts +2 -0
- package/dist/__tests__/runtime-dist-isolation.test.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack-consumer.test-d.d.ts +2 -0
- package/dist/__tests__/scriptable-pack-consumer.test-d.d.ts.map +1 -0
- package/dist/__tests__/stage-source.unit.test.d.ts +2 -0
- package/dist/__tests__/stage-source.unit.test.d.ts.map +1 -0
- package/dist/__tests__/vfx-data-interface-vocabulary-owner.test-d.d.ts +2 -0
- package/dist/__tests__/vfx-data-interface-vocabulary-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/vfx-performance-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/vfx-performance-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/vfx-value-type-owner.test-d.d.ts +2 -0
- package/dist/__tests__/vfx-value-type-owner.test-d.d.ts.map +1 -0
- package/dist/assets/particle-effect-decoder.d.ts +3 -0
- package/dist/assets/particle-effect-decoder.d.ts.map +1 -0
- package/dist/authoring-descriptor.d.ts +60 -0
- package/dist/authoring-descriptor.d.ts.map +1 -0
- package/dist/code-source.d.ts +140 -0
- package/dist/code-source.d.ts.map +1 -0
- package/dist/data-interface.d.ts +53 -0
- package/dist/data-interface.d.ts.map +1 -0
- package/dist/effect-contract.d.ts +57 -0
- package/dist/effect-contract.d.ts.map +1 -0
- package/dist/gpu-loader.d.ts +33 -0
- package/dist/gpu-loader.d.ts.map +1 -0
- package/dist/gpu-program.d.ts +80 -0
- package/dist/gpu-program.d.ts.map +1 -0
- package/dist/gpu-runtime.d.ts +174 -0
- package/dist/gpu-runtime.d.ts.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +1986 -0
- package/dist/index.mjs.map +1 -0
- package/dist/instance.d.ts +86 -0
- package/dist/instance.d.ts.map +1 -0
- package/dist/player.d.ts +17 -0
- package/dist/player.d.ts.map +1 -0
- package/package.json +66 -0
- package/src/__tests__/authoring-descriptor.unit.test.ts +138 -0
- package/src/__tests__/channel-input.unit.test.ts +65 -0
- package/src/__tests__/channel-inspect.unit.test.ts +65 -0
- package/src/__tests__/code-source-v2.unit.test.ts +121 -0
- package/src/__tests__/data-interface-contract.unit.test.ts +92 -0
- package/src/__tests__/effect-contract.unit.test.ts +62 -0
- package/src/__tests__/gpu-program-simulation-culling-owner.test-d.ts +58 -0
- package/src/__tests__/gpu-reflection-vocabulary-owner.test-d.ts +23 -0
- package/src/__tests__/gpu-runtime.integration.test.ts +549 -0
- package/src/__tests__/inspect-snapshot.unit.test.ts +51 -0
- package/src/__tests__/instance-public-api.unit.test.ts +27 -0
- package/src/__tests__/instance.unit.test.ts +107 -0
- package/src/__tests__/loader-v1-rejection.unit.test.ts +125 -0
- package/src/__tests__/player-reflection.unit.test.ts +91 -0
- package/src/__tests__/player-type.test-d.ts +54 -0
- package/src/__tests__/renderer-source.unit.test.ts +58 -0
- package/src/__tests__/replay-canonical.unit.test.ts +68 -0
- package/src/__tests__/runtime-dist-isolation.test.ts +100 -0
- package/src/__tests__/scriptable-pack-consumer.test-d.ts +9 -0
- package/src/__tests__/stage-source.unit.test.ts +64 -0
- package/src/__tests__/vfx-data-interface-vocabulary-owner.test-d.ts +34 -0
- package/src/__tests__/vfx-performance-contract.unit.test.ts +19 -0
- package/src/__tests__/vfx-value-type-owner.test-d.ts +32 -0
- package/src/assets/particle-effect-decoder.ts +34 -0
- package/src/authoring-descriptor.ts +355 -0
- package/src/code-source.ts +827 -0
- package/src/data-interface.ts +181 -0
- package/src/effect-contract.ts +220 -0
- package/src/gpu-loader.ts +279 -0
- package/src/gpu-program.ts +101 -0
- package/src/gpu-runtime.ts +807 -0
- package/src/index.ts +110 -0
- package/src/instance.ts +379 -0
- package/src/player.ts +21 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
2
|
+
import type {
|
|
3
|
+
VfxDataInterfaceBindingType,
|
|
4
|
+
VfxDataInterfaceKind,
|
|
5
|
+
VfxDataInterfaceProvider,
|
|
6
|
+
VfxDataInterfaceToken,
|
|
7
|
+
} from '../index.js';
|
|
8
|
+
|
|
9
|
+
describe('VFX Data Interface vocabulary owner', () => {
|
|
10
|
+
it('preserves the exact kinds, binding types, and tokens', () => {
|
|
11
|
+
expectTypeOf<VfxDataInterfaceKind>().toEqualTypeOf<
|
|
12
|
+
'camera' | 'scene-depth' | 'noise' | 'channel'
|
|
13
|
+
>();
|
|
14
|
+
expectTypeOf<VfxDataInterfaceBindingType>().toEqualTypeOf<
|
|
15
|
+
'uniform' | 'sampled-depth' | 'sampled-float' | 'storage-read'
|
|
16
|
+
>();
|
|
17
|
+
expectTypeOf<VfxDataInterfaceToken>().toEqualTypeOf<
|
|
18
|
+
'vfx:camera' | 'vfx:scene-depth' | 'vfx:noise' | 'vfx:channel'
|
|
19
|
+
>();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('narrows provider binding types from the selected kind', () => {
|
|
23
|
+
expectTypeOf<VfxDataInterfaceProvider<'camera'>['bindingType']>().toEqualTypeOf<'uniform'>();
|
|
24
|
+
expectTypeOf<
|
|
25
|
+
VfxDataInterfaceProvider<'scene-depth'>['bindingType']
|
|
26
|
+
>().toEqualTypeOf<'sampled-depth'>();
|
|
27
|
+
expectTypeOf<
|
|
28
|
+
VfxDataInterfaceProvider<'noise'>['bindingType']
|
|
29
|
+
>().toEqualTypeOf<'sampled-float'>();
|
|
30
|
+
expectTypeOf<
|
|
31
|
+
VfxDataInterfaceProvider<'channel'>['bindingType']
|
|
32
|
+
>().toEqualTypeOf<'storage-read'>();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
describe('VFX performance contract', () => {
|
|
4
|
+
it('names every Batch B workload and its bounded evidence fields', () => {
|
|
5
|
+
const capacities = [10_000, 100_000, 1_000_000];
|
|
6
|
+
expect(capacities).toHaveLength(3);
|
|
7
|
+
expect({
|
|
8
|
+
backend: 'dawn-wgpu',
|
|
9
|
+
warmupFrames: 30,
|
|
10
|
+
samples: 60,
|
|
11
|
+
p95: true,
|
|
12
|
+
p99: true,
|
|
13
|
+
overflow: true,
|
|
14
|
+
allocation: true,
|
|
15
|
+
}).toEqual(
|
|
16
|
+
expect.objectContaining({ backend: 'dawn-wgpu', p95: true, p99: true, allocation: true }),
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
2
|
+
import type { VfxReflectedField, VfxValueType } from '../index.js';
|
|
3
|
+
|
|
4
|
+
describe('VfxValueType owner contract', () => {
|
|
5
|
+
it('preserves the exact public six-member vocabulary', () => {
|
|
6
|
+
expectTypeOf<VfxValueType>().toEqualTypeOf<
|
|
7
|
+
'f32' | 'i32' | 'u32' | 'vec2<f32>' | 'vec3<f32>' | 'vec4<f32>'
|
|
8
|
+
>();
|
|
9
|
+
expectTypeOf<VfxReflectedField['type']>().toEqualTypeOf<VfxValueType>();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('keeps scalar and vector members precisely narrowed', () => {
|
|
13
|
+
type ScalarTypes = Extract<VfxValueType, 'f32' | 'i32' | 'u32'>;
|
|
14
|
+
type VectorTypes = Exclude<VfxValueType, ScalarTypes>;
|
|
15
|
+
|
|
16
|
+
expectTypeOf<ScalarTypes>().toEqualTypeOf<'f32' | 'i32' | 'u32'>();
|
|
17
|
+
expectTypeOf<VectorTypes>().toEqualTypeOf<'vec2<f32>' | 'vec3<f32>' | 'vec4<f32>'>();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('rejects values outside the reflected public type declaration', () => {
|
|
21
|
+
const valid: VfxReflectedField = {
|
|
22
|
+
name: 'value',
|
|
23
|
+
type: 'vec4<f32>',
|
|
24
|
+
offset: 0,
|
|
25
|
+
size: 16,
|
|
26
|
+
alignment: 16,
|
|
27
|
+
};
|
|
28
|
+
// @ts-expect-error unknown WGSL value types are outside the public declaration.
|
|
29
|
+
const invalid: VfxReflectedField = { ...valid, type: 'mat4x4<f32>' };
|
|
30
|
+
void invalid;
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AssetDecoderContribution,
|
|
3
|
+
type AssetKind,
|
|
4
|
+
err,
|
|
5
|
+
ok,
|
|
6
|
+
type ParticleEffectAsset,
|
|
7
|
+
} from '@forgeax/engine-types';
|
|
8
|
+
|
|
9
|
+
export const particleEffectContribution: AssetDecoderContribution<
|
|
10
|
+
ParticleEffectAsset,
|
|
11
|
+
'particle-effect'
|
|
12
|
+
> = {
|
|
13
|
+
kind: { kind: 'particle-effect' } as AssetKind<ParticleEffectAsset, 'particle-effect'>,
|
|
14
|
+
consumer: 'VfxGpuRuntime',
|
|
15
|
+
decoder: {
|
|
16
|
+
async decode({ envelope }) {
|
|
17
|
+
const payload = envelope.payload;
|
|
18
|
+
if (
|
|
19
|
+
payload.kind === 'particle-effect' &&
|
|
20
|
+
payload.schemaVersion === 2 &&
|
|
21
|
+
payload.emitters.length === payload.program.emitters.length &&
|
|
22
|
+
payload.programFingerprint === payload.program.fingerprint
|
|
23
|
+
) {
|
|
24
|
+
return ok(payload);
|
|
25
|
+
}
|
|
26
|
+
return err({
|
|
27
|
+
code: 'asset-package-invalid',
|
|
28
|
+
expected: 'a schema v2 particle payload matching its cooked program',
|
|
29
|
+
hint: 'recook the particle effect atomically with its GPU program',
|
|
30
|
+
detail: { guid: envelope.guid, reason: 'particle owner validation failed' },
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import type { VfxGpuEffectAsset, VfxGpuEmitterProgram } from './gpu-program.js';
|
|
2
|
+
|
|
3
|
+
export type VfxAuthoringValue =
|
|
4
|
+
| null
|
|
5
|
+
| string
|
|
6
|
+
| number
|
|
7
|
+
| boolean
|
|
8
|
+
| readonly VfxAuthoringValue[]
|
|
9
|
+
| { readonly [key: string]: VfxAuthoringValue };
|
|
10
|
+
|
|
11
|
+
export interface VfxAuthoringFieldDescriptor {
|
|
12
|
+
readonly path: string;
|
|
13
|
+
readonly label: string;
|
|
14
|
+
readonly value: VfxAuthoringValue;
|
|
15
|
+
readonly valueType: 'text' | 'number' | 'boolean' | 'vector' | 'object';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type VfxAuthoringNodeRole =
|
|
19
|
+
| 'emitter'
|
|
20
|
+
| 'program'
|
|
21
|
+
| 'parameters'
|
|
22
|
+
| 'custom'
|
|
23
|
+
| 'stage'
|
|
24
|
+
| 'channel'
|
|
25
|
+
| 'event'
|
|
26
|
+
| 'renderer';
|
|
27
|
+
|
|
28
|
+
export interface VfxAuthoringNodeDescriptor {
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly role: VfxAuthoringNodeRole;
|
|
31
|
+
readonly label: string;
|
|
32
|
+
readonly sourcePath: string;
|
|
33
|
+
readonly fields: readonly VfxAuthoringFieldDescriptor[];
|
|
34
|
+
readonly children: readonly VfxAuthoringNodeDescriptor[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface VfxAuthoringEmitterDescriptor extends VfxAuthoringNodeDescriptor {
|
|
38
|
+
readonly role: 'emitter';
|
|
39
|
+
readonly module: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface VfxAuthoringTimelineDescriptor {
|
|
43
|
+
readonly emitterId: string;
|
|
44
|
+
readonly rate: number;
|
|
45
|
+
readonly bursts: readonly { readonly time: number; readonly count: number }[];
|
|
46
|
+
readonly loopDuration?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface VfxAuthoringDependencyDescriptor {
|
|
50
|
+
readonly kind: 'asset' | 'module' | 'data-interface';
|
|
51
|
+
readonly identity: string;
|
|
52
|
+
readonly sourcePath: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface VfxAuthoringCapabilityDescriptor {
|
|
56
|
+
readonly id: string;
|
|
57
|
+
readonly state: 'executable' | 'partial' | 'unavailable';
|
|
58
|
+
readonly reason?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface VfxAuthoringDescriptor {
|
|
62
|
+
readonly version: 1;
|
|
63
|
+
readonly assetGuid: string;
|
|
64
|
+
readonly schemaVersion: 2;
|
|
65
|
+
readonly artifactFingerprint: string;
|
|
66
|
+
readonly emitters: readonly VfxAuthoringEmitterDescriptor[];
|
|
67
|
+
readonly timeline: readonly VfxAuthoringTimelineDescriptor[];
|
|
68
|
+
readonly dependencies: readonly VfxAuthoringDependencyDescriptor[];
|
|
69
|
+
readonly capabilities: readonly VfxAuthoringCapabilityDescriptor[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const CAPABILITIES: readonly VfxAuthoringCapabilityDescriptor[] = Object.freeze([
|
|
73
|
+
Object.freeze({ id: 'wgsl-behavior', state: 'executable' }),
|
|
74
|
+
Object.freeze({ id: 'multi-emitter', state: 'executable' }),
|
|
75
|
+
Object.freeze({ id: 'deterministic-replay', state: 'executable' }),
|
|
76
|
+
Object.freeze({ id: 'emitter-visibility', state: 'executable' }),
|
|
77
|
+
Object.freeze({
|
|
78
|
+
id: 'runtime-parameters',
|
|
79
|
+
state: 'partial',
|
|
80
|
+
reason: 'reflected and packed, but not yet bound to managed author WGSL',
|
|
81
|
+
}),
|
|
82
|
+
Object.freeze({
|
|
83
|
+
id: 'custom-attributes',
|
|
84
|
+
state: 'partial',
|
|
85
|
+
reason: 'reflected without executable per-particle custom storage',
|
|
86
|
+
}),
|
|
87
|
+
Object.freeze({
|
|
88
|
+
id: 'data-interfaces',
|
|
89
|
+
state: 'partial',
|
|
90
|
+
reason: 'requirements resolve provider readiness without author resource bindings',
|
|
91
|
+
}),
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
/** Narrow an AssetRegistry payload without making editor consumers duplicate
|
|
95
|
+
* the cooked program shape. This deliberately validates the stable producer
|
|
96
|
+
* discriminants only; detailed artifact validation remains the pack loader's
|
|
97
|
+
* responsibility. */
|
|
98
|
+
export function isVfxGpuEffectAsset(value: unknown): value is VfxGpuEffectAsset {
|
|
99
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
100
|
+
const candidate = value as Partial<VfxGpuEffectAsset>;
|
|
101
|
+
return (
|
|
102
|
+
candidate.kind === 'particle-effect' &&
|
|
103
|
+
candidate.schemaVersion === 2 &&
|
|
104
|
+
typeof candidate.guid === 'string' &&
|
|
105
|
+
candidate.guid.length > 0 &&
|
|
106
|
+
typeof candidate.program === 'object' &&
|
|
107
|
+
candidate.program !== null &&
|
|
108
|
+
candidate.program.format === 'forgeax-vfx-program-2' &&
|
|
109
|
+
typeof candidate.program.fingerprint === 'string' &&
|
|
110
|
+
Array.isArray(candidate.program.emitters)
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function label(path: string): string {
|
|
115
|
+
const leaf = path.split('.').at(-1) ?? path;
|
|
116
|
+
return leaf.replaceAll(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, (value) => value.toUpperCase());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function authoringValue(value: unknown): VfxAuthoringValue {
|
|
120
|
+
if (
|
|
121
|
+
value === null ||
|
|
122
|
+
typeof value === 'string' ||
|
|
123
|
+
typeof value === 'number' ||
|
|
124
|
+
typeof value === 'boolean'
|
|
125
|
+
) {
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
if (Array.isArray(value)) return Object.freeze(value.map(authoringValue));
|
|
129
|
+
if (typeof value === 'object') {
|
|
130
|
+
const out: Record<string, VfxAuthoringValue> = {};
|
|
131
|
+
for (const [key, child] of Object.entries(value)) out[key] = authoringValue(child);
|
|
132
|
+
return Object.freeze(out);
|
|
133
|
+
}
|
|
134
|
+
return String(value);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function valueType(value: VfxAuthoringValue): VfxAuthoringFieldDescriptor['valueType'] {
|
|
138
|
+
if (typeof value === 'number') return 'number';
|
|
139
|
+
if (typeof value === 'boolean') return 'boolean';
|
|
140
|
+
if (Array.isArray(value) && value.every((entry) => typeof entry === 'number')) return 'vector';
|
|
141
|
+
if (typeof value === 'object' && value !== null) return 'object';
|
|
142
|
+
return 'text';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function field(path: string, value: unknown): VfxAuthoringFieldDescriptor {
|
|
146
|
+
const normalized = authoringValue(value);
|
|
147
|
+
return Object.freeze({
|
|
148
|
+
path,
|
|
149
|
+
label: label(path),
|
|
150
|
+
value: normalized,
|
|
151
|
+
valueType: valueType(normalized),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function fields(
|
|
156
|
+
path: string,
|
|
157
|
+
value: object,
|
|
158
|
+
omit: readonly string[] = [],
|
|
159
|
+
): readonly VfxAuthoringFieldDescriptor[] {
|
|
160
|
+
const excluded = new Set(omit);
|
|
161
|
+
return Object.freeze(
|
|
162
|
+
Object.entries(value)
|
|
163
|
+
.filter(([key, child]) => !excluded.has(key) && child !== undefined)
|
|
164
|
+
.map(([key, child]) => field(`${path}.${key}`, child)),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function node(
|
|
169
|
+
input: Omit<VfxAuthoringNodeDescriptor, 'children'> & {
|
|
170
|
+
readonly children?: readonly VfxAuthoringNodeDescriptor[];
|
|
171
|
+
},
|
|
172
|
+
): VfxAuthoringNodeDescriptor {
|
|
173
|
+
return Object.freeze({ ...input, children: Object.freeze([...(input.children ?? [])]) });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function reflectionNodes(
|
|
177
|
+
emitter: VfxGpuEmitterProgram,
|
|
178
|
+
path: string,
|
|
179
|
+
): VfxAuthoringNodeDescriptor[] {
|
|
180
|
+
const nodes: VfxAuthoringNodeDescriptor[] = [];
|
|
181
|
+
const layout = emitter.reflection.layout;
|
|
182
|
+
if (layout !== undefined && layout.parameters.fields.length > 0) {
|
|
183
|
+
nodes.push(
|
|
184
|
+
node({
|
|
185
|
+
id: `parameters:${emitter.id}`,
|
|
186
|
+
role: 'parameters',
|
|
187
|
+
label: layout.parameters.name,
|
|
188
|
+
sourcePath: `${path}.program`,
|
|
189
|
+
fields: Object.freeze(
|
|
190
|
+
layout.parameters.fields.map((entry) =>
|
|
191
|
+
field(`parameters.${entry.name}`, {
|
|
192
|
+
type: entry.type,
|
|
193
|
+
offset: entry.offset,
|
|
194
|
+
size: entry.size,
|
|
195
|
+
alignment: entry.alignment,
|
|
196
|
+
defaultValue: entry.defaultValue ?? null,
|
|
197
|
+
}),
|
|
198
|
+
),
|
|
199
|
+
),
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (layout !== undefined && layout.custom.fields.length > 0) {
|
|
204
|
+
nodes.push(
|
|
205
|
+
node({
|
|
206
|
+
id: `custom:${emitter.id}`,
|
|
207
|
+
role: 'custom',
|
|
208
|
+
label: layout.custom.name,
|
|
209
|
+
sourcePath: `${path}.program`,
|
|
210
|
+
fields: Object.freeze(
|
|
211
|
+
layout.custom.fields.map((entry) =>
|
|
212
|
+
field(`custom.${entry.name}`, {
|
|
213
|
+
type: entry.type,
|
|
214
|
+
offset: entry.offset,
|
|
215
|
+
size: entry.size,
|
|
216
|
+
alignment: entry.alignment,
|
|
217
|
+
defaultValue: entry.defaultValue ?? null,
|
|
218
|
+
}),
|
|
219
|
+
),
|
|
220
|
+
),
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
for (const stage of emitter.reflection.stages ?? []) {
|
|
225
|
+
nodes.push(
|
|
226
|
+
node({
|
|
227
|
+
id: `stage:${emitter.id}:${stage.id}`,
|
|
228
|
+
role: 'stage',
|
|
229
|
+
label: stage.id,
|
|
230
|
+
sourcePath: `${path}.program`,
|
|
231
|
+
fields: fields(`stages.${stage.id}`, stage, ['id', 'entryPoint']),
|
|
232
|
+
}),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return nodes;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function emitterNode(emitter: VfxGpuEmitterProgram, index: number): VfxAuthoringEmitterDescriptor {
|
|
239
|
+
const path = `emitters[${index}]`;
|
|
240
|
+
const children: VfxAuthoringNodeDescriptor[] = [
|
|
241
|
+
node({
|
|
242
|
+
id: `program:${emitter.id}`,
|
|
243
|
+
role: 'program',
|
|
244
|
+
label: emitter.module,
|
|
245
|
+
sourcePath: `${path}.program.module`,
|
|
246
|
+
fields: Object.freeze([
|
|
247
|
+
field(`${path}.program.module`, emitter.module),
|
|
248
|
+
field(`${path}.program.imports`, emitter.reflection.imports),
|
|
249
|
+
]),
|
|
250
|
+
}),
|
|
251
|
+
...reflectionNodes(emitter, path),
|
|
252
|
+
...(emitter.channels ?? []).map((channel, channelIndex) =>
|
|
253
|
+
node({
|
|
254
|
+
id: `channel:${emitter.id}:${channel.id}`,
|
|
255
|
+
role: 'channel',
|
|
256
|
+
label: channel.id,
|
|
257
|
+
sourcePath: `${path}.channels[${channelIndex}]`,
|
|
258
|
+
fields: fields(`${path}.channels[${channelIndex}]`, channel, ['id']),
|
|
259
|
+
}),
|
|
260
|
+
),
|
|
261
|
+
...(emitter.events ?? []).map((event, eventIndex) =>
|
|
262
|
+
node({
|
|
263
|
+
id: `event:${emitter.id}:${event.id}`,
|
|
264
|
+
role: 'event',
|
|
265
|
+
label: event.id,
|
|
266
|
+
sourcePath: `${path}.events[${eventIndex}]`,
|
|
267
|
+
fields: fields(`${path}.events[${eventIndex}]`, event, ['id']),
|
|
268
|
+
}),
|
|
269
|
+
),
|
|
270
|
+
...emitter.renderers.map((renderer, rendererIndex) =>
|
|
271
|
+
node({
|
|
272
|
+
id: `renderer:${emitter.id}:${rendererIndex}`,
|
|
273
|
+
role: 'renderer',
|
|
274
|
+
label: `${renderer.kind} renderer`,
|
|
275
|
+
sourcePath: `${path}.renderers[${rendererIndex}]`,
|
|
276
|
+
fields: fields(`${path}.renderers[${rendererIndex}]`, renderer, ['kind']),
|
|
277
|
+
}),
|
|
278
|
+
),
|
|
279
|
+
];
|
|
280
|
+
return Object.freeze({
|
|
281
|
+
id: `emitter:${emitter.id}`,
|
|
282
|
+
role: 'emitter',
|
|
283
|
+
label: emitter.id,
|
|
284
|
+
module: emitter.module,
|
|
285
|
+
sourcePath: path,
|
|
286
|
+
fields: Object.freeze([
|
|
287
|
+
field(`${path}.capacity`, emitter.capacity),
|
|
288
|
+
field(`${path}.space`, emitter.space),
|
|
289
|
+
field(`${path}.bounds`, emitter.bounds),
|
|
290
|
+
field(`${path}.simulationWhenCulled`, emitter.simulationWhenCulled),
|
|
291
|
+
]),
|
|
292
|
+
children: Object.freeze(children),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function dependencies(effect: VfxGpuEffectAsset): readonly VfxAuthoringDependencyDescriptor[] {
|
|
297
|
+
const entries: VfxAuthoringDependencyDescriptor[] = [];
|
|
298
|
+
const seen = new Set<string>();
|
|
299
|
+
const add = (entry: VfxAuthoringDependencyDescriptor): void => {
|
|
300
|
+
const key = `${entry.kind}:${entry.identity}`;
|
|
301
|
+
if (seen.has(key)) return;
|
|
302
|
+
seen.add(key);
|
|
303
|
+
entries.push(Object.freeze(entry));
|
|
304
|
+
};
|
|
305
|
+
for (const [emitterIndex, emitter] of effect.program.emitters.entries()) {
|
|
306
|
+
const path = `emitters[${emitterIndex}]`;
|
|
307
|
+
add({ kind: 'module', identity: emitter.module, sourcePath: `${path}.program.module` });
|
|
308
|
+
for (const identity of emitter.reflection.imports) {
|
|
309
|
+
add({ kind: 'module', identity, sourcePath: `${path}.program.imports` });
|
|
310
|
+
}
|
|
311
|
+
for (const [rendererIndex, renderer] of emitter.renderers.entries()) {
|
|
312
|
+
add({
|
|
313
|
+
kind: 'asset',
|
|
314
|
+
identity: renderer.material,
|
|
315
|
+
sourcePath: `${path}.renderers[${rendererIndex}].material`,
|
|
316
|
+
});
|
|
317
|
+
if (renderer.kind === 'mesh') {
|
|
318
|
+
add({
|
|
319
|
+
kind: 'asset',
|
|
320
|
+
identity: renderer.mesh,
|
|
321
|
+
sourcePath: `${path}.renderers[${rendererIndex}].mesh`,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
for (const requirement of emitter.reflection.dataInterfaces ?? []) {
|
|
326
|
+
add({ kind: 'data-interface', identity: requirement.token, sourcePath: `${path}.program` });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return Object.freeze(entries);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Project the cooked runtime asset into a compiler-free, UI-neutral authoring read model. */
|
|
333
|
+
export function describeVfxGpuEffect(effect: VfxGpuEffectAsset): VfxAuthoringDescriptor {
|
|
334
|
+
return Object.freeze({
|
|
335
|
+
version: 1,
|
|
336
|
+
assetGuid: effect.guid,
|
|
337
|
+
schemaVersion: 2,
|
|
338
|
+
artifactFingerprint: effect.program.fingerprint,
|
|
339
|
+
emitters: Object.freeze(effect.program.emitters.map(emitterNode)),
|
|
340
|
+
timeline: Object.freeze(
|
|
341
|
+
effect.program.emitters.map((emitter) =>
|
|
342
|
+
Object.freeze({
|
|
343
|
+
emitterId: emitter.id,
|
|
344
|
+
rate: emitter.schedule.rate,
|
|
345
|
+
bursts: Object.freeze([...(emitter.schedule.bursts ?? [])]),
|
|
346
|
+
...(emitter.schedule.loopDuration === undefined
|
|
347
|
+
? {}
|
|
348
|
+
: { loopDuration: emitter.schedule.loopDuration }),
|
|
349
|
+
}),
|
|
350
|
+
),
|
|
351
|
+
),
|
|
352
|
+
dependencies: dependencies(effect),
|
|
353
|
+
capabilities: CAPABILITIES,
|
|
354
|
+
});
|
|
355
|
+
}
|