@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/engine-vfx-render",
3
- "version": "0.1.6",
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.6",
26
- "@forgeax/engine-ecs": "0.1.6",
27
- "@forgeax/engine-math": "0.1.6",
28
- "@forgeax/engine-render": "0.1.6",
29
- "@forgeax/engine-scene": "0.1.6",
30
- "@forgeax/engine-types": "0.1.6",
31
- "@forgeax/engine-vfx": "0.1.6"
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(1);
25
- expect(source, name).toMatch(/world[_A-Za-z]*position|worldPosition/);
26
- expect(source, name).toMatch(/ray[_A-Za-z]*distance|distance/);
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 { RenderFeaturePlan } from '@forgeax/engine-render';
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
- ...(isBillboard ? { sceneDepthBinding: 1 } : {}),
707
+ ...(sceneDepthBinding === undefined ? {} : { sceneDepthBinding }),
694
708
  },
695
- ...(isBillboard && depthTarget !== undefined
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
- ...(isBillboard && depthTarget !== undefined
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 { AssetRegistry as LegacyAssetRegistry } from '@forgeax/engine-assets-runtime';
2
- import type { AssetRegistry as CoreAssetRegistry } from '@forgeax/engine-assets-runtime/internal';
3
- import {
4
- type AssetRegistryResolver,
5
- getAssetRegistryResolver,
6
- } from '@forgeax/engine-assets-runtime/internal';
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 { AssetDecoderLease, MaterialAsset, MeshAsset, Result } from '@forgeax/engine-types';
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
- type AssetRegistry = LegacyAssetRegistry | CoreAssetRegistry;
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: CoreAssetRegistry): AssetDecoderLease {
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 legacy = attached.assets.lookup(guid);
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 ('load' in attached.assets && !attached.pendingRenderAssets.has(key)) {
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 request = attached.assets
191
- .load(guid, kind)
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
- if ('installDecoder' in assets) {
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.loaders.registerPackLoader(vfxGpuEffectPackLoader);
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
@@ -46,6 +46,7 @@ export {
46
46
  createVfxDataInterfaceRegistry,
47
47
  } from './host/data-interface-providers.js';
48
48
  export type {
49
+ VfxAuthoringAssetRegistry,
49
50
  VfxRuntimeHost,
50
51
  VfxRuntimeHostControl,
51
52
  VfxRuntimeHostControlError,
@@ -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>,
@@ -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;
@@ -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>(
@@ -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>(