@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 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
- 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,
@@ -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: "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 },
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).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);
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(params) / Math.max(0.1, params.riseSpeed) * 0.18 + 0.45) * 1e3
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(params) : getOutlineHeight(params);
96
- const width = params.strandWidth * getHorizontalScale(params) * (mode === "center" ? 1.35 : 0.92);
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 * 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);
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 = (mode === "center" ? 0.22 : 0.12) * getHorizontalScale(params);
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(params) * 1.06;
154
- const glowWidth = Math.max(0.04, BASE_GLOW_THICKNESS * getHorizontalScale(params));
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 * 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);
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 * getHorizontalScale(params);
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(params) * 0.5 * params.centerSpread;
213
- const zRadius = getFootprintZ(params) * 0.5 * params.centerSpread;
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 * getVerticalScale(params);
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 width = getFootprintX(params);
223
- const depth = getFootprintZ(params);
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) * params.outlineThickness * getHorizontalScale(params);
227
- const y = sequence01(index, 2) * 0.16 * getVerticalScale(params);
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 getFootprintX(params) {
234
- return BASE_FOOTPRINT_X * Math.max(0.05, params.scale) * Math.max(0.05, params.scaleX);
252
+ function getFootprintShape(params) {
253
+ if (params.footprintShape === "circle") return params.footprintShape;
254
+ return "rectangle";
235
255
  }
236
- function getFootprintZ(params) {
237
- return BASE_FOOTPRINT_Z * Math.max(0.05, params.scale) * Math.max(0.05, params.scaleZ);
256
+ function getFootprintX(footprintScale) {
257
+ return BASE_FOOTPRINT_X * footprintScale.x;
238
258
  }
239
- function getVerticalScale(params) {
240
- return Math.max(0.05, params.scale) * Math.max(0.05, params.scaleY);
259
+ function getFootprintZ(footprintScale) {
260
+ return BASE_FOOTPRINT_Z * footprintScale.z;
241
261
  }
242
- function getHorizontalScale(params) {
243
- return Math.max(0.05, params.scale) * Math.max(0.05, (params.scaleX + params.scaleZ) * 0.5);
262
+ function getCenterHeight() {
263
+ return BASE_CENTER_HEIGHT;
244
264
  }
245
- function getCenterHeight(params) {
246
- return BASE_CENTER_HEIGHT * getVerticalScale(params);
265
+ function getOutlineHeight() {
266
+ return BASE_OUTLINE_HEIGHT;
247
267
  }
248
- function getOutlineHeight(params) {
249
- return BASE_OUTLINE_HEIGHT * getVerticalScale(params);
268
+ function getMaxStrandHeight() {
269
+ return Math.max(getCenterHeight(), getOutlineHeight());
250
270
  }
251
- function getMaxStrandHeight(params) {
252
- return Math.max(getCenterHeight(params), getOutlineHeight(params));
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, 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
  };
@@ -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.012,
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 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;
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 > 0.5) {
179
- float haloRim = pow(saturate(1.0 - ndv), max(0.35, uFresnelPower * 0.55));
180
- float softRim = smoothstep(0.08, 0.86, haloRim);
181
- float edgeTaper = 1.0 - smoothstep(0.78, 1.0, haloRim) * 0.62;
182
- color = uColor * (0.82 + softRim * 0.32) * (0.9 + cloud * 0.1);
183
- alpha = (0.06 + softRim * 0.62 + noiseEdge * 0.12) * edgeTaper;
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 && haloMaterial) return;
269
- halo = MeshBuilder.CreateSphere(
270
- "vfx_energyShield_halo",
271
- {
272
- diameter: 2,
273
- segments: Math.max(16, Math.round(params.segments)),
274
- slice: 0.5,
275
- updatable: false
276
- },
277
- scene
278
- );
279
- halo.parent = shield;
280
- halo.isPickable = false;
281
- halo.alwaysSelectAsActiveMesh = true;
282
- halo.renderingGroupId = shield.renderingGroupId;
283
- halo.alphaIndex = shield.alphaIndex + 1;
284
- halo.scaling.setAll(Math.max(1, params.haloScale));
285
- haloMaterial = createShieldMaterial(scene, {
286
- name: "vfx_energyShield_halo_material",
287
- alphaMode: Constants.ALPHA_ADD
288
- });
289
- halo.material = haloMaterial;
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
- params.opacity * 0.95 * Math.min(2.2, params.bloomIntensity),
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;
@@ -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);
@@ -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
- applySpawnToRoot(root, spawn);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fps-games/vfx",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
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,7 @@
23
23
  "flowIntensity": 1.15,
24
24
  "marbleContrast": 0.67,
25
25
  "bloomIntensity": 0.45,
26
- "haloScale": 1.012,
26
+ "haloScale": 1.018,
27
27
  "showHalo": false,
28
28
  "segments": 64
29
29
  }