@fps-games/vfx 0.3.5 → 0.3.7
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 +18 -0
- package/dist/effects/building-upgrade/index.d.ts +2 -5
- package/dist/effects/building-upgrade/index.js +77 -51
- package/dist/effects/debris-smoke/index.d.ts +8 -0
- package/dist/effects/debris-smoke/index.js +37 -2
- package/dist/effects/energy-shield/index.d.ts +2 -3
- package/dist/effects/energy-shield/index.js +144 -39
- package/dist/effects/treasure-attention/index.d.ts +1 -0
- package/dist/effects/treasure-attention/index.js +5 -3
- package/dist/effects/weapon-trail/index.d.ts +1 -0
- package/dist/effects/weapon-trail/index.js +62 -9
- package/dist/index.js +1 -0
- package/dist/types.d.ts +3 -1
- package/package.json +1 -1
- package/packages/effects/building-upgrade/vfx-params.json +1 -5
- package/packages/effects/energy-shield/vfx-params.json +2 -1
- package/packages/effects/treasure-attention/vfx-params.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,24 @@
|
|
|
12
12
|
|
|
13
13
|
<!-- release.mjs 在此行下方插入新版本区块 -->
|
|
14
14
|
|
|
15
|
+
## v0.3.7 — 2026-07-02
|
|
16
|
+
|
|
17
|
+
### 改动
|
|
18
|
+
- feat(effects): update trail debris shield and building upgrade controls
|
|
19
|
+
|
|
20
|
+
### 特效
|
|
21
|
+
- 无新增 / 删除(仍 37 个)。
|
|
22
|
+
|
|
23
|
+
## v0.3.6 — 2026-07-01
|
|
24
|
+
|
|
25
|
+
### 改动
|
|
26
|
+
- perf(energy-shield): 去掉 per-instance GlowLayer,改用 shader halo shell,并默认关闭外层壳。
|
|
27
|
+
- perf(treasure-attention): 默认不创建 per-instance GlowLayer,保留 `showGlowLayer` 调试开关用于后续 AB。
|
|
28
|
+
- feat(energy-shield/treasure-attention): 补充 boolean 调试开关,方便在 demo/debug panel 中临时切换。
|
|
29
|
+
|
|
30
|
+
### 特效
|
|
31
|
+
- 无新增 / 删除(仍 37 个)。
|
|
32
|
+
|
|
15
33
|
## v0.3.5 — 2026-07-01
|
|
16
34
|
|
|
17
35
|
### 改动
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { VfxColorValue, VfxEffectPackage, VfxParamValues } from '../../types';
|
|
2
|
+
type BuildingUpgradeFootprintShape = 'rectangle' | 'circle';
|
|
2
3
|
interface BuildingUpgradeParams extends VfxParamValues {
|
|
3
4
|
lifetime: number;
|
|
4
|
-
scale: number;
|
|
5
|
-
initialHeight: number;
|
|
6
5
|
color: VfxColorValue;
|
|
7
6
|
opacity: number;
|
|
8
|
-
|
|
9
|
-
scaleY: number;
|
|
10
|
-
scaleZ: number;
|
|
7
|
+
footprintShape: BuildingUpgradeFootprintShape;
|
|
11
8
|
centerCount: number;
|
|
12
9
|
outlineCount: number;
|
|
13
10
|
strandWidth: number;
|
|
@@ -15,18 +15,15 @@ const manifest = {
|
|
|
15
15
|
};
|
|
16
16
|
const BASE_FOOTPRINT_X = 3.2;
|
|
17
17
|
const BASE_FOOTPRINT_Z = 2.6;
|
|
18
|
+
const BASE_CIRCLE_RADIUS = 1.4;
|
|
18
19
|
const BASE_CENTER_HEIGHT = 2.2;
|
|
19
20
|
const BASE_OUTLINE_HEIGHT = 3.1;
|
|
20
21
|
const BASE_GLOW_THICKNESS = 0.38;
|
|
21
22
|
const DEFAULT_PARAMS = {
|
|
22
23
|
lifetime: 1.8,
|
|
23
|
-
scale: 1,
|
|
24
|
-
initialHeight: 0.02,
|
|
25
24
|
color: { r: 1, g: 0.84, b: 0.12 },
|
|
26
25
|
opacity: 1,
|
|
27
|
-
|
|
28
|
-
scaleY: 1,
|
|
29
|
-
scaleZ: 1,
|
|
26
|
+
footprintShape: "rectangle",
|
|
30
27
|
centerCount: 46,
|
|
31
28
|
outlineCount: 150,
|
|
32
29
|
strandWidth: 0.035,
|
|
@@ -43,13 +40,12 @@ const buildingUpgradeEffectPackage = {
|
|
|
43
40
|
debugAnchor: "playerRight",
|
|
44
41
|
debugParams: [
|
|
45
42
|
{ key: "lifetime", label: "生命周期", kind: "lifetime", min: 0.2, max: 6, step: 0.01 },
|
|
46
|
-
{ key: "scale", label: "整体缩放", kind: "scale", min: 0.1, max: 6, step: 0.01 },
|
|
47
|
-
{ key: "initialHeight", label: "初始高度", kind: "number", min: -1, max: 3, step: 0.01 },
|
|
48
43
|
{ key: "color", label: "金光颜色", kind: "color" },
|
|
49
44
|
{ key: "opacity", label: "透明度", kind: "opacity", min: 0, max: 1, step: 0.01 },
|
|
50
|
-
{ key: "
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
{ key: "footprintShape", label: "发射形状", kind: "enum", options: [
|
|
46
|
+
{ value: "rectangle", label: "矩形" },
|
|
47
|
+
{ value: "circle", label: "圆形" }
|
|
48
|
+
] },
|
|
53
49
|
{ key: "centerCount", label: "中心金丝数量", kind: "number", min: 0, max: 240, step: 1 },
|
|
54
50
|
{ key: "outlineCount", label: "轮廓金丝数量", kind: "number", min: 0, max: 420, step: 1 },
|
|
55
51
|
{ key: "strandWidth", label: "金丝粗细", kind: "number", min: 5e-3, max: 0.2, step: 1e-3 },
|
|
@@ -58,14 +54,15 @@ const buildingUpgradeEffectPackage = {
|
|
|
58
54
|
{ key: "outlineThickness", label: "轮廓厚度", kind: "number", min: 0, max: 1, step: 0.01 },
|
|
59
55
|
{ key: "glowOpacity", label: "光芒强度", kind: "opacity", min: 0, max: 1, step: 0.01 }
|
|
60
56
|
],
|
|
61
|
-
create({ scene, root, position, params }) {
|
|
62
|
-
root.position.copyFrom(position)
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
57
|
+
create({ scene, root, position, params, scale }) {
|
|
58
|
+
root.position.copyFrom(position);
|
|
59
|
+
const visualScale = normalizeBuildingUpgradeScale(scale);
|
|
60
|
+
const centerStrands = createStrandSystem(scene, root, params, visualScale, "center");
|
|
61
|
+
const outlineStrands = createStrandSystem(scene, root, params, visualScale, "outline");
|
|
62
|
+
const glow = createOutlineGlow(scene, root, params, visualScale);
|
|
66
63
|
const disposeTimer = window.setTimeout(
|
|
67
64
|
() => disposeAll(),
|
|
68
|
-
Math.max(0.05, params.lifetime + getMaxStrandHeight(
|
|
65
|
+
Math.max(0.05, params.lifetime + getMaxStrandHeight(visualScale) / Math.max(0.1, params.riseSpeed) * 0.18 + 0.45) * 1e3
|
|
69
66
|
);
|
|
70
67
|
let disposed = false;
|
|
71
68
|
centerStrands.start();
|
|
@@ -88,12 +85,12 @@ const buildingUpgradeEffectPackage = {
|
|
|
88
85
|
}
|
|
89
86
|
}
|
|
90
87
|
};
|
|
91
|
-
function createStrandSystem(scene, root, params, mode) {
|
|
88
|
+
function createStrandSystem(scene, root, params, visualScale, mode) {
|
|
92
89
|
const count = Math.max(0, Math.round(mode === "center" ? params.centerCount : params.outlineCount));
|
|
93
90
|
const system = new ParticleSystem(`building-upgrade.${mode}-strands`, Math.max(1, count), scene);
|
|
94
91
|
const color = params.color;
|
|
95
|
-
const height = mode === "center" ? getCenterHeight(
|
|
96
|
-
const width = params.strandWidth *
|
|
92
|
+
const height = mode === "center" ? getCenterHeight(visualScale) : getOutlineHeight(visualScale);
|
|
93
|
+
const width = params.strandWidth * visualScale.horizontal * (mode === "center" ? 1.35 : 0.92);
|
|
97
94
|
configureVfxParticleSystem(system);
|
|
98
95
|
system.particleTexture = getGoldStrandTexture(scene);
|
|
99
96
|
system.emitter = root;
|
|
@@ -110,9 +107,9 @@ function createStrandSystem(scene, root, params, mode) {
|
|
|
110
107
|
system.maxScaleX = Math.max(system.minScaleX, width * 1.8);
|
|
111
108
|
system.minScaleY = 1;
|
|
112
109
|
system.maxScaleY = mode === "center" ? 1.45 : 1.75;
|
|
113
|
-
system.minEmitPower = Math.max(0.01, params.riseSpeed *
|
|
114
|
-
system.maxEmitPower = Math.max(system.minEmitPower, params.riseSpeed *
|
|
115
|
-
system.gravity = new Vector3(0, 0.38
|
|
110
|
+
system.minEmitPower = Math.max(0.01, params.riseSpeed * (mode === "center" ? 0.5 : 0.7));
|
|
111
|
+
system.maxEmitPower = Math.max(system.minEmitPower, params.riseSpeed * (mode === "center" ? 1.15 : 1.65));
|
|
112
|
+
system.gravity = new Vector3(0, 0.38, 0);
|
|
116
113
|
system.blendMode = ParticleSystem.BLENDMODE_ADD;
|
|
117
114
|
system.color1 = toColor4(color, params.opacity * (mode === "center" ? 0.72 : 0.86));
|
|
118
115
|
system.color2 = toColor4({ r: 1, g: 0.98, b: 0.52 }, params.opacity);
|
|
@@ -136,7 +133,7 @@ function createStrandSystem(scene, root, params, mode) {
|
|
|
136
133
|
system.startDirectionFunction = (worldMatrix, directionToUpdate, particle, isLocal) => {
|
|
137
134
|
const index = particle.id + directionIndex;
|
|
138
135
|
directionIndex += 1;
|
|
139
|
-
const sway =
|
|
136
|
+
const sway = mode === "center" ? 0.22 : 0.12;
|
|
140
137
|
const local = new Vector3(
|
|
141
138
|
(sequence01(index, 4) - 0.5) * sway,
|
|
142
139
|
0.8 + sequence01(index, 5) * 0.65,
|
|
@@ -147,11 +144,11 @@ function createStrandSystem(scene, root, params, mode) {
|
|
|
147
144
|
};
|
|
148
145
|
return system;
|
|
149
146
|
}
|
|
150
|
-
function createOutlineGlow(scene, root, params) {
|
|
147
|
+
function createOutlineGlow(scene, root, params, visualScale) {
|
|
151
148
|
const count = Math.max(1, Math.round(params.outlineCount * 0.82));
|
|
152
149
|
const system = new ParticleSystem("building-upgrade.outline-glow-rays", count, scene);
|
|
153
|
-
const height = getOutlineHeight(
|
|
154
|
-
const glowWidth = Math.max(0.04, BASE_GLOW_THICKNESS *
|
|
150
|
+
const height = getOutlineHeight(visualScale) * 1.06;
|
|
151
|
+
const glowWidth = Math.max(0.04, BASE_GLOW_THICKNESS * visualScale.horizontal);
|
|
155
152
|
const alpha = params.glowOpacity * params.opacity;
|
|
156
153
|
configureVfxParticleSystem(system);
|
|
157
154
|
system.particleTexture = getOutlineGlowTexture(scene);
|
|
@@ -169,9 +166,9 @@ function createOutlineGlow(scene, root, params) {
|
|
|
169
166
|
system.maxScaleX = glowWidth * 1.18;
|
|
170
167
|
system.minScaleY = 1.05;
|
|
171
168
|
system.maxScaleY = 1.75;
|
|
172
|
-
system.minEmitPower = Math.max(0.01, params.riseSpeed *
|
|
173
|
-
system.maxEmitPower = Math.max(system.minEmitPower, params.riseSpeed *
|
|
174
|
-
system.gravity = new Vector3(0, 0.3
|
|
169
|
+
system.minEmitPower = Math.max(0.01, params.riseSpeed * 0.58);
|
|
170
|
+
system.maxEmitPower = Math.max(system.minEmitPower, params.riseSpeed * 1.38);
|
|
171
|
+
system.gravity = new Vector3(0, 0.3, 0);
|
|
175
172
|
system.blendMode = ParticleSystem.BLENDMODE_ADD;
|
|
176
173
|
system.color1 = toColor4(params.color, alpha * 0.62);
|
|
177
174
|
system.color2 = toColor4({ r: 1, g: 0.98, b: 0.58 }, alpha * 0.86);
|
|
@@ -195,7 +192,7 @@ function createOutlineGlow(scene, root, params) {
|
|
|
195
192
|
system.startDirectionFunction = (worldMatrix, directionToUpdate, particle, isLocal) => {
|
|
196
193
|
const index = particle.id + directionIndex + 31;
|
|
197
194
|
directionIndex += 1;
|
|
198
|
-
const sway = 0.08
|
|
195
|
+
const sway = 0.08;
|
|
199
196
|
const local = new Vector3(
|
|
200
197
|
(sequence01(index, 4) - 0.5) * sway,
|
|
201
198
|
0.82 + sequence01(index, 5) * 0.58,
|
|
@@ -207,49 +204,78 @@ function createOutlineGlow(scene, root, params) {
|
|
|
207
204
|
return system;
|
|
208
205
|
}
|
|
209
206
|
function sampleCenterPosition(index, params) {
|
|
207
|
+
const shape = getFootprintShape(params);
|
|
208
|
+
if (shape === "circle") return sampleCircleCenterPosition(index, params);
|
|
210
209
|
const angle = sequence01(index, 0) * Math.PI * 2;
|
|
211
210
|
const radius = Math.sqrt(0.16 + sequence01(index, 1) * 0.84);
|
|
212
|
-
const xRadius = getFootprintX(
|
|
213
|
-
const zRadius = getFootprintZ(
|
|
211
|
+
const xRadius = getFootprintX() * 0.5 * params.centerSpread;
|
|
212
|
+
const zRadius = getFootprintZ() * 0.5 * params.centerSpread;
|
|
214
213
|
const jitterX = (sequence01(index, 7) - 0.5) * xRadius * 0.18;
|
|
215
214
|
const jitterZ = (sequence01(index, 8) - 0.5) * zRadius * 0.18;
|
|
216
215
|
const x = Math.cos(angle) * radius * xRadius + jitterX;
|
|
217
216
|
const z = Math.sin(angle) * radius * zRadius + jitterZ;
|
|
218
|
-
const y = sequence01(index, 2) * 0.12
|
|
217
|
+
const y = sequence01(index, 2) * 0.12;
|
|
219
218
|
return new Vector3(x, y, z);
|
|
220
219
|
}
|
|
221
220
|
function sampleOutlinePosition(index, params) {
|
|
222
|
-
const
|
|
223
|
-
|
|
221
|
+
const shape = getFootprintShape(params);
|
|
222
|
+
if (shape === "circle") return sampleCircleOutlinePosition(index, params);
|
|
223
|
+
return sampleRectOutlinePosition(index, getFootprintX(), getFootprintZ(), params.outlineThickness);
|
|
224
|
+
}
|
|
225
|
+
function sampleCircleCenterPosition(index, params) {
|
|
226
|
+
const angle = sequence01(index, 0) * Math.PI * 2;
|
|
227
|
+
const radius = Math.sqrt(sequence01(index, 1)) * BASE_CIRCLE_RADIUS * params.centerSpread;
|
|
228
|
+
const jitterRadius = (sequence01(index, 7) - 0.5) * BASE_CIRCLE_RADIUS * 0.08;
|
|
229
|
+
const r = Math.max(0, radius + jitterRadius);
|
|
230
|
+
const y = sequence01(index, 2) * 0.12;
|
|
231
|
+
return new Vector3(Math.cos(angle) * r, y, Math.sin(angle) * r);
|
|
232
|
+
}
|
|
233
|
+
function sampleCircleOutlinePosition(index, params) {
|
|
234
|
+
const angle = sequence01(index, 0) * Math.PI * 2;
|
|
235
|
+
const jitter = (sequence01(index, 1) - 0.5) * params.outlineThickness;
|
|
236
|
+
const radius = Math.max(1e-3, BASE_CIRCLE_RADIUS + jitter);
|
|
237
|
+
const y = sequence01(index, 2) * 0.16;
|
|
238
|
+
return new Vector3(Math.cos(angle) * radius, y, Math.sin(angle) * radius);
|
|
239
|
+
}
|
|
240
|
+
function sampleRectOutlinePosition(index, width, depth, thickness) {
|
|
224
241
|
const perimeter = Math.max(1e-3, width * 2 + depth * 2);
|
|
225
242
|
const distance = sequence01(index, 0) * perimeter;
|
|
226
|
-
const jitter = (sequence01(index, 1) - 0.5) *
|
|
227
|
-
const y = sequence01(index, 2) * 0.16
|
|
243
|
+
const jitter = (sequence01(index, 1) - 0.5) * thickness;
|
|
244
|
+
const y = sequence01(index, 2) * 0.16;
|
|
228
245
|
if (distance < width) return new Vector3(-width * 0.5 + distance, y, -depth * 0.5 + jitter);
|
|
229
246
|
if (distance < width + depth) return new Vector3(width * 0.5 + jitter, y, -depth * 0.5 + distance - width);
|
|
230
247
|
if (distance < width * 2 + depth) return new Vector3(width * 0.5 - (distance - width - depth), y, depth * 0.5 + jitter);
|
|
231
248
|
return new Vector3(-width * 0.5 + jitter, y, depth * 0.5 - (distance - width * 2 - depth));
|
|
232
249
|
}
|
|
233
|
-
function
|
|
234
|
-
|
|
250
|
+
function getFootprintShape(params) {
|
|
251
|
+
if (params.footprintShape === "circle") return params.footprintShape;
|
|
252
|
+
return "rectangle";
|
|
235
253
|
}
|
|
236
|
-
function
|
|
237
|
-
return
|
|
254
|
+
function getFootprintX() {
|
|
255
|
+
return BASE_FOOTPRINT_X;
|
|
238
256
|
}
|
|
239
|
-
function
|
|
240
|
-
return
|
|
257
|
+
function getFootprintZ() {
|
|
258
|
+
return BASE_FOOTPRINT_Z;
|
|
241
259
|
}
|
|
242
|
-
function
|
|
243
|
-
return
|
|
260
|
+
function getCenterHeight(visualScale) {
|
|
261
|
+
return BASE_CENTER_HEIGHT * visualScale.y;
|
|
244
262
|
}
|
|
245
|
-
function
|
|
246
|
-
return
|
|
263
|
+
function getOutlineHeight(visualScale) {
|
|
264
|
+
return BASE_OUTLINE_HEIGHT * visualScale.y;
|
|
247
265
|
}
|
|
248
|
-
function
|
|
249
|
-
return
|
|
266
|
+
function getMaxStrandHeight(visualScale) {
|
|
267
|
+
return Math.max(getCenterHeight(visualScale), getOutlineHeight(visualScale));
|
|
250
268
|
}
|
|
251
|
-
function
|
|
252
|
-
|
|
269
|
+
function normalizeBuildingUpgradeScale(scale) {
|
|
270
|
+
const x = Math.max(0.05, Math.abs(scale.x));
|
|
271
|
+
const y = Math.max(0.05, Math.abs(scale.y));
|
|
272
|
+
const z = Math.max(0.05, Math.abs(scale.z));
|
|
273
|
+
return {
|
|
274
|
+
x,
|
|
275
|
+
y,
|
|
276
|
+
z,
|
|
277
|
+
horizontal: Math.max(0.05, (x + z) * 0.5)
|
|
278
|
+
};
|
|
253
279
|
}
|
|
254
280
|
function getGoldStrandTexture(scene) {
|
|
255
281
|
const key = "__vfx_building_upgrade_gold_strand_texture";
|
|
@@ -3,6 +3,14 @@ interface DebrisSmokeParams extends VfxParamValues {
|
|
|
3
3
|
debrisCount: number;
|
|
4
4
|
debrisSize: number;
|
|
5
5
|
debrisRadius: number;
|
|
6
|
+
debrisSpeed: number;
|
|
7
|
+
debrisHorizontalSpeed: number;
|
|
8
|
+
debrisUpSpeed: number;
|
|
9
|
+
debrisDirectionX: number;
|
|
10
|
+
debrisDirectionY: number;
|
|
11
|
+
debrisDirectionZ: number;
|
|
12
|
+
debrisGravity: number;
|
|
13
|
+
debrisLifetime: number;
|
|
6
14
|
smokeCount: number;
|
|
7
15
|
smokeSize: number;
|
|
8
16
|
}
|
|
@@ -11,6 +11,14 @@ const DEFAULT_PARAMS = {
|
|
|
11
11
|
debrisCount: 14,
|
|
12
12
|
debrisSize: 0.35,
|
|
13
13
|
debrisRadius: 1.8,
|
|
14
|
+
debrisSpeed: 1,
|
|
15
|
+
debrisHorizontalSpeed: 1,
|
|
16
|
+
debrisUpSpeed: 1,
|
|
17
|
+
debrisDirectionX: 0,
|
|
18
|
+
debrisDirectionY: 0,
|
|
19
|
+
debrisDirectionZ: 0,
|
|
20
|
+
debrisGravity: 7,
|
|
21
|
+
debrisLifetime: 1.2,
|
|
14
22
|
smokeCount: 10,
|
|
15
23
|
smokeSize: 1
|
|
16
24
|
};
|
|
@@ -51,6 +59,14 @@ const debrisSmokeEffectPackage = {
|
|
|
51
59
|
{ key: "debrisCount", label: "碎石·数量", kind: "number", min: 0, max: 30, step: 1 },
|
|
52
60
|
{ key: "debrisSize", label: "碎石·大小", kind: "number", min: 0.1, max: 1, step: 0.01 },
|
|
53
61
|
{ key: "debrisRadius", label: "碎石·范围", kind: "scale", min: 0.5, max: 5, step: 0.1 },
|
|
62
|
+
{ key: "debrisSpeed", label: "碎石·整体速度", kind: "number", min: 0, max: 5, step: 0.05 },
|
|
63
|
+
{ key: "debrisHorizontalSpeed", label: "碎石·横向速度", kind: "number", min: 0, max: 5, step: 0.05 },
|
|
64
|
+
{ key: "debrisUpSpeed", label: "碎石·上抛速度", kind: "number", min: 0, max: 5, step: 0.05 },
|
|
65
|
+
{ key: "debrisDirectionX", label: "碎石·方向 X", kind: "number", min: -1, max: 1, step: 0.01 },
|
|
66
|
+
{ key: "debrisDirectionY", label: "碎石·方向 Y", kind: "number", min: -1, max: 1, step: 0.01 },
|
|
67
|
+
{ key: "debrisDirectionZ", label: "碎石·方向 Z", kind: "number", min: -1, max: 1, step: 0.01 },
|
|
68
|
+
{ key: "debrisGravity", label: "碎石·下落重力", kind: "number", min: 0, max: 20, step: 0.1 },
|
|
69
|
+
{ key: "debrisLifetime", label: "碎石·可见时长", kind: "lifetime", min: 0.2, max: 4, step: 0.05 },
|
|
54
70
|
// 段2 · 烟雾
|
|
55
71
|
{ key: "smokeCount", label: "烟雾·数量", kind: "number", min: 0, max: 40, step: 1 },
|
|
56
72
|
{ key: "smokeSize", label: "烟雾·尺寸", kind: "scale", min: 0.2, max: 3, step: 0.05 }
|
|
@@ -85,12 +101,12 @@ const debrisSmokeEffectPackage = {
|
|
|
85
101
|
d.isPickable = false;
|
|
86
102
|
d.normalizeToUnitCube();
|
|
87
103
|
d.scaling.scaleInPlace(r * params.debrisSize * (0.5 + Math.random() * 0.8));
|
|
88
|
-
pool.spawn(d, position,
|
|
104
|
+
pool.spawn(d, position, makeDebrisVelocity(params), params.debrisGravity, params.debrisLifetime);
|
|
89
105
|
}
|
|
90
106
|
}).catch((e) => {
|
|
91
107
|
console.warn("[VFX] debris-smoke load failed", e);
|
|
92
108
|
});
|
|
93
|
-
const disposeTimer = window.setTimeout(() => disposeAll(), 2200);
|
|
109
|
+
const disposeTimer = window.setTimeout(() => disposeAll(), Math.max(2200, params.debrisLifetime * 1e3 + 1e3));
|
|
94
110
|
function disposeAll() {
|
|
95
111
|
if (disposed) return;
|
|
96
112
|
disposed = true;
|
|
@@ -104,6 +120,25 @@ const debrisSmokeEffectPackage = {
|
|
|
104
120
|
} };
|
|
105
121
|
}
|
|
106
122
|
};
|
|
123
|
+
function makeDebrisVelocity(params) {
|
|
124
|
+
const velocity = new Vector3(
|
|
125
|
+
(Math.random() - 0.5) * 13 * params.debrisHorizontalSpeed,
|
|
126
|
+
(Math.random() * 6.75 + 2.25) * params.debrisUpSpeed,
|
|
127
|
+
(Math.random() - 0.5) * 13 * params.debrisHorizontalSpeed
|
|
128
|
+
);
|
|
129
|
+
const direction = new Vector3(params.debrisDirectionX, params.debrisDirectionY, params.debrisDirectionZ);
|
|
130
|
+
const directionLength = direction.length();
|
|
131
|
+
if (directionLength > 1e-4) {
|
|
132
|
+
direction.scaleInPlace(1 / directionLength);
|
|
133
|
+
const directionSpeed = 6.5 * Math.min(directionLength, 1);
|
|
134
|
+
velocity.addInPlace(new Vector3(
|
|
135
|
+
direction.x * directionSpeed * params.debrisHorizontalSpeed,
|
|
136
|
+
direction.y * directionSpeed * params.debrisUpSpeed,
|
|
137
|
+
direction.z * directionSpeed * params.debrisHorizontalSpeed
|
|
138
|
+
));
|
|
139
|
+
}
|
|
140
|
+
return velocity.scaleInPlace(params.debrisSpeed);
|
|
141
|
+
}
|
|
107
142
|
export {
|
|
108
143
|
debrisSmokeEffectPackage
|
|
109
144
|
};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { GlowLayer } from '@babylonjs/core/Layers/glowLayer';
|
|
2
1
|
import { ShaderMaterial } from '@babylonjs/core/Materials/shaderMaterial';
|
|
3
2
|
import { Vector3 } from '@babylonjs/core/Maths/math.vector';
|
|
4
3
|
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
|
|
@@ -24,7 +23,8 @@ export interface EnergyShieldParams extends VfxParamValues {
|
|
|
24
23
|
flowIntensity: number;
|
|
25
24
|
marbleContrast: number;
|
|
26
25
|
bloomIntensity: number;
|
|
27
|
-
|
|
26
|
+
haloScale: number;
|
|
27
|
+
showHalo: boolean;
|
|
28
28
|
segments: number;
|
|
29
29
|
}
|
|
30
30
|
export interface CreateEnergyShieldOptions {
|
|
@@ -38,7 +38,6 @@ export interface CreateEnergyShieldOptions {
|
|
|
38
38
|
export interface EnergyShieldHandle extends VfxEffectHandle {
|
|
39
39
|
shield: Mesh;
|
|
40
40
|
material: ShaderMaterial;
|
|
41
|
-
glowLayer: GlowLayer | null;
|
|
42
41
|
update(params: Partial<EnergyShieldParams>): void;
|
|
43
42
|
}
|
|
44
43
|
export declare const energyShieldEffectPackage: VfxEffectPackage<EnergyShieldParams>;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Constants } from "@babylonjs/core/Engines/constants";
|
|
2
|
-
import { GlowLayer } from "@babylonjs/core/Layers/glowLayer";
|
|
3
2
|
import { Effect } from "@babylonjs/core/Materials/effect";
|
|
4
3
|
import { ShaderMaterial } from "@babylonjs/core/Materials/shaderMaterial";
|
|
5
4
|
import { Color3 } from "@babylonjs/core/Maths/math.color";
|
|
@@ -8,6 +7,9 @@ import { MeshBuilder } from "@babylonjs/core/Meshes/meshBuilder";
|
|
|
8
7
|
import { TransformNode } from "@babylonjs/core/Meshes/transformNode";
|
|
9
8
|
import { V as VFX_RENDERING_GROUP_ID } from "../../chunks/rendering-CV3nhygX.js";
|
|
10
9
|
const SHADER_NAME = "vfxEnergyShield";
|
|
10
|
+
const HALO_ALPHA_SCALE_MAX = 0.3;
|
|
11
|
+
const HALO_FEATHER_ALPHA_RATIO = 0.72;
|
|
12
|
+
const HALO_FEATHER_SCALE_OFFSET = 0.018;
|
|
11
13
|
const DEFAULT_PARAMS = {
|
|
12
14
|
radius: 3.18,
|
|
13
15
|
verticalScale: 1.02,
|
|
@@ -26,7 +28,8 @@ const DEFAULT_PARAMS = {
|
|
|
26
28
|
flowIntensity: 1.15,
|
|
27
29
|
marbleContrast: 0.67,
|
|
28
30
|
bloomIntensity: 0.45,
|
|
29
|
-
|
|
31
|
+
haloScale: 1.018,
|
|
32
|
+
showHalo: false,
|
|
30
33
|
segments: 64
|
|
31
34
|
};
|
|
32
35
|
const VERTEX_SHADER = `
|
|
@@ -69,6 +72,8 @@ uniform float uFlowScale;
|
|
|
69
72
|
uniform float uFlowSpeed;
|
|
70
73
|
uniform float uFlowIntensity;
|
|
71
74
|
uniform float uMarbleContrast;
|
|
75
|
+
uniform float uHaloAlphaScale;
|
|
76
|
+
uniform float uHaloMode;
|
|
72
77
|
|
|
73
78
|
varying vec3 vObjPos;
|
|
74
79
|
varying vec3 vWorldPos;
|
|
@@ -144,7 +149,8 @@ void main(void) {
|
|
|
144
149
|
vec3 normalW = normalize(vNormalW);
|
|
145
150
|
vec3 viewDir = normalize(cameraPosition - vWorldPos);
|
|
146
151
|
float ndv = saturate(dot(normalW, viewDir));
|
|
147
|
-
float
|
|
152
|
+
float viewRim = saturate(1.0 - ndv);
|
|
153
|
+
float fresnel = pow(viewRim, max(0.05, uFresnelPower)) * uFresnelStrength;
|
|
148
154
|
|
|
149
155
|
float t = uTime * uFlowSpeed;
|
|
150
156
|
vec3 spherePos = normalize(vObjPos + vec3(0.0001)) * max(0.05, uNoiseScale);
|
|
@@ -173,6 +179,29 @@ void main(void) {
|
|
|
173
179
|
alpha += fresnel * uOpacity * 0.58;
|
|
174
180
|
alpha += noiseEdge * uNoiseEdgeIntensity * 0.05;
|
|
175
181
|
|
|
182
|
+
if (uHaloMode > 1.5) {
|
|
183
|
+
float haloRim = pow(viewRim, max(0.28, uFresnelPower * 0.44));
|
|
184
|
+
float featherIn = smoothstep(0.1, 0.46, haloRim);
|
|
185
|
+
float featherOut = 1.0 - smoothstep(0.72, 1.0, haloRim);
|
|
186
|
+
float featherRim = featherIn * featherOut;
|
|
187
|
+
float outerFade = 1.0 - smoothstep(0.88, 1.0, haloRim);
|
|
188
|
+
color = uColor * (0.2 + featherRim * 0.24 + cloud * 0.04);
|
|
189
|
+
alpha = featherRim * 0.58 * outerFade;
|
|
190
|
+
} else if (uHaloMode > 0.5) {
|
|
191
|
+
float haloRim = pow(viewRim, max(0.3, uFresnelPower * 0.48));
|
|
192
|
+
float bodyIn = smoothstep(0.34, 0.68, haloRim);
|
|
193
|
+
float bodyOut = 1.0 - smoothstep(0.72, 1.0, haloRim);
|
|
194
|
+
float bodyRim = bodyIn * bodyOut;
|
|
195
|
+
float featherIn = smoothstep(0.12, 0.44, haloRim);
|
|
196
|
+
float featherOut = 1.0 - smoothstep(0.48, 1.0, haloRim);
|
|
197
|
+
float featherRim = featherIn * featherOut;
|
|
198
|
+
float outerFade = 1.0 - smoothstep(0.86, 1.0, haloRim);
|
|
199
|
+
color = uColor * (0.24 + bodyRim * 0.34 + featherRim * 0.12 + cloud * 0.05);
|
|
200
|
+
alpha = (bodyRim * 0.5 + featherRim * 0.14 + noiseEdge * 0.01) * outerFade;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
alpha *= uHaloAlphaScale;
|
|
204
|
+
|
|
176
205
|
gl_FragColor = vec4(color, saturate(alpha));
|
|
177
206
|
}
|
|
178
207
|
`;
|
|
@@ -205,7 +234,8 @@ const energyShieldEffectPackage = {
|
|
|
205
234
|
{ key: "flowIntensity", label: "流动强度", kind: "number", min: 0, max: 8, step: 0.01 },
|
|
206
235
|
{ key: "marbleContrast", label: "纹路锐度", kind: "number", min: 0.25, max: 4, step: 0.01 },
|
|
207
236
|
{ key: "bloomIntensity", label: "Bloom 强度", kind: "number", min: 0, max: 4, step: 0.01 },
|
|
208
|
-
{ key: "
|
|
237
|
+
{ key: "haloScale", label: "外层光晕缩放", kind: "scale", min: 1, max: 1.08, step: 1e-3 },
|
|
238
|
+
{ key: "showHalo", label: "显示外层光晕", kind: "boolean" }
|
|
209
239
|
],
|
|
210
240
|
create({ scene, root, position, renderingGroupId, params }) {
|
|
211
241
|
return createEnergyShield(scene, { root, position, renderingGroupId, params });
|
|
@@ -239,28 +269,87 @@ function createEnergyShield(scene, options = {}) {
|
|
|
239
269
|
shield.alphaIndex = 20;
|
|
240
270
|
const material = createShieldMaterial(scene);
|
|
241
271
|
shield.material = material;
|
|
242
|
-
let
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
272
|
+
let halo = null;
|
|
273
|
+
let haloMaterial = null;
|
|
274
|
+
let haloFeather = null;
|
|
275
|
+
let haloFeatherMaterial = null;
|
|
276
|
+
const disposeHalo = () => {
|
|
277
|
+
halo?.dispose(false, false);
|
|
278
|
+
haloMaterial?.dispose();
|
|
279
|
+
haloFeather?.dispose(false, false);
|
|
280
|
+
haloFeatherMaterial?.dispose();
|
|
281
|
+
halo = null;
|
|
282
|
+
haloMaterial = null;
|
|
283
|
+
haloFeather = null;
|
|
284
|
+
haloFeatherMaterial = null;
|
|
285
|
+
};
|
|
286
|
+
const ensureHalo = () => {
|
|
287
|
+
if (!params.showHalo || params.bloomIntensity <= 1e-3) {
|
|
288
|
+
disposeHalo();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (!halo || !haloMaterial) {
|
|
292
|
+
halo = MeshBuilder.CreateSphere(
|
|
293
|
+
"vfx_energyShield_halo",
|
|
294
|
+
{
|
|
295
|
+
diameter: 2,
|
|
296
|
+
segments: Math.max(16, Math.round(params.segments)),
|
|
297
|
+
slice: 0.5,
|
|
298
|
+
updatable: false
|
|
299
|
+
},
|
|
300
|
+
scene
|
|
301
|
+
);
|
|
302
|
+
halo.parent = shield;
|
|
303
|
+
halo.isPickable = false;
|
|
304
|
+
halo.alwaysSelectAsActiveMesh = true;
|
|
305
|
+
halo.renderingGroupId = shield.renderingGroupId;
|
|
306
|
+
halo.alphaIndex = shield.alphaIndex + 1;
|
|
307
|
+
halo.scaling.setAll(Math.max(1, params.haloScale));
|
|
308
|
+
haloMaterial = createShieldMaterial(scene, {
|
|
309
|
+
name: "vfx_energyShield_halo_material",
|
|
310
|
+
alphaMode: Constants.ALPHA_ADD
|
|
311
|
+
});
|
|
312
|
+
halo.material = haloMaterial;
|
|
313
|
+
}
|
|
314
|
+
if (!haloFeather || !haloFeatherMaterial) {
|
|
315
|
+
haloFeather = MeshBuilder.CreateSphere(
|
|
316
|
+
"vfx_energyShield_halo_feather",
|
|
317
|
+
{
|
|
318
|
+
diameter: 2,
|
|
319
|
+
segments: Math.max(16, Math.round(params.segments)),
|
|
320
|
+
slice: 0.5,
|
|
321
|
+
updatable: false
|
|
322
|
+
},
|
|
323
|
+
scene
|
|
324
|
+
);
|
|
325
|
+
haloFeather.parent = shield;
|
|
326
|
+
haloFeather.isPickable = false;
|
|
327
|
+
haloFeather.alwaysSelectAsActiveMesh = true;
|
|
328
|
+
haloFeather.renderingGroupId = shield.renderingGroupId;
|
|
329
|
+
haloFeather.alphaIndex = shield.alphaIndex + 2;
|
|
330
|
+
haloFeather.scaling.setAll(getHaloFeatherScale(params));
|
|
331
|
+
haloFeatherMaterial = createShieldMaterial(scene, {
|
|
332
|
+
name: "vfx_energyShield_halo_feather_material",
|
|
333
|
+
alphaMode: Constants.ALPHA_ADD
|
|
334
|
+
});
|
|
335
|
+
haloFeather.material = haloFeatherMaterial;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
ensureHalo();
|
|
339
|
+
applyParams(material, shield, halo, haloMaterial, haloFeather, haloFeatherMaterial, params);
|
|
253
340
|
let elapsed = 0;
|
|
254
341
|
const observer = scene.onBeforeRenderObservable.add(() => {
|
|
255
342
|
elapsed += Math.min(scene.getEngine().getDeltaTime() / 1e3, 0.05);
|
|
256
343
|
material.setFloat("uTime", elapsed);
|
|
344
|
+
haloMaterial?.setFloat("uTime", elapsed);
|
|
345
|
+
haloFeatherMaterial?.setFloat("uTime", elapsed);
|
|
257
346
|
});
|
|
258
347
|
let disposed = false;
|
|
259
348
|
const dispose = () => {
|
|
260
349
|
if (disposed) return;
|
|
261
350
|
disposed = true;
|
|
262
351
|
scene.onBeforeRenderObservable.remove(observer);
|
|
263
|
-
|
|
352
|
+
disposeHalo();
|
|
264
353
|
shield.dispose(false, false);
|
|
265
354
|
material.dispose();
|
|
266
355
|
if (disposeRoot) root.dispose();
|
|
@@ -269,28 +358,17 @@ function createEnergyShield(scene, options = {}) {
|
|
|
269
358
|
root,
|
|
270
359
|
shield,
|
|
271
360
|
material,
|
|
272
|
-
glowLayer,
|
|
273
361
|
update(partial) {
|
|
274
362
|
params = mergeParams(partial, params);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
glowLayer = new GlowLayer(`vfx_energyShield_glow_${shield.uniqueId}`, scene, {
|
|
278
|
-
blurKernelSize: params.bloomKernelSize,
|
|
279
|
-
mainTextureRatio: 0.35
|
|
280
|
-
});
|
|
281
|
-
glowLayer.addIncludedOnlyMesh(shield);
|
|
282
|
-
glowLayer.referenceMeshToUseItsOwnMaterial(shield);
|
|
283
|
-
glowLayer.intensity = params.bloomIntensity;
|
|
284
|
-
applyParams(material, shield, glowLayer, params);
|
|
285
|
-
this.glowLayer = glowLayer;
|
|
286
|
-
}
|
|
363
|
+
ensureHalo();
|
|
364
|
+
applyParams(material, shield, halo, haloMaterial, haloFeather, haloFeatherMaterial, params);
|
|
287
365
|
},
|
|
288
366
|
dispose
|
|
289
367
|
};
|
|
290
368
|
}
|
|
291
|
-
function createShieldMaterial(scene) {
|
|
369
|
+
function createShieldMaterial(scene, options = {}) {
|
|
292
370
|
const material = new ShaderMaterial(
|
|
293
|
-
"vfx_energyShield_material",
|
|
371
|
+
options.name ?? "vfx_energyShield_material",
|
|
294
372
|
scene,
|
|
295
373
|
{ vertex: SHADER_NAME, fragment: SHADER_NAME },
|
|
296
374
|
{
|
|
@@ -313,21 +391,47 @@ function createShieldMaterial(scene) {
|
|
|
313
391
|
"uFlowScale",
|
|
314
392
|
"uFlowSpeed",
|
|
315
393
|
"uFlowIntensity",
|
|
316
|
-
"uMarbleContrast"
|
|
394
|
+
"uMarbleContrast",
|
|
395
|
+
"uHaloAlphaScale",
|
|
396
|
+
"uHaloMode"
|
|
317
397
|
],
|
|
318
398
|
needAlphaBlending: true
|
|
319
399
|
}
|
|
320
400
|
);
|
|
321
|
-
material.alphaMode = Constants.ALPHA_COMBINE;
|
|
401
|
+
material.alphaMode = options.alphaMode ?? Constants.ALPHA_COMBINE;
|
|
322
402
|
material.backFaceCulling = false;
|
|
323
403
|
material.disableDepthWrite = true;
|
|
324
404
|
material.fogEnabled = false;
|
|
325
405
|
return material;
|
|
326
406
|
}
|
|
327
|
-
function applyParams(material, shield,
|
|
407
|
+
function applyParams(material, shield, halo, haloMaterial, haloFeather, haloFeatherMaterial, params) {
|
|
328
408
|
shield.scaling.set(params.radius, params.radius * params.verticalScale, params.radius);
|
|
409
|
+
applyMaterialParams(material, params, params.opacity, 1, 0);
|
|
410
|
+
const haloAlphaScale = Math.min(HALO_ALPHA_SCALE_MAX, params.opacity * Math.min(1, params.bloomIntensity));
|
|
411
|
+
if (halo && haloMaterial) {
|
|
412
|
+
halo.scaling.setAll(Math.max(1, params.haloScale));
|
|
413
|
+
applyMaterialParams(
|
|
414
|
+
haloMaterial,
|
|
415
|
+
params,
|
|
416
|
+
1,
|
|
417
|
+
haloAlphaScale,
|
|
418
|
+
1
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
if (haloFeather && haloFeatherMaterial) {
|
|
422
|
+
haloFeather.scaling.setAll(getHaloFeatherScale(params));
|
|
423
|
+
applyMaterialParams(
|
|
424
|
+
haloFeatherMaterial,
|
|
425
|
+
params,
|
|
426
|
+
1,
|
|
427
|
+
haloAlphaScale * HALO_FEATHER_ALPHA_RATIO,
|
|
428
|
+
2
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function applyMaterialParams(material, params, opacity, haloAlphaScale, haloMode) {
|
|
329
433
|
material.setColor3("uColor", toColor3(params.color));
|
|
330
|
-
material.setFloat("uOpacity",
|
|
434
|
+
material.setFloat("uOpacity", opacity);
|
|
331
435
|
material.setFloat("uFresnelPower", params.fresnelPower);
|
|
332
436
|
material.setFloat("uFresnelStrength", params.fresnelStrength);
|
|
333
437
|
material.setFloat("uRimIntensity", params.rimIntensity);
|
|
@@ -340,10 +444,8 @@ function applyParams(material, shield, glowLayer, params) {
|
|
|
340
444
|
material.setFloat("uFlowSpeed", params.flowSpeed);
|
|
341
445
|
material.setFloat("uFlowIntensity", params.flowIntensity);
|
|
342
446
|
material.setFloat("uMarbleContrast", params.marbleContrast);
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
glowLayer.blurKernelSize = params.bloomKernelSize;
|
|
346
|
-
}
|
|
447
|
+
material.setFloat("uHaloAlphaScale", haloAlphaScale);
|
|
448
|
+
material.setFloat("uHaloMode", haloMode);
|
|
347
449
|
}
|
|
348
450
|
function mergeParams(patch, base = DEFAULT_PARAMS) {
|
|
349
451
|
return {
|
|
@@ -355,6 +457,9 @@ function mergeParams(patch, base = DEFAULT_PARAMS) {
|
|
|
355
457
|
function toColor3(color) {
|
|
356
458
|
return new Color3(color.r, color.g, color.b);
|
|
357
459
|
}
|
|
460
|
+
function getHaloFeatherScale(params) {
|
|
461
|
+
return Math.max(1, params.haloScale + HALO_FEATHER_SCALE_OFFSET);
|
|
462
|
+
}
|
|
358
463
|
function registerShaders() {
|
|
359
464
|
Effect.ShadersStore[`${SHADER_NAME}VertexShader`] = VERTEX_SHADER;
|
|
360
465
|
Effect.ShadersStore[`${SHADER_NAME}FragmentShader`] = FRAGMENT_SHADER;
|
|
@@ -35,7 +35,8 @@ const DEFAULT_PARAMS = {
|
|
|
35
35
|
riseSpeed: 0.72,
|
|
36
36
|
drift: 0.12,
|
|
37
37
|
bloomIntensity: 0.55,
|
|
38
|
-
bloomKernelSize: 32
|
|
38
|
+
bloomKernelSize: 32,
|
|
39
|
+
showGlowLayer: false
|
|
39
40
|
};
|
|
40
41
|
const treasureAttentionEffectPackage = {
|
|
41
42
|
id: "treasure-attention",
|
|
@@ -72,7 +73,8 @@ const treasureAttentionEffectPackage = {
|
|
|
72
73
|
{ key: "riseSpeed", label: "上浮速度", kind: "number", min: 0, max: 2.5, step: 0.01 },
|
|
73
74
|
{ key: "drift", label: "横向漂移", kind: "number", min: 0, max: 1.2, step: 0.01 },
|
|
74
75
|
{ key: "bloomIntensity", label: "Bloom 强度", kind: "number", min: 0, max: 2.5, step: 0.01 },
|
|
75
|
-
{ key: "bloomKernelSize", label: "Bloom 半径", kind: "number", min: 8, max: 96, step: 1 }
|
|
76
|
+
{ key: "bloomKernelSize", label: "Bloom 半径", kind: "number", min: 8, max: 96, step: 1 },
|
|
77
|
+
{ key: "showGlowLayer", label: "显示 GlowLayer", kind: "boolean" }
|
|
76
78
|
],
|
|
77
79
|
create({ scene, root, position, renderingGroupId, params }) {
|
|
78
80
|
return createTreasureAttentionFx(scene, { root, position, renderingGroupId, params, serviceOwnedRoot: true });
|
|
@@ -264,7 +266,7 @@ function createTreasureAttentionFx(scene, options = {}) {
|
|
|
264
266
|
}
|
|
265
267
|
}
|
|
266
268
|
function syncGlowLayer() {
|
|
267
|
-
const shouldGlow = params.bloomIntensity > 0;
|
|
269
|
+
const shouldGlow = params.showGlowLayer && params.bloomIntensity > 0;
|
|
268
270
|
if (!shouldGlow) {
|
|
269
271
|
if (glowLayer) {
|
|
270
272
|
for (const mesh of meshes) removeGlowMesh(mesh);
|
|
@@ -12,6 +12,7 @@ const trailTextureUrl = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP4AAABNC
|
|
|
12
12
|
const DEFAULT_PARAMS = {
|
|
13
13
|
...LIFETIME_DEFAULTS,
|
|
14
14
|
playbackMode: "follow",
|
|
15
|
+
historySpace: "world",
|
|
15
16
|
orientation: "horizontal",
|
|
16
17
|
showGlow: true,
|
|
17
18
|
showOuter: true,
|
|
@@ -101,6 +102,7 @@ const weaponTrailEffectPackage = {
|
|
|
101
102
|
nameZh: "武器拖尾",
|
|
102
103
|
placement: "socket",
|
|
103
104
|
geometrySpace: "world",
|
|
105
|
+
optionalInputs: ["reference"],
|
|
104
106
|
debugAnchor: "player",
|
|
105
107
|
defaultParams: DEFAULT_PARAMS,
|
|
106
108
|
debugParams: [
|
|
@@ -109,6 +111,10 @@ const weaponTrailEffectPackage = {
|
|
|
109
111
|
{ value: "follow", label: "跟随挂点" },
|
|
110
112
|
{ value: "orbit", label: "环绕预览" }
|
|
111
113
|
] },
|
|
114
|
+
{ key: "historySpace", label: "历史空间", kind: "enum", options: [
|
|
115
|
+
{ value: "world", label: "世界坐标" },
|
|
116
|
+
{ value: "reference", label: "参考点相对" }
|
|
117
|
+
] },
|
|
112
118
|
{ key: "orientation", label: "拖尾朝向", kind: "enum", options: [
|
|
113
119
|
{ value: "horizontal", label: "水平面" },
|
|
114
120
|
{ value: "vertical", label: "竖直" }
|
|
@@ -134,7 +140,7 @@ const weaponTrailEffectPackage = {
|
|
|
134
140
|
{ key: "flowSpeed", label: "能量流动速度", kind: "number", min: 0, max: 4, step: 0.05 },
|
|
135
141
|
{ key: "tint", label: "颜色", kind: "color" }
|
|
136
142
|
],
|
|
137
|
-
create({ scene, root, position, params, renderingGroupId }) {
|
|
143
|
+
create({ scene, root, position, params, renderingGroupId, inputs }) {
|
|
138
144
|
root.position.copyFrom(position);
|
|
139
145
|
const rgid = renderingGroupId ?? 0;
|
|
140
146
|
registerShaders();
|
|
@@ -199,6 +205,10 @@ const weaponTrailEffectPackage = {
|
|
|
199
205
|
let outerInit = false, coreInit = false, glowInit = false;
|
|
200
206
|
const cur = sample();
|
|
201
207
|
const mountPos = new Vector3();
|
|
208
|
+
const referencePos = new Vector3();
|
|
209
|
+
const sampleWorldPos = new Vector3();
|
|
210
|
+
let usingReferenceHistory = false;
|
|
211
|
+
let warnedMissingReference = false;
|
|
202
212
|
const lastWidthAxis = new Vector3(0, 0, 1);
|
|
203
213
|
const tmpDir = new Vector3();
|
|
204
214
|
const tailDir = new Vector3(1, 0, 0);
|
|
@@ -208,6 +218,32 @@ const weaponTrailEffectPackage = {
|
|
|
208
218
|
if (params.playbackMode === "follow" || params.playbackMode === "orbit") return params.playbackMode;
|
|
209
219
|
return params.orbitRadius > 0 ? "orbit" : "follow";
|
|
210
220
|
}
|
|
221
|
+
function resolveHistorySpace() {
|
|
222
|
+
return params.historySpace === "reference" ? "reference" : "world";
|
|
223
|
+
}
|
|
224
|
+
function updateReferencePosition() {
|
|
225
|
+
if (resolveHistorySpace() !== "reference") return false;
|
|
226
|
+
const provider = inputs?.reference;
|
|
227
|
+
if (!provider) {
|
|
228
|
+
if (!warnedMissingReference) {
|
|
229
|
+
console.warn('[weapon-trail] historySpace="reference" 需要 inputs.reference; 已回退到世界坐标历史。');
|
|
230
|
+
warnedMissingReference = true;
|
|
231
|
+
}
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
const value = provider();
|
|
235
|
+
if (!value) return false;
|
|
236
|
+
referencePos.copyFrom(value);
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
function clearHistory() {
|
|
240
|
+
rawCount = 0;
|
|
241
|
+
for (let i = 0; i < targetCount; i++) {
|
|
242
|
+
points[i].position.set(0, 0, 0);
|
|
243
|
+
points[i].widthAxis.copyFrom(lastWidthAxis);
|
|
244
|
+
pointPathT[i] = 0;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
211
247
|
function sampleMinDistance() {
|
|
212
248
|
return Math.max(3e-3, Math.min(0.04, params.worldLength / Math.max(16, targetCount * 3)));
|
|
213
249
|
}
|
|
@@ -262,15 +298,24 @@ const weaponTrailEffectPackage = {
|
|
|
262
298
|
rawCount += 1;
|
|
263
299
|
trimRawHistory();
|
|
264
300
|
}
|
|
265
|
-
function sampleEmitter(out) {
|
|
301
|
+
function sampleEmitter(out, useReference) {
|
|
266
302
|
const playbackMode = resolvePlaybackMode();
|
|
267
303
|
const orbitRadius = playbackMode === "orbit" ? params.orbitRadius : 0;
|
|
268
304
|
const c = Math.cos(angle), s = Math.sin(angle);
|
|
269
|
-
|
|
305
|
+
sampleWorldPos.set(
|
|
270
306
|
mountPos.x + c * orbitRadius,
|
|
271
307
|
mountPos.y + params.height,
|
|
272
308
|
mountPos.z + s * orbitRadius
|
|
273
309
|
);
|
|
310
|
+
if (useReference) {
|
|
311
|
+
out.position.set(
|
|
312
|
+
sampleWorldPos.x - referencePos.x,
|
|
313
|
+
sampleWorldPos.y - referencePos.y,
|
|
314
|
+
sampleWorldPos.z - referencePos.z
|
|
315
|
+
);
|
|
316
|
+
} else {
|
|
317
|
+
out.position.copyFrom(sampleWorldPos);
|
|
318
|
+
}
|
|
274
319
|
if (params.orientation === "vertical") {
|
|
275
320
|
out.widthAxis.set(0, 1, 0);
|
|
276
321
|
return;
|
|
@@ -330,7 +375,7 @@ const weaponTrailEffectPackage = {
|
|
|
330
375
|
if (!found) extrapolateTailSample(targetDist, distBefore, t);
|
|
331
376
|
}
|
|
332
377
|
}
|
|
333
|
-
function buildRibbon(mesh, halfWidth, wasInit) {
|
|
378
|
+
function buildRibbon(mesh, halfWidth, wasInit, useReference) {
|
|
334
379
|
positions.length = 0;
|
|
335
380
|
uvs.length = 0;
|
|
336
381
|
colors.length = 0;
|
|
@@ -343,7 +388,10 @@ const weaponTrailEffectPackage = {
|
|
|
343
388
|
const tailAlpha = Math.max(0, 1 - Math.pow(pathT, params.tailFadeExponent) * params.tailFadeStrength);
|
|
344
389
|
const alpha = tailAlpha;
|
|
345
390
|
const ox = s.widthAxis.x * halfWidth, oy = s.widthAxis.y * halfWidth, oz = s.widthAxis.z * halfWidth;
|
|
346
|
-
|
|
391
|
+
const px = useReference ? s.position.x + referencePos.x : s.position.x;
|
|
392
|
+
const py = useReference ? s.position.y + referencePos.y : s.position.y;
|
|
393
|
+
const pz = useReference ? s.position.z + referencePos.z : s.position.z;
|
|
394
|
+
positions.push(px + ox, py + oy, pz + oz, px - ox, py - oy, pz - oz);
|
|
347
395
|
uvs.push(u, 0, u, 1);
|
|
348
396
|
colors.push(1, 1, 1, alpha, 1, 1, 1, alpha);
|
|
349
397
|
if (i < targetCount - 1) {
|
|
@@ -373,13 +421,18 @@ const weaponTrailEffectPackage = {
|
|
|
373
421
|
if (resolvePlaybackMode() === "orbit") angle += params.orbitSpeed * dt;
|
|
374
422
|
root.computeWorldMatrix(true);
|
|
375
423
|
mountPos.copyFrom(root.getAbsolutePosition());
|
|
376
|
-
|
|
424
|
+
const useReference = updateReferencePosition();
|
|
425
|
+
if (useReference !== usingReferenceHistory) {
|
|
426
|
+
usingReferenceHistory = useReference;
|
|
427
|
+
clearHistory();
|
|
428
|
+
}
|
|
429
|
+
sampleEmitter(cur, useReference);
|
|
377
430
|
if (rawCount === 0 || Vector3.Distance(cur.position, raw[0].position) > sampleMinDistance()) insertRawSample(cur);
|
|
378
431
|
resample();
|
|
379
432
|
const halfW = params.width * 0.5;
|
|
380
|
-
if (glow) glowInit = buildRibbon(glow, halfW * 2.2, glowInit);
|
|
381
|
-
if (outer) outerInit = buildRibbon(outer, halfW, outerInit);
|
|
382
|
-
if (core) coreInit = buildRibbon(core, halfW * params.coreWidth, coreInit);
|
|
433
|
+
if (glow) glowInit = buildRibbon(glow, halfW * 2.2, glowInit, useReference);
|
|
434
|
+
if (outer) outerInit = buildRibbon(outer, halfW, outerInit, useReference);
|
|
435
|
+
if (core) coreInit = buildRibbon(core, halfW * params.coreWidth, coreInit, useReference);
|
|
383
436
|
outerMat?.setFloat("uTime", time);
|
|
384
437
|
coreMat?.setFloat("uTime", time);
|
|
385
438
|
glowMat?.setFloat("uTime", time);
|
package/dist/index.js
CHANGED
|
@@ -192,6 +192,7 @@ function normalizeInputs(inputs) {
|
|
|
192
192
|
if (inputs.normal != null) out.normal = asFn(inputs.normal);
|
|
193
193
|
if (inputs.endpoints) out.endpoints = inputs.endpoints.map((e) => asFn(e)).filter(Boolean);
|
|
194
194
|
if (inputs.target) out.target = inputs.target;
|
|
195
|
+
if (inputs.reference != null) out.reference = asFn(inputs.reference);
|
|
195
196
|
if (inputs.scalars) {
|
|
196
197
|
out.scalars = {};
|
|
197
198
|
for (const [k, v] of Object.entries(inputs.scalars)) {
|
package/dist/types.d.ts
CHANGED
|
@@ -4,13 +4,14 @@ import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
|
4
4
|
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
|
|
5
5
|
export type VfxDebugAnchor = 'playerRight' | 'player';
|
|
6
6
|
export type VfxInput<T> = T | (() => T);
|
|
7
|
-
export type VfxInputKey = 'aim' | 'normal' | 'endpoints' | 'target';
|
|
7
|
+
export type VfxInputKey = 'aim' | 'normal' | 'endpoints' | 'target' | 'reference';
|
|
8
8
|
/** 调用方提供的 inputs(放在 VfxSpawnTransform.inputs)。 */
|
|
9
9
|
export interface VfxEffectInputs {
|
|
10
10
|
aim?: VfxInput<Vector3>;
|
|
11
11
|
normal?: VfxInput<Vector3>;
|
|
12
12
|
endpoints?: Array<VfxInput<Vector3>>;
|
|
13
13
|
target?: AbstractMesh;
|
|
14
|
+
reference?: VfxInput<Vector3>;
|
|
14
15
|
/** 运行时标量(throttle/intensity 等),值可静态或 provider;特效每帧读取做活改(G5)。 */
|
|
15
16
|
scalars?: Record<string, VfxInput<number>>;
|
|
16
17
|
}
|
|
@@ -20,6 +21,7 @@ export interface VfxResolvedInputs {
|
|
|
20
21
|
normal?: () => Vector3;
|
|
21
22
|
endpoints?: Array<() => Vector3>;
|
|
22
23
|
target?: AbstractMesh;
|
|
24
|
+
reference?: () => Vector3;
|
|
23
25
|
scalars?: Record<string, () => number>;
|
|
24
26
|
}
|
|
25
27
|
export type VfxParamValue = number | string | boolean | VfxColorValue;
|
package/package.json
CHANGED
|
@@ -3,17 +3,13 @@
|
|
|
3
3
|
"__generation.offsetY": 0,
|
|
4
4
|
"__generation.offsetZ": 0,
|
|
5
5
|
"lifetime": 1,
|
|
6
|
-
"scale": 2,
|
|
7
|
-
"initialHeight": 0.05,
|
|
8
6
|
"color": {
|
|
9
7
|
"r": 1,
|
|
10
8
|
"g": 0.8392156862745098,
|
|
11
9
|
"b": 0.12156862745098039
|
|
12
10
|
},
|
|
13
11
|
"opacity": 1,
|
|
14
|
-
"
|
|
15
|
-
"scaleY": 0.2,
|
|
16
|
-
"scaleZ": 2,
|
|
12
|
+
"footprintShape": "rectangle",
|
|
17
13
|
"centerCount": 100,
|
|
18
14
|
"outlineCount": 300,
|
|
19
15
|
"strandWidth": 0.1,
|