@forgeax/engine-vfx-render 0.0.0-dev.8d955ade1c79
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 +157 -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 +90 -0
- package/dist/feature/gpu-particle-feature.d.ts.map +1 -0
- package/dist/feature/particle-resources.d.ts +63 -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 +103 -0
- package/dist/host/vfx-runtime-host.d.ts.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +1308 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +63 -0
- package/src/__tests__/billboard-advanced.integration.test.ts +46 -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 +60 -0
- package/src/__tests__/gpu-host.integration.test.ts +459 -0
- package/src/__tests__/inspect-snapshot.integration.test.ts +26 -0
- package/src/__tests__/particle-resources.unit.test.ts +66 -0
- package/src/__tests__/render-vocabulary-owner.test-d.ts +81 -0
- package/src/__tests__/stage-plan.integration.test.ts +148 -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 +819 -0
- package/src/feature/particle-resources.ts +195 -0
- package/src/feature/stage-plan.ts +285 -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 +459 -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,819 @@
|
|
|
1
|
+
import type { EntityHandle, World } from '@forgeax/engine-ecs';
|
|
2
|
+
import { frustum } from '@forgeax/engine-math';
|
|
3
|
+
import type { RenderFeature, RenderFeaturePlan } from '@forgeax/engine-render';
|
|
4
|
+
import { Transform } from '@forgeax/engine-scene';
|
|
5
|
+
import { err, type MaterialAsset, type MeshAsset, ok } from '@forgeax/engine-types';
|
|
6
|
+
import type { ParticleRendererSource } from '@forgeax/engine-vfx';
|
|
7
|
+
import {
|
|
8
|
+
VFX_GPU_RUNTIME_RESOURCE_KEY,
|
|
9
|
+
type VfxGpuRuntime,
|
|
10
|
+
type VfxGpuTickIntent,
|
|
11
|
+
} from '@forgeax/engine-vfx';
|
|
12
|
+
import { RenderFeatureStageFailedError } from '../../../render/src/errors/render';
|
|
13
|
+
import { RENDER_FEATURE_VERTEX_LAYOUTS } from '../../../render/src/features/prepared-graphics';
|
|
14
|
+
import type { VfxDataInterfaceRegistry } from '../host/data-interface-providers.js';
|
|
15
|
+
import type { ParticleRenderCamera } from './camera.js';
|
|
16
|
+
import {
|
|
17
|
+
encodeEventInputs,
|
|
18
|
+
eventCapacity,
|
|
19
|
+
eventInputCapacity,
|
|
20
|
+
VFX_EVENT_BYTES,
|
|
21
|
+
VFX_EVENT_INPUT_BYTES,
|
|
22
|
+
} from './event-resources.js';
|
|
23
|
+
import {
|
|
24
|
+
canonicalMeshVertices,
|
|
25
|
+
createTopologyResourcePlan,
|
|
26
|
+
PARTICLE_SHADER_IDENTIFIERS,
|
|
27
|
+
particleMaterialPass,
|
|
28
|
+
particleMaterialUsesBindings,
|
|
29
|
+
} from './particle-resources.js';
|
|
30
|
+
import type { VfxStagePlanObservation, VfxValidatedStagePlan } from './stage-plan.js';
|
|
31
|
+
import { validatedStagePlan } from './stage-plan.js';
|
|
32
|
+
|
|
33
|
+
const IDENTITY = 'forgeax.vfx-render.gpu-particles';
|
|
34
|
+
const WORKGROUP_SIZE = 256;
|
|
35
|
+
const PARTICLE_BYTES = 80;
|
|
36
|
+
const BILLBOARD_INSTANCE_BYTES = 31 * 4;
|
|
37
|
+
const MESH_INSTANCE_BYTES = 28 * 4;
|
|
38
|
+
const COUNTERS_BYTES = 24;
|
|
39
|
+
const RUNTIME_BYTES = 72 * 4;
|
|
40
|
+
const IDENTITY_MATRIX = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
|
|
41
|
+
|
|
42
|
+
type ParticleRendererKind = ParticleRendererSource['kind'];
|
|
43
|
+
type ParticleTopologyRenderer = Extract<ParticleRendererSource, { readonly capacity: number }>;
|
|
44
|
+
type ParticleTopologyKind = ParticleTopologyRenderer['kind'];
|
|
45
|
+
type VfxStageOutput = VfxStagePlanObservation['stageOutput'];
|
|
46
|
+
|
|
47
|
+
interface VfxRenderStageState {
|
|
48
|
+
readonly stageOutput: VfxStageOutput;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface VfxRenderInspectSnapshot extends VfxRenderStageState {
|
|
52
|
+
readonly topology: ParticleRendererKind;
|
|
53
|
+
readonly counters: {
|
|
54
|
+
readonly capacity: number;
|
|
55
|
+
readonly produced: number;
|
|
56
|
+
readonly dropped: number;
|
|
57
|
+
};
|
|
58
|
+
readonly stageReadiness: readonly unknown[];
|
|
59
|
+
readonly providerReadiness: unknown;
|
|
60
|
+
readonly gpuTiming: unknown;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface VfxRenderInspectInput {
|
|
64
|
+
readonly topology: ParticleRendererKind;
|
|
65
|
+
readonly capacity: number;
|
|
66
|
+
readonly produced: number;
|
|
67
|
+
readonly dropped: number;
|
|
68
|
+
readonly stageReadiness: readonly unknown[];
|
|
69
|
+
readonly stageOutput?: VfxStageOutput;
|
|
70
|
+
readonly providerReadiness: unknown;
|
|
71
|
+
readonly gpuTiming: unknown;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createVfxRenderInspectSnapshot(
|
|
75
|
+
input: VfxRenderInspectInput,
|
|
76
|
+
): VfxRenderInspectSnapshot {
|
|
77
|
+
return {
|
|
78
|
+
topology: input.topology,
|
|
79
|
+
counters: { capacity: input.capacity, produced: input.produced, dropped: input.dropped },
|
|
80
|
+
stageReadiness: input.stageReadiness,
|
|
81
|
+
stageOutput: input.stageOutput ?? 'empty',
|
|
82
|
+
providerReadiness: input.providerReadiness,
|
|
83
|
+
gpuTiming: input.gpuTiming,
|
|
84
|
+
} as const;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface BillboardAdvancedSample {
|
|
88
|
+
readonly age: number;
|
|
89
|
+
readonly lifetime: number;
|
|
90
|
+
readonly particleDepth: number;
|
|
91
|
+
readonly sceneDepth: number;
|
|
92
|
+
readonly depthAvailable: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function resolveBillboardAdvancedState(
|
|
96
|
+
renderer: Extract<ParticleRendererSource, { readonly kind: 'billboard' }>,
|
|
97
|
+
sample: BillboardAdvancedSample,
|
|
98
|
+
) {
|
|
99
|
+
if (renderer.softParticle !== undefined && !sample.depthAvailable) {
|
|
100
|
+
return err({
|
|
101
|
+
code: 'vfx-renderer-depth-missing' as const,
|
|
102
|
+
expected: 'a scene-depth provider for soft particles',
|
|
103
|
+
hint: 'attach the scene-depth data interface or disable soft particles',
|
|
104
|
+
detail: { path: 'renderer.softParticle' },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const sheet = renderer.textureSheet;
|
|
108
|
+
const frameCount = sheet?.frameCount ?? (sheet === undefined ? 1 : sheet.columns * sheet.rows);
|
|
109
|
+
const frameIndex =
|
|
110
|
+
sheet === undefined || sheet.frameRate === 0
|
|
111
|
+
? 0
|
|
112
|
+
: Math.min(
|
|
113
|
+
frameCount - 1,
|
|
114
|
+
Math.max(0, Math.floor(Math.max(0, sample.age) * sheet.frameRate)),
|
|
115
|
+
);
|
|
116
|
+
const softParticleFade =
|
|
117
|
+
renderer.softParticle === undefined
|
|
118
|
+
? 1
|
|
119
|
+
: Math.max(
|
|
120
|
+
0,
|
|
121
|
+
Math.min(
|
|
122
|
+
1,
|
|
123
|
+
(sample.sceneDepth - sample.particleDepth) / renderer.softParticle.fadeDistance,
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
return ok({
|
|
127
|
+
frameIndex,
|
|
128
|
+
pivot: renderer.pivot ?? ([0, 0] as const),
|
|
129
|
+
softParticleFade,
|
|
130
|
+
sortingKey: renderer.sorting === 'back-to-front' ? sample.particleDepth : 0,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function topologyRecoveryHint(
|
|
135
|
+
topology: ParticleTopologyKind,
|
|
136
|
+
reason: 'capacity' | 'broken' | 'degenerate' | 'device',
|
|
137
|
+
): string {
|
|
138
|
+
if (reason === 'capacity')
|
|
139
|
+
return `${topology} capacity overflow was bounded; increase its explicit capacity`;
|
|
140
|
+
if (reason === 'broken')
|
|
141
|
+
return `${topology} continuity broke; inspect its explicit source key and keep the last valid segment`;
|
|
142
|
+
if (reason === 'degenerate')
|
|
143
|
+
return `${topology} produced no drawable segment; preserve zero output and inspect source endpoints`;
|
|
144
|
+
return `${topology} device resources recovered from the last known good generation`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface GpuParticleFeatureOptions {
|
|
148
|
+
readonly camera: { read(world: World): ParticleRenderCamera | undefined };
|
|
149
|
+
readonly dataInterfaces?: Pick<VfxDataInterfaceRegistry, 'resolve'>;
|
|
150
|
+
readonly material?: { read(world: World, guid: string): MaterialAsset | undefined };
|
|
151
|
+
readonly mesh?: { read(world: World, guid: string): MeshAsset | undefined };
|
|
152
|
+
readonly playerConsumption?: {
|
|
153
|
+
readonly isEnabled: (world: World, player: EntityHandle) => boolean;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
interface ExtractedWorld {
|
|
158
|
+
readonly world: World;
|
|
159
|
+
readonly runtime: VfxGpuRuntime;
|
|
160
|
+
readonly camera: ParticleRenderCamera;
|
|
161
|
+
readonly intents: readonly VfxGpuTickIntent[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface ExtractedFrame {
|
|
165
|
+
readonly worlds: readonly ExtractedWorld[];
|
|
166
|
+
readonly frameNumber: number;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function finite(value: unknown, fallback: number): number {
|
|
170
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function vector(value: unknown, fallback: readonly number[], size: number): readonly number[] {
|
|
174
|
+
return Array.isArray(value)
|
|
175
|
+
? Array.from({ length: size }, (_, index) => finite(value[index], fallback[index] ?? 0))
|
|
176
|
+
: fallback;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function runtimeData(
|
|
180
|
+
intent: VfxGpuTickIntent,
|
|
181
|
+
camera: ParticleRenderCamera,
|
|
182
|
+
material: MaterialAsset | undefined,
|
|
183
|
+
localToWorld: Float32Array,
|
|
184
|
+
renderer?: ParticleRendererSource,
|
|
185
|
+
rendererIndex = 0,
|
|
186
|
+
): Uint8Array {
|
|
187
|
+
const storage = new ArrayBuffer(RUNTIME_BYTES);
|
|
188
|
+
const floats = new Float32Array(storage);
|
|
189
|
+
const words = new Uint32Array(storage);
|
|
190
|
+
floats[0] = intent.fixedDelta;
|
|
191
|
+
words[1] = intent.phaseTick;
|
|
192
|
+
words[2] = intent.seed;
|
|
193
|
+
words[3] = intent.playCycle;
|
|
194
|
+
words[4] = intent.emitter.capacity;
|
|
195
|
+
words[5] = intent.spawnCount;
|
|
196
|
+
words[6] = intent.firstParticleId;
|
|
197
|
+
words[7] = intent.emitter.renderers.length;
|
|
198
|
+
floats.set(camera.viewProjection, 8);
|
|
199
|
+
floats.set(camera.right, 24);
|
|
200
|
+
floats.set(camera.up, 28);
|
|
201
|
+
const values = material?.values ?? {};
|
|
202
|
+
floats.set(vector(values.baseColor, [1, 1, 1, 1], 4), 32);
|
|
203
|
+
const emissive = vector(values.emissive, [0, 0, 0], 3);
|
|
204
|
+
floats.set(emissive, 36);
|
|
205
|
+
floats[39] = finite(values.emissiveIntensity, 0);
|
|
206
|
+
floats[40] = finite(values.metallic, 0);
|
|
207
|
+
floats[41] = finite(values.roughness, 0.5);
|
|
208
|
+
floats[42] = finite(values.clearcoat, 0);
|
|
209
|
+
floats[43] = finite(values.clearcoatRoughness, 0.5);
|
|
210
|
+
floats.set(localToWorld, 44);
|
|
211
|
+
words[60] = rendererIndex;
|
|
212
|
+
words[61] = renderer?.kind === 'trail' ? renderer.historyLength : 0;
|
|
213
|
+
words[62] =
|
|
214
|
+
renderer?.kind === 'ribbon' || renderer?.kind === 'trail' || renderer?.kind === 'beam'
|
|
215
|
+
? renderer.capacity
|
|
216
|
+
: intent.emitter.capacity;
|
|
217
|
+
words[63] =
|
|
218
|
+
renderer?.kind === 'billboard' && renderer.sorting === 'back-to-front'
|
|
219
|
+
? 2
|
|
220
|
+
: renderer?.kind === 'billboard' && renderer.sorting === 'emitter'
|
|
221
|
+
? 1
|
|
222
|
+
: 0;
|
|
223
|
+
floats[64] =
|
|
224
|
+
renderer?.kind === 'billboard'
|
|
225
|
+
? (renderer.pivot?.[0] ?? 0)
|
|
226
|
+
: renderer?.kind === 'ribbon' || renderer?.kind === 'trail' || renderer?.kind === 'beam'
|
|
227
|
+
? (renderer.width ?? 0.1)
|
|
228
|
+
: 0.1;
|
|
229
|
+
floats[65] = renderer?.kind === 'billboard' ? (renderer.pivot?.[1] ?? 0) : 0;
|
|
230
|
+
floats[66] = renderer?.kind === 'billboard' ? (renderer.softParticle?.fadeDistance ?? 0) : 0;
|
|
231
|
+
const sheet = renderer?.kind === 'billboard' ? renderer.textureSheet : undefined;
|
|
232
|
+
floats[68] = sheet?.columns ?? 1;
|
|
233
|
+
floats[69] = sheet?.rows ?? 1;
|
|
234
|
+
floats[70] = sheet?.frameRate ?? 0;
|
|
235
|
+
floats[71] = sheet?.frameCount ?? (sheet === undefined ? 1 : sheet.columns * sheet.rows);
|
|
236
|
+
return new Uint8Array(storage);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function emitterTransform(world: World, intent: VfxGpuTickIntent): Float32Array {
|
|
240
|
+
if (intent.emitter.space === 'world') return IDENTITY_MATRIX;
|
|
241
|
+
const transform = world.get(intent.player, Transform);
|
|
242
|
+
return transform.ok ? transform.value.world : IDENTITY_MATRIX;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function emitterVisible(
|
|
246
|
+
intent: VfxGpuTickIntent,
|
|
247
|
+
camera: ParticleRenderCamera,
|
|
248
|
+
localToWorld: Float32Array,
|
|
249
|
+
): boolean {
|
|
250
|
+
const bounds = intent.emitter.bounds;
|
|
251
|
+
const center =
|
|
252
|
+
bounds.kind === 'sphere'
|
|
253
|
+
? bounds.center
|
|
254
|
+
: ([
|
|
255
|
+
(bounds.min[0] + bounds.max[0]) * 0.5,
|
|
256
|
+
(bounds.min[1] + bounds.max[1]) * 0.5,
|
|
257
|
+
(bounds.min[2] + bounds.max[2]) * 0.5,
|
|
258
|
+
] as const);
|
|
259
|
+
const radius =
|
|
260
|
+
bounds.kind === 'sphere'
|
|
261
|
+
? bounds.radius
|
|
262
|
+
: Math.hypot(
|
|
263
|
+
(bounds.max[0] - bounds.min[0]) * 0.5,
|
|
264
|
+
(bounds.max[1] - bounds.min[1]) * 0.5,
|
|
265
|
+
(bounds.max[2] - bounds.min[2]) * 0.5,
|
|
266
|
+
);
|
|
267
|
+
const matrix = (index: number): number => localToWorld[index] ?? 0;
|
|
268
|
+
const worldCenter = new Float32Array([
|
|
269
|
+
matrix(0) * center[0] + matrix(4) * center[1] + matrix(8) * center[2] + matrix(12),
|
|
270
|
+
matrix(1) * center[0] + matrix(5) * center[1] + matrix(9) * center[2] + matrix(13),
|
|
271
|
+
matrix(2) * center[0] + matrix(6) * center[1] + matrix(10) * center[2] + matrix(14),
|
|
272
|
+
]);
|
|
273
|
+
const scale = Math.max(
|
|
274
|
+
Math.hypot(matrix(0), matrix(1), matrix(2)),
|
|
275
|
+
Math.hypot(matrix(4), matrix(5), matrix(6)),
|
|
276
|
+
Math.hypot(matrix(8), matrix(9), matrix(10)),
|
|
277
|
+
);
|
|
278
|
+
const planes = frustum.fromViewProjection(frustum.create(), camera.viewProjection);
|
|
279
|
+
return frustum.intersectsSphere(planes, worldCenter, radius * scale);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function resetData(size: number): Uint8Array {
|
|
283
|
+
return new Uint8Array(size);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function requiresSceneDepth(intent: VfxGpuTickIntent): boolean {
|
|
287
|
+
return (intent.emitter.reflection.dataInterfaces ?? []).some(
|
|
288
|
+
(requirement) => requirement.kind === 'scene-depth',
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function planFailure(): RenderFeatureStageFailedError {
|
|
293
|
+
return new RenderFeatureStageFailedError(IDENTITY, -1, 'plan', 'next-frame');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
type PlanResource = RenderFeaturePlan['resources'][number];
|
|
297
|
+
type PlanPass = RenderFeaturePlan['passes'][number];
|
|
298
|
+
|
|
299
|
+
function planName(value: string, maxLength = 24): string {
|
|
300
|
+
const normalized = value.toLowerCase().replaceAll(/[^a-z0-9.-]/g, '-');
|
|
301
|
+
return (normalized.length === 0 ? 'unnamed' : normalized).slice(0, maxLength);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function computeBindingEntries(
|
|
305
|
+
intent: VfxGpuTickIntent,
|
|
306
|
+
resources: Readonly<Record<number, string>>,
|
|
307
|
+
): readonly { readonly binding: number; readonly resource: string }[] {
|
|
308
|
+
const declared = new Set(
|
|
309
|
+
(intent.emitter.reflection.bindings[0]?.entries ?? [])
|
|
310
|
+
.filter((entry) => entry.buffer !== undefined)
|
|
311
|
+
.map((entry) => entry.binding),
|
|
312
|
+
);
|
|
313
|
+
return Object.entries(resources).flatMap(([binding, resource]) =>
|
|
314
|
+
declared.has(Number(binding)) ? [{ binding: Number(binding), resource }] : [],
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function simulationDispatches(
|
|
319
|
+
intent: VfxGpuTickIntent,
|
|
320
|
+
stages: VfxValidatedStagePlan,
|
|
321
|
+
): Extract<PlanPass, { readonly kind: 'compute' }>['dispatches'] {
|
|
322
|
+
const groups = Math.max(1, Math.ceil(intent.emitter.capacity / WORKGROUP_SIZE));
|
|
323
|
+
return [
|
|
324
|
+
{ kind: 'direct', entryPoint: 'forgeax_vfx_spawn_main', workgroups: [groups] },
|
|
325
|
+
{ kind: 'direct', entryPoint: 'forgeax_vfx_update_main', workgroups: [groups] },
|
|
326
|
+
...stages.stages.map((stage) => ({
|
|
327
|
+
kind: 'direct' as const,
|
|
328
|
+
entryPoint: stage.entryPoint,
|
|
329
|
+
workgroups: [groups] as const,
|
|
330
|
+
})),
|
|
331
|
+
{ kind: 'direct', entryPoint: 'forgeax_vfx_scan_blocks_main', workgroups: [groups] },
|
|
332
|
+
{ kind: 'direct', entryPoint: 'forgeax_vfx_scan_block_offsets_main', workgroups: [1] },
|
|
333
|
+
{ kind: 'direct', entryPoint: 'forgeax_vfx_add_offsets_main', workgroups: [groups] },
|
|
334
|
+
{ kind: 'direct', entryPoint: 'forgeax_vfx_compact_main', workgroups: [groups] },
|
|
335
|
+
{
|
|
336
|
+
kind: 'direct',
|
|
337
|
+
entryPoint: 'forgeax_vfx_event_main',
|
|
338
|
+
workgroups: [Math.max(1, Math.ceil(eventInputCapacity(intent.emitter) / 64))],
|
|
339
|
+
},
|
|
340
|
+
];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function gpuParticleRenderFeature(
|
|
344
|
+
options: GpuParticleFeatureOptions,
|
|
345
|
+
): RenderFeature<ExtractedFrame> {
|
|
346
|
+
return {
|
|
347
|
+
identity: IDENTITY,
|
|
348
|
+
requiredCapabilities: ['compute', 'indirectDrawing'],
|
|
349
|
+
requiredMaterialShaders: Object.values(PARTICLE_SHADER_IDENTIFIERS),
|
|
350
|
+
extract: (context) => {
|
|
351
|
+
const extracted: ExtractedWorld[] = [];
|
|
352
|
+
for (const world of context.worlds) {
|
|
353
|
+
if (!world.hasResource(VFX_GPU_RUNTIME_RESOURCE_KEY)) continue;
|
|
354
|
+
const camera = options.camera.read(world);
|
|
355
|
+
if (camera === undefined) continue;
|
|
356
|
+
const runtime = world.getResource<VfxGpuRuntime>(VFX_GPU_RUNTIME_RESOURCE_KEY);
|
|
357
|
+
const intents = runtime.snapshot().filter((intent) => {
|
|
358
|
+
if (options.playerConsumption?.isEnabled(world, intent.player) === false) return false;
|
|
359
|
+
const requirements = intent.emitter.reflection.dataInterfaces ?? [];
|
|
360
|
+
return (
|
|
361
|
+
requirements.length === 0 ||
|
|
362
|
+
options.dataInterfaces?.resolve(requirements, intent.instanceGeneration).ok === true
|
|
363
|
+
);
|
|
364
|
+
});
|
|
365
|
+
extracted.push({ world, runtime, camera, intents });
|
|
366
|
+
}
|
|
367
|
+
return ok({ worlds: extracted, frameNumber: context.frameNumber });
|
|
368
|
+
},
|
|
369
|
+
plan: (frame, context) => {
|
|
370
|
+
const resources: PlanResource[] = [];
|
|
371
|
+
const passes: PlanPass[] = [];
|
|
372
|
+
const dispatchedIntents = new Set<VfxGpuTickIntent>();
|
|
373
|
+
const colorTarget =
|
|
374
|
+
context.targets.find((candidate) => candidate.kind === 'color') ??
|
|
375
|
+
context.targets.find((candidate) => candidate.kind === 'swapchain');
|
|
376
|
+
const depthTarget = context.targets.find((candidate) => candidate.kind === 'depth');
|
|
377
|
+
|
|
378
|
+
for (const [worldIndex, entry] of frame.worlds.entries()) {
|
|
379
|
+
for (const [intentIndex, intent] of entry.intents.entries()) {
|
|
380
|
+
if (!entry.runtime.isEmitterSessionEnabled(intent.player, intent.emitter.id)) continue;
|
|
381
|
+
const localToWorld = emitterTransform(entry.world, intent);
|
|
382
|
+
const visible = emitterVisible(intent, entry.camera, localToWorld);
|
|
383
|
+
entry.runtime.setEmitterCameraVisibility(intent.player, intent.emitter.id, visible);
|
|
384
|
+
if (!visible) continue;
|
|
385
|
+
if (requiresSceneDepth(intent) && depthTarget === undefined) continue;
|
|
386
|
+
|
|
387
|
+
const stagePlan = validatedStagePlan(
|
|
388
|
+
intent.emitter.reflection.stages,
|
|
389
|
+
intent.instanceGeneration,
|
|
390
|
+
);
|
|
391
|
+
if (!stagePlan.ok) return err(planFailure());
|
|
392
|
+
|
|
393
|
+
const prefix = `vfx.w-${worldIndex}.i-${intentIndex}.${planName(intent.emitter.id)}`;
|
|
394
|
+
const program = `${prefix}.compute-program`;
|
|
395
|
+
const particles = `${prefix}.particles`;
|
|
396
|
+
const runtime = `${prefix}.runtime`;
|
|
397
|
+
const aliveIndices = `${prefix}.alive-indices`;
|
|
398
|
+
const counters = `${prefix}.counters`;
|
|
399
|
+
const indirect = `${prefix}.indirect`;
|
|
400
|
+
const scratch = `${prefix}.scratch`;
|
|
401
|
+
const sharedInstances = `${prefix}.shared-instances`;
|
|
402
|
+
const eventInputs = `${prefix}.event-inputs`;
|
|
403
|
+
const events = `${prefix}.events`;
|
|
404
|
+
const bindings = `${prefix}.simulation-bindings`;
|
|
405
|
+
const capacity = intent.emitter.capacity;
|
|
406
|
+
const renderers = intent.emitter.renderers;
|
|
407
|
+
const meshes = renderers.map((renderer) =>
|
|
408
|
+
renderer.kind === 'mesh' ? options.mesh?.read(entry.world, renderer.mesh) : undefined,
|
|
409
|
+
);
|
|
410
|
+
const indirectWords = new Uint32Array(Math.max(1, renderers.length) * 5);
|
|
411
|
+
|
|
412
|
+
for (const [rendererIndex, renderer] of renderers.entries()) {
|
|
413
|
+
const mesh = meshes[rendererIndex];
|
|
414
|
+
const submesh =
|
|
415
|
+
renderer.kind === 'mesh' ? mesh?.submeshes[renderer.submesh ?? 0] : undefined;
|
|
416
|
+
if (renderer.kind === 'mesh' && submesh === undefined) return err(planFailure());
|
|
417
|
+
if (
|
|
418
|
+
(renderer.kind === 'ribbon' ||
|
|
419
|
+
renderer.kind === 'trail' ||
|
|
420
|
+
renderer.kind === 'beam') &&
|
|
421
|
+
!createTopologyResourcePlan(renderer).ok
|
|
422
|
+
) {
|
|
423
|
+
return err(planFailure());
|
|
424
|
+
}
|
|
425
|
+
indirectWords[rendererIndex * 5] =
|
|
426
|
+
renderer.kind === 'mesh'
|
|
427
|
+
? mesh?.indices === undefined
|
|
428
|
+
? (submesh?.vertexCount ?? 0)
|
|
429
|
+
: (submesh?.indexCount ?? 0)
|
|
430
|
+
: 6;
|
|
431
|
+
indirectWords[rendererIndex * 5 + 2] =
|
|
432
|
+
renderer.kind === 'mesh' && mesh?.indices !== undefined
|
|
433
|
+
? (submesh?.indexOffset ?? 0)
|
|
434
|
+
: 0;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const scratchBytes = (capacity * 2 + Math.ceil(capacity / WORKGROUP_SIZE)) * 4;
|
|
438
|
+
const eventInputBytes = Math.max(
|
|
439
|
+
4,
|
|
440
|
+
eventInputCapacity(intent.emitter) * VFX_EVENT_INPUT_BYTES,
|
|
441
|
+
);
|
|
442
|
+
const eventBytes = Math.max(4, eventCapacity(intent.emitter) * VFX_EVENT_BYTES);
|
|
443
|
+
resources.push(
|
|
444
|
+
{
|
|
445
|
+
kind: 'compute-program',
|
|
446
|
+
name: program,
|
|
447
|
+
program: {
|
|
448
|
+
wgsl: intent.emitter.wgsl,
|
|
449
|
+
entryPoints: intent.emitter.reflection.entryPoints,
|
|
450
|
+
bindings: intent.emitter.reflection.bindings,
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
kind: 'buffer',
|
|
455
|
+
name: particles,
|
|
456
|
+
size: capacity * PARTICLE_BYTES,
|
|
457
|
+
usage: ['storage'],
|
|
458
|
+
...(intent.reset ? { data: resetData(capacity * PARTICLE_BYTES) } : {}),
|
|
459
|
+
},
|
|
460
|
+
{ kind: 'buffer', name: aliveIndices, size: capacity * 4, usage: ['storage'] },
|
|
461
|
+
{
|
|
462
|
+
kind: 'buffer',
|
|
463
|
+
name: counters,
|
|
464
|
+
size: COUNTERS_BYTES,
|
|
465
|
+
usage: ['storage'],
|
|
466
|
+
...(intent.reset ? { data: resetData(COUNTERS_BYTES) } : {}),
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
kind: 'buffer',
|
|
470
|
+
name: indirect,
|
|
471
|
+
size: indirectWords.byteLength,
|
|
472
|
+
usage: ['storage', 'indirect'],
|
|
473
|
+
data: indirectWords,
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
kind: 'buffer',
|
|
477
|
+
name: scratch,
|
|
478
|
+
size: scratchBytes,
|
|
479
|
+
usage: ['storage'],
|
|
480
|
+
...(intent.reset ? { data: resetData(scratchBytes) } : {}),
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
kind: 'buffer',
|
|
484
|
+
name: sharedInstances,
|
|
485
|
+
size: capacity * Math.max(BILLBOARD_INSTANCE_BYTES, MESH_INSTANCE_BYTES),
|
|
486
|
+
usage: ['storage', 'vertex'],
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
kind: 'buffer',
|
|
490
|
+
name: eventInputs,
|
|
491
|
+
size: eventInputBytes,
|
|
492
|
+
usage: ['storage'],
|
|
493
|
+
data: encodeEventInputs(intent),
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
kind: 'buffer',
|
|
497
|
+
name: events,
|
|
498
|
+
size: eventBytes,
|
|
499
|
+
usage: ['storage'],
|
|
500
|
+
...(intent.reset ? { data: resetData(eventBytes) } : {}),
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
kind: 'buffer',
|
|
504
|
+
name: runtime,
|
|
505
|
+
size: RUNTIME_BYTES,
|
|
506
|
+
usage: ['uniform'],
|
|
507
|
+
data: runtimeData(intent, entry.camera, undefined, localToWorld),
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
kind: 'compute-bindings',
|
|
511
|
+
name: bindings,
|
|
512
|
+
program,
|
|
513
|
+
entries: computeBindingEntries(intent, {
|
|
514
|
+
0: particles,
|
|
515
|
+
1: runtime,
|
|
516
|
+
2: aliveIndices,
|
|
517
|
+
3: counters,
|
|
518
|
+
4: indirect,
|
|
519
|
+
5: scratch,
|
|
520
|
+
6: sharedInstances,
|
|
521
|
+
8: eventInputs,
|
|
522
|
+
9: events,
|
|
523
|
+
}),
|
|
524
|
+
},
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
const entryPoints = new Set(intent.emitter.reflection.entryPoints);
|
|
528
|
+
const dispatches = simulationDispatches(intent, stagePlan.value).filter((dispatch) =>
|
|
529
|
+
entryPoints.has(dispatch.entryPoint),
|
|
530
|
+
);
|
|
531
|
+
if (dispatches.length > 0) {
|
|
532
|
+
passes.push({
|
|
533
|
+
kind: 'compute',
|
|
534
|
+
name: `${prefix}.simulate`,
|
|
535
|
+
program,
|
|
536
|
+
bindings,
|
|
537
|
+
dispatches,
|
|
538
|
+
});
|
|
539
|
+
dispatchedIntents.add(intent);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
for (const [rendererIndex, renderer] of renderers.entries()) {
|
|
543
|
+
const rendererPrefix = `${prefix}.renderer-${rendererIndex}`;
|
|
544
|
+
const isBillboard = renderer.kind === 'billboard';
|
|
545
|
+
const isTopology =
|
|
546
|
+
renderer.kind === 'ribbon' || renderer.kind === 'trail' || renderer.kind === 'beam';
|
|
547
|
+
const topologyPlan = isTopology ? createTopologyResourcePlan(renderer) : undefined;
|
|
548
|
+
if (topologyPlan !== undefined && !topologyPlan.ok) return err(planFailure());
|
|
549
|
+
const material = options.material?.read(entry.world, renderer.material);
|
|
550
|
+
const materialPass = particleMaterialPass(renderer.kind, material);
|
|
551
|
+
const mesh = meshes[rendererIndex];
|
|
552
|
+
const submesh =
|
|
553
|
+
renderer.kind === 'mesh' ? mesh?.submeshes[renderer.submesh ?? 0] : undefined;
|
|
554
|
+
const indexFormat = mesh?.indices instanceof Uint32Array ? 'uint32' : 'uint16';
|
|
555
|
+
const instances = `${rendererPrefix}.instances`;
|
|
556
|
+
const history = `${rendererPrefix}.history`;
|
|
557
|
+
const projectionRuntime = `${rendererPrefix}.runtime`;
|
|
558
|
+
const projectionBindings = `${rendererPrefix}.compute-bindings`;
|
|
559
|
+
const vertexLayout = isBillboard
|
|
560
|
+
? RENDER_FEATURE_VERTEX_LAYOUTS.billboardMaterialInstance
|
|
561
|
+
: isTopology
|
|
562
|
+
? RENDER_FEATURE_VERTEX_LAYOUTS.topologySegmentInstance
|
|
563
|
+
: RENDER_FEATURE_VERTEX_LAYOUTS.meshGeometryMaterialInstance;
|
|
564
|
+
const instanceBytes = isTopology
|
|
565
|
+
? (topologyPlan?.value.vertexBytes ?? 16)
|
|
566
|
+
: capacity * (isBillboard ? BILLBOARD_INSTANCE_BYTES : MESH_INSTANCE_BYTES);
|
|
567
|
+
const historyBytes =
|
|
568
|
+
renderer.kind === 'trail'
|
|
569
|
+
? Math.max(16, renderer.capacity * renderer.historyLength * 16)
|
|
570
|
+
: 16;
|
|
571
|
+
|
|
572
|
+
resources.push(
|
|
573
|
+
{
|
|
574
|
+
kind: 'buffer',
|
|
575
|
+
name: instances,
|
|
576
|
+
size: instanceBytes,
|
|
577
|
+
usage: ['storage', 'vertex'],
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
kind: 'buffer',
|
|
581
|
+
name: history,
|
|
582
|
+
size: historyBytes,
|
|
583
|
+
usage: ['storage'],
|
|
584
|
+
...(intent.reset ? { data: resetData(historyBytes) } : {}),
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
kind: 'buffer',
|
|
588
|
+
name: projectionRuntime,
|
|
589
|
+
size: RUNTIME_BYTES,
|
|
590
|
+
usage: ['uniform'],
|
|
591
|
+
data: runtimeData(
|
|
592
|
+
{ ...intent, fixedDelta: 0, spawnCount: 0 },
|
|
593
|
+
entry.camera,
|
|
594
|
+
material,
|
|
595
|
+
localToWorld,
|
|
596
|
+
renderer,
|
|
597
|
+
rendererIndex,
|
|
598
|
+
),
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
kind: 'compute-bindings',
|
|
602
|
+
name: projectionBindings,
|
|
603
|
+
program,
|
|
604
|
+
entries: computeBindingEntries(intent, {
|
|
605
|
+
0: particles,
|
|
606
|
+
1: projectionRuntime,
|
|
607
|
+
2: aliveIndices,
|
|
608
|
+
3: counters,
|
|
609
|
+
4: indirect,
|
|
610
|
+
5: history,
|
|
611
|
+
6: instances,
|
|
612
|
+
8: eventInputs,
|
|
613
|
+
9: events,
|
|
614
|
+
}),
|
|
615
|
+
},
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
const projectionDispatches: Extract<
|
|
619
|
+
PlanPass,
|
|
620
|
+
{ readonly kind: 'compute' }
|
|
621
|
+
>['dispatches'][number][] = [];
|
|
622
|
+
const pushProjection = (entryPoint: string, workgroups: number): void => {
|
|
623
|
+
if (!entryPoints.has(entryPoint)) return;
|
|
624
|
+
projectionDispatches.push({
|
|
625
|
+
kind: 'direct',
|
|
626
|
+
entryPoint,
|
|
627
|
+
workgroups: [Math.max(1, workgroups)],
|
|
628
|
+
});
|
|
629
|
+
};
|
|
630
|
+
if (isBillboard && renderer.sorting === 'back-to-front') {
|
|
631
|
+
pushProjection('forgeax_vfx_sort_main', 1);
|
|
632
|
+
}
|
|
633
|
+
if (renderer.kind === 'trail') {
|
|
634
|
+
pushProjection(
|
|
635
|
+
'forgeax_vfx_trail_history_main',
|
|
636
|
+
Math.ceil(renderer.capacity / WORKGROUP_SIZE),
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
const projectionCount =
|
|
640
|
+
renderer.kind === 'trail'
|
|
641
|
+
? renderer.capacity * Math.max(1, renderer.historyLength - 1)
|
|
642
|
+
: isTopology
|
|
643
|
+
? renderer.capacity
|
|
644
|
+
: capacity;
|
|
645
|
+
pushProjection(
|
|
646
|
+
renderer.kind === 'billboard'
|
|
647
|
+
? 'forgeax_vfx_billboard_main'
|
|
648
|
+
: renderer.kind === 'mesh'
|
|
649
|
+
? 'forgeax_vfx_mesh_main'
|
|
650
|
+
: `forgeax_vfx_${renderer.kind}_main`,
|
|
651
|
+
Math.ceil(projectionCount / WORKGROUP_SIZE),
|
|
652
|
+
);
|
|
653
|
+
if (projectionDispatches.length > 0) {
|
|
654
|
+
passes.push({
|
|
655
|
+
kind: 'compute',
|
|
656
|
+
name: `${rendererPrefix}.project`,
|
|
657
|
+
program,
|
|
658
|
+
bindings: projectionBindings,
|
|
659
|
+
dispatches: projectionDispatches,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const graphicsProgram = `${rendererPrefix}.graphics-program`;
|
|
664
|
+
const graphicsBindings = `${rendererPrefix}.graphics-bindings`;
|
|
665
|
+
const vertexData = `${rendererPrefix}.vertex-data`;
|
|
666
|
+
const renderState =
|
|
667
|
+
isBillboard && depthTarget !== undefined
|
|
668
|
+
? { ...(materialPass.renderState ?? {}), depthWriteEnabled: false }
|
|
669
|
+
: materialPass.renderState;
|
|
670
|
+
resources.push(
|
|
671
|
+
{
|
|
672
|
+
kind: 'graphics-program',
|
|
673
|
+
name: graphicsProgram,
|
|
674
|
+
program: {
|
|
675
|
+
shader: materialPass.shader,
|
|
676
|
+
vertexLayout,
|
|
677
|
+
colorFormats: [colorTarget?.format ?? 'rgba8unorm-srgb'],
|
|
678
|
+
...(depthTarget === undefined ? {} : { depthFormat: depthTarget.format }),
|
|
679
|
+
sampleCount: colorTarget?.sampleCount ?? 1,
|
|
680
|
+
topology: submesh?.topology ?? 'triangle-list',
|
|
681
|
+
...(mesh?.indices === undefined ? {} : { indexFormat }),
|
|
682
|
+
...(renderState === undefined ? {} : { renderState }),
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
kind: 'graphics-bindings',
|
|
687
|
+
name: graphicsBindings,
|
|
688
|
+
program: graphicsProgram,
|
|
689
|
+
values: {
|
|
690
|
+
group: 0,
|
|
691
|
+
runtime: projectionRuntime,
|
|
692
|
+
instances,
|
|
693
|
+
...(isBillboard ? { sceneDepthBinding: 1 } : {}),
|
|
694
|
+
},
|
|
695
|
+
...(isBillboard && depthTarget !== undefined
|
|
696
|
+
? { logicalTargets: { sceneDepth: depthTarget.name } }
|
|
697
|
+
: {}),
|
|
698
|
+
},
|
|
699
|
+
{ kind: 'vertex-data', name: vertexData, layout: vertexLayout, buffer: instances },
|
|
700
|
+
);
|
|
701
|
+
const drawBindings = [graphicsBindings];
|
|
702
|
+
if (particleMaterialUsesBindings(material)) {
|
|
703
|
+
const materialBindings = `${rendererPrefix}.material-bindings`;
|
|
704
|
+
resources.push({
|
|
705
|
+
kind: 'graphics-bindings',
|
|
706
|
+
name: materialBindings,
|
|
707
|
+
program: graphicsProgram,
|
|
708
|
+
values: {
|
|
709
|
+
group: 1,
|
|
710
|
+
material: { world: worldIndex, guid: renderer.material },
|
|
711
|
+
},
|
|
712
|
+
});
|
|
713
|
+
drawBindings.push(materialBindings);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const vertexBindings: { readonly slot: number; readonly resource: string }[] = [];
|
|
717
|
+
let indexData:
|
|
718
|
+
| { readonly resource: string; readonly format: 'uint16' | 'uint32' }
|
|
719
|
+
| undefined;
|
|
720
|
+
if (renderer.kind === 'mesh') {
|
|
721
|
+
if (mesh === undefined) return err(planFailure());
|
|
722
|
+
const geometryBuffer = `${rendererPrefix}.geometry-buffer`;
|
|
723
|
+
const geometry = `${rendererPrefix}.geometry`;
|
|
724
|
+
const geometryData = canonicalMeshVertices(mesh);
|
|
725
|
+
resources.push(
|
|
726
|
+
{
|
|
727
|
+
kind: 'buffer',
|
|
728
|
+
name: geometryBuffer,
|
|
729
|
+
size: geometryData.byteLength,
|
|
730
|
+
usage: ['vertex'],
|
|
731
|
+
data: geometryData,
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
kind: 'vertex-data',
|
|
735
|
+
name: geometry,
|
|
736
|
+
layout: vertexLayout,
|
|
737
|
+
buffer: geometryBuffer,
|
|
738
|
+
},
|
|
739
|
+
);
|
|
740
|
+
vertexBindings.push(
|
|
741
|
+
{ slot: 0, resource: geometry },
|
|
742
|
+
{ slot: 1, resource: vertexData },
|
|
743
|
+
);
|
|
744
|
+
if (mesh.indices !== undefined) {
|
|
745
|
+
const indexBuffer = `${rendererPrefix}.index-buffer`;
|
|
746
|
+
const indices = `${rendererPrefix}.indices`;
|
|
747
|
+
resources.push(
|
|
748
|
+
{
|
|
749
|
+
kind: 'buffer',
|
|
750
|
+
name: indexBuffer,
|
|
751
|
+
size: mesh.indices.byteLength,
|
|
752
|
+
usage: ['index'],
|
|
753
|
+
data: mesh.indices,
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
kind: 'index-data',
|
|
757
|
+
name: indices,
|
|
758
|
+
format: indexFormat,
|
|
759
|
+
buffer: indexBuffer,
|
|
760
|
+
},
|
|
761
|
+
);
|
|
762
|
+
indexData = { resource: indices, format: indexFormat };
|
|
763
|
+
}
|
|
764
|
+
} else {
|
|
765
|
+
vertexBindings.push({ slot: 0, resource: vertexData });
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
passes.push({
|
|
769
|
+
kind: 'raster',
|
|
770
|
+
name: `${rendererPrefix}.raster`,
|
|
771
|
+
colorAttachments: [
|
|
772
|
+
{
|
|
773
|
+
target: colorTarget?.name ?? 'swapchain',
|
|
774
|
+
loadOp: 'load',
|
|
775
|
+
storeOp: 'store',
|
|
776
|
+
},
|
|
777
|
+
],
|
|
778
|
+
...(depthTarget === undefined
|
|
779
|
+
? {}
|
|
780
|
+
: {
|
|
781
|
+
depthStencilAttachment: {
|
|
782
|
+
target: depthTarget.name,
|
|
783
|
+
depthLoadOp: 'load',
|
|
784
|
+
depthStoreOp: 'store',
|
|
785
|
+
},
|
|
786
|
+
}),
|
|
787
|
+
...(isBillboard && depthTarget !== undefined
|
|
788
|
+
? { sampledTargets: [depthTarget.name] }
|
|
789
|
+
: {}),
|
|
790
|
+
draws: [
|
|
791
|
+
{
|
|
792
|
+
program: graphicsProgram,
|
|
793
|
+
bindings: drawBindings,
|
|
794
|
+
vertexData: vertexBindings,
|
|
795
|
+
...(indexData === undefined ? {} : { indexData }),
|
|
796
|
+
draw: {
|
|
797
|
+
kind: indexData === undefined ? 'draw-indirect' : 'draw-indexed-indirect',
|
|
798
|
+
resource: indirect,
|
|
799
|
+
offset: rendererIndex * 20,
|
|
800
|
+
},
|
|
801
|
+
},
|
|
802
|
+
],
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
for (const entry of frame.worlds) {
|
|
808
|
+
for (const intent of entry.intents) {
|
|
809
|
+
if (dispatchedIntents.has(intent)) {
|
|
810
|
+
entry.runtime.markEventDispatched(intent.player, intent.eventCounters);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const lastIntent = entry.intents.at(-1);
|
|
814
|
+
if (lastIntent !== undefined) entry.runtime.commit(lastIntent.sequence);
|
|
815
|
+
}
|
|
816
|
+
return ok<RenderFeaturePlan>({ resources, passes });
|
|
817
|
+
},
|
|
818
|
+
};
|
|
819
|
+
}
|