@fps-games/vfx 0.3.11 → 0.3.14

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.
@@ -0,0 +1,7 @@
1
+ declare const _default: {
2
+ "id": "golden-bubble-pop",
3
+ "nameZh": "金色泡泡炸开"
4
+ }
5
+ ;
6
+
7
+ export default _default;
@@ -0,0 +1,25 @@
1
+ import { VfxColorValue, VfxEffectPackage, VfxParamValues } from '../../types';
2
+ interface GoldenBubblePopParams extends VfxParamValues {
3
+ lifetime: number;
4
+ emitRate: number;
5
+ spawnRadius: number;
6
+ spawnHeight: number;
7
+ spawnHeightRandom: number;
8
+ bubbleMinSize: number;
9
+ bubbleMaxSize: number;
10
+ growTime: number;
11
+ growCurve: number;
12
+ wobbleAmount: number;
13
+ wobbleSpeed: number;
14
+ popSparkCount: number;
15
+ popRadius: number;
16
+ popDuration: number;
17
+ popDropletSize: number;
18
+ popGravity: number;
19
+ popFlashStrength: number;
20
+ color: VfxColorValue;
21
+ opacity: number;
22
+ glowStrength: number;
23
+ }
24
+ export declare const goldenBubblePopEffectPackage: VfxEffectPackage<GoldenBubblePopParams>;
25
+ export default goldenBubblePopEffectPackage;
@@ -0,0 +1,323 @@
1
+ import { Constants } from "@babylonjs/core/Engines/constants";
2
+ import { Material } from "@babylonjs/core/Materials/material";
3
+ import { StandardMaterial } from "@babylonjs/core/Materials/standardMaterial";
4
+ import { DynamicTexture } from "@babylonjs/core/Materials/Textures/dynamicTexture";
5
+ import { Texture } from "@babylonjs/core/Materials/Textures/texture";
6
+ import { Color3 } from "@babylonjs/core/Maths/math.color";
7
+ import { Vector3 } from "@babylonjs/core/Maths/math.vector";
8
+ import { Mesh } from "@babylonjs/core/Meshes/mesh";
9
+ import { MeshBuilder } from "@babylonjs/core/Meshes/meshBuilder";
10
+ import { V as VFX_RENDERING_GROUP_ID } from "../../chunks/rendering-CV3nhygX.js";
11
+ const id = "golden-bubble-pop";
12
+ const nameZh = "金色泡泡炸开";
13
+ const manifest = {
14
+ id,
15
+ nameZh
16
+ };
17
+ const DEFAULT_PARAMS = {
18
+ lifetime: 2.5,
19
+ emitRate: 8,
20
+ spawnRadius: 0.6,
21
+ spawnHeight: 0,
22
+ spawnHeightRandom: 0,
23
+ bubbleMinSize: 0.05,
24
+ bubbleMaxSize: 0.42,
25
+ growTime: 0.75,
26
+ growCurve: 1.35,
27
+ wobbleAmount: 0.025,
28
+ wobbleSpeed: 3.2,
29
+ popSparkCount: 5,
30
+ popRadius: 0.38,
31
+ popDuration: 0.42,
32
+ popDropletSize: 0.12,
33
+ popGravity: 3.2,
34
+ popFlashStrength: 0.75,
35
+ color: { r: 1, g: 0.78, b: 0.18 },
36
+ opacity: 0.72,
37
+ glowStrength: 0.9
38
+ };
39
+ const goldenBubblePopEffectPackage = {
40
+ id: "golden-bubble-pop",
41
+ nameZh: "金色泡泡炸开",
42
+ placement: "socket",
43
+ geometrySpace: "local",
44
+ debugAnchor: "playerRight",
45
+ manifest,
46
+ defaultParams: DEFAULT_PARAMS,
47
+ debugParams: [
48
+ { key: "lifetime", label: "生成时长", kind: "lifetime", min: 0.1, max: 8, step: 0.01 },
49
+ { key: "emitRate", label: "生成速度", kind: "number", min: 0, max: 40, step: 0.1 },
50
+ { key: "spawnRadius", label: "生成半径", kind: "scale", min: 0, max: 4, step: 0.01 },
51
+ { key: "spawnHeight", label: "生成平面高度", kind: "scale", min: -2, max: 2, step: 0.01 },
52
+ { key: "spawnHeightRandom", label: "生成高度随机", kind: "scale", min: 0, max: 2, step: 0.01 },
53
+ { key: "bubbleMinSize", label: "泡泡初始大小", kind: "scale", min: 5e-3, max: 1.5, step: 5e-3 },
54
+ { key: "bubbleMaxSize", label: "泡泡最大大小", kind: "scale", min: 0.02, max: 2.5, step: 0.01 },
55
+ { key: "growTime", label: "泡泡生长时间", kind: "lifetime", min: 0.05, max: 3, step: 0.01 },
56
+ { key: "growCurve", label: "生长曲线", kind: "number", min: 0.3, max: 3, step: 0.01 },
57
+ { key: "wobbleAmount", label: "原地微抖", kind: "scale", min: 0, max: 0.3, step: 1e-3 },
58
+ { key: "wobbleSpeed", label: "微抖速度", kind: "number", min: 0, max: 12, step: 0.05 },
59
+ { key: "popSparkCount", label: "炸开碎光数量", kind: "number", min: 0, max: 20, step: 1 },
60
+ { key: "popRadius", label: "碎光飞散范围", kind: "scale", min: 0, max: 2.5, step: 0.01 },
61
+ { key: "popDuration", label: "碎光淡出时长", kind: "lifetime", min: 0.05, max: 1.5, step: 0.01 },
62
+ { key: "popDropletSize", label: "碎光大小", kind: "scale", min: 0.01, max: 1, step: 5e-3 },
63
+ { key: "popGravity", label: "碎光重力", kind: "number", min: 0, max: 12, step: 0.05 },
64
+ { key: "popFlashStrength", label: "破裂亮度", kind: "number", min: 0, max: 2, step: 0.01 },
65
+ { key: "color", label: "泡泡颜色", kind: "color" },
66
+ { key: "opacity", label: "透明度", kind: "opacity", min: 0, max: 1.5, step: 0.01 },
67
+ { key: "glowStrength", label: "发光强度", kind: "number", min: 0.1, max: 5, step: 0.05 }
68
+ ],
69
+ create({ scene, root, params, renderingGroupId }) {
70
+ const textures = [
71
+ makeTexture(scene, "vfx_goldenBubblePop_bubbleTex", 128, (context, size) => drawBubbleTexture(context, size, params.color)),
72
+ makeTexture(scene, "vfx_goldenBubblePop_sparkTex", 64, (context, size) => drawSparkTexture(context, size, params.color))
73
+ ];
74
+ const bubbleMat = makeMaterial(scene, "vfx_goldenBubblePop_bubbleMat", textures[0], params.opacity, params.glowStrength);
75
+ const dropletMat = makeMaterial(scene, "vfx_goldenBubblePop_dropletMat", textures[1], params.opacity, params.glowStrength * params.popFlashStrength);
76
+ const materials = [bubbleMat, dropletMat];
77
+ const bubbles = [];
78
+ const pops = [];
79
+ const groupId = renderingGroupId ?? VFX_RENDERING_GROUP_ID;
80
+ const growTime = Math.max(0.03, params.growTime);
81
+ const emitDuration = Math.max(0, params.lifetime);
82
+ const disposeAfter = emitDuration + growTime + Math.max(0.05, params.popDuration) + 0.2;
83
+ let elapsed = 0;
84
+ let emitCarry = 0;
85
+ let disposed = false;
86
+ const observer = scene.onBeforeRenderObservable.add(() => {
87
+ const dt = Math.min(0.05, scene.getEngine().getDeltaTime() / 1e3);
88
+ elapsed += dt;
89
+ if (elapsed <= emitDuration) {
90
+ emitCarry += Math.max(0, params.emitRate) * dt;
91
+ const emitCount = Math.floor(emitCarry);
92
+ emitCarry -= emitCount;
93
+ for (let i = 0; i < emitCount; i += 1) spawnBubble();
94
+ }
95
+ updateBubbles(dt);
96
+ updatePops(dt);
97
+ if (elapsed > disposeAfter && bubbles.length === 0 && pops.length === 0) disposeAll();
98
+ });
99
+ function spawnBubble() {
100
+ const maxSize = randomRange(
101
+ Math.min(params.bubbleMinSize, params.bubbleMaxSize),
102
+ Math.max(params.bubbleMinSize, params.bubbleMaxSize)
103
+ );
104
+ const mesh = MeshBuilder.CreatePlane("vfx_goldenBubblePop_bubble", { size: 1 }, scene);
105
+ mesh.parent = root;
106
+ mesh.billboardMode = Mesh.BILLBOARDMODE_ALL;
107
+ mesh.material = bubbleMat;
108
+ mesh.renderingGroupId = groupId;
109
+ mesh.isPickable = false;
110
+ mesh.alphaIndex = 20;
111
+ const position = sampleSpawnPosition(params.spawnRadius, params.spawnHeight, params.spawnHeightRandom);
112
+ mesh.position.copyFrom(position);
113
+ mesh.scaling.setAll(Math.max(1e-3, params.bubbleMinSize));
114
+ bubbles.push({
115
+ mesh,
116
+ age: 0,
117
+ lifetime: growTime,
118
+ seed: Math.random() * Math.PI * 2,
119
+ basePosition: position,
120
+ maxSize
121
+ });
122
+ }
123
+ function updateBubbles(dt) {
124
+ for (let i = bubbles.length - 1; i >= 0; i -= 1) {
125
+ const bubble = bubbles[i];
126
+ bubble.age += dt;
127
+ const t = clamp01(bubble.age / bubble.lifetime);
128
+ const eased = Math.pow(t, Math.max(0.05, params.growCurve));
129
+ const size = lerp(Math.max(1e-3, params.bubbleMinSize), bubble.maxSize, eased);
130
+ const pulse = 1 + Math.sin((bubble.age * params.wobbleSpeed + bubble.seed) * 1.7) * 0.035;
131
+ const wobble = Math.max(0, params.wobbleAmount);
132
+ bubble.mesh.position.set(
133
+ bubble.basePosition.x + Math.sin(bubble.age * params.wobbleSpeed + bubble.seed) * wobble,
134
+ bubble.basePosition.y,
135
+ bubble.basePosition.z + Math.cos(bubble.age * params.wobbleSpeed + bubble.seed * 1.37) * wobble
136
+ );
137
+ bubble.mesh.scaling.setAll(size * pulse);
138
+ bubbleMat.alpha = params.opacity * (0.34 + 0.66 * (1 - t * 0.24));
139
+ if (t >= 1) {
140
+ spawnPop(bubble.mesh.position, bubble.maxSize);
141
+ bubble.mesh.dispose();
142
+ bubbles.splice(i, 1);
143
+ }
144
+ }
145
+ }
146
+ function spawnPop(position, bubbleSize) {
147
+ const duration = Math.max(0.03, params.popDuration);
148
+ const dropletCount = Math.max(0, Math.round(params.popSparkCount));
149
+ const droplets = [];
150
+ const radius = Math.max(0, params.popRadius);
151
+ for (let i = 0; i < dropletCount; i += 1) {
152
+ const droplet = MeshBuilder.CreatePlane("vfx_goldenBubblePop_droplet", { size: 1 }, scene);
153
+ droplet.parent = root;
154
+ droplet.billboardMode = Mesh.BILLBOARDMODE_ALL;
155
+ droplet.material = dropletMat;
156
+ droplet.renderingGroupId = groupId;
157
+ droplet.isPickable = false;
158
+ droplet.position.copyFrom(position);
159
+ droplet.alphaIndex = 31;
160
+ const direction = sampleBurstDirection(i, dropletCount);
161
+ const travel = radius * randomRange(0.62, 1.1);
162
+ droplets.push({
163
+ mesh: droplet,
164
+ velocity: new Vector3(
165
+ direction.x * travel / duration,
166
+ randomRange(0.06, 0.18) + Math.max(0, direction.y) * travel / duration,
167
+ direction.z * travel / duration
168
+ ),
169
+ size: Math.max(1e-3, params.popDropletSize) * randomRange(0.72, 1.32) + bubbleSize * 0.05,
170
+ seed: Math.random() * Math.PI * 2
171
+ });
172
+ }
173
+ pops.push({
174
+ origin: position.clone(),
175
+ droplets,
176
+ age: 0,
177
+ duration
178
+ });
179
+ }
180
+ function updatePops(dt) {
181
+ for (let i = pops.length - 1; i >= 0; i -= 1) {
182
+ const pop = pops[i];
183
+ pop.age += dt;
184
+ const t = clamp01(pop.age / pop.duration);
185
+ const fade = 1 - smoothstep(0.48, 1, t);
186
+ const appear = smoothstep(0, 0.18, t);
187
+ dropletMat.alpha = params.opacity * params.popFlashStrength * fade;
188
+ for (const droplet of pop.droplets) {
189
+ const gravity = Math.max(0, params.popGravity);
190
+ droplet.mesh.position.set(
191
+ pop.origin.x + droplet.velocity.x * pop.age,
192
+ pop.origin.y + droplet.velocity.y * pop.age - 0.5 * gravity * pop.age * pop.age,
193
+ pop.origin.z + droplet.velocity.z * pop.age
194
+ );
195
+ droplet.mesh.scaling.setAll(droplet.size * appear * (1 - t * 0.35));
196
+ droplet.mesh.rotation.z = droplet.seed + pop.age * 6;
197
+ }
198
+ if (t >= 1) {
199
+ for (const droplet of pop.droplets) droplet.mesh.dispose();
200
+ pops.splice(i, 1);
201
+ }
202
+ }
203
+ }
204
+ function disposeAll() {
205
+ if (disposed) return;
206
+ disposed = true;
207
+ if (observer) scene.onBeforeRenderObservable.remove(observer);
208
+ for (const bubble of bubbles) bubble.mesh.dispose();
209
+ for (const pop of pops) {
210
+ for (const droplet of pop.droplets) droplet.mesh.dispose();
211
+ }
212
+ for (const material of materials) material.dispose();
213
+ for (const texture of textures) texture.dispose();
214
+ root.dispose();
215
+ }
216
+ return {
217
+ root,
218
+ dispose() {
219
+ disposeAll();
220
+ }
221
+ };
222
+ }
223
+ };
224
+ function makeMaterial(scene, name, texture, opacity, glowStrength) {
225
+ const material = new StandardMaterial(name, scene);
226
+ const brightness = Math.max(0.1, glowStrength);
227
+ material.diffuseTexture = texture;
228
+ material.emissiveTexture = texture;
229
+ material.diffuseColor = Color3.White();
230
+ material.emissiveColor = Color3.White().scale(brightness);
231
+ material.specularColor = Color3.Black();
232
+ material.alpha = opacity;
233
+ material.disableLighting = true;
234
+ material.backFaceCulling = false;
235
+ material.useAlphaFromDiffuseTexture = true;
236
+ material.transparencyMode = Material.MATERIAL_ALPHABLEND;
237
+ material.alphaMode = Constants.ALPHA_ADD;
238
+ material.disableDepthWrite = true;
239
+ return material;
240
+ }
241
+ function makeTexture(scene, name, size, draw) {
242
+ const texture = new DynamicTexture(name, { width: size, height: size }, scene, true);
243
+ texture.wrapU = Texture.CLAMP_ADDRESSMODE;
244
+ texture.wrapV = Texture.CLAMP_ADDRESSMODE;
245
+ const context = texture.getContext();
246
+ context.clearRect(0, 0, size, size);
247
+ draw(context, size);
248
+ texture.update(false);
249
+ return texture;
250
+ }
251
+ function drawBubbleTexture(context, size, color) {
252
+ const center = size * 0.5;
253
+ const radius = size * 0.42;
254
+ const fill = context.createRadialGradient(center, center, radius * 0.08, center, center, radius);
255
+ fill.addColorStop(0, colorRgba(color, 0.1, 1.18));
256
+ fill.addColorStop(0.68, colorRgba(color, 0.06, 1.08));
257
+ fill.addColorStop(0.82, colorRgba(color, 0.68, 1.18));
258
+ fill.addColorStop(0.95, colorRgba(color, 0.9, 1.28));
259
+ fill.addColorStop(1, colorRgba(color, 0, 1.28));
260
+ context.fillStyle = fill;
261
+ context.beginPath();
262
+ context.arc(center, center, radius, 0, Math.PI * 2);
263
+ context.fill();
264
+ const highlight = context.createRadialGradient(size * 0.34, size * 0.3, 1, size * 0.34, size * 0.3, size * 0.16);
265
+ highlight.addColorStop(0, colorRgba(color, 0.72, 1.6));
266
+ highlight.addColorStop(0.55, colorRgba(color, 0.28, 1.4));
267
+ highlight.addColorStop(1, colorRgba(color, 0, 1.4));
268
+ context.fillStyle = highlight;
269
+ context.beginPath();
270
+ context.arc(size * 0.34, size * 0.3, size * 0.16, 0, Math.PI * 2);
271
+ context.fill();
272
+ }
273
+ function drawSparkTexture(context, size, color) {
274
+ const center = size * 0.5;
275
+ const spark = context.createRadialGradient(center, center, 1, center, center, size * 0.45);
276
+ spark.addColorStop(0, colorRgba(color, 0.98, 1.5));
277
+ spark.addColorStop(0.34, colorRgba(color, 0.82, 1.22));
278
+ spark.addColorStop(0.78, colorRgba(color, 0.32, 1));
279
+ spark.addColorStop(1, colorRgba(color, 0, 1));
280
+ context.fillStyle = spark;
281
+ context.beginPath();
282
+ context.arc(center, center, size * 0.45, 0, Math.PI * 2);
283
+ context.fill();
284
+ }
285
+ function sampleSpawnPosition(radius, height, heightRandom) {
286
+ const r = Math.sqrt(Math.random()) * Math.max(0, radius);
287
+ const a = Math.random() * Math.PI * 2;
288
+ return new Vector3(
289
+ Math.cos(a) * r,
290
+ height + (Math.random() - 0.5) * Math.max(0, heightRandom),
291
+ Math.sin(a) * r
292
+ );
293
+ }
294
+ function sampleBurstDirection(index, count) {
295
+ const angle = (index + Math.random() * 0.35) / Math.max(1, count) * Math.PI * 2;
296
+ const y = (Math.random() - 0.5) * 0.36;
297
+ const direction = new Vector3(Math.cos(angle), y, Math.sin(angle));
298
+ direction.normalize();
299
+ return direction;
300
+ }
301
+ function randomRange(min, max) {
302
+ return min + Math.random() * (max - min);
303
+ }
304
+ function lerp(a, b, t) {
305
+ return a + (b - a) * t;
306
+ }
307
+ function clamp01(value) {
308
+ return Math.max(0, Math.min(1, value));
309
+ }
310
+ function smoothstep(edge0, edge1, value) {
311
+ const t = clamp01((value - edge0) / (edge1 - edge0));
312
+ return t * t * (3 - 2 * t);
313
+ }
314
+ function colorRgba(color, alpha, boost) {
315
+ const r = Math.round(clamp01(color.r * boost) * 255);
316
+ const g = Math.round(clamp01(color.g * boost) * 255);
317
+ const b = Math.round(clamp01(color.b * boost) * 255);
318
+ return `rgba(${r}, ${g}, ${b}, ${clamp01(alpha)})`;
319
+ }
320
+ export {
321
+ goldenBubblePopEffectPackage as default,
322
+ goldenBubblePopEffectPackage
323
+ };
@@ -20,12 +20,13 @@ const manifest = {
20
20
  assets,
21
21
  source
22
22
  };
