@thunderclap-engine/engine 0.0.0-stage → 1.1.0
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 +390 -0
- package/LICENSE +21 -0
- package/README.md +333 -2
- package/dist/analytics/analytics.d.ts +80 -0
- package/dist/analytics/analytics.js +129 -0
- package/dist/analytics/index.d.ts +1 -0
- package/dist/analytics/index.js +1 -0
- package/dist/anim/aim.d.ts +43 -0
- package/dist/anim/aim.js +157 -0
- package/dist/anim/clip.d.ts +98 -0
- package/dist/anim/clip.js +282 -0
- package/dist/anim/compress.d.ts +65 -0
- package/dist/anim/compress.js +339 -0
- package/dist/anim/foot-ik.d.ts +74 -0
- package/dist/anim/foot-ik.js +241 -0
- package/dist/anim/graph.d.ts +153 -0
- package/dist/anim/graph.js +532 -0
- package/dist/anim/ik.d.ts +105 -0
- package/dist/anim/ik.js +435 -0
- package/dist/anim/index.d.ts +36 -0
- package/dist/anim/index.js +21 -0
- package/dist/anim/match.d.ts +66 -0
- package/dist/anim/match.js +153 -0
- package/dist/anim/mixer.d.ts +113 -0
- package/dist/anim/mixer.js +314 -0
- package/dist/anim/morph.d.ts +41 -0
- package/dist/anim/morph.js +94 -0
- package/dist/anim/pose.d.ts +74 -0
- package/dist/anim/pose.js +254 -0
- package/dist/anim/quat.d.ts +44 -0
- package/dist/anim/quat.js +187 -0
- package/dist/anim/retarget.d.ts +97 -0
- package/dist/anim/retarget.js +366 -0
- package/dist/anim/root-motion.d.ts +44 -0
- package/dist/anim/root-motion.js +224 -0
- package/dist/anim/skin-pose.d.ts +39 -0
- package/dist/anim/skin-pose.js +53 -0
- package/dist/anim/socket.d.ts +47 -0
- package/dist/anim/socket.js +84 -0
- package/dist/anim/spring-chain.d.ts +73 -0
- package/dist/anim/spring-chain.js +294 -0
- package/dist/assets/atlas.d.ts +90 -0
- package/dist/assets/atlas.js +212 -0
- package/dist/assets/baked.d.ts +33 -0
- package/dist/assets/baked.js +38 -0
- package/dist/assets/bundle.d.ts +88 -0
- package/dist/assets/bundle.js +464 -0
- package/dist/assets/cache.d.ts +56 -0
- package/dist/assets/cache.js +123 -0
- package/dist/assets/gltf-load.d.ts +252 -0
- package/dist/assets/gltf-load.js +661 -0
- package/dist/assets/gltf-skin.d.ts +36 -0
- package/dist/assets/gltf-skin.js +109 -0
- package/dist/assets/gltf.d.ts +706 -0
- package/dist/assets/gltf.js +1376 -0
- package/dist/assets/index.d.ts +20 -0
- package/dist/assets/index.js +10 -0
- package/dist/assets/ktx2.d.ts +71 -0
- package/dist/assets/ktx2.js +213 -0
- package/dist/assets/manifest.d.ts +79 -0
- package/dist/assets/manifest.js +7 -0
- package/dist/assets/meshopt.d.ts +23 -0
- package/dist/assets/meshopt.js +403 -0
- package/dist/audio/audio.d.ts +733 -0
- package/dist/audio/audio.js +2013 -0
- package/dist/audio/dsp.d.ts +61 -0
- package/dist/audio/dsp.js +159 -0
- package/dist/audio/duck.d.ts +44 -0
- package/dist/audio/duck.js +101 -0
- package/dist/audio/envelope.d.ts +53 -0
- package/dist/audio/envelope.js +106 -0
- package/dist/audio/index.d.ts +13 -0
- package/dist/audio/index.js +14 -0
- package/dist/audio/music.d.ts +113 -0
- package/dist/audio/music.js +548 -0
- package/dist/audio/occlusion.d.ts +55 -0
- package/dist/audio/occlusion.js +61 -0
- package/dist/audio/persist.d.ts +23 -0
- package/dist/audio/persist.js +27 -0
- package/dist/audio/recipes.d.ts +55 -0
- package/dist/audio/recipes.js +227 -0
- package/dist/audio/send.d.ts +41 -0
- package/dist/audio/send.js +118 -0
- package/dist/audio/sprites.d.ts +59 -0
- package/dist/audio/sprites.js +43 -0
- package/dist/audio/synth.d.ts +103 -0
- package/dist/audio/synth.js +197 -0
- package/dist/audio/variations.d.ts +25 -0
- package/dist/audio/variations.js +55 -0
- package/dist/audio/zones.d.ts +100 -0
- package/dist/audio/zones.js +167 -0
- package/dist/core/detached-surface.d.ts +44 -0
- package/dist/core/detached-surface.js +104 -0
- package/dist/core/events.d.ts +24 -0
- package/dist/core/events.js +43 -0
- package/dist/core/game.d.ts +409 -0
- package/dist/core/game.js +530 -0
- package/dist/core/hash.d.ts +24 -0
- package/dist/core/hash.js +49 -0
- package/dist/core/index.d.ts +17 -0
- package/dist/core/index.js +9 -0
- package/dist/core/loop.d.ts +113 -0
- package/dist/core/loop.js +185 -0
- package/dist/core/math.d.ts +58 -0
- package/dist/core/math.js +210 -0
- package/dist/core/noise.d.ts +40 -0
- package/dist/core/noise.js +247 -0
- package/dist/core/pool.d.ts +42 -0
- package/dist/core/pool.js +85 -0
- package/dist/core/random.d.ts +61 -0
- package/dist/core/random.js +199 -0
- package/dist/core/scene.d.ts +79 -0
- package/dist/core/scene.js +195 -0
- package/dist/core/timer.d.ts +66 -0
- package/dist/core/timer.js +286 -0
- package/dist/fx/billboard-wgsl.d.ts +29 -0
- package/dist/fx/billboard-wgsl.js +158 -0
- package/dist/fx/billboard.d.ts +249 -0
- package/dist/fx/billboard.js +702 -0
- package/dist/fx/debug-draw.d.ts +127 -0
- package/dist/fx/debug-draw.js +523 -0
- package/dist/fx/debug-wgsl.d.ts +7 -0
- package/dist/fx/debug-wgsl.js +32 -0
- package/dist/fx/decals-wgsl.d.ts +15 -0
- package/dist/fx/decals-wgsl.js +173 -0
- package/dist/fx/decals.d.ts +216 -0
- package/dist/fx/decals.js +607 -0
- package/dist/fx/effect.d.ts +129 -0
- package/dist/fx/effect.js +349 -0
- package/dist/fx/effects/hit-spark.json +48 -0
- package/dist/fx/effects/index.d.ts +8 -0
- package/dist/fx/effects/index.js +9 -0
- package/dist/fx/effects/magic-aura.json +47 -0
- package/dist/fx/effects/smoke-puff.json +34 -0
- package/dist/fx/emit-surface.d.ts +40 -0
- package/dist/fx/emit-surface.js +124 -0
- package/dist/fx/flicker.d.ts +30 -0
- package/dist/fx/flicker.js +42 -0
- package/dist/fx/index.d.ts +31 -0
- package/dist/fx/index.js +20 -0
- package/dist/fx/particles-wgsl.d.ts +57 -0
- package/dist/fx/particles-wgsl.js +828 -0
- package/dist/fx/particles.d.ts +566 -0
- package/dist/fx/particles.js +1533 -0
- package/dist/fx/random.d.ts +15 -0
- package/dist/fx/random.js +20 -0
- package/dist/fx/sky-wgsl.d.ts +23 -0
- package/dist/fx/sky-wgsl.js +127 -0
- package/dist/fx/sky.d.ts +96 -0
- package/dist/fx/sky.js +175 -0
- package/dist/fx/text-atlas.d.ts +143 -0
- package/dist/fx/text-atlas.js +327 -0
- package/dist/fx/trail-wgsl.d.ts +11 -0
- package/dist/fx/trail-wgsl.js +42 -0
- package/dist/fx/trail.d.ts +194 -0
- package/dist/fx/trail.js +555 -0
- package/dist/fx/water-wgsl.d.ts +34 -0
- package/dist/fx/water-wgsl.js +197 -0
- package/dist/fx/water.d.ts +176 -0
- package/dist/fx/water.js +371 -0
- package/dist/geom/collide/bvh.d.ts +151 -0
- package/dist/geom/collide/bvh.js +493 -0
- package/dist/geom/collide/debug.d.ts +14 -0
- package/dist/geom/collide/debug.js +74 -0
- package/dist/geom/collide/grid.d.ts +26 -0
- package/dist/geom/collide/grid.js +91 -0
- package/dist/geom/collide/index.d.ts +8 -0
- package/dist/geom/collide/index.js +8 -0
- package/dist/geom/collide/queries.d.ts +70 -0
- package/dist/geom/collide/queries.js +583 -0
- package/dist/geom/collide/serialize.d.ts +14 -0
- package/dist/geom/collide/serialize.js +145 -0
- package/dist/geom/collide/shapes.d.ts +161 -0
- package/dist/geom/collide/shapes.js +686 -0
- package/dist/geom/collide/slide.d.ts +66 -0
- package/dist/geom/collide/slide.js +375 -0
- package/dist/geom/collide/tile-slide.d.ts +88 -0
- package/dist/geom/collide/tile-slide.js +255 -0
- package/dist/geom/geometry.d.ts +95 -0
- package/dist/geom/geometry.js +322 -0
- package/dist/geom/heightfield.d.ts +72 -0
- package/dist/geom/heightfield.js +295 -0
- package/dist/geom/hull.d.ts +27 -0
- package/dist/geom/hull.js +279 -0
- package/dist/geom/index.d.ts +35 -0
- package/dist/geom/index.js +23 -0
- package/dist/geom/iso.d.ts +74 -0
- package/dist/geom/iso.js +89 -0
- package/dist/geom/lod.d.ts +29 -0
- package/dist/geom/lod.js +27 -0
- package/dist/geom/ops.d.ts +47 -0
- package/dist/geom/ops.js +285 -0
- package/dist/geom/primitives.d.ts +97 -0
- package/dist/geom/primitives.js +359 -0
- package/dist/geom/shapes.d.ts +43 -0
- package/dist/geom/shapes.js +88 -0
- package/dist/geom/simplify.d.ts +38 -0
- package/dist/geom/simplify.js +519 -0
- package/dist/geom/split.d.ts +37 -0
- package/dist/geom/split.js +413 -0
- package/dist/geom/sweep.d.ts +83 -0
- package/dist/geom/sweep.js +534 -0
- package/dist/geom/tangents.d.ts +16 -0
- package/dist/geom/tangents.js +124 -0
- package/dist/geom/triangulate.d.ts +19 -0
- package/dist/geom/triangulate.js +147 -0
- package/dist/host/default.d.ts +10 -0
- package/dist/host/default.js +10 -0
- package/dist/host/index.d.ts +4 -0
- package/dist/host/index.js +3 -0
- package/dist/host/types.d.ts +144 -0
- package/dist/host/types.js +1 -0
- package/dist/host/web-links.d.ts +38 -0
- package/dist/host/web-links.js +110 -0
- package/dist/host/web.d.ts +30 -0
- package/dist/host/web.js +216 -0
- package/dist/host/xr.d.ts +24 -0
- package/dist/host/xr.js +59 -0
- package/dist/i18n/i18n.d.ts +59 -0
- package/dist/i18n/i18n.js +241 -0
- package/dist/i18n/index.d.ts +3 -0
- package/dist/i18n/index.js +3 -0
- package/dist/i18n/message.d.ts +72 -0
- package/dist/i18n/message.js +369 -0
- package/dist/i18n/negotiate.d.ts +17 -0
- package/dist/i18n/negotiate.js +58 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1 -0
- package/dist/input/bindings.d.ts +31 -0
- package/dist/input/bindings.js +141 -0
- package/dist/input/clock.d.ts +44 -0
- package/dist/input/clock.js +49 -0
- package/dist/input/follow.d.ts +133 -0
- package/dist/input/follow.js +218 -0
- package/dist/input/gestures.d.ts +281 -0
- package/dist/input/gestures.js +662 -0
- package/dist/input/glyphs.d.ts +35 -0
- package/dist/input/glyphs.js +216 -0
- package/dist/input/gyro.d.ts +85 -0
- package/dist/input/gyro.js +140 -0
- package/dist/input/index.d.ts +19 -0
- package/dist/input/index.js +16 -0
- package/dist/input/input.d.ts +543 -0
- package/dist/input/input.js +1492 -0
- package/dist/input/look.d.ts +327 -0
- package/dist/input/look.js +681 -0
- package/dist/input/magnify.d.ts +177 -0
- package/dist/input/magnify.js +131 -0
- package/dist/input/pointer-dispatch.d.ts +138 -0
- package/dist/input/pointer-dispatch.js +180 -0
- package/dist/input/pointer-router.d.ts +204 -0
- package/dist/input/pointer-router.js +533 -0
- package/dist/input/pointer-sources.d.ts +164 -0
- package/dist/input/pointer-sources.js +306 -0
- package/dist/input/rays.d.ts +138 -0
- package/dist/input/rays.js +615 -0
- package/dist/input/record.d.ts +209 -0
- package/dist/input/record.js +601 -0
- package/dist/input/touch-zones.d.ts +147 -0
- package/dist/input/touch-zones.js +300 -0
- package/dist/input/types.d.ts +158 -0
- package/dist/input/types.js +1 -0
- package/dist/input/xr-source.d.ts +53 -0
- package/dist/input/xr-source.js +164 -0
- package/dist/nav/astar.d.ts +45 -0
- package/dist/nav/astar.js +182 -0
- package/dist/nav/corridor.d.ts +40 -0
- package/dist/nav/corridor.js +95 -0
- package/dist/nav/crowd.d.ts +94 -0
- package/dist/nav/crowd.js +624 -0
- package/dist/nav/flow.d.ts +61 -0
- package/dist/nav/flow.js +193 -0
- package/dist/nav/grid.d.ts +108 -0
- package/dist/nav/grid.js +210 -0
- package/dist/nav/heap.d.ts +22 -0
- package/dist/nav/heap.js +92 -0
- package/dist/nav/index.d.ts +37 -0
- package/dist/nav/index.js +19 -0
- package/dist/nav/mesh/areas.d.ts +44 -0
- package/dist/nav/mesh/areas.js +98 -0
- package/dist/nav/mesh/bake-options.d.ts +33 -0
- package/dist/nav/mesh/bake-options.js +76 -0
- package/dist/nav/mesh/contour.d.ts +22 -0
- package/dist/nav/mesh/contour.js +323 -0
- package/dist/nav/mesh/flow3.d.ts +35 -0
- package/dist/nav/mesh/flow3.js +229 -0
- package/dist/nav/mesh/links.d.ts +38 -0
- package/dist/nav/mesh/links.js +72 -0
- package/dist/nav/mesh/mesh-path.d.ts +152 -0
- package/dist/nav/mesh/mesh-path.js +643 -0
- package/dist/nav/mesh/mesh-reach.d.ts +78 -0
- package/dist/nav/mesh/mesh-reach.js +450 -0
- package/dist/nav/mesh/navmesh.d.ts +117 -0
- package/dist/nav/mesh/navmesh.js +647 -0
- package/dist/nav/mesh/polymesh.d.ts +24 -0
- package/dist/nav/mesh/polymesh.js +384 -0
- package/dist/nav/mesh/regions.d.ts +12 -0
- package/dist/nav/mesh/regions.js +367 -0
- package/dist/nav/mesh/serialize.d.ts +13 -0
- package/dist/nav/mesh/serialize.js +197 -0
- package/dist/nav/mesh/voxelize.d.ts +111 -0
- package/dist/nav/mesh/voxelize.js +552 -0
- package/dist/nav/obstacles.d.ts +75 -0
- package/dist/nav/obstacles.js +294 -0
- package/dist/nav/path.d.ts +59 -0
- package/dist/nav/path.js +177 -0
- package/dist/nav/query-queue.d.ts +46 -0
- package/dist/nav/query-queue.js +308 -0
- package/dist/nav/reach.d.ts +53 -0
- package/dist/nav/reach.js +148 -0
- package/dist/nav/steer.d.ts +36 -0
- package/dist/nav/steer.js +78 -0
- package/dist/platform/boot.d.ts +28 -0
- package/dist/platform/boot.js +43 -0
- package/dist/platform/device-mode.d.ts +40 -0
- package/dist/platform/device-mode.js +104 -0
- package/dist/platform/device.d.ts +31 -0
- package/dist/platform/device.js +91 -0
- package/dist/platform/environment.d.ts +42 -0
- package/dist/platform/environment.js +82 -0
- package/dist/platform/gpu.d.ts +40 -0
- package/dist/platform/gpu.js +57 -0
- package/dist/platform/index.d.ts +18 -0
- package/dist/platform/index.js +10 -0
- package/dist/platform/letterbox.d.ts +43 -0
- package/dist/platform/letterbox.js +57 -0
- package/dist/platform/links.d.ts +20 -0
- package/dist/platform/links.js +43 -0
- package/dist/platform/screens.d.ts +36 -0
- package/dist/platform/screens.js +138 -0
- package/dist/platform/surface.d.ts +72 -0
- package/dist/platform/surface.js +157 -0
- package/dist/platform/xr-fake.d.ts +168 -0
- package/dist/platform/xr-fake.js +434 -0
- package/dist/platform/xr-surface.d.ts +120 -0
- package/dist/platform/xr-surface.js +214 -0
- package/dist/platform/xr-types.d.ts +262 -0
- package/dist/post/ao.d.ts +12 -0
- package/dist/post/ao.js +136 -0
- package/dist/post/blend.d.ts +27 -0
- package/dist/post/blend.js +129 -0
- package/dist/post/depth-pass.d.ts +43 -0
- package/dist/post/depth-pass.js +118 -0
- package/dist/post/dof.d.ts +1 -0
- package/dist/post/dof.js +66 -0
- package/dist/post/exposure.d.ts +80 -0
- package/dist/post/exposure.js +322 -0
- package/dist/post/index.d.ts +12 -0
- package/dist/post/index.js +11 -0
- package/dist/post/lens-dirt.d.ts +13 -0
- package/dist/post/lens-dirt.js +91 -0
- package/dist/post/lut.d.ts +19 -0
- package/dist/post/lut.js +50 -0
- package/dist/post/motion-blur.d.ts +9 -0
- package/dist/post/motion-blur.js +78 -0
- package/dist/post/post-stack.d.ts +302 -0
- package/dist/post/post-stack.js +1137 -0
- package/dist/post/settings.d.ts +485 -0
- package/dist/post/settings.js +416 -0
- package/dist/post/shaders.d.ts +25 -0
- package/dist/post/shaders.js +410 -0
- package/dist/post/ssr.d.ts +16 -0
- package/dist/post/ssr.js +155 -0
- package/dist/post/taa.d.ts +74 -0
- package/dist/post/taa.js +235 -0
- package/dist/post/white-balance.d.ts +25 -0
- package/dist/post/white-balance.js +98 -0
- package/dist/render/bindings.d.ts +98 -0
- package/dist/render/bindings.js +281 -0
- package/dist/render/blend.d.ts +13 -0
- package/dist/render/blend.js +26 -0
- package/dist/render/camera-2d.d.ts +72 -0
- package/dist/render/camera-2d.js +92 -0
- package/dist/render/camera.d.ts +302 -0
- package/dist/render/camera.js +671 -0
- package/dist/render/color.d.ts +16 -0
- package/dist/render/color.js +41 -0
- package/dist/render/compressed.d.ts +104 -0
- package/dist/render/compressed.js +248 -0
- package/dist/render/diagnostics.d.ts +15 -0
- package/dist/render/diagnostics.js +43 -0
- package/dist/render/dynamic-resolution.d.ts +70 -0
- package/dist/render/dynamic-resolution.js +126 -0
- package/dist/render/fake-gpu.d.ts +204 -0
- package/dist/render/fake-gpu.js +426 -0
- package/dist/render/flipbook.d.ts +56 -0
- package/dist/render/flipbook.js +87 -0
- package/dist/render/frame.d.ts +278 -0
- package/dist/render/frame.js +174 -0
- package/dist/render/fullscreen.d.ts +54 -0
- package/dist/render/fullscreen.js +91 -0
- package/dist/render/gpu-timer.d.ts +73 -0
- package/dist/render/gpu-timer.js +186 -0
- package/dist/render/index.d.ts +53 -0
- package/dist/render/index.js +29 -0
- package/dist/render/instance-list.d.ts +185 -0
- package/dist/render/instance-list.js +547 -0
- package/dist/render/lights.d.ts +181 -0
- package/dist/render/lights.js +233 -0
- package/dist/render/lod.d.ts +54 -0
- package/dist/render/lod.js +52 -0
- package/dist/render/material.d.ts +165 -0
- package/dist/render/material.js +292 -0
- package/dist/render/mesh.d.ts +284 -0
- package/dist/render/mesh.js +472 -0
- package/dist/render/mipmap.d.ts +16 -0
- package/dist/render/mipmap.js +121 -0
- package/dist/render/parallax.d.ts +50 -0
- package/dist/render/parallax.js +47 -0
- package/dist/render/pbr-wgsl.d.ts +31 -0
- package/dist/render/pbr-wgsl.js +119 -0
- package/dist/render/ping-pong.d.ts +42 -0
- package/dist/render/ping-pong.js +74 -0
- package/dist/render/renderer.d.ts +741 -0
- package/dist/render/renderer.js +2486 -0
- package/dist/render/scene-pipelines.d.ts +44 -0
- package/dist/render/scene-pipelines.js +92 -0
- package/dist/render/scene-target.d.ts +170 -0
- package/dist/render/scene-target.js +450 -0
- package/dist/render/shadow.d.ts +74 -0
- package/dist/render/shadow.js +208 -0
- package/dist/render/skin.d.ts +112 -0
- package/dist/render/skin.js +240 -0
- package/dist/render/snapshot.d.ts +36 -0
- package/dist/render/snapshot.js +34 -0
- package/dist/render/standard.d.ts +162 -0
- package/dist/render/standard.js +263 -0
- package/dist/render/texture.d.ts +142 -0
- package/dist/render/texture.js +228 -0
- package/dist/render/tile-layer.d.ts +110 -0
- package/dist/render/tile-layer.js +304 -0
- package/dist/render/transform.d.ts +115 -0
- package/dist/render/transform.js +318 -0
- package/dist/render/wgsl.d.ts +131 -0
- package/dist/render/wgsl.js +528 -0
- package/dist/save/codec.d.ts +49 -0
- package/dist/save/codec.js +169 -0
- package/dist/save/index.d.ts +8 -0
- package/dist/save/index.js +8 -0
- package/dist/save/journal.d.ts +24 -0
- package/dist/save/journal.js +102 -0
- package/dist/save/migrate.d.ts +15 -0
- package/dist/save/migrate.js +37 -0
- package/dist/save/schema.d.ts +72 -0
- package/dist/save/schema.js +154 -0
- package/dist/save/settings.d.ts +28 -0
- package/dist/save/settings.js +59 -0
- package/dist/save/slots.d.ts +56 -0
- package/dist/save/slots.js +200 -0
- package/dist/save/storage.d.ts +35 -0
- package/dist/save/storage.js +62 -0
- package/dist/save/store.d.ts +114 -0
- package/dist/save/store.js +269 -0
- package/dist/testing/fake-host.d.ts +103 -0
- package/dist/testing/fake-host.js +163 -0
- package/dist/testing/fixed-clock.d.ts +25 -0
- package/dist/testing/fixed-clock.js +30 -0
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/test-game.d.ts +76 -0
- package/dist/testing/test-game.js +92 -0
- package/dist/tween/ease.d.ts +77 -0
- package/dist/tween/ease.js +192 -0
- package/dist/tween/index.d.ts +8 -0
- package/dist/tween/index.js +7 -0
- package/dist/tween/timeline.d.ts +78 -0
- package/dist/tween/timeline.js +269 -0
- package/dist/tween/tween.d.ts +168 -0
- package/dist/tween/tween.js +320 -0
- package/dist/ui/anchor.d.ts +206 -0
- package/dist/ui/anchor.js +307 -0
- package/dist/ui/caption.d.ts +122 -0
- package/dist/ui/caption.js +286 -0
- package/dist/ui/cursor.d.ts +105 -0
- package/dist/ui/cursor.js +261 -0
- package/dist/ui/dom.d.ts +30 -0
- package/dist/ui/dom.js +92 -0
- package/dist/ui/floats.d.ts +86 -0
- package/dist/ui/floats.js +260 -0
- package/dist/ui/hud.d.ts +198 -0
- package/dist/ui/hud.js +431 -0
- package/dist/ui/index.d.ts +37 -0
- package/dist/ui/index.js +34 -0
- package/dist/ui/inventory-card.d.ts +52 -0
- package/dist/ui/inventory-card.js +173 -0
- package/dist/ui/inventory-context.d.ts +29 -0
- package/dist/ui/inventory-context.js +125 -0
- package/dist/ui/inventory-drag.d.ts +63 -0
- package/dist/ui/inventory-drag.js +394 -0
- package/dist/ui/inventory-grid.d.ts +66 -0
- package/dist/ui/inventory-grid.js +625 -0
- package/dist/ui/inventory-radial.d.ts +39 -0
- package/dist/ui/inventory-radial.js +168 -0
- package/dist/ui/inventory.d.ts +128 -0
- package/dist/ui/inventory.js +154 -0
- package/dist/ui/layout.d.ts +356 -0
- package/dist/ui/layout.js +856 -0
- package/dist/ui/link.d.ts +64 -0
- package/dist/ui/link.js +155 -0
- package/dist/ui/loading.d.ts +40 -0
- package/dist/ui/loading.js +157 -0
- package/dist/ui/loupe.d.ts +74 -0
- package/dist/ui/loupe.js +164 -0
- package/dist/ui/menu-nav.d.ts +33 -0
- package/dist/ui/menu-nav.js +28 -0
- package/dist/ui/minimap-canvas.d.ts +145 -0
- package/dist/ui/minimap-canvas.js +460 -0
- package/dist/ui/notify.d.ts +40 -0
- package/dist/ui/notify.js +141 -0
- package/dist/ui/orb.d.ts +53 -0
- package/dist/ui/orb.js +304 -0
- package/dist/ui/overlay.d.ts +213 -0
- package/dist/ui/overlay.js +394 -0
- package/dist/ui/padnav.d.ts +53 -0
- package/dist/ui/padnav.js +146 -0
- package/dist/ui/perf.d.ts +130 -0
- package/dist/ui/perf.js +329 -0
- package/dist/ui/rotate-card.d.ts +54 -0
- package/dist/ui/rotate-card.js +97 -0
- package/dist/ui/stack.d.ts +128 -0
- package/dist/ui/stack.js +312 -0
- package/dist/ui/styles.d.ts +13 -0
- package/dist/ui/styles.js +200 -0
- package/dist/ui/text-scale.d.ts +67 -0
- package/dist/ui/text-scale.js +102 -0
- package/dist/ui/theme.d.ts +70 -0
- package/dist/ui/theme.js +98 -0
- package/dist/ui/toast.d.ts +23 -0
- package/dist/ui/toast.js +46 -0
- package/dist/ui/tooltip.d.ts +89 -0
- package/dist/ui/tooltip.js +333 -0
- package/dist/ui/widgets-2.d.ts +134 -0
- package/dist/ui/widgets-2.js +399 -0
- package/dist/ui/widgets-3.d.ts +91 -0
- package/dist/ui/widgets-3.js +212 -0
- package/dist/ui/widgets.d.ts +34 -0
- package/dist/ui/widgets.js +50 -0
- package/dist/ui/world-panel/index.d.ts +5 -0
- package/dist/ui/world-panel/index.js +5 -0
- package/dist/ui/world-panel/layout.d.ts +38 -0
- package/dist/ui/world-panel/layout.js +68 -0
- package/dist/ui/world-panel/paint.d.ts +96 -0
- package/dist/ui/world-panel/paint.js +136 -0
- package/dist/ui/world-panel/panel.d.ts +235 -0
- package/dist/ui/world-panel/panel.js +762 -0
- package/dist/ui/world-panel/wgsl.d.ts +10 -0
- package/dist/ui/world-panel/wgsl.js +52 -0
- package/dist/ui/world-panel/widgets.d.ts +207 -0
- package/dist/ui/world-panel/widgets.js +569 -0
- package/dist/voxel/chunks.d.ts +77 -0
- package/dist/voxel/chunks.js +149 -0
- package/dist/voxel/grid.d.ts +84 -0
- package/dist/voxel/grid.js +223 -0
- package/dist/voxel/index.d.ts +22 -0
- package/dist/voxel/index.js +7 -0
- package/dist/voxel/mesher-worker.d.ts +14 -0
- package/dist/voxel/mesher-worker.js +47 -0
- package/dist/voxel/mesher.d.ts +73 -0
- package/dist/voxel/mesher.js +306 -0
- package/dist/voxel/ray.d.ts +32 -0
- package/dist/voxel/ray.js +73 -0
- package/dist/voxel/wgsl.d.ts +16 -0
- package/dist/voxel/wgsl.js +28 -0
- package/dist/voxel/worker-client.d.ts +79 -0
- package/dist/voxel/worker-client.js +122 -0
- package/package.json +177 -3
|
@@ -0,0 +1,1533 @@
|
|
|
1
|
+
import { auxSuffix, motionVariant, ScenePipelineCache, sceneVariant, velocityLocation, } from '../render/scene-pipelines.js';
|
|
2
|
+
import { createShaderModule } from '../render/wgsl.js';
|
|
3
|
+
import { buildEmitSurface } from './emit-surface.js';
|
|
4
|
+
import { COLOR_CURVE_FLOATS, FX_PARTICLE_FLOATS, FX_REQUEST_FLOATS, FX_SIM_FLOATS, FX_SPAWN_FLOATS, fxRenderWgsl, MAX_FORCES, PARTICLE_COLLIDE_WGSL, PARTICLE_COMPUTE_WGSL, PARTICLE_COUNT_WGSL, PARTICLE_DEPTH_COPY_WGSL, PARTICLE_EXT_COMPUTE_WGSL, PARTICLE_EXT_RENDER_WGSL, PARTICLE_RENDER_WGSL, PARTICLE_WORLD_COMPUTE_WGSL, PARTICLE_WORLD_RENDER_WGSL, SIZE_CURVE_FLOATS, SURFACE_TRI_FLOATS, } from './particles-wgsl.js';
|
|
5
|
+
import { defaultFxRandom, SPAWN_SEED_MAX } from './random.js';
|
|
6
|
+
const PARTICLE_BYTES = 80;
|
|
7
|
+
const SPAWN_FLOATS = 28;
|
|
8
|
+
const SIM_FLOATS = 8;
|
|
9
|
+
const FX_PARTICLE_BYTES = FX_PARTICLE_FLOATS * 4;
|
|
10
|
+
/** Floats of the collision uniform: two mat4, viewport, params. */
|
|
11
|
+
const COLLIDE_FLOATS = 40;
|
|
12
|
+
/** The soft-particle depth copy's format (sampleable as `texture_depth_2d`). */
|
|
13
|
+
const SOFT_DEPTH_FORMAT = 'depth32float';
|
|
14
|
+
/** Most stops a `sizeCurve`/`colorCurve` may have. */
|
|
15
|
+
export const MAX_CURVE_STOPS = 8;
|
|
16
|
+
const POINT = { kind: 'point' };
|
|
17
|
+
/** 2D maps `box` to `rect` and `sphere` to `circle`, so the 2D shader is unchanged. */
|
|
18
|
+
const SHAPE_ID = { point: 0, circle: 1, rect: 2, line: 3, box: 2, sphere: 1, mesh: 6 };
|
|
19
|
+
const WORLD_SHAPE_ID = {
|
|
20
|
+
point: 0,
|
|
21
|
+
circle: 1,
|
|
22
|
+
rect: 2,
|
|
23
|
+
line: 3,
|
|
24
|
+
box: 4,
|
|
25
|
+
sphere: 5,
|
|
26
|
+
mesh: 6,
|
|
27
|
+
};
|
|
28
|
+
const WORLD_GRAVITY = [0, -9.8, 0];
|
|
29
|
+
const GRAVITY_2D = [0, 400];
|
|
30
|
+
const ZERO2 = [0, 0];
|
|
31
|
+
const ZERO3 = [0, 0, 0];
|
|
32
|
+
const SPEED_2D = [80, 300];
|
|
33
|
+
const WORLD_SPEED = [0.5, 2];
|
|
34
|
+
const SIZE_2D = [6, 1];
|
|
35
|
+
const WORLD_SIZE = [0.1, 0.02];
|
|
36
|
+
const LIFE = [0.5, 1.2];
|
|
37
|
+
const FORCE_ID = { attract: 0, vortex: 1, turbulence: 2, drag: 3 };
|
|
38
|
+
const FALLOFF_ID = { constant: 0, linear: 1, 'inverse-square': 2 };
|
|
39
|
+
const ADDITIVE = {
|
|
40
|
+
color: { srcFactor: 'one', dstFactor: 'one', operation: 'add' },
|
|
41
|
+
alpha: { srcFactor: 'zero', dstFactor: 'one', operation: 'add' },
|
|
42
|
+
};
|
|
43
|
+
const PREMULTIPLIED = {
|
|
44
|
+
color: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
|
|
45
|
+
alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* GPU particles: spawn + integrate in compute, rendered as additive (or alpha) quads into both
|
|
49
|
+
* scene targets (colour + emissive → bloom). Draw with `frame.drawable(system)`.
|
|
50
|
+
*
|
|
51
|
+
* In 2D (`space: '2d'`, the default) quads are velocity-stretched in the XY plane. In world
|
|
52
|
+
* space they face the camera, stretch along the velocity's projection on the view plane,
|
|
53
|
+
* test depth, fade by distance and near scene geometry, and may be lit.
|
|
54
|
+
*
|
|
55
|
+
* The extended kernel (world space, or 2D with `extended`) adds sprite sheets, spin, curves,
|
|
56
|
+
* `forces`, depth collision, sub-emission and mesh emission; the plain 2D kernel stays what it
|
|
57
|
+
* was, pixel for pixel.
|
|
58
|
+
*
|
|
59
|
+
* Particles live in a ring buffer: when full, the oldest are overwritten.
|
|
60
|
+
*/
|
|
61
|
+
export class ParticleSystem {
|
|
62
|
+
capacity;
|
|
63
|
+
space;
|
|
64
|
+
/** Whether this system runs the extended kernel. */
|
|
65
|
+
extended;
|
|
66
|
+
/** Multiplier on every emit count (set < 1 for prefers-reduced-motion). */
|
|
67
|
+
spawnScale = 1;
|
|
68
|
+
floorY;
|
|
69
|
+
bounce;
|
|
70
|
+
stretch;
|
|
71
|
+
/** World space only: see `ParticleSystemOptions.lit`. */
|
|
72
|
+
lit;
|
|
73
|
+
/** World space only: see `ParticleSystemOptions.fade` (null = no distance fade). */
|
|
74
|
+
fade;
|
|
75
|
+
/** World space only: see `ParticleSystemOptions.softness`. */
|
|
76
|
+
softness;
|
|
77
|
+
/** World space only: the target soft particles read depth from (null = `renderer.target`). */
|
|
78
|
+
depthTarget;
|
|
79
|
+
/** See `ParticleSystemOptions.readsDepth`. Live. */
|
|
80
|
+
readsDepth;
|
|
81
|
+
/** Live particles at the last readback (`countAlive` only; -1 before the first). */
|
|
82
|
+
alive = -1;
|
|
83
|
+
/** Live particles below `countBelowY` at the last readback (`countAlive` only; -1 before the first). */
|
|
84
|
+
below = -1;
|
|
85
|
+
/** Live: the forces applied each frame (extended kernel; the first `MAX_FORCES` count). */
|
|
86
|
+
forces;
|
|
87
|
+
/** Live: depth collision parameters (world, `readsDepth`; null = off). */
|
|
88
|
+
collide;
|
|
89
|
+
/** Live: the stream `emit` draws spawn seeds from (see `ParticleSystemOptions.random`). */
|
|
90
|
+
random;
|
|
91
|
+
renderer;
|
|
92
|
+
device;
|
|
93
|
+
particleBuffer;
|
|
94
|
+
spawnBuffer;
|
|
95
|
+
simBuffer;
|
|
96
|
+
spawnData;
|
|
97
|
+
simData;
|
|
98
|
+
spawnFloats;
|
|
99
|
+
maxSpawns;
|
|
100
|
+
spawnPipeline;
|
|
101
|
+
updatePipeline;
|
|
102
|
+
/**
|
|
103
|
+
* Render pipelines per depth format (null: none; a depth one tests without writing) × the
|
|
104
|
+
* view target's aux key, made on first use from `renderer.sceneTargets` (ws/render-10).
|
|
105
|
+
*/
|
|
106
|
+
pipelines = new ScenePipelineCache();
|
|
107
|
+
renderDesc;
|
|
108
|
+
blend;
|
|
109
|
+
/** World space: the velocity builds of the render shader by fragment location (ws/render-11). */
|
|
110
|
+
motionModules = [];
|
|
111
|
+
computeGroup;
|
|
112
|
+
renderGroup;
|
|
113
|
+
soft = null;
|
|
114
|
+
counter = null;
|
|
115
|
+
fx = null;
|
|
116
|
+
collider = null;
|
|
117
|
+
/** `debug`: the staging buffer `debugReadback` copies the particles into. */
|
|
118
|
+
debugRead = null;
|
|
119
|
+
/** The copy in flight, shared by `debugReadback` and `readbackFields` calls made meanwhile. */
|
|
120
|
+
debugJob = null;
|
|
121
|
+
/** Whether this frame's `prepare` copied scene depth (world space). */
|
|
122
|
+
softActive = false;
|
|
123
|
+
/** The renderer's `depthCopy` view `hookGroup` was made for (`readsDepth`). */
|
|
124
|
+
hookView = null;
|
|
125
|
+
hookGroup = null;
|
|
126
|
+
/** The group `draw` binds: own copy, the renderer's copy, or the stand-in. */
|
|
127
|
+
depthGroup = null;
|
|
128
|
+
spawnCount = 0;
|
|
129
|
+
maxSpawnInCmd = 0;
|
|
130
|
+
head = 0;
|
|
131
|
+
pendingDt = 0;
|
|
132
|
+
time = 0;
|
|
133
|
+
warnedOverflow = false;
|
|
134
|
+
warnedExtended = false;
|
|
135
|
+
clearPending = false;
|
|
136
|
+
constructor(renderer, opts = {}) {
|
|
137
|
+
const device = renderer.device;
|
|
138
|
+
this.renderer = renderer;
|
|
139
|
+
this.device = device;
|
|
140
|
+
const world = opts.space === 'world';
|
|
141
|
+
const fx = world ||
|
|
142
|
+
!!opts.extended ||
|
|
143
|
+
!!opts.texture ||
|
|
144
|
+
!!opts.collide ||
|
|
145
|
+
!!opts.atlas ||
|
|
146
|
+
(opts.forces?.length ?? 0) > 0;
|
|
147
|
+
this.space = world ? 'world' : '2d';
|
|
148
|
+
this.extended = fx;
|
|
149
|
+
this.capacity = Math.max(64, Math.floor(opts.capacity ?? 65536));
|
|
150
|
+
this.maxSpawns = opts.maxSpawnsPerFrame ?? 256;
|
|
151
|
+
this.floorY = opts.floorY ?? (world ? -1e9 : 1e9);
|
|
152
|
+
this.bounce = opts.bounce ?? 0.35;
|
|
153
|
+
this.stretch = opts.stretch ?? 0.02;
|
|
154
|
+
this.lit = opts.lit ?? false;
|
|
155
|
+
this.fade = opts.fade ? { near: opts.fade.near, far: opts.fade.far } : null;
|
|
156
|
+
this.softness = opts.softness ?? 0.25;
|
|
157
|
+
this.depthTarget = opts.depthTarget ?? null;
|
|
158
|
+
this.readsDepth = opts.readsDepth ?? false;
|
|
159
|
+
this.forces = opts.forces ?? [];
|
|
160
|
+
this.collide = world && opts.collide ? opts.collide : null;
|
|
161
|
+
this.random = opts.random ?? defaultFxRandom('particles');
|
|
162
|
+
this.spawnFloats = fx ? FX_SPAWN_FLOATS : SPAWN_FLOATS;
|
|
163
|
+
this.simData = new Float32Array(fx ? FX_SIM_FLOATS : SIM_FLOATS);
|
|
164
|
+
this.particleBuffer = device.createBuffer({
|
|
165
|
+
label: 'particles',
|
|
166
|
+
size: this.capacity * (fx ? FX_PARTICLE_BYTES : PARTICLE_BYTES),
|
|
167
|
+
// COPY_DST: `clear()` zeroes it with `encoder.clearBuffer`. COPY_SRC only for `debug`.
|
|
168
|
+
usage: GPUBufferUsage.STORAGE |
|
|
169
|
+
GPUBufferUsage.COPY_DST |
|
|
170
|
+
(opts.debug ? GPUBufferUsage.COPY_SRC : 0),
|
|
171
|
+
});
|
|
172
|
+
if (opts.debug) {
|
|
173
|
+
this.debugRead = device.createBuffer({
|
|
174
|
+
label: 'particles-debug-read',
|
|
175
|
+
size: this.particleBuffer.size,
|
|
176
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
this.spawnData = new Float32Array(this.maxSpawns * this.spawnFloats);
|
|
180
|
+
this.spawnBuffer = device.createBuffer({
|
|
181
|
+
label: 'particle-spawns',
|
|
182
|
+
size: this.spawnData.byteLength,
|
|
183
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
184
|
+
});
|
|
185
|
+
this.simBuffer = device.createBuffer({
|
|
186
|
+
label: 'particle-sim',
|
|
187
|
+
size: this.simData.byteLength,
|
|
188
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
189
|
+
});
|
|
190
|
+
const computeEntries = [
|
|
191
|
+
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
|
|
192
|
+
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
|
|
193
|
+
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: {} },
|
|
194
|
+
];
|
|
195
|
+
if (fx) {
|
|
196
|
+
computeEntries.push({ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, { binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, { binding: 5, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } });
|
|
197
|
+
}
|
|
198
|
+
const computeLayout = device.createBindGroupLayout({
|
|
199
|
+
label: 'particles-compute-layout',
|
|
200
|
+
entries: computeEntries,
|
|
201
|
+
});
|
|
202
|
+
const cmod = createShaderModule(device, world ? PARTICLE_WORLD_COMPUTE_WGSL : fx ? PARTICLE_EXT_COMPUTE_WGSL : PARTICLE_COMPUTE_WGSL, world ? 'particles-world-compute' : fx ? 'particles-ext-compute' : 'particles-compute');
|
|
203
|
+
const cpl = device.createPipelineLayout({ bindGroupLayouts: [computeLayout] });
|
|
204
|
+
this.spawnPipeline = device.createComputePipeline({
|
|
205
|
+
label: 'particles-spawn',
|
|
206
|
+
layout: cpl,
|
|
207
|
+
compute: { module: cmod, entryPoint: 'spawn' },
|
|
208
|
+
});
|
|
209
|
+
this.updatePipeline = device.createComputePipeline({
|
|
210
|
+
label: 'particles-update',
|
|
211
|
+
layout: cpl,
|
|
212
|
+
compute: { module: cmod, entryPoint: 'update' },
|
|
213
|
+
});
|
|
214
|
+
if (fx) {
|
|
215
|
+
this.fx = this.makeFx(computeLayout, cmod, cpl, opts);
|
|
216
|
+
this.computeGroup = this.fx.computeGroup;
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
this.computeGroup = device.createBindGroup({
|
|
220
|
+
label: 'particles-compute',
|
|
221
|
+
layout: computeLayout,
|
|
222
|
+
entries: [
|
|
223
|
+
{ binding: 0, resource: { buffer: this.particleBuffer } },
|
|
224
|
+
{ binding: 1, resource: { buffer: this.spawnBuffer } },
|
|
225
|
+
{ binding: 2, resource: { buffer: this.simBuffer } },
|
|
226
|
+
],
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const renderEntries = [
|
|
230
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } },
|
|
231
|
+
{ binding: 1, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: {} },
|
|
232
|
+
];
|
|
233
|
+
if (world) {
|
|
234
|
+
renderEntries.push({
|
|
235
|
+
binding: 2,
|
|
236
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
237
|
+
texture: { sampleType: 'depth' },
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
if (fx) {
|
|
241
|
+
renderEntries.push({ binding: 3, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } }, { binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: {} }, { binding: 5, visibility: GPUShaderStage.FRAGMENT, sampler: {} });
|
|
242
|
+
}
|
|
243
|
+
const renderLayout = device.createBindGroupLayout({
|
|
244
|
+
label: world ? 'particles-world-render-layout' : 'particles-render-layout',
|
|
245
|
+
entries: renderEntries,
|
|
246
|
+
});
|
|
247
|
+
const rmod = createShaderModule(device, world ? PARTICLE_WORLD_RENDER_WGSL : fx ? PARTICLE_EXT_RENDER_WGSL : PARTICLE_RENDER_WGSL, world ? 'particles-world-render' : fx ? 'particles-ext-render' : 'particles-render');
|
|
248
|
+
const blend = opts.blend === 'alpha' ? PREMULTIPLIED : ADDITIVE;
|
|
249
|
+
this.renderDesc = {
|
|
250
|
+
label: world ? 'particles-world' : 'particles-render',
|
|
251
|
+
layout: device.createPipelineLayout({
|
|
252
|
+
bindGroupLayouts: [renderer.globalsLayout, renderLayout],
|
|
253
|
+
}),
|
|
254
|
+
vertex: { module: rmod, entryPoint: 'vs' },
|
|
255
|
+
fragment: {
|
|
256
|
+
module: rmod,
|
|
257
|
+
entryPoint: 'fs',
|
|
258
|
+
targets: renderer.sceneTargets(blend),
|
|
259
|
+
},
|
|
260
|
+
primitive: { topology: 'triangle-list' },
|
|
261
|
+
};
|
|
262
|
+
this.blend = blend;
|
|
263
|
+
this.pipelineFor(null, renderer.target.auxKey);
|
|
264
|
+
if (world)
|
|
265
|
+
this.soft = this.makeSoftDepth(renderLayout);
|
|
266
|
+
this.renderGroup = this.makeRenderGroup(world ? 'particles-world-render' : 'particles-render', renderLayout, this.soft ? this.soft.dummy.createView() : null);
|
|
267
|
+
if (world && opts.collide)
|
|
268
|
+
this.collider = this.makeCollider();
|
|
269
|
+
if (opts.countAlive)
|
|
270
|
+
this.counter = this.makeCounter(fx, opts.countBelowY ?? -Infinity);
|
|
271
|
+
}
|
|
272
|
+
/** A render bind group: the particles and sim, the depth view (world), the fx table/sheet. */
|
|
273
|
+
makeRenderGroup(label, layout, depth) {
|
|
274
|
+
const entries = [
|
|
275
|
+
{ binding: 0, resource: { buffer: this.particleBuffer } },
|
|
276
|
+
{ binding: 1, resource: { buffer: this.simBuffer } },
|
|
277
|
+
];
|
|
278
|
+
if (depth)
|
|
279
|
+
entries.push({ binding: 2, resource: depth });
|
|
280
|
+
const fx = this.fx;
|
|
281
|
+
if (fx) {
|
|
282
|
+
entries.push({ binding: 3, resource: { buffer: fx.curves } }, { binding: 4, resource: fx.sheet }, { binding: 5, resource: fx.sampler });
|
|
283
|
+
}
|
|
284
|
+
return this.device.createBindGroup({ label, layout, entries });
|
|
285
|
+
}
|
|
286
|
+
makeFx(computeLayout, cmod, cpl, opts) {
|
|
287
|
+
const device = this.device;
|
|
288
|
+
const maxSubs = Math.max(1, Math.floor(opts.maxSubEmitters ?? 16));
|
|
289
|
+
const maxCurves = Math.max(1, Math.floor(opts.maxCurves ?? 32));
|
|
290
|
+
const state = device.createBuffer({
|
|
291
|
+
label: 'particles-state',
|
|
292
|
+
size: 16 + this.maxSpawns * FX_REQUEST_FLOATS * 4,
|
|
293
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
294
|
+
});
|
|
295
|
+
const subData = new Float32Array(maxSubs * FX_SPAWN_FLOATS);
|
|
296
|
+
const subs = device.createBuffer({
|
|
297
|
+
label: 'particles-subs',
|
|
298
|
+
size: subData.byteLength,
|
|
299
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
300
|
+
});
|
|
301
|
+
const curveData = new Float32Array(maxCurves * (SIZE_CURVE_FLOATS + COLOR_CURVE_FLOATS));
|
|
302
|
+
const curves = device.createBuffer({
|
|
303
|
+
label: 'particles-curves',
|
|
304
|
+
size: curveData.byteLength,
|
|
305
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
306
|
+
});
|
|
307
|
+
const surfaceData = new Float32Array(SURFACE_TRI_FLOATS);
|
|
308
|
+
const surface = device.createBuffer({
|
|
309
|
+
label: 'particles-surface',
|
|
310
|
+
size: surfaceData.byteLength,
|
|
311
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
312
|
+
});
|
|
313
|
+
let white = null;
|
|
314
|
+
let sheet;
|
|
315
|
+
if (opts.texture) {
|
|
316
|
+
sheet = opts.texture.view;
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
white = device.createTexture({
|
|
320
|
+
label: 'particles-sheet-none',
|
|
321
|
+
size: [1, 1],
|
|
322
|
+
format: 'rgba8unorm',
|
|
323
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
324
|
+
});
|
|
325
|
+
device.queue.writeTexture({ texture: white }, new Uint8Array([255, 255, 255, 255]), { bytesPerRow: 4 }, [1, 1]);
|
|
326
|
+
sheet = white.createView();
|
|
327
|
+
}
|
|
328
|
+
const sampler = device.createSampler({
|
|
329
|
+
label: 'particles-sheet',
|
|
330
|
+
magFilter: 'linear',
|
|
331
|
+
minFilter: 'linear',
|
|
332
|
+
addressModeU: 'clamp-to-edge',
|
|
333
|
+
addressModeV: 'clamp-to-edge',
|
|
334
|
+
});
|
|
335
|
+
const fx = {
|
|
336
|
+
computeLayout,
|
|
337
|
+
computeGroup: null,
|
|
338
|
+
advancePipeline: device.createComputePipeline({
|
|
339
|
+
label: 'particles-advance',
|
|
340
|
+
layout: cpl,
|
|
341
|
+
compute: { module: cmod, entryPoint: 'advance' },
|
|
342
|
+
}),
|
|
343
|
+
subPipeline: device.createComputePipeline({
|
|
344
|
+
label: 'particles-subspawn',
|
|
345
|
+
layout: cpl,
|
|
346
|
+
compute: { module: cmod, entryPoint: 'subspawn' },
|
|
347
|
+
}),
|
|
348
|
+
state,
|
|
349
|
+
subs,
|
|
350
|
+
subData,
|
|
351
|
+
subSlots: new WeakMap(),
|
|
352
|
+
subNext: 0,
|
|
353
|
+
subUsed: 0,
|
|
354
|
+
subDirty: false,
|
|
355
|
+
curves,
|
|
356
|
+
curveData,
|
|
357
|
+
sizeSlots: new WeakMap(),
|
|
358
|
+
colorSlots: new WeakMap(),
|
|
359
|
+
sizeNext: 0,
|
|
360
|
+
colorNext: 0,
|
|
361
|
+
curveDirty: false,
|
|
362
|
+
surface,
|
|
363
|
+
surfaceData,
|
|
364
|
+
surfaceUsed: 0,
|
|
365
|
+
surfaceSlots: new WeakMap(),
|
|
366
|
+
surfaceDirty: false,
|
|
367
|
+
sheet,
|
|
368
|
+
sampler,
|
|
369
|
+
white,
|
|
370
|
+
textured: !!opts.texture,
|
|
371
|
+
columns: Math.max(1, Math.floor(opts.atlas?.columns ?? 1)),
|
|
372
|
+
rows: Math.max(1, Math.floor(opts.atlas?.rows ?? 1)),
|
|
373
|
+
spawnTotal: 0,
|
|
374
|
+
warnedSubs: false,
|
|
375
|
+
warnedCurves: false,
|
|
376
|
+
};
|
|
377
|
+
fx.computeGroup = this.makeComputeGroup(fx);
|
|
378
|
+
const s = this.simData;
|
|
379
|
+
s[17] = maxCurves * SIZE_CURVE_FLOATS;
|
|
380
|
+
s[18] = this.maxSpawns;
|
|
381
|
+
return fx;
|
|
382
|
+
}
|
|
383
|
+
makeComputeGroup(fx) {
|
|
384
|
+
return this.device.createBindGroup({
|
|
385
|
+
label: 'particles-compute',
|
|
386
|
+
layout: fx.computeLayout,
|
|
387
|
+
entries: [
|
|
388
|
+
{ binding: 0, resource: { buffer: this.particleBuffer } },
|
|
389
|
+
{ binding: 1, resource: { buffer: this.spawnBuffer } },
|
|
390
|
+
{ binding: 2, resource: { buffer: this.simBuffer } },
|
|
391
|
+
{ binding: 3, resource: { buffer: fx.state } },
|
|
392
|
+
{ binding: 4, resource: { buffer: fx.subs } },
|
|
393
|
+
{ binding: 5, resource: { buffer: fx.surface } },
|
|
394
|
+
],
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
makeSoftDepth(renderLayout) {
|
|
398
|
+
const device = this.device;
|
|
399
|
+
const copyLayout = device.createBindGroupLayout({
|
|
400
|
+
label: 'particles-depth-copy-layout',
|
|
401
|
+
entries: [
|
|
402
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
|
|
403
|
+
],
|
|
404
|
+
});
|
|
405
|
+
const mod = createShaderModule(device, PARTICLE_DEPTH_COPY_WGSL, 'particles-depth-copy');
|
|
406
|
+
const copyPipeline = device.createRenderPipeline({
|
|
407
|
+
label: 'particles-depth-copy',
|
|
408
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [copyLayout] }),
|
|
409
|
+
vertex: { module: mod, entryPoint: 'vs' },
|
|
410
|
+
fragment: { module: mod, entryPoint: 'fs', targets: [] },
|
|
411
|
+
primitive: { topology: 'triangle-list' },
|
|
412
|
+
depthStencil: {
|
|
413
|
+
format: SOFT_DEPTH_FORMAT,
|
|
414
|
+
depthWriteEnabled: true,
|
|
415
|
+
depthCompare: 'always',
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
const dummy = device.createTexture({
|
|
419
|
+
label: 'particles-depth-none',
|
|
420
|
+
size: [1, 1],
|
|
421
|
+
format: SOFT_DEPTH_FORMAT,
|
|
422
|
+
usage: GPUTextureUsage.TEXTURE_BINDING,
|
|
423
|
+
});
|
|
424
|
+
const att = {
|
|
425
|
+
view: undefined,
|
|
426
|
+
depthClearValue: 1,
|
|
427
|
+
depthLoadOp: 'clear',
|
|
428
|
+
depthStoreOp: 'store',
|
|
429
|
+
};
|
|
430
|
+
return {
|
|
431
|
+
copyLayout,
|
|
432
|
+
copyPipeline,
|
|
433
|
+
renderLayout,
|
|
434
|
+
dummy,
|
|
435
|
+
copy: null,
|
|
436
|
+
copyGroup: null,
|
|
437
|
+
src: null,
|
|
438
|
+
srcGroup: null,
|
|
439
|
+
att,
|
|
440
|
+
passDesc: {
|
|
441
|
+
label: 'particles-depth-copy',
|
|
442
|
+
colorAttachments: [],
|
|
443
|
+
depthStencilAttachment: att,
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
makeCollider() {
|
|
448
|
+
const device = this.device;
|
|
449
|
+
const layout = device.createBindGroupLayout({
|
|
450
|
+
label: 'particles-collide-layout',
|
|
451
|
+
entries: [
|
|
452
|
+
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
|
|
453
|
+
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: {} },
|
|
454
|
+
{ binding: 2, visibility: GPUShaderStage.COMPUTE, texture: { sampleType: 'depth' } },
|
|
455
|
+
],
|
|
456
|
+
});
|
|
457
|
+
const pipeline = device.createComputePipeline({
|
|
458
|
+
label: 'particles-collide',
|
|
459
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
|
|
460
|
+
compute: {
|
|
461
|
+
module: createShaderModule(device, PARTICLE_COLLIDE_WGSL, 'particles-collide'),
|
|
462
|
+
entryPoint: 'collide',
|
|
463
|
+
},
|
|
464
|
+
});
|
|
465
|
+
const data = new Float32Array(COLLIDE_FLOATS);
|
|
466
|
+
const uniform = device.createBuffer({
|
|
467
|
+
label: 'particles-collide-params',
|
|
468
|
+
size: data.byteLength,
|
|
469
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
470
|
+
});
|
|
471
|
+
return { pipeline, layout, uniform, data, group: null, view: null };
|
|
472
|
+
}
|
|
473
|
+
makeCounter(fx, belowY) {
|
|
474
|
+
const device = this.device;
|
|
475
|
+
const count = device.createBuffer({
|
|
476
|
+
label: 'particles-alive',
|
|
477
|
+
size: 8,
|
|
478
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
|
|
479
|
+
});
|
|
480
|
+
const read = device.createBuffer({
|
|
481
|
+
label: 'particles-alive-read',
|
|
482
|
+
size: 8,
|
|
483
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
484
|
+
});
|
|
485
|
+
const params = device.createBuffer({
|
|
486
|
+
label: 'particles-alive-params',
|
|
487
|
+
size: 32,
|
|
488
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
489
|
+
});
|
|
490
|
+
// capacity, vec4s per particle, index of the `life` vec4, index of the position vec4, belowY.
|
|
491
|
+
const p = new ArrayBuffer(32);
|
|
492
|
+
new Uint32Array(p, 0, 4).set([this.capacity, fx ? 10 : 5, fx ? 4 : 3, 0]);
|
|
493
|
+
new Float32Array(p, 16, 1)[0] = belowY;
|
|
494
|
+
device.queue.writeBuffer(params, 0, p);
|
|
495
|
+
const layout = device.createBindGroupLayout({
|
|
496
|
+
label: 'particles-alive-layout',
|
|
497
|
+
entries: [
|
|
498
|
+
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
|
|
499
|
+
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
|
|
500
|
+
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: {} },
|
|
501
|
+
],
|
|
502
|
+
});
|
|
503
|
+
const pipeline = device.createComputePipeline({
|
|
504
|
+
label: 'particles-alive',
|
|
505
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
|
|
506
|
+
compute: {
|
|
507
|
+
module: createShaderModule(device, PARTICLE_COUNT_WGSL, 'particles-alive'),
|
|
508
|
+
entryPoint: 'count',
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
const group = device.createBindGroup({
|
|
512
|
+
label: 'particles-alive',
|
|
513
|
+
layout,
|
|
514
|
+
entries: [
|
|
515
|
+
{ binding: 0, resource: { buffer: this.particleBuffer } },
|
|
516
|
+
{ binding: 1, resource: { buffer: count } },
|
|
517
|
+
{ binding: 2, resource: { buffer: params } },
|
|
518
|
+
],
|
|
519
|
+
});
|
|
520
|
+
return { pipeline, group, count, read, params, state: 'idle' };
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Queue `count` new particles. Cheap: one small record, spawned on the GPU next frame. The
|
|
524
|
+
* record's seed (every per-particle random on the GPU derives from it) is drawn from `random`,
|
|
525
|
+
* default the system's own stream.
|
|
526
|
+
*/
|
|
527
|
+
emit(p, count, random = this.random) {
|
|
528
|
+
const n = Math.min(Math.floor(count * this.spawnScale), this.capacity);
|
|
529
|
+
if (n <= 0)
|
|
530
|
+
return;
|
|
531
|
+
if (this.spawnCount >= this.maxSpawns) {
|
|
532
|
+
if (!this.warnedOverflow) {
|
|
533
|
+
console.warn(`[particles] more than ${this.maxSpawns} emits in one frame; dropping`);
|
|
534
|
+
this.warnedOverflow = true;
|
|
535
|
+
}
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const o = this.spawnCount++ * this.spawnFloats;
|
|
539
|
+
const fx = this.fx;
|
|
540
|
+
if (fx) {
|
|
541
|
+
this.writeFxSpawn(this.spawnData, p, n, fx.spawnTotal, o, 0, random);
|
|
542
|
+
fx.spawnTotal += n;
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
this.writeSpawn(p, n, o, random);
|
|
546
|
+
this.head = (this.head + n) % this.capacity;
|
|
547
|
+
}
|
|
548
|
+
if (n > this.maxSpawnInCmd)
|
|
549
|
+
this.maxSpawnInCmd = n;
|
|
550
|
+
}
|
|
551
|
+
writeSpawn(p, n, at, random) {
|
|
552
|
+
const d = this.spawnData;
|
|
553
|
+
let o = at;
|
|
554
|
+
const shape = p.shape ?? POINT;
|
|
555
|
+
let sa = 0;
|
|
556
|
+
let sb = 0;
|
|
557
|
+
if (shape.kind === 'circle' || shape.kind === 'sphere')
|
|
558
|
+
sa = shape.radius;
|
|
559
|
+
else if (shape.kind === 'rect' || shape.kind === 'box') {
|
|
560
|
+
sa = shape.width;
|
|
561
|
+
sb = shape.height;
|
|
562
|
+
}
|
|
563
|
+
else if (shape.kind === 'line') {
|
|
564
|
+
sa = shape.dx;
|
|
565
|
+
sb = shape.dy;
|
|
566
|
+
}
|
|
567
|
+
if (!this.warnedExtended &&
|
|
568
|
+
(shape.kind === 'mesh' ||
|
|
569
|
+
p.tile !== undefined ||
|
|
570
|
+
p.rotation ||
|
|
571
|
+
p.spin ||
|
|
572
|
+
p.sizeCurve ||
|
|
573
|
+
p.colorCurve ||
|
|
574
|
+
p.onDeath ||
|
|
575
|
+
p.onLife)) {
|
|
576
|
+
console.warn('[particles] sprite, spin, curve, mesh and sub-emission fields need `extended`');
|
|
577
|
+
this.warnedExtended = true;
|
|
578
|
+
}
|
|
579
|
+
const speed = p.speed ?? [80, 300];
|
|
580
|
+
const life = p.life ?? [0.5, 1.2];
|
|
581
|
+
const size = p.size ?? [6, 1];
|
|
582
|
+
const c0 = p.colorStart;
|
|
583
|
+
const c1 = p.colorEnd;
|
|
584
|
+
const g = p.gravity ?? [0, 400];
|
|
585
|
+
// a
|
|
586
|
+
d[o++] = p.x;
|
|
587
|
+
d[o++] = p.y;
|
|
588
|
+
d[o++] = sa;
|
|
589
|
+
d[o++] = sb;
|
|
590
|
+
// b
|
|
591
|
+
d[o++] = p.angle ?? -Math.PI / 2;
|
|
592
|
+
d[o++] = p.spread ?? Math.PI * 2;
|
|
593
|
+
d[o++] = speed[0];
|
|
594
|
+
d[o++] = speed[1];
|
|
595
|
+
// c
|
|
596
|
+
d[o++] = life[0];
|
|
597
|
+
d[o++] = life[1];
|
|
598
|
+
d[o++] = size[0];
|
|
599
|
+
d[o++] = size[1];
|
|
600
|
+
// c0, c1
|
|
601
|
+
o = writeColors(d, o, c0, c1);
|
|
602
|
+
// d
|
|
603
|
+
d[o++] = g[0];
|
|
604
|
+
d[o++] = g[1];
|
|
605
|
+
d[o++] = p.drag ?? 1;
|
|
606
|
+
d[o++] = shape.kind === 'mesh' ? 0 : SHAPE_ID[shape.kind];
|
|
607
|
+
// e
|
|
608
|
+
d[o++] = this.head;
|
|
609
|
+
d[o++] = n;
|
|
610
|
+
d[o++] = random.int(0, SPAWN_SEED_MAX);
|
|
611
|
+
d[o++] = p.emissive ?? 1;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* An extended spawn record at `at` of `d`: `n` particles from `start` (relative to the
|
|
615
|
+
* frame's ring head). `depth` 1 writes a sub-emitter record (no sub-emitters of its own).
|
|
616
|
+
*/
|
|
617
|
+
writeFxSpawn(d, p, n, start, at, depth, random) {
|
|
618
|
+
const world = this.space === 'world';
|
|
619
|
+
const fx = this.fx;
|
|
620
|
+
let o = at;
|
|
621
|
+
const shape = p.shape ?? POINT;
|
|
622
|
+
let sa = 0;
|
|
623
|
+
let sb = 0;
|
|
624
|
+
let sc = 0;
|
|
625
|
+
let meshOffset = 0;
|
|
626
|
+
let meshCount = 0;
|
|
627
|
+
let flags = p.alignToVelocity === false ? 0 : 1;
|
|
628
|
+
if (shape.kind === 'circle' || shape.kind === 'sphere')
|
|
629
|
+
sa = shape.radius;
|
|
630
|
+
else if (shape.kind === 'rect') {
|
|
631
|
+
sa = shape.width;
|
|
632
|
+
sb = shape.height;
|
|
633
|
+
}
|
|
634
|
+
else if (shape.kind === 'box') {
|
|
635
|
+
sa = shape.width;
|
|
636
|
+
sb = shape.height;
|
|
637
|
+
sc = world ? shape.depth : 0;
|
|
638
|
+
}
|
|
639
|
+
else if (shape.kind === 'line') {
|
|
640
|
+
sa = shape.dx;
|
|
641
|
+
sb = shape.dy;
|
|
642
|
+
sc = world ? (shape.dz ?? 0) : 0;
|
|
643
|
+
}
|
|
644
|
+
else if (shape.kind === 'mesh') {
|
|
645
|
+
const m = this.internSurface(shape);
|
|
646
|
+
meshOffset = m.offset;
|
|
647
|
+
meshCount = m.count;
|
|
648
|
+
if (shape.alongNormal)
|
|
649
|
+
flags |= 2;
|
|
650
|
+
}
|
|
651
|
+
const speed = p.speed ?? (world ? WORLD_SPEED : SPEED_2D);
|
|
652
|
+
const life = p.life ?? LIFE;
|
|
653
|
+
const size = p.size ?? (world ? WORLD_SIZE : SIZE_2D);
|
|
654
|
+
const g = p.gravity ?? (world ? WORLD_GRAVITY : GRAVITY_2D);
|
|
655
|
+
const w = p.wind ?? ZERO3;
|
|
656
|
+
const tile = p.tile ?? 0;
|
|
657
|
+
const rot = p.rotation ?? ZERO2;
|
|
658
|
+
const spin = p.spin ?? ZERO2;
|
|
659
|
+
const sizeSlot = p.sizeCurve ? this.internCurve(fx, p.sizeCurve, false) : -1;
|
|
660
|
+
const colorSlot = p.colorCurve ? this.internCurve(fx, p.colorCurve, true) : -1;
|
|
661
|
+
let deathSlot = -1;
|
|
662
|
+
let lifeSlot = -1;
|
|
663
|
+
if (depth === 0) {
|
|
664
|
+
if (p.onDeath && p.onDeath.count > 0) {
|
|
665
|
+
deathSlot = this.internSub(fx, p.onDeath, p.onDeath.params, p.inherit ?? 0, random);
|
|
666
|
+
}
|
|
667
|
+
if (p.onLife && p.onLife.rate > 0) {
|
|
668
|
+
lifeSlot = this.internSub(fx, p.onLife, p.onLife.params, p.inherit ?? 0, random);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
// a
|
|
672
|
+
d[o++] = p.x;
|
|
673
|
+
d[o++] = p.y;
|
|
674
|
+
d[o++] = world ? (p.z ?? 0) : 0;
|
|
675
|
+
d[o++] = world ? WORLD_SHAPE_ID[shape.kind] : SHAPE_ID[shape.kind];
|
|
676
|
+
// b
|
|
677
|
+
d[o++] = sa;
|
|
678
|
+
d[o++] = sb;
|
|
679
|
+
d[o++] = sc;
|
|
680
|
+
d[o++] = p.spread ?? Math.PI * 2;
|
|
681
|
+
// c
|
|
682
|
+
d[o++] = p.angle ?? (world ? Math.PI / 2 : -Math.PI / 2);
|
|
683
|
+
d[o++] = world ? (p.vz ?? 0) : 0;
|
|
684
|
+
d[o++] = speed[0];
|
|
685
|
+
d[o++] = speed[1];
|
|
686
|
+
// d
|
|
687
|
+
d[o++] = life[0];
|
|
688
|
+
d[o++] = life[1];
|
|
689
|
+
d[o++] = size[0];
|
|
690
|
+
d[o++] = size[1];
|
|
691
|
+
// c0, c1
|
|
692
|
+
o = writeColors(d, o, p.colorStart, p.colorEnd);
|
|
693
|
+
// e
|
|
694
|
+
d[o++] = g[0];
|
|
695
|
+
d[o++] = g[1];
|
|
696
|
+
d[o++] = world ? (g[2] ?? 0) : 0;
|
|
697
|
+
d[o++] = p.drag ?? 1;
|
|
698
|
+
// f
|
|
699
|
+
d[o++] = world ? w[0] : 0;
|
|
700
|
+
d[o++] = world ? w[1] : 0;
|
|
701
|
+
d[o++] = world ? w[2] : 0;
|
|
702
|
+
d[o++] = p.emissive ?? 1;
|
|
703
|
+
// g
|
|
704
|
+
d[o++] = start;
|
|
705
|
+
d[o++] = n;
|
|
706
|
+
d[o++] = random.int(0, SPAWN_SEED_MAX);
|
|
707
|
+
d[o++] = p.inherit ?? 0;
|
|
708
|
+
// h
|
|
709
|
+
d[o++] = typeof tile === 'number' ? tile : tile[0];
|
|
710
|
+
d[o++] = typeof tile === 'number' ? tile : tile[1];
|
|
711
|
+
d[o++] = p.tileRate ?? 0;
|
|
712
|
+
d[o++] = flags;
|
|
713
|
+
// i
|
|
714
|
+
d[o++] = rot[0];
|
|
715
|
+
d[o++] = rot[1];
|
|
716
|
+
d[o++] = spin[0];
|
|
717
|
+
d[o++] = spin[1];
|
|
718
|
+
// j
|
|
719
|
+
d[o++] = sizeSlot;
|
|
720
|
+
d[o++] = colorSlot;
|
|
721
|
+
d[o++] = deathSlot;
|
|
722
|
+
d[o++] = lifeSlot;
|
|
723
|
+
// k
|
|
724
|
+
d[o++] = p.onDeath?.count ?? 0;
|
|
725
|
+
d[o++] = p.onLife?.rate ?? 0;
|
|
726
|
+
d[o++] = meshOffset;
|
|
727
|
+
d[o++] = meshCount;
|
|
728
|
+
}
|
|
729
|
+
/** The curve table slot of `stops` (by identity; rewritten on every emit, so edits are live). */
|
|
730
|
+
internCurve(fx, stops, color) {
|
|
731
|
+
const n = stops.length;
|
|
732
|
+
if (n < 2 || n > MAX_CURVE_STOPS) {
|
|
733
|
+
throw new Error(`[particles] a curve needs 2..${MAX_CURVE_STOPS} stops, got ${n}`);
|
|
734
|
+
}
|
|
735
|
+
const slots = color ? fx.colorSlots : fx.sizeSlots;
|
|
736
|
+
const maxCurves = this.simData[17] / SIZE_CURVE_FLOATS;
|
|
737
|
+
let slot = slots.get(stops);
|
|
738
|
+
if (slot === undefined) {
|
|
739
|
+
const next = color ? fx.colorNext : fx.sizeNext;
|
|
740
|
+
slot = next % maxCurves;
|
|
741
|
+
if (next >= maxCurves && !fx.warnedCurves) {
|
|
742
|
+
console.warn(`[particles] more than ${maxCurves} distinct curves; reusing slots`);
|
|
743
|
+
fx.warnedCurves = true;
|
|
744
|
+
}
|
|
745
|
+
if (color)
|
|
746
|
+
fx.colorNext = next + 1;
|
|
747
|
+
else
|
|
748
|
+
fx.sizeNext = next + 1;
|
|
749
|
+
slots.set(stops, slot);
|
|
750
|
+
}
|
|
751
|
+
const t = fx.curveData;
|
|
752
|
+
if (color) {
|
|
753
|
+
const base = maxCurves * SIZE_CURVE_FLOATS + slot * COLOR_CURVE_FLOATS;
|
|
754
|
+
t[base] = n;
|
|
755
|
+
for (let i = 0; i < n; i++) {
|
|
756
|
+
const c = stops[i];
|
|
757
|
+
t[base + 4 + i * 4] = c[0];
|
|
758
|
+
t[base + 5 + i * 4] = c[1];
|
|
759
|
+
t[base + 6 + i * 4] = c[2];
|
|
760
|
+
t[base + 7 + i * 4] = c[3] ?? 1;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
const base = slot * SIZE_CURVE_FLOATS;
|
|
765
|
+
t[base] = n;
|
|
766
|
+
for (let i = 0; i < n; i++)
|
|
767
|
+
t[base + 1 + i] = stops[i];
|
|
768
|
+
}
|
|
769
|
+
fx.curveDirty = true;
|
|
770
|
+
return slot;
|
|
771
|
+
}
|
|
772
|
+
/** The sub-emitter record slot of `key` (by identity; rewritten on every parent emit). */
|
|
773
|
+
internSub(fx, key, params, inherit, random) {
|
|
774
|
+
const maxSubs = fx.subData.length / FX_SPAWN_FLOATS;
|
|
775
|
+
let slot = fx.subSlots.get(key);
|
|
776
|
+
if (slot === undefined) {
|
|
777
|
+
slot = fx.subNext % maxSubs;
|
|
778
|
+
if (fx.subNext >= maxSubs && !fx.warnedSubs) {
|
|
779
|
+
console.warn(`[particles] more than ${maxSubs} distinct sub-emitters; reusing slots`);
|
|
780
|
+
fx.warnedSubs = true;
|
|
781
|
+
}
|
|
782
|
+
fx.subNext++;
|
|
783
|
+
fx.subUsed = Math.min(fx.subNext, maxSubs);
|
|
784
|
+
fx.subSlots.set(key, slot);
|
|
785
|
+
}
|
|
786
|
+
this.writeFxSpawn(fx.subData, params, 0, 0, slot * FX_SPAWN_FLOATS, 1, random);
|
|
787
|
+
fx.subData[slot * FX_SPAWN_FLOATS + 35] = inherit;
|
|
788
|
+
fx.subDirty = true;
|
|
789
|
+
return slot;
|
|
790
|
+
}
|
|
791
|
+
/** The surface table range of a mesh shape (built and appended on first sight). */
|
|
792
|
+
internSurface(shape) {
|
|
793
|
+
const fx = this.fx;
|
|
794
|
+
let m = fx.surfaceSlots.get(shape);
|
|
795
|
+
if (m)
|
|
796
|
+
return m;
|
|
797
|
+
const surface = 'table' in shape ? shape : buildEmitSurface(shape, shape);
|
|
798
|
+
const floats = surface.triangles * SURFACE_TRI_FLOATS;
|
|
799
|
+
if (fx.surfaceUsed + floats > fx.surfaceData.length) {
|
|
800
|
+
const grown = new Float32Array(Math.max(fx.surfaceUsed + floats, fx.surfaceData.length * 2));
|
|
801
|
+
grown.set(fx.surfaceData.subarray(0, fx.surfaceUsed));
|
|
802
|
+
fx.surfaceData = grown;
|
|
803
|
+
}
|
|
804
|
+
fx.surfaceData.set(surface.table.subarray(0, floats), fx.surfaceUsed);
|
|
805
|
+
m = { offset: fx.surfaceUsed / 4, count: surface.triangles };
|
|
806
|
+
fx.surfaceUsed += floats;
|
|
807
|
+
fx.surfaceDirty = true;
|
|
808
|
+
fx.surfaceSlots.set(shape, m);
|
|
809
|
+
return m;
|
|
810
|
+
}
|
|
811
|
+
/** Advance simulation time. Call from fixed update; integration happens on the GPU. */
|
|
812
|
+
update(dt) {
|
|
813
|
+
this.pendingDt += dt;
|
|
814
|
+
}
|
|
815
|
+
prepare(encoder, frame) {
|
|
816
|
+
const dt = Math.min(this.pendingDt, 0.1);
|
|
817
|
+
this.pendingDt = 0;
|
|
818
|
+
this.time += dt;
|
|
819
|
+
const s = this.simData;
|
|
820
|
+
const fx = this.fx;
|
|
821
|
+
s[0] = dt;
|
|
822
|
+
s[1] = this.floorY;
|
|
823
|
+
s[2] = this.bounce;
|
|
824
|
+
s[3] = this.time;
|
|
825
|
+
s[4] = this.capacity;
|
|
826
|
+
s[5] = this.stretch;
|
|
827
|
+
s[6] = this.spawnCount;
|
|
828
|
+
s[7] = fx ? fx.spawnTotal : 0;
|
|
829
|
+
if (this.soft) {
|
|
830
|
+
// With `readsDepth` the renderer's hook (`afterOpaque`) supplies this frame's depth.
|
|
831
|
+
this.softActive = this.readsDepth ? false : this.copyDepth(encoder, frame);
|
|
832
|
+
this.depthGroup = this.softActive ? this.soft.copyGroup : null;
|
|
833
|
+
const f = this.fade;
|
|
834
|
+
s[8] = f ? f.near : 0;
|
|
835
|
+
s[9] = f ? f.far : 0;
|
|
836
|
+
s[10] = this.softness;
|
|
837
|
+
s[11] = this.softActive ? 1 : 0;
|
|
838
|
+
s[12] = this.lit ? 1 : 0;
|
|
839
|
+
}
|
|
840
|
+
if (fx) {
|
|
841
|
+
s[13] = fx.textured ? 1 : 0;
|
|
842
|
+
s[14] = fx.columns;
|
|
843
|
+
s[15] = fx.rows;
|
|
844
|
+
s[16] = this.writeForces(s);
|
|
845
|
+
}
|
|
846
|
+
const q = this.device.queue;
|
|
847
|
+
q.writeBuffer(this.simBuffer, 0, s);
|
|
848
|
+
if (this.clearPending) {
|
|
849
|
+
encoder.clearBuffer(this.particleBuffer);
|
|
850
|
+
if (fx)
|
|
851
|
+
encoder.clearBuffer(fx.state);
|
|
852
|
+
this.clearPending = false;
|
|
853
|
+
}
|
|
854
|
+
if (fx)
|
|
855
|
+
this.uploadTables(fx);
|
|
856
|
+
const counter = this.counter;
|
|
857
|
+
if (counter?.state === 'copied')
|
|
858
|
+
this.readAlive(counter);
|
|
859
|
+
const count = counter?.state === 'idle';
|
|
860
|
+
if (count)
|
|
861
|
+
encoder.clearBuffer(counter.count);
|
|
862
|
+
const pass = encoder.beginComputePass({ label: 'particles' });
|
|
863
|
+
pass.setBindGroup(0, this.computeGroup);
|
|
864
|
+
if (this.spawnCount > 0) {
|
|
865
|
+
q.writeBuffer(this.spawnBuffer, 0, this.spawnData.buffer, 0, this.spawnCount * this.spawnFloats * 4);
|
|
866
|
+
pass.setPipeline(this.spawnPipeline);
|
|
867
|
+
pass.dispatchWorkgroups(Math.ceil(this.maxSpawnInCmd / 64), this.spawnCount);
|
|
868
|
+
}
|
|
869
|
+
if (fx) {
|
|
870
|
+
pass.setPipeline(fx.advancePipeline);
|
|
871
|
+
pass.dispatchWorkgroups(1);
|
|
872
|
+
}
|
|
873
|
+
if (dt > 0) {
|
|
874
|
+
pass.setPipeline(this.updatePipeline);
|
|
875
|
+
pass.dispatchWorkgroups(Math.ceil(this.capacity / 64));
|
|
876
|
+
if (fx && fx.subUsed > 0) {
|
|
877
|
+
pass.setPipeline(fx.subPipeline);
|
|
878
|
+
pass.dispatchWorkgroups(Math.ceil(this.maxSpawns / 64));
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (count) {
|
|
882
|
+
pass.setPipeline(counter.pipeline);
|
|
883
|
+
pass.setBindGroup(0, counter.group);
|
|
884
|
+
pass.dispatchWorkgroups(Math.ceil(this.capacity / 64));
|
|
885
|
+
}
|
|
886
|
+
pass.end();
|
|
887
|
+
if (count) {
|
|
888
|
+
encoder.copyBufferToBuffer(counter.count, 0, counter.read, 0, 8);
|
|
889
|
+
counter.state = 'copied';
|
|
890
|
+
}
|
|
891
|
+
this.spawnCount = 0;
|
|
892
|
+
this.maxSpawnInCmd = 0;
|
|
893
|
+
if (fx)
|
|
894
|
+
fx.spawnTotal = 0;
|
|
895
|
+
this.warnedOverflow = false;
|
|
896
|
+
}
|
|
897
|
+
/** Pack `forces` (the first `MAX_FORCES`) into the sim block; returns how many. */
|
|
898
|
+
writeForces(s) {
|
|
899
|
+
const forces = this.forces;
|
|
900
|
+
const n = Math.min(forces.length, MAX_FORCES);
|
|
901
|
+
for (let i = 0; i < n; i++) {
|
|
902
|
+
const f = forces[i];
|
|
903
|
+
const o = 20 + i * 12;
|
|
904
|
+
s[o] = FORCE_ID[f.kind];
|
|
905
|
+
for (let k = 1; k < 12; k++)
|
|
906
|
+
s[o + k] = 0;
|
|
907
|
+
if (f.kind === 'attract' || f.kind === 'vortex') {
|
|
908
|
+
s[o + 1] = f.x;
|
|
909
|
+
s[o + 2] = f.y;
|
|
910
|
+
s[o + 3] = f.z ?? 0;
|
|
911
|
+
s[o + 4] = f.strength;
|
|
912
|
+
s[o + 5] = f.radius ?? 0;
|
|
913
|
+
if (f.kind === 'vortex') {
|
|
914
|
+
s[o + 8] = f.axis[0];
|
|
915
|
+
s[o + 9] = f.axis[1];
|
|
916
|
+
s[o + 10] = f.axis[2];
|
|
917
|
+
}
|
|
918
|
+
else {
|
|
919
|
+
s[o + 8] = FALLOFF_ID[f.falloff ?? 'constant'];
|
|
920
|
+
s[o + 9] = f.deadZone ?? 0;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
else if (f.kind === 'turbulence') {
|
|
924
|
+
s[o + 4] = f.strength;
|
|
925
|
+
s[o + 6] = f.frequency;
|
|
926
|
+
s[o + 7] = f.seed ?? 0;
|
|
927
|
+
}
|
|
928
|
+
else {
|
|
929
|
+
s[o + 11] = f.amount;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return n;
|
|
933
|
+
}
|
|
934
|
+
/** Upload the dirty curve, sub-emitter and surface tables (the surface buffer may grow). */
|
|
935
|
+
uploadTables(fx) {
|
|
936
|
+
const q = this.device.queue;
|
|
937
|
+
if (fx.curveDirty) {
|
|
938
|
+
q.writeBuffer(fx.curves, 0, fx.curveData);
|
|
939
|
+
fx.curveDirty = false;
|
|
940
|
+
}
|
|
941
|
+
if (fx.subDirty) {
|
|
942
|
+
q.writeBuffer(fx.subs, 0, fx.subData.buffer, 0, fx.subUsed * FX_SPAWN_FLOATS * 4);
|
|
943
|
+
fx.subDirty = false;
|
|
944
|
+
}
|
|
945
|
+
if (fx.surfaceDirty) {
|
|
946
|
+
const bytes = fx.surfaceUsed * 4;
|
|
947
|
+
if (bytes > fx.surface.size) {
|
|
948
|
+
fx.surface.destroy();
|
|
949
|
+
fx.surface = this.device.createBuffer({
|
|
950
|
+
label: 'particles-surface',
|
|
951
|
+
size: fx.surfaceData.byteLength,
|
|
952
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
953
|
+
});
|
|
954
|
+
fx.computeGroup = this.makeComputeGroup(fx);
|
|
955
|
+
this.computeGroup = fx.computeGroup;
|
|
956
|
+
}
|
|
957
|
+
q.writeBuffer(fx.surface, 0, fx.surfaceData.buffer, 0, bytes);
|
|
958
|
+
fx.surfaceDirty = false;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Map last frame's count (its commands were submitted by now). This frame records nothing
|
|
963
|
+
* into the readback buffer, so mapping it while this frame is encoded is valid.
|
|
964
|
+
*/
|
|
965
|
+
readAlive(c) {
|
|
966
|
+
c.state = 'mapping';
|
|
967
|
+
c.read.mapAsync(GPUMapMode.READ).then(() => {
|
|
968
|
+
if (c.state !== 'mapping')
|
|
969
|
+
return;
|
|
970
|
+
const counts = new Uint32Array(c.read.getMappedRange(0, 8));
|
|
971
|
+
this.alive = counts[0];
|
|
972
|
+
this.below = counts[1] ?? 0;
|
|
973
|
+
c.read.unmap();
|
|
974
|
+
c.state = 'idle';
|
|
975
|
+
}, () => {
|
|
976
|
+
c.state = 'dead';
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* World space: copy the scene's depth into this system's own depth32float texture, which the
|
|
981
|
+
* fragment shader samples for the soft fade. It runs before this frame's scene pass, so it
|
|
982
|
+
* holds what the target's depth held then: the previous frame's, or that of an earlier
|
|
983
|
+
* `render` call this frame (draw the particles on their own layer in a second view of the
|
|
984
|
+
* same camera for a same-frame fade). Returns false (soft fade off) without depth.
|
|
985
|
+
*/
|
|
986
|
+
copyDepth(encoder, frame) {
|
|
987
|
+
const sd = this.soft;
|
|
988
|
+
if (this.softness <= 0 || !frame?.depthFormat)
|
|
989
|
+
return false;
|
|
990
|
+
const src = (this.depthTarget ?? this.renderer.target).depth;
|
|
991
|
+
if (!src)
|
|
992
|
+
return false;
|
|
993
|
+
const device = this.device;
|
|
994
|
+
if (!sd.copy || sd.copy.width !== src.width || sd.copy.height !== src.height) {
|
|
995
|
+
sd.copy?.destroy();
|
|
996
|
+
sd.copy = device.createTexture({
|
|
997
|
+
label: 'particles-depth',
|
|
998
|
+
size: [src.width, src.height],
|
|
999
|
+
format: SOFT_DEPTH_FORMAT,
|
|
1000
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1001
|
+
});
|
|
1002
|
+
const view = sd.copy.createView();
|
|
1003
|
+
sd.att.view = view;
|
|
1004
|
+
sd.copyGroup = this.makeRenderGroup('particles-world-render-soft', sd.renderLayout, view);
|
|
1005
|
+
}
|
|
1006
|
+
if (sd.src !== src) {
|
|
1007
|
+
sd.src = src;
|
|
1008
|
+
sd.srcGroup = device.createBindGroup({
|
|
1009
|
+
label: 'particles-depth-copy',
|
|
1010
|
+
layout: sd.copyLayout,
|
|
1011
|
+
entries: [{ binding: 0, resource: src.view }],
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
const pass = encoder.beginRenderPass(sd.passDesc);
|
|
1015
|
+
pass.setPipeline(sd.copyPipeline);
|
|
1016
|
+
pass.setBindGroup(0, sd.srcGroup);
|
|
1017
|
+
pass.draw(3);
|
|
1018
|
+
pass.end();
|
|
1019
|
+
return true;
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Draw inside the scene pass. With a depth camera (`frame.depthFormat` set) the pipeline
|
|
1023
|
+
* tests against depth without writing it, like any blended material.
|
|
1024
|
+
*/
|
|
1025
|
+
draw(pass, frame) {
|
|
1026
|
+
const fmt = frame?.depthFormat ?? null;
|
|
1027
|
+
pass.setPipeline(this.pipelineFor(fmt, frame ? frame.auxKey : this.renderer.target.auxKey));
|
|
1028
|
+
pass.setBindGroup(1, this.depthGroup ?? this.renderGroup);
|
|
1029
|
+
pass.draw(6, this.capacity);
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* The renderer's depth hook (`readsDepth`, world space): with `softness` > 0 bind this
|
|
1033
|
+
* frame's opaque depth copy and flip the shader's soft flag on (a 4-byte write that lands
|
|
1034
|
+
* before the frame's commands run); with `collide` run the collision kernel against the same
|
|
1035
|
+
* depth. New bind groups only when the copy texture changed.
|
|
1036
|
+
*/
|
|
1037
|
+
afterOpaque(encoder, view) {
|
|
1038
|
+
const sd = this.soft;
|
|
1039
|
+
if (!sd)
|
|
1040
|
+
return;
|
|
1041
|
+
if (this.softness > 0) {
|
|
1042
|
+
if (view.depthView !== this.hookView) {
|
|
1043
|
+
this.hookView = view.depthView;
|
|
1044
|
+
this.hookGroup = this.makeRenderGroup('particles-world-render-hook', sd.renderLayout, view.depthView);
|
|
1045
|
+
}
|
|
1046
|
+
this.softActive = true;
|
|
1047
|
+
this.depthGroup = this.hookGroup;
|
|
1048
|
+
const s = this.simData;
|
|
1049
|
+
s[11] = 1;
|
|
1050
|
+
this.device.queue.writeBuffer(this.simBuffer, 11 * 4, s.buffer, 11 * 4, 4);
|
|
1051
|
+
}
|
|
1052
|
+
const col = this.collider;
|
|
1053
|
+
const params = this.collide;
|
|
1054
|
+
if (col && params) {
|
|
1055
|
+
if (view.depthView !== col.view) {
|
|
1056
|
+
col.view = view.depthView;
|
|
1057
|
+
col.group = this.device.createBindGroup({
|
|
1058
|
+
label: 'particles-collide',
|
|
1059
|
+
layout: col.layout,
|
|
1060
|
+
entries: [
|
|
1061
|
+
{ binding: 0, resource: { buffer: this.particleBuffer } },
|
|
1062
|
+
{ binding: 1, resource: { buffer: col.uniform } },
|
|
1063
|
+
{ binding: 2, resource: view.depthView },
|
|
1064
|
+
],
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
const d = col.data;
|
|
1068
|
+
d.set(view.camera.viewProjection, 0);
|
|
1069
|
+
d.set(view.camera.invViewProj, 16);
|
|
1070
|
+
const vp = view.viewport;
|
|
1071
|
+
d[32] = vp.x;
|
|
1072
|
+
d[33] = vp.y;
|
|
1073
|
+
d[34] = vp.width;
|
|
1074
|
+
d[35] = vp.height;
|
|
1075
|
+
d[36] = params.bounce;
|
|
1076
|
+
d[37] = params.friction;
|
|
1077
|
+
d[38] = params.thickness ?? 1;
|
|
1078
|
+
// The last advance's dt: the swept test steps back along the velocity by it.
|
|
1079
|
+
d[39] = this.simData[0];
|
|
1080
|
+
this.device.queue.writeBuffer(col.uniform, 0, d);
|
|
1081
|
+
const pass = encoder.beginComputePass({ label: 'particles-collide' });
|
|
1082
|
+
pass.setPipeline(col.pipeline);
|
|
1083
|
+
pass.setBindGroup(0, col.group);
|
|
1084
|
+
pass.dispatchWorkgroups(Math.ceil(this.capacity / 64));
|
|
1085
|
+
pass.end();
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
pipelineFor(format, aux) {
|
|
1089
|
+
const p = this.pipelines.get(format, aux);
|
|
1090
|
+
if (p)
|
|
1091
|
+
return p;
|
|
1092
|
+
const desc = this.renderDesc;
|
|
1093
|
+
const targets = this.renderer.sceneTargets(this.blend, aux);
|
|
1094
|
+
const label = `${desc.label}${format ? `:${format}` : ''}${auxSuffix(aux)}`;
|
|
1095
|
+
const depth = format
|
|
1096
|
+
? { format, depthWriteEnabled: false, depthCompare: 'less-equal' }
|
|
1097
|
+
: undefined;
|
|
1098
|
+
// World particles write their own motion into a velocity attachment (ws/render-11); 2D
|
|
1099
|
+
// ones (screen space, no camera history) keep it masked.
|
|
1100
|
+
const at = this.space === 'world' ? velocityLocation(aux) : 0;
|
|
1101
|
+
return this.pipelines.set(format, aux, this.device.createRenderPipeline(at === 0
|
|
1102
|
+
? sceneVariant(desc, targets, label, depth)
|
|
1103
|
+
: motionVariant(desc, this.motionModule(at), targets, aux, `${label}:motion`, depth)));
|
|
1104
|
+
}
|
|
1105
|
+
motionModule(at) {
|
|
1106
|
+
let m = this.motionModules[at];
|
|
1107
|
+
if (!m) {
|
|
1108
|
+
m = createShaderModule(this.device, fxRenderWgsl(true, at), 'particles-world-render-motion');
|
|
1109
|
+
this.motionModules[at] = m;
|
|
1110
|
+
}
|
|
1111
|
+
return m;
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Kill every particle (e.g. on scene change), including emits queued earlier this frame.
|
|
1115
|
+
* The buffer is zeroed on the GPU at the start of the next `prepare` (no CPU allocation);
|
|
1116
|
+
* particles emitted after `clear()` survive.
|
|
1117
|
+
*/
|
|
1118
|
+
clear() {
|
|
1119
|
+
this.clearPending = true;
|
|
1120
|
+
this.spawnCount = 0;
|
|
1121
|
+
this.maxSpawnInCmd = 0;
|
|
1122
|
+
this.head = 0;
|
|
1123
|
+
if (this.fx)
|
|
1124
|
+
this.fx.spawnTotal = 0;
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Copy the particle buffer into the staging buffer, map it and return the live particles
|
|
1128
|
+
* (`debug: true` only; rejects otherwise). Never runs on its own. The copy is submitted at
|
|
1129
|
+
* once, so call it outside a frame (after `endFrame`): it then holds every frame submitted so
|
|
1130
|
+
* far. A call while one is in flight returns that one's promise.
|
|
1131
|
+
*/
|
|
1132
|
+
debugReadback() {
|
|
1133
|
+
const job = this.debugCopy('debugReadback');
|
|
1134
|
+
if (!job) {
|
|
1135
|
+
return Promise.reject(new Error('ParticleSystem.debugReadback needs `debug: true`'));
|
|
1136
|
+
}
|
|
1137
|
+
if (!job.packed) {
|
|
1138
|
+
job.wantPacked = true;
|
|
1139
|
+
job.packed = job.done.then((r) => r.packed);
|
|
1140
|
+
}
|
|
1141
|
+
return job.packed;
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* As `debugReadback`, but resolves to every field of the live particles by name
|
|
1145
|
+
* (`ParticleFields`), unpacked from the kernel's layout (ws/fx-7). `debug: true` only. Calls
|
|
1146
|
+
* made while a copy is in flight share it with each other and with `debugReadback`, so both
|
|
1147
|
+
* forms called back to back describe the same copy.
|
|
1148
|
+
*/
|
|
1149
|
+
readbackFields() {
|
|
1150
|
+
const job = this.debugCopy('readbackFields');
|
|
1151
|
+
if (!job) {
|
|
1152
|
+
return Promise.reject(new Error('ParticleSystem.readbackFields needs `debug: true`'));
|
|
1153
|
+
}
|
|
1154
|
+
if (!job.fields) {
|
|
1155
|
+
job.wantFields = true;
|
|
1156
|
+
job.fields = job.done.then((r) => r.fields);
|
|
1157
|
+
}
|
|
1158
|
+
return job.fields;
|
|
1159
|
+
}
|
|
1160
|
+
/** The copy in flight, or a new one submitted now; null without `debug`. */
|
|
1161
|
+
debugCopy(label) {
|
|
1162
|
+
const read = this.debugRead;
|
|
1163
|
+
if (!read)
|
|
1164
|
+
return null;
|
|
1165
|
+
if (this.debugJob)
|
|
1166
|
+
return this.debugJob;
|
|
1167
|
+
const enc = this.device.createCommandEncoder({ label: `particles-${label}` });
|
|
1168
|
+
enc.copyBufferToBuffer(this.particleBuffer, 0, read, 0, read.size);
|
|
1169
|
+
this.device.queue.submit([enc.finish()]);
|
|
1170
|
+
const time = this.time;
|
|
1171
|
+
const job = {
|
|
1172
|
+
wantPacked: false,
|
|
1173
|
+
wantFields: false,
|
|
1174
|
+
packed: null,
|
|
1175
|
+
fields: null,
|
|
1176
|
+
done: read.mapAsync(GPUMapMode.READ).then(() => {
|
|
1177
|
+
const d = new Float32Array(read.getMappedRange(0, read.size));
|
|
1178
|
+
const out = {
|
|
1179
|
+
packed: job.wantPacked ? parseParticles(d, this.capacity, this.extended, time) : null,
|
|
1180
|
+
fields: job.wantFields ? this.parseFields(d, time) : null,
|
|
1181
|
+
};
|
|
1182
|
+
read.unmap();
|
|
1183
|
+
this.debugJob = null;
|
|
1184
|
+
return out;
|
|
1185
|
+
}, (err) => {
|
|
1186
|
+
this.debugJob = null;
|
|
1187
|
+
throw err;
|
|
1188
|
+
}),
|
|
1189
|
+
};
|
|
1190
|
+
this.debugJob = job;
|
|
1191
|
+
return job;
|
|
1192
|
+
}
|
|
1193
|
+
/** `parseFields` with this system's layout and curve table. */
|
|
1194
|
+
parseFields(d, time) {
|
|
1195
|
+
const fx = this.fx;
|
|
1196
|
+
return parseFields(d, this.capacity, this.extended, time, fx ? fx.curveData : null);
|
|
1197
|
+
}
|
|
1198
|
+
destroy() {
|
|
1199
|
+
this.debugRead?.destroy();
|
|
1200
|
+
this.particleBuffer.destroy();
|
|
1201
|
+
this.spawnBuffer.destroy();
|
|
1202
|
+
this.simBuffer.destroy();
|
|
1203
|
+
if (this.soft) {
|
|
1204
|
+
this.soft.dummy.destroy();
|
|
1205
|
+
this.soft.copy?.destroy();
|
|
1206
|
+
this.soft.copy = null;
|
|
1207
|
+
}
|
|
1208
|
+
if (this.counter) {
|
|
1209
|
+
this.counter.state = 'dead';
|
|
1210
|
+
this.counter.count.destroy();
|
|
1211
|
+
this.counter.read.destroy();
|
|
1212
|
+
this.counter.params.destroy();
|
|
1213
|
+
}
|
|
1214
|
+
const fx = this.fx;
|
|
1215
|
+
if (fx) {
|
|
1216
|
+
fx.state.destroy();
|
|
1217
|
+
fx.subs.destroy();
|
|
1218
|
+
fx.curves.destroy();
|
|
1219
|
+
fx.surface.destroy();
|
|
1220
|
+
fx.white?.destroy();
|
|
1221
|
+
}
|
|
1222
|
+
this.collider?.uniform.destroy();
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* The live particles of a particle buffer's floats. Layouts: the original kernel's 20 floats
|
|
1227
|
+
* (`posVel`, c0, c1, `life` at 12), the extended kernel's 40 (`pos` at 0, `vel` at 4, `life`
|
|
1228
|
+
* at 16). Live means age < lifetime, as `PARTICLE_COUNT_WGSL` counts.
|
|
1229
|
+
*/
|
|
1230
|
+
function parseParticles(d, capacity, extended, time) {
|
|
1231
|
+
const stride = extended ? FX_PARTICLE_FLOATS : PARTICLE_BYTES / 4;
|
|
1232
|
+
const lifeAt = extended ? 16 : 12;
|
|
1233
|
+
let alive = 0;
|
|
1234
|
+
for (let i = 0; i < capacity; i++) {
|
|
1235
|
+
const o = i * stride + lifeAt;
|
|
1236
|
+
if (d[o] < d[o + 1])
|
|
1237
|
+
alive++;
|
|
1238
|
+
}
|
|
1239
|
+
const positions = new Float32Array(alive * 3);
|
|
1240
|
+
const velocities = new Float32Array(alive * 3);
|
|
1241
|
+
const slots = new Uint32Array(alive);
|
|
1242
|
+
let n = 0;
|
|
1243
|
+
for (let i = 0; i < capacity; i++) {
|
|
1244
|
+
const o = i * stride;
|
|
1245
|
+
if (!(d[o + lifeAt] < d[o + lifeAt + 1]))
|
|
1246
|
+
continue;
|
|
1247
|
+
slots[n] = i;
|
|
1248
|
+
const k = n * 3;
|
|
1249
|
+
positions[k] = d[o];
|
|
1250
|
+
positions[k + 1] = d[o + 1];
|
|
1251
|
+
if (extended) {
|
|
1252
|
+
positions[k + 2] = d[o + 2];
|
|
1253
|
+
velocities[k] = d[o + 4];
|
|
1254
|
+
velocities[k + 1] = d[o + 5];
|
|
1255
|
+
velocities[k + 2] = d[o + 6];
|
|
1256
|
+
}
|
|
1257
|
+
else {
|
|
1258
|
+
velocities[k] = d[o + 2];
|
|
1259
|
+
velocities[k + 1] = d[o + 3];
|
|
1260
|
+
}
|
|
1261
|
+
n++;
|
|
1262
|
+
}
|
|
1263
|
+
return { alive, positions, velocities, slots, time };
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Every field of the live particles of a particle buffer's floats (ws/fx-7). The original
|
|
1267
|
+
* kernel's 20 floats: `posVel` (pos.xy, vel.xy), c0, c1, `life` (age, lifetime, size at birth,
|
|
1268
|
+
* size at death), `phys` (gravity.xy, drag, emissive). The extended kernel's 40: `pos` (xyz,
|
|
1269
|
+
* rotation), `vel` (xyz, spin), c0, c1, `life`, `phys` (gravity xyz, drag), `wind` (xyz,
|
|
1270
|
+
* emissive), `tile`, `curve` (slots), `sub`. `curves` is the curve table (extended only) for
|
|
1271
|
+
* `radius`, read as the kernel's `size_at` does.
|
|
1272
|
+
*/
|
|
1273
|
+
function parseFields(d, capacity, extended, time, curves) {
|
|
1274
|
+
const stride = extended ? FX_PARTICLE_FLOATS : PARTICLE_BYTES / 4;
|
|
1275
|
+
const lifeAt = extended ? 16 : 12;
|
|
1276
|
+
let alive = 0;
|
|
1277
|
+
for (let i = 0; i < capacity; i++) {
|
|
1278
|
+
const o = i * stride + lifeAt;
|
|
1279
|
+
if (d[o] < d[o + 1])
|
|
1280
|
+
alive++;
|
|
1281
|
+
}
|
|
1282
|
+
const f = {
|
|
1283
|
+
alive,
|
|
1284
|
+
slots: new Uint32Array(alive),
|
|
1285
|
+
time,
|
|
1286
|
+
position: new Float32Array(alive * 3),
|
|
1287
|
+
velocity: new Float32Array(alive * 3),
|
|
1288
|
+
age: new Float32Array(alive),
|
|
1289
|
+
life: new Float32Array(alive),
|
|
1290
|
+
radius: new Float32Array(alive),
|
|
1291
|
+
sizeStart: new Float32Array(alive),
|
|
1292
|
+
sizeEnd: new Float32Array(alive),
|
|
1293
|
+
color0: new Float32Array(alive * 4),
|
|
1294
|
+
color1: new Float32Array(alive * 4),
|
|
1295
|
+
gravity: new Float32Array(alive * 3),
|
|
1296
|
+
drag: new Float32Array(alive),
|
|
1297
|
+
emissive: new Float32Array(alive),
|
|
1298
|
+
rotation: new Float32Array(alive),
|
|
1299
|
+
spin: new Float32Array(alive),
|
|
1300
|
+
wind: new Float32Array(alive * 3),
|
|
1301
|
+
tile: new Float32Array(alive * 4),
|
|
1302
|
+
curveSlots: new Int32Array(alive * 4).fill(-1),
|
|
1303
|
+
sub: new Float32Array(alive * 3),
|
|
1304
|
+
};
|
|
1305
|
+
let n = 0;
|
|
1306
|
+
for (let i = 0; i < capacity; i++) {
|
|
1307
|
+
const o = i * stride;
|
|
1308
|
+
const age = d[o + lifeAt];
|
|
1309
|
+
const life = d[o + lifeAt + 1];
|
|
1310
|
+
if (!(age < life))
|
|
1311
|
+
continue;
|
|
1312
|
+
const s0 = d[o + lifeAt + 2];
|
|
1313
|
+
const s1 = d[o + lifeAt + 3];
|
|
1314
|
+
const t = age / life;
|
|
1315
|
+
const k3 = n * 3;
|
|
1316
|
+
const k4 = n * 4;
|
|
1317
|
+
f.slots[n] = i;
|
|
1318
|
+
f.age[n] = age;
|
|
1319
|
+
f.life[n] = life;
|
|
1320
|
+
f.sizeStart[n] = s0;
|
|
1321
|
+
f.sizeEnd[n] = s1;
|
|
1322
|
+
// c0 then c1 straight before `life` in both layouts.
|
|
1323
|
+
f.color0.set(d.subarray(o + lifeAt - 8, o + lifeAt - 4), k4);
|
|
1324
|
+
f.color1.set(d.subarray(o + lifeAt - 4, o + lifeAt), k4);
|
|
1325
|
+
f.position[k3] = d[o];
|
|
1326
|
+
f.position[k3 + 1] = d[o + 1];
|
|
1327
|
+
let size = s0 + (s1 - s0) * t;
|
|
1328
|
+
if (extended) {
|
|
1329
|
+
f.position[k3 + 2] = d[o + 2];
|
|
1330
|
+
f.rotation[n] = d[o + 3];
|
|
1331
|
+
f.velocity.set(d.subarray(o + 4, o + 7), k3);
|
|
1332
|
+
f.spin[n] = d[o + 7];
|
|
1333
|
+
f.gravity.set(d.subarray(o + 20, o + 23), k3);
|
|
1334
|
+
f.drag[n] = d[o + 23];
|
|
1335
|
+
f.wind.set(d.subarray(o + 24, o + 27), k3);
|
|
1336
|
+
f.emissive[n] = d[o + 27];
|
|
1337
|
+
f.tile.set(d.subarray(o + 28, o + 32), k4);
|
|
1338
|
+
for (let c = 0; c < 4; c++)
|
|
1339
|
+
f.curveSlots[k4 + c] = d[o + 32 + c];
|
|
1340
|
+
f.sub.set(d.subarray(o + 36, o + 39), k3);
|
|
1341
|
+
const slot = d[o + 32];
|
|
1342
|
+
if (slot >= 0 && curves) {
|
|
1343
|
+
// size_at: piecewise-linear over the slot's stops, times the size at birth.
|
|
1344
|
+
const base = Math.floor(slot) * SIZE_CURVE_FLOATS;
|
|
1345
|
+
const stops = curves[base];
|
|
1346
|
+
const x = Math.min(Math.max(t, 0), 1) * (stops - 1);
|
|
1347
|
+
const j = Math.min(Math.floor(x), stops - 2);
|
|
1348
|
+
const a = curves[base + 1 + j];
|
|
1349
|
+
size = (a + (curves[base + 2 + j] - a) * (x - j)) * s0;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
else {
|
|
1353
|
+
f.velocity[k3] = d[o + 2];
|
|
1354
|
+
f.velocity[k3 + 1] = d[o + 3];
|
|
1355
|
+
f.gravity[k3] = d[o + 16];
|
|
1356
|
+
f.gravity[k3 + 1] = d[o + 17];
|
|
1357
|
+
f.drag[n] = d[o + 18];
|
|
1358
|
+
f.emissive[n] = d[o + 19];
|
|
1359
|
+
}
|
|
1360
|
+
f.radius[n] = Math.max(size, 0) * 0.5;
|
|
1361
|
+
n++;
|
|
1362
|
+
}
|
|
1363
|
+
return f;
|
|
1364
|
+
}
|
|
1365
|
+
/** Write `c0` and `c1` (default: `c0` with alpha 0) at `o`; returns the next offset. */
|
|
1366
|
+
function writeColors(d, at, c0, c1) {
|
|
1367
|
+
let o = at;
|
|
1368
|
+
d[o++] = c0[0];
|
|
1369
|
+
d[o++] = c0[1];
|
|
1370
|
+
d[o++] = c0[2];
|
|
1371
|
+
d[o++] = c0[3] ?? 1;
|
|
1372
|
+
d[o++] = c1 ? c1[0] : c0[0];
|
|
1373
|
+
d[o++] = c1 ? c1[1] : c0[1];
|
|
1374
|
+
d[o++] = c1 ? c1[2] : c0[2];
|
|
1375
|
+
d[o++] = c1 ? (c1[3] ?? 1) : 0;
|
|
1376
|
+
return o;
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* The CPU reference of the kernel's curve lookup: piecewise-linear over `stops` (2..8) spread
|
|
1380
|
+
* evenly on 0..1, each stop `stride` numbers (1 for a size curve, 4 for RGBA), written to `out`.
|
|
1381
|
+
*/
|
|
1382
|
+
export function evaluateCurve(stops, stride, t, out) {
|
|
1383
|
+
const n = Math.floor(stops.length / stride);
|
|
1384
|
+
const x = Math.min(Math.max(t, 0), 1) * (n - 1);
|
|
1385
|
+
const i = Math.min(Math.floor(x), n - 2);
|
|
1386
|
+
const f = x - i;
|
|
1387
|
+
for (let k = 0; k < stride; k++) {
|
|
1388
|
+
const a = stops[i * stride + k];
|
|
1389
|
+
const b = stops[(i + 1) * stride + k];
|
|
1390
|
+
out[k] = a + (b - a) * f;
|
|
1391
|
+
}
|
|
1392
|
+
return out;
|
|
1393
|
+
}
|
|
1394
|
+
function pcg(v) {
|
|
1395
|
+
const s = (Math.imul(v, 747796405) + 2891336453) >>> 0;
|
|
1396
|
+
const w = Math.imul(((s >>> ((s >>> 28) + 4)) ^ s) >>> 0, 277803737) >>> 0;
|
|
1397
|
+
return ((w >>> 22) ^ w) >>> 0;
|
|
1398
|
+
}
|
|
1399
|
+
function lattice(x, y, z, seed) {
|
|
1400
|
+
const h = pcg((Math.imul(x, 73856093) ^ Math.imul(y, 19349663) ^ Math.imul(z, 83492791) ^ seed) >>> 0);
|
|
1401
|
+
return h / 4294967295;
|
|
1402
|
+
}
|
|
1403
|
+
function vnoise(x, y, z, seed) {
|
|
1404
|
+
const ix = Math.floor(x);
|
|
1405
|
+
const iy = Math.floor(y);
|
|
1406
|
+
const iz = Math.floor(z);
|
|
1407
|
+
const fx = x - ix;
|
|
1408
|
+
const fy = y - iy;
|
|
1409
|
+
const fz = z - iz;
|
|
1410
|
+
const ux = fx * fx * (3 - 2 * fx);
|
|
1411
|
+
const uy = fy * fy * (3 - 2 * fy);
|
|
1412
|
+
const uz = fz * fz * (3 - 2 * fz);
|
|
1413
|
+
const l = (dx, dy, dz) => lattice(ix + dx, iy + dy, iz + dz, seed);
|
|
1414
|
+
const x00 = l(0, 0, 0) + (l(1, 0, 0) - l(0, 0, 0)) * ux;
|
|
1415
|
+
const x10 = l(0, 1, 0) + (l(1, 1, 0) - l(0, 1, 0)) * ux;
|
|
1416
|
+
const x01 = l(0, 0, 1) + (l(1, 0, 1) - l(0, 0, 1)) * ux;
|
|
1417
|
+
const x11 = l(0, 1, 1) + (l(1, 1, 1) - l(0, 1, 1)) * ux;
|
|
1418
|
+
const y0 = x00 + (x10 - x00) * uy;
|
|
1419
|
+
const y1 = x01 + (x11 - x01) * uy;
|
|
1420
|
+
return y0 + (y1 - y0) * uz;
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* The CPU reference of the kernel's force step: accelerate `vel` (xyz) by `forces` at `pos`
|
|
1424
|
+
* over `dt` and apply the extra drag (the particle's own gravity, drag and wind are not
|
|
1425
|
+
* included). For tests and shadow simulations; the GPU does the real work.
|
|
1426
|
+
*/
|
|
1427
|
+
export function applyForces(forces, pos, vel, dt) {
|
|
1428
|
+
let ax = 0;
|
|
1429
|
+
let ay = 0;
|
|
1430
|
+
let az = 0;
|
|
1431
|
+
let drag = 0;
|
|
1432
|
+
const px = pos[0];
|
|
1433
|
+
const py = pos[1];
|
|
1434
|
+
const pz = pos[2] ?? 0;
|
|
1435
|
+
for (const f of forces) {
|
|
1436
|
+
if (f.kind === 'attract') {
|
|
1437
|
+
const dx = f.x - px;
|
|
1438
|
+
const dy = f.y - py;
|
|
1439
|
+
const dz = (f.z ?? 0) - pz;
|
|
1440
|
+
const r = Math.hypot(dx, dy, dz);
|
|
1441
|
+
const radius = f.radius ?? 0;
|
|
1442
|
+
let w = radius > 0 ? Math.max(1 - r / radius, 0) : 1;
|
|
1443
|
+
if (f.falloff === 'linear')
|
|
1444
|
+
w *= r;
|
|
1445
|
+
else if (f.falloff === 'inverse-square')
|
|
1446
|
+
w /= Math.max(r * r, 1e-4);
|
|
1447
|
+
if (r > 1e-4 && r >= (f.deadZone ?? 0)) {
|
|
1448
|
+
ax += (dx / r) * f.strength * w;
|
|
1449
|
+
ay += (dy / r) * f.strength * w;
|
|
1450
|
+
az += (dz / r) * f.strength * w;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
else if (f.kind === 'vortex') {
|
|
1454
|
+
const al = Math.hypot(f.axis[0], f.axis[1], f.axis[2]) || 1;
|
|
1455
|
+
const nx = f.axis[0] / al;
|
|
1456
|
+
const ny = f.axis[1] / al;
|
|
1457
|
+
const nz = f.axis[2] / al;
|
|
1458
|
+
const rx = px - f.x;
|
|
1459
|
+
const ry = py - f.y;
|
|
1460
|
+
const rz = pz - (f.z ?? 0);
|
|
1461
|
+
const d = rx * nx + ry * ny + rz * nz;
|
|
1462
|
+
const qx = rx - nx * d;
|
|
1463
|
+
const qy = ry - ny * d;
|
|
1464
|
+
const qz = rz - nz * d;
|
|
1465
|
+
const r = Math.hypot(qx, qy, qz);
|
|
1466
|
+
const radius = f.radius ?? 0;
|
|
1467
|
+
const w = radius > 0 ? Math.max(1 - r / radius, 0) : 1;
|
|
1468
|
+
if (r > 1e-4) {
|
|
1469
|
+
const s = (f.strength * w) / r;
|
|
1470
|
+
ax += (ny * qz - nz * qy) * s;
|
|
1471
|
+
ay += (nz * qx - nx * qz) * s;
|
|
1472
|
+
az += (nx * qy - ny * qx) * s;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
else if (f.kind === 'turbulence') {
|
|
1476
|
+
const e = 0.05;
|
|
1477
|
+
const s = (f.seed ?? 0) >>> 0;
|
|
1478
|
+
const x = px * f.frequency;
|
|
1479
|
+
const y = py * f.frequency;
|
|
1480
|
+
const z = pz * f.frequency;
|
|
1481
|
+
const dzdy = vnoise(x, y + e, z, s + 2) - vnoise(x, y - e, z, s + 2);
|
|
1482
|
+
const dydz = vnoise(x, y, z + e, s + 1) - vnoise(x, y, z - e, s + 1);
|
|
1483
|
+
const dxdz = vnoise(x, y, z + e, s) - vnoise(x, y, z - e, s);
|
|
1484
|
+
const dzdx = vnoise(x + e, y, z, s + 2) - vnoise(x - e, y, z, s + 2);
|
|
1485
|
+
const dydx = vnoise(x + e, y, z, s + 1) - vnoise(x - e, y, z, s + 1);
|
|
1486
|
+
const dxdy = vnoise(x, y + e, z, s) - vnoise(x, y - e, z, s);
|
|
1487
|
+
const k = f.strength / (2 * e);
|
|
1488
|
+
ax += (dzdy - dydz) * k;
|
|
1489
|
+
ay += (dxdz - dzdx) * k;
|
|
1490
|
+
az += (dydx - dxdy) * k;
|
|
1491
|
+
}
|
|
1492
|
+
else {
|
|
1493
|
+
drag += f.amount;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
const damp = Math.exp(-drag * dt);
|
|
1497
|
+
vel[0] = (vel[0] + ax * dt) * damp;
|
|
1498
|
+
vel[1] = (vel[1] + ay * dt) * damp;
|
|
1499
|
+
if (vel.length > 2)
|
|
1500
|
+
vel[2] = (vel[2] + az * dt) * damp;
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* Continuous emission at `rate` particles/second, plus bursts. Mutate `params` freely. With
|
|
1504
|
+
* `random` the emitter's spawn seeds come from that stream instead of the system's (ws/fx-4).
|
|
1505
|
+
*/
|
|
1506
|
+
export class ParticleEmitter {
|
|
1507
|
+
system;
|
|
1508
|
+
params;
|
|
1509
|
+
rate;
|
|
1510
|
+
active = true;
|
|
1511
|
+
/** Live: the stream this emitter's emits draw from; null uses the system's `random`. */
|
|
1512
|
+
random;
|
|
1513
|
+
carry = 0;
|
|
1514
|
+
constructor(system, params, rate = 0, random) {
|
|
1515
|
+
this.system = system;
|
|
1516
|
+
this.params = params;
|
|
1517
|
+
this.rate = rate;
|
|
1518
|
+
this.random = random ?? null;
|
|
1519
|
+
}
|
|
1520
|
+
update(dt) {
|
|
1521
|
+
if (!this.active || this.rate <= 0)
|
|
1522
|
+
return;
|
|
1523
|
+
this.carry += this.rate * dt;
|
|
1524
|
+
const n = Math.floor(this.carry);
|
|
1525
|
+
if (n > 0) {
|
|
1526
|
+
this.carry -= n;
|
|
1527
|
+
this.system.emit(this.params, n, this.random ?? this.system.random);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
burst(count, overrides) {
|
|
1531
|
+
this.system.emit(overrides ? { ...this.params, ...overrides } : this.params, count, this.random ?? this.system.random);
|
|
1532
|
+
}
|
|
1533
|
+
}
|