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