@fps-games/vfx 0.3.5 → 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,16 @@
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
+
15
25
  ## v0.3.5 — 2026-07-01
16
26
 
17
27
  ### 改动
@@ -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 {
@@ -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);
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.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
  }