@fps-games/vfx 0.3.6 → 0.3.8
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.d.ts +2 -5
- package/dist/effects/building-upgrade/index.js +81 -55
- 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.js +96 -34
- package/dist/effects/weapon-trail/index.d.ts +1 -0
- package/dist/effects/weapon-trail/index.js +62 -9
- 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 +7 -4
- package/dist/types.d.ts +9 -1
- package/package.json +1 -1
- package/packages/effects/building-upgrade/vfx-params.json +1 -5
- package/packages/effects/energy-shield/vfx-params.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,22 @@
|
|
|
12
12
|
|
|
13
13
|
<!-- release.mjs 在此行下方插入新版本区块 -->
|
|
14
14
|
|
|
15
|
+
## v0.3.8 — 2026-07-02
|
|
16
|
+
|
|
17
|
+
### 改动
|
|
18
|
+
- fix(building-upgrade): treat offset scale as footprint scale
|
|
19
|
+
|
|
20
|
+
### 特效
|
|
21
|
+
- 无新增 / 删除(仍 37 个)。
|
|
22
|
+
|
|
23
|
+
## v0.3.7 — 2026-07-02
|
|
24
|
+
|
|
25
|
+
### 改动
|
|
26
|
+
- feat(effects): update trail debris shield and building upgrade controls
|
|
27
|
+
|
|
28
|
+
### 特效
|
|
29
|
+
- 无新增 / 删除(仍 37 个)。
|
|
30
|
+
|
|
15
31
|
## v0.3.6 — 2026-07-01
|
|
16
32
|
|
|
17
33
|
### 改动
|
|
@@ -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,
|
|
@@ -41,15 +38,15 @@ const buildingUpgradeEffectPackage = {
|
|
|
41
38
|
manifest,
|
|
42
39
|
defaultParams: DEFAULT_PARAMS,
|
|
43
40
|
debugAnchor: "playerRight",
|
|
41
|
+
applyRootScale: false,
|
|
44
42
|
debugParams: [
|
|
45
43
|
{ 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
44
|
{ key: "color", label: "金光颜色", kind: "color" },
|
|
49
45
|
{ key: "opacity", label: "透明度", kind: "opacity", min: 0, max: 1, step: 0.01 },
|
|
50
|
-
{ key: "
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
{ key: "footprintShape", label: "发射形状", kind: "enum", options: [
|
|
47
|
+
{ value: "rectangle", label: "矩形" },
|
|
48
|
+
{ value: "circle", label: "圆形" }
|
|
49
|
+
] },
|
|
53
50
|
{ key: "centerCount", label: "中心金丝数量", kind: "number", min: 0, max: 240, step: 1 },
|
|
54
51
|
{ key: "outlineCount", label: "轮廓金丝数量", kind: "number", min: 0, max: 420, step: 1 },
|
|
55
52
|
{ key: "strandWidth", label: "金丝粗细", kind: "number", min: 5e-3, max: 0.2, step: 1e-3 },
|
|
@@ -58,14 +55,15 @@ const buildingUpgradeEffectPackage = {
|
|
|
58
55
|
{ key: "outlineThickness", label: "轮廓厚度", kind: "number", min: 0, max: 1, step: 0.01 },
|
|
59
56
|
{ key: "glowOpacity", label: "光芒强度", kind: "opacity", min: 0, max: 1, step: 0.01 }
|
|
60
57
|
],
|
|
61
|
-
create({ scene, root, position, params }) {
|
|
62
|
-
root.position.copyFrom(position)
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
58
|
+
create({ scene, root, position, params, scale }) {
|
|
59
|
+
root.position.copyFrom(position);
|
|
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);
|
|
66
64
|
const disposeTimer = window.setTimeout(
|
|
67
65
|
() => disposeAll(),
|
|
68
|
-
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
|
|
69
67
|
);
|
|
70
68
|
let disposed = false;
|
|
71
69
|
centerStrands.start();
|
|
@@ -88,12 +86,12 @@ const buildingUpgradeEffectPackage = {
|
|
|
88
86
|
}
|
|
89
87
|
}
|
|
90
88
|
};
|
|
91
|
-
function createStrandSystem(scene, root, params, mode) {
|
|
89
|
+
function createStrandSystem(scene, root, params, footprintScale, mode) {
|
|
92
90
|
const count = Math.max(0, Math.round(mode === "center" ? params.centerCount : params.outlineCount));
|
|
93
91
|
const system = new ParticleSystem(`building-upgrade.${mode}-strands`, Math.max(1, count), scene);
|
|
94
92
|
const color = params.color;
|
|
95
|
-
const height = mode === "center" ? getCenterHeight(
|
|
96
|
-
const width = params.strandWidth *
|
|
93
|
+
const height = mode === "center" ? getCenterHeight() : getOutlineHeight();
|
|
94
|
+
const width = params.strandWidth * (mode === "center" ? 1.35 : 0.92);
|
|
97
95
|
configureVfxParticleSystem(system);
|
|
98
96
|
system.particleTexture = getGoldStrandTexture(scene);
|
|
99
97
|
system.emitter = root;
|
|
@@ -110,9 +108,9 @@ function createStrandSystem(scene, root, params, mode) {
|
|
|
110
108
|
system.maxScaleX = Math.max(system.minScaleX, width * 1.8);
|
|
111
109
|
system.minScaleY = 1;
|
|
112
110
|
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
|
|
111
|
+
system.minEmitPower = Math.max(0.01, params.riseSpeed * (mode === "center" ? 0.5 : 0.7));
|
|
112
|
+
system.maxEmitPower = Math.max(system.minEmitPower, params.riseSpeed * (mode === "center" ? 1.15 : 1.65));
|
|
113
|
+
system.gravity = new Vector3(0, 0.38, 0);
|
|
116
114
|
system.blendMode = ParticleSystem.BLENDMODE_ADD;
|
|
117
115
|
system.color1 = toColor4(color, params.opacity * (mode === "center" ? 0.72 : 0.86));
|
|
118
116
|
system.color2 = toColor4({ r: 1, g: 0.98, b: 0.52 }, params.opacity);
|
|
@@ -129,14 +127,14 @@ function createStrandSystem(scene, root, params, mode) {
|
|
|
129
127
|
system.startPositionFunction = (worldMatrix, positionToUpdate, particle, isLocal) => {
|
|
130
128
|
const index = particle.id + positionIndex;
|
|
131
129
|
positionIndex += 1;
|
|
132
|
-
const local = mode === "center" ? sampleCenterPosition(index, params) : sampleOutlinePosition(index, params);
|
|
130
|
+
const local = mode === "center" ? sampleCenterPosition(index, params, footprintScale) : sampleOutlinePosition(index, params, footprintScale);
|
|
133
131
|
if (isLocal) positionToUpdate.copyFrom(local);
|
|
134
132
|
else Vector3.TransformCoordinatesToRef(local, worldMatrix, positionToUpdate);
|
|
135
133
|
};
|
|
136
134
|
system.startDirectionFunction = (worldMatrix, directionToUpdate, particle, isLocal) => {
|
|
137
135
|
const index = particle.id + directionIndex;
|
|
138
136
|
directionIndex += 1;
|
|
139
|
-
const sway =
|
|
137
|
+
const sway = mode === "center" ? 0.22 : 0.12;
|
|
140
138
|
const local = new Vector3(
|
|
141
139
|
(sequence01(index, 4) - 0.5) * sway,
|
|
142
140
|
0.8 + sequence01(index, 5) * 0.65,
|
|
@@ -147,11 +145,11 @@ function createStrandSystem(scene, root, params, mode) {
|
|
|
147
145
|
};
|
|
148
146
|
return system;
|
|
149
147
|
}
|
|
150
|
-
function createOutlineGlow(scene, root, params) {
|
|
148
|
+
function createOutlineGlow(scene, root, params, footprintScale) {
|
|
151
149
|
const count = Math.max(1, Math.round(params.outlineCount * 0.82));
|
|
152
150
|
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
|
|
151
|
+
const height = getOutlineHeight() * 1.06;
|
|
152
|
+
const glowWidth = Math.max(0.04, BASE_GLOW_THICKNESS);
|
|
155
153
|
const alpha = params.glowOpacity * params.opacity;
|
|
156
154
|
configureVfxParticleSystem(system);
|
|
157
155
|
system.particleTexture = getOutlineGlowTexture(scene);
|
|
@@ -169,9 +167,9 @@ function createOutlineGlow(scene, root, params) {
|
|
|
169
167
|
system.maxScaleX = glowWidth * 1.18;
|
|
170
168
|
system.minScaleY = 1.05;
|
|
171
169
|
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
|
|
170
|
+
system.minEmitPower = Math.max(0.01, params.riseSpeed * 0.58);
|
|
171
|
+
system.maxEmitPower = Math.max(system.minEmitPower, params.riseSpeed * 1.38);
|
|
172
|
+
system.gravity = new Vector3(0, 0.3, 0);
|
|
175
173
|
system.blendMode = ParticleSystem.BLENDMODE_ADD;
|
|
176
174
|
system.color1 = toColor4(params.color, alpha * 0.62);
|
|
177
175
|
system.color2 = toColor4({ r: 1, g: 0.98, b: 0.58 }, alpha * 0.86);
|
|
@@ -188,14 +186,14 @@ function createOutlineGlow(scene, root, params) {
|
|
|
188
186
|
system.startPositionFunction = (worldMatrix, positionToUpdate, particle, isLocal) => {
|
|
189
187
|
const index = particle.id + positionIndex + 31;
|
|
190
188
|
positionIndex += 1;
|
|
191
|
-
const local = sampleOutlinePosition(index, params);
|
|
189
|
+
const local = sampleOutlinePosition(index, params, footprintScale);
|
|
192
190
|
if (isLocal) positionToUpdate.copyFrom(local);
|
|
193
191
|
else Vector3.TransformCoordinatesToRef(local, worldMatrix, positionToUpdate);
|
|
194
192
|
};
|
|
195
193
|
system.startDirectionFunction = (worldMatrix, directionToUpdate, particle, isLocal) => {
|
|
196
194
|
const index = particle.id + directionIndex + 31;
|
|
197
195
|
directionIndex += 1;
|
|
198
|
-
const sway = 0.08
|
|
196
|
+
const sway = 0.08;
|
|
199
197
|
const local = new Vector3(
|
|
200
198
|
(sequence01(index, 4) - 0.5) * sway,
|
|
201
199
|
0.82 + sequence01(index, 5) * 0.58,
|
|
@@ -206,50 +204,78 @@ function createOutlineGlow(scene, root, params) {
|
|
|
206
204
|
};
|
|
207
205
|
return system;
|
|
208
206
|
}
|
|
209
|
-
function sampleCenterPosition(index, params) {
|
|
207
|
+
function sampleCenterPosition(index, params, footprintScale) {
|
|
208
|
+
const shape = getFootprintShape(params);
|
|
209
|
+
if (shape === "circle") return sampleCircleCenterPosition(index, params, footprintScale);
|
|
210
210
|
const angle = sequence01(index, 0) * Math.PI * 2;
|
|
211
211
|
const radius = Math.sqrt(0.16 + sequence01(index, 1) * 0.84);
|
|
212
|
-
const xRadius = getFootprintX(
|
|
213
|
-
const zRadius = getFootprintZ(
|
|
212
|
+
const xRadius = getFootprintX(footprintScale) * 0.5 * params.centerSpread;
|
|
213
|
+
const zRadius = getFootprintZ(footprintScale) * 0.5 * params.centerSpread;
|
|
214
214
|
const jitterX = (sequence01(index, 7) - 0.5) * xRadius * 0.18;
|
|
215
215
|
const jitterZ = (sequence01(index, 8) - 0.5) * zRadius * 0.18;
|
|
216
216
|
const x = Math.cos(angle) * radius * xRadius + jitterX;
|
|
217
217
|
const z = Math.sin(angle) * radius * zRadius + jitterZ;
|
|
218
|
-
const y = sequence01(index, 2) * 0.12
|
|
218
|
+
const y = sequence01(index, 2) * 0.12;
|
|
219
219
|
return new Vector3(x, y, z);
|
|
220
220
|
}
|
|
221
|
-
function sampleOutlinePosition(index, params) {
|
|
222
|
-
const
|
|
223
|
-
|
|
221
|
+
function sampleOutlinePosition(index, params, footprintScale) {
|
|
222
|
+
const shape = getFootprintShape(params);
|
|
223
|
+
if (shape === "circle") return sampleCircleOutlinePosition(index, params, footprintScale);
|
|
224
|
+
return sampleRectOutlinePosition(index, getFootprintX(footprintScale), getFootprintZ(footprintScale), params.outlineThickness);
|
|
225
|
+
}
|
|
226
|
+
function sampleCircleCenterPosition(index, params, footprintScale) {
|
|
227
|
+
const angle = sequence01(index, 0) * Math.PI * 2;
|
|
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;
|
|
231
|
+
const r = Math.max(0, radius + jitterRadius);
|
|
232
|
+
const y = sequence01(index, 2) * 0.12;
|
|
233
|
+
return new Vector3(Math.cos(angle) * r, y, Math.sin(angle) * r);
|
|
234
|
+
}
|
|
235
|
+
function sampleCircleOutlinePosition(index, params, footprintScale) {
|
|
236
|
+
const angle = sequence01(index, 0) * Math.PI * 2;
|
|
237
|
+
const jitter = (sequence01(index, 1) - 0.5) * params.outlineThickness;
|
|
238
|
+
const radius = Math.max(1e-3, BASE_CIRCLE_RADIUS * footprintScale.horizontal + jitter);
|
|
239
|
+
const y = sequence01(index, 2) * 0.16;
|
|
240
|
+
return new Vector3(Math.cos(angle) * radius, y, Math.sin(angle) * radius);
|
|
241
|
+
}
|
|
242
|
+
function sampleRectOutlinePosition(index, width, depth, thickness) {
|
|
224
243
|
const perimeter = Math.max(1e-3, width * 2 + depth * 2);
|
|
225
244
|
const distance = sequence01(index, 0) * perimeter;
|
|
226
|
-
const jitter = (sequence01(index, 1) - 0.5) *
|
|
227
|
-
const y = sequence01(index, 2) * 0.16
|
|
245
|
+
const jitter = (sequence01(index, 1) - 0.5) * thickness;
|
|
246
|
+
const y = sequence01(index, 2) * 0.16;
|
|
228
247
|
if (distance < width) return new Vector3(-width * 0.5 + distance, y, -depth * 0.5 + jitter);
|
|
229
248
|
if (distance < width + depth) return new Vector3(width * 0.5 + jitter, y, -depth * 0.5 + distance - width);
|
|
230
249
|
if (distance < width * 2 + depth) return new Vector3(width * 0.5 - (distance - width - depth), y, depth * 0.5 + jitter);
|
|
231
250
|
return new Vector3(-width * 0.5 + jitter, y, depth * 0.5 - (distance - width * 2 - depth));
|
|
232
251
|
}
|
|
233
|
-
function
|
|
234
|
-
|
|
252
|
+
function getFootprintShape(params) {
|
|
253
|
+
if (params.footprintShape === "circle") return params.footprintShape;
|
|
254
|
+
return "rectangle";
|
|
235
255
|
}
|
|
236
|
-
function
|
|
237
|
-
return
|
|
256
|
+
function getFootprintX(footprintScale) {
|
|
257
|
+
return BASE_FOOTPRINT_X * footprintScale.x;
|
|
238
258
|
}
|
|
239
|
-
function
|
|
240
|
-
return
|
|
259
|
+
function getFootprintZ(footprintScale) {
|
|
260
|
+
return BASE_FOOTPRINT_Z * footprintScale.z;
|
|
241
261
|
}
|
|
242
|
-
function
|
|
243
|
-
return
|
|
262
|
+
function getCenterHeight() {
|
|
263
|
+
return BASE_CENTER_HEIGHT;
|
|
244
264
|
}
|
|
245
|
-
function
|
|
246
|
-
return
|
|
265
|
+
function getOutlineHeight() {
|
|
266
|
+
return BASE_OUTLINE_HEIGHT;
|
|
247
267
|
}
|
|
248
|
-
function
|
|
249
|
-
return
|
|
268
|
+
function getMaxStrandHeight() {
|
|
269
|
+
return Math.max(getCenterHeight(), getOutlineHeight());
|
|
250
270
|
}
|
|
251
|
-
function
|
|
252
|
-
|
|
271
|
+
function normalizeBuildingUpgradeScale(scale) {
|
|
272
|
+
const x = Math.max(0.05, Math.abs(scale.x));
|
|
273
|
+
const z = Math.max(0.05, Math.abs(scale.z));
|
|
274
|
+
return {
|
|
275
|
+
x,
|
|
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
|
};
|
|
@@ -7,6 +7,9 @@ import { MeshBuilder } from "@babylonjs/core/Meshes/meshBuilder";
|
|
|
7
7
|
import { TransformNode } from "@babylonjs/core/Meshes/transformNode";
|
|
8
8
|
import { V as VFX_RENDERING_GROUP_ID } from "../../chunks/rendering-CV3nhygX.js";
|
|
9
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;
|
|
10
13
|
const DEFAULT_PARAMS = {
|
|
11
14
|
radius: 3.18,
|
|
12
15
|
verticalScale: 1.02,
|
|
@@ -25,7 +28,7 @@ const DEFAULT_PARAMS = {
|
|
|
25
28
|
flowIntensity: 1.15,
|
|
26
29
|
marbleContrast: 0.67,
|
|
27
30
|
bloomIntensity: 0.45,
|
|
28
|
-
haloScale: 1.
|
|
31
|
+
haloScale: 1.018,
|
|
29
32
|
showHalo: false,
|
|
30
33
|
segments: 64
|
|
31
34
|
};
|
|
@@ -146,7 +149,8 @@ void main(void) {
|
|
|
146
149
|
vec3 normalW = normalize(vNormalW);
|
|
147
150
|
vec3 viewDir = normalize(cameraPosition - vWorldPos);
|
|
148
151
|
float ndv = saturate(dot(normalW, viewDir));
|
|
149
|
-
float
|
|
152
|
+
float viewRim = saturate(1.0 - ndv);
|
|
153
|
+
float fresnel = pow(viewRim, max(0.05, uFresnelPower)) * uFresnelStrength;
|
|
150
154
|
|
|
151
155
|
float t = uTime * uFlowSpeed;
|
|
152
156
|
vec3 spherePos = normalize(vObjPos + vec3(0.0001)) * max(0.05, uNoiseScale);
|
|
@@ -175,12 +179,25 @@ void main(void) {
|
|
|
175
179
|
alpha += fresnel * uOpacity * 0.58;
|
|
176
180
|
alpha += noiseEdge * uNoiseEdgeIntensity * 0.05;
|
|
177
181
|
|
|
178
|
-
if (uHaloMode >
|
|
179
|
-
float haloRim = pow(
|
|
180
|
-
float
|
|
181
|
-
float
|
|
182
|
-
|
|
183
|
-
|
|
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;
|
|
184
201
|
}
|
|
185
202
|
|
|
186
203
|
alpha *= uHaloAlphaScale;
|
|
@@ -254,47 +271,78 @@ function createEnergyShield(scene, options = {}) {
|
|
|
254
271
|
shield.material = material;
|
|
255
272
|
let halo = null;
|
|
256
273
|
let haloMaterial = null;
|
|
274
|
+
let haloFeather = null;
|
|
275
|
+
let haloFeatherMaterial = null;
|
|
257
276
|
const disposeHalo = () => {
|
|
258
277
|
halo?.dispose(false, false);
|
|
259
278
|
haloMaterial?.dispose();
|
|
279
|
+
haloFeather?.dispose(false, false);
|
|
280
|
+
haloFeatherMaterial?.dispose();
|
|
260
281
|
halo = null;
|
|
261
282
|
haloMaterial = null;
|
|
283
|
+
haloFeather = null;
|
|
284
|
+
haloFeatherMaterial = null;
|
|
262
285
|
};
|
|
263
286
|
const ensureHalo = () => {
|
|
264
287
|
if (!params.showHalo || params.bloomIntensity <= 1e-3) {
|
|
265
288
|
disposeHalo();
|
|
266
289
|
return;
|
|
267
290
|
}
|
|
268
|
-
if (halo
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
+
}
|
|
290
337
|
};
|
|
291
338
|
ensureHalo();
|
|
292
|
-
applyParams(material, shield, halo, haloMaterial, params);
|
|
339
|
+
applyParams(material, shield, halo, haloMaterial, haloFeather, haloFeatherMaterial, params);
|
|
293
340
|
let elapsed = 0;
|
|
294
341
|
const observer = scene.onBeforeRenderObservable.add(() => {
|
|
295
342
|
elapsed += Math.min(scene.getEngine().getDeltaTime() / 1e3, 0.05);
|
|
296
343
|
material.setFloat("uTime", elapsed);
|
|
297
344
|
haloMaterial?.setFloat("uTime", elapsed);
|
|
345
|
+
haloFeatherMaterial?.setFloat("uTime", elapsed);
|
|
298
346
|
});
|
|
299
347
|
let disposed = false;
|
|
300
348
|
const dispose = () => {
|
|
@@ -313,7 +361,7 @@ function createEnergyShield(scene, options = {}) {
|
|
|
313
361
|
update(partial) {
|
|
314
362
|
params = mergeParams(partial, params);
|
|
315
363
|
ensureHalo();
|
|
316
|
-
applyParams(material, shield, halo, haloMaterial, params);
|
|
364
|
+
applyParams(material, shield, halo, haloMaterial, haloFeather, haloFeatherMaterial, params);
|
|
317
365
|
},
|
|
318
366
|
dispose
|
|
319
367
|
};
|
|
@@ -356,19 +404,30 @@ function createShieldMaterial(scene, options = {}) {
|
|
|
356
404
|
material.fogEnabled = false;
|
|
357
405
|
return material;
|
|
358
406
|
}
|
|
359
|
-
function applyParams(material, shield, halo, haloMaterial, params) {
|
|
407
|
+
function applyParams(material, shield, halo, haloMaterial, haloFeather, haloFeatherMaterial, params) {
|
|
360
408
|
shield.scaling.set(params.radius, params.radius * params.verticalScale, params.radius);
|
|
361
409
|
applyMaterialParams(material, params, params.opacity, 1, 0);
|
|
410
|
+
const haloAlphaScale = Math.min(HALO_ALPHA_SCALE_MAX, params.opacity * Math.min(1, params.bloomIntensity));
|
|
362
411
|
if (halo && haloMaterial) {
|
|
363
412
|
halo.scaling.setAll(Math.max(1, params.haloScale));
|
|
364
413
|
applyMaterialParams(
|
|
365
414
|
haloMaterial,
|
|
366
415
|
params,
|
|
367
416
|
1,
|
|
368
|
-
|
|
417
|
+
haloAlphaScale,
|
|
369
418
|
1
|
|
370
419
|
);
|
|
371
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
|
+
}
|
|
372
431
|
}
|
|
373
432
|
function applyMaterialParams(material, params, opacity, haloAlphaScale, haloMode) {
|
|
374
433
|
material.setColor3("uColor", toColor3(params.color));
|
|
@@ -398,6 +457,9 @@ function mergeParams(patch, base = DEFAULT_PARAMS) {
|
|
|
398
457
|
function toColor3(color) {
|
|
399
458
|
return new Color3(color.r, color.g, color.b);
|
|
400
459
|
}
|
|
460
|
+
function getHaloFeatherScale(params) {
|
|
461
|
+
return Math.max(1, params.haloScale + HALO_FEATHER_SCALE_OFFSET);
|
|
462
|
+
}
|
|
401
463
|
function registerShaders() {
|
|
402
464
|
Effect.ShadersStore[`${SHADER_NAME}VertexShader`] = VERTEX_SHADER;
|
|
403
465
|
Effect.ShadersStore[`${SHADER_NAME}FragmentShader`] = FRAGMENT_SHADER;
|
|
@@ -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);
|
|
@@ -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 {
|
|
@@ -192,6 +193,7 @@ function normalizeInputs(inputs) {
|
|
|
192
193
|
if (inputs.normal != null) out.normal = asFn(inputs.normal);
|
|
193
194
|
if (inputs.endpoints) out.endpoints = inputs.endpoints.map((e) => asFn(e)).filter(Boolean);
|
|
194
195
|
if (inputs.target) out.target = inputs.target;
|
|
196
|
+
if (inputs.reference != null) out.reference = asFn(inputs.reference);
|
|
195
197
|
if (inputs.scalars) {
|
|
196
198
|
out.scalars = {};
|
|
197
199
|
for (const [k, v] of Object.entries(inputs.scalars)) {
|
|
@@ -211,10 +213,11 @@ function warnMissingRequiredInputs(effectId, required, inputs) {
|
|
|
211
213
|
console.warn(`[EffectPackageService] effect "${effectId}" 声明 requiredInputs [${missing.join(", ")}] 但未提供 → 可能静默降级。`);
|
|
212
214
|
}
|
|
213
215
|
}
|
|
214
|
-
function applySpawnToRoot(root, spawn) {
|
|
216
|
+
function applySpawnToRoot(root, spawn, applyRootScale = true) {
|
|
215
217
|
if (spawn.parent) root.parent = spawn.parent;
|
|
216
218
|
root.rotation.copyFrom(spawn.rotation);
|
|
217
|
-
root.scaling.copyFrom(spawn.scale);
|
|
219
|
+
if (applyRootScale) root.scaling.copyFrom(spawn.scale);
|
|
220
|
+
else root.scaling.set(1, 1, 1);
|
|
218
221
|
if (spawn.parent && !spawn.offsetIsLocal) {
|
|
219
222
|
root.computeWorldMatrix(true);
|
|
220
223
|
root.setAbsolutePosition(spawn.position);
|
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;
|
|
@@ -155,6 +157,12 @@ export interface VfxEffectPackage<PParams extends VfxParamValues = VfxParamValue
|
|
|
155
157
|
* 默认随 placement 推断('socket'→local,其余→world)。
|
|
156
158
|
*/
|
|
157
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;
|
|
158
166
|
/**
|
|
159
167
|
* 构建特效。**约定(G6):root 的 transform(position/rotation/scaling/parent)归 service 独占** ——
|
|
160
168
|
* service 在 create() 后会二次 `applySpawnToRoot` 覆盖,create 内写 `root.position/rotation/scaling` 会被静默清掉。
|
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,
|