@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,780 +0,0 @@
1
- import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
2
- import { Color3 } from '@babylonjs/core/Maths/math.color';
3
- import { PBRMaterial } from '@babylonjs/core/Materials/PBR/pbrMaterial';
4
- import type { BaseTexture } from '@babylonjs/core/Materials/Textures/baseTexture';
5
- import type { Material } from '@babylonjs/core/Materials/material';
6
- import { Mesh } from '@babylonjs/core/Meshes/mesh';
7
- import { CreateBox } from '@babylonjs/core/Meshes/Builders/boxBuilder';
8
- import { SceneLoader } from '@babylonjs/core/Loading/sceneLoader';
9
- import { Vector3 } from '@babylonjs/core/Maths/math.vector';
10
- import type { Scene } from '@babylonjs/core/scene';
11
- // 精简 glTF loader:只注册 2.0 核心加载器,不引入扩展(尤其避免 EXT_meshopt_compression
12
- // 把 MeshoptDecoder 拉进包——本特效的 GLB 是 plain、零扩展,核心加载器即可)。
13
- import '@babylonjs/loaders/glTF/2.0/glTFLoader';
14
- import type { VfxEffectPackage, VfxParamValues } from '../../types';
15
- import { VFX_RENDERING_GROUP_ID } from '../../rendering';
16
- import sourceManifest from './effect.json';
17
-
18
- type LumberDebrisScatterMode = 'vertical' | 'radial';
19
-
20
- const USE_SHARED_SCENE_MATERIAL_FOR_DEBRIS = 'useSharedSceneMaterialForDebris';
21
- // 模型自带在包内 assets/models/(库纯净契约:不依赖项目资产注册)
22
-
23
- interface LumberDebrisScatterParams extends VfxParamValues {
24
- lifetime: number;
25
- scale: number;
26
- initialHeight: number;
27
- count: number;
28
- spread: number;
29
- speed: number;
30
- size: number;
31
- sizeX: number;
32
- sizeY: number;
33
- sizeZ: number;
34
- scatterShape: LumberDebrisScatterMode;
35
- outwardAngle: number;
36
- scatterGravity: number;
37
- rotationStrength: number;
38
- }
39
-
40
- interface LumberDebrisPieceState {
41
- active: boolean;
42
- startPos: Vector3;
43
- targetPos: Vector3;
44
- startRotation: Vector3;
45
- rotationDelta: Vector3;
46
- duration: number;
47
- elapsed: number;
48
- gravity: number;
49
- scale: number;
50
- scaleAxes: Vector3;
51
- }
52
-
53
- interface LumberDebrisScatterOptions {
54
- count: number;
55
- capacity: number;
56
- effectScale: number;
57
- resourceScale: number;
58
- resourceScaleAxes: Vector3;
59
- initialHeight: number;
60
- spread: number;
61
- duration: number;
62
- speedScale: number;
63
- intervalMs: number;
64
- emissionDirection: Vector3;
65
- scatterMode: LumberDebrisScatterMode;
66
- scatterGravity: number;
67
- rotationStrength: number;
68
- outwardAngleDegrees: number;
69
- renderingGroupId?: number;
70
- }
71
-
72
- import MODEL_URL from './assets/models/lumber_prop_model.glb?url';
73
- const SOURCE_EMITTER = readSourceEmitter();
74
- const cachedModelSourceByScene = new WeakMap<Scene, Promise<Mesh>>();
75
-
76
- const DEFAULT_PARAMS: LumberDebrisScatterParams = {
77
- lifetime: readNumber(SOURCE_EMITTER.duration, 0.86),
78
- scale: readNumber(SOURCE_EMITTER.effectScale, 1),
79
- initialHeight: 0.42,
80
- count: readNumber(SOURCE_EMITTER.count, 18),
81
- spread: readNumber(SOURCE_EMITTER.spread, 0.78),
82
- speed: readNumber(SOURCE_EMITTER.speedScale, 1),
83
- size: readNumber(SOURCE_EMITTER.resourceScale, 1),
84
- sizeX: 1,
85
- sizeY: 1,
86
- sizeZ: 1,
87
- scatterShape: SOURCE_EMITTER.scatterMode === 'vertical' ? 'vertical' : 'radial',
88
- outwardAngle: readNumber(SOURCE_EMITTER.outwardAngleDegrees, 240),
89
- scatterGravity: readNumber(SOURCE_EMITTER.scatterGravity, 1),
90
- rotationStrength: readNumber(SOURCE_EMITTER.rotationStrength, 0.85),
91
- };
92
-
93
- export const lumberDebrisScatterEffectPackage: VfxEffectPackage<LumberDebrisScatterParams> = {
94
- id: 'lumber-debris-scatter',
95
- nameZh: '木材碎块散落',
96
- manifest: {
97
- id: 'build.particle.lumber_debris_scatter',
98
- nameZh: '木材碎块散落',
99
- assets: ['imported/asset_0ec63a18aa2d4b12a9b4ca5dff6416e7.glb'],
100
- source: 'lumber_debris_scatter.json',
101
- },
102
- defaultParams: DEFAULT_PARAMS,
103
- debugAnchor: 'playerRight',
104
- debugParams: [
105
- { key: 'lifetime', label: '生命周期', kind: 'lifetime', min: 0.08, max: 6, step: 0.01 },
106
- { key: 'scale', label: '整体缩放', kind: 'scale', min: 0.01, max: 20, step: 0.01 },
107
- { key: 'initialHeight', label: '初始高度', kind: 'number', min: 0, max: 6, step: 0.01 },
108
- { key: 'count', label: '碎块数量', kind: 'number', min: 1, max: 90, step: 1 },
109
- { key: 'spread', label: '扩散幅度', kind: 'number', min: 0, max: 20, step: 0.01 },
110
- { key: 'speed', label: '播放速度', kind: 'number', min: 0.1, max: 20, step: 0.01 },
111
- { key: 'size', label: '碎块大小', kind: 'number', min: 0.01, max: 20, step: 0.01 },
112
- { key: 'sizeX', label: '木头X缩放', kind: 'number', min: 0.01, max: 20, step: 0.01 },
113
- { key: 'sizeY', label: '木头Y缩放', kind: 'number', min: 0.01, max: 20, step: 0.01 },
114
- { key: 'sizeZ', label: '木头Z缩放', kind: 'number', min: 0.01, max: 20, step: 0.01 },
115
- {
116
- key: 'scatterShape',
117
- label: '散落形态',
118
- kind: 'enum',
119
- options: [
120
- { value: 'radial', label: '中心外扩' },
121
- { value: 'vertical', label: '垂直下落' },
122
- ],
123
- },
124
- { key: 'outwardAngle', label: '外扩角度', kind: 'number', min: 0, max: 360, step: 0.1 },
125
- { key: 'scatterGravity', label: '下落重力', kind: 'number', min: 0.01, max: 20, step: 0.01 },
126
- { key: 'rotationStrength', label: '旋转强度', kind: 'number', min: 0, max: 20, step: 0.01 },
127
- ],
128
- create({ scene, root, position, renderingGroupId, params }) {
129
- root.position.copyFrom(position);
130
- let disposed = false;
131
- let effect: LumberDebrisScatterEffect | null = null;
132
- const disposeTimer = window.setTimeout(() => disposeAll(), getDisposeDelaySeconds(params) * 1000);
133
-
134
- const ready = emitLumberDebrisScatterFromModel(scene, position, toScatterOptions(params, renderingGroupId))
135
- .then((createdEffect) => {
136
- if (disposed) {
137
- createdEffect.dispose();
138
- return;
139
- }
140
- effect = createdEffect;
141
- })
142
- .catch((error) => {
143
- console.warn('[VFX] Failed to warm lumber debris scatter.', error);
144
- });
145
-
146
- return {
147
- root,
148
- ready,
149
- dispose() {
150
- disposeAll();
151
- },
152
- };
153
-
154
- function disposeAll(): void {
155
- if (disposed) return;
156
- disposed = true;
157
- window.clearTimeout(disposeTimer);
158
- effect?.dispose();
159
- effect = null;
160
- root.dispose();
161
- }
162
- },
163
- };
164
-
165
- class LumberDebrisScatterEffect {
166
- private readonly sourceTemplate: Mesh;
167
- private readonly debrisMeshes: Mesh[] = [];
168
- private readonly material: Material;
169
- private readonly ownsMaterial: boolean;
170
- private readonly ownsSourceTemplate: boolean;
171
- private readonly baseScaling: Vector3;
172
- private readonly states: LumberDebrisPieceState[] = [];
173
- private readonly observer;
174
- private elapsed = 0;
175
- private activeCount = 0;
176
- private disposed = false;
177
- private readonly disposeAfter: number;
178
-
179
- constructor(
180
- private readonly scene: Scene,
181
- origin: Vector3,
182
- options: LumberDebrisScatterOptions,
183
- sourceMesh?: Mesh,
184
- ownsSourceTemplate = true,
185
- ) {
186
- const count = Math.max(0, Math.round(options.count));
187
- const capacity = Math.max(8, Math.round(options.capacity));
188
- const effectScale = Math.max(0.01, options.effectScale);
189
- const resourceScale = Math.max(0.01, options.resourceScale);
190
- const resourceScaleAxes = new Vector3(
191
- Math.max(0.01, options.resourceScaleAxes.x),
192
- Math.max(0.01, options.resourceScaleAxes.y),
193
- Math.max(0.01, options.resourceScaleAxes.z),
194
- );
195
- const duration = Math.max(0.08, options.duration) / Math.max(0.1, options.speedScale);
196
- const interval = Math.max(0, options.intervalMs) / 1000;
197
- const spread = Math.max(0, options.spread) * effectScale;
198
- const initialHeight = Math.max(0, options.initialHeight) * effectScale;
199
- const gravity = Math.max(0.05, options.scatterGravity);
200
- const rotationStrength = Math.max(0, options.rotationStrength);
201
- const outwardAngleRadians = clamp(options.outwardAngleDegrees, 0, 360) * (Math.PI / 180);
202
-
203
- const source = sourceMesh ?? createFallbackSourceMesh(scene, 'lumber_debris_scatter_source');
204
- const sourceMaterial = source.material;
205
- const materialBinding = createDebrisMaterialBinding(scene, sourceMaterial, shouldUseSharedSceneMaterial(source));
206
- this.material = materialBinding.material;
207
- this.ownsMaterial = materialBinding.ownsMaterial;
208
- this.sourceTemplate = source;
209
- this.ownsSourceTemplate = ownsSourceTemplate;
210
- this.baseScaling = source.scaling.clone();
211
-
212
- for (let index = 0; index < capacity; index += 1) {
213
- const mesh = createDebrisPieceMesh(source, this.material, index, options.renderingGroupId);
214
- if (!mesh) continue;
215
- this.debrisMeshes.push(mesh);
216
- this.states.push(createDebrisPieceState(index, count, origin, {
217
- duration,
218
- effectScale,
219
- gravity,
220
- initialHeight,
221
- interval,
222
- mode: options.scatterMode,
223
- resourceScale,
224
- resourceScaleAxes,
225
- rotationStrength,
226
- spread,
227
- outwardAngleRadians,
228
- emissionDirection: options.emissionDirection,
229
- }));
230
- }
231
-
232
- this.activeCount = this.states.filter((state) => state.active).length;
233
- this.disposeAfter = duration + interval * Math.max(0, count - 1) + 0.35;
234
- this.updateDebrisMeshes();
235
- this.observer = scene.onBeforeRenderObservable.add(() => this.update());
236
- }
237
-
238
- dispose(): void {
239
- if (this.disposed) return;
240
- this.disposed = true;
241
- this.scene.onBeforeRenderObservable.remove(this.observer);
242
- for (const mesh of this.debrisMeshes) mesh.dispose();
243
- this.debrisMeshes.length = 0;
244
- if (this.ownsSourceTemplate) this.sourceTemplate.dispose();
245
- if (this.ownsMaterial) this.material.dispose();
246
- }
247
-
248
- private update(): void {
249
- if (this.disposed) return;
250
- const deltaTime = this.scene.getEngine().getDeltaTime() / 1000;
251
- this.elapsed += deltaTime;
252
- for (const state of this.states) {
253
- if (!state.active) continue;
254
- state.elapsed += deltaTime;
255
- if (state.elapsed >= state.duration) {
256
- state.active = false;
257
- this.activeCount = Math.max(0, this.activeCount - 1);
258
- }
259
- }
260
- this.updateDebrisMeshes();
261
- if (this.activeCount <= 0 && this.elapsed >= this.disposeAfter) this.dispose();
262
- }
263
-
264
- private updateDebrisMeshes(): void {
265
- for (let index = 0; index < this.debrisMeshes.length; index += 1) {
266
- const mesh = this.debrisMeshes[index];
267
- const state = this.states[index];
268
- if (!state?.active || state.elapsed < 0) {
269
- mesh.isVisible = false;
270
- mesh.scaling.set(0, 0, 0);
271
- continue;
272
- }
273
-
274
- const t = clamp(state.elapsed / state.duration, 0, 1);
275
- const horizontalT = bounceOut(t);
276
- const gravityT = 1 - Math.pow(1 - t, 1 + state.gravity * 1.15);
277
- const lift = Math.sin(Math.PI * t) * (0.32 / Math.max(0.25, state.gravity));
278
-
279
- mesh.isVisible = true;
280
- mesh.position.set(
281
- lerp(state.startPos.x, state.targetPos.x, horizontalT),
282
- lerp(state.startPos.y, state.targetPos.y, gravityT) + lift,
283
- lerp(state.startPos.z, state.targetPos.z, horizontalT),
284
- );
285
- mesh.rotation.set(
286
- state.startRotation.x + state.rotationDelta.x * t,
287
- state.startRotation.y + state.rotationDelta.y * t,
288
- state.startRotation.z + state.rotationDelta.z * t,
289
- );
290
- mesh.scaling.set(
291
- this.baseScaling.x * state.scale * state.scaleAxes.x,
292
- this.baseScaling.y * state.scale * state.scaleAxes.y,
293
- this.baseScaling.z * state.scale * state.scaleAxes.z,
294
- );
295
- }
296
- }
297
- }
298
-
299
- function createDebrisPieceMesh(source: Mesh, material: Material, index: number, renderingGroupId?: number): Mesh | null {
300
- const mesh = source.clone(`lumber_debris_scatter_piece_${index}`, null, false);
301
- if (!mesh) return null;
302
- mesh.parent = null;
303
- mesh.rotationQuaternion = null;
304
- mesh.material = material;
305
- mesh.isPickable = false;
306
- mesh.alwaysSelectAsActiveMesh = true;
307
- mesh.isVisible = false;
308
- mesh.setEnabled(true);
309
- copyDebrisMeshRenderState(mesh, source, renderingGroupId);
310
- mesh.metadata = {
311
- ...((mesh.metadata as Record<string, unknown> | null) ?? {}),
312
- disablePlanarShadow: true,
313
- disablePostOutlineDepth: true,
314
- };
315
- return mesh;
316
- }
317
-
318
- async function emitLumberDebrisScatterFromModel(
319
- scene: Scene,
320
- origin: Vector3,
321
- options: LumberDebrisScatterOptions,
322
- ): Promise<LumberDebrisScatterEffect> {
323
- try {
324
- const source = await getCachedLumberSourceMesh(scene);
325
- return new LumberDebrisScatterEffect(scene, origin, options, source, false);
326
- } catch (error) {
327
- console.warn('[VFX] Unable to load lumber debris model; trying scene wood source.', error);
328
- const sceneSource = createSceneWoodSourceMesh(scene);
329
- if (sceneSource) return new LumberDebrisScatterEffect(scene, origin, options, sceneSource);
330
-
331
- console.warn('[VFX] Unable to find scene wood source; using box fallback.');
332
- return new LumberDebrisScatterEffect(scene, origin, options);
333
- }
334
- }
335
-
336
- function getCachedLumberSourceMesh(scene: Scene): Promise<Mesh> {
337
- const existing = cachedModelSourceByScene.get(scene);
338
- if (existing) return existing;
339
- const promise = loadLumberSourceMesh(scene, MODEL_URL, 'lumber_debris_scatter_model_source')
340
- .catch((error) => {
341
- cachedModelSourceByScene.delete(scene);
342
- throw error;
343
- });
344
- cachedModelSourceByScene.set(scene, promise);
345
- return promise;
346
- }
347
-
348
- function createSceneWoodSourceMesh(scene: Scene): Mesh | null {
349
- const sourceMesh = findSceneWoodMesh(scene);
350
- if (!sourceMesh) return null;
351
- const clone = sourceMesh.clone('lumber_debris_scatter_scene_source', null, false);
352
- if (!clone) return null;
353
- clone.parent = null;
354
- clone.position.set(0, 0, 0);
355
- clone.rotation.set(0, 0, 0);
356
- clone.rotationQuaternion = null;
357
- clone.material = sourceMesh.material;
358
- clone.metadata = {
359
- ...((clone.metadata as Record<string, unknown> | null) ?? {}),
360
- [USE_SHARED_SCENE_MATERIAL_FOR_DEBRIS]: true,
361
- };
362
- normalizeLumberSourceMesh(clone);
363
- clone.isVisible = false;
364
- clone.setEnabled(false);
365
- return clone;
366
- }
367
-
368
- function findSceneWoodMesh(scene: Scene): Mesh | null {
369
- const sceneWoodCandidates = scene.meshes.filter((mesh) => !isVfxGeneratedMesh(mesh));
370
- const namedWoodMesh = findPreferredRenderableMesh(sceneWoodCandidates.filter((mesh) => isSceneSingleWoodMesh(mesh)));
371
- if (namedWoodMesh) return namedWoodMesh;
372
-
373
- // 库纯净契约:不引用项目 gameplay 配置的节点 id;退化为按名称启发式查找(见下方 woodFallback)。
374
- const ids: string[] = [];
375
- for (const id of ids) {
376
- const root = scene.transformNodes.find((node) => isExactAssetNodeMatch(node.id, id) || isExactAssetNodeMatch(node.name, id));
377
- const mesh = root ? findPreferredRenderableMesh(root.getChildMeshes(false).filter((item) => !isVfxGeneratedMesh(item))) : findPreferredRenderableMesh(
378
- sceneWoodCandidates.filter((item) => isExactAssetNodeMatch(item.id, id) || isExactAssetNodeMatch(item.name, id)),
379
- );
380
- if (mesh) return mesh;
381
- }
382
- const woodFallback = findPreferredRenderableMesh(sceneWoodCandidates.filter((mesh) => /wood|lumber|\u6728\u6750|\u6574\u6346/i.test(`${mesh.name} ${mesh.id}`)));
383
- if (woodFallback) return woodFallback;
384
- return findPreferredRenderableMesh(sceneWoodCandidates.filter((mesh) => /wood|lumber|木材|整捆/i.test(`${mesh.name} ${mesh.id}`)));
385
- }
386
-
387
- function isExactAssetNodeMatch(value: string, assetId: string): boolean {
388
- return value === assetId || value.startsWith(`${assetId}_`) || value.startsWith(`${assetId}:`);
389
- }
390
-
391
- function findPreferredRenderableMesh(meshes: AbstractMesh[]): Mesh | null {
392
- const renderableMeshes = meshes.filter(isRenderableMesh);
393
- const rankedWoodMeshes = renderableMeshes
394
- .map((mesh) => ({ mesh, score: getSceneWoodMeshScore(mesh) }))
395
- .filter((item) => item.score > 0)
396
- .sort((a, b) => b.score - a.score);
397
-
398
- return rankedWoodMeshes.find(({ mesh }) => Boolean(mesh.material) && mesh.isEnabled() && mesh.isVisible)?.mesh
399
- ?? rankedWoodMeshes.find(({ mesh }) => Boolean(mesh.material))?.mesh
400
- ?? renderableMeshes.find((mesh) => Boolean(mesh.material) && mesh.isEnabled() && mesh.isVisible)
401
- ?? renderableMeshes.find((mesh) => Boolean(mesh.material))
402
- ?? renderableMeshes[0]
403
- ?? null;
404
- }
405
-
406
- function getSceneWoodMeshScore(mesh: AbstractMesh): number {
407
- const meshText = `${mesh.name} ${mesh.id}`;
408
- const material = mesh.material;
409
- const materialText = `${material?.name ?? ''} ${material?.id ?? ''}`;
410
- const textureText = [
411
- readMaterialTexture(material, 'albedoTexture'),
412
- readMaterialTexture(material, 'diffuseTexture'),
413
- ].map(readTextureSearchText).join(' ');
414
- const combinedText = `${meshText} ${materialText} ${textureText}`;
415
-
416
- let score = 0;
417
- if (isSceneSingleWoodMesh(mesh)) score += 10;
418
- if (/单根|單根|single/i.test(combinedText)) score += 8;
419
- if (/木材|wood|lumber/i.test(combinedText)) score += 4;
420
- if (/pipe|stack|整捆|bundle/i.test(materialText)) score -= 6;
421
- if (material) score += 2;
422
- if (mesh.isEnabled() && mesh.isVisible) score += 1;
423
- return score;
424
- }
425
-
426
- function readTextureSearchText(texture: BaseTexture | null): string {
427
- const textureData = texture as (BaseTexture & { url?: string }) | null;
428
- return `${textureData?.name ?? ''} ${textureData?.url ?? ''}`;
429
- }
430
-
431
- function isSceneSingleWoodMesh(mesh: AbstractMesh): boolean {
432
- if (/wood|lumber|\u6728\u6750|\u5355\u6839/i.test(`${mesh.name} ${mesh.id}`)) return true;
433
- return /wood|lumber|木材|單根|单根/i.test(`${mesh.name} ${mesh.id}`);
434
- }
435
-
436
- function isVfxGeneratedMesh(mesh: AbstractMesh): boolean {
437
- let current: { name?: string; id?: string; parent?: unknown } | null = mesh;
438
- while (current) {
439
- const name = `${current.name} ${current.id}`;
440
- if (
441
- name.includes('vfx_package_')
442
- || name.includes('lumber-cart-suction')
443
- || name.includes('lumber_debris_scatter')
444
- ) {
445
- return true;
446
- }
447
- current = current.parent && typeof current.parent === 'object'
448
- ? current.parent as { name?: string; id?: string; parent?: unknown }
449
- : null;
450
- }
451
- return false;
452
- }
453
-
454
- async function loadLumberSourceMesh(scene: Scene, modelUrl: string, name: string): Promise<Mesh> {
455
- // 内联后 modelUrl 可能是 base64 data URI(含 '/',不可按斜杠切分):整串作 fileName + 显式 .glb 插件
456
- const isDataUri = modelUrl.startsWith('data:');
457
- const lastSlash = isDataUri ? -1 : modelUrl.lastIndexOf('/');
458
- const rootUrl = lastSlash >= 0 ? modelUrl.slice(0, lastSlash + 1) : '';
459
- const fileName = lastSlash >= 0 ? modelUrl.slice(lastSlash + 1) : modelUrl;
460
- const result = await SceneLoader.ImportMeshAsync('', rootUrl, fileName, scene, undefined, '.glb');
461
- const meshes = result.meshes.filter(isRenderableMesh);
462
- if (meshes.length === 0) throw new Error(`No mesh was found in ${modelUrl}.`);
463
-
464
- const source = meshes.length === 1
465
- ? meshes[0]
466
- : Mesh.MergeMeshes(meshes, true, true, undefined, false, true);
467
- if (!source) throw new Error(`Unable to merge meshes from ${modelUrl}.`);
468
-
469
- source.name = name;
470
- source.isVisible = false;
471
- source.setEnabled(false);
472
- source.rotationQuaternion = null;
473
- source.metadata = {
474
- ...((source.metadata as Record<string, unknown> | null) ?? {}),
475
- [USE_SHARED_SCENE_MATERIAL_FOR_DEBRIS]: true,
476
- };
477
- normalizeLumberSourceMesh(source);
478
- return source;
479
- }
480
-
481
- function normalizeLumberSourceMesh(source: Mesh): void {
482
- source.refreshBoundingInfo(true);
483
- const size = source.getBoundingInfo().boundingBox.extendSize.scale(2);
484
- const maxDimension = Math.max(size.x, size.y, size.z, 0.0001);
485
- source.scaling.scaleInPlace(0.82 / maxDimension);
486
- source.refreshBoundingInfo(true);
487
- }
488
-
489
- function createFallbackSourceMesh(scene: Scene, name: string): Mesh {
490
- const source = CreateBox(name, {
491
- width: 0.22,
492
- height: 0.16,
493
- depth: 0.82,
494
- }, scene);
495
- source.isVisible = false;
496
- source.setEnabled(false);
497
- return source;
498
- }
499
-
500
- function createFallbackMaterial(scene: Scene): PBRMaterial {
501
- const material = new PBRMaterial('lumber_debris_scatter_material', scene);
502
- material.albedoColor = new Color3(0.52, 0.31, 0.13);
503
- material.emissiveColor = Color3.Black();
504
- material.metallic = 0;
505
- material.roughness = 0.92;
506
- material.environmentIntensity = 1.15;
507
- material.directIntensity = 1;
508
- material.unlit = false;
509
- return material;
510
- }
511
-
512
- interface DebrisMaterialBinding {
513
- material: Material;
514
- ownsMaterial: boolean;
515
- }
516
-
517
- function createDebrisMaterialBinding(
518
- scene: Scene,
519
- sourceMaterial: Material | null,
520
- useSharedSceneMaterial: boolean,
521
- ): DebrisMaterialBinding {
522
- if (useSharedSceneMaterial && sourceMaterial) {
523
- return { material: sourceMaterial, ownsMaterial: false };
524
- }
525
-
526
- return {
527
- material: createDebrisMaterialFromSceneMaterial(scene, sourceMaterial),
528
- ownsMaterial: true,
529
- };
530
- }
531
-
532
- function shouldUseSharedSceneMaterial(mesh: Mesh): boolean {
533
- return Boolean((mesh.metadata as Record<string, unknown> | null)?.[USE_SHARED_SCENE_MATERIAL_FOR_DEBRIS]);
534
- }
535
-
536
- function createDebrisMaterialFromSceneMaterial(scene: Scene, sourceMaterial: Material | null): Material {
537
- if (sourceMaterial instanceof PBRMaterial) {
538
- const cloned = sourceMaterial.clone(`lumber_debris_scatter_material_from_${sourceMaterial.name || sourceMaterial.id}`);
539
- if (cloned instanceof PBRMaterial) {
540
- configureDebrisPbrMaterial(cloned, sourceMaterial);
541
- return cloned;
542
- }
543
- }
544
-
545
- const material = createFallbackMaterial(scene);
546
- if (sourceMaterial) material.name = `lumber_debris_scatter_material_from_${sourceMaterial.name || sourceMaterial.id}`;
547
- configureDebrisPbrMaterial(material, sourceMaterial);
548
- return material;
549
- }
550
-
551
- function readMaterialColor(material: Material | null, key: string): Color3 | null {
552
- const value = readMaterialProperty(material, key);
553
- if (value instanceof Color3) return value;
554
- if (!value || typeof value !== 'object') return null;
555
- const colorLike = value as Record<string, unknown>;
556
- const r = readFiniteNumber(colorLike.r) ?? readFiniteNumber(colorLike._r);
557
- const g = readFiniteNumber(colorLike.g) ?? readFiniteNumber(colorLike._g);
558
- const b = readFiniteNumber(colorLike.b) ?? readFiniteNumber(colorLike._b);
559
- return r == null || g == null || b == null ? null : new Color3(r, g, b);
560
- }
561
-
562
- function readMaterialTexture(material: Material | null, key: string): BaseTexture | null {
563
- const value = readMaterialProperty(material, key);
564
- return value && typeof value === 'object' ? value as BaseTexture : null;
565
- }
566
-
567
- function readMaterialNumber(material: Material | null, key: string, fallback: number): number {
568
- const value = readMaterialProperty(material, key);
569
- return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
570
- }
571
-
572
- function readMaterialProperty(material: Material | null, key: string): unknown {
573
- return material && key in material ? (material as unknown as Record<string, unknown>)[key] : undefined;
574
- }
575
-
576
- function configureDebrisPbrMaterial(material: PBRMaterial, sourceMaterial: Material | null): void {
577
- const albedoTexture = readMaterialTexture(sourceMaterial, 'albedoTexture')
578
- ?? readMaterialTexture(sourceMaterial, 'diffuseTexture');
579
- if (albedoTexture && 'gammaSpace' in albedoTexture) albedoTexture.gammaSpace = true;
580
- const albedoColor = readMaterialColor(sourceMaterial, 'albedoColor')
581
- ?? readMaterialColor(sourceMaterial, 'diffuseColor');
582
- material.albedoTexture = albedoTexture;
583
- material.albedoColor = albedoTexture
584
- ? (albedoColor && !isNearlyBlack(albedoColor) ? albedoColor.clone() : Color3.White())
585
- : (albedoColor && !isNearlyBlack(albedoColor) ? albedoColor.clone() : new Color3(0.52, 0.31, 0.13));
586
-
587
- material.emissiveTexture = null;
588
- material.emissiveColor = material.albedoColor.clone();
589
-
590
- material.alpha = readMaterialNumber(sourceMaterial, 'alpha', 1);
591
- material.backFaceCulling = sourceMaterial?.backFaceCulling ?? false;
592
- material.metallic = readMaterialNumber(sourceMaterial, 'metallic', 0);
593
- material.roughness = readMaterialNumber(sourceMaterial, 'roughness', 0.92);
594
- material.environmentIntensity = 0;
595
- material.directIntensity = 0;
596
- disableMaterialVertexColor(material);
597
- material.unlit = true;
598
- }
599
-
600
- function readFiniteNumber(value: unknown): number | null {
601
- return typeof value === 'number' && Number.isFinite(value) ? value : null;
602
- }
603
-
604
- function disableMaterialVertexColor(material: Material): void {
605
- const materialFlags = material as unknown as Record<string, unknown>;
606
- materialFlags.useVertexColor = false;
607
- materialFlags.useVertexColors = false;
608
- materialFlags.useVertexAlpha = false;
609
- materialFlags.useVertexAlphas = false;
610
- }
611
-
612
- function copyDebrisMeshRenderState(mesh: Mesh, source: Mesh, renderingGroupId?: number): void {
613
- mesh.layerMask = source.layerMask;
614
- mesh.renderingGroupId = renderingGroupId ?? Math.max(source.renderingGroupId, VFX_RENDERING_GROUP_ID);
615
- }
616
-
617
- function isNearlyBlack(color: Color3): boolean {
618
- return color.r + color.g + color.b < 0.045;
619
- }
620
-
621
- function createDebrisPieceState(
622
- index: number,
623
- count: number,
624
- origin: Vector3,
625
- options: {
626
- duration: number;
627
- effectScale: number;
628
- gravity: number;
629
- initialHeight: number;
630
- interval: number;
631
- mode: LumberDebrisScatterMode;
632
- resourceScale: number;
633
- resourceScaleAxes: Vector3;
634
- rotationStrength: number;
635
- spread: number;
636
- outwardAngleRadians: number;
637
- emissionDirection: Vector3;
638
- },
639
- ): LumberDebrisPieceState {
640
- const active = index < count;
641
- const angleSeed = sequence01(index, 0);
642
- const radiusSeed = Math.sqrt(sequence01(index, 1));
643
- const sideSeed = sequence01(index, 2) < 0.5 ? -1 : 1;
644
- const normalizedDirection = options.emissionDirection.clone();
645
- normalizedDirection.y = 0;
646
- if (normalizedDirection.lengthSquared() < 0.0001) normalizedDirection.set(1, 0, 0);
647
- normalizedDirection.normalize();
648
-
649
- const baseAngle = Math.atan2(normalizedDirection.z, normalizedDirection.x);
650
- const radialAngle = options.mode === 'radial'
651
- ? baseAngle + (angleSeed - 0.5) * options.outwardAngleRadians
652
- : angleSeed * Math.PI * 2;
653
- const scatterRadius = options.spread * (0.35 + radiusSeed * 0.65);
654
- const localX = Math.cos(radialAngle) * scatterRadius;
655
- const localZ = Math.sin(radialAngle) * scatterRadius;
656
-
657
- const startPos = options.mode === 'radial'
658
- ? new Vector3(
659
- origin.x + normalizedDirection.x * options.spread * 0.08,
660
- origin.y + options.initialHeight,
661
- origin.z + normalizedDirection.z * options.spread * 0.08,
662
- )
663
- : new Vector3(
664
- origin.x + localX * 0.52,
665
- origin.y + options.initialHeight + sequence01(index, 3) * 0.24 * options.effectScale,
666
- origin.z + localZ * 0.52,
667
- );
668
- const targetPos = options.mode === 'radial'
669
- ? new Vector3(origin.x + localX, origin.y, origin.z + localZ)
670
- : new Vector3(origin.x + localX * 0.52, origin.y, origin.z + localZ * 0.52);
671
- const rotationBase = Math.PI * 2 * options.rotationStrength;
672
-
673
- return {
674
- active,
675
- startPos,
676
- targetPos,
677
- startRotation: new Vector3(
678
- (sequence01(index, 4) - 0.5) * rotationBase,
679
- (sequence01(index, 5) - 0.5) * rotationBase,
680
- (sequence01(index, 6) - 0.5) * rotationBase,
681
- ),
682
- rotationDelta: new Vector3(
683
- sideSeed * (2.6 + sequence01(index, 7) * 2.2) * options.rotationStrength,
684
- (sequence01(index, 8) - 0.5) * 7.2 * options.rotationStrength,
685
- sideSeed * (1.7 + sequence01(index, 9) * 2.8) * options.rotationStrength,
686
- ),
687
- duration: options.duration * (0.88 + sequence01(index, 10) * 0.18),
688
- elapsed: -index * options.interval,
689
- gravity: options.gravity,
690
- scale: options.resourceScale * options.effectScale * (0.68 + sequence01(index, 11) * 0.24),
691
- scaleAxes: options.resourceScaleAxes.clone(),
692
- };
693
- }
694
-
695
- function toScatterOptions(params: LumberDebrisScatterParams, renderingGroupId?: number): LumberDebrisScatterOptions {
696
- return {
697
- count: params.count,
698
- capacity: Math.max(readNumber(SOURCE_EMITTER.capacity, params.count), params.count),
699
- effectScale: params.scale,
700
- resourceScale: params.size,
701
- resourceScaleAxes: new Vector3(
702
- readNumber(params.sizeX, 1),
703
- readNumber(params.sizeY, 1),
704
- readNumber(params.sizeZ, 1),
705
- ),
706
- initialHeight: params.initialHeight,
707
- spread: params.spread,
708
- duration: params.lifetime,
709
- speedScale: params.speed,
710
- intervalMs: readNumber(SOURCE_EMITTER.intervalMs, 18),
711
- emissionDirection: new Vector3(1, 0.35, 0),
712
- scatterMode: params.scatterShape === 'vertical' ? 'vertical' : 'radial',
713
- scatterGravity: params.scatterGravity,
714
- rotationStrength: params.rotationStrength,
715
- outwardAngleDegrees: params.outwardAngle,
716
- renderingGroupId,
717
- };
718
- }
719
-
720
- function getDisposeDelaySeconds(params: LumberDebrisScatterParams): number {
721
- const duration = Math.max(0.08, params.lifetime) / Math.max(0.1, params.speed);
722
- const interval = Math.max(0, readNumber(SOURCE_EMITTER.intervalMs, 18)) / 1000;
723
- return duration + interval * Math.max(0, Math.round(params.count) - 1) + 0.55;
724
- }
725
-
726
- function readSourceEmitter(): Record<string, unknown> {
727
- const manifest = sourceManifest as { emitters?: Array<Record<string, unknown>> };
728
- return manifest.emitters?.[0] ?? {};
729
- }
730
-
731
- function isRenderableMesh(mesh: AbstractMesh): mesh is Mesh {
732
- return mesh instanceof Mesh && mesh.getTotalVertices() > 0 && !isPlanarShadowMesh(mesh);
733
- }
734
-
735
- function isPlanarShadowMesh(mesh: AbstractMesh): boolean {
736
- const metadata = mesh.metadata as { planarShadowInternal?: unknown; disablePlanarShadow?: unknown } | null;
737
- if (metadata?.planarShadowInternal === true) return true;
738
- if (mesh.name.includes('_planarShadow') || mesh.id.includes('_planarShadow')) return true;
739
- const materialName = `${mesh.material?.name ?? ''} ${mesh.material?.id ?? ''}`;
740
- return /fps\.planarShadow/i.test(materialName);
741
- }
742
-
743
- function readNumber(value: unknown, fallback: number): number {
744
- const next = Number(value);
745
- return Number.isFinite(next) ? next : fallback;
746
- }
747
-
748
- function clamp(value: number, min: number, max: number): number {
749
- return Math.min(max, Math.max(min, value));
750
- }
751
-
752
- function fract(value: number): number {
753
- return value - Math.floor(value);
754
- }
755
-
756
- function sequence01(index: number, axis: number): number {
757
- if (index === 0) return 0.5;
758
- const strides = [0.618033988749895, 0.414213562373095, 0.732050807568877, 0.324717957244746, 0.754877666246693, 0.569840290998053];
759
- return fract(0.5 + index * strides[axis % strides.length] + axis * 0.173205080756888);
760
- }
761
-
762
- function bounceOut(t: number): number {
763
- const n1 = 7.5625;
764
- const d1 = 2.75;
765
- if (t < 1 / d1) return n1 * t * t;
766
- if (t < 2 / d1) {
767
- const next = t - 1.5 / d1;
768
- return n1 * next * next + 0.75;
769
- }
770
- if (t < 2.5 / d1) {
771
- const next = t - 2.25 / d1;
772
- return n1 * next * next + 0.9375;
773
- }
774
- const next = t - 2.625 / d1;
775
- return n1 * next * next + 0.984375;
776
- }
777
-
778
- function lerp(start: number, end: number, t: number): number {
779
- return start + (end - start) * t;
780
- }