@fps-games/vfx 0.3.2 → 0.3.4

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 (234) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +5 -5
  3. package/dist/effects/treasure-attention/index.d.ts +1 -1
  4. package/package.json +6 -5
  5. package/tooling/vfx-cli.mjs +31 -22
  6. package/tooling/vfx-package-resolution.mjs +8 -8
  7. package/docs/AUTHORING_EFFECTS.md +0 -297
  8. package/docs/INTEGRATION.md +0 -194
  9. package/docs/VFX_LIBRARY_ARCHITECTURE_chatgpt.md +0 -1580
  10. package/docs/VFX_LIBRARY_EXECUTION_PLAN.md +0 -229
  11. package/docs/VFX_LIBRARY_PLAN.md +0 -546
  12. package/docs/hit-flash-red-method.md +0 -155
  13. package/docs/treasure_attention_fx_babylonjs_guide.md +0 -901
  14. package/docs/vfx-ab/RESULTS.md +0 -35
  15. package/docs/vfx-ab/lumber-order/vehicle-exhaust-smoke/after.png +0 -0
  16. package/docs/vfx-ab/lumber-order/vehicle-exhaust-smoke/before.png +0 -0
  17. package/docs/vfx-ab/lumber-truck/vehicle-exhaust-smoke/after.png +0 -0
  18. package/docs/vfx-ab/lumber-truck/vehicle-exhaust-smoke/before.png +0 -0
  19. package/docs/vfx-integration_chatgpt.md +0 -462
  20. package/docs/vfx-placement-design.md +0 -164
  21. package/docs/weapon-trail-visualization.html +0 -241
  22. package/packages/EffectPackageService.ts +0 -346
  23. package/packages/SceneVfxService.ts +0 -96
  24. package/packages/build.mesh.yellow_build_upgrade_indicator/assets/runtime/simpleMeshEffect.v1.js +0 -333
  25. package/packages/build.mesh.yellow_build_upgrade_indicator/assets/shaders/mesh-effect-preview.frag.glsl +0 -25
  26. package/packages/build.mesh.yellow_build_upgrade_indicator/assets/shaders/mesh-effect-preview.vert.glsl +0 -11
  27. package/packages/build.mesh.yellow_build_upgrade_indicator/assets/textures/effect_jianbian.png +0 -0
  28. package/packages/build.mesh.yellow_build_upgrade_indicator/effect.json +0 -927
  29. package/packages/build.mesh.yellow_build_upgrade_indicator/media/contact-sheet.png +0 -0
  30. package/packages/build.mesh.yellow_build_upgrade_indicator/media/preview.gif +0 -0
  31. package/packages/build.mesh.yellow_build_upgrade_indicator/media/preview.mp4 +0 -0
  32. package/packages/build.mesh.yellow_build_upgrade_indicator/media/preview.webm +0 -0
  33. package/packages/build.mesh.yellow_build_upgrade_indicator/media/thumbnail.png +0 -0
  34. package/packages/build.mesh.yellow_build_upgrade_indicator/metadata/description.json +0 -197
  35. package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/03-effect.original.json +0 -895
  36. package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/03-normalized-vfx-to-babylon-report.json +0 -241
  37. package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/04-preview-overrides.json +0 -542
  38. package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/04-preview-tuning-report.json +0 -69
  39. package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/asset-map.json +0 -125
  40. package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/lineage.json +0 -44
  41. package/packages/build.particle.golden_upgrade_starburst/assets/runtime/particleEmitter.v1.js +0 -210
  42. package/packages/build.particle.golden_upgrade_starburst/assets/textures/effect_guangdian.png +0 -0
  43. package/packages/build.particle.golden_upgrade_starburst/effect.json +0 -880
  44. package/packages/build.particle.golden_upgrade_starburst/media/contact-sheet.png +0 -0
  45. package/packages/build.particle.golden_upgrade_starburst/media/preview.gif +0 -0
  46. package/packages/build.particle.golden_upgrade_starburst/media/preview.mp4 +0 -0
  47. package/packages/build.particle.golden_upgrade_starburst/media/preview.webm +0 -0
  48. package/packages/build.particle.golden_upgrade_starburst/media/thumbnail.png +0 -0
  49. package/packages/build.particle.golden_upgrade_starburst/metadata/description.json +0 -161
  50. package/packages/build.particle.golden_upgrade_starburst/provenance/03-effect.original.json +0 -762
  51. package/packages/build.particle.golden_upgrade_starburst/provenance/03-normalized-vfx-to-babylon-report.json +0 -260
  52. package/packages/build.particle.golden_upgrade_starburst/provenance/04-preview-overrides.json +0 -436
  53. package/packages/build.particle.golden_upgrade_starburst/provenance/04-preview-tuning-report.json +0 -40
  54. package/packages/build.particle.golden_upgrade_starburst/provenance/asset-map.json +0 -115
  55. package/packages/build.particle.golden_upgrade_starburst/provenance/lineage.json +0 -40
  56. package/packages/build.particle.orange_upgrade_transform_sparks/assets/runtime/particleEmitter.v1.js +0 -210
  57. package/packages/build.particle.orange_upgrade_transform_sparks/assets/textures/b5b27ab1-e740-4398-b407-848fc2b2c897.png +0 -0
  58. package/packages/build.particle.orange_upgrade_transform_sparks/effect.json +0 -887
  59. package/packages/build.particle.orange_upgrade_transform_sparks/media/preview.gif +0 -0
  60. package/packages/build.particle.orange_upgrade_transform_sparks/media/preview.mp4 +0 -0
  61. package/packages/build.particle.orange_upgrade_transform_sparks/media/preview.webm +0 -0
  62. package/packages/build.particle.orange_upgrade_transform_sparks/media/thumbnail.png +0 -0
  63. package/packages/build.particle.orange_upgrade_transform_sparks/metadata/description.json +0 -179
  64. package/packages/build.particle.orange_upgrade_transform_sparks/provenance/03-effect.original.json +0 -859
  65. package/packages/build.particle.orange_upgrade_transform_sparks/provenance/03-normalized-vfx-to-babylon-report.json +0 -260
  66. package/packages/build.particle.orange_upgrade_transform_sparks/provenance/04-preview-overrides.json +0 -478
  67. package/packages/build.particle.orange_upgrade_transform_sparks/provenance/04-preview-tuning-report.json +0 -40
  68. package/packages/build.particle.orange_upgrade_transform_sparks/provenance/asset-map.json +0 -114
  69. package/packages/build.particle.orange_upgrade_transform_sparks/provenance/lineage.json +0 -41
  70. package/packages/build.particle.wood_chip_dust_burst/assets/runtime/particleEmitter.v1.js +0 -210
  71. package/packages/build.particle.wood_chip_dust_burst/assets/textures/effect_yanwu4.png +0 -0
  72. package/packages/build.particle.wood_chip_dust_burst/assets/textures/suipian3.png +0 -0
  73. package/packages/build.particle.wood_chip_dust_burst/effect.json +0 -1400
  74. package/packages/build.particle.wood_chip_dust_burst/media/contact-sheet.png +0 -0
  75. package/packages/build.particle.wood_chip_dust_burst/media/preview.gif +0 -0
  76. package/packages/build.particle.wood_chip_dust_burst/media/preview.mp4 +0 -0
  77. package/packages/build.particle.wood_chip_dust_burst/media/preview.webm +0 -0
  78. package/packages/build.particle.wood_chip_dust_burst/media/thumbnail.png +0 -0
  79. package/packages/build.particle.wood_chip_dust_burst/metadata/description.json +0 -173
  80. package/packages/build.particle.wood_chip_dust_burst/provenance/03-effect.original.json +0 -1277
  81. package/packages/build.particle.wood_chip_dust_burst/provenance/03-normalized-vfx-to-babylon-report.json +0 -329
  82. package/packages/build.particle.wood_chip_dust_burst/provenance/04-preview-overrides.json +0 -749
  83. package/packages/build.particle.wood_chip_dust_burst/provenance/04-preview-tuning-report.json +0 -60
  84. package/packages/build.particle.wood_chip_dust_burst/provenance/asset-map.json +0 -146
  85. package/packages/build.particle.wood_chip_dust_burst/provenance/lineage.json +0 -69
  86. package/packages/effects/building-upgrade/effect.json +0 -6
  87. package/packages/effects/building-upgrade/index.ts +0 -387
  88. package/packages/effects/character-entrance/effect.json +0 -6
  89. package/packages/effects/character-entrance/index.ts +0 -308
  90. package/packages/effects/coin-burst/index.ts +0 -48
  91. package/packages/effects/comet-trail-flame/assets/textures/trail_5x4.webp +0 -0
  92. package/packages/effects/comet-trail-flame/index.ts +0 -107
  93. package/packages/effects/conveyor-upgrade/effect.json +0 -6
  94. package/packages/effects/conveyor-upgrade/index.ts +0 -401
  95. package/packages/effects/debris-smoke/assets/models/rock_debris.glb +0 -0
  96. package/packages/effects/debris-smoke/assets/textures/smoke22.webp +0 -0
  97. package/packages/effects/debris-smoke/index.ts +0 -102
  98. package/packages/effects/door_open_gold_flakes/assets/runtime/goldFlakes.v1.js +0 -397
  99. package/packages/effects/door_open_gold_flakes/door_open_gold_flakes.json +0 -498
  100. package/packages/effects/door_open_gold_flakes/index.ts +0 -11
  101. package/packages/effects/electric-arc/assets/textures/spark_4x4.webp +0 -0
  102. package/packages/effects/electric-arc/index.ts +0 -63
  103. package/packages/effects/energy-shield/effect.json +0 -7
  104. package/packages/effects/energy-shield/index.ts +0 -432
  105. package/packages/effects/explosion/assets/textures/baozha29.webp +0 -0
  106. package/packages/effects/explosion/assets/textures/glow01.webp +0 -0
  107. package/packages/effects/explosion/assets/textures/lizi01.webp +0 -0
  108. package/packages/effects/explosion/assets/textures/xulie_lizibaozha02_5x5.webp +0 -0
  109. package/packages/effects/explosion/index.ts +0 -115
  110. package/packages/effects/ground-tile-upgrade/effect.json +0 -6
  111. package/packages/effects/ground-tile-upgrade/index.ts +0 -410
  112. package/packages/effects/hit-flash/index.ts +0 -77
  113. package/packages/effects/laser-beam/assets/textures/laser_glow.webp +0 -0
  114. package/packages/effects/laser-beam/assets/textures/laser_noise.webp +0 -0
  115. package/packages/effects/laser-beam/index.ts +0 -6
  116. package/packages/effects/laser-beam-factory.ts +0 -146
  117. package/packages/effects/lumber-cart-suction/effect.json +0 -6
  118. package/packages/effects/lumber-cart-suction/index.ts +0 -399
  119. package/packages/effects/lumber-debris-scatter/assets/models/lumber_prop_model.glb +0 -0
  120. package/packages/effects/lumber-debris-scatter/assets/models/lumber_prop_model.glb.versions/.active +0 -1
  121. package/packages/effects/lumber-debris-scatter/assets/models/lumber_prop_model.glb.versions/1781589063890_upload.glb +0 -0
  122. package/packages/effects/lumber-debris-scatter/assets/runtime/solidParticleScatter.v1.js +0 -236
  123. package/packages/effects/lumber-debris-scatter/effect.json +0 -228
  124. package/packages/effects/lumber-debris-scatter/index.ts +0 -780
  125. package/packages/effects/lumber_vehicle_gear_spin/assets/runtime/gearSpin.v1.js +0 -202
  126. package/packages/effects/lumber_vehicle_gear_spin/index.ts +0 -11
  127. package/packages/effects/lumber_vehicle_gear_spin/lumber_vehicle_gear_spin.json +0 -338
  128. package/packages/effects/lumber_vehicle_upgrade_aura/assets/runtime/upgradeAura.v1.js +0 -353
  129. package/packages/effects/lumber_vehicle_upgrade_aura/index.ts +0 -13
  130. package/packages/effects/lumber_vehicle_upgrade_aura/lumber_vehicle_upgrade_aura.json +0 -365
  131. package/packages/effects/lumber_vehicle_wood_chips/assets/runtime/solidParticleScatter.v1.js +0 -310
  132. package/packages/effects/lumber_vehicle_wood_chips/index.ts +0 -11
  133. package/packages/effects/lumber_vehicle_wood_chips/lumber_vehicle_wood_chips.json +0 -335
  134. package/packages/effects/monster-hit/index.ts +0 -129
  135. package/packages/effects/player-upgrade/effect.json +0 -6
  136. package/packages/effects/player-upgrade/index.ts +0 -640
  137. package/packages/effects/pulse-laser-volley/assets/textures/bolt_guangshu.webp +0 -0
  138. package/packages/effects/pulse-laser-volley/index.ts +0 -215
  139. package/packages/effects/resource-collect-sparkles/effect.json +0 -6
  140. package/packages/effects/resource-collect-sparkles/index.ts +0 -254
  141. package/packages/effects/run-footstep-dust/assets/runtime/runFootstepDust.v1.js +0 -215
  142. package/packages/effects/run-footstep-dust/effect.json +0 -183
  143. package/packages/effects/run-footstep-dust/index.ts +0 -377
  144. package/packages/effects/space-shared.ts +0 -120
  145. package/packages/effects/thruster-flame/index.ts +0 -306
  146. package/packages/effects/treasure-attention/effect.json +0 -7
  147. package/packages/effects/treasure-attention/index.ts +0 -630
  148. package/packages/effects/tree-chop-dust/assets/textures/tree_chop_dust_atlas.png +0 -0
  149. package/packages/effects/tree-chop-dust/effect.json +0 -8
  150. package/packages/effects/tree-chop-dust/index.ts +0 -202
  151. package/packages/effects/tree-leaf-chop-burst/assets/runtime/particleEmitter.v1.js +0 -303
  152. package/packages/effects/tree-leaf-chop-burst/assets/textures/tree_leaf_chop_burst_leaf_atlas.png +0 -0
  153. package/packages/effects/tree-leaf-chop-burst/effect.json +0 -498
  154. package/packages/effects/tree-leaf-chop-burst/index.ts +0 -294
  155. package/packages/effects/tree_chop_dust_burst/assets/runtime/particleEmitter.v1.js +0 -318
  156. package/packages/effects/tree_chop_dust_burst/assets/textures/tree_chop_dust_burst_dust.png +0 -0
  157. package/packages/effects/tree_chop_dust_burst/index.ts +0 -13
  158. package/packages/effects/tree_chop_dust_burst/tree_chop_dust_burst.json +0 -412
  159. package/packages/effects/ufo-thread-flame/assets/textures/flame_flow.webp +0 -0
  160. package/packages/effects/ufo-thread-flame/assets/textures/flame_spike.webp +0 -0
  161. package/packages/effects/ufo-thread-flame/index.ts +0 -153
  162. package/packages/effects/vehicle-exhaust-smoke/assets/textures/vehicle_exhaust_smoke_atlas.png +0 -0
  163. package/packages/effects/vehicle-exhaust-smoke/effect.json +0 -8
  164. package/packages/effects/vehicle-exhaust-smoke/index.ts +0 -298
  165. package/packages/effects/vehicle-upgrade/effect.json +0 -6
  166. package/packages/effects/vehicle-upgrade/index.ts +0 -404
  167. package/packages/effects/weapon-trail/assets/textures/trail_wedge.png +0 -0
  168. package/packages/effects/weapon-trail/index.ts +0 -317
  169. package/packages/effects/wood-crusher-sawdust/assets/runtime/particleEmitter.v1.js +0 -312
  170. package/packages/effects/wood-crusher-sawdust/assets/textures/wood_crusher_sawdust_dust.png +0 -0
  171. package/packages/effects/wood-crusher-sawdust/assets/textures/wood_crusher_sawdust_fiber.png +0 -0
  172. package/packages/effects/wood-crusher-sawdust/index.ts +0 -43
  173. package/packages/effects/wood-crusher-sawdust/wood_crusher_sawdust.json +0 -592
  174. package/packages/effects/wood-cutter-sawdust/assets/runtime/particleEmitter.v1.js +0 -312
  175. package/packages/effects/wood-cutter-sawdust/assets/textures/wood_cutter_sawdust_chip.png +0 -0
  176. package/packages/effects/wood-cutter-sawdust/assets/textures/wood_cutter_sawdust_dust.png +0 -0
  177. package/packages/effects/wood-cutter-sawdust/index.ts +0 -43
  178. package/packages/effects/wood-cutter-sawdust/wood_cutter_sawdust.json +0 -592
  179. package/packages/effects/wood-sawdust-effect-factory.ts +0 -194
  180. package/packages/effects/wood_crusher_sawdust/assets/runtime/particleEmitter.v1.js +0 -318
  181. package/packages/effects/wood_crusher_sawdust/assets/textures/wood_crusher_sawdust_dust.png +0 -0
  182. package/packages/effects/wood_crusher_sawdust/assets/textures/wood_crusher_sawdust_fiber.png +0 -0
  183. package/packages/effects/wood_crusher_sawdust/index.ts +0 -29
  184. package/packages/effects/wood_crusher_sawdust/wood_crusher_sawdust.json +0 -592
  185. package/packages/effects/wood_cutter_sawdust/assets/runtime/particleEmitter.v1.js +0 -318
  186. package/packages/effects/wood_cutter_sawdust/assets/textures/wood_cutter_sawdust_chip.png +0 -0
  187. package/packages/effects/wood_cutter_sawdust/assets/textures/wood_cutter_sawdust_dust.png +0 -0
  188. package/packages/effects/wood_cutter_sawdust/index.ts +0 -29
  189. package/packages/effects/wood_cutter_sawdust/wood_cutter_sawdust.json +0 -592
  190. package/packages/effects/wood_peel_material_test/assets/runtime/woodPeelMaterialTest.v1.js +0 -196
  191. package/packages/effects/wood_peel_material_test/index.ts +0 -11
  192. package/packages/effects/wood_peel_material_test/wood_peel_material_test.json +0 -136
  193. package/packages/env.particle.forest_edge_fog/assets/runtime/particleEmitter.v1.js +0 -210
  194. package/packages/env.particle.forest_edge_fog/assets/textures/f12a23c4-b924-4322-a260-3d982428f1e8.png +0 -0
  195. package/packages/env.particle.forest_edge_fog/assets/textures/yuan.png +0 -0
  196. package/packages/env.particle.forest_edge_fog/effect.json +0 -854
  197. package/packages/env.particle.forest_edge_fog/media/preview.gif +0 -0
  198. package/packages/env.particle.forest_edge_fog/media/preview.mp4 +0 -0
  199. package/packages/env.particle.forest_edge_fog/media/preview.webm +0 -0
  200. package/packages/env.particle.forest_edge_fog/media/thumbnail.png +0 -0
  201. package/packages/env.particle.forest_edge_fog/metadata/description.json +0 -148
  202. package/packages/env.particle.forest_edge_fog/provenance/03-effect.original.json +0 -827
  203. package/packages/env.particle.forest_edge_fog/provenance/03-normalized-vfx-to-babylon-report.json +0 -196
  204. package/packages/env.particle.forest_edge_fog/provenance/04-preview-overrides.json +0 -432
  205. package/packages/env.particle.forest_edge_fog/provenance/04-preview-tuning-report.json +0 -28
  206. package/packages/env.particle.forest_edge_fog/provenance/asset-map.json +0 -124
  207. package/packages/env.particle.forest_edge_fog/provenance/lineage.json +0 -41
  208. package/packages/generation.ts +0 -14
  209. package/packages/index.ts +0 -14
  210. package/packages/legacy-particle-runtime.ts +0 -113
  211. package/packages/manifestRuntimePackage.ts +0 -519
  212. package/packages/registry.ts +0 -85
  213. package/packages/rendering.ts +0 -13
  214. package/packages/resource.particle.viscous_oil_suction/assets/runtime/top_disk_suction/particleEmitter.v1.js +0 -424
  215. package/packages/resource.particle.viscous_oil_suction/assets/textures/Glow ad.png +0 -0
  216. package/packages/resource.particle.viscous_oil_suction/effect.json +0 -1254
  217. package/packages/resource.particle.viscous_oil_suction/media/preview.gif +0 -0
  218. package/packages/resource.particle.viscous_oil_suction/media/preview.mp4 +0 -0
  219. package/packages/resource.particle.viscous_oil_suction/media/preview.webm +0 -0
  220. package/packages/resource.particle.viscous_oil_suction/media/thumbnail.png +0 -0
  221. package/packages/resource.particle.viscous_oil_suction/metadata/description.json +0 -122
  222. package/packages/resource.particle.viscous_oil_suction/provenance/03-effect.original.json +0 -1051
  223. package/packages/resource.particle.viscous_oil_suction/provenance/03-normalized-vfx-to-babylon-report.json +0 -200
  224. package/packages/resource.particle.viscous_oil_suction/provenance/04-preview-overrides.json +0 -581
  225. package/packages/resource.particle.viscous_oil_suction/provenance/04-preview-tuning-report.json +0 -28
  226. package/packages/resource.particle.viscous_oil_suction/provenance/asset-map.json +0 -111
  227. package/packages/resource.particle.viscous_oil_suction/provenance/lineage.json +0 -45
  228. package/packages/types.ts +0 -195
  229. package/tooling/check-dist-runtime.mjs +0 -166
  230. package/tooling/generate-index.mjs +0 -55
  231. package/tooling/test-guardrails.mjs +0 -157
  232. package/tooling/validate-boundary.mjs +0 -92
  233. package/tooling/validate-effects.mjs +0 -304
  234. package/tooling/validate.mjs +0 -53
