@fps-games/vfx 0.3.4 → 0.3.6

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,28 @@
12
12
 
13
13
  <!-- release.mjs 在此行下方插入新版本区块 -->
14
14
 
15
+ ## v0.3.6 — 2026-07-01
16
+
17
+ ### 改动
18
+ - perf(energy-shield): 去掉 per-instance GlowLayer,改用 shader halo shell,并默认关闭外层壳。
19
+ - perf(treasure-attention): 默认不创建 per-instance GlowLayer,保留 `showGlowLayer` 调试开关用于后续 AB。
20
+ - feat(energy-shield/treasure-attention): 补充 boolean 调试开关,方便在 demo/debug panel 中临时切换。
21
+
22
+ ### 特效
23
+ - 无新增 / 删除(仍 37 个)。
24
+
25
+ ## v0.3.5 — 2026-07-01
26
+
27
+ ### 改动
28
+ - fix(weapon-trail): 拖尾按实际历史弧长映射,消除运动采样不足导致的截断
29
+ - feat(weapon-trail): refine layered trail controls
30
+ - feat(weapon-trail): 默认关闭刃头火花 (sparkRate 20→0)
31
+ - feat(weapon-trail): 软边加色外晕 ribbon 替代 GlowLayer 的柔光晕
32
+ - perf(weapon-trail): 去掉 per-instance GlowLayer,bloom 折进 additive 亮度 (#11 L-D)
33
+
34
+ ### 特效
35
+ - 无新增 / 删除(仍 37 个)。
36
+
15
37
  ## v0.3.4 — 2026-06-30
16
38
 
17
39
  ### 改动
@@ -294,6 +294,14 @@ function buildDebugParams(manifest) {
294
294
  });
295
295
  continue;
296
296
  }
