@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.
- package/CHANGELOG.md +8 -0
- package/package.json +5 -4
- package/tooling/vfx-cli.mjs +8 -1
- package/docs/AUTHORING_EFFECTS.md +0 -297
- package/docs/INTEGRATION.md +0 -194
- package/docs/VFX_LIBRARY_ARCHITECTURE_chatgpt.md +0 -1580
- package/docs/VFX_LIBRARY_EXECUTION_PLAN.md +0 -229
- package/docs/VFX_LIBRARY_PLAN.md +0 -546
- package/docs/hit-flash-red-method.md +0 -155
- package/docs/treasure_attention_fx_babylonjs_guide.md +0 -901
- package/docs/vfx-ab/RESULTS.md +0 -35
- package/docs/vfx-ab/lumber-order/vehicle-exhaust-smoke/after.png +0 -0
- package/docs/vfx-ab/lumber-order/vehicle-exhaust-smoke/before.png +0 -0
- package/docs/vfx-ab/lumber-truck/vehicle-exhaust-smoke/after.png +0 -0
- package/docs/vfx-ab/lumber-truck/vehicle-exhaust-smoke/before.png +0 -0
- package/docs/vfx-integration_chatgpt.md +0 -462
- package/docs/vfx-placement-design.md +0 -164
- package/docs/weapon-trail-visualization.html +0 -241
- package/packages/EffectPackageService.ts +0 -346
- package/packages/SceneVfxService.ts +0 -96
- package/packages/build.mesh.yellow_build_upgrade_indicator/assets/runtime/simpleMeshEffect.v1.js +0 -333
- package/packages/build.mesh.yellow_build_upgrade_indicator/assets/shaders/mesh-effect-preview.frag.glsl +0 -25
- package/packages/build.mesh.yellow_build_upgrade_indicator/assets/shaders/mesh-effect-preview.vert.glsl +0 -11
- package/packages/build.mesh.yellow_build_upgrade_indicator/assets/textures/effect_jianbian.png +0 -0
- package/packages/build.mesh.yellow_build_upgrade_indicator/effect.json +0 -927
- package/packages/build.mesh.yellow_build_upgrade_indicator/media/contact-sheet.png +0 -0
- package/packages/build.mesh.yellow_build_upgrade_indicator/media/preview.gif +0 -0
- package/packages/build.mesh.yellow_build_upgrade_indicator/media/preview.mp4 +0 -0
- package/packages/build.mesh.yellow_build_upgrade_indicator/media/preview.webm +0 -0
- package/packages/build.mesh.yellow_build_upgrade_indicator/media/thumbnail.png +0 -0
- package/packages/build.mesh.yellow_build_upgrade_indicator/metadata/description.json +0 -197
- package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/03-effect.original.json +0 -895
- package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/03-normalized-vfx-to-babylon-report.json +0 -241
- package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/04-preview-overrides.json +0 -542
- package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/04-preview-tuning-report.json +0 -69
- package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/asset-map.json +0 -125
- package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/lineage.json +0 -44
- package/packages/build.particle.golden_upgrade_starburst/assets/runtime/particleEmitter.v1.js +0 -210
- package/packages/build.particle.golden_upgrade_starburst/assets/textures/effect_guangdian.png +0 -0
- package/packages/build.particle.golden_upgrade_starburst/effect.json +0 -880
- package/packages/build.particle.golden_upgrade_starburst/media/contact-sheet.png +0 -0
- package/packages/build.particle.golden_upgrade_starburst/media/preview.gif +0 -0
- package/packages/build.particle.golden_upgrade_starburst/media/preview.mp4 +0 -0
- package/packages/build.particle.golden_upgrade_starburst/media/preview.webm +0 -0
- package/packages/build.particle.golden_upgrade_starburst/media/thumbnail.png +0 -0
- package/packages/build.particle.golden_upgrade_starburst/metadata/description.json +0 -161
- package/packages/build.particle.golden_upgrade_starburst/provenance/03-effect.original.json +0 -762
- package/packages/build.particle.golden_upgrade_starburst/provenance/03-normalized-vfx-to-babylon-report.json +0 -260
- package/packages/build.particle.golden_upgrade_starburst/provenance/04-preview-overrides.json +0 -436
- package/packages/build.particle.golden_upgrade_starburst/provenance/04-preview-tuning-report.json +0 -40
- package/packages/build.particle.golden_upgrade_starburst/provenance/asset-map.json +0 -115
- package/packages/build.particle.golden_upgrade_starburst/provenance/lineage.json +0 -40
- package/packages/build.particle.orange_upgrade_transform_sparks/assets/runtime/particleEmitter.v1.js +0 -210
- package/packages/build.particle.orange_upgrade_transform_sparks/assets/textures/b5b27ab1-e740-4398-b407-848fc2b2c897.png +0 -0
- package/packages/build.particle.orange_upgrade_transform_sparks/effect.json +0 -887
- package/packages/build.particle.orange_upgrade_transform_sparks/media/preview.gif +0 -0
- package/packages/build.particle.orange_upgrade_transform_sparks/media/preview.mp4 +0 -0
- package/packages/build.particle.orange_upgrade_transform_sparks/media/preview.webm +0 -0
- package/packages/build.particle.orange_upgrade_transform_sparks/media/thumbnail.png +0 -0
- package/packages/build.particle.orange_upgrade_transform_sparks/metadata/description.json +0 -179
- package/packages/build.particle.orange_upgrade_transform_sparks/provenance/03-effect.original.json +0 -859
- package/packages/build.particle.orange_upgrade_transform_sparks/provenance/03-normalized-vfx-to-babylon-report.json +0 -260
- package/packages/build.particle.orange_upgrade_transform_sparks/provenance/04-preview-overrides.json +0 -478
- package/packages/build.particle.orange_upgrade_transform_sparks/provenance/04-preview-tuning-report.json +0 -40
- package/packages/build.particle.orange_upgrade_transform_sparks/provenance/asset-map.json +0 -114
- package/packages/build.particle.orange_upgrade_transform_sparks/provenance/lineage.json +0 -41
- package/packages/build.particle.wood_chip_dust_burst/assets/runtime/particleEmitter.v1.js +0 -210
- package/packages/build.particle.wood_chip_dust_burst/assets/textures/effect_yanwu4.png +0 -0
- package/packages/build.particle.wood_chip_dust_burst/assets/textures/suipian3.png +0 -0
- package/packages/build.particle.wood_chip_dust_burst/effect.json +0 -1400
- package/packages/build.particle.wood_chip_dust_burst/media/contact-sheet.png +0 -0
- package/packages/build.particle.wood_chip_dust_burst/media/preview.gif +0 -0
- package/packages/build.particle.wood_chip_dust_burst/media/preview.mp4 +0 -0
- package/packages/build.particle.wood_chip_dust_burst/media/preview.webm +0 -0
- package/packages/build.particle.wood_chip_dust_burst/media/thumbnail.png +0 -0
- package/packages/build.particle.wood_chip_dust_burst/metadata/description.json +0 -173
- package/packages/build.particle.wood_chip_dust_burst/provenance/03-effect.original.json +0 -1277
- package/packages/build.particle.wood_chip_dust_burst/provenance/03-normalized-vfx-to-babylon-report.json +0 -329
- package/packages/build.particle.wood_chip_dust_burst/provenance/04-preview-overrides.json +0 -749
- package/packages/build.particle.wood_chip_dust_burst/provenance/04-preview-tuning-report.json +0 -60
- package/packages/build.particle.wood_chip_dust_burst/provenance/asset-map.json +0 -146
- package/packages/build.particle.wood_chip_dust_burst/provenance/lineage.json +0 -69
- package/packages/effects/building-upgrade/effect.json +0 -6
- package/packages/effects/building-upgrade/index.ts +0 -387
- package/packages/effects/character-entrance/effect.json +0 -6
- package/packages/effects/character-entrance/index.ts +0 -308
- package/packages/effects/coin-burst/index.ts +0 -48
- package/packages/effects/comet-trail-flame/assets/textures/trail_5x4.webp +0 -0
- package/packages/effects/comet-trail-flame/index.ts +0 -107
- package/packages/effects/conveyor-upgrade/effect.json +0 -6
- package/packages/effects/conveyor-upgrade/index.ts +0 -401
- package/packages/effects/debris-smoke/assets/models/rock_debris.glb +0 -0
- package/packages/effects/debris-smoke/assets/textures/smoke22.webp +0 -0
- package/packages/effects/debris-smoke/index.ts +0 -102
- package/packages/effects/door_open_gold_flakes/assets/runtime/goldFlakes.v1.js +0 -397
- package/packages/effects/door_open_gold_flakes/door_open_gold_flakes.json +0 -498
- package/packages/effects/door_open_gold_flakes/index.ts +0 -11
- package/packages/effects/electric-arc/assets/textures/spark_4x4.webp +0 -0
- package/packages/effects/electric-arc/index.ts +0 -63
- package/packages/effects/energy-shield/effect.json +0 -7
- package/packages/effects/energy-shield/index.ts +0 -432
- package/packages/effects/explosion/assets/textures/baozha29.webp +0 -0
- package/packages/effects/explosion/assets/textures/glow01.webp +0 -0
- package/packages/effects/explosion/assets/textures/lizi01.webp +0 -0
- package/packages/effects/explosion/assets/textures/xulie_lizibaozha02_5x5.webp +0 -0
- package/packages/effects/explosion/index.ts +0 -115
- package/packages/effects/ground-tile-upgrade/effect.json +0 -6
- package/packages/effects/ground-tile-upgrade/index.ts +0 -410
- package/packages/effects/hit-flash/index.ts +0 -77
- package/packages/effects/laser-beam/assets/textures/laser_glow.webp +0 -0
- package/packages/effects/laser-beam/assets/textures/laser_noise.webp +0 -0
- package/packages/effects/laser-beam/index.ts +0 -6
- package/packages/effects/laser-beam-factory.ts +0 -146
- package/packages/effects/lumber-cart-suction/effect.json +0 -6
- package/packages/effects/lumber-cart-suction/index.ts +0 -399
- package/packages/effects/lumber-debris-scatter/assets/models/lumber_prop_model.glb +0 -0
- package/packages/effects/lumber-debris-scatter/assets/models/lumber_prop_model.glb.versions/.active +0 -1
- package/packages/effects/lumber-debris-scatter/assets/models/lumber_prop_model.glb.versions/1781589063890_upload.glb +0 -0
- package/packages/effects/lumber-debris-scatter/assets/runtime/solidParticleScatter.v1.js +0 -236
- package/packages/effects/lumber-debris-scatter/effect.json +0 -228
- package/packages/effects/lumber-debris-scatter/index.ts +0 -780
- package/packages/effects/lumber_vehicle_gear_spin/assets/runtime/gearSpin.v1.js +0 -202
- package/packages/effects/lumber_vehicle_gear_spin/index.ts +0 -11
- package/packages/effects/lumber_vehicle_gear_spin/lumber_vehicle_gear_spin.json +0 -338
- package/packages/effects/lumber_vehicle_upgrade_aura/assets/runtime/upgradeAura.v1.js +0 -353
- package/packages/effects/lumber_vehicle_upgrade_aura/index.ts +0 -13
- package/packages/effects/lumber_vehicle_upgrade_aura/lumber_vehicle_upgrade_aura.json +0 -365
- package/packages/effects/lumber_vehicle_wood_chips/assets/runtime/solidParticleScatter.v1.js +0 -310
- package/packages/effects/lumber_vehicle_wood_chips/index.ts +0 -11
- package/packages/effects/lumber_vehicle_wood_chips/lumber_vehicle_wood_chips.json +0 -335
- package/packages/effects/monster-hit/index.ts +0 -129
- package/packages/effects/player-upgrade/effect.json +0 -6
- package/packages/effects/player-upgrade/index.ts +0 -640
- package/packages/effects/pulse-laser-volley/assets/textures/bolt_guangshu.webp +0 -0
- package/packages/effects/pulse-laser-volley/index.ts +0 -215
- package/packages/effects/resource-collect-sparkles/effect.json +0 -6
- package/packages/effects/resource-collect-sparkles/index.ts +0 -254
- package/packages/effects/run-footstep-dust/assets/runtime/runFootstepDust.v1.js +0 -215
- package/packages/effects/run-footstep-dust/effect.json +0 -183
- package/packages/effects/run-footstep-dust/index.ts +0 -377
- package/packages/effects/space-shared.ts +0 -120
- package/packages/effects/thruster-flame/index.ts +0 -306
- package/packages/effects/treasure-attention/effect.json +0 -7
- package/packages/effects/treasure-attention/index.ts +0 -630
- package/packages/effects/tree-chop-dust/assets/textures/tree_chop_dust_atlas.png +0 -0
- package/packages/effects/tree-chop-dust/effect.json +0 -8
- package/packages/effects/tree-chop-dust/index.ts +0 -202
- package/packages/effects/tree-leaf-chop-burst/assets/runtime/particleEmitter.v1.js +0 -303
- package/packages/effects/tree-leaf-chop-burst/assets/textures/tree_leaf_chop_burst_leaf_atlas.png +0 -0
- package/packages/effects/tree-leaf-chop-burst/effect.json +0 -498
- package/packages/effects/tree-leaf-chop-burst/index.ts +0 -294
- package/packages/effects/tree_chop_dust_burst/assets/runtime/particleEmitter.v1.js +0 -318
- package/packages/effects/tree_chop_dust_burst/assets/textures/tree_chop_dust_burst_dust.png +0 -0
- package/packages/effects/tree_chop_dust_burst/index.ts +0 -13
- package/packages/effects/tree_chop_dust_burst/tree_chop_dust_burst.json +0 -412
- package/packages/effects/ufo-thread-flame/assets/textures/flame_flow.webp +0 -0
- package/packages/effects/ufo-thread-flame/assets/textures/flame_spike.webp +0 -0
- package/packages/effects/ufo-thread-flame/index.ts +0 -153
- package/packages/effects/vehicle-exhaust-smoke/assets/textures/vehicle_exhaust_smoke_atlas.png +0 -0
- package/packages/effects/vehicle-exhaust-smoke/effect.json +0 -8
- package/packages/effects/vehicle-exhaust-smoke/index.ts +0 -298
- package/packages/effects/vehicle-upgrade/effect.json +0 -6
- package/packages/effects/vehicle-upgrade/index.ts +0 -404
- package/packages/effects/weapon-trail/assets/textures/trail_wedge.png +0 -0
- package/packages/effects/weapon-trail/index.ts +0 -317
- package/packages/effects/wood-crusher-sawdust/assets/runtime/particleEmitter.v1.js +0 -312
- package/packages/effects/wood-crusher-sawdust/assets/textures/wood_crusher_sawdust_dust.png +0 -0
- package/packages/effects/wood-crusher-sawdust/assets/textures/wood_crusher_sawdust_fiber.png +0 -0
- package/packages/effects/wood-crusher-sawdust/index.ts +0 -43
- package/packages/effects/wood-crusher-sawdust/wood_crusher_sawdust.json +0 -592
- package/packages/effects/wood-cutter-sawdust/assets/runtime/particleEmitter.v1.js +0 -312
- package/packages/effects/wood-cutter-sawdust/assets/textures/wood_cutter_sawdust_chip.png +0 -0
- package/packages/effects/wood-cutter-sawdust/assets/textures/wood_cutter_sawdust_dust.png +0 -0
- package/packages/effects/wood-cutter-sawdust/index.ts +0 -43
- package/packages/effects/wood-cutter-sawdust/wood_cutter_sawdust.json +0 -592
- package/packages/effects/wood-sawdust-effect-factory.ts +0 -194
- package/packages/effects/wood_crusher_sawdust/assets/runtime/particleEmitter.v1.js +0 -318
- package/packages/effects/wood_crusher_sawdust/assets/textures/wood_crusher_sawdust_dust.png +0 -0
- package/packages/effects/wood_crusher_sawdust/assets/textures/wood_crusher_sawdust_fiber.png +0 -0
- package/packages/effects/wood_crusher_sawdust/index.ts +0 -29
- package/packages/effects/wood_crusher_sawdust/wood_crusher_sawdust.json +0 -592
- package/packages/effects/wood_cutter_sawdust/assets/runtime/particleEmitter.v1.js +0 -318
- package/packages/effects/wood_cutter_sawdust/assets/textures/wood_cutter_sawdust_chip.png +0 -0
- package/packages/effects/wood_cutter_sawdust/assets/textures/wood_cutter_sawdust_dust.png +0 -0
- package/packages/effects/wood_cutter_sawdust/index.ts +0 -29
- package/packages/effects/wood_cutter_sawdust/wood_cutter_sawdust.json +0 -592
- package/packages/effects/wood_peel_material_test/assets/runtime/woodPeelMaterialTest.v1.js +0 -196
- package/packages/effects/wood_peel_material_test/index.ts +0 -11
- package/packages/effects/wood_peel_material_test/wood_peel_material_test.json +0 -136
- package/packages/env.particle.forest_edge_fog/assets/runtime/particleEmitter.v1.js +0 -210
- package/packages/env.particle.forest_edge_fog/assets/textures/f12a23c4-b924-4322-a260-3d982428f1e8.png +0 -0
- package/packages/env.particle.forest_edge_fog/assets/textures/yuan.png +0 -0
- package/packages/env.particle.forest_edge_fog/effect.json +0 -854
- package/packages/env.particle.forest_edge_fog/media/preview.gif +0 -0
- package/packages/env.particle.forest_edge_fog/media/preview.mp4 +0 -0
- package/packages/env.particle.forest_edge_fog/media/preview.webm +0 -0
- package/packages/env.particle.forest_edge_fog/media/thumbnail.png +0 -0
- package/packages/env.particle.forest_edge_fog/metadata/description.json +0 -148
- package/packages/env.particle.forest_edge_fog/provenance/03-effect.original.json +0 -827
- package/packages/env.particle.forest_edge_fog/provenance/03-normalized-vfx-to-babylon-report.json +0 -196
- package/packages/env.particle.forest_edge_fog/provenance/04-preview-overrides.json +0 -432
- package/packages/env.particle.forest_edge_fog/provenance/04-preview-tuning-report.json +0 -28
- package/packages/env.particle.forest_edge_fog/provenance/asset-map.json +0 -124
- package/packages/env.particle.forest_edge_fog/provenance/lineage.json +0 -41
- package/packages/generation.ts +0 -14
- package/packages/index.ts +0 -14
- package/packages/legacy-particle-runtime.ts +0 -113
- package/packages/manifestRuntimePackage.ts +0 -519
- package/packages/registry.ts +0 -85
- package/packages/rendering.ts +0 -13
- package/packages/resource.particle.viscous_oil_suction/assets/runtime/top_disk_suction/particleEmitter.v1.js +0 -424
- package/packages/resource.particle.viscous_oil_suction/assets/textures/Glow ad.png +0 -0
- package/packages/resource.particle.viscous_oil_suction/effect.json +0 -1254
- package/packages/resource.particle.viscous_oil_suction/media/preview.gif +0 -0
- package/packages/resource.particle.viscous_oil_suction/media/preview.mp4 +0 -0
- package/packages/resource.particle.viscous_oil_suction/media/preview.webm +0 -0
- package/packages/resource.particle.viscous_oil_suction/media/thumbnail.png +0 -0
- package/packages/resource.particle.viscous_oil_suction/metadata/description.json +0 -122
- package/packages/resource.particle.viscous_oil_suction/provenance/03-effect.original.json +0 -1051
- package/packages/resource.particle.viscous_oil_suction/provenance/03-normalized-vfx-to-babylon-report.json +0 -200
- package/packages/resource.particle.viscous_oil_suction/provenance/04-preview-overrides.json +0 -581
- package/packages/resource.particle.viscous_oil_suction/provenance/04-preview-tuning-report.json +0 -28
- package/packages/resource.particle.viscous_oil_suction/provenance/asset-map.json +0 -111
- package/packages/resource.particle.viscous_oil_suction/provenance/lineage.json +0 -45
- package/packages/types.ts +0 -195
- package/tooling/check-dist-runtime.mjs +0 -166
- package/tooling/generate-index.mjs +0 -55
- package/tooling/test-guardrails.mjs +0 -157
- package/tooling/validate-boundary.mjs +0 -92
- package/tooling/validate-effects.mjs +0 -304
- 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
|
-
}
|