@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.
Files changed (250) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +41 -0
  3. package/dist/effects/coin-burst/index.js +6 -2
  4. package/dist/effects/comet-trail-flame/index.js +3 -1
  5. package/dist/effects/debris-smoke/index.js +1 -0
  6. package/dist/effects/electric-arc/index.js +1 -0
  7. package/dist/effects/energy-shield/index.js +1 -0
  8. package/dist/effects/explosion/index.js +3 -2
  9. package/dist/effects/hit-flash/index.js +12 -4
  10. package/dist/effects/laser-beam/index.js +14 -2
  11. package/dist/effects/lumber_vehicle_upgrade_aura/index.js +1 -0
  12. package/dist/effects/monster-hit/index.js +2 -1
  13. package/dist/effects/player-upgrade/index.js +5 -3
  14. package/dist/effects/pulse-laser-volley/index.js +14 -4
  15. package/dist/effects/thruster-flame/index.js +3 -1
  16. package/dist/effects/treasure-attention/index.d.ts +54 -0
  17. package/dist/effects/treasure-attention/index.js +535 -0
  18. package/dist/effects/ufo-thread-flame/index.js +1 -0
  19. package/dist/effects/vehicle-upgrade/index.js +4 -1
  20. package/dist/effects/weapon-trail/index.js +8 -3
  21. package/dist/effects/wood_crusher_sawdust/index.d.ts +4 -0
  22. package/dist/effects/wood_cutter_sawdust/index.d.ts +4 -0
  23. package/dist/index.js +85 -17
  24. package/dist/types.d.ts +80 -0
  25. package/index.json +19 -1
  26. package/package.json +9 -4
  27. package/packages/effects/treasure-attention/vfx-params.json +49 -0
  28. package/tooling/vfx-cli.mjs +8 -1
  29. package/docs/AUTHORING_EFFECTS.md +0 -297
  30. package/docs/VFX_LIBRARY_ARCHITECTURE_chatgpt.md +0 -1580
  31. package/docs/VFX_LIBRARY_EXECUTION_PLAN.md +0 -229
  32. package/docs/VFX_LIBRARY_PLAN.md +0 -546
  33. package/docs/vfx-ab/RESULTS.md +0 -35
  34. package/docs/vfx-ab/lumber-order/vehicle-exhaust-smoke/after.png +0 -0
  35. package/docs/vfx-ab/lumber-order/vehicle-exhaust-smoke/before.png +0 -0
  36. package/docs/vfx-ab/lumber-truck/vehicle-exhaust-smoke/after.png +0 -0
  37. package/docs/vfx-ab/lumber-truck/vehicle-exhaust-smoke/before.png +0 -0
  38. package/docs/vfx-integration_chatgpt.md +0 -462
  39. package/docs/weapon-trail-visualization.html +0 -241
  40. package/packages/EffectPackageService.ts +0 -240
  41. package/packages/SceneVfxService.ts +0 -96
  42. package/packages/build.mesh.yellow_build_upgrade_indicator/assets/runtime/simpleMeshEffect.v1.js +0 -333
  43. package/packages/build.mesh.yellow_build_upgrade_indicator/assets/shaders/mesh-effect-preview.frag.glsl +0 -25
  44. package/packages/build.mesh.yellow_build_upgrade_indicator/assets/shaders/mesh-effect-preview.vert.glsl +0 -11
  45. package/packages/build.mesh.yellow_build_upgrade_indicator/assets/textures/effect_jianbian.png +0 -0
  46. package/packages/build.mesh.yellow_build_upgrade_indicator/effect.json +0 -927
  47. package/packages/build.mesh.yellow_build_upgrade_indicator/media/contact-sheet.png +0 -0
  48. package/packages/build.mesh.yellow_build_upgrade_indicator/media/preview.gif +0 -0
  49. package/packages/build.mesh.yellow_build_upgrade_indicator/media/preview.mp4 +0 -0
  50. package/packages/build.mesh.yellow_build_upgrade_indicator/media/preview.webm +0 -0
  51. package/packages/build.mesh.yellow_build_upgrade_indicator/media/thumbnail.png +0 -0
  52. package/packages/build.mesh.yellow_build_upgrade_indicator/metadata/description.json +0 -197
  53. package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/03-effect.original.json +0 -895
  54. package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/03-normalized-vfx-to-babylon-report.json +0 -241
  55. package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/04-preview-overrides.json +0 -542
  56. package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/04-preview-tuning-report.json +0 -69
  57. package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/asset-map.json +0 -125
  58. package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/lineage.json +0 -44
  59. package/packages/build.particle.golden_upgrade_starburst/assets/runtime/particleEmitter.v1.js +0 -210
  60. package/packages/build.particle.golden_upgrade_starburst/assets/textures/effect_guangdian.png +0 -0
  61. package/packages/build.particle.golden_upgrade_starburst/effect.json +0 -880
  62. package/packages/build.particle.golden_upgrade_starburst/media/contact-sheet.png +0 -0
  63. package/packages/build.particle.golden_upgrade_starburst/media/preview.gif +0 -0
  64. package/packages/build.particle.golden_upgrade_starburst/media/preview.mp4 +0 -0
  65. package/packages/build.particle.golden_upgrade_starburst/media/preview.webm +0 -0
  66. package/packages/build.particle.golden_upgrade_starburst/media/thumbnail.png +0 -0
  67. package/packages/build.particle.golden_upgrade_starburst/metadata/description.json +0 -161
  68. package/packages/build.particle.golden_upgrade_starburst/provenance/03-effect.original.json +0 -762
  69. package/packages/build.particle.golden_upgrade_starburst/provenance/03-normalized-vfx-to-babylon-report.json +0 -260
  70. package/packages/build.particle.golden_upgrade_starburst/provenance/04-preview-overrides.json +0 -436
  71. package/packages/build.particle.golden_upgrade_starburst/provenance/04-preview-tuning-report.json +0 -40
  72. package/packages/build.particle.golden_upgrade_starburst/provenance/asset-map.json +0 -115
  73. package/packages/build.particle.golden_upgrade_starburst/provenance/lineage.json +0 -40
  74. package/packages/build.particle.orange_upgrade_transform_sparks/assets/runtime/particleEmitter.v1.js +0 -210
  75. package/packages/build.particle.orange_upgrade_transform_sparks/assets/textures/b5b27ab1-e740-4398-b407-848fc2b2c897.png +0 -0
  76. package/packages/build.particle.orange_upgrade_transform_sparks/effect.json +0 -887
  77. package/packages/build.particle.orange_upgrade_transform_sparks/media/preview.gif +0 -0
  78. package/packages/build.particle.orange_upgrade_transform_sparks/media/preview.mp4 +0 -0
  79. package/packages/build.particle.orange_upgrade_transform_sparks/media/preview.webm +0 -0
  80. package/packages/build.particle.orange_upgrade_transform_sparks/media/thumbnail.png +0 -0
  81. package/packages/build.particle.orange_upgrade_transform_sparks/metadata/description.json +0 -179
  82. package/packages/build.particle.orange_upgrade_transform_sparks/provenance/03-effect.original.json +0 -859
  83. package/packages/build.particle.orange_upgrade_transform_sparks/provenance/03-normalized-vfx-to-babylon-report.json +0 -260
  84. package/packages/build.particle.orange_upgrade_transform_sparks/provenance/04-preview-overrides.json +0 -478
  85. package/packages/build.particle.orange_upgrade_transform_sparks/provenance/04-preview-tuning-report.json +0 -40
  86. package/packages/build.particle.orange_upgrade_transform_sparks/provenance/asset-map.json +0 -114
  87. package/packages/build.particle.orange_upgrade_transform_sparks/provenance/lineage.json +0 -41
  88. package/packages/build.particle.wood_chip_dust_burst/assets/runtime/particleEmitter.v1.js +0 -210
  89. package/packages/build.particle.wood_chip_dust_burst/assets/textures/effect_yanwu4.png +0 -0
  90. package/packages/build.particle.wood_chip_dust_burst/assets/textures/suipian3.png +0 -0
  91. package/packages/build.particle.wood_chip_dust_burst/effect.json +0 -1400
  92. package/packages/build.particle.wood_chip_dust_burst/media/contact-sheet.png +0 -0
  93. package/packages/build.particle.wood_chip_dust_burst/media/preview.gif +0 -0
  94. package/packages/build.particle.wood_chip_dust_burst/media/preview.mp4 +0 -0
  95. package/packages/build.particle.wood_chip_dust_burst/media/preview.webm +0 -0
  96. package/packages/build.particle.wood_chip_dust_burst/media/thumbnail.png +0 -0
  97. package/packages/build.particle.wood_chip_dust_burst/metadata/description.json +0 -173
  98. package/packages/build.particle.wood_chip_dust_burst/provenance/03-effect.original.json +0 -1277
  99. package/packages/build.particle.wood_chip_dust_burst/provenance/03-normalized-vfx-to-babylon-report.json +0 -329
  100. package/packages/build.particle.wood_chip_dust_burst/provenance/04-preview-overrides.json +0 -749
  101. package/packages/build.particle.wood_chip_dust_burst/provenance/04-preview-tuning-report.json +0 -60
  102. package/packages/build.particle.wood_chip_dust_burst/provenance/asset-map.json +0 -146
  103. package/packages/build.particle.wood_chip_dust_burst/provenance/lineage.json +0 -69
  104. package/packages/effects/building-upgrade/effect.json +0 -6
  105. package/packages/effects/building-upgrade/index.ts +0 -387
  106. package/packages/effects/character-entrance/effect.json +0 -6
  107. package/packages/effects/character-entrance/index.ts +0 -308
  108. package/packages/effects/coin-burst/index.ts +0 -43
  109. package/packages/effects/comet-trail-flame/assets/textures/trail_5x4.webp +0 -0
  110. package/packages/effects/comet-trail-flame/index.ts +0 -106
  111. package/packages/effects/conveyor-upgrade/effect.json +0 -6
  112. package/packages/effects/conveyor-upgrade/index.ts +0 -401
  113. package/packages/effects/debris-smoke/assets/models/rock_debris.glb +0 -0
  114. package/packages/effects/debris-smoke/assets/textures/smoke22.webp +0 -0
  115. package/packages/effects/debris-smoke/index.ts +0 -101
  116. package/packages/effects/door_open_gold_flakes/assets/runtime/goldFlakes.v1.js +0 -397
  117. package/packages/effects/door_open_gold_flakes/door_open_gold_flakes.json +0 -498
  118. package/packages/effects/door_open_gold_flakes/index.ts +0 -11
  119. package/packages/effects/electric-arc/assets/textures/spark_4x4.webp +0 -0
  120. package/packages/effects/electric-arc/index.ts +0 -62
  121. package/packages/effects/energy-shield/effect.json +0 -7
  122. package/packages/effects/energy-shield/index.ts +0 -431
  123. package/packages/effects/explosion/assets/textures/baozha29.webp +0 -0
  124. package/packages/effects/explosion/assets/textures/glow01.webp +0 -0
  125. package/packages/effects/explosion/assets/textures/lizi01.webp +0 -0
  126. package/packages/effects/explosion/assets/textures/xulie_lizibaozha02_5x5.webp +0 -0
  127. package/packages/effects/explosion/index.ts +0 -114
  128. package/packages/effects/ground-tile-upgrade/effect.json +0 -6
  129. package/packages/effects/ground-tile-upgrade/index.ts +0 -410
  130. package/packages/effects/hit-flash/index.ts +0 -69
  131. package/packages/effects/laser-beam/assets/textures/laser_glow.webp +0 -0
  132. package/packages/effects/laser-beam/assets/textures/laser_noise.webp +0 -0
  133. package/packages/effects/laser-beam/index.ts +0 -6
  134. package/packages/effects/laser-beam-factory.ts +0 -135
  135. package/packages/effects/lumber-cart-suction/effect.json +0 -6
  136. package/packages/effects/lumber-cart-suction/index.ts +0 -399
  137. package/packages/effects/lumber-debris-scatter/assets/models/lumber_prop_model.glb +0 -0
  138. package/packages/effects/lumber-debris-scatter/assets/models/lumber_prop_model.glb.versions/.active +0 -1
  139. package/packages/effects/lumber-debris-scatter/assets/models/lumber_prop_model.glb.versions/1781589063890_upload.glb +0 -0
  140. package/packages/effects/lumber-debris-scatter/assets/runtime/solidParticleScatter.v1.js +0 -236
  141. package/packages/effects/lumber-debris-scatter/effect.json +0 -228
  142. package/packages/effects/lumber-debris-scatter/index.ts +0 -780
  143. package/packages/effects/lumber_vehicle_gear_spin/assets/runtime/gearSpin.v1.js +0 -202
  144. package/packages/effects/lumber_vehicle_gear_spin/index.ts +0 -11
  145. package/packages/effects/lumber_vehicle_gear_spin/lumber_vehicle_gear_spin.json +0 -338
  146. package/packages/effects/lumber_vehicle_upgrade_aura/assets/runtime/upgradeAura.v1.js +0 -353
  147. package/packages/effects/lumber_vehicle_upgrade_aura/index.ts +0 -11
  148. package/packages/effects/lumber_vehicle_upgrade_aura/lumber_vehicle_upgrade_aura.json +0 -365
  149. package/packages/effects/lumber_vehicle_wood_chips/assets/runtime/solidParticleScatter.v1.js +0 -310
  150. package/packages/effects/lumber_vehicle_wood_chips/index.ts +0 -11
  151. package/packages/effects/lumber_vehicle_wood_chips/lumber_vehicle_wood_chips.json +0 -335
  152. package/packages/effects/monster-hit/index.ts +0 -128
  153. package/packages/effects/player-upgrade/effect.json +0 -6
  154. package/packages/effects/player-upgrade/index.ts +0 -635
  155. package/packages/effects/pulse-laser-volley/assets/textures/bolt_guangshu.webp +0 -0
  156. package/packages/effects/pulse-laser-volley/index.ts +0 -204
  157. package/packages/effects/resource-collect-sparkles/effect.json +0 -6
  158. package/packages/effects/resource-collect-sparkles/index.ts +0 -254
  159. package/packages/effects/run-footstep-dust/assets/runtime/runFootstepDust.v1.js +0 -215
  160. package/packages/effects/run-footstep-dust/effect.json +0 -183
  161. package/packages/effects/run-footstep-dust/index.ts +0 -377
  162. package/packages/effects/space-shared.ts +0 -120
  163. package/packages/effects/thruster-flame/index.ts +0 -305
  164. package/packages/effects/tree-chop-dust/assets/textures/tree_chop_dust_atlas.png +0 -0
  165. package/packages/effects/tree-chop-dust/effect.json +0 -8
  166. package/packages/effects/tree-chop-dust/index.ts +0 -202
  167. package/packages/effects/tree-leaf-chop-burst/assets/runtime/particleEmitter.v1.js +0 -303
  168. package/packages/effects/tree-leaf-chop-burst/assets/textures/tree_leaf_chop_burst_leaf_atlas.png +0 -0
  169. package/packages/effects/tree-leaf-chop-burst/effect.json +0 -498
  170. package/packages/effects/tree-leaf-chop-burst/index.ts +0 -294
  171. package/packages/effects/tree_chop_dust_burst/assets/runtime/particleEmitter.v1.js +0 -318
  172. package/packages/effects/tree_chop_dust_burst/assets/textures/tree_chop_dust_burst_dust.png +0 -0
  173. package/packages/effects/tree_chop_dust_burst/index.ts +0 -13
  174. package/packages/effects/tree_chop_dust_burst/tree_chop_dust_burst.json +0 -412
  175. package/packages/effects/ufo-thread-flame/assets/textures/flame_flow.webp +0 -0
  176. package/packages/effects/ufo-thread-flame/assets/textures/flame_spike.webp +0 -0
  177. package/packages/effects/ufo-thread-flame/index.ts +0 -152
  178. package/packages/effects/vehicle-exhaust-smoke/assets/textures/vehicle_exhaust_smoke_atlas.png +0 -0
  179. package/packages/effects/vehicle-exhaust-smoke/effect.json +0 -8
  180. package/packages/effects/vehicle-exhaust-smoke/index.ts +0 -298
  181. package/packages/effects/vehicle-upgrade/effect.json +0 -6
  182. package/packages/effects/vehicle-upgrade/index.ts +0 -401
  183. package/packages/effects/weapon-trail/assets/textures/trail_wedge.png +0 -0
  184. package/packages/effects/weapon-trail/index.ts +0 -313
  185. package/packages/effects/wood-crusher-sawdust/assets/runtime/particleEmitter.v1.js +0 -312
  186. package/packages/effects/wood-crusher-sawdust/assets/textures/wood_crusher_sawdust_dust.png +0 -0
  187. package/packages/effects/wood-crusher-sawdust/assets/textures/wood_crusher_sawdust_fiber.png +0 -0
  188. package/packages/effects/wood-crusher-sawdust/index.ts +0 -43
  189. package/packages/effects/wood-crusher-sawdust/wood_crusher_sawdust.json +0 -592
  190. package/packages/effects/wood-cutter-sawdust/assets/runtime/particleEmitter.v1.js +0 -312
  191. package/packages/effects/wood-cutter-sawdust/assets/textures/wood_cutter_sawdust_chip.png +0 -0
  192. package/packages/effects/wood-cutter-sawdust/assets/textures/wood_cutter_sawdust_dust.png +0 -0
  193. package/packages/effects/wood-cutter-sawdust/index.ts +0 -43
  194. package/packages/effects/wood-cutter-sawdust/wood_cutter_sawdust.json +0 -592
  195. package/packages/effects/wood-sawdust-effect-factory.ts +0 -194
  196. package/packages/effects/wood_crusher_sawdust/assets/runtime/particleEmitter.v1.js +0 -318
  197. package/packages/effects/wood_crusher_sawdust/assets/textures/wood_crusher_sawdust_dust.png +0 -0
  198. package/packages/effects/wood_crusher_sawdust/assets/textures/wood_crusher_sawdust_fiber.png +0 -0
  199. package/packages/effects/wood_crusher_sawdust/index.ts +0 -29
  200. package/packages/effects/wood_crusher_sawdust/wood_crusher_sawdust.json +0 -592
  201. package/packages/effects/wood_cutter_sawdust/assets/runtime/particleEmitter.v1.js +0 -318
  202. package/packages/effects/wood_cutter_sawdust/assets/textures/wood_cutter_sawdust_chip.png +0 -0
  203. package/packages/effects/wood_cutter_sawdust/assets/textures/wood_cutter_sawdust_dust.png +0 -0
  204. package/packages/effects/wood_cutter_sawdust/index.ts +0 -29
  205. package/packages/effects/wood_cutter_sawdust/wood_cutter_sawdust.json +0 -592
  206. package/packages/effects/wood_peel_material_test/assets/runtime/woodPeelMaterialTest.v1.js +0 -196
  207. package/packages/effects/wood_peel_material_test/index.ts +0 -11
  208. package/packages/effects/wood_peel_material_test/wood_peel_material_test.json +0 -136
  209. package/packages/env.particle.forest_edge_fog/assets/runtime/particleEmitter.v1.js +0 -210
  210. package/packages/env.particle.forest_edge_fog/assets/textures/f12a23c4-b924-4322-a260-3d982428f1e8.png +0 -0
  211. package/packages/env.particle.forest_edge_fog/assets/textures/yuan.png +0 -0
  212. package/packages/env.particle.forest_edge_fog/effect.json +0 -854
  213. package/packages/env.particle.forest_edge_fog/media/preview.gif +0 -0
  214. package/packages/env.particle.forest_edge_fog/media/preview.mp4 +0 -0
  215. package/packages/env.particle.forest_edge_fog/media/preview.webm +0 -0
  216. package/packages/env.particle.forest_edge_fog/media/thumbnail.png +0 -0
  217. package/packages/env.particle.forest_edge_fog/metadata/description.json +0 -148
  218. package/packages/env.particle.forest_edge_fog/provenance/03-effect.original.json +0 -827
  219. package/packages/env.particle.forest_edge_fog/provenance/03-normalized-vfx-to-babylon-report.json +0 -196
  220. package/packages/env.particle.forest_edge_fog/provenance/04-preview-overrides.json +0 -432
  221. package/packages/env.particle.forest_edge_fog/provenance/04-preview-tuning-report.json +0 -28
  222. package/packages/env.particle.forest_edge_fog/provenance/asset-map.json +0 -124
  223. package/packages/env.particle.forest_edge_fog/provenance/lineage.json +0 -41
  224. package/packages/generation.ts +0 -14
  225. package/packages/index.ts +0 -14
  226. package/packages/legacy-particle-runtime.ts +0 -113
  227. package/packages/manifestRuntimePackage.ts +0 -519
  228. package/packages/registry.ts +0 -85
  229. package/packages/rendering.ts +0 -13
  230. package/packages/resource.particle.viscous_oil_suction/assets/runtime/top_disk_suction/particleEmitter.v1.js +0 -424
  231. package/packages/resource.particle.viscous_oil_suction/assets/textures/Glow ad.png +0 -0
  232. package/packages/resource.particle.viscous_oil_suction/effect.json +0 -1254
  233. package/packages/resource.particle.viscous_oil_suction/media/preview.gif +0 -0
  234. package/packages/resource.particle.viscous_oil_suction/media/preview.mp4 +0 -0
  235. package/packages/resource.particle.viscous_oil_suction/media/preview.webm +0 -0
  236. package/packages/resource.particle.viscous_oil_suction/media/thumbnail.png +0 -0
  237. package/packages/resource.particle.viscous_oil_suction/metadata/description.json +0 -122
  238. package/packages/resource.particle.viscous_oil_suction/provenance/03-effect.original.json +0 -1051
  239. package/packages/resource.particle.viscous_oil_suction/provenance/03-normalized-vfx-to-babylon-report.json +0 -200
  240. package/packages/resource.particle.viscous_oil_suction/provenance/04-preview-overrides.json +0 -581
  241. package/packages/resource.particle.viscous_oil_suction/provenance/04-preview-tuning-report.json +0 -28
  242. package/packages/resource.particle.viscous_oil_suction/provenance/asset-map.json +0 -111
  243. package/packages/resource.particle.viscous_oil_suction/provenance/lineage.json +0 -45
  244. package/packages/types.ts +0 -107
  245. package/tooling/check-dist-runtime.mjs +0 -166
  246. package/tooling/generate-index.mjs +0 -55
  247. package/tooling/test-guardrails.mjs +0 -157
  248. package/tooling/validate-boundary.mjs +0 -92
  249. package/tooling/validate-effects.mjs +0 -304
  250. 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
- }