@fps-games/vfx 0.3.16 → 0.3.18

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.18 — 2026-07-14
16
+
17
+ ### 改动
18
+ - fix(tooling): support validation on Windows
19
+ - feat(energy-shield): align dome shader rendering
20
+ - feat(energy-shield): refine glass shield rendering
21
+
22
+ ### 特效
23
+ - 无新增 / 删除(仍 41 个)。
24
+
15
25
  ## v0.3.16 — 2026-07-08
16
26
 
17
27
  ### 改动
@@ -1,16 +1,18 @@
1
1
  import { Vector3 } from '@babylonjs/core/Maths/math.vector';
2
2
  import { Scene } from '@babylonjs/core/scene';
3
3
  import { VfxRegistry } from './registry';
4
- import { VfxDebugAnchor, VfxEffectHandle, VfxEffectPackage, VfxParamValues, VfxSpawnTransform } from './types';
4
+ import { VfxDebugAnchor, VfxEffectHandle, VfxEffectPackage, VfxParamValues, VfxRuntimeResources, VfxSpawnTransform } from './types';
5
5
  export interface EffectPackageServiceOptions {
6
6
  /** 注入的注册表(由项目侧 glob + buildRegistry 构建);库不直接引用项目注册表 */
7
7
  registry: VfxRegistry;
8
8
  getDebugAnchorPosition?: (anchor: VfxDebugAnchor) => Vector3 | null;
9
+ runtime?: VfxRuntimeResources;
9
10
  }
