@fps-games/vfx 0.1.1 → 0.3.3
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 +115 -0
- package/README.md +41 -0
- package/dist/effects/coin-burst/index.js +6 -2
- package/dist/effects/comet-trail-flame/index.js +3 -1
- package/dist/effects/debris-smoke/index.js +1 -0
- package/dist/effects/electric-arc/index.js +1 -0
- package/dist/effects/energy-shield/index.js +1 -0
- package/dist/effects/explosion/index.js +3 -2
- package/dist/effects/hit-flash/index.js +12 -4
- package/dist/effects/laser-beam/index.js +14 -2
- package/dist/effects/lumber_vehicle_upgrade_aura/index.js +1 -0
- package/dist/effects/monster-hit/index.js +2 -1
- package/dist/effects/player-upgrade/index.js +5 -3
- package/dist/effects/pulse-laser-volley/index.js +14 -4
- package/dist/effects/thruster-flame/index.js +3 -1
- package/dist/effects/treasure-attention/index.d.ts +54 -0
- package/dist/effects/treasure-attention/index.js +535 -0
- package/dist/effects/ufo-thread-flame/index.js +1 -0
- package/dist/effects/vehicle-upgrade/index.js +4 -1
- package/dist/effects/weapon-trail/index.js +8 -3
- package/dist/effects/wood_crusher_sawdust/index.d.ts +4 -0
- package/dist/effects/wood_cutter_sawdust/index.d.ts +4 -0
- package/dist/index.js +85 -17
- package/dist/types.d.ts +80 -0
- package/index.json +19 -1
- package/package.json +9 -4
- package/packages/effects/treasure-attention/vfx-params.json +49 -0
- package/tooling/vfx-cli.mjs +8 -1
- package/docs/AUTHORING_EFFECTS.md +0 -297
- package/docs/VFX_LIBRARY_ARCHITECTURE_chatgpt.md +0 -1580
- package/docs/VFX_LIBRARY_EXECUTION_PLAN.md +0 -229
- package/docs/VFX_LIBRARY_PLAN.md +0 -546
- package/docs/vfx-ab/RESULTS.md +0 -35
- package/docs/vfx-ab/lumber-order/vehicle-exhaust-smoke/after.png +0 -0
- package/docs/vfx-ab/lumber-order/vehicle-exhaust-smoke/before.png +0 -0
- package/docs/vfx-ab/lumber-truck/vehicle-exhaust-smoke/after.png +0 -0
- package/docs/vfx-ab/lumber-truck/vehicle-exhaust-smoke/before.png +0 -0
- package/docs/vfx-integration_chatgpt.md +0 -462
- package/docs/weapon-trail-visualization.html +0 -241
- package/packages/EffectPackageService.ts +0 -240
- package/packages/SceneVfxService.ts +0 -96
- package/packages/build.mesh.yellow_build_upgrade_indicator/assets/runtime/simpleMeshEffect.v1.js +0 -333
- package/packages/build.mesh.yellow_build_upgrade_indicator/assets/shaders/mesh-effect-preview.frag.glsl +0 -25
- package/packages/build.mesh.yellow_build_upgrade_indicator/assets/shaders/mesh-effect-preview.vert.glsl +0 -11
- package/packages/build.mesh.yellow_build_upgrade_indicator/assets/textures/effect_jianbian.png +0 -0
- package/packages/build.mesh.yellow_build_upgrade_indicator/effect.json +0 -927
- package/packages/build.mesh.yellow_build_upgrade_indicator/media/contact-sheet.png +0 -0
- package/packages/build.mesh.yellow_build_upgrade_indicator/media/preview.gif +0 -0
- package/packages/build.mesh.yellow_build_upgrade_indicator/media/preview.mp4 +0 -0
- package/packages/build.mesh.yellow_build_upgrade_indicator/media/preview.webm +0 -0
- package/packages/build.mesh.yellow_build_upgrade_indicator/media/thumbnail.png +0 -0
- package/packages/build.mesh.yellow_build_upgrade_indicator/metadata/description.json +0 -197
- package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/03-effect.original.json +0 -895
- package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/03-normalized-vfx-to-babylon-report.json +0 -241
- package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/04-preview-overrides.json +0 -542
- package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/04-preview-tuning-report.json +0 -69
- package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/asset-map.json +0 -125
- package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/lineage.json +0 -44
- package/packages/build.particle.golden_upgrade_starburst/assets/runtime/particleEmitter.v1.js +0 -210
- package/packages/build.particle.golden_upgrade_starburst/assets/textures/effect_guangdian.png +0 -0
- package/packages/build.particle.golden_upgrade_starburst/effect.json +0 -880
- package/packages/build.particle.golden_upgrade_starburst/media/contact-sheet.png +0 -0
- package/packages/build.particle.golden_upgrade_starburst/media/preview.gif +0 -0
- package/packages/build.particle.golden_upgrade_starburst/media/preview.mp4 +0 -0
- package/packages/build.particle.golden_upgrade_starburst/media/preview.webm +0 -0
- package/packages/build.particle.golden_upgrade_starburst/media/thumbnail.png +0 -0
- package/packages/build.particle.golden_upgrade_starburst/metadata/description.json +0 -161
- package/packages/build.particle.golden_upgrade_starburst/provenance/03-effect.original.json +0 -762
- package/packages/build.particle.golden_upgrade_starburst/provenance/03-normalized-vfx-to-babylon-report.json +0 -260
- package/packages/build.particle.golden_upgrade_starburst/provenance/04-preview-overrides.json +0 -436
- package/packages/build.particle.golden_upgrade_starburst/provenance/04-preview-tuning-report.json +0 -40
- package/packages/build.particle.golden_upgrade_starburst/provenance/asset-map.json +0 -115
- package/packages/build.particle.golden_upgrade_starburst/provenance/lineage.json +0 -40
- package/packages/build.particle.orange_upgrade_transform_sparks/assets/runtime/particleEmitter.v1.js +0 -210
- package/packages/build.particle.orange_upgrade_transform_sparks/assets/textures/b5b27ab1-e740-4398-b407-848fc2b2c897.png +0 -0
- package/packages/build.particle.orange_upgrade_transform_sparks/effect.json +0 -887
- package/packages/build.particle.orange_upgrade_transform_sparks/media/preview.gif +0 -0
- package/packages/build.particle.orange_upgrade_transform_sparks/media/preview.mp4 +0 -0
- package/packages/build.particle.orange_upgrade_transform_sparks/media/preview.webm +0 -0
- package/packages/build.particle.orange_upgrade_transform_sparks/media/thumbnail.png +0 -0
- package/packages/build.particle.orange_upgrade_transform_sparks/metadata/description.json +0 -179
- package/packages/build.particle.orange_upgrade_transform_sparks/provenance/03-effect.original.json +0 -859
- package/packages/build.particle.orange_upgrade_transform_sparks/provenance/03-normalized-vfx-to-babylon-report.json +0 -260
- package/packages/build.particle.orange_upgrade_transform_sparks/provenance/04-preview-overrides.json +0 -478
- package/packages/build.particle.orange_upgrade_transform_sparks/provenance/04-preview-tuning-report.json +0 -40
- package/packages/build.particle.orange_upgrade_transform_sparks/provenance/asset-map.json +0 -114
- package/packages/build.particle.orange_upgrade_transform_sparks/provenance/lineage.json +0 -41
- package/packages/build.particle.wood_chip_dust_burst/assets/runtime/particleEmitter.v1.js +0 -210
- package/packages/build.particle.wood_chip_dust_burst/assets/textures/effect_yanwu4.png +0 -0
- package/packages/build.particle.wood_chip_dust_burst/assets/textures/suipian3.png +0 -0
- package/packages/build.particle.wood_chip_dust_burst/effect.json +0 -1400
- package/packages/build.particle.wood_chip_dust_burst/media/contact-sheet.png +0 -0
- package/packages/build.particle.wood_chip_dust_burst/media/preview.gif +0 -0
- package/packages/build.particle.wood_chip_dust_burst/media/preview.mp4 +0 -0
- package/packages/build.particle.wood_chip_dust_burst/media/preview.webm +0 -0
- package/packages/build.particle.wood_chip_dust_burst/media/thumbnail.png +0 -0
- package/packages/build.particle.wood_chip_dust_burst/metadata/description.json +0 -173
- package/packages/build.particle.wood_chip_dust_burst/provenance/03-effect.original.json +0 -1277
- package/packages/build.particle.wood_chip_dust_burst/provenance/03-normalized-vfx-to-babylon-report.json +0 -329
- package/packages/build.particle.wood_chip_dust_burst/provenance/04-preview-overrides.json +0 -749
- package/packages/build.particle.wood_chip_dust_burst/provenance/04-preview-tuning-report.json +0 -60
- package/packages/build.particle.wood_chip_dust_burst/provenance/asset-map.json +0 -146
- package/packages/build.particle.wood_chip_dust_burst/provenance/lineage.json +0 -69
- package/packages/effects/building-upgrade/effect.json +0 -6
- package/packages/effects/building-upgrade/index.ts +0 -387
- package/packages/effects/character-entrance/effect.json +0 -6
- package/packages/effects/character-entrance/index.ts +0 -308
- package/packages/effects/coin-burst/index.ts +0 -43
- package/packages/effects/comet-trail-flame/assets/textures/trail_5x4.webp +0 -0
- package/packages/effects/comet-trail-flame/index.ts +0 -106
- package/packages/effects/conveyor-upgrade/effect.json +0 -6
- package/packages/effects/conveyor-upgrade/index.ts +0 -401
- package/packages/effects/debris-smoke/assets/models/rock_debris.glb +0 -0
- package/packages/effects/debris-smoke/assets/textures/smoke22.webp +0 -0
- package/packages/effects/debris-smoke/index.ts +0 -101
- package/packages/effects/door_open_gold_flakes/assets/runtime/goldFlakes.v1.js +0 -397
- package/packages/effects/door_open_gold_flakes/door_open_gold_flakes.json +0 -498
- package/packages/effects/door_open_gold_flakes/index.ts +0 -11
- package/packages/effects/electric-arc/assets/textures/spark_4x4.webp +0 -0
- package/packages/effects/electric-arc/index.ts +0 -62
- package/packages/effects/energy-shield/effect.json +0 -7
- package/packages/effects/energy-shield/index.ts +0 -431
- package/packages/effects/explosion/assets/textures/baozha29.webp +0 -0
- package/packages/effects/explosion/assets/textures/glow01.webp +0 -0
- package/packages/effects/explosion/assets/textures/lizi01.webp +0 -0
- package/packages/effects/explosion/assets/textures/xulie_lizibaozha02_5x5.webp +0 -0
- package/packages/effects/explosion/index.ts +0 -114
- package/packages/effects/ground-tile-upgrade/effect.json +0 -6
- package/packages/effects/ground-tile-upgrade/index.ts +0 -410
- package/packages/effects/hit-flash/index.ts +0 -69
- package/packages/effects/laser-beam/assets/textures/laser_glow.webp +0 -0
- package/packages/effects/laser-beam/assets/textures/laser_noise.webp +0 -0
- package/packages/effects/laser-beam/index.ts +0 -6
- package/packages/effects/laser-beam-factory.ts +0 -135
- package/packages/effects/lumber-cart-suction/effect.json +0 -6
- package/packages/effects/lumber-cart-suction/index.ts +0 -399
- package/packages/effects/lumber-debris-scatter/assets/models/lumber_prop_model.glb +0 -0
- package/packages/effects/lumber-debris-scatter/assets/models/lumber_prop_model.glb.versions/.active +0 -1
- package/packages/effects/lumber-debris-scatter/assets/models/lumber_prop_model.glb.versions/1781589063890_upload.glb +0 -0
- package/packages/effects/lumber-debris-scatter/assets/runtime/solidParticleScatter.v1.js +0 -236
- package/packages/effects/lumber-debris-scatter/effect.json +0 -228
- package/packages/effects/lumber-debris-scatter/index.ts +0 -780
- package/packages/effects/lumber_vehicle_gear_spin/assets/runtime/gearSpin.v1.js +0 -202
- package/packages/effects/lumber_vehicle_gear_spin/index.ts +0 -11
- package/packages/effects/lumber_vehicle_gear_spin/lumber_vehicle_gear_spin.json +0 -338
- package/packages/effects/lumber_vehicle_upgrade_aura/assets/runtime/upgradeAura.v1.js +0 -353
- package/packages/effects/lumber_vehicle_upgrade_aura/index.ts +0 -11
- package/packages/effects/lumber_vehicle_upgrade_aura/lumber_vehicle_upgrade_aura.json +0 -365
- package/packages/effects/lumber_vehicle_wood_chips/assets/runtime/solidParticleScatter.v1.js +0 -310
- package/packages/effects/lumber_vehicle_wood_chips/index.ts +0 -11
- package/packages/effects/lumber_vehicle_wood_chips/lumber_vehicle_wood_chips.json +0 -335
- package/packages/effects/monster-hit/index.ts +0 -128
- package/packages/effects/player-upgrade/effect.json +0 -6
- package/packages/effects/player-upgrade/index.ts +0 -635
- package/packages/effects/pulse-laser-volley/assets/textures/bolt_guangshu.webp +0 -0
- package/packages/effects/pulse-laser-volley/index.ts +0 -204
- package/packages/effects/resource-collect-sparkles/effect.json +0 -6
- package/packages/effects/resource-collect-sparkles/index.ts +0 -254
- package/packages/effects/run-footstep-dust/assets/runtime/runFootstepDust.v1.js +0 -215
- package/packages/effects/run-footstep-dust/effect.json +0 -183
- package/packages/effects/run-footstep-dust/index.ts +0 -377
- package/packages/effects/space-shared.ts +0 -120
- package/packages/effects/thruster-flame/index.ts +0 -305
- package/packages/effects/tree-chop-dust/assets/textures/tree_chop_dust_atlas.png +0 -0
- package/packages/effects/tree-chop-dust/effect.json +0 -8
- package/packages/effects/tree-chop-dust/index.ts +0 -202
- package/packages/effects/tree-leaf-chop-burst/assets/runtime/particleEmitter.v1.js +0 -303
- package/packages/effects/tree-leaf-chop-burst/assets/textures/tree_leaf_chop_burst_leaf_atlas.png +0 -0
- package/packages/effects/tree-leaf-chop-burst/effect.json +0 -498
- package/packages/effects/tree-leaf-chop-burst/index.ts +0 -294
- package/packages/effects/tree_chop_dust_burst/assets/runtime/particleEmitter.v1.js +0 -318
- package/packages/effects/tree_chop_dust_burst/assets/textures/tree_chop_dust_burst_dust.png +0 -0
- package/packages/effects/tree_chop_dust_burst/index.ts +0 -13
- package/packages/effects/tree_chop_dust_burst/tree_chop_dust_burst.json +0 -412
- package/packages/effects/ufo-thread-flame/assets/textures/flame_flow.webp +0 -0
- package/packages/effects/ufo-thread-flame/assets/textures/flame_spike.webp +0 -0
- package/packages/effects/ufo-thread-flame/index.ts +0 -152
- package/packages/effects/vehicle-exhaust-smoke/assets/textures/vehicle_exhaust_smoke_atlas.png +0 -0
- package/packages/effects/vehicle-exhaust-smoke/effect.json +0 -8
- package/packages/effects/vehicle-exhaust-smoke/index.ts +0 -298
- package/packages/effects/vehicle-upgrade/effect.json +0 -6
- package/packages/effects/vehicle-upgrade/index.ts +0 -401
- package/packages/effects/weapon-trail/assets/textures/trail_wedge.png +0 -0
- package/packages/effects/weapon-trail/index.ts +0 -313
- package/packages/effects/wood-crusher-sawdust/assets/runtime/particleEmitter.v1.js +0 -312
- package/packages/effects/wood-crusher-sawdust/assets/textures/wood_crusher_sawdust_dust.png +0 -0
- package/packages/effects/wood-crusher-sawdust/assets/textures/wood_crusher_sawdust_fiber.png +0 -0
- package/packages/effects/wood-crusher-sawdust/index.ts +0 -43
- package/packages/effects/wood-crusher-sawdust/wood_crusher_sawdust.json +0 -592
- package/packages/effects/wood-cutter-sawdust/assets/runtime/particleEmitter.v1.js +0 -312
- package/packages/effects/wood-cutter-sawdust/assets/textures/wood_cutter_sawdust_chip.png +0 -0
- package/packages/effects/wood-cutter-sawdust/assets/textures/wood_cutter_sawdust_dust.png +0 -0
- package/packages/effects/wood-cutter-sawdust/index.ts +0 -43
- package/packages/effects/wood-cutter-sawdust/wood_cutter_sawdust.json +0 -592
- package/packages/effects/wood-sawdust-effect-factory.ts +0 -194
- package/packages/effects/wood_crusher_sawdust/assets/runtime/particleEmitter.v1.js +0 -318
- package/packages/effects/wood_crusher_sawdust/assets/textures/wood_crusher_sawdust_dust.png +0 -0
- package/packages/effects/wood_crusher_sawdust/assets/textures/wood_crusher_sawdust_fiber.png +0 -0
- package/packages/effects/wood_crusher_sawdust/index.ts +0 -29
- package/packages/effects/wood_crusher_sawdust/wood_crusher_sawdust.json +0 -592
- package/packages/effects/wood_cutter_sawdust/assets/runtime/particleEmitter.v1.js +0 -318
- package/packages/effects/wood_cutter_sawdust/assets/textures/wood_cutter_sawdust_chip.png +0 -0
- package/packages/effects/wood_cutter_sawdust/assets/textures/wood_cutter_sawdust_dust.png +0 -0
- package/packages/effects/wood_cutter_sawdust/index.ts +0 -29
- package/packages/effects/wood_cutter_sawdust/wood_cutter_sawdust.json +0 -592
- package/packages/effects/wood_peel_material_test/assets/runtime/woodPeelMaterialTest.v1.js +0 -196
- package/packages/effects/wood_peel_material_test/index.ts +0 -11
- package/packages/effects/wood_peel_material_test/wood_peel_material_test.json +0 -136
- package/packages/env.particle.forest_edge_fog/assets/runtime/particleEmitter.v1.js +0 -210
- package/packages/env.particle.forest_edge_fog/assets/textures/f12a23c4-b924-4322-a260-3d982428f1e8.png +0 -0
- package/packages/env.particle.forest_edge_fog/assets/textures/yuan.png +0 -0
- package/packages/env.particle.forest_edge_fog/effect.json +0 -854
- package/packages/env.particle.forest_edge_fog/media/preview.gif +0 -0
- package/packages/env.particle.forest_edge_fog/media/preview.mp4 +0 -0
- package/packages/env.particle.forest_edge_fog/media/preview.webm +0 -0
- package/packages/env.particle.forest_edge_fog/media/thumbnail.png +0 -0
- package/packages/env.particle.forest_edge_fog/metadata/description.json +0 -148
- package/packages/env.particle.forest_edge_fog/provenance/03-effect.original.json +0 -827
- package/packages/env.particle.forest_edge_fog/provenance/03-normalized-vfx-to-babylon-report.json +0 -196
- package/packages/env.particle.forest_edge_fog/provenance/04-preview-overrides.json +0 -432
- package/packages/env.particle.forest_edge_fog/provenance/04-preview-tuning-report.json +0 -28
- package/packages/env.particle.forest_edge_fog/provenance/asset-map.json +0 -124
- package/packages/env.particle.forest_edge_fog/provenance/lineage.json +0 -41
- package/packages/generation.ts +0 -14
- package/packages/index.ts +0 -14
- package/packages/legacy-particle-runtime.ts +0 -113
- package/packages/manifestRuntimePackage.ts +0 -519
- package/packages/registry.ts +0 -85
- package/packages/rendering.ts +0 -13
- package/packages/resource.particle.viscous_oil_suction/assets/runtime/top_disk_suction/particleEmitter.v1.js +0 -424
- package/packages/resource.particle.viscous_oil_suction/assets/textures/Glow ad.png +0 -0
- package/packages/resource.particle.viscous_oil_suction/effect.json +0 -1254
- package/packages/resource.particle.viscous_oil_suction/media/preview.gif +0 -0
- package/packages/resource.particle.viscous_oil_suction/media/preview.mp4 +0 -0
- package/packages/resource.particle.viscous_oil_suction/media/preview.webm +0 -0
- package/packages/resource.particle.viscous_oil_suction/media/thumbnail.png +0 -0
- package/packages/resource.particle.viscous_oil_suction/metadata/description.json +0 -122
- package/packages/resource.particle.viscous_oil_suction/provenance/03-effect.original.json +0 -1051
- package/packages/resource.particle.viscous_oil_suction/provenance/03-normalized-vfx-to-babylon-report.json +0 -200
- package/packages/resource.particle.viscous_oil_suction/provenance/04-preview-overrides.json +0 -581
- package/packages/resource.particle.viscous_oil_suction/provenance/04-preview-tuning-report.json +0 -28
- package/packages/resource.particle.viscous_oil_suction/provenance/asset-map.json +0 -111
- package/packages/resource.particle.viscous_oil_suction/provenance/lineage.json +0 -45
- package/packages/types.ts +0 -107
- package/tooling/check-dist-runtime.mjs +0 -166
- package/tooling/generate-index.mjs +0 -55
- package/tooling/test-guardrails.mjs +0 -157
- package/tooling/validate-boundary.mjs +0 -92
- package/tooling/validate-effects.mjs +0 -304
- package/tooling/validate.mjs +0 -53
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="zh">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
-
<title>武器拖尾 weapon-trail · 贴图 / 颜色 / 形状 可视化</title>
|
|
7
|
-
<style>
|
|
8
|
-
:root { --bg:#0a0e17; --panel:#121826; --ink:#cfe1ff; --dim:#7e93b8; --line:#22304d; --accent:#6cc0ff; }
|
|
9
|
-
* { box-sizing:border-box; }
|
|
10
|
-
body { margin:0; background:var(--bg); color:var(--ink); font:14px/1.6 -apple-system,system-ui,"PingFang SC",sans-serif; }
|
|
11
|
-
h1 { font-size:18px; margin:0 0 4px; }
|
|
12
|
-
h2 { font-size:14px; color:var(--accent); margin:0 0 8px; letter-spacing:.5px; }
|
|
13
|
-
.sub { color:var(--dim); font-size:12px; margin-bottom:18px; }
|
|
14
|
-
.wrap { max-width:1080px; margin:0 auto; padding:22px 18px 60px; }
|
|
15
|
-
.grid { display:grid; grid-template-columns:1fr 1fr; gap:16px; }
|
|
16
|
-
.card { background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:14px 16px; }
|
|
17
|
-
.full { grid-column:1 / -1; }
|
|
18
|
-
canvas { display:block; width:100%; border-radius:8px; background:
|
|
19
|
-
conic-gradient(#1a2233 0 25%, #141a28 0 50%, #1a2233 0 75%, #141a28 0) 0 0 / 16px 16px; }
|
|
20
|
-
.row { display:flex; align-items:center; gap:10px; margin:7px 0; }
|
|
21
|
-
.row label { flex:0 0 120px; color:var(--dim); font-size:12px; }
|
|
22
|
-
.row input[type=range] { flex:1; }
|
|
23
|
-
.row .val { flex:0 0 48px; text-align:right; color:var(--accent); font-variant-numeric:tabular-nums; }
|
|
24
|
-
.row input[type=color] { width:40px; height:24px; border:1px solid var(--line); border-radius:5px; background:#0c1322; padding:0; }
|
|
25
|
-
code { background:#0c1322; border:1px solid var(--line); border-radius:4px; padding:1px 5px; color:#9fd0ff; font-size:12px; }
|
|
26
|
-
.legend { font-size:12px; color:var(--dim); margin-top:8px; }
|
|
27
|
-
.legend b { color:var(--ink); }
|
|
28
|
-
.pipe { font-size:12.5px; line-height:1.9; }
|
|
29
|
-
.pipe .step { color:var(--ink); } .pipe .op { color:var(--accent); }
|
|
30
|
-
.tag { display:inline-block; font-size:11px; padding:1px 6px; border:1px solid var(--line); border-radius:999px; color:var(--dim); margin-left:6px; }
|
|
31
|
-
.axis { font-size:11px; color:var(--dim); }
|
|
32
|
-
</style>
|
|
33
|
-
</head>
|
|
34
|
-
<body>
|
|
35
|
-
<div class="wrap">
|
|
36
|
-
<h1>武器拖尾 <code>weapon-trail</code> — 贴图 / 颜色 / 形状</h1>
|
|
37
|
-
<div class="sub">贴图 <b>trail_wedge.png</b> 254×77 RGBA · 下面用 canvas 实时复刻片元着色器,拖滑块即时看每个参数的影响。背景棋盘格 = 透明区。</div>
|
|
38
|
-
|
|
39
|
-
<div class="grid">
|
|
40
|
-
<!-- 1. 原始贴图 -->
|
|
41
|
-
<div class="card">
|
|
42
|
-
<h2>① 原始贴图 trail_wedge.png</h2>
|
|
43
|
-
<canvas id="texCanvas" height="120"></canvas>
|
|
44
|
-
<div class="legend">
|
|
45
|
-
左 = 拖尾<b>头部</b>(u≈0),右 = <b>尾端</b>(u≈1);上下 = 拖尾<b>宽度</b>方向(v 0→1)。<br>
|
|
46
|
-
这是一个<b>楔形 + alpha 渐变</b>:RGB 蓝白,alpha 决定可见形状(头宽尾窄/边缘羽化)。
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
|
|
50
|
-
<!-- 2. alpha 通道 -->
|
|
51
|
-
<div class="card">
|
|
52
|
-
<h2>② 贴图通道拆解</h2>
|
|
53
|
-
<canvas id="chanCanvas" height="120"></canvas>
|
|
54
|
-
<div class="legend">上 = <b>RGB</b>(颜色)· 下 = <b>Alpha</b>(白=不透明,黑=透明)。拖尾的「形状」几乎全在 alpha 里。</div>
|
|
55
|
-
</div>
|
|
56
|
-
|
|
57
|
-
<!-- 3. 颜色管线 -->
|
|
58
|
-
<div class="card full">
|
|
59
|
-
<h2>③ 片元颜色管线(逐步)</h2>
|
|
60
|
-
<div class="pipe">
|
|
61
|
-
<span class="step">base = texture(uv)</span> <span class="op">→</span>
|
|
62
|
-
<span class="step">color = base.rgb × <b style="color:#6cc0ff">uTint</b></span> <span class="op">→</span>
|
|
63
|
-
<span class="step">饱和度 mix(灰, color, <b style="color:#6cc0ff">uSaturation</b>)</span> <span class="op">→</span>
|
|
64
|
-
<span class="step">对比度 (color−0.5)×<b style="color:#6cc0ff">uContrast</b>+0.5</span> <span class="op">→</span>
|
|
65
|
-
<span class="step">alpha = base.a<sup><b style="color:#6cc0ff">uAlphaPower</b></sup> × 顶点α(尾部淡出) × <b style="color:#6cc0ff">uAlphaScale</b></span>
|
|
66
|
-
<span class="tag">ALPHA_COMBINE</span>
|
|
67
|
-
</div>
|
|
68
|
-
<canvas id="stripCanvas" height="120" style="margin-top:10px"></canvas>
|
|
69
|
-
<div class="legend">↑ <b>展开的 ribbon</b>:贴图经上面整条管线 + <b>尾部 alpha 衰减</b>(<code>1 − u^淡出曲线 × 淡出强度</code>,沿长度从头到尾越来越透)后的最终样子。</div>
|
|
70
|
-
</div>
|
|
71
|
-
|
|
72
|
-
<!-- 4. 拖尾形状 -->
|
|
73
|
-
<div class="card full">
|
|
74
|
-
<h2>④ 拖尾形状(几何)</h2>
|
|
75
|
-
<canvas id="arcCanvas" height="300"></canvas>
|
|
76
|
-
<div class="legend">
|
|
77
|
-
每个采样点出 <b>2 个顶点</b> = 中心点 ± (宽度/2) × <b>宽度轴</b>,连成三角带 = ribbon。<br>
|
|
78
|
-
采样点是 emitter 绕圈路径按<b>弧长均匀重采样</b>到固定段数的结果;<b>水平面</b>朝向时宽度轴=径向(平躺),<b>竖直</b>时=上方向(立着)。
|
|
79
|
-
<span class="axis">— 虚线 = 采样中心线(emitter 轨迹弧段),短线 = 各点宽度轴。</span>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
</div>
|
|
83
|
-
|
|
84
|
-
<!-- 控制 -->
|
|
85
|
-
<div class="card full" style="margin-top:16px">
|
|
86
|
-
<h2>参数(对应 weapon-trail debugParams)</h2>
|
|
87
|
-
<div class="grid" style="gap:4px 28px">
|
|
88
|
-
<div>
|
|
89
|
-
<div class="row"><label>颜色 uTint</label><input type="color" id="tint" value="#ffffff"><span class="val" id="tintv">#fff</span></div>
|
|
90
|
-
<div class="row"><label>透明度 opacity</label><input type="range" id="opacity" min="0" max="3" step="0.01" value="1.35"><span class="val" id="opacityv"></span></div>
|
|
91
|
-
<div class="row"><label>饱和度 saturation</label><input type="range" id="saturation" min="0" max="3" step="0.01" value="1.45"><span class="val" id="saturationv"></span></div>
|
|
92
|
-
<div class="row"><label>对比度 contrast</label><input type="range" id="contrast" min="0" max="3" step="0.01" value="1.32"><span class="val" id="contrastv"></span></div>
|
|
93
|
-
<div class="row"><label>alphaPower</label><input type="range" id="alphaPower" min="0.1" max="3" step="0.01" value="0.82"><span class="val" id="alphaPowerv"></span></div>
|
|
94
|
-
</div>
|
|
95
|
-
<div>
|
|
96
|
-
<div class="row"><label>尾部淡出强度</label><input type="range" id="fadeStrength" min="0" max="1" step="0.01" value="0.58"><span class="val" id="fadeStrengthv"></span></div>
|
|
97
|
-
<div class="row"><label>尾部淡出曲线</label><input type="range" id="fadeExp" min="0.2" max="4" step="0.05" value="1.35"><span class="val" id="fadeExpv"></span></div>
|
|
98
|
-
<div class="row"><label>拖尾宽度 width</label><input type="range" id="width" min="0.1" max="4" step="0.01" value="1.2"><span class="val" id="widthv"></span></div>
|
|
99
|
-
<div class="row"><label>拖尾弧度</label><input type="range" id="arcSpan" min="20" max="300" step="5" value="150"><span class="val" id="arcSpanv"></span></div>
|
|
100
|
-
<div class="row"><label>朝向</label>
|
|
101
|
-
<select id="orient" style="flex:1;background:#0c1322;color:var(--ink);border:1px solid var(--line);border-radius:5px;padding:3px 6px;">
|
|
102
|
-
<option value="horizontal">水平面</option><option value="vertical">竖直</option>
|
|
103
|
-
</select><span class="val"></span></div>
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
107
|
-
</div>
|
|
108
|
-
|
|
109
|
-
<script>
|
|
110
|
-
const TEX_DATA_URI = "data:image/png;base64,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";
|
|
111
|
-
const P = {};
|
|
112
|
-
const ids = ['opacity','saturation','contrast','alphaPower','fadeStrength','fadeExp','width','arcSpan'];
|
|
113
|
-
function readParams(){
|
|
114
|
-
for (const id of ids){ P[id]=parseFloat(document.getElementById(id).value); document.getElementById(id+'v').textContent = P[id]; }
|
|
115
|
-
const t = document.getElementById('tint').value;
|
|
116
|
-
P.tint = [parseInt(t.substr(1,2),16)/255, parseInt(t.substr(3,2),16)/255, parseInt(t.substr(5,2),16)/255];
|
|
117
|
-
document.getElementById('tintv').textContent = t;
|
|
118
|
-
P.orient = document.getElementById('orient').value;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// 读贴图像素
|
|
122
|
-
const img = new Image();
|
|
123
|
-
let texW=0, texH=0, texData=null;
|
|
124
|
-
function sampleTex(u, v){ // u,v in 0..1 → {r,g,b,a} 0..1 (nearest)
|
|
125
|
-
const x = Math.min(texW-1, Math.max(0, Math.floor(u*texW)));
|
|
126
|
-
const y = Math.min(texH-1, Math.max(0, Math.floor(v*texH)));
|
|
127
|
-
const i = (y*texW + x)*4;
|
|
128
|
-
return [texData[i]/255, texData[i+1]/255, texData[i+2]/255, texData[i+3]/255];
|
|
129
|
-
}
|
|
130
|
-
// 复刻片元:base + tint + sat + contrast + alpha(含尾部衰减 vertexAlpha(u))
|
|
131
|
-
function shade(u, v){
|
|
132
|
-
const b = sampleTex(u*0.97+0.015, v);
|
|
133
|
-
let c = [b[0]*P.tint[0], b[1]*P.tint[1], b[2]*P.tint[2]];
|
|
134
|
-
const lum = c[0]*0.299 + c[1]*0.587 + c[2]*0.114;
|
|
135
|
-
c = c.map(x => lum + (x-lum)*P.saturation);
|
|
136
|
-
c = c.map(x => (x-0.5)*P.contrast + 0.5);
|
|
137
|
-
c = c.map(x => Math.min(1, Math.max(0, x)));
|
|
138
|
-
const vertexAlpha = Math.max(0, 1 - Math.pow(u, P.fadeExp) * P.fadeStrength);
|
|
139
|
-
let a = Math.pow(Math.min(1,Math.max(0,b[3])), P.alphaPower) * vertexAlpha * P.opacity;
|
|
140
|
-
a = Math.min(1, Math.max(0, a));
|
|
141
|
-
return [c[0]*255, c[1]*255, c[2]*255, a];
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function fitCanvas(cv){ const w = cv.clientWidth; cv.width = w; return cv.getContext('2d'); }
|
|
145
|
-
|
|
146
|
-
function drawTex(){ // 原始贴图(直接画 img,棋盘格背景透出 alpha)
|
|
147
|
-
const cv = document.getElementById('texCanvas'); const ctx = fitCanvas(cv);
|
|
148
|
-
ctx.clearRect(0,0,cv.width,cv.height);
|
|
149
|
-
ctx.imageSmoothingEnabled = false;
|
|
150
|
-
ctx.drawImage(img, 0, 0, cv.width, cv.height);
|
|
151
|
-
}
|
|
152
|
-
function drawChannels(){
|
|
153
|
-
const cv = document.getElementById('chanCanvas'); const ctx = fitCanvas(cv);
|
|
154
|
-
ctx.clearRect(0,0,cv.width,cv.height);
|
|
155
|
-
const h = cv.height/2, w = cv.width;
|
|
156
|
-
const out = ctx.createImageData(w, cv.height);
|
|
157
|
-
for (let y=0;y<cv.height;y++) for (let x=0;x<w;x++){
|
|
158
|
-
const u=x/w, top = y<h;
|
|
159
|
-
const v = top ? y/h : (y-h)/h;
|
|
160
|
-
const b = sampleTex(u, v); const o=(y*w+x)*4;
|
|
161
|
-
if (top){ out.data[o]=b[0]*255; out.data[o+1]=b[1]*255; out.data[o+2]=b[2]*255; out.data[o+3]=255; }
|
|
162
|
-
else { const g=b[3]*255; out.data[o]=g; out.data[o+1]=g; out.data[o+2]=g; out.data[o+3]=255; }
|
|
163
|
-
}
|
|
164
|
-
ctx.putImageData(out,0,0);
|
|
165
|
-
ctx.fillStyle='#0008'; ctx.fillRect(0,h-9,42,18); ctx.fillStyle='#cfe1ff'; ctx.font='11px monospace';
|
|
166
|
-
ctx.fillText('RGB', 6, h-12); ctx.fillText('Alpha', 6, cv.height-6);
|
|
167
|
-
}
|
|
168
|
-
function drawStrip(){ // 展开 ribbon:整条管线后的最终像素
|
|
169
|
-
const cv = document.getElementById('stripCanvas'); const ctx = fitCanvas(cv);
|
|
170
|
-
ctx.clearRect(0,0,cv.width,cv.height);
|
|
171
|
-
const w=cv.width, h=cv.height;
|
|
172
|
-
const out = ctx.createImageData(w,h);
|
|
173
|
-
for (let y=0;y<h;y++) for (let x=0;x<w;x++){
|
|
174
|
-
const u=x/w, v=y/h; const s=shade(u,v); const o=(y*w+x)*4;
|
|
175
|
-
// 在棋盘格上做 alpha 合成
|
|
176
|
-
const checker = (((x>>4)+(y>>4))&1) ? [26,34,51] : [20,26,40];
|
|
177
|
-
out.data[o] = s[0]*s[3] + checker[0]*(1-s[3]);
|
|
178
|
-
out.data[o+1] = s[1]*s[3] + checker[1]*(1-s[3]);
|
|
179
|
-
out.data[o+2] = s[2]*s[3] + checker[2]*(1-s[3]);
|
|
180
|
-
out.data[o+3] = 255;
|
|
181
|
-
}
|
|
182
|
-
ctx.putImageData(out,0,0);
|
|
183
|
-
ctx.fillStyle='#cfe1ff'; ctx.font='11px monospace';
|
|
184
|
-
ctx.fillText('头 u=0', 4, 14); ctx.textAlign='right'; ctx.fillText('尾 u=1', w-4, 14); ctx.textAlign='left';
|
|
185
|
-
}
|
|
186
|
-
function drawArc(){ // 弯成弧的实际形状 + 几何标注
|
|
187
|
-
const cv = document.getElementById('arcCanvas'); const ctx = fitCanvas(cv);
|
|
188
|
-
const w=cv.width, h=cv.height; ctx.clearRect(0,0,w,h);
|
|
189
|
-
const cx=w*0.5, cy=h*0.62, R=Math.min(w*0.32, h*0.7);
|
|
190
|
-
const span = P.arcSpan * Math.PI/180;
|
|
191
|
-
const a0 = -span/2 - Math.PI/2; // 起始角
|
|
192
|
-
const SEG=140, VW=18; // u 段数 / v 行数
|
|
193
|
-
const halfW = (P.width*0.5) * (R*0.34);// 屏幕上的半宽(可视化缩放)
|
|
194
|
-
// 逐格 quad 画 ribbon
|
|
195
|
-
for (let i=0;i<SEG;i++){
|
|
196
|
-
const u0=i/SEG, u1=(i+1)/SEG;
|
|
197
|
-
const ang0=a0+span*u0, ang1=a0+span*u1;
|
|
198
|
-
for (let j=0;j<VW;j++){
|
|
199
|
-
const v0=j/VW, v1=(j+1)/VW, vm=(v0+v1)/2, um=(u0+u1)/2;
|
|
200
|
-
const s=shade(um,vm); if (s[3]<0.02) continue;
|
|
201
|
-
// 中心线点
|
|
202
|
-
const p0x=cx+Math.cos(ang0)*R, p0y=cy+Math.sin(ang0)*R*0.55; // *0.55 模拟俯视压扁(水平面)
|
|
203
|
-
const p1x=cx+Math.cos(ang1)*R, p1y=cy+Math.sin(ang1)*R*0.55;
|
|
204
|
-
// 宽度方向:水平=径向(在压扁平面里),竖直=屏幕上下
|
|
205
|
-
let wx0,wy0,wx1,wy1;
|
|
206
|
-
if (P.orient==='vertical'){ wx0=0; wy0=-1; wx1=0; wy1=-1; }
|
|
207
|
-
else { wx0=Math.cos(ang0); wy0=Math.sin(ang0)*0.55; const l0=Math.hypot(wx0,wy0); wx0/=l0; wy0/=l0; wx1=Math.cos(ang1); wy1=Math.sin(ang1)*0.55; const l1=Math.hypot(wx1,wy1); wx1/=l1; wy1/=l1; }
|
|
208
|
-
const oy = P.orient==='vertical' ? halfW : halfW; // 屏幕半宽
|
|
209
|
-
const A=[p0x+wx0*oy*(0.5-v0)*2, p0y+wy0*oy*(0.5-v0)*2];
|
|
210
|
-
const B=[p0x+wx0*oy*(0.5-v1)*2, p0y+wy0*oy*(0.5-v1)*2];
|
|
211
|
-
const C=[p1x+wx1*oy*(0.5-v1)*2, p1y+wy1*oy*(0.5-v1)*2];
|
|
212
|
-
const D=[p1x+wx1*oy*(0.5-v0)*2, p1y+wy1*oy*(0.5-v0)*2];
|
|
213
|
-
ctx.fillStyle=`rgba(${s[0]|0},${s[1]|0},${s[2]|0},${s[3]})`;
|
|
214
|
-
ctx.beginPath(); ctx.moveTo(A[0],A[1]); ctx.lineTo(B[0],B[1]); ctx.lineTo(C[0],C[1]); ctx.lineTo(D[0],D[1]); ctx.closePath(); ctx.fill();
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
// 标注:中心线 + 几个宽度轴
|
|
218
|
-
ctx.setLineDash([4,4]); ctx.strokeStyle='#6cc0ff88'; ctx.beginPath();
|
|
219
|
-
for (let i=0;i<=SEG;i++){ const ang=a0+span*(i/SEG); const x=cx+Math.cos(ang)*R, y=cy+Math.sin(ang)*R*0.55; i?ctx.lineTo(x,y):ctx.moveTo(x,y); }
|
|
220
|
-
ctx.stroke(); ctx.setLineDash([]);
|
|
221
|
-
ctx.strokeStyle='#ffd36c'; ctx.lineWidth=1.5;
|
|
222
|
-
for (let k=0;k<=6;k++){ const u=k/6; const ang=a0+span*u; const px=cx+Math.cos(ang)*R, py=cy+Math.sin(ang)*R*0.55;
|
|
223
|
-
let wx,wy; if(P.orient==='vertical'){wx=0;wy=-1;} else {wx=Math.cos(ang); wy=Math.sin(ang)*0.55; const l=Math.hypot(wx,wy); wx/=l; wy/=l;}
|
|
224
|
-
const hw=(P.width*0.5)*(R*0.34); ctx.beginPath(); ctx.moveTo(px-wx*hw,py-wy*hw); ctx.lineTo(px+wx*hw,py+wy*hw); ctx.stroke(); }
|
|
225
|
-
ctx.fillStyle='#cfe1ff'; ctx.font='12px monospace'; ctx.fillText('头(u=0)', cx+Math.cos(a0)*R-10, cy+Math.sin(a0)*R*0.55-8);
|
|
226
|
-
ctx.fillText('尾(u=1)', cx+Math.cos(a0+span)*R-10, cy+Math.sin(a0+span)*R*0.55+16);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function redraw(){ readParams(); drawTex(); drawChannels(); drawStrip(); drawArc(); }
|
|
230
|
-
img.onload = () => {
|
|
231
|
-
const off = document.createElement('canvas'); off.width=img.width; off.height=img.height;
|
|
232
|
-
const octx = off.getContext('2d'); octx.drawImage(img,0,0);
|
|
233
|
-
texW=img.width; texH=img.height; texData=octx.getImageData(0,0,texW,texH).data;
|
|
234
|
-
redraw();
|
|
235
|
-
};
|
|
236
|
-
img.src = TEX_DATA_URI;
|
|
237
|
-
document.querySelectorAll('input,select').forEach(el => el.addEventListener('input', redraw));
|
|
238
|
-
window.addEventListener('resize', redraw);
|
|
239
|
-
</script>
|
|
240
|
-
</body>
|
|
241
|
-
</html>
|
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
import { Vector3 } from '@babylonjs/core/Maths/math.vector';
|
|
2
|
-
import type { Scene } from '@babylonjs/core/scene';
|
|
3
|
-
import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
4
|
-
import { VFX_DEFAULT_GENERATION_PARAMS, VFX_GENERATION_PARAM_KEYS } from './generation';
|
|
5
|
-
import type { VfxRegistry } from './registry';
|
|
6
|
-
import type {
|
|
7
|
-
VfxColorValue,
|
|
8
|
-
VfxDebugAnchor,
|
|
9
|
-
VfxEffectHandle,
|
|
10
|
-
VfxEffectPackage,
|
|
11
|
-
VfxParamValue,
|
|
12
|
-
VfxParamValues,
|
|
13
|
-
VfxSpawnParamKeys,
|
|
14
|
-
VfxSpawnTransform,
|
|
15
|
-
} from './types';
|
|
16
|
-
|
|
17
|
-
export interface EffectPackageServiceOptions {
|
|
18
|
-
/** 注入的注册表(由项目侧 glob + buildRegistry 构建);库不直接引用项目注册表 */
|
|
19
|
-
registry: VfxRegistry;
|
|
20
|
-
getDebugAnchorPosition?: (anchor: VfxDebugAnchor) => Vector3 | null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const DEFAULT_SPAWN_PARAM_KEYS: Required<Pick<VfxSpawnParamKeys, 'offsetX' | 'offsetY' | 'offsetZ'>> = {
|
|
24
|
-
offsetX: VFX_GENERATION_PARAM_KEYS.offsetX,
|
|
25
|
-
offsetY: VFX_GENERATION_PARAM_KEYS.offsetY,
|
|
26
|
-
offsetZ: VFX_GENERATION_PARAM_KEYS.offsetZ,
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export class EffectPackageService {
|
|
30
|
-
private readonly scene: Scene;
|
|
31
|
-
private readonly registry: VfxRegistry;
|
|
32
|
-
private readonly getDebugAnchorPosition?: (anchor: VfxDebugAnchor) => Vector3 | null;
|
|
33
|
-
private readonly activeHandles = new Set<VfxEffectHandle>();
|
|
34
|
-
private readonly savedParams: Record<string, Partial<VfxParamValues>>;
|
|
35
|
-
|
|
36
|
-
constructor(scene: Scene, options: EffectPackageServiceOptions) {
|
|
37
|
-
this.scene = scene;
|
|
38
|
-
this.registry = options.registry;
|
|
39
|
-
this.getDebugAnchorPosition = options.getDebugAnchorPosition;
|
|
40
|
-
this.savedParams = cloneOverrideMap(options.registry.savedParams);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
getEffectPackages(): VfxEffectPackage[] {
|
|
44
|
-
return this.registry.getEffectPackages();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
getEffectPackage(effectId: string): VfxEffectPackage | undefined {
|
|
48
|
-
return this.registry.getEffectPackage(effectId);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
getMergedParams(effectId: string, explicitParams?: Partial<VfxParamValues>): VfxParamValues | null {
|
|
52
|
-
const effectPackage = this.getEffectPackage(effectId);
|
|
53
|
-
if (!effectPackage) return null;
|
|
54
|
-
return mergeVfxParams(
|
|
55
|
-
VFX_DEFAULT_GENERATION_PARAMS,
|
|
56
|
-
effectPackage.defaultParams,
|
|
57
|
-
effectPackage.savedParams,
|
|
58
|
-
this.savedParams[effectId],
|
|
59
|
-
explicitParams,
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
getDebugOverride(effectId: string): Partial<VfxParamValues> {
|
|
64
|
-
return cloneParams(this.savedParams[effectId]);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
setDebugOverride(effectId: string, params: Partial<VfxParamValues>): void {
|
|
68
|
-
this.savedParams[effectId] = cloneParams(params);
|
|
69
|
-
const effectPackage = this.getEffectPackage(effectId);
|
|
70
|
-
if (effectPackage) effectPackage.savedParams = cloneParams(params);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
playEffectPackage(
|
|
74
|
-
effectId: string,
|
|
75
|
-
explicitParams?: Partial<VfxParamValues>,
|
|
76
|
-
spawnTransform?: VfxSpawnTransform | Vector3,
|
|
77
|
-
): VfxEffectHandle | null {
|
|
78
|
-
const effectPackage = this.getEffectPackage(effectId);
|
|
79
|
-
if (!effectPackage) {
|
|
80
|
-
console.warn(`[EffectPackageService] Unknown VFX effect package: ${effectId}`);
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const params = this.getMergedParams(effectId, explicitParams);
|
|
85
|
-
if (!params) return null;
|
|
86
|
-
|
|
87
|
-
const spawn = this.resolveSpawnTransform(effectPackage, params, spawnTransform);
|
|
88
|
-
const runtimeParams = stripSpawnParams(params, effectPackage.spawnParams);
|
|
89
|
-
const root = new TransformNode(`vfx_package_${effectId}`, this.scene);
|
|
90
|
-
root.position.copyFrom(spawn.position);
|
|
91
|
-
root.rotation.y = spawn.rotationY;
|
|
92
|
-
let handle: VfxEffectHandle;
|
|
93
|
-
try {
|
|
94
|
-
handle = effectPackage.create({
|
|
95
|
-
scene: this.scene,
|
|
96
|
-
root,
|
|
97
|
-
position: spawn.position.clone(),
|
|
98
|
-
rotationY: spawn.rotationY,
|
|
99
|
-
renderingGroupId: spawn.renderingGroupId,
|
|
100
|
-
params: runtimeParams,
|
|
101
|
-
});
|
|
102
|
-
} catch (error) {
|
|
103
|
-
// 错误兜底:单个坏包不得崩掉整个场景(plan §7)
|
|
104
|
-
console.warn(`[EffectPackageService] effect "${effectId}" create() failed; skipped.`, error);
|
|
105
|
-
try { root.dispose(); } catch { /* ignore */ }
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
this.activeHandles.add(handle);
|
|
109
|
-
const originalDispose = handle.dispose.bind(handle);
|
|
110
|
-
handle.dispose = () => {
|
|
111
|
-
this.activeHandles.delete(handle);
|
|
112
|
-
originalDispose();
|
|
113
|
-
};
|
|
114
|
-
return handle;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
dispose(): void {
|
|
118
|
-
for (const handle of this.activeHandles) {
|
|
119
|
-
handle.dispose();
|
|
120
|
-
}
|
|
121
|
-
this.activeHandles.clear();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
private resolveDebugPosition(effectPackage: VfxEffectPackage): Vector3 {
|
|
125
|
-
const anchor = effectPackage.debugAnchor ?? 'playerRight';
|
|
126
|
-
const anchorPosition = this.getDebugAnchorPosition?.(anchor);
|
|
127
|
-
if (anchorPosition) return anchorPosition.clone();
|
|
128
|
-
const cameraTarget = (this.scene.activeCamera as any)?.target as Vector3 | undefined;
|
|
129
|
-
if (cameraTarget?.clone) return cameraTarget.clone().addInPlace(new Vector3(1.2, 0, 0));
|
|
130
|
-
return new Vector3(0, 0, 0);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
private resolveSpawnTransform(
|
|
134
|
-
effectPackage: VfxEffectPackage,
|
|
135
|
-
params: VfxParamValues,
|
|
136
|
-
spawnTransform?: VfxSpawnTransform | Vector3,
|
|
137
|
-
): Required<Pick<VfxSpawnTransform, 'position' | 'rotationY'>> & Pick<VfxSpawnTransform, 'renderingGroupId'> {
|
|
138
|
-
const normalized = normalizeSpawnTransform(spawnTransform);
|
|
139
|
-
const basePosition = normalized.position?.clone() ?? this.resolveDebugPosition(effectPackage);
|
|
140
|
-
const rotationY = readFiniteNumber(normalized.rotationY, 0);
|
|
141
|
-
const renderingGroupId = readOptionalFiniteNumber(normalized.renderingGroupId);
|
|
142
|
-
const offset = readSpawnOffset(params, effectPackage.spawnParams);
|
|
143
|
-
if (offset.lengthSquared() <= 0.0000001) {
|
|
144
|
-
return { position: basePosition, rotationY, renderingGroupId };
|
|
145
|
-
}
|
|
146
|
-
const cos = Math.cos(rotationY);
|
|
147
|
-
const sin = Math.sin(rotationY);
|
|
148
|
-
return {
|
|
149
|
-
position: basePosition.addInPlace(new Vector3(
|
|
150
|
-
offset.x * cos - offset.z * sin,
|
|
151
|
-
offset.y,
|
|
152
|
-
offset.x * sin + offset.z * cos,
|
|
153
|
-
)),
|
|
154
|
-
rotationY,
|
|
155
|
-
renderingGroupId,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function normalizeSpawnTransform(spawnTransform?: VfxSpawnTransform | Vector3): VfxSpawnTransform {
|
|
161
|
-
if (!spawnTransform) return {};
|
|
162
|
-
if (spawnTransform instanceof Vector3) return { position: spawnTransform };
|
|
163
|
-
return spawnTransform;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function readSpawnOffset(params: VfxParamValues, spawnParams?: VfxSpawnParamKeys): Vector3 {
|
|
167
|
-
const keys = resolveSpawnParamKeys(spawnParams);
|
|
168
|
-
return new Vector3(
|
|
169
|
-
readParamNumber(params[keys.offsetX ?? ''], 0),
|
|
170
|
-
readParamNumber(params[keys.offsetY ?? ''], 0),
|
|
171
|
-
readParamNumber(params[keys.offsetZ ?? ''], 0),
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function stripSpawnParams(params: VfxParamValues, spawnParams?: VfxSpawnParamKeys): VfxParamValues {
|
|
176
|
-
const result = cloneParams(params) as VfxParamValues;
|
|
177
|
-
const keys = resolveSpawnParamKeys(spawnParams);
|
|
178
|
-
for (const key of [keys.offsetX, keys.offsetY, keys.offsetZ]) {
|
|
179
|
-
if (key) delete result[key];
|
|
180
|
-
}
|
|
181
|
-
return result;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function resolveSpawnParamKeys(spawnParams?: VfxSpawnParamKeys): VfxSpawnParamKeys {
|
|
185
|
-
return spawnParams ?? DEFAULT_SPAWN_PARAM_KEYS;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function readParamNumber(value: VfxParamValue | undefined, fallback: number): number {
|
|
189
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function readFiniteNumber(value: unknown, fallback: number): number {
|
|
193
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function readOptionalFiniteNumber(value: unknown): number | undefined {
|
|
197
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function mergeVfxParams(
|
|
201
|
-
...sources: Array<Partial<VfxParamValues> | undefined>
|
|
202
|
-
): VfxParamValues {
|
|
203
|
-
const result: Partial<VfxParamValues> = {};
|
|
204
|
-
for (const source of sources) {
|
|
205
|
-
Object.assign(result, cloneParams(source));
|
|
206
|
-
}
|
|
207
|
-
return result as VfxParamValues;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function cloneParams(value: Partial<VfxParamValues> | undefined): Partial<VfxParamValues> {
|
|
211
|
-
if (!value) return {};
|
|
212
|
-
const result: Partial<VfxParamValues> = {};
|
|
213
|
-
for (const [key, item] of Object.entries(value)) {
|
|
214
|
-
if (item === undefined) continue;
|
|
215
|
-
result[key] = cloneParamValue(item);
|
|
216
|
-
}
|
|
217
|
-
return result;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function cloneOverrideMap(value: Record<string, Partial<VfxParamValues>>): Record<string, Partial<VfxParamValues>> {
|
|
221
|
-
const result: Record<string, Partial<VfxParamValues>> = {};
|
|
222
|
-
for (const [effectId, params] of Object.entries(value)) {
|
|
223
|
-
result[effectId] = cloneParams(params);
|
|
224
|
-
}
|
|
225
|
-
return result;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function cloneParamValue(value: VfxParamValue): VfxParamValue {
|
|
229
|
-
if (isColorValue(value)) return { ...value };
|
|
230
|
-
return value;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function isColorValue(value: unknown): value is VfxColorValue {
|
|
234
|
-
return !!value
|
|
235
|
-
&& typeof value === 'object'
|
|
236
|
-
&& !Array.isArray(value)
|
|
237
|
-
&& typeof (value as VfxColorValue).r === 'number'
|
|
238
|
-
&& typeof (value as VfxColorValue).g === 'number'
|
|
239
|
-
&& typeof (value as VfxColorValue).b === 'number';
|
|
240
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SceneVfxService —— 特效播放门面(库纯净版)
|
|
3
|
-
*
|
|
4
|
-
* 只委托 EffectPackageService:播放 / 预热 / 销毁。注册表经 options.registry 注入。
|
|
5
|
-
* 注:旧版的 `initFromConfig`/`createParticleSystem`(读 scene.json + UIImages 的常驻场景粒子路径)
|
|
6
|
-
* 属项目耦合,已从库剥离(契约 §17)。场景常驻粒子应建模为普通特效包,由项目在 init 时触发。
|
|
7
|
-
*/
|
|
8
|
-
import { Scene } from '@babylonjs/core/scene';
|
|
9
|
-
import { Vector3 } from '@babylonjs/core/Maths/math.vector';
|
|
10
|
-
|
|
11
|
-
import type { VfxRegistry } from './registry';
|
|
12
|
-
import type {
|
|
13
|
-
VfxDebugAnchor,
|
|
14
|
-
VfxEffectHandle,
|
|
15
|
-
VfxEffectPackage,
|
|
16
|
-
VfxParamValues,
|
|
17
|
-
VfxSpawnTransform,
|
|
18
|
-
} from './types';
|
|
19
|
-
import { EffectPackageService } from './EffectPackageService';
|
|
20
|
-
|
|
21
|
-
export interface SceneVfxWarmupEntry {
|
|
22
|
-
effectId: string;
|
|
23
|
-
params?: Partial<VfxParamValues>;
|
|
24
|
-
spawnTransform?: VfxSpawnTransform | Vector3;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface SceneVfxServiceOptions {
|
|
28
|
-
/** 注入的注册表(项目侧 glob + buildRegistry 构建) */
|
|
29
|
-
registry: VfxRegistry;
|
|
30
|
-
getDebugAnchorPosition?: (anchor: VfxDebugAnchor) => Vector3 | null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export class SceneVfxService {
|
|
34
|
-
private effectPackageService: EffectPackageService;
|
|
35
|
-
|
|
36
|
-
constructor(scene: Scene, options: SceneVfxServiceOptions) {
|
|
37
|
-
this.effectPackageService = new EffectPackageService(scene, {
|
|
38
|
-
registry: options.registry,
|
|
39
|
-
getDebugAnchorPosition: options.getDebugAnchorPosition,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
getEffectPackageService(): EffectPackageService {
|
|
44
|
-
return this.effectPackageService;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
getEffectPackages(): VfxEffectPackage[] {
|
|
48
|
-
return this.effectPackageService.getEffectPackages();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
getMergedParams(effectId: string, explicitParams?: Partial<VfxParamValues>): VfxParamValues | null {
|
|
52
|
-
return this.effectPackageService.getMergedParams(effectId, explicitParams);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
playEffectPackage(
|
|
56
|
-
effectId: string,
|
|
57
|
-
params: Partial<VfxParamValues> = {},
|
|
58
|
-
spawnTransform?: VfxSpawnTransform | Vector3,
|
|
59
|
-
): VfxEffectHandle | null {
|
|
60
|
-
return this.effectPackageService.playEffectPackage(effectId, params, spawnTransform);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async warmupEffectPackages(entries: SceneVfxWarmupEntry[]): Promise<void> {
|
|
64
|
-
await Promise.all(entries.map((entry) => this.warmupEffectPackage(entry)));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
private async warmupEffectPackage(entry: SceneVfxWarmupEntry): Promise<void> {
|
|
68
|
-
let handle: VfxEffectHandle | null = null;
|
|
69
|
-
try {
|
|
70
|
-
handle = this.playEffectPackage(entry.effectId, entry.params ?? {}, entry.spawnTransform);
|
|
71
|
-
if (!handle) return;
|
|
72
|
-
await handle.ready;
|
|
73
|
-
await this.waitForAnimationFrames(2);
|
|
74
|
-
} catch (error) {
|
|
75
|
-
console.warn(`[SceneVfxService] Failed to warm up effect package "${entry.effectId}".`, error);
|
|
76
|
-
} finally {
|
|
77
|
-
handle?.dispose();
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
private async waitForAnimationFrames(frameCount: number): Promise<void> {
|
|
82
|
-
for (let i = 0; i < frameCount; i += 1) {
|
|
83
|
-
await new Promise<void>((resolve) => {
|
|
84
|
-
if (typeof requestAnimationFrame === 'function') {
|
|
85
|
-
requestAnimationFrame(() => resolve());
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
window.setTimeout(resolve, 0);
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
dispose(): void {
|
|
94
|
-
this.effectPackageService.dispose();
|
|
95
|
-
}
|
|
96
|
-
}
|