@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 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
- scaleX: number;
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
- scaleX: 1,
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: "scaleX", label: "X 轴缩放", kind: "number", min: 0.1, max: 6, step: 0.01 },
51
- { key: "scaleY", label: "Y 轴缩放", kind: "number", min: 0.1, max: 6, step: 0.01 },
52
- { key: "scaleZ", label: "Z 轴缩放", kind: "number", min: 0.1, max: 6, step: 0.01 },
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).addInPlace(new Vector3(0, params.initialHeight, 0));
63
- const centerStrands = createStrandSystem(scene, root, params, "center");
64
- const outlineStrands = createStrandSystem(scene, root, params, "outline");
65
- const glow = createOutlineGlow(scene, root, params);
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(params) / Math.max(0.1, params.riseSpeed) * 0.18 + 0.45) * 1e3
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(params) : getOutlineHeight(params);
96
- const width = params.strandWidth * getHorizontalScale(params) * (mode === "center" ? 1.35 : 0.92);
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 * getVerticalScale(params) * (mode === "center" ? 0.5 : 0.7));
114
- system.maxEmitPower = Math.max(system.minEmitPower, params.riseSpeed * getVerticalScale(params) * (mode === "center" ? 1.15 : 1.65));
115
- system.gravity = new Vector3(0, 0.38 * getVerticalScale(params), 0);
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 = (mode === "center" ? 0.22 : 0.12) * getHorizontalScale(params);
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(params) * 1.06;
154
- const glowWidth = Math.max(0.04, BASE_GLOW_THICKNESS * getHorizontalScale(params));
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 * getVerticalScale(params) * 0.58);
173
- system.maxEmitPower = Math.max(system.minEmitPower, params.riseSpeed * getVerticalScale(params) * 1.38);
174
- system.gravity = new Vector3(0, 0.3 * getVerticalScale(params), 0);
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 * getHorizontalScale(params);
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(params) * 0.5 * params.centerSpread;
213
- const zRadius = getFootprintZ(params) * 0.5 * params.centerSpread;
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 * getVerticalScale(params);
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 width = getFootprintX(params);
223
- const depth = getFootprintZ(params);
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) * params.outlineThickness * getHorizontalScale(params);
227
- const y = sequence01(index, 2) * 0.16 * getVerticalScale(params);
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 getFootprintX(params) {
234
- return BASE_FOOTPRINT_X * Math.max(0.05, params.scale) * Math.max(0.05, params.scaleX);
250
+ function getFootprintShape(params) {
251
+ if (params.footprintShape === "circle") return params.footprintShape;
252
+ return "rectangle";
235
253
  }
236
- function getFootprintZ(params) {
237
- return BASE_FOOTPRINT_Z * Math.max(0.05, params.scale) * Math.max(0.05, params.scaleZ);
254
+ function getFootprintX() {
255
+ return BASE_FOOTPRINT_X;
238
256
  }
239
- function getVerticalScale(params) {
240
- return Math.max(0.05, params.scale) * Math.max(0.05, params.scaleY);
257
+ function getFootprintZ() {
258
+ return BASE_FOOTPRINT_Z;
241
259
  }
242
- function getHorizontalScale(params) {
243
- return Math.max(0.05, params.scale) * Math.max(0.05, (params.scaleX + params.scaleZ) * 0.5);
260
+ function getCenterHeight(visualScale) {
261
+ return BASE_CENTER_HEIGHT * visualScale.y;
244
262
  }
245
- function getCenterHeight(params) {
246
- return BASE_CENTER_HEIGHT * getVerticalScale(params);
263
+ function getOutlineHeight(visualScale) {
264
+ return BASE_OUTLINE_HEIGHT * visualScale.y;
247
265
  }
248
- function getOutlineHeight(params) {
249
- return BASE_OUTLINE_HEIGHT * getVerticalScale(params);
266
+ function getMaxStrandHeight(visualScale) {
267
+ return Math.max(getCenterHeight(visualScale), getOutlineHeight(visualScale));
250
268
  }
251
- function getMaxStrandHeight(params) {
252
- return Math.max(getCenterHeight(params), getOutlineHeight(params));
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, new Vector3((Math.random() - 0.5) * 13, Math.random() * 6.75 + 2.25, (Math.random() - 0.5) * 13), 7, 1.2);
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
- bloomKernelSize: number;
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
- bloomKernelSize: 36,
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 fresnel = pow(1.0 - ndv, max(0.05, uFresnelPower)) * uFresnelStrength;
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: "bloomKernelSize", label: "Bloom 半径", kind: "number", min: 8, max: 128, step: 1 }
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 glowLayer = null;
243
- if (params.bloomIntensity > 0) {
244
- glowLayer = new GlowLayer(`vfx_energyShield_glow_${shield.uniqueId}`, scene, {
245
- blurKernelSize: params.bloomKernelSize,
246
- mainTextureRatio: 0.35
247
- });
248
- glowLayer.intensity = params.bloomIntensity;
249
- glowLayer.addIncludedOnlyMesh(shield);
250
- glowLayer.referenceMeshToUseItsOwnMaterial(shield);
251
- }
252
- applyParams(material, shield, glowLayer, params);
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
- glowLayer?.dispose();
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
- applyParams(material, shield, glowLayer, params);
276
- if (!glowLayer && params.bloomIntensity > 0) {
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, glowLayer, params) {
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", params.opacity);
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
- if (glowLayer) {
344
- glowLayer.intensity = params.bloomIntensity;
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;
@@ -29,6 +29,7 @@ export interface TreasureAttentionParams extends VfxParamValues {
29
29
  drift: number;
30
30
  bloomIntensity: number;
31
31
  bloomKernelSize: number;
32
+ showGlowLayer: boolean;
32
33
  }
33
34
  export interface CreateTreasureAttentionOptions {
34
35
  root?: TransformNode;
@@ -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);
@@ -3,6 +3,7 @@ interface WeaponTrailParams extends VfxParamValues {
3
3
  lifetimeMode: string;
4
4
  lifetime: number;
5
5
  playbackMode: string;
6
+ historySpace: string;
6
7
  orientation: string;
7
8
  showGlow: boolean;
8
9
  showOuter: boolean;
@@ -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
- out.position.set(
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
- positions.push(s.position.x + ox, s.position.y + oy, s.position.z + oz, s.position.x - ox, s.position.y - oy, s.position.z - oz);
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
- sampleEmitter(cur);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fps-games/vfx",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "type": "module",
5
5
  "description": "Shared VFX library for Playable projects: engine-injected effect packages + runtime. Project-pure (no imports of host project code).",
6
6
  "publishConfig": {
@@ -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
- "scaleX": 2,
15
- "scaleY": 0.2,
16
- "scaleZ": 2,
12
+ "footprintShape": "rectangle",
17
13
  "centerCount": 100,
18
14
  "outlineCount": 300,
19
15
  "strandWidth": 0.1,
@@ -23,7 +23,8 @@
23
23
  "flowIntensity": 1.15,
24
24
  "marbleContrast": 0.67,
25
25
  "bloomIntensity": 0.45,
26
- "bloomKernelSize": 36,
26
+ "haloScale": 1.018,
27
+ "showHalo": false,
27
28
  "segments": 64
28
29
  }
29
30
  }
@@ -44,6 +44,7 @@
44
44
  "riseSpeed": 0.72,
45
45
  "drift": 0.12,
46
46
  "bloomIntensity": 0.55,
47
- "bloomKernelSize": 32
47
+ "bloomKernelSize": 32,
48
+ "showGlowLayer": false
48
49
  }
49
50
  }