@fps-games/vfx 0.3.7 → 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 +16 -0
- package/dist/effects/building-upgrade/index.js +37 -37
- package/dist/effects/magnet-suction/effect.json.d.ts +9 -0
- package/dist/effects/magnet-suction/index.d.ts +35 -0
- package/dist/effects/magnet-suction/index.js +419 -0
- package/dist/effects/wood_crusher_sawdust/index.d.ts +1 -0
- package/dist/effects/wood_cutter_sawdust/index.d.ts +1 -0
- package/dist/index.js +6 -4
- package/dist/types.d.ts +6 -0
- package/index.json +11 -1
- package/package.json +1 -1
- package/packages/effects/magnet-suction/vfx-params.json +36 -0
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,22 @@
|
|
|
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
|
+
|
|
23
|
+
## v0.3.8 — 2026-07-02
|
|
24
|
+
|
|
25
|
+
### 改动
|
|
26
|
+
- fix(building-upgrade): treat offset scale as footprint scale
|
|
27
|
+
|
|
28
|
+
### 特效
|
|
29
|
+
- 无新增 / 删除(仍 37 个)。
|
|
30
|
+
|
|
15
31
|
## v0.3.7 — 2026-07-02
|
|
16
32
|
|
|
17
33
|
### 改动
|
|
@@ -38,6 +38,7 @@ const buildingUpgradeEffectPackage = {
|
|
|
38
38
|
manifest,
|
|
39
39
|
defaultParams: DEFAULT_PARAMS,
|
|
40
40
|
debugAnchor: "playerRight",
|
|
41
|
+
applyRootScale: false,
|
|
41
42
|
debugParams: [
|
|
42
43
|
{ key: "lifetime", label: "生命周期", kind: "lifetime", min: 0.2, max: 6, step: 0.01 },
|
|
43
44
|
{ key: "color", label: "金光颜色", kind: "color" },
|
|
@@ -56,13 +57,13 @@ const buildingUpgradeEffectPackage = {
|
|
|
56
57
|
],
|
|
57
58
|
create({ scene, root, position, params, scale }) {
|
|
58
59
|
root.position.copyFrom(position);
|
|
59
|
-
const
|
|
60
|
-
const centerStrands = createStrandSystem(scene, root, params,
|
|
61
|
-
const outlineStrands = createStrandSystem(scene, root, params,
|
|
62
|
-
const glow = createOutlineGlow(scene, root, params,
|
|
60
|
+
const footprintScale = normalizeBuildingUpgradeScale(scale);
|
|
61
|
+
const centerStrands = createStrandSystem(scene, root, params, footprintScale, "center");
|
|
62
|
+
const outlineStrands = createStrandSystem(scene, root, params, footprintScale, "outline");
|
|
63
|
+
const glow = createOutlineGlow(scene, root, params, footprintScale);
|
|
63
64
|
const disposeTimer = window.setTimeout(
|
|
64
65
|
() => disposeAll(),
|
|
65
|
-
Math.max(0.05, params.lifetime + getMaxStrandHeight(
|
|
66
|
+
Math.max(0.05, params.lifetime + getMaxStrandHeight() / Math.max(0.1, params.riseSpeed) * 0.18 + 0.45) * 1e3
|
|
66
67
|
);
|
|
67
68
|
let disposed = false;
|
|
68
69
|
centerStrands.start();
|
|
@@ -85,12 +86,12 @@ const buildingUpgradeEffectPackage = {
|
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
};
|
|
88
|
-
function createStrandSystem(scene, root, params,
|
|
89
|
+
function createStrandSystem(scene, root, params, footprintScale, mode) {
|
|
89
90
|
const count = Math.max(0, Math.round(mode === "center" ? params.centerCount : params.outlineCount));
|
|
90
91
|
const system = new ParticleSystem(`building-upgrade.${mode}-strands`, Math.max(1, count), scene);
|
|
91
92
|
const color = params.color;
|
|
92
|
-
const height = mode === "center" ? getCenterHeight(
|
|
93
|
-
const width = params.strandWidth *
|
|
93
|
+
const height = mode === "center" ? getCenterHeight() : getOutlineHeight();
|
|
94
|
+
const width = params.strandWidth * (mode === "center" ? 1.35 : 0.92);
|
|
94
95
|
configureVfxParticleSystem(system);
|
|
95
96
|
system.particleTexture = getGoldStrandTexture(scene);
|
|
96
97
|
system.emitter = root;
|
|
@@ -126,7 +127,7 @@ function createStrandSystem(scene, root, params, visualScale, mode) {
|
|
|
126
127
|
system.startPositionFunction = (worldMatrix, positionToUpdate, particle, isLocal) => {
|
|
127
128
|
const index = particle.id + positionIndex;
|
|
128
129
|
positionIndex += 1;
|
|
129
|
-
const local = mode === "center" ? sampleCenterPosition(index, params) : sampleOutlinePosition(index, params);
|
|
130
|
+
const local = mode === "center" ? sampleCenterPosition(index, params, footprintScale) : sampleOutlinePosition(index, params, footprintScale);
|
|
130
131
|
if (isLocal) positionToUpdate.copyFrom(local);
|
|
131
132
|
else Vector3.TransformCoordinatesToRef(local, worldMatrix, positionToUpdate);
|
|
132
133
|
};
|
|
@@ -144,11 +145,11 @@ function createStrandSystem(scene, root, params, visualScale, mode) {
|
|
|
144
145
|
};
|
|
145
146
|
return system;
|
|
146
147
|
}
|
|
147
|
-
function createOutlineGlow(scene, root, params,
|
|
148
|
+
function createOutlineGlow(scene, root, params, footprintScale) {
|
|
148
149
|
const count = Math.max(1, Math.round(params.outlineCount * 0.82));
|
|
149
150
|
const system = new ParticleSystem("building-upgrade.outline-glow-rays", count, scene);
|
|
150
|
-
const height = getOutlineHeight(
|
|
151
|
-
const glowWidth = Math.max(0.04, BASE_GLOW_THICKNESS
|
|
151
|
+
const height = getOutlineHeight() * 1.06;
|
|
152
|
+
const glowWidth = Math.max(0.04, BASE_GLOW_THICKNESS);
|
|
152
153
|
const alpha = params.glowOpacity * params.opacity;
|
|
153
154
|
configureVfxParticleSystem(system);
|
|
154
155
|
system.particleTexture = getOutlineGlowTexture(scene);
|
|
@@ -185,7 +186,7 @@ function createOutlineGlow(scene, root, params, visualScale) {
|
|
|
185
186
|
system.startPositionFunction = (worldMatrix, positionToUpdate, particle, isLocal) => {
|
|
186
187
|
const index = particle.id + positionIndex + 31;
|
|
187
188
|
positionIndex += 1;
|
|
188
|
-
const local = sampleOutlinePosition(index, params);
|
|
189
|
+
const local = sampleOutlinePosition(index, params, footprintScale);
|
|
189
190
|
if (isLocal) positionToUpdate.copyFrom(local);
|
|
190
191
|
else Vector3.TransformCoordinatesToRef(local, worldMatrix, positionToUpdate);
|
|
191
192
|
};
|
|
@@ -203,13 +204,13 @@ function createOutlineGlow(scene, root, params, visualScale) {
|
|
|
203
204
|
};
|
|
204
205
|
return system;
|
|
205
206
|
}
|
|
206
|
-
function sampleCenterPosition(index, params) {
|
|
207
|
+
function sampleCenterPosition(index, params, footprintScale) {
|
|
207
208
|
const shape = getFootprintShape(params);
|
|
208
|
-
if (shape === "circle") return sampleCircleCenterPosition(index, params);
|
|
209
|
+
if (shape === "circle") return sampleCircleCenterPosition(index, params, footprintScale);
|
|
209
210
|
const angle = sequence01(index, 0) * Math.PI * 2;
|
|
210
211
|
const radius = Math.sqrt(0.16 + sequence01(index, 1) * 0.84);
|
|
211
|
-
const xRadius = getFootprintX() * 0.5 * params.centerSpread;
|
|
212
|
-
const zRadius = getFootprintZ() * 0.5 * params.centerSpread;
|
|
212
|
+
const xRadius = getFootprintX(footprintScale) * 0.5 * params.centerSpread;
|
|
213
|
+
const zRadius = getFootprintZ(footprintScale) * 0.5 * params.centerSpread;
|
|
213
214
|
const jitterX = (sequence01(index, 7) - 0.5) * xRadius * 0.18;
|
|
214
215
|
const jitterZ = (sequence01(index, 8) - 0.5) * zRadius * 0.18;
|
|
215
216
|
const x = Math.cos(angle) * radius * xRadius + jitterX;
|
|
@@ -217,23 +218,24 @@ function sampleCenterPosition(index, params) {
|
|
|
217
218
|
const y = sequence01(index, 2) * 0.12;
|
|
218
219
|
return new Vector3(x, y, z);
|
|
219
220
|
}
|
|
220
|
-
function sampleOutlinePosition(index, params) {
|
|
221
|
+
function sampleOutlinePosition(index, params, footprintScale) {
|
|
221
222
|
const shape = getFootprintShape(params);
|
|
222
|
-
if (shape === "circle") return sampleCircleOutlinePosition(index, params);
|
|
223
|
-
return sampleRectOutlinePosition(index, getFootprintX(), getFootprintZ(), params.outlineThickness);
|
|
223
|
+
if (shape === "circle") return sampleCircleOutlinePosition(index, params, footprintScale);
|
|
224
|
+
return sampleRectOutlinePosition(index, getFootprintX(footprintScale), getFootprintZ(footprintScale), params.outlineThickness);
|
|
224
225
|
}
|
|
225
|
-
function sampleCircleCenterPosition(index, params) {
|
|
226
|
+
function sampleCircleCenterPosition(index, params, footprintScale) {
|
|
226
227
|
const angle = sequence01(index, 0) * Math.PI * 2;
|
|
227
|
-
const
|
|
228
|
-
const
|
|
228
|
+
const baseRadius = BASE_CIRCLE_RADIUS * footprintScale.horizontal;
|
|
229
|
+
const radius = Math.sqrt(sequence01(index, 1)) * baseRadius * params.centerSpread;
|
|
230
|
+
const jitterRadius = (sequence01(index, 7) - 0.5) * baseRadius * 0.08;
|
|
229
231
|
const r = Math.max(0, radius + jitterRadius);
|
|
230
232
|
const y = sequence01(index, 2) * 0.12;
|
|
231
233
|
return new Vector3(Math.cos(angle) * r, y, Math.sin(angle) * r);
|
|
232
234
|
}
|
|
233
|
-
function sampleCircleOutlinePosition(index, params) {
|
|
235
|
+
function sampleCircleOutlinePosition(index, params, footprintScale) {
|
|
234
236
|
const angle = sequence01(index, 0) * Math.PI * 2;
|
|
235
237
|
const jitter = (sequence01(index, 1) - 0.5) * params.outlineThickness;
|
|
236
|
-
const radius = Math.max(1e-3, BASE_CIRCLE_RADIUS + jitter);
|
|
238
|
+
const radius = Math.max(1e-3, BASE_CIRCLE_RADIUS * footprintScale.horizontal + jitter);
|
|
237
239
|
const y = sequence01(index, 2) * 0.16;
|
|
238
240
|
return new Vector3(Math.cos(angle) * radius, y, Math.sin(angle) * radius);
|
|
239
241
|
}
|
|
@@ -251,28 +253,26 @@ function getFootprintShape(params) {
|
|
|
251
253
|
if (params.footprintShape === "circle") return params.footprintShape;
|
|
252
254
|
return "rectangle";
|
|
253
255
|
}
|
|
254
|
-
function getFootprintX() {
|
|
255
|
-
return BASE_FOOTPRINT_X;
|
|
256
|
+
function getFootprintX(footprintScale) {
|
|
257
|
+
return BASE_FOOTPRINT_X * footprintScale.x;
|
|
256
258
|
}
|
|
257
|
-
function getFootprintZ() {
|
|
258
|
-
return BASE_FOOTPRINT_Z;
|
|
259
|
+
function getFootprintZ(footprintScale) {
|
|
260
|
+
return BASE_FOOTPRINT_Z * footprintScale.z;
|
|
259
261
|
}
|
|
260
|
-
function getCenterHeight(
|
|
261
|
-
return BASE_CENTER_HEIGHT
|
|
262
|
+
function getCenterHeight() {
|
|
263
|
+
return BASE_CENTER_HEIGHT;
|
|
262
264
|
}
|
|
263
|
-
function getOutlineHeight(
|
|
264
|
-
return BASE_OUTLINE_HEIGHT
|
|
265
|
+
function getOutlineHeight() {
|
|
266
|
+
return BASE_OUTLINE_HEIGHT;
|
|
265
267
|
}
|
|
266
|
-
function getMaxStrandHeight(
|
|
267
|
-
return Math.max(getCenterHeight(
|
|
268
|
+
function getMaxStrandHeight() {
|
|
269
|
+
return Math.max(getCenterHeight(), getOutlineHeight());
|
|
268
270
|
}
|
|
269
271
|
function normalizeBuildingUpgradeScale(scale) {
|
|
270
272
|
const x = Math.max(0.05, Math.abs(scale.x));
|
|
271
|
-
const y = Math.max(0.05, Math.abs(scale.y));
|
|
272
273
|
const z = Math.max(0.05, Math.abs(scale.z));
|
|
273
274
|
return {
|
|
274
275
|
x,
|
|
275
|
-
y,
|
|
276
276
|
z,
|
|
277
277
|
horizontal: Math.max(0.05, (x + z) * 0.5)
|
|
278
278
|
};
|
|
@@ -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
|
+
};
|
|
@@ -16,5 +16,6 @@ export declare const woodCrusherSawdustEffectPackage: {
|
|
|
16
16
|
requiredInputs?: import('../..').VfxInputKey[];
|
|
17
17
|
optionalInputs?: import('../..').VfxInputKey[];
|
|
18
18
|
geometrySpace?: "local" | "world";
|
|
19
|
+
applyRootScale?: boolean;
|
|
19
20
|
create(context: import('../..').VfxEffectPackageContext<import('../..').VfxParamValues>): import('../..').VfxEffectHandle;
|
|
20
21
|
};
|
|
@@ -16,5 +16,6 @@ export declare const woodCutterSawdustEffectPackage: {
|
|
|
16
16
|
requiredInputs?: import('../..').VfxInputKey[];
|
|
17
17
|
optionalInputs?: import('../..').VfxInputKey[];
|
|
18
18
|
geometrySpace?: "local" | "world";
|
|
19
|
+
applyRootScale?: boolean;
|
|
19
20
|
create(context: import('../..').VfxEffectPackageContext<import('../..').VfxParamValues>): import('../..').VfxEffectHandle;
|
|
20
21
|
};
|
package/dist/index.js
CHANGED
|
@@ -114,7 +114,8 @@ class EffectPackageService {
|
|
|
114
114
|
warnMissingRequiredInputs(effectId, effectPackage.requiredInputs, spawn.inputs);
|
|
115
115
|
const runtimeParams = stripSpawnParams(params, effectPackage.spawnParams);
|
|
116
116
|
const root = new TransformNode(`vfx_package_${effectId}`, this.scene);
|
|
117
|
-
|
|
117
|
+
const applyRootScale = effectPackage.applyRootScale !== false;
|
|
118
|
+
applySpawnToRoot(root, spawn, applyRootScale);
|
|
118
119
|
const worldPosition = root.getAbsolutePosition().clone();
|
|
119
120
|
let handle;
|
|
120
121
|
try {
|
|
@@ -128,7 +129,7 @@ class EffectPackageService {
|
|
|
128
129
|
scale: spawn.scale,
|
|
129
130
|
inputs: spawn.inputs
|
|
130
131
|
});
|
|
131
|
-
applySpawnToRoot(root, spawn);
|
|
132
|
+
applySpawnToRoot(root, spawn, applyRootScale);
|
|
132
133
|
} catch (error) {
|
|
133
134
|
console.warn(`[EffectPackageService] effect "${effectId}" create() failed; skipped.`, error);
|
|
134
135
|
try {
|
|
@@ -212,10 +213,11 @@ function warnMissingRequiredInputs(effectId, required, inputs) {
|
|
|
212
213
|
console.warn(`[EffectPackageService] effect "${effectId}" 声明 requiredInputs [${missing.join(", ")}] 但未提供 → 可能静默降级。`);
|
|
213
214
|
}
|
|
214
215
|
}
|
|
215
|
-
function applySpawnToRoot(root, spawn) {
|
|
216
|
+
function applySpawnToRoot(root, spawn, applyRootScale = true) {
|
|
216
217
|
if (spawn.parent) root.parent = spawn.parent;
|
|
217
218
|
root.rotation.copyFrom(spawn.rotation);
|
|
218
|
-
root.scaling.copyFrom(spawn.scale);
|
|
219
|
+
if (applyRootScale) root.scaling.copyFrom(spawn.scale);
|
|
220
|
+
else root.scaling.set(1, 1, 1);
|
|
219
221
|
if (spawn.parent && !spawn.offsetIsLocal) {
|
|
220
222
|
root.computeWorldMatrix(true);
|
|
221
223
|
root.setAbsolutePosition(spawn.position);
|
package/dist/types.d.ts
CHANGED
|
@@ -157,6 +157,12 @@ export interface VfxEffectPackage<PParams extends VfxParamValues = VfxParamValue
|
|
|
157
157
|
* 默认随 placement 推断('socket'→local,其余→world)。
|
|
158
158
|
*/
|
|
159
159
|
geometrySpace?: 'local' | 'world';
|
|
160
|
+
/**
|
|
161
|
+
* 是否把 spawnTransform.scale 应用到 root.scaling。默认 true。
|
|
162
|
+
* 对需要自行解释 scale 的特效(例如只把 scale 当作发射 footprint 尺寸)可设为 false,
|
|
163
|
+
* 此时 service 仍会把 scale 传入 create(context.scale),但 root 保持单位缩放。
|
|
164
|
+
*/
|
|
165
|
+
applyRootScale?: boolean;
|
|
160
166
|
/**
|
|
161
167
|
* 构建特效。**约定(G6):root 的 transform(position/rotation/scaling/parent)归 service 独占** ——
|
|
162
168
|
* service 在 create() 后会二次 `applySpawnToRoot` 覆盖,create 内写 `root.position/rotation/scaling` 会被静默清掉。
|
package/index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "vfx-index/1.0",
|
|
3
|
-
"count":
|
|
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
|
@@ -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
|
+
}
|