@fps-games/vfx 0.3.15 → 0.3.17

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/CHANGELOG.md CHANGED
@@ -12,6 +12,14 @@
12
12
 
13
13
  <!-- release.mjs 在此行下方插入新版本区块 -->
14
14
 
15
+ ## v0.3.16 — 2026-07-08
16
+
17
+ ### 改动
18
+ - feat(pulse-laser-volley-sequence): add single-shot pulse laser variant
19
+
20
+ ### 特效
21
+ - 新增:pulse-laser-volley-sequence
22
+
15
23
  ## v0.3.11 — 2026-07-07
16
24
 
17
25
  ### 改动
@@ -1,16 +1,18 @@
1
1
  import { Vector3 } from '@babylonjs/core/Maths/math.vector';
2
2
  import { Scene } from '@babylonjs/core/scene';
3
3
  import { VfxRegistry } from './registry';
4
- import { VfxDebugAnchor, VfxEffectHandle, VfxEffectPackage, VfxParamValues, VfxSpawnTransform } from './types';
4
+ import { VfxDebugAnchor, VfxEffectHandle, VfxEffectPackage, VfxParamValues, VfxRuntimeResources, VfxSpawnTransform } from './types';
5
5
  export interface EffectPackageServiceOptions {
6
6
  /** 注入的注册表(由项目侧 glob + buildRegistry 构建);库不直接引用项目注册表 */
7
7
  registry: VfxRegistry;
8
8
  getDebugAnchorPosition?: (anchor: VfxDebugAnchor) => Vector3 | null;
9
+ runtime?: VfxRuntimeResources;
9
10
  }