23
+ const BOLT_SEGMENTS = 6;
23
24
  const DEFAULT_PARAMS = {
24
25
  lifetimeMode: "infinite",
25
26
  lifetime: 2.5,
26
27
  scale: 1.15,
27
28
  initialHeight: 0,
28
- color: { r: 0.82, g: 0.95, b: 1 },
29
+ color: { r: 0.48, g: 0.9, b: 1 },
29
30
  opacity: 1,
30
31
  intensity: 1,
31
32
  forwardOffset: 0,
@@ -37,18 +38,18 @@ const DEFAULT_PARAMS = {
37
38
  fadeDistance: 0.58,
38
39
  spawnFade: 0.45,
39
40
  suctionSpeed: 1.65,
40
- lineCount: 32,
41
+ lineCount: 0,
41
42
  lineLength: 0.46,
42
- lineWidth: 0.045,
43
+ lineWidth: 0.052,
43
44
  lineOpacity: 1,
44
45
  lineSpread: 1,
45
46
  lineTwist: 0,
46
- particleCount: 14,
47
- particleSize: 0.032,
48
- particleSpeed: 2.15,
47
+ particleCount: 5,
48
+ particleSize: 0.036,
49
+ particleSpeed: 1.65,
49
50
  particleDrift: 0,
50
51
  particleOuterOffset: 0,
51
- particleOpacity: 0.55,
52
+ particleOpacity: 0.68,
52
53
  directionX: 0,
53
54
  directionZ: -1
54
55
  };
@@ -79,16 +80,14 @@ const magnetSuctionEffectPackage = {
79
80
  { key: "mouthPadding", label: "入口保护距离", kind: "number", min: 0.01, max: 2, step: 0.01 },
80
81
  { key: "fadeDistance", label: "入口淡出距离", kind: "number", min: 0.01, max: 3, step: 0.01 },
81
82
  { key: "spawnFade", label: "外侧淡入距离", kind: "number", min: 0.01, max: 3, step: 0.01 },
82
- { key: "suctionSpeed", label: "线条速度", kind: "number", min: 0, max: 8, step: 0.01 },
83
- { key: "lineCount", label: "白线数量", kind: "number", min: 0, max: 96, step: 1 },
84
- { key: "lineLength", label: "白线长度", kind: "number", min: 0.02, max: 2, step: 0.01 },
85
- { key: "lineWidth", label: "白线尾宽", kind: "number", min: 2e-3, max: 0.18, step: 1e-3 },
86
- { key: "lineOpacity", label: "白线透明度", kind: "opacity", min: 0, max: 1.5, step: 0.01 },
83
+ { key: "lineLength", label: "电弧长度", kind: "number", min: 0.02, max: 2, step: 0.01 },
84
+ { key: "lineWidth", label: "电弧宽度", kind: "number", min: 2e-3, max: 0.18, step: 1e-3 },
85
+ { key: "lineOpacity", label: "电弧透明度", kind: "opacity", min: 0, max: 1.5, step: 0.01 },
87
86
  { key: "lineSpread", label: "散布缩放", kind: "number", min: 0.1, max: 2, step: 0.01 },
88
- { key: "particleCount", label: "粒子数量", kind: "number", min: 0, max: 80, step: 1 },
89
- { key: "particleSize", label: "粒子大小", kind: "number", min: 5e-3, max: 0.3, step: 1e-3 },
90
- { key: "particleSpeed", label: "粒子速度", kind: "number", min: 0, max: 10, step: 0.01 },
91
- { key: "particleOpacity", label: "粒子透明度", kind: "opacity", min: 0, max: 1, step: 0.01 },
87
+ { key: "particleCount", label: "电弧数量", kind: "number", min: 0, max: 24, step: 1 },
88
+ { key: "particleSize", label: "电弧抖动大小", kind: "number", min: 5e-3, max: 0.3, step: 1e-3 },
89
+ { key: "particleSpeed", label: "电弧吸入速度", kind: "number", min: 0, max: 8, step: 0.01 },
90
+ { key: "particleOpacity", label: "电弧闪烁强度", kind: "opacity", min: 0, max: 1, step: 0.01 },
92
91
  { key: "directionX", label: "吸入方向 X", kind: "number", min: -1, max: 1, step: 0.01 },
93
92
  { key: "directionZ", label: "吸入方向 Z", kind: "number", min: -1, max: 1, step: 0.01 }
94
93
  ],
@@ -139,26 +138,15 @@ function createMagnetSuctionField(scene, root, params, renderingGroupId) {
139
138
  }
140
139
  }
