@fps-games/vfx 0.3.14 → 0.3.16
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 +8 -0
- package/dist/effects/magnet-suction/index.d.ts +2 -0
- package/dist/effects/magnet-suction/index.js +106 -83
- package/dist/effects/pulse-laser-volley-sequence/index.d.ts +14 -0
- package/dist/effects/pulse-laser-volley-sequence/index.js +142 -0
- package/index.json +11 -1
- package/package.json +1 -1
- package/packages/effects/pulse-laser-volley-sequence/vfx-params.json +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
|
|
13
13
|
<!-- release.mjs 在此行下方插入新版本区块 -->
|
|
14
14
|
|
|
15
|
+
## v0.3.16 — 2026-07-08
|
|
16
|
+
|
|
17
|
+
### 改动
|
|
18
|
+
- feat(pulse-laser-volley-sequence): add single-shot pulse laser variant
|
|
19
|
+
|
|
20
|
+
### 特效
|
|
21
|
+
- 新增:pulse-laser-volley-sequence
|
|
22
|
+
|
|
15
23
|
## v0.3.11 — 2026-07-07
|
|
16
24
|
|
|
17
25
|
### 改动
|
|
@@ -20,13 +20,14 @@ const manifest = {
|
|
|
20
20
|
assets,
|
|
21
21
|
source
|
|
22
22
|
};
|
|
23
|
-
const BOLT_SEGMENTS = 6;
|
|
24
23
|
const DEFAULT_PARAMS = {
|
|
25
24
|
lifetimeMode: "infinite",
|
|
26
25
|
lifetime: 2.5,
|
|
27
26
|
scale: 1.15,
|
|
28
27
|
initialHeight: 0,
|
|
29
|
-
color: { r: 0.
|
|
28
|
+
color: { r: 0.82, g: 0.95, b: 1 },
|
|
29
|
+
lineColor: { r: 0.82, g: 0.95, b: 1 },
|
|
30
|
+
particleColor: { r: 0.82, g: 0.95, b: 1 },
|
|
30
31
|
opacity: 1,
|
|
31
32
|
intensity: 1,
|
|
32
33
|
forwardOffset: 0,
|
|
@@ -38,18 +39,18 @@ const DEFAULT_PARAMS = {
|
|
|
38
39
|
fadeDistance: 0.58,
|
|
39
40
|
spawnFade: 0.45,
|
|
40
41
|
suctionSpeed: 1.65,
|
|
41
|
-
lineCount:
|
|
42
|
+
lineCount: 32,
|
|
42
43
|
lineLength: 0.46,
|
|
43
|
-
lineWidth: 0.
|
|
44
|
+
lineWidth: 0.045,
|
|
44
45
|
lineOpacity: 1,
|
|
45
46
|
lineSpread: 1,
|
|
46
47
|
lineTwist: 0,
|
|
47
|
-
particleCount:
|
|
48
|
-
particleSize: 0.
|
|
49
|
-
particleSpeed:
|
|
48
|
+
particleCount: 22,
|
|
49
|
+
particleSize: 0.06,
|
|
50
|
+
particleSpeed: 2.15,
|
|
50
51
|
particleDrift: 0,
|
|
51
52
|
particleOuterOffset: 0,
|
|
52
|
-
particleOpacity: 0.
|
|
53
|
+
particleOpacity: 0.85,
|
|
53
54
|
directionX: 0,
|
|
54
55
|
directionZ: -1
|
|
55
56
|
};
|
|
@@ -69,7 +70,8 @@ const magnetSuctionEffectPackage = {
|
|
|
69
70
|
{ key: "lifetime", label: "自定义时长", kind: "lifetime", min: 0.1, max: 30, step: 0.01 },
|
|
70
71
|
{ key: "scale", label: "整体缩放", kind: "scale", min: 0.05, max: 6, step: 0.01 },
|
|
71
72
|
{ key: "initialHeight", label: "局部高度", kind: "number", min: -2, max: 4, step: 0.01 },
|
|
72
|
-
{ key: "
|
|
73
|
+
{ key: "lineColor", label: "线条颜色", kind: "color" },
|
|
74
|
+
{ key: "particleColor", label: "圆点颜色", kind: "color" },
|
|
73
75
|
{ key: "opacity", label: "整体透明度", kind: "opacity", min: 0, max: 1, step: 0.01 },
|
|
74
76
|
{ key: "intensity", label: "强度", kind: "opacity", min: 0, max: 1, step: 0.01 },
|
|
75
77
|
{ key: "forwardOffset", label: "入口前偏移", kind: "number", min: -2, max: 4, step: 0.01 },
|
|
@@ -80,14 +82,16 @@ const magnetSuctionEffectPackage = {
|
|
|
80
82
|
{ key: "mouthPadding", label: "入口保护距离", kind: "number", min: 0.01, max: 2, step: 0.01 },
|
|
81
83
|
{ key: "fadeDistance", label: "入口淡出距离", kind: "number", min: 0.01, max: 3, step: 0.01 },
|
|
82
84
|
{ key: "spawnFade", label: "外侧淡入距离", kind: "number", min: 0.01, max: 3, step: 0.01 },
|
|
83
|
-
{ key: "
|
|
84
|
-
{ key: "
|
|
85
|
-
{ key: "
|
|
85
|
+
{ key: "suctionSpeed", label: "线条速度", kind: "number", min: 0, max: 8, step: 0.01 },
|
|
86
|
+
{ key: "lineCount", label: "白线数量", kind: "number", min: 0, max: 96, step: 1 },
|
|
87
|
+
{ key: "lineLength", label: "白线长度", kind: "number", min: 0.02, max: 2, step: 0.01 },
|
|
88
|
+
{ key: "lineWidth", label: "白线尾宽", kind: "number", min: 2e-3, max: 0.18, step: 1e-3 },
|
|
89
|
+
{ key: "lineOpacity", label: "白线透明度", kind: "opacity", min: 0, max: 1.5, step: 0.01 },
|
|
86
90
|
{ key: "lineSpread", label: "散布缩放", kind: "number", min: 0.1, max: 2, step: 0.01 },
|
|
87
|
-
{ key: "particleCount", label: "
|
|
88
|
-
{ key: "particleSize", label: "
|
|
89
|
-
{ key: "particleSpeed", label: "
|
|
90
|
-
{ key: "particleOpacity", label: "
|
|
91
|
+
{ key: "particleCount", label: "粒子数量", kind: "number", min: 0, max: 80, step: 1 },
|
|
92
|
+
{ key: "particleSize", label: "粒子大小", kind: "number", min: 5e-3, max: 0.3, step: 1e-3 },
|
|
93
|
+
{ key: "particleSpeed", label: "粒子速度", kind: "number", min: 0, max: 10, step: 0.01 },
|
|
94
|
+
{ key: "particleOpacity", label: "粒子透明度", kind: "opacity", min: 0, max: 1, step: 0.01 },
|
|
91
95
|
{ key: "directionX", label: "吸入方向 X", kind: "number", min: -1, max: 1, step: 0.01 },
|
|
92
96
|
{ key: "directionZ", label: "吸入方向 Z", kind: "number", min: -1, max: 1, step: 0.01 }
|
|
93
97
|
],
|
|
@@ -138,15 +142,26 @@ function createMagnetSuctionField(scene, root, params, renderingGroupId) {
|
|
|
138
142
|
}
|
|
139
143
|
}
|
|
140
144
|
function createSuctionMesh(scene, root, params, material, renderingGroupId) {
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
const
|
|
145
|
+
const lineCount = Math.max(0, Math.round(params.lineCount));
|
|
146
|
+
const particleCount = Math.max(0, Math.round(params.particleCount));
|
|
147
|
+
const lineVertexCount = lineCount * 3;
|
|
148
|
+
const particleVertexCount = particleCount * 4;
|
|
149
|
+
const totalVertexCount = lineVertexCount + particleVertexCount;
|
|
144
150
|
const positions = new Float32Array(totalVertexCount * 3);
|
|
145
151
|
const colors = new Float32Array(totalVertexCount * 4);
|
|
146
152
|
const indices = [];
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
153
|
+
const lines = [];
|
|
154
|
+
const particles = [];
|
|
155
|
+
let resetIndex = 1;
|
|
156
|
+
for (let index = 0; index < lineCount; index += 1) {
|
|
157
|
+
const base = index * 3;
|
|
158
|
+
indices.push(base, base + 1, base + 2);
|
|
159
|
+
lines.push(resetLine({}, true));
|
|
160
|
+
}
|
|
161
|
+
for (let index = 0; index < particleCount; index += 1) {
|
|
162
|
+
const base = lineVertexCount + index * 4;
|
|
163
|
+
indices.push(base, base + 1, base + 2, base, base + 2, base + 3);
|
|
164
|
+
particles.push(resetParticle({}, true));
|
|
150
165
|
}
|
|
151
166
|
const mesh = new Mesh("magnet-suction.mesh", scene);
|
|
152
167
|
mesh.parent = root;
|
|
@@ -164,7 +179,15 @@ function createSuctionMesh(scene, root, params, material, renderingGroupId) {
|
|
|
164
179
|
writeGeometry();
|
|
165
180
|
return { update, dispose };
|
|
166
181
|
function update(deltaSeconds) {
|
|
167
|
-
|
|
182
|
+
const basis = createSuctionBasis(params);
|
|
183
|
+
for (const line of lines) {
|
|
184
|
+
line.d -= line.speed * deltaSeconds;
|
|
185
|
+
if (line.d < basis.mouthPadding) resetLine(line, false);
|
|
186
|
+
}
|
|
187
|
+
for (const particle of particles) {
|
|
188
|
+
particle.d -= particle.speed * deltaSeconds;
|
|
189
|
+
if (particle.d < basis.mouthPadding) resetParticle(particle, false);
|
|
190
|
+
}
|
|
168
191
|
writeGeometry();
|
|
169
192
|
}
|
|
170
193
|
function dispose() {
|
|
@@ -173,9 +196,8 @@ function createSuctionMesh(scene, root, params, material, renderingGroupId) {
|
|
|
173
196
|
function writeGeometry() {
|
|
174
197
|
const basis = createSuctionBasis(params);
|
|
175
198
|
const cameraBasis = createCameraBasis(scene, root, basis);
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
const boltBlue = new Color3(0.25, 0.62, 1);
|
|
199
|
+
const lineColor = toColor3(params.lineColor ?? params.color);
|
|
200
|
+
const particleColor = toColor3(params.particleColor ?? params.color);
|
|
179
201
|
const alphaScale = clamp01(params.opacity) * Math.max(0, params.intensity);
|
|
180
202
|
let vertexIndex = 0;
|
|
181
203
|
const putVertex = (position, color, alpha, brightness = 1) => {
|
|
@@ -190,66 +212,61 @@ function createSuctionMesh(scene, root, params, material, renderingGroupId) {
|
|
|
190
212
|
colors[colorOffset + 3] = clamp01(alpha * alphaScale);
|
|
191
213
|
vertexIndex += 1;
|
|
192
214
|
};
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
215
|
+
for (const line of lines) {
|
|
216
|
+
const alpha = lineFade(line.d, basis) * params.lineOpacity * line.alphaMul;
|
|
217
|
+
const headD = Math.max(basis.mouthPadding, line.d);
|
|
218
|
+
const tailD = Math.min(basis.length, line.d + line.len);
|
|
219
|
+
const head = suctionPosition(headD, line.u, line.v, basis);
|
|
220
|
+
const tail = suctionPosition(tailD, line.u, line.v, basis);
|
|
221
|
+
const shrink = Math.max(lateralScale(headD, basis), 0.18);
|
|
222
|
+
const half = cameraBasis.ribbonSide.scale(line.width * shrink * 0.5);
|
|
223
|
+
putVertex(tail.subtract(half), lineColor, alpha, line.brightness);
|
|
224
|
+
putVertex(tail.add(half), lineColor, alpha * 0.92, line.brightness);
|
|
225
|
+
putVertex(head, lineColor, alpha * 0.22, line.brightness);
|
|
226
|
+
}
|
|
227
|
+
for (const particle of particles) {
|
|
228
|
+
const fade = lineFade(particle.d, basis);
|
|
229
|
+
const alpha = Math.sqrt(fade) * params.particleOpacity * particle.alphaMul;
|
|
230
|
+
const center = suctionPosition(particle.d, particle.u, particle.v, basis);
|
|
231
|
+
const shrink = Math.max(lateralScale(particle.d, basis), 0.48);
|
|
232
|
+
const right = cameraBasis.particleRight.scale(particle.size * shrink);
|
|
233
|
+
const up = cameraBasis.particleUp.scale(particle.size * shrink * 0.7);
|
|
234
|
+
putVertex(center.subtract(right).subtractInPlace(up), particleColor, alpha, particle.brightness);
|
|
235
|
+
putVertex(center.add(right).subtractInPlace(up), particleColor, alpha, particle.brightness);
|
|
236
|
+
putVertex(center.add(right).addInPlace(up), particleColor, alpha * 0.8, particle.brightness);
|
|
237
|
+
putVertex(center.subtract(right).addInPlace(up), particleColor, alpha * 0.8, particle.brightness);
|
|
210
238
|
}
|
|
211
239
|
mesh.updateVerticesData(VertexBuffer.PositionKind, positions, false, false);
|
|
212
240
|
mesh.updateVerticesData(VertexBuffer.ColorKind, colors, false, false);
|
|
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
|
-
}
|
|
249
241
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
242
|
+
function resetLine(line, initial) {
|
|
243
|
+
const basis = createSuctionBasis(params);
|
|
244
|
+
line.d = initial ? randomRange(basis.mouthPadding + 0.05, basis.length) : basis.length + randomRange(0, basis.spawnFade);
|
|
245
|
+
line.u = randomRange(-basis.spreadX * 0.5, basis.spreadX * 0.5);
|
|
246
|
+
line.v = randomRange(-basis.spreadY * 0.5, basis.spreadY * 0.5);
|
|
247
|
+
line.len = randomRange(basis.lineLength * 0.65, basis.lineLength * 1.35);
|
|
248
|
+
line.width = randomRange(basis.lineWidth * 0.65, basis.lineWidth * 1.35);
|
|
249
|
+
line.speed = Math.max(0, params.suctionSpeed * Math.max(0.01, params.scale)) * randomRange(0.78, 1.25);
|
|
250
|
+
line.alphaMul = randomRange(0.65, 1);
|
|
251
|
+
line.brightness = randomRange(0.82, 1);
|
|
252
|
+
return line;
|
|
253
|
+
}
|
|
254
|
+
function resetParticle(particle, initial) {
|
|
255
|
+
const basis = createSuctionBasis(params);
|
|
256
|
+
particle.d = initial ? randomRange(basis.mouthPadding + 0.05, basis.length) : basis.length + randomRange(0, basis.spawnFade);
|
|
257
|
+
particle.u = randomRange(-basis.spreadX * 0.55, basis.spreadX * 0.55);
|
|
258
|
+
particle.v = randomRange(-basis.spreadY * 0.55, basis.spreadY * 0.55);
|
|
259
|
+
particle.size = randomRange(basis.particleSize * 0.45, basis.particleSize * 1.15);
|
|
260
|
+
particle.speed = Math.max(0, params.particleSpeed * Math.max(0.01, params.scale)) * randomRange(0.75, 1.35);
|
|
261
|
+
particle.alphaMul = randomRange(0.35, 0.9);
|
|
262
|
+
particle.brightness = randomRange(0.75, 1);
|
|
263
|
+
return particle;
|
|
264
|
+
}
|
|
265
|
+
function randomRange(min, max) {
|
|
266
|
+
const id2 = resetIndex;
|
|
267
|
+
resetIndex += 1;
|
|
268
|
+
return lerp(min, max, sequence01(id2, resetIndex % 8));
|
|
269
|
+
}
|
|
253
270
|
}
|
|
254
271
|
function createSuctionBasis(params) {
|
|
255
272
|
const scale = Math.max(0.01, params.scale);
|
|
@@ -348,7 +365,7 @@ function createSuctionMaterial(scene) {
|
|
|
348
365
|
});
|
|
349
366
|
material.backFaceCulling = false;
|
|
350
367
|
material.transparencyMode = Material.MATERIAL_ALPHABLEND;
|
|
351
|
-
material.alphaMode = Constants.
|
|
368
|
+
material.alphaMode = Constants.ALPHA_COMBINE;
|
|
352
369
|
material.disableDepthWrite = true;
|
|
353
370
|
return material;
|
|
354
371
|
}
|
|
@@ -377,6 +394,12 @@ function registerSuctionShaders() {
|
|
|
377
394
|
`;
|
|
378
395
|
}
|
|
379
396
|
}
|
|
397
|
+
function lineFade(distance, basis) {
|
|
398
|
+
const mouthFade = smoothstep(basis.mouthPadding, basis.mouthPadding + basis.fadeDistance, distance);
|
|
399
|
+
const clampedDistance = Math.min(distance, basis.length);
|
|
400
|
+
const outerFade = smoothstep(0, basis.spawnFade, basis.length - clampedDistance);
|
|
401
|
+
return mouthFade * outerFade;
|
|
402
|
+
}
|
|
380
403
|
function toColor3(color) {
|
|
381
404
|
return new Color3(clamp01(color.r), clamp01(color.g), clamp01(color.b));
|
|
382
405
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { VfxColorValue, VfxEffectPackage, VfxParamValues } from '../../types';
|
|
2
|
+
interface PulseLaserParams extends VfxParamValues {
|
|
3
|
+
lifetimeMode: string;
|
|
4
|
+
lifetime: number;
|
|
5
|
+
targetDistance: number;
|
|
6
|
+
boltSpeed: number;
|
|
7
|
+
boltLen: number;
|
|
8
|
+
boltColor: VfxColorValue;
|
|
9
|
+
boltCore: number;
|
|
10
|
+
boltCorePow: number;
|
|
11
|
+
boltBright: number;
|
|
12
|
+
}
|
|
13
|
+
export declare const pulseLaserVolleySequenceEffectPackage: VfxEffectPackage<PulseLaserParams>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { CreatePlane } from "@babylonjs/core/Meshes/Builders/planeBuilder";
|
|
2
|
+
import { ShaderMaterial } from "@babylonjs/core/Materials/shaderMaterial";
|
|
3
|
+
import { Effect } from "@babylonjs/core/Materials/effect";
|
|
4
|
+
import { Texture } from "@babylonjs/core/Materials/Textures/texture";
|
|
5
|
+
import { Color3 } from "@babylonjs/core/Maths/math.color";
|
|
6
|
+
import { Vector3 } from "@babylonjs/core/Maths/math.vector";
|
|
7
|
+
import { Space } from "@babylonjs/core/Maths/math.axis";
|
|
8
|
+
import { Constants } from "@babylonjs/core/Engines/constants";
|
|
9
|
+
import { L as LIFETIME_DEBUG_PARAMS, a as LIFETIME_DEFAULTS, s as scheduleLifetime } from "../../chunks/space-shared-xh5IOcLH.js";
|
|
10
|
+
const boltTextureUrl = "data:image/webp;base64,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";
|
|
11
|
+
const DEFAULT_PARAMS = {
|
|
12
|
+
...LIFETIME_DEFAULTS,
|
|
13
|
+
targetDistance: 12,
|
|
14
|
+
boltSpeed: 26,
|
|
15
|
+
boltLen: 1,
|
|
16
|
+
boltColor: { r: 0.071, g: 0.408, b: 1 },
|
|
17
|
+
// #1268ff
|
|
18
|
+
boltCore: 0.3,
|
|
19
|
+
boltCorePow: 3,
|
|
20
|
+
boltBright: 1.3
|
|
21
|
+
};
|
|
22
|
+
const BOLT_LEN = 2, BOLT_W = 0.34;
|
|
23
|
+
const BOLT_VERTEX = `precision highp float;
|
|
24
|
+
attribute vec3 position; attribute vec2 uv;
|
|
25
|
+
uniform mat4 worldViewProjection;
|
|
26
|
+
varying vec2 vUV;
|
|
27
|
+
void main() { vUV = uv; gl_Position = worldViewProjection * vec4(position, 1.0); }`;
|
|
28
|
+
const BOLT_FRAGMENT = `precision highp float;
|
|
29
|
+
varying vec2 vUV;
|
|
30
|
+
uniform sampler2D tex;
|
|
31
|
+
uniform vec3 tint;
|
|
32
|
+
uniform float coreStr; uniform float corePow; uniform float bright;
|
|
33
|
+
void main() {
|
|
34
|
+
vec4 t = texture2D(tex, vUV);
|
|
35
|
+
float core = pow(t.a, corePow) * coreStr;
|
|
36
|
+
vec3 col = mix(tint, vec3(1.0), core) * bright;
|
|
37
|
+
gl_FragColor = vec4(col, t.a);
|
|
38
|
+
}`;
|
|
39
|
+
let shadersRegistered = false;
|
|
40
|
+
function registerShaders() {
|
|
41
|
+
if (shadersRegistered) return;
|
|
42
|
+
Effect.ShadersStore["vfxPulseBoltVertexShader"] = BOLT_VERTEX;
|
|
43
|
+
Effect.ShadersStore["vfxPulseBoltFragmentShader"] = BOLT_FRAGMENT;
|
|
44
|
+
shadersRegistered = true;
|
|
45
|
+
}
|
|
46
|
+
function colorVec(c) {
|
|
47
|
+
return new Color3(c.r, c.g, c.b);
|
|
48
|
+
}
|
|
49
|
+
function excludeFromGlow(scene, mesh) {
|
|
50
|
+
for (const layer of scene.effectLayers ?? []) {
|
|
51
|
+
const add = layer.addExcludedMesh;
|
|
52
|
+
if (typeof add === "function") add.call(layer, mesh);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const pulseLaserVolleySequenceEffectPackage = {
|
|
56
|
+
id: "pulse-laser-volley-sequence",
|
|
57
|
+
nameZh: "脉冲激光(单发)",
|
|
58
|
+
placement: "socket",
|
|
59
|
+
optionalInputs: ["aim"],
|
|
60
|
+
defaultParams: DEFAULT_PARAMS,
|
|
61
|
+
debugAnchor: "playerRight",
|
|
62
|
+
debugParams: [
|
|
63
|
+
...LIFETIME_DEBUG_PARAMS,
|
|
64
|
+
{ key: "targetDistance", label: "目标距离", kind: "number", min: 2, max: 40, step: 0.5 },
|
|
65
|
+
{ key: "boltSpeed", label: "光弹速度", kind: "number", min: 5, max: 60, step: 1 },
|
|
66
|
+
{ key: "boltLen", label: "光弹长度", kind: "number", min: 0.3, max: 4, step: 0.05 },
|
|
67
|
+
{ key: "boltColor", label: "光弹颜色", kind: "color" },
|
|
68
|
+
{ key: "boltCore", label: "白芯强度", kind: "number", min: 0, max: 1, step: 0.01 },
|
|
69
|
+
{ key: "boltCorePow", label: "白芯收窄", kind: "number", min: 0.5, max: 8, step: 0.1 },
|
|
70
|
+
{ key: "boltBright", label: "光弹亮度", kind: "number", min: 0.2, max: 3, step: 0.05 }
|
|
71
|
+
],
|
|
72
|
+
create({ scene, root, position, rotationY, params, inputs }) {
|
|
73
|
+
root.position.copyFrom(position);
|
|
74
|
+
root.rotation.y = rotationY;
|
|
75
|
+
registerShaders();
|
|
76
|
+
const effectiveDist = () => {
|
|
77
|
+
if (!inputs?.aim) return params.targetDistance;
|
|
78
|
+
const tgt = inputs.aim();
|
|
79
|
+
root.lookAt(tgt, 0, 0, 0, Space.WORLD);
|
|
80
|
+
return Vector3.Distance(root.getAbsolutePosition(), tgt);
|
|
81
|
+
};
|
|
82
|
+
const boltTex = new Texture(boltTextureUrl, scene);
|
|
83
|
+
boltTex.hasAlpha = true;
|
|
84
|
+
const boltMat = new ShaderMaterial("vfx_pulse_boltMat", scene, { vertex: "vfxPulseBolt", fragment: "vfxPulseBolt" }, {
|
|
85
|
+
attributes: ["position", "uv"],
|
|
86
|
+
uniforms: ["worldViewProjection", "tint", "coreStr", "corePow", "bright"],
|
|
87
|
+
samplers: ["tex"],
|
|
88
|
+
needAlphaBlending: true
|
|
89
|
+
});
|
|
90
|
+
boltMat.setTexture("tex", boltTex);
|
|
91
|
+
boltMat.backFaceCulling = false;
|
|
92
|
+
boltMat.disableDepthWrite = true;
|
|
93
|
+
boltMat.alphaMode = Constants.ALPHA_ADD;
|
|
94
|
+
boltMat.setColor3("tint", colorVec(params.boltColor));
|
|
95
|
+
boltMat.setFloat("coreStr", params.boltCore);
|
|
96
|
+
boltMat.setFloat("corePow", params.boltCorePow);
|
|
97
|
+
boltMat.setFloat("bright", params.boltBright);
|
|
98
|
+
const mesh = CreatePlane("vfx_pulse_single_bolt", { width: BOLT_W, height: BOLT_LEN }, scene);
|
|
99
|
+
mesh.rotation.x = Math.PI / 2;
|
|
100
|
+
mesh.material = boltMat;
|
|
101
|
+
mesh.isPickable = false;
|
|
102
|
+
mesh.parent = root;
|
|
103
|
+
mesh.setEnabled(false);
|
|
104
|
+
excludeFromGlow(scene, mesh);
|
|
105
|
+
const bolt = { mesh, left: 0 };
|
|
106
|
+
let fired = false;
|
|
107
|
+
const observer = scene.onBeforeRenderObservable.add(() => {
|
|
108
|
+
const dt = Math.min(scene.getEngine().getDeltaTime() / 1e3, 0.05);
|
|
109
|
+
const dist = effectiveDist();
|
|
110
|
+
if (!fired) {
|
|
111
|
+
bolt.mesh.position.set(0, 0, 0);
|
|
112
|
+
bolt.mesh.scaling.z = params.boltLen / BOLT_LEN;
|
|
113
|
+
bolt.left = dist / params.boltSpeed;
|
|
114
|
+
bolt.mesh.setEnabled(true);
|
|
115
|
+
fired = true;
|
|
116
|
+
}
|
|
117
|
+
const step = params.boltSpeed * dt;
|
|
118
|
+
if (bolt.left > 0) {
|
|
119
|
+
bolt.left -= dt;
|
|
120
|
+
bolt.mesh.position.z += step;
|
|
121
|
+
if (bolt.left <= 0) bolt.mesh.setEnabled(false);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
let disposed = false;
|
|
125
|
+
let ltTimer = 0;
|
|
126
|
+
const dispose = () => {
|
|
127
|
+
if (disposed) return;
|
|
128
|
+
disposed = true;
|
|
129
|
+
if (ltTimer) window.clearTimeout(ltTimer);
|
|
130
|
+
scene.onBeforeRenderObservable.remove(observer);
|
|
131
|
+
bolt.mesh.dispose();
|
|
132
|
+
boltMat.dispose();
|
|
133
|
+
boltTex.dispose();
|
|
134
|
+
root.dispose();
|
|
135
|
+
};
|
|
136
|
+
ltTimer = scheduleLifetime(params, dispose);
|
|
137
|
+
return { root, dispose };
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
export {
|
|
141
|
+
pulseLaserVolleySequenceEffectPackage
|
|
142
|
+
};
|
package/index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "vfx-index/1.0",
|
|
3
|
-
"count":
|
|
3
|
+
"count": 41,
|
|
4
4
|
"effects": [
|
|
5
5
|
{
|
|
6
6
|
"id": "build.particle.lumber_debris_scatter",
|
|
@@ -277,6 +277,16 @@
|
|
|
277
277
|
"media": {},
|
|
278
278
|
"path": "packages/effects/pulse-laser-volley"
|
|
279
279
|
},
|
|
280
|
+
{
|
|
281
|
+
"id": "pulse-laser-volley-sequence",
|
|
282
|
+
"dir": "pulse-laser-volley-sequence",
|
|
283
|
+
"nameZh": "pulse-laser-volley-sequence",
|
|
284
|
+
"kind": "code",
|
|
285
|
+
"category": null,
|
|
286
|
+
"tags": [],
|
|
287
|
+
"media": {},
|
|
288
|
+
"path": "packages/effects/pulse-laser-volley-sequence"
|
|
289
|
+
},
|
|
280
290
|
{
|
|
281
291
|
"id": "resource-collect-sparkles",
|
|
282
292
|
"dir": "resource-collect-sparkles",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fps-games/vfx",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.16",
|
|
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": {
|