@fps-games/vfx 0.3.8 → 0.3.9

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.9 — 2026-07-03
16
+
17
+ ### 改动
18
+ - feat: add magnet suction effect
19
+
20
+ ### 特效
21
+ - 新增:magnet-suction
22
+
15
23
  ## v0.3.8 — 2026-07-02
16
24
 
17
25
  ### 改动
@@ -0,0 +1,9 @@
1
+ declare const _default: {
2
+ "id": "magnet-suction",
3
+ "nameZh": "磁铁吸入线",
4
+ "assets": [],
5
+ "source": "procedural"
6
+ }
7
+ ;
8
+
9
+ export default _default;
@@ -0,0 +1,35 @@
1
+ import { VfxColorValue, VfxEffectPackage, VfxParamValues } from '../../types';
2
+ interface MagnetSuctionParams extends VfxParamValues {
3
+ lifetimeMode: string;
4
+ lifetime: number;
5
+ scale: number;
6
+ initialHeight: number;
7
+ color: VfxColorValue;
8
+ opacity: number;
9
+ intensity: number;
10
+ forwardOffset: number;
11
+ suctionLength: number;
12
+ startRadius: number;
13
+ spreadY: number;
14
+ endRadius: number;
15
+ mouthPadding: number;
16
+ fadeDistance: number;
17
+ spawnFade: number;
18
+ suctionSpeed: number;
19
+ lineCount: number;
20
+ lineLength: number;
21
+ lineWidth: number;
22
+ lineOpacity: number;
23
+ lineSpread: number;
24
+ lineTwist: number;
25
+ particleCount: number;
26
+ particleSize: number;
27
+ particleSpeed: number;
28
+ particleDrift: number;
29
+ particleOuterOffset: number;
30
+ particleOpacity: number;
31
+ directionX: number;
32
+ directionZ: number;
33
+ }
34
+ export declare const magnetSuctionEffectPackage: VfxEffectPackage<MagnetSuctionParams>;
35
+ export default magnetSuctionEffectPackage;
@@ -0,0 +1,419 @@
1
+ import { Constants } from "@babylonjs/core/Engines/constants";
2
+ import { ShaderStore } from "@babylonjs/core/Engines/shaderStore";
3
+ import { Color3 } from "@babylonjs/core/Maths/math.color";
4
+ import { Vector3 } from "@babylonjs/core/Maths/math.vector";
5
+ import { VertexBuffer } from "@babylonjs/core/Buffers/buffer";
6
+ import { Material } from "@babylonjs/core/Materials/material";
7
+ import { ShaderMaterial } from "@babylonjs/core/Materials/shaderMaterial";
8
+ import { Mesh } from "@babylonjs/core/Meshes/mesh";
9
+ import { VertexData } from "@babylonjs/core/Meshes/mesh.vertexData";
10
+ import { TransformNode } from "@babylonjs/core/Meshes/transformNode";
11
+ import { V as VFX_RENDERING_GROUP_ID } from "../../chunks/rendering-CV3nhygX.js";
12
+ import { s as scheduleLifetime } from "../../chunks/space-shared-BXV1hc0k.js";
13
+ const id = "magnet-suction";
14
+ const nameZh = "磁铁吸入线";
15
+ const assets = [];
16
+ const source = "procedural";
17
+ const manifest = {
18
+ id,
19
+ nameZh,
20
+ assets,
21
+ source
22
+ };
23
+ const DEFAULT_PARAMS = {
24
+ lifetimeMode: "infinite",
25
+ lifetime: 2.5,
26
+ scale: 1.15,
27
+ initialHeight: 0,
28
+ color: { r: 0.82, g: 0.95, b: 1 },
29
+ opacity: 1,
30
+ intensity: 1,
31
+ forwardOffset: 0,
32
+ suctionLength: 2.25,
33
+ startRadius: 0.7,
34
+ spreadY: 0.34,
35
+ endRadius: 0.08,
36
+ mouthPadding: 0.18,
37
+ fadeDistance: 0.58,
38
+ spawnFade: 0.45,
39
+ suctionSpeed: 1.65,
40
+ lineCount: 32,
41
+ lineLength: 0.46,
42
+ lineWidth: 0.045,
43
+ lineOpacity: 1,
44
+ lineSpread: 1,
45
+ lineTwist: 0,
46
+ particleCount: 14,
47
+ particleSize: 0.032,
48
+ particleSpeed: 2.15,
49
+ particleDrift: 0,
50
+ particleOuterOffset: 0,
51
+ particleOpacity: 0.55,
52
+ directionX: 0,
53
+ directionZ: -1
54
+ };
55
+ const magnetSuctionEffectPackage = {
56
+ id: "magnet-suction",
57
+ nameZh: "磁铁吸入线",
58
+ placement: "socket",
59
+ geometrySpace: "local",
60
+ manifest,
61
+ defaultParams: DEFAULT_PARAMS,
62
+ debugAnchor: "playerRight",
63
+ debugParams: [
64
+ { key: "lifetimeMode", label: "生命周期模式", kind: "enum", options: [
65
+ { value: "infinite", label: "无限" },
66
+ { value: "duration", label: "自定义时间" }
67
+ ] },
68
+ { key: "lifetime", label: "自定义时长", kind: "lifetime", min: 0.1, max: 30, step: 0.01 },
69
+ { key: "scale", label: "整体缩放", kind: "scale", min: 0.05, max: 6, step: 0.01 },
70
+ { key: "initialHeight", label: "局部高度", kind: "number", min: -2, max: 4, step: 0.01 },
71
+ { key: "color", label: "吸入颜色", kind: "color" },
72
+ { key: "opacity", label: "整体透明度", kind: "opacity", min: 0, max: 1, step: 0.01 },
73
+ { key: "intensity", label: "强度", kind: "opacity", min: 0, max: 1, step: 0.01 },
74
+ { key: "forwardOffset", label: "入口前偏移", kind: "number", min: -2, max: 4, step: 0.01 },
75
+ { key: "suctionLength", label: "远近长度", kind: "number", min: 0.1, max: 8, step: 0.01 },
76
+ { key: "startRadius", label: "远处横向半径", kind: "number", min: 0.02, max: 5, step: 0.01 },
77
+ { key: "spreadY", label: "远处纵向半径", kind: "number", min: 0.02, max: 4, step: 0.01 },
78
+ { key: "endRadius", label: "近处半径", kind: "number", min: 0, max: 2, step: 0.01 },
79
+ { key: "mouthPadding", label: "入口保护距离", kind: "number", min: 0.01, max: 2, step: 0.01 },
80
+ { key: "fadeDistance", label: "入口淡出距离", kind: "number", min: 0.01, max: 3, step: 0.01 },
81
+ { key: "spawnFade", label: "外侧淡入距离", kind: "number", min: 0.01, max: 3, step: 0.01 },
82
+ { key: "suctionSpeed", label: "线条速度", kind: "number", min: 0, max: 8, step: 0.01 },
83
+ { key: "lineCount", label: "白线数量", kind: "number", min: 0, max: 96, step: 1 },
84
+ { key: "lineLength", label: "白线长度", kind: "number", min: 0.02, max: 2, step: 0.01 },
85
+ { key: "lineWidth", label: "白线尾宽", kind: "number", min: 2e-3, max: 0.18, step: 1e-3 },
86
+ { key: "lineOpacity", label: "白线透明度", kind: "opacity", min: 0, max: 1.5, step: 0.01 },
87
+ { key: "lineSpread", label: "散布缩放", kind: "number", min: 0.1, max: 2, step: 0.01 },
88
+ { key: "particleCount", label: "粒子数量", kind: "number", min: 0, max: 80, step: 1 },
89
+ { key: "particleSize", label: "粒子大小", kind: "number", min: 5e-3, max: 0.3, step: 1e-3 },
90
+ { key: "particleSpeed", label: "粒子速度", kind: "number", min: 0, max: 10, step: 0.01 },
91
+ { key: "particleOpacity", label: "粒子透明度", kind: "opacity", min: 0, max: 1, step: 0.01 },
92
+ { key: "directionX", label: "吸入方向 X", kind: "number", min: -1, max: 1, step: 0.01 },
93
+ { key: "directionZ", label: "吸入方向 Z", kind: "number", min: -1, max: 1, step: 0.01 }
94
+ ],
95
+ create({ scene, root, params, renderingGroupId }) {
96
+ const fieldRoot = new TransformNode("magnet-suction.root", scene);
97
+ fieldRoot.parent = root;
98
+ fieldRoot.position.set(0, params.initialHeight, 0);
99
+ const effect = createMagnetSuctionField(scene, fieldRoot, params, renderingGroupId ?? VFX_RENDERING_GROUP_ID);
100
+ let disposed = false;
101
+ let lifetimeTimer = 0;
102
+ effect.start();
103
+ lifetimeTimer = scheduleLifetime(params, disposeAll);
104
+ return {
105
+ root,
106
+ dispose: disposeAll
107
+ };
108
+ function disposeAll() {
109
+ if (disposed) return;
110
+ disposed = true;
111
+ if (lifetimeTimer) window.clearTimeout(lifetimeTimer);
112
+ effect.dispose();
113
+ fieldRoot.dispose();
114
+ root.dispose();
115
+ }
116
+ }
117
+ };
118
+ function createMagnetSuctionField(scene, root, params, renderingGroupId) {
119
+ const material = createSuctionMaterial(scene);
120
+ const mesh = createSuctionMesh(scene, root, params, material, renderingGroupId);
121
+ let observer = null;
122
+ let disposed = false;
123
+ return { start, dispose };
124
+ function start() {
125
+ if (observer) return;
126
+ observer = scene.onBeforeRenderObservable.add(() => {
127
+ if (disposed) return;
128
+ const deltaSeconds = Math.min(0.05, scene.getEngine().getDeltaTime() / 1e3);
129
+ mesh.update(deltaSeconds);
130
+ });
131
+ }
132
+ function dispose() {
133
+ if (disposed) return;
134
+ disposed = true;
135
+ if (observer) scene.onBeforeRenderObservable.remove(observer);
136
+ observer = null;
137
+ mesh.dispose();
138
+ material.dispose();
139
+ }
140
+ }
141
+ function createSuctionMesh(scene, root, params, material, renderingGroupId) {
142
+ const lineCount = Math.max(0, Math.round(params.lineCount));
143
+ const particleCount = Math.max(0, Math.round(params.particleCount));
144
+ const lineVertexCount = lineCount * 3;
145
+ const particleVertexCount = particleCount * 4;
146
+ const totalVertexCount = lineVertexCount + particleVertexCount;
147
+ const positions = new Float32Array(totalVertexCount * 3);
148
+ const colors = new Float32Array(totalVertexCount * 4);
149
+ const indices = [];
150
+ const lines = [];
151
+ const particles = [];
152
+ let resetIndex = 1;
153
+ for (let index = 0; index < lineCount; index += 1) {
154
+ const base = index * 3;
155
+ indices.push(base, base + 1, base + 2);
156
+ lines.push(resetLine({}, true));
157
+ }
158
+ for (let index = 0; index < particleCount; index += 1) {
159
+ const base = lineVertexCount + index * 4;
160
+ indices.push(base, base + 1, base + 2, base, base + 2, base + 3);
161
+ particles.push(resetParticle({}, true));
162
+ }
163
+ const mesh = new Mesh("magnet-suction.mesh", scene);
164
+ mesh.parent = root;
165
+ mesh.material = material;
166
+ mesh.hasVertexAlpha = true;
167
+ mesh.useVertexColors = true;
168
+ mesh.alwaysSelectAsActiveMesh = true;
169
+ mesh.renderingGroupId = renderingGroupId;
170
+ mesh.isPickable = false;
171
+ const vertexData = new VertexData();
172
+ vertexData.positions = positions;
173
+ vertexData.colors = colors;
174
+ vertexData.indices = indices;
175
+ vertexData.applyToMesh(mesh, true);
176
+ writeGeometry();
177
+ return { update, dispose };
178
+ function update(deltaSeconds) {
179
+ const basis = createSuctionBasis(params);
180
+ for (const line of lines) {
181
+ line.d -= line.speed * deltaSeconds;
182
+ if (line.d < basis.mouthPadding) resetLine(line, false);
183
+ }
184
+ for (const particle of particles) {
185
+ particle.d -= particle.speed * deltaSeconds;
186
+ if (particle.d < basis.mouthPadding) resetParticle(particle, false);
187
+ }
188
+ writeGeometry();
189
+ }
190
+ function dispose() {
191
+ mesh.dispose();
192
+ }
193
+ function writeGeometry() {
194
+ const basis = createSuctionBasis(params);
195
+ const cameraBasis = createCameraBasis(scene, root, basis);
196
+ const color = toColor3(params.color);
197
+ const alphaScale = clamp01(params.opacity) * Math.max(0, params.intensity);
198
+ let vertexIndex = 0;
199
+ const putVertex = (position, alpha, brightness = 1) => {
200
+ const positionOffset = vertexIndex * 3;
201
+ const colorOffset = vertexIndex * 4;
202
+ positions[positionOffset] = position.x;
203
+ positions[positionOffset + 1] = position.y;
204
+ positions[positionOffset + 2] = position.z;
205
+ colors[colorOffset] = clamp01(color.r * brightness);
206
+ colors[colorOffset + 1] = clamp01(color.g * brightness);
207
+ colors[colorOffset + 2] = clamp01(color.b * brightness);
208
+ colors[colorOffset + 3] = clamp01(alpha * alphaScale);
209
+ vertexIndex += 1;
210
+ };
211
+ for (const line of lines) {
212
+ const alpha = lineFade(line.d, basis) * params.lineOpacity * line.alphaMul;
213
+ const headD = Math.max(basis.mouthPadding, line.d);
214
+ const tailD = Math.min(basis.length, line.d + line.len);
215
+ const head = suctionPosition(headD, line.u, line.v, basis);
216
+ const tail = suctionPosition(tailD, line.u, line.v, basis);
217
+ const shrink = Math.max(lateralScale(headD, basis), 0.18);
218
+ const half = cameraBasis.ribbonSide.scale(line.width * shrink * 0.5);
219
+ putVertex(tail.subtract(half), alpha, line.brightness);
220
+ putVertex(tail.add(half), alpha * 0.92, line.brightness);
221
+ putVertex(head, alpha * 0.22, line.brightness);
222
+ }
223
+ for (const particle of particles) {
224
+ const alpha = lineFade(particle.d, basis) * params.particleOpacity * particle.alphaMul;
225
+ const center = suctionPosition(particle.d, particle.u, particle.v, basis);
226
+ const shrink = Math.max(lateralScale(particle.d, basis), 0.22);
227
+ const right = cameraBasis.particleRight.scale(particle.size * shrink);
228
+ const up = cameraBasis.particleUp.scale(particle.size * shrink * 0.7);
229
+ putVertex(center.subtract(right).subtractInPlace(up), alpha, particle.brightness);
230
+ putVertex(center.add(right).subtractInPlace(up), alpha, particle.brightness);
231
+ putVertex(center.add(right).addInPlace(up), alpha * 0.8, particle.brightness);
232
+ putVertex(center.subtract(right).addInPlace(up), alpha * 0.8, particle.brightness);
233
+ }
234
+ mesh.updateVerticesData(VertexBuffer.PositionKind, positions, false, false);
235
+ mesh.updateVerticesData(VertexBuffer.ColorKind, colors, false, false);
236
+ }
237
+ function resetLine(line, initial) {
238
+ const basis = createSuctionBasis(params);
239
+ line.d = initial ? randomRange(basis.mouthPadding + 0.05, basis.length) : basis.length + randomRange(0, basis.spawnFade);
240
+ line.u = randomRange(-basis.spreadX * 0.5, basis.spreadX * 0.5);
241
+ line.v = randomRange(-basis.spreadY * 0.5, basis.spreadY * 0.5);
242
+ line.len = randomRange(basis.lineLength * 0.65, basis.lineLength * 1.35);
243
+ line.width = randomRange(basis.lineWidth * 0.65, basis.lineWidth * 1.35);
244
+ line.speed = Math.max(0, params.suctionSpeed * Math.max(0.01, params.scale)) * randomRange(0.78, 1.25);
245
+ line.alphaMul = randomRange(0.65, 1);
246
+ line.brightness = randomRange(0.82, 1);
247
+ return line;
248
+ }
249
+ function resetParticle(particle, initial) {
250
+ const basis = createSuctionBasis(params);
251
+ particle.d = initial ? randomRange(basis.mouthPadding + 0.05, basis.length) : basis.length + randomRange(0, basis.spawnFade);
252
+ particle.u = randomRange(-basis.spreadX * 0.55, basis.spreadX * 0.55);
253
+ particle.v = randomRange(-basis.spreadY * 0.55, basis.spreadY * 0.55);
254
+ particle.size = randomRange(basis.particleSize * 0.45, basis.particleSize * 1.15);
255
+ particle.speed = Math.max(0, params.particleSpeed * Math.max(0.01, params.scale)) * randomRange(0.75, 1.35);
256
+ particle.alphaMul = randomRange(0.35, 0.9);
257
+ particle.brightness = randomRange(0.75, 1);
258
+ return particle;
259
+ }
260
+ function randomRange(min, max) {
261
+ const id2 = resetIndex;
262
+ resetIndex += 1;
263
+ return lerp(min, max, sequence01(id2, resetIndex % 8));
264
+ }
265
+ }
266
+ function createSuctionBasis(params) {
267
+ const scale = Math.max(0.01, params.scale);
268
+ const axis = new Vector3(params.directionX, 0, params.directionZ);
269
+ if (axis.lengthSquared() < 1e-4) axis.set(0, 0, 1);
270
+ axis.normalize();
271
+ let up = Vector3.Up();
272
+ if (Math.abs(Vector3.Dot(up, axis)) > 0.94) up = Vector3.Forward();
273
+ let right = Vector3.Cross(up, axis);
274
+ if (right.lengthSquared() < 1e-4) right = Vector3.Right();
275
+ right.normalize();
276
+ let realUp = Vector3.Cross(axis, right);
277
+ if (realUp.lengthSquared() < 1e-4) realUp = Vector3.Up();
278
+ realUp.normalize();
279
+ const lineSpread = Math.max(0.1, params.lineSpread);
280
+ const length = Math.max(0.01, params.suctionLength * scale);
281
+ const mouthPadding = Math.max(1e-3, (params.mouthPadding || params.endRadius || 0.12) * scale);
282
+ const outerRadius = Math.max(1e-3, Math.max(params.startRadius, params.spreadY) * lineSpread * scale);
283
+ const endRadius = Math.max(0, params.endRadius * scale);
284
+ return {
285
+ axis,
286
+ right,
287
+ up: realUp,
288
+ mouth: axis.scale(params.forwardOffset * scale),
289
+ length,
290
+ spreadX: Math.max(1e-3, params.startRadius * lineSpread * scale),
291
+ spreadY: Math.max(1e-3, params.spreadY * lineSpread * scale),
292
+ mouthPadding: Math.min(length, mouthPadding),
293
+ fadeDistance: Math.max(1e-3, params.fadeDistance * scale),
294
+ spawnFade: Math.max(1e-3, params.spawnFade * scale),
295
+ endRadiusRatio: clamp01(endRadius / outerRadius),
296
+ lineLength: Math.max(0.01, params.lineLength * scale),
297
+ lineWidth: Math.max(1e-3, params.lineWidth * scale),
298
+ particleSize: Math.max(1e-3, params.particleSize * scale)
299
+ };
300
+ }
301
+ function suctionPosition(distance, u, v, basis) {
302
+ const laneScale = lateralScale(distance, basis);
303
+ const lane = basis.right.scale(u * laneScale).addInPlace(basis.up.scale(v * laneScale));
304
+ return basis.mouth.subtract(basis.axis.scale(distance)).addInPlace(lane);
305
+ }
306
+ function lateralScale(distance, basis) {
307
+ const travel = clamp01((distance - basis.mouthPadding) / Math.max(1e-5, basis.length - basis.mouthPadding));
308
+ const eased = travel * travel;
309
+ return lerp(basis.endRadiusRatio, 1, eased);
310
+ }
311
+ function createCameraBasis(scene, root, basis) {
312
+ let ribbonSide = basis.right.clone();
313
+ let particleRight = basis.right.clone();
314
+ let particleUp = basis.up.clone();
315
+ const camera = scene.activeCamera;
316
+ if (camera) {
317
+ const cameraPosition = camera.globalPosition ?? camera.position;
318
+ if (cameraPosition) {
319
+ const inverseRoot = root.getWorldMatrix().clone();
320
+ inverseRoot.invert();
321
+ const localCameraPosition = Vector3.TransformCoordinates(cameraPosition, inverseRoot);
322
+ const view = localCameraPosition.subtract(basis.mouth);
323
+ if (view.lengthSquared() > 1e-4) {
324
+ view.normalize();
325
+ const side = Vector3.Cross(basis.axis, view);
326
+ if (side.lengthSquared() > 1e-4) {
327
+ side.normalize();
328
+ ribbonSide = side;
329
+ }
330
+ }
331
+ }
332
+ if (camera.getDirection) {
333
+ const inverseRoot = root.getWorldMatrix().clone();
334
+ inverseRoot.invert();
335
+ const worldRight = camera.getDirection(Vector3.Right());
336
+ const worldUp = camera.getDirection(Vector3.Up());
337
+ const localRight = Vector3.TransformNormal(worldRight, inverseRoot);
338
+ const localUp = Vector3.TransformNormal(worldUp, inverseRoot);
339
+ if (localRight.lengthSquared() > 1e-4) {
340
+ localRight.normalize();
341
+ particleRight = localRight;
342
+ }
343
+ if (localUp.lengthSquared() > 1e-4) {
344
+ localUp.normalize();
345
+ particleUp = localUp;
346
+ }
347
+ }
348
+ }
349
+ return { ribbonSide, particleRight, particleUp };
350
+ }
351
+ function createSuctionMaterial(scene) {
352
+ registerSuctionShaders();
353
+ const material = new ShaderMaterial("magnet-suction.material", scene, {
354
+ vertex: "magnetSuction",
355
+ fragment: "magnetSuction"
356
+ }, {
357
+ attributes: ["position", "color"],
358
+ uniforms: ["worldViewProjection"],
359
+ needAlphaBlending: true
360
+ });
361
+ material.backFaceCulling = false;
362
+ material.transparencyMode = Material.MATERIAL_ALPHABLEND;
363
+ material.alphaMode = Constants.ALPHA_COMBINE;
364
+ material.disableDepthWrite = true;
365
+ return material;
366
+ }
367
+ function registerSuctionShaders() {
368
+ if (!ShaderStore.ShadersStore.magnetSuctionVertexShader) {
369
+ ShaderStore.ShadersStore.magnetSuctionVertexShader = `
370
+ precision highp float;
371
+ attribute vec3 position;
372
+ attribute vec4 color;
373
+ uniform mat4 worldViewProjection;
374
+ varying vec4 vColor;
375
+ void main(void) {
376
+ gl_Position = worldViewProjection * vec4(position, 1.0);
377
+ vColor = color;
378
+ }
379
+ `;
380
+ }
381
+ if (!ShaderStore.ShadersStore.magnetSuctionFragmentShader) {
382
+ ShaderStore.ShadersStore.magnetSuctionFragmentShader = `
383
+ precision highp float;
384
+ varying vec4 vColor;
385
+ void main(void) {
386
+ if (vColor.a <= 0.01) discard;
387
+ gl_FragColor = vColor;
388
+ }
389
+ `;
390
+ }
391
+ }
392
+ function lineFade(distance, basis) {
393
+ const mouthFade = smoothstep(basis.mouthPadding, basis.mouthPadding + basis.fadeDistance, distance);
394
+ const clampedDistance = Math.min(distance, basis.length);
395
+ const outerFade = smoothstep(0, basis.spawnFade, basis.length - clampedDistance);
396
+ return mouthFade * outerFade;
397
+ }
398
+ function toColor3(color) {
399
+ return new Color3(clamp01(color.r), clamp01(color.g), clamp01(color.b));
400
+ }
401
+ function lerp(min, max, t) {
402
+ return min + (max - min) * t;
403
+ }
404
+ function clamp01(value) {
405
+ return Math.min(1, Math.max(0, value));
406
+ }
407
+ function smoothstep(edge0, edge1, value) {
408
+ const t = clamp01((value - edge0) / Math.max(1e-5, edge1 - edge0));
409
+ return t * t * (3 - 2 * t);
410
+ }
411
+ function sequence01(index, axis) {
412
+ if (index === 0) return 0.5;
413
+ const strides = [0.618033988749895, 0.414213562373095, 0.732050807568877, 0.324717957244746, 0.754877666246693, 0.569840290998053, 0.438744359656398, 0.280776406404415];
414
+ return (0.5 + index * strides[axis % strides.length] + axis * 0.173205080756888) % 1;
415
+ }
416
+ export {
417
+ magnetSuctionEffectPackage as default,
418
+ magnetSuctionEffectPackage
419
+ };
package/index.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "vfx-index/1.0",
3
- "count": 37,
3
+ "count": 38,
4
4
  "effects": [
5
5
  {
6
6
  "id": "build.particle.lumber_debris_scatter",
@@ -217,6 +217,16 @@
217
217
  "media": {},
218
218
  "path": "packages/effects/lumber-cart-suction"
219
219
  },
220
+ {
221
+ "id": "magnet-suction",
222
+ "dir": "magnet-suction",
223
+ "nameZh": "磁铁吸入线",
224
+ "kind": "code",
225
+ "category": "magnet-suction",
226
+ "tags": [],
227
+ "media": {},
228
+ "path": "packages/effects/magnet-suction"
229
+ },
220
230
  {
221
231
  "id": "monster-hit",
222
232
  "dir": "monster-hit",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fps-games/vfx",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
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,36 @@
1
+ {
2
+ "lifetimeMode": "infinite",
3
+ "lifetime": 2.5,
4
+ "scale": 1.15,
5
+ "initialHeight": 0,
6
+ "color": {
7
+ "r": 0.82,
8
+ "g": 0.95,
9
+ "b": 1
10
+ },
11
+ "opacity": 1,
12
+ "intensity": 1,
13
+ "forwardOffset": 0,
14
+ "suctionLength": 2.25,
15
+ "startRadius": 0.7,
16
+ "spreadY": 0.34,
17
+ "endRadius": 0.08,
18
+ "mouthPadding": 0.18,
19
+ "fadeDistance": 0.58,
20
+ "spawnFade": 0.45,
21
+ "suctionSpeed": 1.65,
22
+ "lineCount": 32,
23
+ "lineLength": 0.46,
24
+ "lineWidth": 0.045,
25
+ "lineOpacity": 1,
26
+ "lineSpread": 1,
27
+ "lineTwist": 0,
28
+ "particleCount": 14,
29
+ "particleSize": 0.032,
30
+ "particleSpeed": 2.15,
31
+ "particleDrift": 0,
32
+ "particleOuterOffset": 0,
33
+ "particleOpacity": 0.55,
34
+ "directionX": 0,
35
+ "directionZ": -1
36
+ }