141
140
  function createSuctionMesh(scene, root, params, material, renderingGroupId) {
142
- const lineCount = Math.max(0, Math.round(params.lineCount));
143
- const particleCount = Math.max(0, Math.round(params.particleCount));
144
- const lineVertexCount = lineCount * 3;
145
- const particleVertexCount = particleCount * 4;
146
- const totalVertexCount = lineVertexCount + particleVertexCount;
141
+ const boltCount = Math.max(0, Math.round(params.particleCount));
142
+ const boltVertexCount = boltCount * BOLT_SEGMENTS * 4;
143
+ const totalVertexCount = boltVertexCount;
147
144
  const positions = new Float32Array(totalVertexCount * 3);
148
145
  const colors = new Float32Array(totalVertexCount * 4);
149
146
  const indices = [];
150
- const lines = [];
151
- const particles = [];
152
- let resetIndex = 1;
153
- for (let index = 0; index < lineCount; index += 1) {
154
- const base = index * 3;
155
- indices.push(base, base + 1, base + 2);
156
- lines.push(resetLine({}, true));
157
- }
158
- for (let index = 0; index < particleCount; index += 1) {
159
- const base = lineVertexCount + index * 4;
160
- indices.push(base, base + 1, base + 2, base, base + 2, base + 3);
161
- particles.push(resetParticle({}, true));
147
+ let time = 0;
148
+ for (let base = 0; base < totalVertexCount; base += 4) {
149
+ pushQuadIndices(indices, base);
162
150
  }
163
151
  const mesh = new Mesh("magnet-suction.mesh", scene);
164
152
  mesh.parent = root;
@@ -176,15 +164,7 @@ function createSuctionMesh(scene, root, params, material, renderingGroupId) {
176
164
  writeGeometry();
177
165
  return { update, dispose };
178
166
  function update(deltaSeconds) {
179
- const basis = createSuctionBasis(params);
180
- for (const line of lines) {
181
- line.d -= line.speed * deltaSeconds;
182
- if (line.d < basis.mouthPadding) resetLine(line, false);
183
- }
184
- for (const particle of particles) {
185
- particle.d -= particle.speed * deltaSeconds;
186
- if (particle.d < basis.mouthPadding) resetParticle(particle, false);
187
- }
167
+ time += deltaSeconds;
188
168
  writeGeometry();
189
169
  }
190
170
  function dispose() {
@@ -193,10 +173,12 @@ function createSuctionMesh(scene, root, params, material, renderingGroupId) {
193
173
  function writeGeometry() {
194
174
  const basis = createSuctionBasis(params);
195
175
  const cameraBasis = createCameraBasis(scene, root, basis);
196
- const color = toColor3(params.color);
176
+ const suctionColor = toColor3(params.color);
177
+ const electric = new Color3(0.82, 0.98, 1);
178
+ const boltBlue = new Color3(0.25, 0.62, 1);
197
179
  const alphaScale = clamp01(params.opacity) * Math.max(0, params.intensity);
198
180
  let vertexIndex = 0;
199
- const putVertex = (position, alpha, brightness = 1) => {
181
+ const putVertex = (position, color, alpha, brightness = 1) => {
200
182
  const positionOffset = vertexIndex * 3;
201
183
  const colorOffset = vertexIndex * 4;
202
184
  positions[positionOffset] = position.x;
@@ -208,61 +190,67 @@ function createSuctionMesh(scene, root, params, material, renderingGroupId) {
208
190
  colors[colorOffset + 3] = clamp01(alpha * alphaScale);
209
191
  vertexIndex += 1;
210
192
  };
211
- for (const line of lines) {
212
- const alpha = lineFade(line.d, basis) * params.lineOpacity * line.alphaMul;
213
- const headD = Math.max(basis.mouthPadding, line.d);
214
- const tailD = Math.min(basis.length, line.d + line.len);
215
- const head = suctionPosition(headD, line.u, line.v, basis);
216
- const tail = suctionPosition(tailD, line.u, line.v, basis);
217
- const shrink = Math.max(lateralScale(headD, basis), 0.18);
218
- const half = cameraBasis.ribbonSide.scale(line.width * shrink * 0.5);
219
- putVertex(tail.subtract(half), alpha, line.brightness);
220
- putVertex(tail.add(half), alpha * 0.92, line.brightness);
221
- putVertex(head, alpha * 0.22, line.brightness);
222
- }
223
- for (const particle of particles) {
224
- const alpha = lineFade(particle.d, basis) * params.particleOpacity * particle.alphaMul;
225
- const center = suctionPosition(particle.d, particle.u, particle.v, basis);
226
- const shrink = Math.max(lateralScale(particle.d, basis), 0.22);
227
- const right = cameraBasis.particleRight.scale(particle.size * shrink);
228
- const up = cameraBasis.particleUp.scale(particle.size * shrink * 0.7);
229
- putVertex(center.subtract(right).subtractInPlace(up), alpha, particle.brightness);
230
- putVertex(center.add(right).subtractInPlace(up), alpha, particle.brightness);
231
- putVertex(center.add(right).addInPlace(up), alpha * 0.8, particle.brightness);
232
- putVertex(center.subtract(right).addInPlace(up), alpha * 0.8, particle.brightness);
193
+ const putQuad = (a, b, c, d, color, alpha, brightness = 1) => {
194
+ putVertex(a, color, alpha, brightness);
195
+ putVertex(b, color, alpha, brightness);
196
+ putVertex(c, color, alpha, brightness);
197
+ putVertex(d, color, alpha, brightness);
198
+ };
199
+ const putRibbon = (a, b, width, color, alpha, brightness = 1) => {
200
+ const segment = b.subtract(a);
201
+ let side = Vector3.Cross(segment, basis.axis);
202
+ if (side.lengthSquared() < 1e-4) side = cameraBasis.ribbonSide.clone();
203
+ side.normalize();
204
+ const half = side.scale(width * 0.5);
205
+ putQuad(a.subtract(half), a.add(half), b.add(half), b.subtract(half), color, alpha, brightness);
206
+ };
207
+ writeBolts(basis, electric, boltBlue, putRibbon);
208
+ while (vertexIndex < totalVertexCount) {
209
+ putVertex(Vector3.Zero(), suctionColor, 0);
233
210
  }
234
211
  mesh.updateVerticesData(VertexBuffer.PositionKind, positions, false, false);
235
212
  mesh.updateVerticesData(VertexBuffer.ColorKind, colors, false, false);
236
- }
237
- function resetLine(line, initial) {
238
- const basis = createSuctionBasis(params);
239
- line.d = initial ? randomRange(basis.mouthPadding + 0.05, basis.length) : basis.length + randomRange(0, basis.spawnFade);
240
- line.u = randomRange(-basis.spreadX * 0.5, basis.spreadX * 0.5);
241
- line.v = randomRange(-basis.spreadY * 0.5, basis.spreadY * 0.5);
242
- line.len = randomRange(basis.lineLength * 0.65, basis.lineLength * 1.35);
243
- line.width = randomRange(basis.lineWidth * 0.65, basis.lineWidth * 1.35);
244
- line.speed = Math.max(0, params.suctionSpeed * Math.max(0.01, params.scale)) * randomRange(0.78, 1.25);
245
- line.alphaMul = randomRange(0.65, 1);
246
- line.brightness = randomRange(0.82, 1);
247
- return line;
248
- }
249
- function resetParticle(particle, initial) {
250
- const basis = createSuctionBasis(params);
251
- particle.d = initial ? randomRange(basis.mouthPadding + 0.05, basis.length) : basis.length + randomRange(0, basis.spawnFade);
252
- particle.u = randomRange(-basis.spreadX * 0.55, basis.spreadX * 0.55);
253
- particle.v = randomRange(-basis.spreadY * 0.55, basis.spreadY * 0.55);
254
- particle.size = randomRange(basis.particleSize * 0.45, basis.particleSize * 1.15);
255
- particle.speed = Math.max(0, params.particleSpeed * Math.max(0.01, params.scale)) * randomRange(0.75, 1.35);
256
- particle.alphaMul = randomRange(0.35, 0.9);
257
- particle.brightness = randomRange(0.75, 1);
258
- return particle;
259
- }
260
- function randomRange(min, max) {
261
- const id2 = resetIndex;
262
- resetIndex += 1;
263
- return lerp(min, max, sequence01(id2, resetIndex % 8));
213
+ function writeBolts(currentBasis, boltColor, edgeColor, writeRibbon) {
214
+ for (let bolt = 0; bolt < boltCount; bolt += 1) {
215
+ const seed = sequence01(bolt + 7, bolt % 8);
216
+ const speedScale = Math.max(0, params.particleSpeed) / DEFAULT_PARAMS.particleSpeed;
217
+ const cycle = (time * speedScale * (1.45 + seed * 0.55) + seed) % 1;
218
+ const flicker = sequence01(bolt * 17 + Math.floor(time * 20), bolt % 8);
219
+ const active = flicker > 0.28 ? 1 : 0.18;
220
+ const angle = bolt * 2.399 + time * (1.7 + seed * 0.9);
221
+ const lengthScale = Math.max(0.04, params.lineLength / DEFAULT_PARAMS.lineLength);
222
+ const boltLength = lerp(currentBasis.length * 0.16, currentBasis.length * 0.28, seed) * lengthScale;
223
+ const headDistance = lerp(currentBasis.length * 0.92, currentBasis.mouthPadding * 1.15, cycle);
224
+ let previousDistance = Math.min(currentBasis.length, headDistance + boltLength);
225
+ let previous = suctionPosition(
226
+ previousDistance,
227
+ Math.cos(angle) * currentBasis.spreadX * 0.5 * lateralScale(previousDistance, currentBasis),
228
+ Math.sin(angle * 1.3) * currentBasis.spreadY * 0.5 * lateralScale(previousDistance, currentBasis),
229
+ currentBasis
230
+ );
231
+ for (let segment = 0; segment < BOLT_SEGMENTS; segment += 1) {
232
+ const t = (segment + 1) / BOLT_SEGMENTS;
233
+ const distance = Math.max(currentBasis.mouthPadding * 1.08, lerp(previousDistance, headDistance, t));
234
+ const suckScale = lateralScale(distance, currentBasis);
235
+ const jitter = (sequence01(bolt * 19 + segment + Math.floor(time * 18), segment) - 0.5) * 0.34 * currentBasis.spreadX * suckScale;
236
+ const curl = angle + t * (2.6 + seed * 1.8) + Math.sin(time * 4 + bolt) * 0.35;
237
+ const next = suctionPosition(
238
+ distance,
239
+ Math.cos(curl) * currentBasis.spreadX * suckScale * (1 - t * 0.45) + jitter,
240
+ Math.sin(curl * 1.25) * currentBasis.spreadY * suckScale * (1 - t * 0.5),
241
+ currentBasis
242
+ );
243
+ const mouthFade = smoothstep(0.04, 0.18, cycle) * (1 - smoothstep(0.88, 1, cycle));
244
+ writeRibbon(previous, next, basis.lineWidth * 0.58, segment % 2 === 0 ? boltColor : edgeColor, 0.38 * params.lineOpacity * active * mouthFade, 1.85);
245
+ previous = next;
246
+ }
247
+ }
248
+ }
264
249
  }
265
250
  }
251
+ function pushQuadIndices(indices, base) {
252
+ indices.push(base, base + 1, base + 2, base, base + 2, base + 3);
253
+ }
266
254
  function createSuctionBasis(params) {
267
255
  const scale = Math.max(0.01, params.scale);
268
256
  const axis = new Vector3(params.directionX, 0, params.directionZ);
@@ -360,7 +348,7 @@ function createSuctionMaterial(scene) {
360
348
  });
361
349
  material.backFaceCulling = false;
362
350
  material.transparencyMode = Material.MATERIAL_ALPHABLEND;
363
- material.alphaMode = Constants.ALPHA_COMBINE;
351
+ material.alphaMode = Constants.ALPHA_ADD;
364
352
  material.disableDepthWrite = true;
365
353
  return material;
366
354
  }
@@ -389,12 +377,6 @@ function registerSuctionShaders() {
389
377
  `;
390
378
  }
391
379
  }
392
- function lineFade(distance, basis) {
393
- const mouthFade = smoothstep(basis.mouthPadding, basis.mouthPadding + basis.fadeDistance, distance);
394
- const clampedDistance = Math.min(distance, basis.length);
395
- const outerFade = smoothstep(0, basis.spawnFade, basis.length - clampedDistance);
396
- return mouthFade * outerFade;
397
- }
398
380
  function toColor3(color) {
399
381
  return new Color3(clamp01(color.r), clamp01(color.g), clamp01(color.b));
400
382
  }
@@ -3,12 +3,10 @@ import { Vector3 } from "@babylonjs/core/Maths/math.vector";
3
3
  import { Constants } from "@babylonjs/core/Engines/constants";
4
4
  import { DynamicTexture } from "@babylonjs/core/Materials/Textures/dynamicTexture";
5
5
  import { StandardMaterial } from "@babylonjs/core/Materials/standardMaterial";
6
- import { CreateCylinder } from "@babylonjs/core/Meshes/Builders/cylinderBuilder";
7
6
  import { CreateDisc } from "@babylonjs/core/Meshes/Builders/discBuilder";
8
7
  import { CreatePlane } from "@babylonjs/core/Meshes/Builders/planeBuilder";
9
- import { CreateTorus } from "@babylonjs/core/Meshes/Builders/torusBuilder";
10
8
  import { ParticleSystem } from "@babylonjs/core/Particles/particleSystem";
11
- import { c as configureVfxParticleSystem, V as VFX_RENDERING_GROUP_ID } from "../../chunks/rendering-CV3nhygX.js";
9
+ import { c as configureVfxParticleSystem } from "../../chunks/rendering-CV3nhygX.js";
12
10
  const id = "player-upgrade";
13
11
  const nameZh = "人物升级光效";
14
12
  const source = "runtime-generated";
@@ -22,6 +20,7 @@ const manifest = {
22
20
  const strandTextureByScene = /* @__PURE__ */ new WeakMap();
23
21
  const glowTextureByScene = /* @__PURE__ */ new WeakMap();
24
22
  const haloTextureByScene = /* @__PURE__ */ new WeakMap();
23
+ const ringTextureByScene = /* @__PURE__ */ new WeakMap();
25
24
  const DEFAULT_PARAMS = {
26
25
  lifetime: 2.4,
27
26
  scale: 1,
@@ -73,13 +72,13 @@ const playerUpgradeEffectPackage = {
73
72
  { key: "glowHeight", label: "光柱高度", kind: "number", min: 0.2, max: 5, step: 0.01 },
74
73
  { key: "glowWidth", label: "光柱宽度", kind: "number", min: 0.1, max: 4, step: 0.01 }
75
74
  ],
76
- create({ scene, root, position, params }) {
75
+ create({ scene, root, position, params, renderingGroupId, scale }) {
77
76
  markVfxRoot(root);
78
77
  root.position.copyFrom(position);
79
- const strands = createStrandSystem(scene, root, params);
80
- const strandMeshes = createStrandMeshes(scene, root, params);
81
- const rings = createExpandingRings(scene, root, params);
82
- const glowAura = createGlowAura(scene, root, params);
78
+ const groupId = renderingGroupId ?? 0;
79
+ const strands = createStrandSystem(scene, root, params, groupId, scale);
80
+ const rings = createExpandingRings(scene, root, params, groupId);
81
+ const glowAura = createGlowAura(scene, root, params, groupId);
83
82
  let disposed = false;
84
83
  const disposeTimer = window.setTimeout(() => disposeAll(), getDisposeDelaySeconds(params) * 1e3);
85
84
  strands.start();
@@ -95,7 +94,6 @@ const playerUpgradeEffectPackage = {
95
94
  window.clearTimeout(disposeTimer);
96
95
  strands.stop();
97
96
  strands.dispose(false);
98
- strandMeshes.dispose();
99
97
  rings.dispose();
100
98
  glowAura.dispose();
101
99
  cleanupPlanarShadowArtifacts(scene);
@@ -133,7 +131,7 @@ function cleanupPlanarShadowArtifacts(scene) {
133
131
  }
134
132
  }
135
133
  }
136
- function createGlowAura(scene, root, params) {
134
+ function createGlowAura(scene, root, params, renderingGroupId) {
137
135
  const effectScale = Math.max(0.05, params.scale);
138
136
  const color = params.color;
139
137
  const glowHeight = Math.max(0.1, params.glowHeight) * effectScale;
@@ -148,7 +146,7 @@ function createGlowAura(scene, root, params) {
148
146
  mesh.position.y = glowHeight * 0.48;
149
147
  mesh.rotation.y = index * Math.PI / 3;
150
148
  mesh.scaling.set(glowWidth, glowHeight, 1);
151
- mesh.renderingGroupId = VFX_RENDERING_GROUP_ID;
149
+ mesh.renderingGroupId = renderingGroupId;
152
150
  const material = new StandardMaterial(`player-upgrade.soft-glow-column-material.${index}`, scene);
153
151
  configureTexturedGlowMaterial(material, color, getGlowTexture(scene), 2.8);
154
152
  material.alpha = 0;
@@ -162,7 +160,7 @@ function createGlowAura(scene, root, params) {
162
160
  halo.position.y = 0.025 * effectScale;
163
161
  halo.rotation.x = Math.PI / 2;
164
162
  halo.scaling.set(glowWidth * 1.12, glowWidth * 1.12, 1);
165
- halo.renderingGroupId = VFX_RENDERING_GROUP_ID;
163
+ halo.renderingGroupId = renderingGroupId;
166
164
  const haloMaterial = new StandardMaterial("player-upgrade.ground-halo-material", scene);
167
165
  configureTexturedGlowMaterial(haloMaterial, color, getHaloTexture(scene), 2.4);
168
166
  haloMaterial.alpha = 0;
@@ -198,19 +196,23 @@ function createGlowAura(scene, root, params) {
198
196
  }
199
197
  return { dispose };
200
198
  }
201
- function createStrandSystem(scene, root, params) {
199
+ function createStrandSystem(scene, root, params, renderingGroupId, spawnScale) {
202
200
  const effectScale = Math.max(0.05, params.scale);
201
+ const spawnHorizontalScale = getHorizontalScale(spawnScale);
202
+ const spawnVerticalScale = Math.max(0.01, Math.abs(spawnScale.y));
203
203
  const count = Math.max(0, Math.round(params.strandCount));
204
204
  const color = params.color;
205
205
  const bright = brighten(color, 0.72);
206
- const strandLength = Math.max(0.05, params.strandLength) * effectScale;
206
+ const strandLength = Math.max(0.05, params.strandLength) * effectScale * spawnVerticalScale;
207
207
  const strandRadius = Math.max(0, params.strandRadius) * effectScale;
208
- const strandWidth = Math.max(1e-3, params.strandWidth) * effectScale;
208
+ const strandWidth = Math.max(1e-3, params.strandWidth) * effectScale * spawnHorizontalScale;
209
209
  const system = new ParticleSystem("player-upgrade.gold-strands", Math.max(1, count), scene);
210
210
  configureVfxParticleSystem(system);
211
+ system.renderingGroupId = renderingGroupId;
211
212
  system.particleTexture = getStrandTexture(scene);
212
213
  system.particleTexture.hasAlpha = true;
213
214
  system.emitter = root;
215
+ system.isLocal = true;
214
216
  system.manualEmitCount = count;
215
217
  system.emitRate = count * 60;
216
218
  system.targetStopDuration = 0.05;
@@ -224,9 +226,9 @@ function createStrandSystem(scene, root, params) {
224
226
  system.maxScaleX = strandWidth * 1.8;
225
227
  system.minScaleY = 1;
226
228
  system.maxScaleY = 1.6;
227
- system.minEmitPower = Math.max(0.01, params.riseSpeed * effectScale * 0.55);
228
- system.maxEmitPower = Math.max(system.minEmitPower, params.riseSpeed * effectScale * 1.28);
229
- system.gravity = new Vector3(0, 0.34 * effectScale, 0);
229
+ system.minEmitPower = Math.max(0.01, params.riseSpeed * effectScale * spawnVerticalScale * 0.55);
230
+ system.maxEmitPower = Math.max(system.minEmitPower, params.riseSpeed * effectScale * spawnVerticalScale * 1.28);
231
+ system.gravity = new Vector3(0, 0.34 * effectScale * spawnVerticalScale, 0);
230
232
  system.updateSpeed = 1 / 60;
231
233
  system.color1 = toColor4(color, params.opacity * 0.72);
232
234
  system.color2 = toColor4(bright, params.opacity);
@@ -267,102 +269,28 @@ function createStrandSystem(scene, root, params) {
267
269
  };
268
270
  return system;
269
271
  }
270
- function createStrandMeshes(scene, root, params) {
271
- const effectScale = Math.max(0.05, params.scale);
272
- const count = Math.min(90, Math.max(0, Math.round(params.strandCount * 0.62)));
273
- const color = params.color;
274
- const radius = Math.max(0.04, params.strandRadius * effectScale);
275
- const strandLength = Math.max(0.05, params.strandLength * effectScale);
276
- const riseHeight = Math.max(0.1, params.strandHeight * effectScale);
277
- const width = Math.max(5e-3, params.strandWidth * effectScale * 1.25);
278
- const speedScale = Math.max(0.2, params.riseSpeed / DEFAULT_PARAMS.riseSpeed);
279
- const duration = Math.max(0.22, params.lifetime * 0.72 / speedScale);
280
- const states = [];
281
- for (let index = 0; index < count; index += 1) {
282
- const angle = sequence01(index, 0) * Math.PI * 2;
283
- const localRadius = radius * lerp(0.42, 1.08, sequence01(index, 1));
284
- const mesh = CreateCylinder(
285
- `player-upgrade.gold-strand-mesh.${index}`,
286
- { height: 1, diameter: width, tessellation: 6 },
287
- scene
288
- );
289
- prepareVfxMesh(mesh);
290
- mesh.parent = root;
291
- mesh.position.x = Math.cos(angle) * localRadius;
292
- mesh.position.z = Math.sin(angle) * localRadius;
293
- mesh.position.y = 0;
294
- mesh.renderingGroupId = VFX_RENDERING_GROUP_ID;
295
- mesh.isVisible = false;
296
- const material = new StandardMaterial(`player-upgrade.gold-strand-mesh-material.${index}`, scene);
297
- configureGlowMaterial(material, color, 2.8);
298
- material.alpha = 0;
299
- mesh.material = material;
300
- states.push({
301
- mesh,
302
- material,
303
- height: strandLength * lerp(0.68, 1.18, sequence01(index, 2)),
304
- startDelay: sequence01(index, 3) * Math.min(0.34, params.lifetime * 0.18),
305
- duration: duration * lerp(0.78, 1.12, sequence01(index, 4))
306
- });
307
- }
308
- let age = 0;
309
- let disposed = false;
310
- const observer = scene.onBeforeRenderObservable.add(() => {
311
- if (disposed) return;
312
- const deltaSeconds = Math.min(0.05, scene.getEngine().getDeltaTime() / 1e3);
313
- age += deltaSeconds;
314
- for (const state of states) {
315
- const localAge = age - state.startDelay;
316
- if (localAge < 0 || localAge > state.duration) {
317
- state.mesh.isVisible = false;
318
- state.material.alpha = 0;
319
- continue;
320
- }
321
- const t = Math.min(1, localAge / state.duration);
322
- const grow = Math.min(1, t / 0.32);
323
- const fadeOut = Math.max(0, 1 - Math.max(0, t - 0.66) / 0.34);
324
- const alpha = params.opacity * fadeOut * Math.min(1, t / 0.12);
325
- const currentHeight = state.height * grow;
326
- state.mesh.isVisible = alpha > 0.01;
327
- state.mesh.scaling.set(1, currentHeight, 1);
328
- state.mesh.position.y = currentHeight * 0.5 + riseHeight * easeOutCubic(t);
329
- state.material.alpha = Math.min(1, alpha * 1.35);
330
- }
331
- if (age >= params.lifetime + 0.45) dispose();
332
- });
333
- function dispose() {
334
- if (disposed) return;
335
- disposed = true;
336
- scene.onBeforeRenderObservable.remove(observer);
337
- for (const state of states) {
338
- state.mesh.dispose();
339
- state.material.dispose();
340
- }
341
- }
342
- return { dispose };
343
- }
344
- function createExpandingRings(scene, root, params) {
272
+ function createExpandingRings(scene, root, params, renderingGroupId) {
345
273
  const effectScale = Math.max(0.05, params.scale);
346
274
  const ringCount = Math.max(0, Math.round(params.ringCount));
347
275
  const ringDuration = Math.max(0.18, Math.min(0.9, params.lifetime * 0.38));
348
276
  const minRadius = 0.14 * effectScale;
349
277
  const maxRadius = Math.max(minRadius + 0.01, params.ringMaxRadius * effectScale);
350
- const thickness = Math.max(2e-3, params.ringThickness * effectScale);
351
278
  const color = params.color;
352
279
  const states = [];
353
280
  for (let index = 0; index < ringCount; index += 1) {
354
- const mesh = CreateTorus(
281
+ const mesh = CreatePlane(
355
282
  `player-upgrade.expanding-ring.${index}`,
356
- { diameter: 1, thickness, tessellation: 96 },
283
+ { size: 2 },
357
284
  scene
358
285
  );
359
286
  prepareVfxMesh(mesh);
360
287
  mesh.parent = root;
361
288
  mesh.position.y = 0.03 * effectScale + index * 0.012 * effectScale;
362
- mesh.renderingGroupId = VFX_RENDERING_GROUP_ID;
289
+ mesh.rotation.x = Math.PI / 2;
290
+ mesh.renderingGroupId = renderingGroupId;
363
291
  mesh.isVisible = false;
364
292
  const material = new StandardMaterial(`player-upgrade.ring-material.${index}`, scene);
365
- configureGlowMaterial(material, color, 2.4);
293
+ configureTexturedGlowMaterial(material, color, getRingTexture(scene), 2.4);
366
294
  material.alpha = 0;
367
295
  mesh.material = material;
368
296
  states.push({
@@ -390,7 +318,7 @@ function createExpandingRings(scene, root, params) {
390
318
  const radius = lerp(minRadius, maxRadius, eased);
391
319
  const alpha = params.ringOpacity * (1 - t) * Math.min(1, t / 0.12);
392
320
  state.mesh.isVisible = alpha > 0.01;
393
- state.mesh.scaling.set(radius, radius, radius);
321
+ state.mesh.scaling.set(radius, radius, 1);
394
322
  const ringColor = brighten(color, 0.1 + 0.08 * (1 - t));
395
323
  state.material.diffuseColor = toColor3(ringColor);
396
324
  state.material.emissiveColor = toGlowColor3(ringColor, 3.4);
@@ -477,6 +405,45 @@ function getHaloTexture(scene) {
477
405
  haloTextureByScene.set(scene, texture);
478
406
  return texture;
479
407
  }
408
+ function getRingTexture(scene) {
409
+ const existing = ringTextureByScene.get(scene);
410
+ if (existing) return existing;
411
+ const size = 256;
412
+ const texture = new DynamicTexture("player_upgrade_expanding_ring_texture", { width: size, height: size }, scene, true);
413
+ const context = texture.getContext();
414
+ const canvasContext = context;
415
+ context.clearRect(0, 0, size, size);
416
+ const center = size / 2;
417
+ const radius = size * 0.39;
418
+ const strokeWidth = size * 0.105;
419
+ canvasContext.lineCap = "round";
420
+ canvasContext.lineJoin = "round";
421
+ const outerGlow = context.createRadialGradient(center, center, radius - strokeWidth * 1.1, center, center, radius + strokeWidth * 1.8);
422
+ outerGlow.addColorStop(0, "rgba(255,255,255,0)");
423
+ outerGlow.addColorStop(0.38, "rgba(255,255,255,0.28)");
424
+ outerGlow.addColorStop(0.58, "rgba(255,255,255,0.34)");
425
+ outerGlow.addColorStop(1, "rgba(255,255,255,0)");
426
+ context.strokeStyle = outerGlow;
427
+ context.lineWidth = strokeWidth * 2.6;
428
+ context.beginPath();
429
+ context.arc(center, center, radius, 0, Math.PI * 2);
430
+ context.stroke();
431
+ const core = context.createRadialGradient(center, center, radius - strokeWidth * 0.85, center, center, radius + strokeWidth * 0.85);
432
+ core.addColorStop(0, "rgba(255,255,255,0)");
433
+ core.addColorStop(0.42, "rgba(255,255,255,0.72)");
434
+ core.addColorStop(0.5, "rgba(255,255,255,1)");
435
+ core.addColorStop(0.62, "rgba(255,255,255,0.62)");
436
+ core.addColorStop(1, "rgba(255,255,255,0)");
437
+ context.strokeStyle = core;
438
+ context.lineWidth = strokeWidth;
439
+ context.beginPath();
440
+ context.arc(center, center, radius, 0, Math.PI * 2);
441
+ context.stroke();
442
+ texture.hasAlpha = true;
443
+ texture.update(false);
444
+ ringTextureByScene.set(scene, texture);
445
+ return texture;
446
+ }
480
447
  function getDisposeDelaySeconds(params) {
481
448
  const ringDuration = Math.max(0.18, Math.min(0.9, params.lifetime * 0.38));
482
449
  return Math.max(0.2, params.lifetime) + getLastRingEndTime(params, ringDuration) + 0.35;
@@ -484,6 +451,9 @@ function getDisposeDelaySeconds(params) {
484
451
  function getLastRingEndTime(params, ringDuration) {
485
452
  return Math.max(0, Math.round(params.ringCount) - 1) * Math.max(0.02, params.ringInterval) + ringDuration;
486
453
  }
454
+ function getHorizontalScale(scale) {
455
+ return Math.max(0.01, (Math.abs(scale.x) + Math.abs(scale.z)) * 0.5);
456
+ }
487
457
  function toColor4(color, alpha) {
488
458
  return new Color4(clamp01(color.r), clamp01(color.g), clamp01(color.b), clamp01(alpha));
489
459
  }
package/index.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "vfx-index/1.0",
3
- "count": 39,
3
+ "count": 40,
4
4
  "effects": [
5
5
  {
6
6
  "id": "build.particle.lumber_debris_scatter",
@@ -147,6 +147,16 @@
147
147
  "media": {},
148
148
  "path": "packages/effects/explosion"
149
149
  },
150
+ {
151
+ "id": "golden-bubble-pop",
152
+ "dir": "golden-bubble-pop",
153
+ "nameZh": "金色泡泡炸开",
154
+ "kind": "code",
155
+ "category": "golden-bubble-pop",
156
+ "tags": [],
157
+ "media": {},
158
+ "path": "packages/effects/golden-bubble-pop"
159
+ },
150
160
  {
151
161
  "id": "ground-tile-upgrade",
152
162
  "dir": "ground-tile-upgrade",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fps-games/vfx",
3
- "version": "0.3.11",
3
+ "version": "0.3.14",
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": {
@@ -0,0 +1,5 @@
1
+ {
2
+ "schemaVersion": "vfx-params/1.0",
3
+ "effectId": "golden-bubble-pop",
4
+ "params": {}
5
+ }