297
+ if (param.type === "boolean") {
298
+ result.push({
299
+ key: param.key,
300
+ label,
301
+ kind: "boolean"
302
+ });
303
+ continue;
304
+ }
297
305
  if (param.type === "enum" && Array.isArray(param.options)) {
298
306
  result.push({
299
307
  key: param.key,
@@ -311,6 +319,7 @@ function buildDebugParams(manifest) {
311
319
  function isVfxParamValue(value) {
312
320
  if (typeof value === "number") return Number.isFinite(value);
313
321
  if (typeof value === "string") return true;
322
+ if (typeof value === "boolean") return true;
314
323
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
315
324
  const color = value;
316
325
  return typeof color.r === "number" && Number.isFinite(color.r) && typeof color.g === "number" && Number.isFinite(color.g) && typeof color.b === "number" && Number.isFinite(color.b);
@@ -1,4 +1,4 @@
1
- import { c as createManifestRuntimeEffectPackage } from "../../chunks/manifestRuntimePackage-CXQFSTo2.js";
1
+ import { c as createManifestRuntimeEffectPackage } from "../../chunks/manifestRuntimePackage-Vgh-cH5X.js";
2
2
  const schemaVersion = "vfx-effect-package/1.0";
3
3
  const id = "build.particle.door_open_gold_flakes";
4
4
  const name = {
@@ -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";
@@ -26,7 +25,8 @@ const DEFAULT_PARAMS = {
26
25
  flowIntensity: 1.15,
27
26
  marbleContrast: 0.67,
28
27
  bloomIntensity: 0.45,
29
- bloomKernelSize: 36,
28
+ haloScale: 1.012,
29
+ showHalo: false,
30
30
  segments: 64
31
31
  };
32
32
  const VERTEX_SHADER = `
@@ -69,6 +69,8 @@ uniform float uFlowScale;
69
69
  uniform float uFlowSpeed;
70
70
  uniform float uFlowIntensity;
71
71
  uniform float uMarbleContrast;
72
+ uniform float uHaloAlphaScale;
73
+ uniform float uHaloMode;
72
74
 
73
75
  varying vec3 vObjPos;
74
76
  varying vec3 vWorldPos;
@@ -173,6 +175,16 @@ void main(void) {
173
175
  alpha += fresnel * uOpacity * 0.58;
174
176
  alpha += noiseEdge * uNoiseEdgeIntensity * 0.05;
175
177
 
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;
184
+ }
185
+
186
+ alpha *= uHaloAlphaScale;
187
+
176
188
  gl_FragColor = vec4(color, saturate(alpha));
177
189
  }
178
190
  `;
@@ -205,7 +217,8 @@ const energyShieldEffectPackage = {
205
217
  { key: "flowIntensity", label: "流动强度", kind: "number", min: 0, max: 8, step: 0.01 },
206
218
  { key: "marbleContrast", label: "纹路锐度", kind: "number", min: 0.25, max: 4, step: 0.01 },
207
219
  { 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 }
220
+ { key: "haloScale", label: "外层光晕缩放", kind: "scale", min: 1, max: 1.08, step: 1e-3 },
221
+ { key: "showHalo", label: "显示外层光晕", kind: "boolean" }
209
222
  ],
210
223
  create({ scene, root, position, renderingGroupId, params }) {
211
224
  return createEnergyShield(scene, { root, position, renderingGroupId, params });
@@ -239,28 +252,56 @@ function createEnergyShield(scene, options = {}) {
239
252
  shield.alphaIndex = 20;
240
253
  const material = createShieldMaterial(scene);
241
254
  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
255
+ let halo = null;
256
+ let haloMaterial = null;
257
+ const disposeHalo = () => {
258
+ halo?.dispose(false, false);
259
+ haloMaterial?.dispose();
260
+ halo = null;
261
+ haloMaterial = null;
262
+ };
263
+ const ensureHalo = () => {
264
+ if (!params.showHalo || params.bloomIntensity <= 1e-3) {
265
+ disposeHalo();
266
+ return;
267
+ }
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
247
288
  });
248
- glowLayer.intensity = params.bloomIntensity;
249
- glowLayer.addIncludedOnlyMesh(shield);
250
- glowLayer.referenceMeshToUseItsOwnMaterial(shield);
251
- }
252
- applyParams(material, shield, glowLayer, params);
289
+ halo.material = haloMaterial;
290
+ };
291
+ ensureHalo();
292
+ applyParams(material, shield, halo, haloMaterial, params);
253
293
  let elapsed = 0;
254
294
  const observer = scene.onBeforeRenderObservable.add(() => {
255
295
  elapsed += Math.min(scene.getEngine().getDeltaTime() / 1e3, 0.05);
256
296
  material.setFloat("uTime", elapsed);
297
+ haloMaterial?.setFloat("uTime", elapsed);
257
298
  });
258
299
  let disposed = false;
259
300
  const dispose = () => {
260
301
  if (disposed) return;
261
302
  disposed = true;
262
303
  scene.onBeforeRenderObservable.remove(observer);
263
- glowLayer?.dispose();
304
+ disposeHalo();
264
305
  shield.dispose(false, false);
265
306
  material.dispose();
266
307
  if (disposeRoot) root.dispose();
@@ -269,28 +310,17 @@ function createEnergyShield(scene, options = {}) {
269
310
  root,
270
311
  shield,
271
312
  material,
272
- glowLayer,
273
313
  update(partial) {
274
314
  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
- }
315
+ ensureHalo();
316
+ applyParams(material, shield, halo, haloMaterial, params);
287
317
  },
288
318
  dispose
289
319
  };
290
320
  }
291
- function createShieldMaterial(scene) {
321
+ function createShieldMaterial(scene, options = {}) {
292
322
  const material = new ShaderMaterial(
293
- "vfx_energyShield_material",
323
+ options.name ?? "vfx_energyShield_material",
294
324
  scene,
295
325
  { vertex: SHADER_NAME, fragment: SHADER_NAME },
296
326
  {
@@ -313,21 +343,36 @@ function createShieldMaterial(scene) {
313
343
  "uFlowScale",
314
344
  "uFlowSpeed",
315
345
  "uFlowIntensity",
316
- "uMarbleContrast"
346
+ "uMarbleContrast",
347
+ "uHaloAlphaScale",
348
+ "uHaloMode"
317
349
  ],
318
350
  needAlphaBlending: true
319
351
  }
320
352
  );
321
- material.alphaMode = Constants.ALPHA_COMBINE;
353
+ material.alphaMode = options.alphaMode ?? Constants.ALPHA_COMBINE;
322
354
  material.backFaceCulling = false;
323
355
  material.disableDepthWrite = true;
324
356
  material.fogEnabled = false;
325
357
  return material;
326
358
  }
327
- function applyParams(material, shield, glowLayer, params) {
359
+ function applyParams(material, shield, halo, haloMaterial, params) {
328
360
  shield.scaling.set(params.radius, params.radius * params.verticalScale, params.radius);
361
+ applyMaterialParams(material, params, params.opacity, 1, 0);
362
+ if (halo && haloMaterial) {
363
+ halo.scaling.setAll(Math.max(1, params.haloScale));
364
+ applyMaterialParams(
365
+ haloMaterial,
366
+ params,
367
+ 1,
368
+ params.opacity * 0.95 * Math.min(2.2, params.bloomIntensity),
369
+ 1
370
+ );
371
+ }
372
+ }
373
+ function applyMaterialParams(material, params, opacity, haloAlphaScale, haloMode) {
329
374
  material.setColor3("uColor", toColor3(params.color));
330
- material.setFloat("uOpacity", params.opacity);
375
+ material.setFloat("uOpacity", opacity);
331
376
  material.setFloat("uFresnelPower", params.fresnelPower);
332
377
  material.setFloat("uFresnelStrength", params.fresnelStrength);
333
378
  material.setFloat("uRimIntensity", params.rimIntensity);
@@ -340,10 +385,8 @@ function applyParams(material, shield, glowLayer, params) {
340
385
  material.setFloat("uFlowSpeed", params.flowSpeed);
341
386
  material.setFloat("uFlowIntensity", params.flowIntensity);
342
387
  material.setFloat("uMarbleContrast", params.marbleContrast);
343
- if (glowLayer) {
344
- glowLayer.intensity = params.bloomIntensity;
345
- glowLayer.blurKernelSize = params.bloomKernelSize;
346
- }
388
+ material.setFloat("uHaloAlphaScale", haloAlphaScale);
389
+ material.setFloat("uHaloMode", haloMode);
347
390
  }
348
391
  function mergeParams(patch, base = DEFAULT_PARAMS) {
349
392
  return {
@@ -1,4 +1,4 @@
1
- import { c as createManifestRuntimeEffectPackage } from "../../chunks/manifestRuntimePackage-CXQFSTo2.js";
1
+ import { c as createManifestRuntimeEffectPackage } from "../../chunks/manifestRuntimePackage-Vgh-cH5X.js";
2
2
  const schemaVersion = "vfx-effect-package/1.0";
3
3
  const id = "build.particle.lumber_vehicle_gear_spin";
4
4
  const name = {
@@ -1,4 +1,4 @@
1
- import { c as createManifestRuntimeEffectPackage } from "../../chunks/manifestRuntimePackage-CXQFSTo2.js";
1
+ import { c as createManifestRuntimeEffectPackage } from "../../chunks/manifestRuntimePackage-Vgh-cH5X.js";
2
2
  const schemaVersion = "vfx-effect-package/1.0";
3
3
  const id = "build.particle.lumber_vehicle_upgrade_aura";
4
4
  const name = {
@@ -1,4 +1,4 @@
1
- import { c as createManifestRuntimeEffectPackage } from "../../chunks/manifestRuntimePackage-CXQFSTo2.js";
1
+ import { c as createManifestRuntimeEffectPackage } from "../../chunks/manifestRuntimePackage-Vgh-cH5X.js";
2
2
  const schemaVersion = "vfx-effect-package/1.0";
3
3
  const id = "build.particle.lumber_vehicle_wood_chips";
4
4
  const name = {
@@ -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);
@@ -1,4 +1,4 @@
1
- import { c as createManifestRuntimeEffectPackage } from "../../chunks/manifestRuntimePackage-CXQFSTo2.js";
1
+ import { c as createManifestRuntimeEffectPackage } from "../../chunks/manifestRuntimePackage-Vgh-cH5X.js";
2
2
  const schemaVersion = "vfx-effect-package/1.0";
3
3
  const id = "build.particle.tree_chop_dust_burst";
4
4
  const name = {
@@ -2,7 +2,11 @@ import { VfxColorValue, VfxEffectPackage, VfxParamValues } from '../../types';
2
2
  interface WeaponTrailParams extends VfxParamValues {
3
3
  lifetimeMode: string;
4
4
  lifetime: number;
5
+ playbackMode: string;
5
6
  orientation: string;
7
+ showGlow: boolean;
8
+ showOuter: boolean;
9
+ showCore: boolean;
6
10
  orbitRadius: number;
7
11
  orbitSpeed: number;
8
12
  height: number;
@@ -12,6 +16,7 @@ interface WeaponTrailParams extends VfxParamValues {
12
16
  coreWidth: number;
13
17
  tailFadeStrength: number;
14
18
  tailFadeExponent: number;
19
+ edgeSoftness: number;
15
20
  opacity: number;
16
21
  saturation: number;
17
22
  contrast: number;
@@ -19,7 +24,6 @@ interface WeaponTrailParams extends VfxParamValues {
19
24
  bloom: number;
20
25
  blendMode: string;
21
26
  flowSpeed: number;
22
- sparkRate: number;
23
27
  tint: VfxColorValue;
24
28
  }
25
29
  export declare const weaponTrailEffectPackage: VfxEffectPackage<WeaponTrailParams>;
@@ -4,26 +4,29 @@ import { VertexBuffer } from "@babylonjs/core/Buffers/buffer";
4
4
  import { ShaderMaterial } from "@babylonjs/core/Materials/shaderMaterial";
5
5
  import { Effect } from "@babylonjs/core/Materials/effect";
6
6
  import { Texture } from "@babylonjs/core/Materials/Textures/texture";
7
- import { Color3, Color4 } from "@babylonjs/core/Maths/math.color";
7
+ import { Color3 } from "@babylonjs/core/Maths/math.color";
8
8
  import { Vector3 } from "@babylonjs/core/Maths/math.vector";
9
9
  import { Constants } from "@babylonjs/core/Engines/constants";
10
- import { GlowLayer } from "@babylonjs/core/Layers/glowLayer";
11
- import { ParticleSystem } from "@babylonjs/core/Particles/particleSystem";
12
- import { L as LIFETIME_DEBUG_PARAMS, a as LIFETIME_DEFAULTS, b as makeFlareTexture, s as scheduleLifetime } from "../../chunks/space-shared-BXV1hc0k.js";
10
+ import { L as LIFETIME_DEBUG_PARAMS, a as LIFETIME_DEFAULTS, s as scheduleLifetime } from "../../chunks/space-shared-BXV1hc0k.js";
13
11
  const trailTextureUrl = "data:image/png;base64,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";
14
12
  const DEFAULT_PARAMS = {
15
13
  ...LIFETIME_DEFAULTS,
14
+ playbackMode: "follow",
16
15
  orientation: "horizontal",
16
+ showGlow: true,
17
+ showOuter: true,
18
+ showCore: true,
17
19
  orbitRadius: 0,
18
20
  orbitSpeed: 7,
19
21
  height: 0,
20
22
  // 默认 0:真实跟随挂点(orbit>0 是演示用的幻影圆周轨道)
21
- trailLength: 16,
23
+ trailLength: 32,
22
24
  worldLength: 4,
23
25
  width: 1.01,
24
26
  coreWidth: 0.4,
25
- tailFadeStrength: 0.58,
27
+ tailFadeStrength: 0,
26
28
  tailFadeExponent: 1.35,
29
+ edgeSoftness: 0.16,
27
30
  opacity: 1.35,
28
31
  saturation: 1.45,
29
32
  contrast: 1.32,
@@ -31,10 +34,12 @@ const DEFAULT_PARAMS = {
31
34
  bloom: 0.9,
32
35
  blendMode: "add",
33
36
  flowSpeed: 5,
34
- sparkRate: 20,
35
37
  tint: { r: 0.4, g: 0.85, b: 1 }
36
38
  // 青色能量,配新武器发光风
37
39
  };
40
+ const TEXTURE_U_MIN = 0;
41
+ const TEXTURE_U_MAX = 1;
42
+ const HISTORY_DISTANCE_MARGIN = 1.4;
38
43
  const TRAIL_VERTEX = `precision highp float;
39
44
  attribute vec3 position;
40
45
  attribute vec2 uv;
@@ -53,6 +58,7 @@ uniform float uContrast;
53
58
  uniform float uAlphaScale;
54
59
  uniform float uAlphaPower;
55
60
  uniform float uBright;
61
+ uniform float uEdgeSoftness;
56
62
  uniform float uTime;
57
63
  uniform float uFlowSpeed;
58
64
  void main(){
@@ -67,6 +73,8 @@ void main(){
67
73
  float band = smoothstep(0.0, 0.12, phase) * (1.0 - smoothstep(0.12, 0.4, phase));
68
74
  color *= 1.0 + band * 0.6 * step(0.0001, uFlowSpeed);
69
75
  float alpha = pow(clamp(base.a, 0.0, 1.0), uAlphaPower) * vColor.a * uAlphaScale;
76
+ float edge = min(vUv.y, 1.0 - vUv.y);
77
+ alpha *= mix(1.0, smoothstep(0.0, max(0.0001, uEdgeSoftness), edge), step(0.0001, uEdgeSoftness));
70
78
  gl_FragColor = vec4(color, clamp(alpha, 0.0, 1.0));
71
79
  }`;
72
80
  let shadersRegistered = false;
@@ -97,22 +105,26 @@ const weaponTrailEffectPackage = {
97
105
  defaultParams: DEFAULT_PARAMS,
98
106
  debugParams: [
99
107
  ...LIFETIME_DEBUG_PARAMS,
108
+ { key: "playbackMode", label: "播放方式", kind: "enum", options: [
109
+ { value: "follow", label: "跟随挂点" },
110
+ { value: "orbit", label: "环绕预览" }
111
+ ] },
100
112
  { key: "orientation", label: "拖尾朝向", kind: "enum", options: [
101
113
  { value: "horizontal", label: "水平面" },
102
114
  { value: "vertical", label: "竖直" }
103
115
  ] },
116
+ { key: "showGlow", label: "显示外晕层", kind: "boolean" },
117
+ { key: "showOuter", label: "显示主带层", kind: "boolean" },
118
+ { key: "showCore", label: "显示亮芯层", kind: "boolean" },
104
119
  { key: "orbitRadius", label: "环绕半径", kind: "scale", min: 0, max: 5, step: 0.05 },
105
120
  { key: "orbitSpeed", label: "环绕速度", kind: "number", min: 0, max: 20, step: 0.1 },
106
- { key: "height", label: "高度", kind: "number", min: -2, max: 3, step: 0.05 },
107
- { key: "trailLength", label: "拖尾段数", kind: "number", min: 4, max: 48, step: 1 },
121
+ { key: "trailLength", label: "拖尾段数", kind: "number", min: 4, max: 72, step: 1 },
108
122
  { key: "worldLength", label: "拖尾长度", kind: "scale", min: 0.5, max: 12, step: 0.1 },
109
123
  { key: "width", label: "拖尾宽度", kind: "scale", min: 0.1, max: 4, step: 0.01 },
110
124
  { key: "coreWidth", label: "亮芯宽度比", kind: "number", min: 0, max: 1, step: 0.01 },
111
125
  { key: "tailFadeStrength", label: "尾部淡出强度", kind: "number", min: 0, max: 1, step: 0.01 },
112
126
  { key: "tailFadeExponent", label: "尾部淡出曲线", kind: "number", min: 0.2, max: 4, step: 0.05 },
113
- { key: "opacity", label: "透明度", kind: "opacity", min: 0, max: 3, step: 0.01 },
114
- { key: "saturation", label: "饱和度", kind: "number", min: 0, max: 3, step: 0.01 },
115
- { key: "contrast", label: "对比度", kind: "number", min: 0, max: 3, step: 0.01 },
127
+ { key: "edgeSoftness", label: "横向软边", kind: "number", min: 0, max: 0.45, step: 0.01 },
116
128
  { key: "bright", label: "亮度(HDR)", kind: "number", min: 0.2, max: 4, step: 0.05 },
117
129
  { key: "bloom", label: "辉光强度", kind: "number", min: 0, max: 3, step: 0.05 },
118
130
  { key: "blendMode", label: "混合模式", kind: "enum", options: [
@@ -120,7 +132,6 @@ const weaponTrailEffectPackage = {
120
132
  { value: "combine", label: "正常(实心刃)" }
121
133
  ] },
122
134
  { key: "flowSpeed", label: "能量流动速度", kind: "number", min: 0, max: 4, step: 0.05 },
123
- { key: "sparkRate", label: "刃头火花/秒", kind: "number", min: 0, max: 80, step: 1 },
124
135
  { key: "tint", label: "颜色", kind: "color" }
125
136
  ],
126
137
  create({ scene, root, position, params, renderingGroupId }) {
@@ -135,7 +146,7 @@ const weaponTrailEffectPackage = {
135
146
  function makeMat(name, tint, bright) {
136
147
  const m = new ShaderMaterial(name, scene, { vertex: "vfxWeaponTrail", fragment: "vfxWeaponTrail" }, {
137
148
  attributes: ["position", "uv", "color"],
138
- uniforms: ["worldViewProjection", "uTint", "uSaturation", "uContrast", "uAlphaScale", "uAlphaPower", "uBright", "uTime", "uFlowSpeed"],
149
+ uniforms: ["worldViewProjection", "uTint", "uSaturation", "uContrast", "uAlphaScale", "uAlphaPower", "uBright", "uEdgeSoftness", "uTime", "uFlowSpeed"],
139
150
  samplers: ["mainTex"],
140
151
  needAlphaBlending: true
141
152
  });
@@ -146,6 +157,7 @@ const weaponTrailEffectPackage = {
146
157
  m.setFloat("uAlphaScale", params.opacity);
147
158
  m.setFloat("uAlphaPower", 0.82);
148
159
  m.setFloat("uBright", bright);
160
+ m.setFloat("uEdgeSoftness", params.edgeSoftness);
149
161
  m.setFloat("uTime", 0);
150
162
  m.setFloat("uFlowSpeed", params.flowSpeed);
151
163
  m.alphaMode = blend;
@@ -156,9 +168,14 @@ const weaponTrailEffectPackage = {
156
168
  }
157
169
  const tintC = new Color3(params.tint.r, params.tint.g, params.tint.b);
158
170
  const coreC = Color3.Lerp(tintC, new Color3(1, 1, 1), 0.75);
159
- const outerMat = makeMat("vfx_weaponTrail_outerMat", tintC, params.bright);
160
- const hasCore = params.coreWidth > 1e-3;
161
- const coreMat = hasCore ? makeMat("vfx_weaponTrail_coreMat", coreC, params.bright * 1.6) : null;
171
+ const bloomBoost = 1 + Math.max(0, params.bloom) * 0.25;
172
+ const hasOuter = params.showOuter !== false;
173
+ const hasCore = params.showCore !== false && params.coreWidth > 1e-3;
174
+ const hasGlow = params.showGlow !== false && params.bloom > 1e-3;
175
+ const outerMat = hasOuter ? makeMat("vfx_weaponTrail_outerMat", tintC, params.bright * bloomBoost) : null;
176
+ const coreMat = hasCore ? makeMat("vfx_weaponTrail_coreMat", coreC, params.bright * 1.6 * bloomBoost) : null;
177
+ const glowMat = hasGlow ? makeMat("vfx_weaponTrail_glowMat", tintC, params.bright) : null;
178
+ if (glowMat) glowMat.setFloat("uAlphaScale", params.opacity * 0.42 * Math.min(2.2, params.bloom));
162
179
  function makeMesh(name, mat, ai) {
163
180
  const m = new Mesh(name, scene);
164
181
  m.material = mat;
@@ -170,63 +187,89 @@ const weaponTrailEffectPackage = {
170
187
  m.alphaIndex = ai;
171
188
  return m;
172
189
  }
173
- const outer = makeMesh("vfx_weaponTrail_outer", outerMat, 10);
190
+ const glow = hasGlow ? makeMesh("vfx_weaponTrail_glow", glowMat, 9) : null;
191
+ const outer = hasOuter ? makeMesh("vfx_weaponTrail_outer", outerMat, 10) : null;
174
192
  const core = hasCore ? makeMesh("vfx_weaponTrail_core", coreMat, 11) : null;
175
- let glow = null;
176
- if (params.bloom > 0) {
177
- glow = new GlowLayer("vfx_weaponTrail_glow", scene, { blurKernelSize: 48 });
178
- glow.intensity = params.bloom;
179
- glow.addIncludedOnlyMesh(outer);
180
- glow.referenceMeshToUseItsOwnMaterial(outer);
181
- if (core) {
182
- glow.addIncludedOnlyMesh(core);
183
- glow.referenceMeshToUseItsOwnMaterial(core);
184
- }
185
- }
186
- let sparkPs = null;
187
- const sparkEmitter = new Vector3();
188
- if (params.sparkRate > 0) {
189
- const c255 = `${Math.round(params.tint.r * 255)},${Math.round(params.tint.g * 255)},${Math.round(params.tint.b * 255)}`;
190
- const flare = makeFlareTexture(scene, "vfx_weaponTrail_sparkTex", "255,255,255", c255);
191
- sparkPs = new ParticleSystem("vfx_weaponTrail_sparks", 160, scene);
192
- sparkPs.renderingGroupId = rgid;
193
- sparkPs.particleTexture = flare;
194
- sparkPs.emitter = sparkEmitter;
195
- sparkPs.minSize = 0.07;
196
- sparkPs.maxSize = 0.26;
197
- sparkPs.minLifeTime = 0.12;
198
- sparkPs.maxLifeTime = 0.4;
199
- sparkPs.emitRate = params.sparkRate;
200
- sparkPs.blendMode = ParticleSystem.BLENDMODE_ADD;
201
- sparkPs.minEmitPower = 0.6;
202
- sparkPs.maxEmitPower = 2.4;
203
- sparkPs.direction1 = new Vector3(-1, -0.6, -1);
204
- sparkPs.direction2 = new Vector3(1, 0.6, 1);
205
- sparkPs.gravity = new Vector3(0, -3, 0);
206
- sparkPs.color1 = new Color4(params.tint.r, params.tint.g, params.tint.b, 1);
207
- sparkPs.color2 = new Color4(1, 1, 1, 1);
208
- sparkPs.colorDead = new Color4(params.tint.r, params.tint.g, params.tint.b, 0);
209
- sparkPs.start();
210
- }
211
193
  const targetCount = Math.max(2, Math.round(params.trailLength));
212
194
  const raw = [];
213
195
  const points = [];
214
- for (let i = 0; i < 96; i++) raw.push(sample());
215
196
  for (let i = 0; i < targetCount; i++) points.push(sample());
197
+ const pointPathT = [];
216
198
  let rawCount = 0, angle = 0, time = 0;
217
- let outerInit = false, coreInit = false;
199
+ let outerInit = false, coreInit = false, glowInit = false;
218
200
  const cur = sample();
219
201
  const mountPos = new Vector3();
220
202
  const lastWidthAxis = new Vector3(0, 0, 1);
221
203
  const tmpDir = new Vector3();
204
+ const tailDir = new Vector3(1, 0, 0);
222
205
  const UP = new Vector3(0, 1, 0);
223
206
  const positions = [], uvs = [], colors = [], indices = [];
207
+ function resolvePlaybackMode() {
208
+ if (params.playbackMode === "follow" || params.playbackMode === "orbit") return params.playbackMode;
209
+ return params.orbitRadius > 0 ? "orbit" : "follow";
210
+ }
211
+ function sampleMinDistance() {
212
+ return Math.max(3e-3, Math.min(0.04, params.worldLength / Math.max(16, targetCount * 3)));
213
+ }
214
+ function ensureRawCapacity(length) {
215
+ while (raw.length < length) raw.push(sample());
216
+ }
217
+ function resolveTailDirection() {
218
+ if (rawCount >= 2) {
219
+ const tail = raw[rawCount - 1].position;
220
+ const newer = raw[rawCount - 2].position;
221
+ tailDir.set(tail.x - newer.x, tail.y - newer.y, tail.z - newer.z);
222
+ if (tailDir.lengthSquared() > 1e-8) {
223
+ tailDir.normalize();
224
+ return tailDir;
225
+ }
226
+ }
227
+ const axis = rawCount > 0 ? raw[rawCount - 1].widthAxis : lastWidthAxis;
228
+ Vector3.CrossToRef(UP, axis, tailDir);
229
+ if (tailDir.lengthSquared() > 1e-8) {
230
+ tailDir.normalize();
231
+ return tailDir;
232
+ }
233
+ tailDir.set(1, 0, 0);
234
+ return tailDir;
235
+ }
236
+ function extrapolateTailSample(targetDist, availableDist, out) {
237
+ const tail = raw[rawCount - 1];
238
+ copySample(tail, out);
239
+ const extra = Math.max(0, targetDist - availableDist);
240
+ const dir = resolveTailDirection();
241
+ out.position.x += dir.x * extra;
242
+ out.position.y += dir.y * extra;
243
+ out.position.z += dir.z * extra;
244
+ }
245
+ function trimRawHistory() {
246
+ const maxDistance = Math.max(1, params.worldLength * HISTORY_DISTANCE_MARGIN + params.width * 2);
247
+ let total = 0;
248
+ let keep = rawCount;
249
+ for (let i = 0; i < rawCount - 1; i++) {
250
+ total += Vector3.Distance(raw[i].position, raw[i + 1].position);
251
+ if (total >= maxDistance) {
252
+ keep = i + 2;
253
+ break;
254
+ }
255
+ }
256
+ rawCount = Math.min(rawCount, keep);
257
+ }
258
+ function insertRawSample(src) {
259
+ ensureRawCapacity(rawCount + 1);
260
+ for (let i = rawCount; i > 0; i--) copySample(raw[i - 1], raw[i]);
261
+ copySample(src, raw[0]);
262
+ rawCount += 1;
263
+ trimRawHistory();
264
+ }
224
265
  function sampleEmitter(out) {
266
+ const playbackMode = resolvePlaybackMode();
267
+ const orbitRadius = playbackMode === "orbit" ? params.orbitRadius : 0;
225
268
  const c = Math.cos(angle), s = Math.sin(angle);
226
269
  out.position.set(
227
- mountPos.x + c * params.orbitRadius,
270
+ mountPos.x + c * orbitRadius,
228
271
  mountPos.y + params.height,
229
- mountPos.z + s * params.orbitRadius
272
+ mountPos.z + s * orbitRadius
230
273
  );
231
274
  if (params.orientation === "vertical") {
232
275
  out.widthAxis.set(0, 1, 0);
@@ -245,17 +288,29 @@ const weaponTrailEffectPackage = {
245
288
  }
246
289
  out.widthAxis.copyFrom(lastWidthAxis);
247
290
  }
291
+ function historyArcLength() {
292
+ let total = 0;
293
+ for (let i = 0; i < rawCount - 1; i++) total += Vector3.Distance(raw[i].position, raw[i + 1].position);
294
+ return total;
295
+ }
248
296
  function resample() {
249
297
  if (rawCount <= 0) return;
250
- const target = params.worldLength;
298
+ const historyLen = historyArcLength();
299
+ const target = Math.max(1e-3, Math.min(params.worldLength, historyLen));
251
300
  let seg = 0, distBefore = 0;
252
301
  for (let i = 0; i < targetCount; i++) {
253
302
  const t = points[i];
254
- if (i === 0 || rawCount === 1) {
303
+ const normalizedDist = i / (targetCount - 1);
304
+ const targetDist = target * normalizedDist;
305
+ pointPathT[i] = normalizedDist;
306
+ if (i === 0) {
255
307
  copySample(raw[0], t);
256
308
  continue;
257
309
  }
258
- const targetDist = target * (i / (targetCount - 1));
310
+ if (rawCount === 1) {
311
+ extrapolateTailSample(targetDist, 0, t);
312
+ continue;
313
+ }
259
314
  let found = false;
260
315
  while (seg < rawCount - 1) {
261
316
  const l = raw[seg], r = raw[seg + 1];
@@ -272,7 +327,7 @@ const weaponTrailEffectPackage = {
272
327
  distBefore += segLen;
273
328
  seg++;
274
329
  }
275
- if (!found) copySample(raw[rawCount - 1], t);
330
+ if (!found) extrapolateTailSample(targetDist, distBefore, t);
276
331
  }
277
332
  }
278
333
  function buildRibbon(mesh, halfWidth, wasInit) {
@@ -283,8 +338,10 @@ const weaponTrailEffectPackage = {
283
338
  for (let i = 0; i < targetCount; i++) {
284
339
  const s = points[i];
285
340
  const t = i / (targetCount - 1);
286
- const u = t * 0.97 + 0.015;
287
- const alpha = Math.max(0, 1 - Math.pow(t, params.tailFadeExponent) * params.tailFadeStrength);
341
+ const pathT = pointPathT[i] ?? t;
342
+ const u = TEXTURE_U_MIN + (TEXTURE_U_MAX - TEXTURE_U_MIN) * pathT;
343
+ const tailAlpha = Math.max(0, 1 - Math.pow(pathT, params.tailFadeExponent) * params.tailFadeStrength);
344
+ const alpha = tailAlpha;
288
345
  const ox = s.widthAxis.x * halfWidth, oy = s.widthAxis.y * halfWidth, oz = s.widthAxis.z * halfWidth;
289
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);
290
347
  uvs.push(u, 0, u, 1);
@@ -313,22 +370,19 @@ const weaponTrailEffectPackage = {
313
370
  const observer = scene.onBeforeRenderObservable.add(() => {
314
371
  const dt = Math.min(scene.getEngine().getDeltaTime() / 1e3, 0.05);
315
372
  time += dt;
316
- angle += params.orbitSpeed * dt;
373
+ if (resolvePlaybackMode() === "orbit") angle += params.orbitSpeed * dt;
317
374
  root.computeWorldMatrix(true);
318
375
  mountPos.copyFrom(root.getAbsolutePosition());
319
376
  sampleEmitter(cur);
320
- if (rawCount === 0 || Vector3.Distance(cur.position, raw[0].position) > 5e-4) {
321
- for (let i = Math.min(rawCount, raw.length - 1); i > 0; i--) copySample(raw[i - 1], raw[i]);
322
- copySample(cur, raw[0]);
323
- rawCount = Math.min(rawCount + 1, raw.length);
324
- }
377
+ if (rawCount === 0 || Vector3.Distance(cur.position, raw[0].position) > sampleMinDistance()) insertRawSample(cur);
325
378
  resample();
326
379
  const halfW = params.width * 0.5;
327
- outerInit = buildRibbon(outer, halfW, outerInit);
380
+ if (glow) glowInit = buildRibbon(glow, halfW * 2.2, glowInit);
381
+ if (outer) outerInit = buildRibbon(outer, halfW, outerInit);
328
382
  if (core) coreInit = buildRibbon(core, halfW * params.coreWidth, coreInit);
329
- outerMat.setFloat("uTime", time);
383
+ outerMat?.setFloat("uTime", time);
330
384
  coreMat?.setFloat("uTime", time);
331
- if (sparkPs) sparkEmitter.copyFrom(points[0].position);
385
+ glowMat?.setFloat("uTime", time);
332
386
  });
333
387
  let disposed = false;
334
388
  let ltTimer = 0;
@@ -337,11 +391,10 @@ const weaponTrailEffectPackage = {
337
391
  disposed = true;
338
392
  if (ltTimer) window.clearTimeout(ltTimer);
339
393
  scene.onBeforeRenderObservable.remove(observer);
340
- sparkPs?.stop();
341
- sparkPs?.dispose();
342
394
  glow?.dispose();
343
- outer.dispose();
344
- outerMat.dispose();
395
+ glowMat?.dispose();
396
+ outer?.dispose();
397
+ outerMat?.dispose();
345
398
  core?.dispose();
346
399
  coreMat?.dispose();
347
400
  tex.dispose();
@@ -1,4 +1,4 @@
1
- import { c as createManifestRuntimeEffectPackage } from "../../chunks/manifestRuntimePackage-CXQFSTo2.js";
1
+ import { c as createManifestRuntimeEffectPackage } from "../../chunks/manifestRuntimePackage-Vgh-cH5X.js";
2
2
  const schemaVersion = "vfx-effect-package/1.0";
3
3
  const id = "build.particle.wood_crusher_sawdust";
4
4
  const name = {
@@ -1,4 +1,4 @@
1
- import { c as createManifestRuntimeEffectPackage } from "../../chunks/manifestRuntimePackage-CXQFSTo2.js";
1
+ import { c as createManifestRuntimeEffectPackage } from "../../chunks/manifestRuntimePackage-Vgh-cH5X.js";
2
2
  const schemaVersion = "vfx-effect-package/1.0";
3
3
  const id = "build.particle.wood_cutter_sawdust";
4
4
  const name = {
@@ -1,4 +1,4 @@
1
- import { c as createManifestRuntimeEffectPackage } from "../../chunks/manifestRuntimePackage-CXQFSTo2.js";
1
+ import { c as createManifestRuntimeEffectPackage } from "../../chunks/manifestRuntimePackage-Vgh-cH5X.js";
2
2
  const schemaVersion = "vfx-effect-package/1.0";
3
3
  const id = "build.material.wood_peel_material_test";
4
4
  const name = {
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { V, c, a } from "./chunks/rendering-CV3nhygX.js";
2
2
  import { Vector3 } from "@babylonjs/core/Maths/math.vector";
3
3
  import { TransformNode } from "@babylonjs/core/Meshes/transformNode";
4
- import { c as c2, c as c3 } from "./chunks/manifestRuntimePackage-CXQFSTo2.js";
4
+ import { c as c2, c as c3 } from "./chunks/manifestRuntimePackage-Vgh-cH5X.js";
5
5
  import { c as c4, f, a as a2, s } from "./chunks/legacy-particle-runtime-CbJYVxdd.js";
6
6
  const VFX_GENERATION_PARAM_KEYS = {
7
7
  offsetX: "__generation.offsetX",
@@ -1,5 +1,5 @@
1
1
  import { VfxEffectPackage } from './types';
2
- type LegacyTunableParamType = 'number' | 'color' | 'enum' | string;
2
+ type LegacyTunableParamType = 'number' | 'boolean' | 'color' | 'enum' | string;
3
3
  interface LegacyTunableTarget {
4
4
  emitter: string;
5
5
  property: string;
package/dist/types.d.ts CHANGED
@@ -22,14 +22,14 @@ export interface VfxResolvedInputs {
22
22
  target?: AbstractMesh;
23
23
  scalars?: Record<string, () => number>;
24
24
  }
25
- export type VfxParamValue = number | string | VfxColorValue;
25
+ export type VfxParamValue = number | string | boolean | VfxColorValue;
26
26
  export type VfxParamValues = Record<string, VfxParamValue>;
27
27
  export interface VfxColorValue {
28
28
  r: number;
29
29
  g: number;
30
30
  b: number;
31
31
  }
32
- export type VfxParamKind = 'number' | 'color' | 'enum' | 'opacity' | 'lifetime' | 'scale';
32
+ export type VfxParamKind = 'number' | 'boolean' | 'color' | 'enum' | 'opacity' | 'lifetime' | 'scale';
33
33
  export interface VfxParamDefinitionBase {
34
34
  key: string;
35
35
  label: string;
@@ -45,6 +45,9 @@ export interface VfxNumberParamDefinition extends VfxParamDefinitionBase {
45
45
  export interface VfxColorParamDefinition extends VfxParamDefinitionBase {
46
46
  kind: 'color';
47
47
  }
48
+ export interface VfxBooleanParamDefinition extends VfxParamDefinitionBase {
49
+ kind: 'boolean';
50
+ }
48
51
  export interface VfxEnumParamOption {
49
52
  value: string;
50
53
  label: string;
@@ -53,7 +56,7 @@ export interface VfxEnumParamDefinition extends VfxParamDefinitionBase {
53
56
  kind: 'enum';
54
57
  options: VfxEnumParamOption[];
55
58
  }
56
- export type VfxParamDefinition = VfxNumberParamDefinition | VfxColorParamDefinition | VfxEnumParamDefinition;
59
+ export type VfxParamDefinition = VfxNumberParamDefinition | VfxColorParamDefinition | VfxBooleanParamDefinition | VfxEnumParamDefinition;
57
60
  export interface VfxEffectPackageContext<PParams extends VfxParamValues = VfxParamValues> {
58
61
  scene: Scene;
59
62
  root: TransformNode;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fps-games/vfx",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
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": {
@@ -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.012,
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
  }