@@ -1,901 +0,0 @@
1
- # 宝藏注意力特效实现文档
2
-
3
- 目标:实现一个类似截图中“宝藏/掉落物周围发光、闪烁、旋转”的游戏特效,用来吸引玩家注意。主实现目标是 **Babylon.js**。本文档也包含 Canvas 2D 原型思路,方便本地 coding agent 先快速验证视觉节奏。
4
-
5
- ---
6
-
7
- ## 1. 视觉拆解
8
-
9
- 这个特效可以拆成 5 层,从下到上绘制:
10
-
11
- 1. **地面柔光光斑**
12
- - 一个贴在地面的半透明金黄色圆/椭圆光斑。
13
- - 使用径向渐变贴图。
14
- - 缓慢脉冲缩放和透明度变化。
15
-
16
- 2. **地面旋转星芒**
17
- - 多片从中心向外发散的金黄色光束。
18
- - 绕 Y 轴慢速旋转。
19
- - 每片光束独立做轻微明暗变化,避免机械感。
20
-
21
- 3. **脉冲光环**
22
- - 一到两个贴地光环。
23
- - 缩放 + 透明度脉冲。
24
- - 用来强调“这里有可交互/可拾取物”。
25
-
26
- 4. **竖向柔光**
27
- - 宝藏中心附近有一个竖向的淡黄色 billboard 光柱。
28
- - 视觉上类似“宝物在发光”。
29
-
30
- 5. **上浮闪片粒子**
31
- - 小星星/菱形亮片从宝物附近向上浮起、旋转、淡出。
32
- - 使用 additive blending。
33
- - 粒子数量不要太多,否则会抢主体。
34
-
35
- ---
36
-
37
- ## 2. Babylon.js 实现原则
38
-
39
- 建议实现为一个可复用函数:
40
-
41
- ```js
42
- const fx = createTreasureAttentionFx(scene, targetMesh, options);
43
- ```
44
-
45
- 其中:
46
-
47
- - `scene`:Babylon.js 场景。
48
- - `targetMesh`:宝藏 mesh、掉落物 mesh 或任意带 `getAbsolutePosition()` 的对象。
49
- - `options`:半径、粒子密度、旋转速度、发光强度等参数。
50
-
51
- 核心设计:
52
-
53
- - 不依赖外部图片资源。
54
- - 所有光纹贴图用 `BABYLON.DynamicTexture` 现场绘制。
55
- - 使用 unlit/additive 材质,避免受到场景灯光影响。
56
- - 使用 `ParticleSystem` 做上浮闪片。
57
- - 使用 `GlowLayer` 增强发光,但生产环境建议复用全局 `GlowLayer`。
58
- - 暴露 `setEnabled()` 和 `dispose()`,方便对象池或掉落物生命周期管理。
59
-
60
- ---
61
-
62
- ## 3. 推荐文件结构
63
-
64
- 建议 coding agent 创建:
65
-
66
- ```txt
67
- src/
68
- effects/
69
- createTreasureAttentionFx.js
70
- ```
71
-
72
- 如果项目是 TypeScript,可以改成:
73
-
74
- ```txt
75
- src/
76
- effects/
77
- createTreasureAttentionFx.ts
78
- ```
79
-
80
- 并给 `options` 补类型即可。
81
-
82
- ---
83
-
84
- ## 4. Babylon.js 完整实现代码
85
-
86
- > 下面代码可以直接作为 `createTreasureAttentionFx.js` 使用。
87
- > 默认假设 Babylon.js 已经通过全局变量 `BABYLON` 可用。如果项目使用模块化导入,可以把 `BABYLON` 替换成 import 进来的命名空间。
88
-
89
- ```js
90
- export function createTreasureAttentionFx(scene, target, options = {}) {
91
- const B = BABYLON;
92
-
93
- const o = Object.assign({
94
- radius: 1.15,
95
- yOffset: 0.05,
96
- height: 1.35,
97
- rays: 9,
98
- rayWidth: 0.34,
99
- spinSpeed: 0.85,
100
- particleRate: 42,
101
- glowIntensity: 0.7,
102
- glowLayer: null
103
- }, options);
104
-
105
- const root = new B.TransformNode("treasureAttentionFx", scene);
106
- const raysRoot = new B.TransformNode("treasureAttentionFx_rays", scene);
107
- raysRoot.parent = root;
108
-
109
- const textures = [];
110
- const materials = [];
111
- const meshes = [];
112
-
113
- const paleGold = new B.Color3(1.0, 0.94, 0.35);
114
-
115
- const makeTex = (name, size, draw) => {
116
- const tex = new B.DynamicTexture(name, { width: size, height: size }, scene, false);
117
- tex.hasAlpha = true;
118
- tex.wrapU = tex.wrapV = B.Texture.CLAMP_ADDRESSMODE;
119
-
120
- const ctx = tex.getContext();
121
- ctx.clearRect(0, 0, size, size);
122
- draw(ctx, size);
123
- tex.update(false);
124
-
125
- textures.push(tex);
126
- return tex;
127
- };
128
-
129
- const makeAddMat = (name, tex, alpha = 1) => {
130
- const mat = new B.StandardMaterial(name, scene);
131
- mat.diffuseTexture = tex;
132
- mat.emissiveTexture = tex;
133
- mat.emissiveColor = paleGold;
134
- mat.diffuseColor = B.Color3.Black();
135
- mat.alpha = alpha;
136
- mat.disableLighting = true;
137
- mat.backFaceCulling = false;
138
- mat.useAlphaFromDiffuseTexture = true;
139
- mat.transparencyMode = B.Material.MATERIAL_ALPHABLEND;
140
- mat.alphaMode = B.Engine.ALPHA_ADD;
141
-
142
- // 贴地光效容易和地面深度冲突,关闭 depth write 可以减少闪烁。
143
- // 如果项目有严格的透明物排序策略,可以按项目规则调整。
144
- mat.disableDepthWrite = true;
145
-
146
- materials.push(mat);
147
- return mat;
148
- };
149
-
150
- const auraTex = makeTex("treasureFx_auraTex", 256, (ctx, s) => {
151
- const c = s / 2;
152
- const g = ctx.createRadialGradient(c, c, 0, c, c, c);
153
- g.addColorStop(0.00, "rgba(255,255,190,0.85)");
154
- g.addColorStop(0.25, "rgba(255,235,80,0.45)");
155
- g.addColorStop(0.55, "rgba(255,210,30,0.16)");
156
- g.addColorStop(1.00, "rgba(255,210,30,0.00)");
157
- ctx.fillStyle = g;
158
- ctx.fillRect(0, 0, s, s);
159
- });
160
-
161
- const ringTex = makeTex("treasureFx_ringTex", 256, (ctx, s) => {
162
- const c = s / 2;
163
-
164
- ctx.shadowColor = "rgba(255,245,120,0.9)";
165
- ctx.shadowBlur = s * 0.05;
166
-
167
- ctx.strokeStyle = "rgba(255,242,80,0.78)";
168
- ctx.lineWidth = s * 0.035;
169
- ctx.beginPath();
170
- ctx.arc(c, c, s * 0.31, 0, Math.PI * 2);
171
- ctx.stroke();
172
-
173
- ctx.lineWidth = s * 0.012;
174
- ctx.strokeStyle = "rgba(255,255,220,0.85)";
175
- ctx.beginPath();
176
- ctx.arc(c, c, s * 0.42, 0, Math.PI * 2);
177
- ctx.stroke();
178
- });
179
-
180
- const rayTex = makeTex("treasureFx_rayTex", 256, (ctx, s) => {
181
- const g = ctx.createLinearGradient(0, 0, s, 0);
182
- g.addColorStop(0.00, "rgba(255,255,170,0.00)");
183
- g.addColorStop(0.18, "rgba(255,250,110,0.70)");
184
- g.addColorStop(0.55, "rgba(255,220,45,0.34)");
185
- g.addColorStop(1.00, "rgba(255,210,25,0.00)");
186
-
187
- ctx.fillStyle = g;
188
- ctx.beginPath();
189
- ctx.moveTo(0, s * 0.50);
190
- ctx.quadraticCurveTo(s * 0.45, s * 0.10, s, s * 0.18);
191
- ctx.lineTo(s, s * 0.82);
192
- ctx.quadraticCurveTo(s * 0.45, s * 0.90, 0, s * 0.50);
193
- ctx.closePath();
194
- ctx.fill();
195
- });
196
-
197
- const beamTex = makeTex("treasureFx_beamTex", 256, (ctx, s) => {
198
- const c = s / 2;
199
-
200
- const radial = ctx.createRadialGradient(c, c, 0, c, c, c * 0.55);
201
- radial.addColorStop(0.00, "rgba(255,255,220,0.75)");
202
- radial.addColorStop(0.28, "rgba(255,245,100,0.35)");
203
- radial.addColorStop(1.00, "rgba(255,220,40,0.00)");
204
- ctx.fillStyle = radial;
205
- ctx.fillRect(0, 0, s, s);
206
-
207
- const vertical = ctx.createLinearGradient(0, 0, 0, s);
208
- vertical.addColorStop(0.00, "rgba(255,255,220,0.00)");
209
- vertical.addColorStop(0.42, "rgba(255,245,90,0.42)");
210
- vertical.addColorStop(0.62, "rgba(255,245,90,0.42)");
211
- vertical.addColorStop(1.00, "rgba(255,220,30,0.00)");
212
- ctx.fillStyle = vertical;
213
- ctx.fillRect(s * 0.43, 0, s * 0.14, s);
214
- });
215
-
216
- const sparkleTex = makeTex("treasureFx_sparkleTex", 128, (ctx, s) => {
217
- const c = s / 2;
218
-
219
- ctx.save();
220
- ctx.translate(c, c);
221
- ctx.shadowColor = "rgba(255,245,120,1)";
222
- ctx.shadowBlur = s * 0.12;
223
- ctx.fillStyle = "rgba(255,255,210,0.95)";
224
-
225
- ctx.beginPath();
226
- for (let i = 0; i < 8; i++) {
227
- const a = i * Math.PI / 4;
228
- const r = i % 2 === 0 ? s * 0.42 : s * 0.12;
229
- const x = Math.cos(a) * r;
230
- const y = Math.sin(a) * r;
231
- i ? ctx.lineTo(x, y) : ctx.moveTo(x, y);
232
- }
233
- ctx.closePath();
234
- ctx.fill();
235
-
236
- ctx.restore();
237
- });
238
-
239
- const auraMat = makeAddMat("treasureFx_auraMat", auraTex, 0.85);
240
- const ringMat = makeAddMat("treasureFx_ringMat", ringTex, 0.95);
241
- const rayMat = makeAddMat("treasureFx_rayMat", rayTex, 0.9);
242
- const beamMat = makeAddMat("treasureFx_beamMat", beamTex, 0.72);
243
-
244
- const aura = B.MeshBuilder.CreatePlane("treasureFx_aura", { size: o.radius * 2.55 }, scene);
245
- aura.rotation.x = Math.PI / 2;
246
- aura.position.y = 0.025;
247
- aura.material = auraMat;
248
- aura.parent = root;
249
- aura.renderingGroupId = 2;
250
- meshes.push(aura);
251
-
252
- const ring = B.MeshBuilder.CreatePlane("treasureFx_ring", { size: o.radius * 2.15 }, scene);
253
- ring.rotation.x = Math.PI / 2;
254
- ring.position.y = 0.04;
255
- ring.material = ringMat;
256
- ring.parent = root;
257
- ring.renderingGroupId = 2;
258
- meshes.push(ring);
259
-
260
- const beam = B.MeshBuilder.CreatePlane("treasureFx_beam", {
261
- width: o.radius * 1.05,
262
- height: o.height
263
- }, scene);
264
- beam.position.y = o.height * 0.52;
265
- beam.billboardMode = B.Mesh.BILLBOARDMODE_Y;
266
- beam.material = beamMat;
267
- beam.parent = root;
268
- beam.renderingGroupId = 2;
269
- meshes.push(beam);
270
-
271
- const createRayMesh = (name, length, width) => {
272
- const mesh = new B.Mesh(name, scene);
273
-
274
- const positions = [
275
- 0, 0, -width * 0.50,
276
- length, 0, -width * 0.12,
277
- length, 0, width * 0.12,
278
- 0, 0, width * 0.50
279
- ];
280
-
281
- const indices = [0, 1, 2, 0, 2, 3];
282
- const normals = [];
283
- B.VertexData.ComputeNormals(positions, indices, normals);
284
-
285
- const vd = new B.VertexData();
286
- vd.positions = positions;
287
- vd.indices = indices;
288
- vd.normals = normals;
289
- vd.uvs = [0, 0, 1, 0, 1, 1, 0, 1];
290
- vd.applyToMesh(mesh, true);
291
-
292
- return mesh;
293
- };
294
-
295
- const rays = [];
296
- for (let i = 0; i < o.rays; i++) {
297
- const angle = i / o.rays * Math.PI * 2;
298
- const length = o.radius * (0.95 + (i % 3) * 0.13);
299
- const width = o.rayWidth * (0.8 + (i % 2) * 0.22);
300
-
301
- const ray = createRayMesh(`treasureFx_ray_${i}`, length, width);
302
- ray.parent = raysRoot;
303
- ray.rotation.y = angle;
304
- ray.position.y = 0.055 + i * 0.001;
305
- ray.material = rayMat;
306
- ray.renderingGroupId = 2;
307
-
308
- rays.push(ray);
309
- meshes.push(ray);
310
- }
311
-
312
- const emitter = new B.Mesh("treasureFx_particleEmitter", scene);
313
- emitter.isVisible = false;
314
- emitter.parent = root;
315
- meshes.push(emitter);
316
-
317
- const ps = new B.ParticleSystem("treasureFx_sparkles", 160, scene);
318
- ps.particleTexture = sparkleTex;
319
- ps.emitter = emitter;
320
-
321
- ps.minEmitBox = new B.Vector3(-o.radius * 0.45, 0.15, -o.radius * 0.45);
322
- ps.maxEmitBox = new B.Vector3( o.radius * 0.45, 0.55, o.radius * 0.45);
323
-
324
- ps.color1 = new B.Color4(1, 0.92, 0.24, 0.95);
325
- ps.color2 = new B.Color4(1, 1, 0.72, 0.75);
326
- ps.colorDead = new B.Color4(1, 0.75, 0.15, 0);
327
-
328
- ps.minSize = 0.045;
329
- ps.maxSize = 0.13;
330
- ps.minLifeTime = 0.45;
331
- ps.maxLifeTime = 0.95;
332
- ps.emitRate = o.particleRate;
333
- ps.blendMode = B.ParticleSystem.BLENDMODE_ADD;
334
-
335
- ps.gravity = new B.Vector3(0, -0.04, 0);
336
- ps.direction1 = new B.Vector3(-0.12, 0.55, -0.12);
337
- ps.direction2 = new B.Vector3( 0.12, 0.95, 0.12);
338
- ps.minAngularSpeed = -4;
339
- ps.maxAngularSpeed = 4;
340
- ps.minEmitPower = 0.18;
341
- ps.maxEmitPower = 0.42;
342
- ps.updateSpeed = 0.016;
343
-
344
- ps.start();
345
-
346
- const ownsGlow = !o.glowLayer;
347
- const glow = o.glowLayer || new B.GlowLayer("treasureFx_glowLayer", scene, { blurKernelSize: 32 });
348
-
349
- if (ownsGlow) {
350
- glow.intensity = o.glowIntensity;
351
- }
352
-
353
- if (glow.addIncludedOnlyMesh) {
354
- meshes.filter(m => m.material).forEach(m => glow.addIncludedOnlyMesh(m));
355
- }
356
-
357
- const syncRoot = () => {
358
- if (!target) {
359
- root.position.set(0, o.yOffset, 0);
360
- return;
361
- }
362
-
363
- if (typeof target.getAbsolutePosition === "function") {
364
- const p = target.getAbsolutePosition();
365
- root.position.set(p.x, p.y + o.yOffset, p.z);
366
- return;
367
- }
368
-
369
- root.position.set(target.x || 0, (target.y || 0) + o.yOffset, target.z || 0);
370
- };
371
-
372
- let time = 0;
373
-
374
- const observer = scene.onBeforeRenderObservable.add(() => {
375
- const dt = scene.getEngine().getDeltaTime() * 0.001;
376
- time += dt;
377
- syncRoot();
378
-
379
- const pulse = 0.5 + 0.5 * Math.sin(time * 5.2);
380
-
381
- aura.scaling.setAll(0.96 + pulse * 0.11);
382
- aura.visibility = 0.72 + pulse * 0.24;
383
-
384
- ring.rotation.z += dt * 0.65;
385
- ring.scaling.setAll(0.94 + pulse * 0.08);
386
- ring.visibility = 0.55 + pulse * 0.36;
387
-
388
- beam.scaling.x = 0.85 + pulse * 0.28;
389
- beam.visibility = 0.35 + pulse * 0.28;
390
-
391
- raysRoot.rotation.y += dt * o.spinSpeed;
392
-
393
- for (let i = 0; i < rays.length; i++) {
394
- const q = 0.5 + 0.5 * Math.sin(time * 4.4 + i * 0.9);
395
- rays[i].visibility = 0.42 + q * 0.42;
396
- rays[i].scaling.x = 0.92 + q * 0.18;
397
- rays[i].scaling.z = 0.82 + q * 0.35;
398
- }
399
-
400
- // 不建议在复用全局 GlowLayer 时反复改全局 intensity,
401
- // 因为会影响场景里其他发光对象。
402
- if (ownsGlow) {
403
- glow.intensity = o.glowIntensity * (0.75 + pulse * 0.35);
404
- }
405
- });
406
-
407
- syncRoot();
408
-
409
- return {
410
- root,
411
- particleSystem: ps,
412
- meshes,
413
- materials,
414
- textures,
415
-
416
- setEnabled(enabled) {
417
- root.setEnabled(enabled);
418
- if (enabled) {
419
- ps.start();
420
- } else {
421
- ps.stop();
422
- }
423
- },
424
-
425
- setTarget(nextTarget) {
426
- target = nextTarget;
427
- syncRoot();
428
- },
429
-
430
- dispose() {
431
- scene.onBeforeRenderObservable.remove(observer);
432
-
433
- ps.dispose();
434
-
435
- meshes.forEach(m => {
436
- if (m && !m.isDisposed()) {
437
- m.dispose();
438
- }
439
- });
440
-
441
- materials.forEach(m => {
442
- if (m) {
443
- m.dispose();
444
- }
445
- });
446
-
447
- textures.forEach(t => {
448
- if (t) {
449
- t.dispose();
450
- }
451
- });
452
-
453
- root.dispose();
454
-
455
- if (ownsGlow) {
456
- glow.dispose();
457
- }
458
- }
459
- };
460
- }
461
- ```
462
-
463
- ---
464
-
465
- ## 5. 使用示例
466
-
467
- ### 5.1 普通用法
468
-
469
- ```js
470
- import { createTreasureAttentionFx } from "./effects/createTreasureAttentionFx.js";
471
-
472
- const treasure = scene.getMeshByName("TreasureChest");
473
-
474
- const fx = createTreasureAttentionFx(scene, treasure, {
475
- radius: 1.05,
476
- yOffset: 0.04,
477
- height: 1.25,
478
- particleRate: 55,
479
- glowIntensity: 0.8
480
- });
481
- ```
482
-
483
- ### 5.2 复用全局 GlowLayer
484
-
485
- 如果一个场景里有多个宝藏/掉落物,不要每个特效都创建一个 `GlowLayer`。推荐全局复用:
486
-
487
- ```js
488
- const sharedGlow = new BABYLON.GlowLayer("sharedGlow", scene, {
489
- blurKernelSize: 32
490
- });
491
-
492
- sharedGlow.intensity = 0.65;
493
-
494
- const fx1 = createTreasureAttentionFx(scene, treasureA, {
495
- glowLayer: sharedGlow,
496
- radius: 1.1
497
- });
498
-
499
- const fx2 = createTreasureAttentionFx(scene, treasureB, {
500
- glowLayer: sharedGlow,
501
- radius: 0.9,
502
- particleRate: 30
503
- });
504
- ```
505
-
506
- ### 5.3 跟随掉落物对象池
507
-
508
- ```js
509
- const fx = createTreasureAttentionFx(scene, dropMesh, {
510
- radius: 0.9,
511
- particleRate: 35
512
- });
513
-
514
- dropMesh.onDisposeObservable.add(() => {
515
- fx.dispose();
516
- });
517
- ```
518
-
519
- 如果掉落物对象池复用 mesh,可以保留 fx,切换 target:
520
-
521
- ```js
522
- fx.setTarget(newDropMesh);
523
- fx.setEnabled(true);
524
- ```
525
-
526
- ---
527
-
528
- ## 6. 参数说明
529
-
530
- | 参数 | 默认值 | 说明 |
531
- |---|---:|---|
532
- | `radius` | `1.15` | 地面光斑和星芒半径。宝物越大,这个值越大。 |
533
- | `yOffset` | `0.05` | 特效根节点相对目标位置的 Y 偏移。用于避免地面 z-fighting。 |
534
- | `height` | `1.35` | 竖向柔光高度。 |
535
- | `rays` | `9` | 星芒光束数量。建议 7~12。 |
536
- | `rayWidth` | `0.34` | 单片星芒宽度。 |
537
- | `spinSpeed` | `0.85` | 星芒旋转速度,单位大致是 rad/s。 |
538
- | `particleRate` | `42` | 每秒发射的闪片数量。移动端建议 20~35。 |
539
- | `glowIntensity` | `0.7` | 自建 GlowLayer 时使用的强度。复用全局 GlowLayer 时不主动改全局强度。 |
540
- | `glowLayer` | `null` | 可传入项目里的共享 GlowLayer。 |
541
-
542
- ---
543
-
544
- ## 7. 调参建议
545
-
546
- ### 更像“稀有宝藏”
547
-
548
- ```js
549
- createTreasureAttentionFx(scene, treasure, {
550
- radius: 1.35,
551
- height: 1.6,
552
- rays: 11,
553
- particleRate: 70,
554
- spinSpeed: 1.1,
555
- glowIntensity: 0.95
556
- });
557
- ```
558
-
559
- ### 更像“普通可拾取物”
560
-
561
- ```js
562
- createTreasureAttentionFx(scene, drop, {
563
- radius: 0.75,
564
- height: 0.8,
565
- rays: 7,
566
- particleRate: 20,
567
- spinSpeed: 0.55,
568
- glowIntensity: 0.45
569
- });
570
- ```
571
-
572
- ### 更像“任务目标/交互点”
573
-
574
- ```js
575
- createTreasureAttentionFx(scene, interactable, {
576
- radius: 1.0,
577
- height: 1.1,
578
- particleRate: 15,
579
- spinSpeed: 0.35
580
- });
581
- ```
582
-
583
- ---
584
-
585
- ## 8. 需要 coding agent 注意的坑
586
-
587
- ### 8.1 地面 z-fighting
588
-
589
- 贴地 plane 如果和地面完全重合,可能闪烁。
590
-
591
- 解决方式:
592
-
593
- - `aura.position.y = 0.025`
594
- - `ring.position.y = 0.04`
595
- - `ray.position.y = 0.055 + i * 0.001`
596
- - 材质设置 `disableDepthWrite = true`
597
- - 必要时把 `renderingGroupId` 设置到透明特效组。
598
-
599
- ### 8.2 透明排序
600
-
601
- 多个透明 mesh 相互叠加时可能排序异常。
602
-
603
- 建议:
604
-
605
- - 所有光效材质使用 additive blending。
606
- - 特效 mesh 的 `renderingGroupId` 保持一致。
607
- - 复杂场景中可以把地面光、竖向光、粒子拆成不同 rendering group。
608
-
609
- ### 8.3 GlowLayer 复用
610
-
611
- `GlowLayer.intensity` 是全局属性,会影响这个 GlowLayer 下所有发光对象。
612
-
613
- 建议:
614
-
615
- - 场景只有一个宝藏特效时,可以让函数内部创建 GlowLayer。
616
- - 场景中有多个发光对象时,传入共享 GlowLayer。
617
- - 传入共享 GlowLayer 时,不要在每个特效实例里每帧修改 `glow.intensity`。
618
-
619
- ### 8.4 顶视角相机
620
-
621
- 如果相机几乎纯俯视,单个竖向 billboard plane 可能不明显。
622
-
623
- 可选优化:
624
-
625
- - 加两片互相垂直的竖向 beam plane。
626
- - 或者只保留地面光斑、星芒、粒子,不使用竖向 beam。
627
-
628
- ### 8.5 移动端性能
629
-
630
- 如果场景中同时存在很多宝藏特效:
631
-
632
- - 粒子池数量从 `160` 降到 `80`。
633
- - `particleRate` 降到 `15~30`。
634
- - `rays` 降到 `6~8`。
635
- - 复用全局 `GlowLayer`。
636
- - 尽量只给屏幕内或玩家附近的物体开启特效。
637
-
638
- ---
639
-
640
- ## 9. Canvas 2D 原型做法
641
-
642
- Canvas 版不直接用于 Babylon 场景,但很适合快速调视觉节奏。绘制顺序和 Babylon 版一致:
643
-
644
- 1. 画地面椭圆径向渐变。
645
- 2. 画旋转光束。
646
- 3. 画竖向柔光。
647
- 4. 画脉冲光环。
648
- 5. 更新并绘制上浮闪片。
649
-
650
- 核心伪代码:
651
-
652
- ```js
653
- class TreasureAttentionFx2D {
654
- constructor() {
655
- this.particles = [];
656
- this.radius = 90;
657
- this.particleRate = 48;
658
- this.spinSpeed = 0.85;
659
- }
660
-
661
- update(dt, x, y) {
662
- this.emit(dt, x, y);
663
-
664
- for (const p of this.particles) {
665
- p.age += dt;
666
- p.x += p.vx * dt;
667
- p.y += p.vy * dt;
668
- p.rot += p.spin * dt;
669
- }
670
-
671
- this.particles = this.particles.filter(p => p.age < p.life);
672
- }
673
-
674
- emit(dt, x, y) {
675
- const count = this.particleRate * dt;
676
- this._carry = (this._carry || 0) + count;
677
-
678
- while (this._carry >= 1) {
679
- this._carry -= 1;
680
-
681
- const a = Math.random() * Math.PI * 2;
682
- const r = Math.random() * this.radius * 0.35;
683
-
684
- this.particles.push({
685
- x: x + Math.cos(a) * r,
686
- y: y + Math.sin(a) * r * 0.35 - 12,
687
- vx: (Math.random() - 0.5) * 18,
688
- vy: -35 - Math.random() * 45,
689
- age: 0,
690
- life: 0.55 + Math.random() * 0.7,
691
- size: 4 + Math.random() * 7,
692
- rot: Math.random() * Math.PI * 2,
693
- spin: -5 + Math.random() * 10
694
- });
695
- }
696
- }
697
-
698
- draw(ctx, x, y, t) {
699
- const pulse = 0.5 + 0.5 * Math.sin(t * 5.2);
700
-
701
- this.drawGroundAura(ctx, x, y, pulse);
702
- this.drawRays(ctx, x, y - 8, t, pulse);
703
- this.drawVerticalGlow(ctx, x, y - 32, pulse);
704
- this.drawPulseRing(ctx, x, y, pulse);
705
- this.drawParticles(ctx);
706
- }
707
-
708
- drawGroundAura(ctx, x, y, pulse) {
709
- const r = this.radius * (0.95 + pulse * 0.12);
710
-
711
- ctx.save();
712
- ctx.translate(x, y);
713
- ctx.scale(1, 0.42);
714
-
715
- const g = ctx.createRadialGradient(0, 0, 0, 0, 0, r);
716
- g.addColorStop(0.00, "rgba(255,255,180,0.78)");
717
- g.addColorStop(0.35, "rgba(255,225,55,0.32)");
718
- g.addColorStop(1.00, "rgba(255,210,20,0)");
719
-
720
- ctx.fillStyle = g;
721
- ctx.beginPath();
722
- ctx.arc(0, 0, r, 0, Math.PI * 2);
723
- ctx.fill();
724
-
725
- ctx.restore();
726
- }
727
-
728
- drawRays(ctx, x, y, t, pulse) {
729
- const rays = 9;
730
- const base = t * this.spinSpeed;
731
-
732
- ctx.save();
733
- ctx.translate(x, y);
734
- ctx.globalCompositeOperation = "lighter";
735
-
736
- for (let i = 0; i < rays; i++) {
737
- const a = base + i / rays * Math.PI * 2;
738
- const len = this.radius * (0.75 + (i % 3) * 0.09);
739
- const width = 10 + (i % 2) * 7;
740
- const alpha = 0.18 + 0.18 * Math.sin(t * 4.4 + i * 0.9) + pulse * 0.08;
741
-
742
- ctx.save();
743
- ctx.rotate(a);
744
- const g = ctx.createLinearGradient(0, 0, len, 0);
745
- g.addColorStop(0.00, `rgba(255,255,180,${alpha})`);
746
- g.addColorStop(0.38, `rgba(255,230,45,${alpha * 0.8})`);
747
- g.addColorStop(1.00, "rgba(255,210,20,0)");
748
-
749
- ctx.fillStyle = g;
750
- ctx.beginPath();
751
- ctx.moveTo(0, -width);
752
- ctx.quadraticCurveTo(len * 0.45, -width * 1.4, len, -width * 0.25);
753
- ctx.lineTo(len, width * 0.25);
754
- ctx.quadraticCurveTo(len * 0.45, width * 1.4, 0, width);
755
- ctx.closePath();
756
- ctx.fill();
757
- ctx.restore();
758
- }
759
-
760
- ctx.restore();
761
- }
762
-
763
- drawVerticalGlow(ctx, x, y, pulse) {
764
- const h = this.radius * 1.25;
765
- const w = this.radius * (0.65 + pulse * 0.16);
766
-
767
- ctx.save();
768
- ctx.translate(x, y - h * 0.35);
769
- ctx.globalCompositeOperation = "lighter";
770
-
771
- const g = ctx.createRadialGradient(0, 0, 0, 0, 0, h);
772
- g.addColorStop(0.00, "rgba(255,255,220,0.28)");
773
- g.addColorStop(0.32, "rgba(255,238,80,0.15)");
774
- g.addColorStop(1.00, "rgba(255,220,20,0)");
775
-
776
- ctx.scale(w / h, 1);
777
- ctx.fillStyle = g;
778
- ctx.beginPath();
779
- ctx.arc(0, 0, h, 0, Math.PI * 2);
780
- ctx.fill();
781
-
782
- ctx.restore();
783
- }
784
-
785
- drawPulseRing(ctx, x, y, pulse) {
786
- ctx.save();
787
- ctx.translate(x, y);
788
- ctx.scale(1, 0.42);
789
- ctx.globalCompositeOperation = "lighter";
790
-
791
- for (let i = 0; i < 2; i++) {
792
- const k = (pulse + i * 0.5) % 1;
793
- const r = this.radius * (0.42 + k * 0.36);
794
- const a = 0.45 * (1 - k);
795
-
796
- ctx.strokeStyle = `rgba(255,245,95,${a})`;
797
- ctx.lineWidth = 3;
798
- ctx.beginPath();
799
- ctx.arc(0, 0, r, 0, Math.PI * 2);
800
- ctx.stroke();
801
- }
802
-
803
- ctx.restore();
804
- }
805
-
806
- drawParticles(ctx) {
807
- ctx.save();
808
- ctx.globalCompositeOperation = "lighter";
809
-
810
- for (const p of this.particles) {
811
- const k = p.age / p.life;
812
- const alpha = Math.sin((1 - k) * Math.PI) * 0.95;
813
-
814
- ctx.save();
815
- ctx.translate(p.x, p.y);
816
- ctx.rotate(p.rot);
817
- ctx.scale(1, 0.72);
818
-
819
- ctx.fillStyle = `rgba(255,255,210,${alpha})`;
820
- ctx.shadowColor = "rgba(255,235,70,0.95)";
821
- ctx.shadowBlur = p.size * 1.4;
822
-
823
- ctx.beginPath();
824
- ctx.moveTo(0, -p.size);
825
- ctx.lineTo(p.size * 0.28, -p.size * 0.28);
826
- ctx.lineTo(p.size, 0);
827
- ctx.lineTo(p.size * 0.28, p.size * 0.28);
828
- ctx.lineTo(0, p.size);
829
- ctx.lineTo(-p.size * 0.28, p.size * 0.28);
830
- ctx.lineTo(-p.size, 0);
831
- ctx.lineTo(-p.size * 0.28, -p.size * 0.28);
832
- ctx.closePath();
833
- ctx.fill();
834
-
835
- ctx.restore();
836
- }
837
-
838
- ctx.restore();
839
- }
840
- }
841
- ```
842
-
843
- Canvas 主循环:
844
-
845
- ```js
846
- const canvas = document.querySelector("canvas");
847
- const ctx = canvas.getContext("2d");
848
- const fx = new TreasureAttentionFx2D();
849
-
850
- let last = performance.now();
851
- let time = 0;
852
-
853
- function tick(now) {
854
- const dt = Math.min(0.033, (now - last) / 1000);
855
- last = now;
856
- time += dt;
857
-
858
- ctx.clearRect(0, 0, canvas.width, canvas.height);
859
-
860
- const x = canvas.width * 0.5;
861
- const y = canvas.height * 0.62;
862
-
863
- fx.update(dt, x, y);
864
- fx.draw(ctx, x, y, time);
865
-
866
- requestAnimationFrame(tick);
867
- }
868
-
869
- requestAnimationFrame(tick);
870
- ```
871
-
872
- ---
873
-
874
- ## 10. 验收标准
875
-
876
- coding agent 实现后,用下面标准检查:
877
-
878
- - 宝藏周围有明显金黄色柔光。
879
- - 地面有慢速旋转的星芒,能吸引注意但不刺眼。
880
- - 光环有脉冲感。
881
- - 有少量小闪片上浮并淡出。
882
- - 特效能跟随目标 mesh 移动。
883
- - `setEnabled(false)` 后粒子停止,root 隐藏。
884
- - `dispose()` 后没有残留 mesh、texture、material、observer、particle system。
885
- - 多个宝藏同时存在时帧率稳定。
886
- - 共享 `GlowLayer` 时不会异常改变其他发光物体强度。
887
- - 从斜俯视/isometric 相机看,效果仍然清楚。
888
-
889
- ---
890
-
891
- ## 11. 建议的实现任务拆分
892
-
893
- 给本地 coding agent 的任务可以拆成:
894
-
895
- 1. 新建 `src/effects/createTreasureAttentionFx.js`。
896
- 2. 粘贴 Babylon.js 实现代码。
897
- 3. 在宝藏/掉落物生成逻辑中调用 `createTreasureAttentionFx(scene, treasureMesh, options)`。
898
- 4. 项目已有全局 `GlowLayer` 时,作为 `options.glowLayer` 传入。
899
- 5. 在宝藏销毁、拾取、隐藏时调用 `fx.dispose()` 或 `fx.setEnabled(false)`。
900
- 6. 根据实际模型尺寸调整 `radius`、`yOffset`、`height`。
901
- 7. 在目标平台上压测多个实例,必要时降低 `particleRate`、`rays` 和粒子池数量。