@fps-games/vfx 0.3.14 → 0.3.16

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
  ### 改动
@@ -5,6 +5,8 @@ interface MagnetSuctionParams extends VfxParamValues {
5
5
  scale: number;
6
6
  initialHeight: number;
7
7
  color: VfxColorValue;
8
+ lineColor: VfxColorValue;
9
+ particleColor: VfxColorValue;
8
10
  opacity: number;
9
11
  intensity: number;
10
12
  forwardOffset: number;
@@ -20,13 +20,14 @@ const manifest = {
20
20
  assets,
21
21
  source
22
22
  };
23
- const BOLT_SEGMENTS = 6;
24
23
  const DEFAULT_PARAMS = {
25
24
  lifetimeMode: "infinite",
26
25
  lifetime: 2.5,
27
26
  scale: 1.15,
28
27
  initialHeight: 0,
29
- color: { r: 0.48, g: 0.9, b: 1 },
28
+ color: { r: 0.82, g: 0.95, b: 1 },
29
+ lineColor: { r: 0.82, g: 0.95, b: 1 },
30
+ particleColor: { r: 0.82, g: 0.95, b: 1 },
30
31
  opacity: 1,
31
32
  intensity: 1,
32
33
  forwardOffset: 0,
@@ -38,18 +39,18 @@ const DEFAULT_PARAMS = {
38
39
  fadeDistance: 0.58,
39
40
  spawnFade: 0.45,
40
41
  suctionSpeed: 1.65,
41
- lineCount: 0,
42
+ lineCount: 32,
42
43
  lineLength: 0.46,
43
- lineWidth: 0.052,
44
+ lineWidth: 0.045,
44
45
  lineOpacity: 1,
45
46
  lineSpread: 1,
46
47
  lineTwist: 0,
47
- particleCount: 5,
48
- particleSize: 0.036,
49
- particleSpeed: 1.65,
48
+ particleCount: 22,
49
+ particleSize: 0.06,
50
+ particleSpeed: 2.15,
50
51
  particleDrift: 0,
51
52
  particleOuterOffset: 0,
52
- particleOpacity: 0.68,
53
+ particleOpacity: 0.85,
53
54
  directionX: 0,
54
55
  directionZ: -1
55
56
  };
@@ -69,7 +70,8 @@ const magnetSuctionEffectPackage = {
69
70
  { key: "lifetime", label: "自定义时长", kind: "lifetime", min: 0.1, max: 30, step: 0.01 },
70
71
  { key: "scale", label: "整体缩放", kind: "scale", min: 0.05, max: 6, step: 0.01 },
71
72
  { key: "initialHeight", label: "局部高度", kind: "number", min: -2, max: 4, step: 0.01 },
72
- { key: "color", label: "吸入颜色", kind: "color" },
73
+ { key: "lineColor", label: "线条颜色", kind: "color" },
74
+ { key: "particleColor", label: "圆点颜色", kind: "color" },
73
75
  { key: "opacity", label: "整体透明度", kind: "opacity", min: 0, max: 1, step: 0.01 },
74
76
  { key: "intensity", label: "强度", kind: "opacity", min: 0, max: 1, step: 0.01 },
75
77
  { key: "forwardOffset", label: "入口前偏移", kind: "number", min: -2, max: 4, step: 0.01 },
@@ -80,14 +82,16 @@ const magnetSuctionEffectPackage = {
80
82
  { key: "mouthPadding", label: "入口保护距离", kind: "number", min: 0.01, max: 2, step: 0.01 },
81
83
  { key: "fadeDistance", label: "入口淡出距离", kind: "number", min: 0.01, max: 3, step: 0.01 },
82
84
  { key: "spawnFade", label: "外侧淡入距离", kind: "number", min: 0.01, max: 3, step: 0.01 },
83
- { key: "lineLength", label: "电弧长度", kind: "number", min: 0.02, max: 2, step: 0.01 },
84
- { key: "lineWidth", label: "电弧宽度", kind: "number", min: 2e-3, max: 0.18, step: 1e-3 },
85
- { key: "lineOpacity", label: "电弧透明度", kind: "opacity", min: 0, max: 1.5, step: 0.01 },
85
+ { key: "suctionSpeed", label: "线条速度", kind: "number", min: 0, max: 8, step: 0.01 },
86
+ { key: "lineCount", label: "白线数量", kind: "number", min: 0, max: 96, step: 1 },
87
+ { key: "lineLength", label: "白线长度", kind: "number", min: 0.02, max: 2, step: 0.01 },
88
+ { key: "lineWidth", label: "白线尾宽", kind: "number", min: 2e-3, max: 0.18, step: 1e-3 },
89
+ { key: "lineOpacity", label: "白线透明度", kind: "opacity", min: 0, max: 1.5, step: 0.01 },
86
90
  { key: "lineSpread", label: "散布缩放", kind: "number", min: 0.1, max: 2, step: 0.01 },
87
- { key: "particleCount", label: "电弧数量", kind: "number", min: 0, max: 24, step: 1 },
88
- { key: "particleSize", label: "电弧抖动大小", kind: "number", min: 5e-3, max: 0.3, step: 1e-3 },
89
- { key: "particleSpeed", label: "电弧吸入速度", kind: "number", min: 0, max: 8, step: 0.01 },
90
- { key: "particleOpacity", label: "电弧闪烁强度", kind: "opacity", min: 0, max: 1, step: 0.01 },
91
+ { key: "particleCount", label: "粒子数量", kind: "number", min: 0, max: 80, step: 1 },
92
+ { key: "particleSize", label: "粒子大小", kind: "number", min: 5e-3, max: 0.3, step: 1e-3 },
93
+ { key: "particleSpeed", label: "粒子速度", kind: "number", min: 0, max: 10, step: 0.01 },
94
+ { key: "particleOpacity", label: "粒子透明度", kind: "opacity", min: 0, max: 1, step: 0.01 },
91
95
  { key: "directionX", label: "吸入方向 X", kind: "number", min: -1, max: 1, step: 0.01 },
92
96
  { key: "directionZ", label: "吸入方向 Z", kind: "number", min: -1, max: 1, step: 0.01 }
93
97
  ],
@@ -138,15 +142,26 @@ function createMagnetSuctionField(scene, root, params, renderingGroupId) {
138
142
  }
139
143
  }
140
144
  function createSuctionMesh(scene, root, params, material, renderingGroupId) {
141
- const boltCount = Math.max(0, Math.round(params.particleCount));
142
- const boltVertexCount = boltCount * BOLT_SEGMENTS * 4;
143
- const totalVertexCount = boltVertexCount;
145
+ const lineCount = Math.max(0, Math.round(params.lineCount));
146
+ const particleCount = Math.max(0, Math.round(params.particleCount));
147
+ const lineVertexCount = lineCount * 3;
148
+ const particleVertexCount = particleCount * 4;
149
+ const totalVertexCount = lineVertexCount + particleVertexCount;
144
150
  const positions = new Float32Array(totalVertexCount * 3);
145
151
  const colors = new Float32Array(totalVertexCount * 4);
146
152
  const indices = [];
147
- let time = 0;
148
- for (let base = 0; base < totalVertexCount; base += 4) {
149
- pushQuadIndices(indices, base);
153
+ const lines = [];
154
+ const particles = [];
155
+ let resetIndex = 1;
156
+ for (let index = 0; index < lineCount; index += 1) {
157
+ const base = index * 3;
158
+ indices.push(base, base + 1, base + 2);
159
+ lines.push(resetLine({}, true));
160
+ }
161
+ for (let index = 0; index < particleCount; index += 1) {
162
+ const base = lineVertexCount + index * 4;
163
+ indices.push(base, base + 1, base + 2, base, base + 2, base + 3);
164
+ particles.push(resetParticle({}, true));
150
165
  }
151
166
  const mesh = new Mesh("magnet-suction.mesh", scene);
152
167
  mesh.parent = root;
@@ -164,7 +179,15 @@ function createSuctionMesh(scene, root, params, material, renderingGroupId) {
164
179
  writeGeometry();
165
180
  return { update, dispose };
166
181
  function update(deltaSeconds) {
167
- time += deltaSeconds;
182
+ const basis = createSuctionBasis(params);
183
+ for (const line of lines) {
184
+ line.d -= line.speed * deltaSeconds;
185
+ if (line.d < basis.mouthPadding) resetLine(line, false);
186
+ }
187
+ for (const particle of particles) {
188
+ particle.d -= particle.speed * deltaSeconds;
189
+ if (particle.d < basis.mouthPadding) resetParticle(particle, false);
190
+ }
168
191
  writeGeometry();
169
192
  }
170
193
  function dispose() {
@@ -173,9 +196,8 @@ function createSuctionMesh(scene, root, params, material, renderingGroupId) {
173
196
  function writeGeometry() {
174
197
  const basis = createSuctionBasis(params);
175
198
  const cameraBasis = createCameraBasis(scene, root, basis);
176
- const suctionColor = toColor3(params.color);
177
- const electric = new Color3(0.82, 0.98, 1);
178
- const boltBlue = new Color3(0.25, 0.62, 1);
199
+ const lineColor = toColor3(params.lineColor ?? params.color);
200
+ const particleColor = toColor3(params.particleColor ?? params.color);
179
201
  const alphaScale = clamp01(params.opacity) * Math.max(0, params.intensity);
180
202
  let vertexIndex = 0;
181
203
  const putVertex = (position, color, alpha, brightness = 1) => {
@@ -190,66 +212,61 @@ function createSuctionMesh(scene, root, params, material, renderingGroupId) {
190
212
  colors[colorOffset + 3] = clamp01(alpha * alphaScale);
191
213
  vertexIndex += 1;
192
214
  };
193
- const putQuad = (a, b, c, d, color, alpha, brightness = 1) => {
194
- putVertex(a, color, alpha, brightness);
195
- putVertex(b, color, alpha, brightness);
196
- putVertex(c, color, alpha, brightness);
197
- putVertex(d, color, alpha, brightness);
198
- };
199
- const putRibbon = (a, b, width, color, alpha, brightness = 1) => {
200
- const segment = b.subtract(a);
201
- let side = Vector3.Cross(segment, basis.axis);
202
- if (side.lengthSquared() < 1e-4) side = cameraBasis.ribbonSide.clone();
203
- side.normalize();
204
- const half = side.scale(width * 0.5);
205
- putQuad(a.subtract(half), a.add(half), b.add(half), b.subtract(half), color, alpha, brightness);
206
- };
207
- writeBolts(basis, electric, boltBlue, putRibbon);
208
- while (vertexIndex < totalVertexCount) {
209
- putVertex(Vector3.Zero(), suctionColor, 0);
215
+ for (const line of lines) {
216
+ const alpha = lineFade(line.d, basis) * params.lineOpacity * line.alphaMul;
217
+ const headD = Math.max(basis.mouthPadding, line.d);
218
+ const tailD = Math.min(basis.length, line.d + line.len);
219
+ const head = suctionPosition(headD, line.u, line.v, basis);
220
+ const tail = suctionPosition(tailD, line.u, line.v, basis);
221
+ const shrink = Math.max(lateralScale(headD, basis), 0.18);
222
+ const half = cameraBasis.ribbonSide.scale(line.width * shrink * 0.5);
223
+ putVertex(tail.subtract(half), lineColor, alpha, line.brightness);
224
+ putVertex(tail.add(half), lineColor, alpha * 0.92, line.brightness);
225
+ putVertex(head, lineColor, alpha * 0.22, line.brightness);
226
+ }
227
+ for (const particle of particles) {
228
+ const fade = lineFade(particle.d, basis);
229
+ const alpha = Math.sqrt(fade) * params.particleOpacity * particle.alphaMul;
230
+ const center = suctionPosition(particle.d, particle.u, particle.v, basis);
231
+ const shrink = Math.max(lateralScale(particle.d, basis), 0.48);
232
+ const right = cameraBasis.particleRight.scale(particle.size * shrink);
233
+ const up = cameraBasis.particleUp.scale(particle.size * shrink * 0.7);
234
+ putVertex(center.subtract(right).subtractInPlace(up), particleColor, alpha, particle.brightness);
235
+ putVertex(center.add(right).subtractInPlace(up), particleColor, alpha, particle.brightness);
236
+ putVertex(center.add(right).addInPlace(up), particleColor, alpha * 0.8, particle.brightness);
237
+ putVertex(center.subtract(right).addInPlace(up), particleColor, alpha * 0.8, particle.brightness);
210
238
  }
211
239
  mesh.updateVerticesData(VertexBuffer.PositionKind, positions, false, false);
212
240
  mesh.updateVerticesData(VertexBuffer.ColorKind, colors, false, false);
213
- function writeBolts(currentBasis, boltColor, edgeColor, writeRibbon) {
214
- for (let bolt = 0; bolt < boltCount; bolt += 1) {
215
- const seed = sequence01(bolt + 7, bolt % 8);
216
- const speedScale = Math.max(0, params.particleSpeed) / DEFAULT_PARAMS.particleSpeed;
217
- const cycle = (time * speedScale * (1.45 + seed * 0.55) + seed) % 1;
218
- const flicker = sequence01(bolt * 17 + Math.floor(time * 20), bolt % 8);
219
- const active = flicker > 0.28 ? 1 : 0.18;
220
- const angle = bolt * 2.399 + time * (1.7 + seed * 0.9);
221
- const lengthScale = Math.max(0.04, params.lineLength / DEFAULT_PARAMS.lineLength);
222
- const boltLength = lerp(currentBasis.length * 0.16, currentBasis.length * 0.28, seed) * lengthScale;
223
- const headDistance = lerp(currentBasis.length * 0.92, currentBasis.mouthPadding * 1.15, cycle);
224
- let previousDistance = Math.min(currentBasis.length, headDistance + boltLength);
225
- let previous = suctionPosition(
226
- previousDistance,
227
- Math.cos(angle) * currentBasis.spreadX * 0.5 * lateralScale(previousDistance, currentBasis),
228
- Math.sin(angle * 1.3) * currentBasis.spreadY * 0.5 * lateralScale(previousDistance, currentBasis),
229
- currentBasis
230
- );
231
- for (let segment = 0; segment < BOLT_SEGMENTS; segment += 1) {
232
- const t = (segment + 1) / BOLT_SEGMENTS;
233
- const distance = Math.max(currentBasis.mouthPadding * 1.08, lerp(previousDistance, headDistance, t));
234
- const suckScale = lateralScale(distance, currentBasis);
235
- const jitter = (sequence01(bolt * 19 + segment + Math.floor(time * 18), segment) - 0.5) * 0.34 * currentBasis.spreadX * suckScale;
236
- const curl = angle + t * (2.6 + seed * 1.8) + Math.sin(time * 4 + bolt) * 0.35;
237
- const next = suctionPosition(
238
- distance,
239
- Math.cos(curl) * currentBasis.spreadX * suckScale * (1 - t * 0.45) + jitter,
240
- Math.sin(curl * 1.25) * currentBasis.spreadY * suckScale * (1 - t * 0.5),
241
- currentBasis
242
- );
243
- const mouthFade = smoothstep(0.04, 0.18, cycle) * (1 - smoothstep(0.88, 1, cycle));
244
- writeRibbon(previous, next, basis.lineWidth * 0.58, segment % 2 === 0 ? boltColor : edgeColor, 0.38 * params.lineOpacity * active * mouthFade, 1.85);
245
- previous = next;
246
- }
247
- }
248
- }
249
241
  }
250
- }
251
- function pushQuadIndices(indices, base) {
252
- indices.push(base, base + 1, base + 2, base, base + 2, base + 3);
242
+ function resetLine(line, initial) {
243
+ const basis = createSuctionBasis(params);
244
+ line.d = initial ? randomRange(basis.mouthPadding + 0.05, basis.length) : basis.length + randomRange(0, basis.spawnFade);
245
+ line.u = randomRange(-basis.spreadX * 0.5, basis.spreadX * 0.5);
246
+ line.v = randomRange(-basis.spreadY * 0.5, basis.spreadY * 0.5);
247
+ line.len = randomRange(basis.lineLength * 0.65, basis.lineLength * 1.35);
248
+ line.width = randomRange(basis.lineWidth * 0.65, basis.lineWidth * 1.35);
249
+ line.speed = Math.max(0, params.suctionSpeed * Math.max(0.01, params.scale)) * randomRange(0.78, 1.25);
250
+ line.alphaMul = randomRange(0.65, 1);
251
+ line.brightness = randomRange(0.82, 1);
252
+ return line;
253
+ }
254
+ function resetParticle(particle, initial) {
255
+ const basis = createSuctionBasis(params);
256
+ particle.d = initial ? randomRange(basis.mouthPadding + 0.05, basis.length) : basis.length + randomRange(0, basis.spawnFade);
257
+ particle.u = randomRange(-basis.spreadX * 0.55, basis.spreadX * 0.55);
258
+ particle.v = randomRange(-basis.spreadY * 0.55, basis.spreadY * 0.55);
259
+ particle.size = randomRange(basis.particleSize * 0.45, basis.particleSize * 1.15);
260
+ particle.speed = Math.max(0, params.particleSpeed * Math.max(0.01, params.scale)) * randomRange(0.75, 1.35);
261
+ particle.alphaMul = randomRange(0.35, 0.9);
262
+ particle.brightness = randomRange(0.75, 1);
263
+ return particle;
264
+ }
265
+ function randomRange(min, max) {
266
+ const id2 = resetIndex;
267
+ resetIndex += 1;
268
+ return lerp(min, max, sequence01(id2, resetIndex % 8));
269
+ }
253
270
  }
254
271
  function createSuctionBasis(params) {
255
272
  const scale = Math.max(0.01, params.scale);
@@ -348,7 +365,7 @@ function createSuctionMaterial(scene) {
348
365
  });
349
366
  material.backFaceCulling = false;
350
367
  material.transparencyMode = Material.MATERIAL_ALPHABLEND;
351
- material.alphaMode = Constants.ALPHA_ADD;
368
+ material.alphaMode = Constants.ALPHA_COMBINE;
352
369
  material.disableDepthWrite = true;
353
370
  return material;
354
371
  }
@@ -377,6 +394,12 @@ function registerSuctionShaders() {
377
394
  `;
378
395
  }
379
396
  }
397
+ function lineFade(distance, basis) {
398
+ const mouthFade = smoothstep(basis.mouthPadding, basis.mouthPadding + basis.fadeDistance, distance);
399
+ const clampedDistance = Math.min(distance, basis.length);
400
+ const outerFade = smoothstep(0, basis.spawnFade, basis.length - clampedDistance);
401
+ return mouthFade * outerFade;
402
+ }
380
403
  function toColor3(color) {
381
404
  return new Color3(clamp01(color.r), clamp01(color.g), clamp01(color.b));
382
405
  }
@@ -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
+ };
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.14",
3
+ "version": "0.3.16",
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": {
@@ -0,0 +1,5 @@
1
+ {
2
+ "schemaVersion": "vfx-params/1.0",
3
+ "effectId": "pulse-laser-volley-sequence",
4
+ "params": {}
5
+ }