10
11
  export declare class EffectPackageService {
11
12
  private readonly scene;
12
13
  private readonly registry;
13
14
  private readonly getDebugAnchorPosition?;
15
+ private readonly runtime?;
14
16
  private readonly activeHandles;
15
17
  private readonly savedParams;
16
18
  constructor(scene: Scene, options: EffectPackageServiceOptions);
@@ -1,7 +1,7 @@
1
1
  import { Scene } from '@babylonjs/core/scene';
2
2
  import { Vector3 } from '@babylonjs/core/Maths/math.vector';
3
3
  import { VfxRegistry } from './registry';
4
- import { VfxDebugAnchor, VfxEffectHandle, VfxEffectPackage, VfxParamValues, VfxSpawnTransform } from './types';
4
+ import { VfxDebugAnchor, VfxEffectHandle, VfxEffectPackage, VfxParamValues, VfxRuntimeResources, VfxSpawnTransform } from './types';
5
5
  import { EffectPackageService } from './EffectPackageService';
6
6
  export interface SceneVfxWarmupEntry {
7
7
  effectId: string;
@@ -12,6 +12,7 @@ export interface SceneVfxServiceOptions {
12
12
  /** 注入的注册表(项目侧 glob + buildRegistry 构建) */
13
13
  registry: VfxRegistry;
14
14
  getDebugAnchorPosition?: (anchor: VfxDebugAnchor) => Vector3 | null;
15
+ runtime?: VfxRuntimeResources;
15
16
  }
16
17
  export declare class SceneVfxService {
17
18
  private effectPackageService;
@@ -1,3 +1,4 @@
1
+ import { GlowLayer } from '@babylonjs/core/Layers/glowLayer';
1
2
  import { ShaderMaterial } from '@babylonjs/core/Materials/shaderMaterial';
2
3
  import { Vector3 } from '@babylonjs/core/Maths/math.vector';
3
4
  import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
@@ -23,6 +24,8 @@ export interface EnergyShieldParams extends VfxParamValues {
23
24
  flowIntensity: number;
24
25
  marbleContrast: number;
25
26
  bloomIntensity: number;
27
+ bloomKernelSize: number;
28
+ showGlowLayer: boolean;
26
29
  haloScale: number;
27
30
  showHalo: boolean;
28
31
  segments: number;
@@ -33,11 +36,13 @@ export interface CreateEnergyShieldOptions {
33
36
  position?: Vector3;
34
37
  params?: Partial<EnergyShieldParams>;
35
38
  renderingGroupId?: number;
39
+ glowLayer?: GlowLayer | null;
36
40
  disposeRoot?: boolean;
37
41
  }
38
42
  export interface EnergyShieldHandle extends VfxEffectHandle {
39
43
  shield: Mesh;
40
44
  material: ShaderMaterial;
45
+ glowLayer: GlowLayer | null;
41
46
  update(params: Partial<EnergyShieldParams>): void;
42
47
  }
43
48
  export declare const energyShieldEffectPackage: VfxEffectPackage<EnergyShieldParams>;
@@ -1,4 +1,5 @@
1
1
  import { Constants } from "@babylonjs/core/Engines/constants";
2
+ import { GlowLayer } from "@babylonjs/core/Layers/glowLayer";
2
3
  import { Effect } from "@babylonjs/core/Materials/effect";
3
4
  import { ShaderMaterial } from "@babylonjs/core/Materials/shaderMaterial";
4
5
  import { Color3 } from "@babylonjs/core/Maths/math.color";
@@ -28,6 +29,8 @@ const DEFAULT_PARAMS = {
28
29
  flowIntensity: 1.15,
29
30
  marbleContrast: 0.67,
30
31
  bloomIntensity: 0.45,
32
+ bloomKernelSize: 36,
33
+ showGlowLayer: true,
31
34
  haloScale: 1.018,
32
35
  showHalo: false,
33
36
  segments: 64
@@ -234,11 +237,19 @@ const energyShieldEffectPackage = {
234
237
  { key: "flowIntensity", label: "流动强度", kind: "number", min: 0, max: 8, step: 0.01 },
235
238
  { key: "marbleContrast", label: "纹路锐度", kind: "number", min: 0.25, max: 4, step: 0.01 },
236
239
  { key: "bloomIntensity", label: "Bloom 强度", kind: "number", min: 0, max: 4, step: 0.01 },
240
+ { key: "bloomKernelSize", label: "GlowLayer 半径", kind: "number", min: 8, max: 128, step: 1 },
241
+ { key: "showGlowLayer", label: "显示 GlowLayer", kind: "boolean" },
237
242
  { key: "haloScale", label: "外层光晕缩放", kind: "scale", min: 1, max: 1.08, step: 1e-3 },
238
243
  { key: "showHalo", label: "显示外层光晕", kind: "boolean" }
239
244
  ],
240
- create({ scene, root, position, renderingGroupId, params }) {
241
- return createEnergyShield(scene, { root, position, renderingGroupId, params });
245
+ create({ scene, root, position, renderingGroupId, params, runtime }) {
246
+ return createEnergyShield(scene, {
247
+ root,
248
+ position,
249
+ renderingGroupId,
250
+ params,
251
+ glowLayer: runtime?.glowLayer ?? runtime?.getGlowLayer?.() ?? null
252
+ });
242
253
  }
243
254
  };
244
255
  function createEnergyShield(scene, options = {}) {
@@ -269,6 +280,10 @@ function createEnergyShield(scene, options = {}) {
269
280
  shield.alphaIndex = 20;
270
281
  const material = createShieldMaterial(scene);
271
282
  shield.material = material;
283
+ const sharedGlowLayer = options.glowLayer ?? null;
284
+ let glowLayer = null;
285
+ let ownsGlowLayer = false;
286
+ let glowMeshIncluded = false;
272
287
  let halo = null;
273
288
  let haloMaterial = null;
274
289
  let haloFeather = null;
@@ -283,6 +298,45 @@ function createEnergyShield(scene, options = {}) {
283
298
  haloFeather = null;
284
299
  haloFeatherMaterial = null;
285
300
  };
301
+ const addGlowMesh = () => {
302
+ if (!glowLayer || glowMeshIncluded) return;
303
+ glowLayer.addIncludedOnlyMesh(shield);
304
+ glowLayer.referenceMeshToUseItsOwnMaterial(shield);
305
+ glowMeshIncluded = true;
306
+ };
307
+ const removeGlowMesh = () => {
308
+ if (!glowLayer || !glowMeshIncluded) return;
309
+ glowLayer.removeIncludedOnlyMesh(shield);
310
+ glowMeshIncluded = false;
311
+ };
312
+ const disposeGlowLayer = () => {
313
+ removeGlowMesh();
314
+ if (ownsGlowLayer) glowLayer?.dispose();
315
+ glowLayer = null;
316
+ ownsGlowLayer = false;
317
+ };
318
+ const ensureGlowLayer = () => {
319
+ if (!params.showGlowLayer || params.bloomIntensity <= 1e-3) {
320
+ disposeGlowLayer();
321
+ return;
322
+ }
323
+ if (!glowLayer) {
324
+ if (sharedGlowLayer) {
325
+ glowLayer = sharedGlowLayer;
326
+ } else {
327
+ glowLayer = new GlowLayer(`vfx_energyShield_glow_${shield.uniqueId}`, scene, {
328
+ blurKernelSize: params.bloomKernelSize,
329
+ mainTextureRatio: 0.35
330
+ });
331
+ ownsGlowLayer = true;
332
+ }
333
+ addGlowMesh();
334
+ }
335
+ if (ownsGlowLayer) {
336
+ glowLayer.intensity = params.bloomIntensity;
337
+ glowLayer.blurKernelSize = params.bloomKernelSize;
338
+ }
339
+ };
286
340
  const ensureHalo = () => {
287
341
  if (!params.showHalo || params.bloomIntensity <= 1e-3) {
288
342
  disposeHalo();
@@ -335,6 +389,7 @@ function createEnergyShield(scene, options = {}) {
335
389
  haloFeather.material = haloFeatherMaterial;
336
390
  }
337
391
  };
392
+ ensureGlowLayer();
338
393
  ensureHalo();
339
394
  applyParams(material, shield, halo, haloMaterial, haloFeather, haloFeatherMaterial, params);
340
395
  let elapsed = 0;
@@ -349,6 +404,7 @@ function createEnergyShield(scene, options = {}) {
349
404
  if (disposed) return;
350
405
  disposed = true;
351
406
  scene.onBeforeRenderObservable.remove(observer);
407
+ disposeGlowLayer();
352
408
  disposeHalo();
353
409
  shield.dispose(false, false);
354
410
  material.dispose();
@@ -358,8 +414,12 @@ function createEnergyShield(scene, options = {}) {
358
414
  root,
359
415
  shield,
360
416
  material,
417
+ get glowLayer() {
418
+ return glowLayer;
419
+ },
361
420
  update(partial) {
362
421
  params = mergeParams(partial, params);
422
+ ensureGlowLayer();
363
423
  ensureHalo();
364
424
  applyParams(material, shield, halo, haloMaterial, haloFeather, haloFeatherMaterial, params);
365
425
  },
@@ -0,0 +1,14 @@
1
+ import { VfxColorValue, VfxEffectPackage, VfxParamValues } from '../../types';
2
+ interface PulseLaserParams extends VfxParamValues {
3
+ lifetimeMode: string;
4
+ lifetime: number;
5
+ targetDistance: number;
6
+ boltSpeed: number;
7
+ boltLen: number;
8
+ boltColor: VfxColorValue;
9
+ boltCore: number;
10
+ boltCorePow: number;
11
+ boltBright: number;
12
+ }
13
+ export declare const pulseLaserVolleySequenceEffectPackage: VfxEffectPackage<PulseLaserParams>;
14
+ export {};
@@ -0,0 +1,142 @@
1
+ import { CreatePlane } from "@babylonjs/core/Meshes/Builders/planeBuilder";
2
+ import { ShaderMaterial } from "@babylonjs/core/Materials/shaderMaterial";
3
+ import { Effect } from "@babylonjs/core/Materials/effect";
4
+ import { Texture } from "@babylonjs/core/Materials/Textures/texture";
5
+ import { Color3 } from "@babylonjs/core/Maths/math.color";
6
+ import { Vector3 } from "@babylonjs/core/Maths/math.vector";
7
+ import { Space } from "@babylonjs/core/Maths/math.axis";
8
+ import { Constants } from "@babylonjs/core/Engines/constants";
9
+ import { L as LIFETIME_DEBUG_PARAMS, a as LIFETIME_DEFAULTS, s as scheduleLifetime } from "../../chunks/space-shared-xh5IOcLH.js";
10
+ const boltTextureUrl = "data:image/webp;base64,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";
11
+ const DEFAULT_PARAMS = {
12
+ ...LIFETIME_DEFAULTS,
13
+ targetDistance: 12,
14
+ boltSpeed: 26,
15
+ boltLen: 1,
16
+ boltColor: { r: 0.071, g: 0.408, b: 1 },
17
+ // #1268ff
18
+ boltCore: 0.3,
19
+ boltCorePow: 3,
20
+ boltBright: 1.3
21
+ };
22
+ const BOLT_LEN = 2, BOLT_W = 0.34;
23
+ const BOLT_VERTEX = `precision highp float;
24
+ attribute vec3 position; attribute vec2 uv;
25
+ uniform mat4 worldViewProjection;
26
+ varying vec2 vUV;
27
+ void main() { vUV = uv; gl_Position = worldViewProjection * vec4(position, 1.0); }`;
28
+ const BOLT_FRAGMENT = `precision highp float;
29
+ varying vec2 vUV;
30
+ uniform sampler2D tex;
31
+ uniform vec3 tint;
32
+ uniform float coreStr; uniform float corePow; uniform float bright;
33
+ void main() {
34
+ vec4 t = texture2D(tex, vUV);
35
+ float core = pow(t.a, corePow) * coreStr;
36
+ vec3 col = mix(tint, vec3(1.0), core) * bright;
37
+ gl_FragColor = vec4(col, t.a);
38
+ }`;
39
+ let shadersRegistered = false;
40
+ function registerShaders() {
41
+ if (shadersRegistered) return;
42
+ Effect.ShadersStore["vfxPulseBoltVertexShader"] = BOLT_VERTEX;
43
+ Effect.ShadersStore["vfxPulseBoltFragmentShader"] = BOLT_FRAGMENT;
44
+ shadersRegistered = true;
45
+ }
46
+ function colorVec(c) {
47
+ return new Color3(c.r, c.g, c.b);
48
+ }
49
+ function excludeFromGlow(scene, mesh) {
50
+ for (const layer of scene.effectLayers ?? []) {
51
+ const add = layer.addExcludedMesh;
52
+ if (typeof add === "function") add.call(layer, mesh);
53
+ }
54
+ }
55
+ const pulseLaserVolleySequenceEffectPackage = {
56
+ id: "pulse-laser-volley-sequence",
57
+ nameZh: "脉冲激光(单发)",
58
+ placement: "socket",
59
+ optionalInputs: ["aim"],
60
+ defaultParams: DEFAULT_PARAMS,
61
+ debugAnchor: "playerRight",
62
+ debugParams: [
63
+ ...LIFETIME_DEBUG_PARAMS,
64
+ { key: "targetDistance", label: "目标距离", kind: "number", min: 2, max: 40, step: 0.5 },
65
+ { key: "boltSpeed", label: "光弹速度", kind: "number", min: 5, max: 60, step: 1 },
66
+ { key: "boltLen", label: "光弹长度", kind: "number", min: 0.3, max: 4, step: 0.05 },
67
+ { key: "boltColor", label: "光弹颜色", kind: "color" },
68
+ { key: "boltCore", label: "白芯强度", kind: "number", min: 0, max: 1, step: 0.01 },
69
+ { key: "boltCorePow", label: "白芯收窄", kind: "number", min: 0.5, max: 8, step: 0.1 },
70
+ { key: "boltBright", label: "光弹亮度", kind: "number", min: 0.2, max: 3, step: 0.05 }
71
+ ],
72
+ create({ scene, root, position, rotationY, params, inputs }) {
73
+ root.position.copyFrom(position);
74
+ root.rotation.y = rotationY;
75
+ registerShaders();
76
+ const effectiveDist = () => {
77
+ if (!inputs?.aim) return params.targetDistance;
78
+ const tgt = inputs.aim();
79
+ root.lookAt(tgt, 0, 0, 0, Space.WORLD);
80
+ return Vector3.Distance(root.getAbsolutePosition(), tgt);
81
+ };
82
+ const boltTex = new Texture(boltTextureUrl, scene);
83
+ boltTex.hasAlpha = true;
84
+ const boltMat = new ShaderMaterial("vfx_pulse_boltMat", scene, { vertex: "vfxPulseBolt", fragment: "vfxPulseBolt" }, {
85
+ attributes: ["position", "uv"],
86
+ uniforms: ["worldViewProjection", "tint", "coreStr", "corePow", "bright"],
87
+ samplers: ["tex"],
88
+ needAlphaBlending: true
89
+ });
90
+ boltMat.setTexture("tex", boltTex);
91
+ boltMat.backFaceCulling = false;
92
+ boltMat.disableDepthWrite = true;
93
+ boltMat.alphaMode = Constants.ALPHA_ADD;
94
+ boltMat.setColor3("tint", colorVec(params.boltColor));
95
+ boltMat.setFloat("coreStr", params.boltCore);
96
+ boltMat.setFloat("corePow", params.boltCorePow);
97
+ boltMat.setFloat("bright", params.boltBright);
98
+ const mesh = CreatePlane("vfx_pulse_single_bolt", { width: BOLT_W, height: BOLT_LEN }, scene);
99
+ mesh.rotation.x = Math.PI / 2;
100
+ mesh.material = boltMat;
101
+ mesh.isPickable = false;
102
+ mesh.parent = root;
103
+ mesh.setEnabled(false);
104
+ excludeFromGlow(scene, mesh);
105
+ const bolt = { mesh, left: 0 };
106
+ let fired = false;
107
+ const observer = scene.onBeforeRenderObservable.add(() => {
108
+ const dt = Math.min(scene.getEngine().getDeltaTime() / 1e3, 0.05);
109
+ const dist = effectiveDist();
110
+ if (!fired) {
111
+ bolt.mesh.position.set(0, 0, 0);
112
+ bolt.mesh.scaling.z = params.boltLen / BOLT_LEN;
113
+ bolt.left = dist / params.boltSpeed;
114
+ bolt.mesh.setEnabled(true);
115
+ fired = true;
116
+ }
117
+ const step = params.boltSpeed * dt;
118
+ if (bolt.left > 0) {
119
+ bolt.left -= dt;
120
+ bolt.mesh.position.z += step;
121
+ if (bolt.left <= 0) bolt.mesh.setEnabled(false);
122
+ }
123
+ });
124
+ let disposed = false;
125
+ let ltTimer = 0;
126
+ const dispose = () => {
127
+ if (disposed) return;
128
+ disposed = true;
129
+ if (ltTimer) window.clearTimeout(ltTimer);
130
+ scene.onBeforeRenderObservable.remove(observer);
131
+ bolt.mesh.dispose();
132
+ boltMat.dispose();
133
+ boltTex.dispose();
134
+ root.dispose();
135
+ };
136
+ ltTimer = scheduleLifetime(params, dispose);
137
+ return { root, dispose };
138
+ }
139
+ };
140
+ export {
141
+ pulseLaserVolleySequenceEffectPackage
142
+ };
@@ -76,8 +76,15 @@ const treasureAttentionEffectPackage = {
76
76
  { key: "bloomKernelSize", label: "Bloom 半径", kind: "number", min: 8, max: 96, step: 1 },
77
77
  { key: "showGlowLayer", label: "显示 GlowLayer", kind: "boolean" }
78
78
  ],
79
- create({ scene, root, position, renderingGroupId, params }) {
80
- return createTreasureAttentionFx(scene, { root, position, renderingGroupId, params, serviceOwnedRoot: true });
79
+ create({ scene, root, position, renderingGroupId, params, runtime }) {
80
+ return createTreasureAttentionFx(scene, {
81
+ root,
82
+ position,
83
+ renderingGroupId,
84
+ params,
85
+ glowLayer: runtime?.glowLayer ?? runtime?.getGlowLayer?.() ?? null,
86
+ serviceOwnedRoot: true
87
+ });
81
88
  }
82
89
  };
83
90
  function createTreasureAttentionFx(scene, options = {}) {
package/dist/index.js CHANGED
@@ -75,6 +75,7 @@ class EffectPackageService {
75
75
  this.scene = scene;
76
76
  this.registry = options.registry;
77
77
  this.getDebugAnchorPosition = options.getDebugAnchorPosition;
78
+ this.runtime = options.runtime;
78
79
  this.savedParams = cloneOverrideMap(options.registry.savedParams);
79
80
  }
80
81
  getEffectPackages() {
@@ -127,7 +128,8 @@ class EffectPackageService {
127
128
  renderingGroupId: spawn.renderingGroupId,
128
129
  params: runtimeParams,
129
130
  scale: spawn.scale,
130
- inputs: spawn.inputs
131
+ inputs: spawn.inputs,
132
+ runtime: this.runtime
131
133
  });
132
134
  applySpawnToRoot(root, spawn, applyRootScale);
133
135
  } catch (error) {
@@ -315,7 +317,8 @@ class SceneVfxService {
315
317
  constructor(scene, options) {
316
318
  this.effectPackageService = new EffectPackageService(scene, {
317
319
  registry: options.registry,
318
- getDebugAnchorPosition: options.getDebugAnchorPosition
320
+ getDebugAnchorPosition: options.getDebugAnchorPosition,
321
+ runtime: options.runtime
319
322
  });
320
323
  }
321
324
  getEffectPackageService() {
package/dist/types.d.ts CHANGED
@@ -2,6 +2,7 @@ import { Scene } from '@babylonjs/core/scene';
2
2
  import { Vector3 } from '@babylonjs/core/Maths/math.vector';
3
3
  import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
4
4
  import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
5
+ import { GlowLayer } from '@babylonjs/core/Layers/glowLayer';
5
6
  export type VfxDebugAnchor = 'playerRight' | 'player';
6
7
  export type VfxInput<T> = T | (() => T);
7
8
  export type VfxInputKey = 'aim' | 'normal' | 'endpoints' | 'target' | 'reference';
@@ -24,6 +25,11 @@ export interface VfxResolvedInputs {
24
25
  reference?: () => Vector3;
25
26
  scalars?: Record<string, () => number>;
26
27
  }
28
+ /** 运行时共享资源:不可序列化,由项目/游戏层注入,与可保存 params 分开。 */
29
+ export interface VfxRuntimeResources {
30
+ glowLayer?: GlowLayer | null;
31
+ getGlowLayer?: () => GlowLayer | null;
32
+ }
27
33
  export type VfxParamValue = number | string | boolean | VfxColorValue;
28
34
  export type VfxParamValues = Record<string, VfxParamValue>;
29
35
  export interface VfxColorValue {
@@ -74,6 +80,8 @@ export interface VfxEffectPackageContext<PParams extends VfxParamValues = VfxPar
74
80
  scale: Vector3;
75
81
  /** 运行时外部信息(已归一化为访问器)。link/target/定向武器/活改标量等按 required/optionalInputs 读取。 */
76
82
  inputs?: VfxResolvedInputs;
83
+ /** 运行时共享资源(如 scene 级共享 GlowLayer)。 */
84
+ runtime?: VfxRuntimeResources;
77
85
  }
78
86
  export interface VfxEffectHandle {
79
87
  root: TransformNode;
package/index.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "vfx-index/1.0",
3
- "count": 40,
3
+ "count": 41,
4
4
  "effects": [
5
5
  {
6
6
  "id": "build.particle.lumber_debris_scatter",
@@ -277,6 +277,16 @@
277
277
  "media": {},
278
278
  "path": "packages/effects/pulse-laser-volley"
279
279
  },
280
+ {
281
+ "id": "pulse-laser-volley-sequence",
282
+ "dir": "pulse-laser-volley-sequence",
283
+ "nameZh": "pulse-laser-volley-sequence",
284
+ "kind": "code",
285
+ "category": null,
286
+ "tags": [],
287
+ "media": {},
288
+ "path": "packages/effects/pulse-laser-volley-sequence"
289
+ },
280
290
  {
281
291
  "id": "resource-collect-sparkles",
282
292
  "dir": "resource-collect-sparkles",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fps-games/vfx",
3
- "version": "0.3.15",
3
+ "version": "0.3.17",
4
4
  "type": "module",
5
5
  "description": "Shared VFX library for Playable projects: engine-injected effect packages + runtime. Project-pure (no imports of host project code).",
6
6
  "publishConfig": {
@@ -23,6 +23,8 @@
23
23
  "flowIntensity": 1.15,
24
24
  "marbleContrast": 0.67,
25
25
  "bloomIntensity": 0.45,
26
+ "bloomKernelSize": 36,
27
+ "showGlowLayer": true,
26
28
  "haloScale": 1.018,
27
29
  "showHalo": false,
28
30
  "segments": 64
@@ -0,0 +1,5 @@
1
+ {
2
+ "schemaVersion": "vfx-params/1.0",
3
+ "effectId": "pulse-laser-volley-sequence",
4
+ "params": {}
5
+ }