10
11
  export declare class EffectPackageService {
11
12
  private readonly scene;
12
13
  private readonly registry;
13
14
  private readonly getDebugAnchorPosition?;
15
+ private readonly runtime?;
14
16
  private readonly activeHandles;
15
17
  private readonly savedParams;
16
18
  constructor(scene: Scene, options: EffectPackageServiceOptions);
@@ -1,7 +1,7 @@
1
1
  import { Scene } from '@babylonjs/core/scene';
2
2
  import { Vector3 } from '@babylonjs/core/Maths/math.vector';
3
3
  import { VfxRegistry } from './registry';
4
- import { VfxDebugAnchor, VfxEffectHandle, VfxEffectPackage, VfxParamValues, VfxSpawnTransform } from './types';
4
+ import { VfxDebugAnchor, VfxEffectHandle, VfxEffectPackage, VfxParamValues, VfxRuntimeResources, VfxSpawnTransform } from './types';
5
5
  import { EffectPackageService } from './EffectPackageService';
6
6
  export interface SceneVfxWarmupEntry {
7
7
  effectId: string;
@@ -12,6 +12,7 @@ export interface SceneVfxServiceOptions {
12
12
  /** 注入的注册表(项目侧 glob + buildRegistry 构建) */
13
13
  registry: VfxRegistry;
14
14
  getDebugAnchorPosition?: (anchor: VfxDebugAnchor) => Vector3 | null;
15
+ runtime?: VfxRuntimeResources;
15
16
  }
16
17
  export declare class SceneVfxService {
17
18
  private effectPackageService;
@@ -1,3 +1,4 @@
1
+ import { GlowLayer } from '@babylonjs/core/Layers/glowLayer';
1
2
  import { ShaderMaterial } from '@babylonjs/core/Materials/shaderMaterial';
2
3
  import { Vector3 } from '@babylonjs/core/Maths/math.vector';
3
4
  import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
@@ -9,10 +10,18 @@ export interface EnergyShieldParams extends VfxParamValues {
9
10
  radius: number;
10
11
  verticalScale: number;
11
12
  color: VfxColorValue;
13
+ baseColor: VfxColorValue;
12
14
  opacity: number;
13
15
  fresnelPower: number;
14
16
  fresnelStrength: number;
15
17
  rimIntensity: number;
18
+ baseGlowStrength: number;
19
+ baseGlowHeight: number;
20
+ surfaceVariation: number;
21
+ backRimStrength: number;
22
+ backGlowStrength: number;
23
+ backGlowHeight: number;
24
+ showSurfaceFlow: boolean;
16
25
  surfaceIntensity: number;
17
26
  noiseScale: number;
18
27
  noiseEdgeWidth: number;
@@ -23,6 +32,8 @@ export interface EnergyShieldParams extends VfxParamValues {
23
32
  flowIntensity: number;
24
33
  marbleContrast: number;
25
34
  bloomIntensity: number;
35
+ bloomKernelSize: number;
36
+ showGlowLayer: boolean;
26
37
  haloScale: number;
27
38
  showHalo: boolean;
28
39
  segments: number;
@@ -33,11 +44,15 @@ export interface CreateEnergyShieldOptions {
33
44
  position?: Vector3;
34
45
  params?: Partial<EnergyShieldParams>;
35
46
  renderingGroupId?: number;
47
+ glowLayer?: GlowLayer | null;
36
48
  disposeRoot?: boolean;
37
49
  }
38
50
  export interface EnergyShieldHandle extends VfxEffectHandle {
39
51
  shield: Mesh;
40
52
  material: ShaderMaterial;
53
+ backShield: Mesh;
54
+ backMaterial: ShaderMaterial;
55
+ glowLayer: GlowLayer | null;
41
56
  update(params: Partial<EnergyShieldParams>): void;
42
57
  }
43
58
  export declare const energyShieldEffectPackage: VfxEffectPackage<EnergyShieldParams>;
@@ -1,4 +1,5 @@
1
1
  import { Constants } from "@babylonjs/core/Engines/constants";
2
+ import { GlowLayer } from "@babylonjs/core/Layers/glowLayer";
2
3
  import { Effect } from "@babylonjs/core/Materials/effect";
3
4
  import { ShaderMaterial } from "@babylonjs/core/Materials/shaderMaterial";
4
5
  import { Color3 } from "@babylonjs/core/Maths/math.color";
@@ -8,27 +9,36 @@ import { TransformNode } from "@babylonjs/core/Meshes/transformNode";
8
9
  import { V as VFX_RENDERING_GROUP_ID } from "../../chunks/rendering-CV3nhygX.js";
9
10
  const SHADER_NAME = "vfxEnergyShield";
10
11
  const HALO_ALPHA_SCALE_MAX = 0.3;
11
- const HALO_FEATHER_ALPHA_RATIO = 0.72;
12
- const HALO_FEATHER_SCALE_OFFSET = 0.018;
12
+ const SHIELD_SLICE = 0.5;
13
13
  const DEFAULT_PARAMS = {
14
14
  radius: 3.18,
15
- verticalScale: 1.02,
16
- color: { r: 0.34902, g: 0.568627, b: 0.968627 },
17
- opacity: 0.2,
18
- fresnelPower: 1.8,
19
- fresnelStrength: 0.66,
20
- rimIntensity: 0.4,
21
- surfaceIntensity: 0.3,
22
- noiseScale: 1,
23
- noiseEdgeWidth: 0.1,
24
- noiseEdgeIntensity: 0.42,
25
- noiseEdgeSmoothness: 0.5,
26
- flowScale: 0.36,
27
- flowSpeed: 0.25,
28
- flowIntensity: 1.15,
29
- marbleContrast: 0.67,
30
- bloomIntensity: 0.45,
31
- haloScale: 1.018,
15
+ verticalScale: 1,
16
+ color: { r: 0.28627451, g: 0.50980392, b: 1 },
17
+ baseColor: { r: 0.28627451, g: 0.50980392, b: 1 },
18
+ opacity: 0.02,
19
+ fresnelPower: 2,
20
+ fresnelStrength: 1,
21
+ rimIntensity: 1,
22
+ baseGlowStrength: 0.32,
23
+ baseGlowHeight: 0.46,
24
+ surfaceVariation: 0,
25
+ backRimStrength: 1,
26
+ backGlowStrength: 0.36,
27
+ backGlowHeight: 0.49,
28
+ showSurfaceFlow: false,
29
+ surfaceIntensity: 0.04,
30
+ noiseScale: 0.9,
31
+ noiseEdgeWidth: 0.08,
32
+ noiseEdgeIntensity: 0.62,
33
+ noiseEdgeSmoothness: 0.3,
34
+ flowScale: 2.15,
35
+ flowSpeed: 0.18,
36
+ flowIntensity: 0.2,
37
+ marbleContrast: 1,
38
+ bloomIntensity: 0.4,
39
+ bloomKernelSize: 36,
40
+ showGlowLayer: false,
41
+ haloScale: 1.006,
32
42
  showHalo: false,
33
43
  segments: 64
34
44
  };
@@ -58,11 +68,16 @@ precision highp float;
58
68
 
59
69
  uniform float uTime;
60
70
  uniform vec3 cameraPosition;
61
- uniform vec3 uColor;
71
+ uniform vec3 uTopColor;
72
+ uniform vec3 uBaseColor;
62
73
  uniform float uOpacity;
63
74
  uniform float uFresnelPower;
64
- uniform float uFresnelStrength;
75
+ uniform float uRimStrength;
65
76
  uniform float uRimIntensity;
77
+ uniform float uHorizonStrength;
78
+ uniform float uHorizonHeight;
79
+ uniform float uSurfaceVariation;
80
+ uniform float uSurfaceFlowEnabled;
66
81
  uniform float uSurfaceIntensity;
67
82
  uniform float uNoiseScale;
68
83
  uniform float uNoiseEdgeWidth;
@@ -72,137 +87,68 @@ uniform float uFlowScale;
72
87
  uniform float uFlowSpeed;
73
88
  uniform float uFlowIntensity;
74
89
  uniform float uMarbleContrast;
75
- uniform float uHaloAlphaScale;
76
- uniform float uHaloMode;
90
+ uniform float uAlphaScale;
77
91
 
78
92
  varying vec3 vObjPos;
79
93
  varying vec3 vWorldPos;
80
94
  varying vec3 vNormalW;
81
95
 
82
- vec3 mod289v3(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
83
- vec4 mod289v4(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
84
- vec4 permute(vec4 x) { return mod289v4(((x * 34.0) + 1.0) * x); }
85
- vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
86
-
87
- float saturate(float v) { return clamp(v, 0.0, 1.0); }
88
-
89
- float snoise(vec3 v) {
90
- const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);
91
- const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
92
- vec3 i = floor(v + dot(v, C.yyy));
93
- vec3 x0 = v - i + dot(i, C.xxx);
94
- vec3 g = step(x0.yzx, x0.xyz);
95
- vec3 l = 1.0 - g;
96
- vec3 i1 = min(g.xyz, l.zxy);
97
- vec3 i2 = max(g.xyz, l.zxy);
98
- vec3 x1 = x0 - i1 + C.xxx;
99
- vec3 x2 = x0 - i2 + C.yyy;
100
- vec3 x3 = x0 - D.yyy;
101
- i = mod289v3(i);
102
- vec4 p = permute(permute(permute(
103
- i.z + vec4(0.0, i1.z, i2.z, 1.0))
104
- + i.y + vec4(0.0, i1.y, i2.y, 1.0))
105
- + i.x + vec4(0.0, i1.x, i2.x, 1.0));
106
- float n_ = 0.142857142857;
107
- vec3 ns = n_ * D.wyz - D.xzx;
108
- vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
109
- vec4 x_ = floor(j * ns.z);
110
- vec4 y_ = floor(j - 7.0 * x_);
111
- vec4 x = x_ * ns.x + ns.yyyy;
112
- vec4 y = y_ * ns.x + ns.yyyy;
113
- vec4 h = 1.0 - abs(x) - abs(y);
114
- vec4 b0 = vec4(x.xy, y.xy);
115
- vec4 b1 = vec4(x.zw, y.zw);
116
- vec4 s0 = floor(b0) * 2.0 + 1.0;
117
- vec4 s1 = floor(b1) * 2.0 + 1.0;
118
- vec4 sh = -step(h, vec4(0.0));
119
- vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
120
- vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
121
- vec3 p0 = vec3(a0.xy, h.x);
122
- vec3 p1 = vec3(a0.zw, h.y);
123
- vec3 p2 = vec3(a1.xy, h.z);
124
- vec3 p3 = vec3(a1.zw, h.w);
125
- vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
126
- p0 *= norm.x;
127
- p1 *= norm.y;
128
- p2 *= norm.z;
129
- p3 *= norm.w;
130
- vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
131
- m = m * m;
132
- return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
96
+ float hash(vec3 position) {
97
+ position = fract(position * 0.3183099 + 0.1);
98
+ position *= 17.0;
99
+ return fract(position.x * position.y * position.z * (position.x + position.y + position.z));
133
100
  }
134
101
 
135
- float fbm(vec3 p) {
136
- float total = 0.0;
137
- float amp = 0.55;
138
- total += snoise(p) * amp;
139
- p = p * 2.03 + vec3(17.1, 3.7, 9.2);
140
- amp *= 0.52;
141
- total += snoise(p) * amp;
142
- p = p * 2.11 + vec3(4.3, 13.9, 6.4);
143
- amp *= 0.5;
144
- total += snoise(p) * amp;
145
- return total;
102
+ float valueNoise(vec3 position) {
103
+ vec3 cell = floor(position);
104
+ vec3 local = fract(position);
105
+ local = local * local * (3.0 - 2.0 * local);
106
+
107
+ return mix(
108
+ mix(
109
+ mix(hash(cell), hash(cell + vec3(1.0, 0.0, 0.0)), local.x),
110
+ mix(hash(cell + vec3(0.0, 1.0, 0.0)), hash(cell + vec3(1.0, 1.0, 0.0)), local.x),
111
+ local.y
112
+ ),
113
+ mix(
114
+ mix(hash(cell + vec3(0.0, 0.0, 1.0)), hash(cell + vec3(1.0, 0.0, 1.0)), local.x),
115
+ mix(hash(cell + vec3(0.0, 1.0, 1.0)), hash(cell + vec3(1.0, 1.0, 1.0)), local.x),
116
+ local.y
117
+ ),
118
+ local.z
119
+ );
146
120
  }
147
121
 
148
122
  void main(void) {
149
- vec3 normalW = normalize(vNormalW);
150
123
  vec3 viewDir = normalize(cameraPosition - vWorldPos);
151
- float ndv = saturate(dot(normalW, viewDir));
152
- float viewRim = saturate(1.0 - ndv);
153
- float fresnel = pow(viewRim, max(0.05, uFresnelPower)) * uFresnelStrength;
154
-
155
- float t = uTime * uFlowSpeed;
156
- vec3 spherePos = normalize(vObjPos + vec3(0.0001)) * max(0.05, uNoiseScale);
157
- vec3 flowA = spherePos * uFlowScale + vec3(t, t * 0.62, t * 0.38);
158
- vec3 flowB = spherePos * (uFlowScale * 1.91) + vec3(-t * 0.46, t * 0.82, -t * 0.27);
159
- float n1 = fbm(flowA);
160
- float n2 = fbm(flowB);
161
- float cloud = saturate((n1 * 0.58 + n2 * 0.42) * 0.5 + 0.5);
162
-
163
- float veinInput = spherePos.y * 0.92 + n1 * 1.65 + n2 * 0.74 + t * 0.13;
164
- float ribbon = abs(sin(veinInput * 3.14159265));
165
- float marble = pow(1.0 - ribbon, max(0.25, uMarbleContrast));
166
- float nebula = saturate(mix(cloud, marble, 0.58) * uFlowIntensity * 0.34);
124
+ float facing = abs(dot(normalize(vNormalW), viewDir));
125
+ float rim = pow(1.0 - facing, max(0.05, uFresnelPower));
126
+ float horizon = pow(1.0 - clamp(vObjPos.y / max(0.001, uHorizonHeight), 0.0, 1.0), 2.5);
127
+ float heightFactor = smoothstep(0.0, 0.92, vObjPos.y);
167
128
 
168
- float edgeWidth = max(0.01, uNoiseEdgeWidth);
169
- float edgeSoft = max(0.01, uNoiseEdgeSmoothness) * 0.22;
170
- float noiseEdge = smoothstep(1.0 - edgeWidth - edgeSoft, 1.0 - edgeWidth * 0.22, nebula);
171
-
172
- vec3 deepBlue = uColor * (0.12 + 0.42 * cloud);
173
- vec3 marbleBlue = uColor * (0.34 + 0.9 * marble) * uSurfaceIntensity;
174
- vec3 rimBlue = uColor * fresnel * uRimIntensity;
175
- vec3 edgeBlue = uColor * noiseEdge * uNoiseEdgeIntensity;
176
- vec3 color = min((deepBlue + marbleBlue + rimBlue + edgeBlue) * 0.72, vec3(1.12));
177
-
178
- float alpha = uOpacity * (0.16 + 0.34 * cloud + 0.24 * marble);
179
- alpha += fresnel * uOpacity * 0.58;
180
- alpha += noiseEdge * uNoiseEdgeIntensity * 0.05;
181
-
182
- if (uHaloMode > 1.5) {
183
- float haloRim = pow(viewRim, max(0.28, uFresnelPower * 0.44));
184
- float featherIn = smoothstep(0.1, 0.46, haloRim);
185
- float featherOut = 1.0 - smoothstep(0.72, 1.0, haloRim);
186
- float featherRim = featherIn * featherOut;
187
- float outerFade = 1.0 - smoothstep(0.88, 1.0, haloRim);
188
- color = uColor * (0.2 + featherRim * 0.24 + cloud * 0.04);
189
- alpha = featherRim * 0.58 * outerFade;
190
- } else if (uHaloMode > 0.5) {
191
- float haloRim = pow(viewRim, max(0.3, uFresnelPower * 0.48));
192
- float bodyIn = smoothstep(0.34, 0.68, haloRim);
193
- float bodyOut = 1.0 - smoothstep(0.72, 1.0, haloRim);
194
- float bodyRim = bodyIn * bodyOut;
195
- float featherIn = smoothstep(0.12, 0.44, haloRim);
196
- float featherOut = 1.0 - smoothstep(0.48, 1.0, haloRim);
197
- float featherRim = featherIn * featherOut;
198
- float outerFade = 1.0 - smoothstep(0.86, 1.0, haloRim);
199
- color = uColor * (0.24 + bodyRim * 0.34 + featherRim * 0.12 + cloud * 0.05);
200
- alpha = (bodyRim * 0.5 + featherRim * 0.14 + noiseEdge * 0.01) * outerFade;
129
+ vec3 motion = vec3(0.0);
130
+ if (uSurfaceFlowEnabled > 0.5) {
131
+ motion = vec3(uTime * uFlowSpeed, -uTime * uFlowSpeed * 0.62, uTime * uFlowSpeed * 0.38);
201
132
  }
133
+ float cloud = valueNoise(vObjPos * max(0.05, uNoiseScale)) * 0.65;
134
+ cloud += valueNoise(vObjPos * max(0.05, uFlowScale) + motion) * 0.35;
135
+ float shapedCloud = pow(clamp(cloud, 0.0, 1.0), max(0.25, uMarbleContrast));
136
+ float variation = mix(1.0 - uSurfaceVariation, 1.0 + uSurfaceVariation, shapedCloud);
137
+ float cloudShade = mix(0.92, 1.08, shapedCloud);
138
+ rim *= cloudShade;
202
139
 
203
- alpha *= uHaloAlphaScale;
140
+ float edgeWidth = max(0.005, uNoiseEdgeWidth);
141
+ float edgeSoftness = max(0.005, uNoiseEdgeSmoothness * 0.2);
142
+ float flowBand = 1.0 - smoothstep(edgeWidth, edgeWidth + edgeSoftness, abs(shapedCloud - 0.5));
143
+ float flowGlow = flowBand * uFlowIntensity * uNoiseEdgeIntensity * uSurfaceFlowEnabled;
204
144
 
205
- gl_FragColor = vec4(color, saturate(alpha));
145
+ float alpha = uOpacity * variation + rim * uRimStrength + horizon * uHorizonStrength * variation;
146
+ alpha += flowGlow * uOpacity * 0.5;
147
+ vec3 gradientColor = mix(uBaseColor, uTopColor, heightFactor);
148
+ vec3 color = gradientColor * (0.25 + rim * 1.3 + horizon * 0.85) * cloudShade * uRimIntensity;
149
+ color += gradientColor * flowGlow * uSurfaceIntensity;
150
+
151
+ gl_FragColor = vec4(color, clamp(alpha * uAlphaScale, 0.0, 0.88));
206
152
  }
207
153
  `;
208
154
  const energyShieldEffectPackage = {
@@ -219,11 +165,19 @@ const energyShieldEffectPackage = {
219
165
  debugParams: [
220
166
  { key: "radius", label: "玻璃罩半径", kind: "scale", min: 0.4, max: 10, step: 0.01 },
221
167
  { key: "verticalScale", label: "纵向缩放", kind: "scale", min: 0.5, max: 1.8, step: 0.01 },
222
- { key: "color", label: "主体颜色", kind: "color" },
223
- { key: "opacity", label: "透明度", kind: "opacity", min: 0.02, max: 1, step: 0.01 },
224
- { key: "fresnelPower", label: "边缘曲线", kind: "number", min: 0.2, max: 6, step: 0.01 },
225
- { key: "fresnelStrength", label: "边缘强度", kind: "number", min: 0, max: 6, step: 0.01 },
168
+ { key: "color", label: "顶部颜色", kind: "color" },
169
+ { key: "baseColor", label: "底部颜色", kind: "color" },
170
+ { key: "opacity", label: "球壳透明度", kind: "opacity", min: 0, max: 1, step: 0.01 },
171
+ { key: "fresnelPower", label: "轮廓衰减", kind: "number", min: 0.2, max: 8, step: 0.01 },
172
+ { key: "fresnelStrength", label: "正面轮廓强度", kind: "number", min: 0, max: 2.5, step: 0.01 },
226
173
  { key: "rimIntensity", label: "轮廓亮度", kind: "number", min: 0, max: 4, step: 0.01 },
174
+ { key: "baseGlowStrength", label: "正面底部辉光", kind: "number", min: 0, max: 1, step: 0.01 },
175
+ { key: "baseGlowHeight", label: "正面辉光高度", kind: "number", min: 0.04, max: 1, step: 0.01 },
176
+ { key: "surfaceVariation", label: "表面明暗变化", kind: "number", min: 0, max: 0.6, step: 0.01 },
177
+ { key: "backRimStrength", label: "背面轮廓强度", kind: "number", min: 0, max: 1.5, step: 0.01 },
178
+ { key: "backGlowStrength", label: "背面底部辉光", kind: "number", min: 0, max: 1, step: 0.01 },
179
+ { key: "backGlowHeight", label: "背面辉光高度", kind: "number", min: 0.04, max: 1, step: 0.01 },
180
+ { key: "showSurfaceFlow", label: "显示表面流动", kind: "boolean" },
227
181
  { key: "surfaceIntensity", label: "表面亮度", kind: "number", min: 0, max: 3, step: 0.01 },
228
182
  { key: "noiseScale", label: "噪声缩放", kind: "number", min: 0.2, max: 5, step: 0.01 },
229
183
  { key: "noiseEdgeWidth", label: "噪声亮边宽度", kind: "number", min: 0.01, max: 0.5, step: 0.01 },
@@ -234,11 +188,19 @@ const energyShieldEffectPackage = {
234
188
  { key: "flowIntensity", label: "流动强度", kind: "number", min: 0, max: 8, step: 0.01 },
235
189
  { key: "marbleContrast", label: "纹路锐度", kind: "number", min: 0.25, max: 4, step: 0.01 },
236
190
  { key: "bloomIntensity", label: "Bloom 强度", kind: "number", min: 0, max: 4, step: 0.01 },
237
- { key: "haloScale", label: "外层光晕缩放", kind: "scale", min: 1, max: 1.08, step: 1e-3 },
238
- { key: "showHalo", label: "显示外层光晕", kind: "boolean" }
191
+ { key: "bloomKernelSize", label: "Bloom/Glow 半径", kind: "number", min: 8, max: 128, step: 1 },
192
+ { key: "showGlowLayer", label: "显示 Bloom/Glow", kind: "boolean" },
193
+ { key: "haloScale", label: "Halo 缩放", kind: "scale", min: 1, max: 1.08, step: 1e-3 },
194
+ { key: "showHalo", label: "显示额外 Halo", kind: "boolean" }
239
195
  ],
240
- create({ scene, root, position, renderingGroupId, params }) {
241
- return createEnergyShield(scene, { root, position, renderingGroupId, params });
196
+ create({ scene, root, position, renderingGroupId, params, runtime }) {
197
+ return createEnergyShield(scene, {
198
+ root,
199
+ position,
200
+ renderingGroupId,
201
+ params,
202
+ glowLayer: runtime?.glowLayer ?? runtime?.getGlowLayer?.() ?? null
203
+ });
242
204
  }
243
205
  };
244
206
  function createEnergyShield(scene, options = {}) {
@@ -257,7 +219,7 @@ function createEnergyShield(scene, options = {}) {
257
219
  {
258
220
  diameter: 2,
259
221
  segments: Math.max(16, Math.round(params.segments)),
260
- slice: 0.5,
222
+ slice: SHIELD_SLICE,
261
223
  updatable: false
262
224
  },
263
225
  scene
@@ -267,21 +229,79 @@ function createEnergyShield(scene, options = {}) {
267
229
  shield.alwaysSelectAsActiveMesh = true;
268
230
  shield.renderingGroupId = options.renderingGroupId ?? VFX_RENDERING_GROUP_ID;
269
231
  shield.alphaIndex = 20;
270
- const material = createShieldMaterial(scene);
232
+ const material = createShieldMaterial(scene, { face: "front" });
271
233
  shield.material = material;
234
+ const backShield = MeshBuilder.CreateSphere(
235
+ "vfx_energyShield_back_hemisphere",
236
+ {
237
+ diameter: 2,
238
+ segments: Math.max(16, Math.round(params.segments)),
239
+ slice: SHIELD_SLICE,
240
+ updatable: false
241
+ },
242
+ scene
243
+ );
244
+ backShield.parent = root;
245
+ backShield.isPickable = false;
246
+ backShield.alwaysSelectAsActiveMesh = true;
247
+ backShield.renderingGroupId = shield.renderingGroupId;
248
+ backShield.alphaIndex = shield.alphaIndex - 1;
249
+ const backMaterial = createShieldMaterial(scene, {
250
+ name: "vfx_energyShield_back_material",
251
+ alphaMode: Constants.ALPHA_ADD,
252
+ face: "back"
253
+ });
254
+ backShield.material = backMaterial;
255
+ const sharedGlowLayer = options.glowLayer ?? null;
256
+ let glowLayer = null;
257
+ let ownsGlowLayer = false;
258
+ let glowMeshIncluded = false;
272
259
  let halo = null;
273
260
  let haloMaterial = null;
274
- let haloFeather = null;
275
- let haloFeatherMaterial = null;
276
261
  const disposeHalo = () => {
277
262
  halo?.dispose(false, false);
278
263
  haloMaterial?.dispose();
279
- haloFeather?.dispose(false, false);
280
- haloFeatherMaterial?.dispose();
281
264
  halo = null;
282
265
  haloMaterial = null;
283
- haloFeather = null;
284
- haloFeatherMaterial = null;
266
+ };
267
+ const addGlowMesh = () => {
268
+ if (!glowLayer || glowMeshIncluded) return;
269
+ glowLayer.addIncludedOnlyMesh(shield);
270
+ glowLayer.referenceMeshToUseItsOwnMaterial(shield);
271
+ glowMeshIncluded = true;
272
+ };
273
+ const removeGlowMesh = () => {
274
+ if (!glowLayer || !glowMeshIncluded) return;
275
+ glowLayer.removeIncludedOnlyMesh(shield);
276
+ glowMeshIncluded = false;
277
+ };
278
+ const disposeGlowLayer = () => {
279
+ removeGlowMesh();
280
+ if (ownsGlowLayer) glowLayer?.dispose();
281
+ glowLayer = null;
282
+ ownsGlowLayer = false;
283
+ };
284
+ const ensureGlowLayer = () => {
285
+ if (!params.showGlowLayer || params.bloomIntensity <= 1e-3) {
286
+ disposeGlowLayer();
287
+ return;
288
+ }
289
+ if (!glowLayer) {
290
+ if (sharedGlowLayer) {
291
+ glowLayer = sharedGlowLayer;
292
+ } else {
293
+ glowLayer = new GlowLayer(`vfx_energyShield_glow_${shield.uniqueId}`, scene, {
294
+ blurKernelSize: params.bloomKernelSize,
295
+ mainTextureRatio: 0.35
296
+ });
297
+ ownsGlowLayer = true;
298
+ }
299
+ addGlowMesh();
300
+ }
301
+ if (ownsGlowLayer) {
302
+ glowLayer.intensity = params.bloomIntensity;
303
+ glowLayer.blurKernelSize = params.bloomKernelSize;
304
+ }
285
305
  };
286
306
  const ensureHalo = () => {
287
307
  if (!params.showHalo || params.bloomIntensity <= 1e-3) {
@@ -294,7 +314,7 @@ function createEnergyShield(scene, options = {}) {
294
314
  {
295
315
  diameter: 2,
296
316
  segments: Math.max(16, Math.round(params.segments)),
297
- slice: 0.5,
317
+ slice: SHIELD_SLICE,
298
318
  updatable: false
299
319
  },
300
320
  scene
@@ -307,49 +327,31 @@ function createEnergyShield(scene, options = {}) {
307
327
  halo.scaling.setAll(Math.max(1, params.haloScale));
308
328
  haloMaterial = createShieldMaterial(scene, {
309
329
  name: "vfx_energyShield_halo_material",
310
- alphaMode: Constants.ALPHA_ADD
330
+ alphaMode: Constants.ALPHA_ADD,
331
+ face: "front"
311
332
  });
312
333
  halo.material = haloMaterial;
313
334
  }
314
- if (!haloFeather || !haloFeatherMaterial) {
315
- haloFeather = MeshBuilder.CreateSphere(
316
- "vfx_energyShield_halo_feather",
317
- {
318
- diameter: 2,
319
- segments: Math.max(16, Math.round(params.segments)),
320
- slice: 0.5,
321
- updatable: false
322
- },
323
- scene
324
- );
325
- haloFeather.parent = shield;
326
- haloFeather.isPickable = false;
327
- haloFeather.alwaysSelectAsActiveMesh = true;
328
- haloFeather.renderingGroupId = shield.renderingGroupId;
329
- haloFeather.alphaIndex = shield.alphaIndex + 2;
330
- haloFeather.scaling.setAll(getHaloFeatherScale(params));
331
- haloFeatherMaterial = createShieldMaterial(scene, {
332
- name: "vfx_energyShield_halo_feather_material",
333
- alphaMode: Constants.ALPHA_ADD
334
- });
335
- haloFeather.material = haloFeatherMaterial;
336
- }
337
335
  };
336
+ ensureGlowLayer();
338
337
  ensureHalo();
339
- applyParams(material, shield, halo, haloMaterial, haloFeather, haloFeatherMaterial, params);
338
+ applyParams(material, shield, backMaterial, backShield, halo, haloMaterial, params);
340
339
  let elapsed = 0;
341
340
  const observer = scene.onBeforeRenderObservable.add(() => {
342
341
  elapsed += Math.min(scene.getEngine().getDeltaTime() / 1e3, 0.05);
343
342
  material.setFloat("uTime", elapsed);
343
+ backMaterial.setFloat("uTime", elapsed);
344
344
  haloMaterial?.setFloat("uTime", elapsed);
345
- haloFeatherMaterial?.setFloat("uTime", elapsed);
346
345
  });
347
346
  let disposed = false;
348
347
  const dispose = () => {
349
348
  if (disposed) return;
350
349
  disposed = true;
351
350
  scene.onBeforeRenderObservable.remove(observer);
351
+ disposeGlowLayer();
352
352
  disposeHalo();
353
+ backShield.dispose(false, false);
354
+ backMaterial.dispose();
353
355
  shield.dispose(false, false);
354
356
  material.dispose();
355
357
  if (disposeRoot) root.dispose();
@@ -358,10 +360,16 @@ function createEnergyShield(scene, options = {}) {
358
360
  root,
359
361
  shield,
360
362
  material,
363
+ backShield,
364
+ backMaterial,
365
+ get glowLayer() {
366
+ return glowLayer;
367
+ },
361
368
  update(partial) {
362
369
  params = mergeParams(partial, params);
370
+ ensureGlowLayer();
363
371
  ensureHalo();
364
- applyParams(material, shield, halo, haloMaterial, haloFeather, haloFeatherMaterial, params);
372
+ applyParams(material, shield, backMaterial, backShield, halo, haloMaterial, params);
365
373
  },
366
374
  dispose
367
375
  };
@@ -378,11 +386,16 @@ function createShieldMaterial(scene, options = {}) {
378
386
  "worldViewProjection",
379
387
  "cameraPosition",
380
388
  "uTime",
381
- "uColor",
389
+ "uTopColor",
390
+ "uBaseColor",
382
391
  "uOpacity",
383
392
  "uFresnelPower",
384
- "uFresnelStrength",
393
+ "uRimStrength",
385
394
  "uRimIntensity",
395
+ "uHorizonStrength",
396
+ "uHorizonHeight",
397
+ "uSurfaceVariation",
398
+ "uSurfaceFlowEnabled",
386
399
  "uSurfaceIntensity",
387
400
  "uNoiseScale",
388
401
  "uNoiseEdgeWidth",
@@ -392,49 +405,68 @@ function createShieldMaterial(scene, options = {}) {
392
405
  "uFlowSpeed",
393
406
  "uFlowIntensity",
394
407
  "uMarbleContrast",
395
- "uHaloAlphaScale",
396
- "uHaloMode"
408
+ "uAlphaScale"
397
409
  ],
398
410
  needAlphaBlending: true
399
411
  }
400
412
  );
401
413
  material.alphaMode = options.alphaMode ?? Constants.ALPHA_COMBINE;
402
- material.backFaceCulling = false;
414
+ material.backFaceCulling = true;
415
+ material.cullBackFaces = options.face !== "back";
403
416
  material.disableDepthWrite = true;
404
417
  material.fogEnabled = false;
405
418
  return material;
406
419
  }
407
- function applyParams(material, shield, halo, haloMaterial, haloFeather, haloFeatherMaterial, params) {
420
+ function applyParams(material, shield, backMaterial, backShield, halo, haloMaterial, params) {
408
421
  shield.scaling.set(params.radius, params.radius * params.verticalScale, params.radius);
409
- applyMaterialParams(material, params, params.opacity, 1, 0);
410
- const haloAlphaScale = Math.min(HALO_ALPHA_SCALE_MAX, params.opacity * Math.min(1, params.bloomIntensity));
422
+ backShield.scaling.copyFrom(shield.scaling);
423
+ applyMaterialParams(material, params, {
424
+ opacity: params.opacity,
425
+ rimStrength: params.fresnelStrength,
426
+ horizonStrength: params.baseGlowStrength,
427
+ horizonHeight: params.baseGlowHeight,
428
+ alphaScale: 1
429
+ });
430
+ applyMaterialParams(backMaterial, params, {
431
+ opacity: 0,
432
+ rimStrength: params.backRimStrength,
433
+ horizonStrength: params.backGlowStrength,
434
+ horizonHeight: params.backGlowHeight,
435
+ alphaScale: 1
436
+ });
437
+ const haloAlphaScale = Math.min(
438
+ HALO_ALPHA_SCALE_MAX,
439
+ Math.max(
440
+ params.opacity * Math.min(1, params.bloomIntensity),
441
+ Math.min(1, params.fresnelStrength) * 0.2
442
+ )
443
+ );
411
444
  if (halo && haloMaterial) {
412
445
  halo.scaling.setAll(Math.max(1, params.haloScale));
413
446
  applyMaterialParams(
414
447
  haloMaterial,
415
448
  params,
416
- 1,
417
- haloAlphaScale,
418
- 1
419
- );
420
- }
421
- if (haloFeather && haloFeatherMaterial) {
422
- haloFeather.scaling.setAll(getHaloFeatherScale(params));
423
- applyMaterialParams(
424
- haloFeatherMaterial,
425
- params,
426
- 1,
427
- haloAlphaScale * HALO_FEATHER_ALPHA_RATIO,
428
- 2
449
+ {
450
+ opacity: 0,
451
+ rimStrength: params.fresnelStrength,
452
+ horizonStrength: 0,
453
+ horizonHeight: params.baseGlowHeight,
454
+ alphaScale: haloAlphaScale
455
+ }
429
456
  );
430
457
  }
431
458
  }
432
- function applyMaterialParams(material, params, opacity, haloAlphaScale, haloMode) {
433
- material.setColor3("uColor", toColor3(params.color));
434
- material.setFloat("uOpacity", opacity);
459
+ function applyMaterialParams(material, params, settings) {
460
+ material.setColor3("uTopColor", toColor3(params.color));
461
+ material.setColor3("uBaseColor", toColor3(params.baseColor));
462
+ material.setFloat("uOpacity", settings.opacity);
435
463
  material.setFloat("uFresnelPower", params.fresnelPower);
436
- material.setFloat("uFresnelStrength", params.fresnelStrength);
464
+ material.setFloat("uRimStrength", settings.rimStrength);
437
465
  material.setFloat("uRimIntensity", params.rimIntensity);
466
+ material.setFloat("uHorizonStrength", settings.horizonStrength);
467
+ material.setFloat("uHorizonHeight", settings.horizonHeight);
468
+ material.setFloat("uSurfaceVariation", params.surfaceVariation);
469
+ material.setFloat("uSurfaceFlowEnabled", params.showSurfaceFlow ? 1 : 0);
438
470
  material.setFloat("uSurfaceIntensity", params.surfaceIntensity);
439
471
  material.setFloat("uNoiseScale", params.noiseScale);
440
472
  material.setFloat("uNoiseEdgeWidth", params.noiseEdgeWidth);
@@ -444,21 +476,18 @@ function applyMaterialParams(material, params, opacity, haloAlphaScale, haloMode
444
476
  material.setFloat("uFlowSpeed", params.flowSpeed);
445
477
  material.setFloat("uFlowIntensity", params.flowIntensity);
446
478
  material.setFloat("uMarbleContrast", params.marbleContrast);
447
- material.setFloat("uHaloAlphaScale", haloAlphaScale);
448
- material.setFloat("uHaloMode", haloMode);
479
+ material.setFloat("uAlphaScale", settings.alphaScale);
449
480
  }
450
481
  function mergeParams(patch, base = DEFAULT_PARAMS) {
451
482
  return {
452
483
  ...base,
453
484
  ...patch,
454
- color: patch?.color ? { ...patch.color } : { ...base.color }
485
+ color: patch?.color ? { ...patch.color } : { ...base.color },
486
+ baseColor: patch?.baseColor ? { ...patch.baseColor } : { ...base.baseColor }
455
487
  };
456
488
  }
457
489
  function toColor3(color) {
458
- return new Color3(color.r, color.g, color.b);
459
- }
460
- function getHaloFeatherScale(params) {
461
- return Math.max(1, params.haloScale + HALO_FEATHER_SCALE_OFFSET);
490
+ return new Color3(color.r, color.g, color.b).toLinearSpace();
462
491
  }
463
492
  function registerShaders() {
464
493
  Effect.ShadersStore[`${SHADER_NAME}VertexShader`] = VERTEX_SHADER;
@@ -76,8 +76,15 @@ const treasureAttentionEffectPackage = {
76
76
  { key: "bloomKernelSize", label: "Bloom 半径", kind: "number", min: 8, max: 96, step: 1 },
77
77
  { key: "showGlowLayer", label: "显示 GlowLayer", kind: "boolean" }
78
78
  ],
79
- create({ scene, root, position, renderingGroupId, params }) {
80
- return createTreasureAttentionFx(scene, { root, position, renderingGroupId, params, serviceOwnedRoot: true });
79
+ create({ scene, root, position, renderingGroupId, params, runtime }) {
80
+ return createTreasureAttentionFx(scene, {
81
+ root,
82
+ position,
83
+ renderingGroupId,
84
+ params,
85
+ glowLayer: runtime?.glowLayer ?? runtime?.getGlowLayer?.() ?? null,
86
+ serviceOwnedRoot: true
87
+ });
81
88
  }
82
89
  };
83
90
  function createTreasureAttentionFx(scene, options = {}) {
package/dist/index.js CHANGED
@@ -75,6 +75,7 @@ class EffectPackageService {
75
75
  this.scene = scene;
76
76
  this.registry = options.registry;
77
77
  this.getDebugAnchorPosition = options.getDebugAnchorPosition;
78
+ this.runtime = options.runtime;
78
79
  this.savedParams = cloneOverrideMap(options.registry.savedParams);
79
80
  }
80
81
  getEffectPackages() {
@@ -127,7 +128,8 @@ class EffectPackageService {
127
128
  renderingGroupId: spawn.renderingGroupId,
128
129
  params: runtimeParams,
129
130
  scale: spawn.scale,
130
- inputs: spawn.inputs
131
+ inputs: spawn.inputs,
132
+ runtime: this.runtime
131
133
  });
132
134
  applySpawnToRoot(root, spawn, applyRootScale);
133
135
  } catch (error) {
@@ -315,7 +317,8 @@ class SceneVfxService {
315
317
  constructor(scene, options) {
316
318
  this.effectPackageService = new EffectPackageService(scene, {
317
319
  registry: options.registry,
318
- getDebugAnchorPosition: options.getDebugAnchorPosition
320
+ getDebugAnchorPosition: options.getDebugAnchorPosition,
321
+ runtime: options.runtime
319
322
  });
320
323
  }
321
324
  getEffectPackageService() {
package/dist/types.d.ts CHANGED
@@ -2,6 +2,7 @@ import { Scene } from '@babylonjs/core/scene';
2
2
  import { Vector3 } from '@babylonjs/core/Maths/math.vector';
3
3
  import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
4
4
  import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
5
+ import { GlowLayer } from '@babylonjs/core/Layers/glowLayer';
5
6
  export type VfxDebugAnchor = 'playerRight' | 'player';
6
7
  export type VfxInput<T> = T | (() => T);
7
8
  export type VfxInputKey = 'aim' | 'normal' | 'endpoints' | 'target' | 'reference';
@@ -24,6 +25,11 @@ export interface VfxResolvedInputs {
24
25
  reference?: () => Vector3;
25
26
  scalars?: Record<string, () => number>;
26
27
  }
28
+ /** 运行时共享资源:不可序列化,由项目/游戏层注入,与可保存 params 分开。 */
29
+ export interface VfxRuntimeResources {
30
+ glowLayer?: GlowLayer | null;
31
+ getGlowLayer?: () => GlowLayer | null;
32
+ }
27
33
  export type VfxParamValue = number | string | boolean | VfxColorValue;
28
34
  export type VfxParamValues = Record<string, VfxParamValue>;
29
35
  export interface VfxColorValue {
@@ -74,6 +80,8 @@ export interface VfxEffectPackageContext<PParams extends VfxParamValues = VfxPar
74
80
  scale: Vector3;
75
81
  /** 运行时外部信息(已归一化为访问器)。link/target/定向武器/活改标量等按 required/optionalInputs 读取。 */
76
82
  inputs?: VfxResolvedInputs;
83
+ /** 运行时共享资源(如 scene 级共享 GlowLayer)。 */
84
+ runtime?: VfxRuntimeResources;
77
85
  }
78
86
  export interface VfxEffectHandle {
79
87
  root: TransformNode;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fps-games/vfx",
3
- "version": "0.3.16",
3
+ "version": "0.3.18",
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,27 +3,41 @@
3
3
  "effectId": "energy-shield",
4
4
  "params": {
5
5
  "radius": 3.18,
6
- "verticalScale": 1.02,
6
+ "verticalScale": 1,
7
7
  "color": {
8
- "r": 0.34902,
9
- "g": 0.568627,
10
- "b": 0.968627
8
+ "r": 0.28627451,
9
+ "g": 0.50980392,
10
+ "b": 1
11
11
  },
12
- "opacity": 0.2,
13
- "fresnelPower": 1.8,
14
- "fresnelStrength": 0.66,
15
- "rimIntensity": 0.4,
16
- "surfaceIntensity": 0.3,
17
- "noiseScale": 1,
18
- "noiseEdgeWidth": 0.1,
19
- "noiseEdgeIntensity": 0.42,
20
- "noiseEdgeSmoothness": 0.5,
21
- "flowScale": 0.36,
22
- "flowSpeed": 0.25,
23
- "flowIntensity": 1.15,
24
- "marbleContrast": 0.67,
25
- "bloomIntensity": 0.45,
26
- "haloScale": 1.018,
12
+ "baseColor": {
13
+ "r": 0.28627451,
14
+ "g": 0.50980392,
15
+ "b": 1
16
+ },
17
+ "opacity": 0.02,
18
+ "fresnelPower": 2,
19
+ "fresnelStrength": 1,
20
+ "rimIntensity": 1,
21
+ "baseGlowStrength": 0.32,
22
+ "baseGlowHeight": 0.46,
23
+ "surfaceVariation": 0,
24
+ "backRimStrength": 1,
25
+ "backGlowStrength": 0.36,
26
+ "backGlowHeight": 0.49,
27
+ "showSurfaceFlow": false,
28
+ "surfaceIntensity": 0.04,
29
+ "noiseScale": 0.9,
30
+ "noiseEdgeWidth": 0.08,
31
+ "noiseEdgeIntensity": 0.62,
32
+ "noiseEdgeSmoothness": 0.3,
33
+ "flowScale": 2.15,
34
+ "flowSpeed": 0.18,
35
+ "flowIntensity": 0.2,
36
+ "marbleContrast": 1,
37
+ "bloomIntensity": 0.4,
38
+ "bloomKernelSize": 36,
39
+ "showGlowLayer": false,
40
+ "haloScale": 1.006,
27
41
  "showHalo": false,
28
42
  "segments": 64
29
43
  }