@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,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
- }