@woosh/meep-engine 3.41.0 → 3.43.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/build/bundle-worker-image-decoder.js +1 -1
- package/build/bundle-worker-terrain.js +1 -1
- package/editor/selection/editor_pick.d.ts.map +1 -1
- package/editor/selection/editor_pick.js +2 -1
- package/editor/tools/TransformToolV2.d.ts.map +1 -1
- package/editor/tools/TransformToolV2.js +7 -1
- package/editor/tools/engine/ToolEngine.d.ts.map +1 -1
- package/editor/tools/engine/ToolEngine.js +10 -0
- package/editor/tools/v2/TransformControls.d.ts.map +1 -1
- package/editor/tools/v2/TransformControls.js +7 -20
- package/editor/tools/v2/handle/gizmo_drag_plane.d.ts.map +1 -1
- package/editor/tools/v2/handle/gizmo_drag_plane.js +4 -0
- package/editor/view/ecs/EntityEditor.d.ts.map +1 -1
- package/editor/view/ecs/EntityEditor.js +3 -1
- package/editor/view/node-graph/NodeGraphView.d.ts.map +1 -1
- package/editor/view/node-graph/NodeGraphView.js +28 -12
- package/package.json +91 -96
- package/samples/engine/first-scene/index.html +4 -1
- package/samples/engine/first-scene/main.js +14 -9
- package/src/core/binary/utf8/utf8_decode.js +3 -3
- package/src/core/bvh2/binary/2/bvh32_query_user_data_ray.d.ts.map +1 -1
- package/src/core/bvh2/binary/2/bvh32_query_user_data_ray.js +112 -113
- package/src/core/bvh2/bvh3/ebvh_nodes_sort_sah_local4.js +2 -2
- package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +14 -14
- package/src/core/collection/RingBuffer.d.ts.map +1 -1
- package/src/core/collection/RingBuffer.js +4 -0
- package/src/core/collection/array/array_contains_duplicates.d.ts.map +1 -1
- package/src/core/collection/array/array_contains_duplicates.js +2 -1
- package/src/core/collection/array/array_copy.d.ts.map +1 -1
- package/src/core/collection/array/array_copy.js +23 -0
- package/src/core/collection/array/typed/sparse_typed_array_hash.d.ts +2 -2
- package/src/core/collection/array/typed/sparse_typed_array_hash.d.ts.map +1 -1
- package/src/core/collection/array/typed/sparse_typed_array_hash.js +9 -8
- package/src/core/collection/array/typed/uint16_array_hash.d.ts +2 -2
- package/src/core/collection/array/typed/uint16_array_hash.js +4 -4
- package/src/core/collection/array/typed/uint8_array_hash.d.ts +2 -2
- package/src/core/collection/array/typed/uint8_array_hash.js +4 -4
- package/src/core/collection/array/weightedRandomFromArray.d.ts +1 -0
- package/src/core/collection/array/weightedRandomFromArray.d.ts.map +1 -1
- package/src/core/collection/array/weightedRandomFromArray.js +9 -1
- package/src/core/collection/heap/BinaryHeap.d.ts.map +1 -1
- package/src/core/collection/heap/BinaryHeap.js +4 -0
- package/src/core/collection/list/List.d.ts.map +1 -1
- package/src/core/collection/list/List.js +4 -2
- package/src/core/collection/set/ArraySet.js +1 -1
- package/src/core/collection/set/HashSet.d.ts +1 -1
- package/src/core/collection/set/HashSet.d.ts.map +1 -1
- package/src/core/collection/set/HashSet.js +4 -2
- package/src/core/debug/matchers/IsEqual.js +1 -1
- package/src/core/debug/matchers/IsIterableContaining.d.ts.map +1 -1
- package/src/core/debug/matchers/IsIterableContaining.js +61 -74
- package/src/core/events/signal/SignalBinding.js +101 -101
- package/src/core/function/extractFunctionBody.js +13 -13
- package/src/core/function/objectsEqual.d.ts.map +1 -1
- package/src/core/function/objectsEqual.js +15 -14
- package/src/core/geom/3d/frustum/frustum3_compute_closest_point.d.ts.map +1 -1
- package/src/core/geom/3d/frustum/frustum3_compute_closest_point.js +207 -172
- package/src/core/geom/3d/gjk/gjk_epa_penetration.d.ts.map +1 -1
- package/src/core/geom/3d/gjk/gjk_epa_penetration.js +5 -4
- package/src/core/geom/3d/line/line3_compute_segment_point_distance_eikonal.d.ts.map +1 -1
- package/src/core/geom/3d/line/line3_compute_segment_point_distance_eikonal.js +8 -2
- package/src/core/geom/3d/triangle/tri3_ray_intersection.d.ts +20 -0
- package/src/core/geom/3d/triangle/tri3_ray_intersection.d.ts.map +1 -1
- package/src/core/geom/3d/triangle/tri3_ray_intersection.js +182 -0
- package/src/core/math/noise/curl_noise_3dt.d.ts.map +1 -1
- package/src/core/math/noise/curl_noise_3dt.js +20 -10
- package/src/core/math/noise/sdnoise.d.ts +16 -5
- package/src/core/math/noise/sdnoise.d.ts.map +1 -1
- package/src/core/math/noise/sdnoise.js +43 -9
- package/src/core/math/spline/spline3_hermite_nearest_point.d.ts +16 -1
- package/src/core/math/spline/spline3_hermite_nearest_point.d.ts.map +1 -1
- package/src/core/math/spline/spline3_hermite_nearest_point.js +44 -3
- package/src/engine/Engine.d.ts.map +1 -1
- package/src/engine/Engine.js +6 -2
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +9 -0
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +26 -0
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +2 -0
- package/src/engine/control/first-person/abilities/AbilitySet.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/AbilitySet.js +9 -0
- package/src/engine/control/first-person/abilities/LedgeGrab.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/LedgeGrab.js +6 -0
- package/src/engine/control/first-person/abilities/Slide.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/Slide.js +1 -0
- package/src/engine/control/first-person/abilities/WallRun.d.ts +4 -1
- package/src/engine/control/first-person/abilities/WallRun.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/WallRun.js +13 -1
- package/src/engine/control/first-person/mastery/SlideInitiationTimingEvaluator.d.ts +1 -1
- package/src/engine/control/first-person/mastery/SlideInitiationTimingEvaluator.d.ts.map +1 -1
- package/src/engine/control/first-person/mastery/SlideInitiationTimingEvaluator.js +6 -5
- package/src/engine/control/first-person/mastery/StrideTimingJumpEvaluator.d.ts +3 -3
- package/src/engine/control/first-person/mastery/StrideTimingJumpEvaluator.d.ts.map +1 -1
- package/src/engine/control/first-person/mastery/StrideTimingJumpEvaluator.js +9 -8
- package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.d.ts.map +1 -1
- package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.js +13 -4
- package/src/engine/ecs/EntityManager.d.ts.map +1 -1
- package/src/engine/ecs/EntityManager.js +18 -15
- package/src/engine/ecs/dynamic_actions/rules/DynamicRuleDescription.d.ts.map +1 -1
- package/src/engine/ecs/dynamic_actions/rules/DynamicRuleDescription.js +2 -1
- package/src/engine/ecs/dynamic_actions/rules/DynamicRuleDescriptionTable.d.ts.map +1 -1
- package/src/engine/ecs/dynamic_actions/rules/DynamicRuleDescriptionTable.js +1 -0
- package/src/engine/ecs/fow/FogOfWarRevealerSystem.d.ts.map +1 -1
- package/src/engine/ecs/fow/FogOfWarRevealerSystem.js +3 -0
- package/src/engine/ecs/parent/EntityNode.js +3 -3
- package/src/engine/ecs/speaker/VoiceSystem.d.ts.map +1 -1
- package/src/engine/ecs/speaker/VoiceSystem.js +4 -0
- package/src/engine/ecs/storage/BinaryBufferDeSerializer.js +5 -5
- package/src/engine/ecs/storage/binary/BinarySerializationRegistry.d.ts.map +1 -1
- package/src/engine/ecs/storage/binary/BinarySerializationRegistry.js +3 -2
- package/src/engine/ecs/storage/binary/object/BinaryObjectSerializationAdapter2.js +1 -1
- package/src/engine/ecs/storage/binary/object-v2/BinaryObjectDeSerializer.d.ts.map +1 -1
- package/src/engine/ecs/storage/binary/object-v2/BinaryObjectDeSerializer.js +8 -1
- package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
- package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +8 -0
- package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.d.ts.map +1 -1
- package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.js +5 -2
- package/src/engine/ecs/terrain/ecs/layers/TerrainLayers.d.ts.map +1 -1
- package/src/engine/ecs/terrain/ecs/layers/TerrainLayers.js +3 -0
- package/src/engine/ecs/terrain/ecs/splat/SplatMapping.d.ts.map +1 -1
- package/src/engine/ecs/terrain/ecs/splat/SplatMapping.js +1 -0
- package/src/engine/ecs/terrain/overlay/TerrainOverlay.js +1 -1
- package/src/engine/ecs/terrain/tiles/TerrainTile.d.ts.map +1 -1
- package/src/engine/ecs/terrain/tiles/TerrainTile.js +13 -1
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts.map +1 -1
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.js +1 -0
- package/src/engine/graphics/ecs/light/LightSerializationAdapter.d.ts.map +1 -1
- package/src/engine/graphics/ecs/light/LightSerializationAdapter.js +13 -1
- package/src/engine/graphics/ecs/light/LightSerializationUpgrader_0_1.d.ts +5 -0
- package/src/engine/graphics/ecs/light/LightSerializationUpgrader_0_1.d.ts.map +1 -0
- package/src/engine/graphics/ecs/light/LightSerializationUpgrader_0_1.js +27 -0
- package/src/engine/graphics/ecs/water/Water.d.ts +11 -0
- package/src/engine/graphics/ecs/water/Water.d.ts.map +1 -1
- package/src/engine/graphics/ecs/water/Water.js +17 -0
- package/src/engine/graphics/ecs/water/WaterSerializationAdapter.d.ts.map +1 -1
- package/src/engine/graphics/ecs/water/WaterSerializationAdapter.js +3 -1
- package/src/engine/graphics/ecs/water/WaterSerializationUpgrader_4_5.d.ts +9 -0
- package/src/engine/graphics/ecs/water/WaterSerializationUpgrader_4_5.d.ts.map +1 -0
- package/src/engine/graphics/ecs/water/WaterSerializationUpgrader_4_5.js +24 -0
- package/src/engine/graphics/ecs/water/WaterWaves.d.ts +30 -9
- package/src/engine/graphics/ecs/water/WaterWaves.d.ts.map +1 -1
- package/src/engine/graphics/ecs/water/WaterWaves.js +41 -9
- package/src/engine/graphics/ecs/water/WaterWavesSerializationAdapter.d.ts.map +1 -1
- package/src/engine/graphics/ecs/water/WaterWavesSerializationAdapter.js +30 -26
- package/src/engine/graphics/ecs/water/WaterWavesSerializationUpgrader_0_1.d.ts +6 -0
- package/src/engine/graphics/ecs/water/WaterWavesSerializationUpgrader_0_1.d.ts.map +1 -0
- package/src/engine/graphics/ecs/water/WaterWavesSerializationUpgrader_0_1.js +22 -0
- package/src/engine/graphics/geometry/buffered/query/bvh32_geometry_raycast.d.ts.map +1 -1
- package/src/engine/graphics/geometry/buffered/query/bvh32_geometry_raycast.js +3 -5
- package/src/engine/graphics/texture/3d/scs3d_sample_linear3.d.ts +20 -0
- package/src/engine/graphics/texture/3d/scs3d_sample_linear3.d.ts.map +1 -1
- package/src/engine/graphics/texture/3d/scs3d_sample_linear3.js +36 -0
- package/src/engine/graphics3/WaterSystem.d.ts +12 -3
- package/src/engine/graphics3/WaterSystem.d.ts.map +1 -1
- package/src/engine/graphics3/WaterSystem.js +78 -46
- package/src/engine/input/devices/PointerDevice.d.ts.map +1 -1
- package/src/engine/input/devices/PointerDevice.js +29 -4
- package/src/engine/intelligence/behavior/selector/WeightedRandomBehavior.d.ts +1 -0
- package/src/engine/intelligence/behavior/selector/WeightedRandomBehavior.d.ts.map +1 -1
- package/src/engine/intelligence/behavior/selector/WeightedRandomBehavior.js +14 -3
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/engine/physics/fluid/solver/v3_grid_advect_maccormack_velocity.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_advect_maccormack_velocity.js +12 -3
- package/src/engine/physics/fluid/solver/v3_grid_advect_sl_velocity.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_advect_sl_velocity.js +8 -2
- package/src/engine/physics/fluid/solver/v3_grid_sample_scalar_masked.d.ts +19 -0
- package/src/engine/physics/fluid/solver/v3_grid_sample_scalar_masked.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_sample_scalar_masked.js +41 -2
- package/src/engine/physics/fluid/solver/v3_mac_advect_scalar.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_mac_advect_scalar.js +9 -3
- package/src/engine/physics/fluid/solver/v3_mac_advect_sl_velocity.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_mac_advect_sl_velocity.js +26 -10
- package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.d.ts +3 -6
- package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.js +22 -9
- package/src/engine/ui/DraggableAspect.d.ts +5 -1
- package/src/engine/ui/DraggableAspect.d.ts.map +1 -1
- package/src/engine/ui/DraggableAspect.js +61 -4
- package/src/format/image/avif/heif/ItemProperty.d.ts +1 -1
- package/src/format/image/avif/heif/ItemProperty.js +1 -1
- package/src/format/image/png/chunk/png_chunk_decode_iTXt.d.ts.map +1 -1
- package/src/format/image/png/chunk/png_chunk_decode_iTXt.js +17 -6
- package/src/shade/device/mock/SoftwareGPUDevice.d.ts +2 -4
- package/src/shade/device/mock/SoftwareGPUDevice.d.ts.map +1 -1
- package/src/shade/device/mock/SoftwareGPUDevice.js +20 -13
- package/src/shade/playground/skinned_blas_refit/sweep_views.d.ts.map +1 -1
- package/src/shade/playground/skinned_blas_refit/sweep_views.js +2 -1
- package/src/shade/playground/vgeo_runtime/README.md +5 -4
- package/src/shade/playground/water_refraction/README.md +10 -11
- package/src/shade/playground/water_refraction/main.js +9 -8
- package/src/shade/playground/water_refraction/world.js +1 -1
- package/src/shade/playground/water_surface/README.md +43 -0
- package/src/shade/playground/water_surface/index.html +30 -0
- package/src/shade/playground/water_surface/main.d.ts +2 -0
- package/src/shade/playground/water_surface/main.d.ts.map +1 -0
- package/src/shade/playground/water_surface/main.js +154 -0
- package/src/shade/playground/water_surface/world.d.ts +44 -0
- package/src/shade/playground/water_surface/world.d.ts.map +1 -0
- package/src/shade/playground/water_surface/world.js +160 -0
- package/src/shade/renderer/Renderer.d.ts.map +1 -1
- package/src/shade/renderer/Renderer.js +6 -5
- package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.d.ts.map +1 -1
- package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.js +5 -0
- package/src/shade/renderer/atmosphere/GPUSky.d.ts +12 -0
- package/src/shade/renderer/atmosphere/GPUSky.d.ts.map +1 -1
- package/src/shade/renderer/atmosphere/GPUSky.js +162 -147
- package/src/shade/renderer/binding/BufferBinding.js +2 -2
- package/src/shade/renderer/buffer/table/GPUTypedTable.d.ts.map +1 -1
- package/src/shade/renderer/buffer/table/GPUTypedTable.js +5 -3
- package/src/shade/renderer/buffer/table/GPUTypedTableDescriptor.d.ts +10 -3
- package/src/shade/renderer/buffer/table/GPUTypedTableDescriptor.d.ts.map +1 -1
- package/src/shade/renderer/buffer/table/GPUTypedTableDescriptor.js +17 -9
- package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts +3 -0
- package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts.map +1 -1
- package/src/shade/renderer/camera/orbital/OrbitalCameraInput.js +16 -0
- package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts +2 -0
- package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts.map +1 -1
- package/src/shade/renderer/camera/orbital/OrbitalCameraRig.js +11 -0
- package/src/shade/renderer/geometry/meshlet/build/meshoptimizer/geometry_to_meshlets_meshoptimizer.d.ts +1 -1
- package/src/shade/renderer/geometry/meshlet/build/meshoptimizer/geometry_to_meshlets_meshoptimizer.d.ts.map +1 -1
- package/src/shade/renderer/geometry/meshlet/build/meshoptimizer/geometry_to_meshlets_meshoptimizer.js +9 -3
- package/src/shade/renderer/geometry/util/geometry_simplify_redundant_topology.d.ts.map +1 -1
- package/src/shade/renderer/geometry/util/geometry_simplify_redundant_topology.js +2 -2
- package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.js +85 -0
- package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.js +12 -6
- package/src/shade/renderer/geometry/virtual/runtime/VGeoPageFetcher.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/runtime/VGeoPageFetcher.js +7 -0
- package/src/shade/renderer/geometry/virtual/runtime/VGeoRuntimeTables.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/runtime/VGeoRuntimeTables.js +3 -1
- package/src/shade/renderer/lightmap/bake/shader_lightmap_denoise_sh.d.ts.map +1 -1
- package/src/shade/renderer/lightmap/bake/shader_lightmap_denoise_sh.js +10 -3
- package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
- package/src/shade/renderer/particles/DESIGN.md +13 -9
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts +1 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting.js +1 -1
- package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
- package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.d.ts +3 -2
- package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.js +3 -2
- package/src/shade/renderer/scene/GPUSceneContext.d.ts +9 -1
- package/src/shade/renderer/scene/GPUSceneContext.d.ts.map +1 -1
- package/src/shade/renderer/scene/GPUSceneContext.js +23 -1
- package/src/shade/renderer/scene/SCENE_DATABASE_SPEC.js +2 -1
- package/src/shade/renderer/shader/chunk/math/fft/chunk_fft128_inverse.d.ts +13 -0
- package/src/shade/renderer/shader/chunk/math/fft/chunk_fft128_inverse.d.ts.map +1 -0
- package/src/shade/renderer/shader/chunk/math/fft/chunk_fft128_inverse.js +42 -0
- package/src/shade/renderer/shader/chunk/math/fft/chunk_fft128x4_inverse_workgroup.d.ts +15 -0
- package/src/shade/renderer/shader/chunk/math/fft/chunk_fft128x4_inverse_workgroup.d.ts.map +1 -0
- package/src/shade/renderer/shader/chunk/math/fft/chunk_fft128x4_inverse_workgroup.js +39 -0
- package/src/shade/renderer/shader/resource/ShaderResourceSetDescriptor.d.ts.map +1 -1
- package/src/shade/renderer/shader/resource/ShaderResourceSetDescriptor.js +2 -0
- package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts +4 -1
- package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +74 -3
- package/src/shade/renderer/volumetrics/NOTES.md +1023 -957
- package/src/shade/renderer/volumetrics/chunk_volumetrics_irradiance_sh.d.ts +5 -4
- package/src/shade/renderer/volumetrics/chunk_volumetrics_irradiance_sh.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/chunk_volumetrics_irradiance_sh.js +5 -4
- package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi.d.ts +1 -1
- package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi.js +1 -1
- package/src/shade/renderer/volumetrics/gi/graph_build_froxel_gi.d.ts +1 -1
- package/src/shade/renderer/volumetrics/gi/graph_build_froxel_gi.js +1 -1
- package/src/shade/renderer/volumetrics/graph_build_froxel_irradiance.d.ts +41 -0
- package/src/shade/renderer/volumetrics/graph_build_froxel_irradiance.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/graph_build_froxel_irradiance.js +112 -0
- package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts +9 -23
- package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +18 -126
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_irradiance.d.ts +22 -0
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_irradiance.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_irradiance.js +228 -0
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js +0 -53
- package/src/shade/renderer/water/GPUWaterRenderer.d.ts +15 -0
- package/src/shade/renderer/water/GPUWaterRenderer.d.ts.map +1 -1
- package/src/shade/renderer/water/GPUWaterRenderer.js +110 -30
- package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts +16 -8
- package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts.map +1 -1
- package/src/shade/renderer/water/medium/GPUWaterBodies.js +31 -26
- package/src/shade/renderer/water/medium/shader_water_meshlets.d.ts +0 -1
- package/src/shade/renderer/water/medium/shader_water_meshlets.d.ts.map +1 -1
- package/src/shade/renderer/water/medium/shader_water_meshlets.js +1 -39
- package/src/shade/renderer/water/shader_water.d.ts +0 -1
- package/src/shade/renderer/water/shader_water.d.ts.map +1 -1
- package/src/shade/renderer/water/shader_water.js +201 -72
- package/src/shade/renderer/water/surface/GPUWaterSurface.d.ts +70 -0
- package/src/shade/renderer/water/surface/GPUWaterSurface.d.ts.map +1 -0
- package/src/shade/renderer/water/surface/GPUWaterSurface.js +199 -0
- package/src/shade/renderer/water/surface/WATER_WAVE_RECORD_STRUCT.d.ts +5 -2
- package/src/shade/renderer/water/surface/WATER_WAVE_RECORD_STRUCT.d.ts.map +1 -1
- package/src/shade/renderer/water/surface/WATER_WAVE_RECORD_STRUCT.js +32 -12
- package/src/shade/renderer/water/surface/chunk_water_surface_sample.d.ts +23 -0
- package/src/shade/renderer/water/surface/chunk_water_surface_sample.d.ts.map +1 -0
- package/src/shade/renderer/water/surface/chunk_water_surface_sample.js +227 -0
- package/src/shade/renderer/water/surface/graph_water_tessellation.d.ts +65 -11
- package/src/shade/renderer/water/surface/graph_water_tessellation.d.ts.map +1 -1
- package/src/shade/renderer/water/surface/graph_water_tessellation.js +163 -28
- package/src/shade/renderer/water/surface/lod/WATER_LOD.d.ts +51 -0
- package/src/shade/renderer/water/surface/lod/WATER_LOD.d.ts.map +1 -0
- package/src/shade/renderer/water/surface/lod/WATER_LOD.js +95 -0
- package/src/shade/renderer/water/surface/lod/chunk_water_lod.d.ts +10 -0
- package/src/shade/renderer/water/surface/lod/chunk_water_lod.d.ts.map +1 -0
- package/src/shade/renderer/water/surface/lod/chunk_water_lod.js +127 -0
- package/src/shade/renderer/water/surface/lod/chunk_water_lod_cell.d.ts +17 -0
- package/src/shade/renderer/water/surface/lod/chunk_water_lod_cell.d.ts.map +1 -0
- package/src/shade/renderer/water/surface/lod/chunk_water_lod_cell.js +246 -0
- package/src/shade/renderer/water/surface/lod/shader_water_lod.d.ts +118 -0
- package/src/shade/renderer/water/surface/lod/shader_water_lod.d.ts.map +1 -0
- package/src/shade/renderer/water/surface/lod/shader_water_lod.js +727 -0
- package/src/shade/renderer/water/surface/particles/GPUWaterParticles.d.ts +60 -0
- package/src/shade/renderer/water/surface/particles/GPUWaterParticles.d.ts.map +1 -0
- package/src/shade/renderer/water/surface/particles/GPUWaterParticles.js +274 -0
- package/src/shade/renderer/water/surface/particles/WATER_PARTICLE_STRUCT.d.ts +17 -0
- package/src/shade/renderer/water/surface/particles/WATER_PARTICLE_STRUCT.d.ts.map +1 -0
- package/src/shade/renderer/water/surface/particles/WATER_PARTICLE_STRUCT.js +57 -0
- package/src/shade/renderer/water/surface/particles/chunk_water_particle.d.ts +43 -0
- package/src/shade/renderer/water/surface/particles/chunk_water_particle.d.ts.map +1 -0
- package/src/shade/renderer/water/surface/particles/chunk_water_particle.js +268 -0
- package/src/shade/renderer/water/surface/particles/chunk_water_particle_step.d.ts +20 -0
- package/src/shade/renderer/water/surface/particles/chunk_water_particle_step.d.ts.map +1 -0
- package/src/shade/renderer/water/surface/particles/chunk_water_particle_step.js +239 -0
- package/src/shade/renderer/water/surface/particles/shader_water_particles.d.ts +153 -0
- package/src/shade/renderer/water/surface/particles/shader_water_particles.d.ts.map +1 -0
- package/src/shade/renderer/water/surface/particles/shader_water_particles.js +874 -0
- package/src/shade/renderer/water/surface/shader_water_cull.d.ts +42 -0
- package/src/shade/renderer/water/surface/shader_water_cull.d.ts.map +1 -0
- package/src/shade/renderer/water/surface/shader_water_cull.js +269 -0
- package/src/shade/renderer/water/surface/shader_water_waves_combine.d.ts +15 -0
- package/src/shade/renderer/water/surface/shader_water_waves_combine.d.ts.map +1 -0
- package/src/shade/renderer/water/surface/shader_water_waves_combine.js +73 -0
- package/src/shade/renderer/water/surface/shader_water_waves_derive.d.ts +11 -0
- package/src/shade/renderer/water/surface/shader_water_waves_derive.d.ts.map +1 -0
- package/src/shade/renderer/water/surface/shader_water_waves_derive.js +78 -0
- package/src/shade/renderer/water/surface/spectrum/GPUWaterSpectrum.d.ts +35 -0
- package/src/shade/renderer/water/surface/spectrum/GPUWaterSpectrum.d.ts.map +1 -0
- package/src/shade/renderer/water/surface/spectrum/GPUWaterSpectrum.js +103 -0
- package/src/shade/renderer/water/surface/spectrum/WATER_BAND_STATE.d.ts +22 -0
- package/src/shade/renderer/water/surface/spectrum/WATER_BAND_STATE.d.ts.map +1 -0
- package/src/shade/renderer/water/surface/spectrum/WATER_BAND_STATE.js +57 -0
- package/src/shade/renderer/water/surface/spectrum/WATER_SPECTRUM.d.ts +22 -0
- package/src/shade/renderer/water/surface/spectrum/WATER_SPECTRUM.d.ts.map +1 -0
- package/src/shade/renderer/water/surface/spectrum/WATER_SPECTRUM.js +25 -0
- package/src/shade/renderer/water/surface/spectrum/chunk_water_jonswap.d.ts +14 -0
- package/src/shade/renderer/water/surface/spectrum/chunk_water_jonswap.d.ts.map +1 -0
- package/src/shade/renderer/water/surface/spectrum/chunk_water_jonswap.js +48 -0
- package/src/shade/renderer/water/surface/spectrum/chunk_water_spectrum.d.ts +23 -0
- package/src/shade/renderer/water/surface/spectrum/chunk_water_spectrum.d.ts.map +1 -0
- package/src/shade/renderer/water/surface/spectrum/chunk_water_spectrum.js +169 -0
- package/src/shade/renderer/water/surface/spectrum/shader_water_spectrum.d.ts +26 -0
- package/src/shade/renderer/water/surface/spectrum/shader_water_spectrum.d.ts.map +1 -0
- package/src/shade/renderer/water/surface/spectrum/shader_water_spectrum.js +161 -0
- package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.d.ts.map +1 -1
- package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +29 -9
- package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
- package/src/shade/wgsl/emulator/WGSLLanguageCore.js +14 -3
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts +0 -28
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts.map +0 -1
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.js +0 -86
- package/src/engine/ecs/storage/binary/object-v2/serializerTestHarness.d.ts +0 -29
- package/src/engine/ecs/storage/binary/object-v2/serializerTestHarness.d.ts.map +0 -1
- package/src/engine/ecs/storage/binary/object-v2/serializerTestHarness.js +0 -44
- package/src/shade/renderer/water/medium/shader_water_bodies_derive.d.ts +0 -4
- package/src/shade/renderer/water/medium/shader_water_bodies_derive.d.ts.map +0 -1
- package/src/shade/renderer/water/medium/shader_water_bodies_derive.js +0 -32
- package/src/shade/renderer/water/surface/GPUWaterWaveSimulation.d.ts +0 -25
- package/src/shade/renderer/water/surface/GPUWaterWaveSimulation.d.ts.map +0 -1
- package/src/shade/renderer/water/surface/GPUWaterWaveSimulation.js +0 -128
- package/src/shade/renderer/water/surface/chunk_water_tessellation.d.ts +0 -9
- package/src/shade/renderer/water/surface/chunk_water_tessellation.d.ts.map +0 -1
- package/src/shade/renderer/water/surface/chunk_water_tessellation.js +0 -144
- package/src/shade/renderer/water/surface/chunk_water_wave.d.ts +0 -13
- package/src/shade/renderer/water/surface/chunk_water_wave.d.ts.map +0 -1
- package/src/shade/renderer/water/surface/chunk_water_wave.js +0 -40
- package/src/shade/renderer/water/surface/shader_water_tiles.d.ts +0 -4
- package/src/shade/renderer/water/surface/shader_water_tiles.d.ts.map +0 -1
- package/src/shade/renderer/water/surface/shader_water_tiles.js +0 -123
- package/src/shade/renderer/water/surface/shader_water_wave_batch.d.ts +0 -6
- package/src/shade/renderer/water/surface/shader_water_wave_batch.d.ts.map +0 -1
- package/src/shade/renderer/water/surface/shader_water_wave_batch.js +0 -85
- package/src/shade/renderer/water/surface/shader_water_wave_domain.d.ts +0 -6
- package/src/shade/renderer/water/surface/shader_water_wave_domain.d.ts.map +0 -1
- package/src/shade/renderer/water/surface/shader_water_wave_domain.js +0 -83
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
import { WebGPUExtensionType } from "../../../../descriptor/WebGPUExtensionType.js";
|
|
2
|
+
import { ComputeShader } from "../../../shader/ComputeShader.js";
|
|
3
|
+
import { CodeChunk } from "../../../shader/compiler/CodeChunk.js";
|
|
4
|
+
import { ShaderResourceSetDescriptor } from "../../../shader/resource/ShaderResourceSetDescriptor.js";
|
|
5
|
+
import { WebGPUArray } from "../../../shader/type/WebGPUArray.js";
|
|
6
|
+
import { WebGPUStruct } from "../../../shader/type/WebGPUStruct.js";
|
|
7
|
+
import { WGSLAttribute } from "../../../shader/type/WGSLAttribute.js";
|
|
8
|
+
import { WGSLAttributes } from "../../../shader/type/WGSLAttributes.js";
|
|
9
|
+
import { WGSLStructField } from "../../../shader/type/WGSLStructField.js";
|
|
10
|
+
import { atomic_u32, f32, u32 } from "../../../shader/type/WEBGPU_WGSL_PRIMITIVE_TYPES.js";
|
|
11
|
+
import { COLLECTION_U32_STRUCT_READ } from "../../../gpu_primitive/COLLECTION_U32_STRUCT_READ.js";
|
|
12
|
+
import { STRUCT_DISPATCH_COMPUTE_COMMAND } from "../../../gpu_primitive/indirect/STRUCT_DISPATCH_COMPUTE_COMMAND.js";
|
|
13
|
+
import { GEOMETRY_METADATA_STRUCT } from "../../../geometry/GEOMETRY_METADATA_STRUCT.js";
|
|
14
|
+
import { MESHLET_METADATA_STRUCT } from "../../../geometry/meshlet/encoding/MESHLET_METADATA_STRUCT.js";
|
|
15
|
+
import { BVH2_NODE_STRUCT } from "../../../shader/chunk/bvh/BVH2_NODE_STRUCT.js";
|
|
16
|
+
import { chunk_folded_workgroup_index } from "../../../shader/chunk/grid/chunk_folded_workgroup_index.js";
|
|
17
|
+
import { chunk_scene_database_access, chunk_scene_database_access_rw } from "../../../scene/chunk_scene_database_access.js";
|
|
18
|
+
import { WaterActivity } from "../shader_water_cull.js";
|
|
19
|
+
import { WATER_WAVES_TABLE as waves } from "../../medium/chunk_water_bodies.js";
|
|
20
|
+
import { WATER_PARTICLE_BODY_BOUNDS_STRIDE } from "../shader_water_waves_combine.js";
|
|
21
|
+
import { WATER_PARTICLE_ENTRY_STRUCT, WATER_PARTICLE_STRUCT } from "./WATER_PARTICLE_STRUCT.js";
|
|
22
|
+
import { WATER_WAVE_IMPULSE_LIMIT } from "../WATER_WAVE_RECORD_STRUCT.js";
|
|
23
|
+
import { chunk_water_particle } from "./chunk_water_particle.js";
|
|
24
|
+
import { chunk_water_particle_step } from "./chunk_water_particle_step.js";
|
|
25
|
+
import { WATER_PARTICLE_GATHER_BUDGET } from "../chunk_water_surface_sample.js";
|
|
26
|
+
|
|
27
|
+
/** A particle array with its live count in front, as the indirect commands read it. */
|
|
28
|
+
export const WATER_PARTICLE_ARRAY_STRUCT = WebGPUStruct.fromFields(
|
|
29
|
+
WGSLStructField.from("count", u32),
|
|
30
|
+
WGSLStructField.from("elements", WebGPUArray.from(WATER_PARTICLE_STRUCT), WGSLAttribute.from(WGSLAttributes.Align, 16)),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
/** The same array where emitters append by an atomic count. */
|
|
34
|
+
export const WATER_PARTICLE_ARRAY_APPEND_STRUCT = WebGPUStruct.fromFields(
|
|
35
|
+
WGSLStructField.from("count", atomic_u32),
|
|
36
|
+
WGSLStructField.from("elements", WebGPUArray.from(WATER_PARTICLE_STRUCT), WGSLAttribute.from(WGSLAttributes.Align, 16)),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
/** Bucket counts while binning: the table size in front, atomic counters behind. */
|
|
40
|
+
export const WATER_PARTICLE_BUCKETS_STRUCT = WebGPUStruct.fromFields(
|
|
41
|
+
WGSLStructField.from("count", u32),
|
|
42
|
+
WGSLStructField.from("elements", WebGPUArray.from(atomic_u32), WGSLAttribute.from(WGSLAttributes.Align, 16)),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
/** Smallest hash table, so an idle frame scans little. */
|
|
46
|
+
export const WATER_PARTICLE_TABLE_MIN = 1024;
|
|
47
|
+
|
|
48
|
+
/** Bucket entries one particle can take: the 2×2 cells its support overlaps. */
|
|
49
|
+
export const WATER_PARTICLE_CORNERS = 4;
|
|
50
|
+
|
|
51
|
+
/** Buckets ordered by particle; a crowded bucket past this keeps its arrival order. */
|
|
52
|
+
export const WATER_PARTICLE_RANK_LIMIT = 64;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Packets a crowded point keeps. A point reads a cell of every level its body has, and a cell is crowded where its most
|
|
56
|
+
* crowded point reads more than this: a point of it at its level and the coarser ones, or one under it where finer
|
|
57
|
+
* levels hold packets too. Then each run of a front in it is drawn by its packets' least share of their cells: this
|
|
58
|
+
* many times the packet's steepness over the summed steepness that point reads, whatever the levels it is read at. So
|
|
59
|
+
* about this many stay at a point, the steepest whole wherever they sit, and a front stays whole or goes. What is
|
|
60
|
+
* elsewhere on the body does not count, so a splash far off, or a front moving to a coarser level, changes nothing here.
|
|
61
|
+
*
|
|
62
|
+
* A front's lot is its own for life and the same in every cell it reaches, so the fronts of the lowest lots are kept in
|
|
63
|
+
* every crowd they are in, whatever their size: the count at a point varies by fronts' worth around this, and a front
|
|
64
|
+
* the outline broke is drawn particle by particle, ~20 entries in one cell. Where a point would read more than the
|
|
65
|
+
* gather budget, its cells keep no more of it than their steepness's part, the lowest keys first
|
|
66
|
+
* ({@link WATER_PARTICLE_CROWD_BINS}), so no point reads past the budget. A whole front is drawn as the seven it was
|
|
67
|
+
* emitted as (water_particle_group), so three fresh splashes cross whole, whatever their steepness and levels; a fourth
|
|
68
|
+
* over them thins one.
|
|
69
|
+
*
|
|
70
|
+
* A cell only one front reaches is not crowded, however many of its packets it holds: one front puts at most ~40 in
|
|
71
|
+
* a cell, within the gather budget.
|
|
72
|
+
*/
|
|
73
|
+
export const WATER_PARTICLE_CELL_SHARE = 24;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Octaves a crowded cell counts its entries in, by the key its lottery keeps them by (a front's lot over its
|
|
77
|
+
* steepness; the lowest kept first), below the most it can keep: bin j holds keys from 2^-(j+1) of that reach to
|
|
78
|
+
* 2^-j of it, the last everything below. A cell whose point could read past the gather budget drops its octaves from
|
|
79
|
+
* the top until the rest fit its part of the budget, so it may keep up to an octave's worth fewer than would fit.
|
|
80
|
+
*/
|
|
81
|
+
export const WATER_PARTICLE_CROWD_BINS = 4;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Words a bucket's crowd takes, as the weigh pass sums it: its summed steepness, its largest front and complement of a
|
|
85
|
+
* front entered, and its entries.
|
|
86
|
+
*/
|
|
87
|
+
export const WATER_PARTICLE_CROWD_WORDS = 4;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Words the nest pass adds to a bucket's crowd, in a buffer of their own so that it reads weigh's sums as plain loads:
|
|
91
|
+
* the most entries and the most summed steepness a point under it reads where finer levels hold packets, and its
|
|
92
|
+
* entries by key, {@link WATER_PARTICLE_CROWD_BINS} octaves.
|
|
93
|
+
*/
|
|
94
|
+
export const WATER_PARTICLE_NEST_WORDS = 2 + WATER_PARTICLE_CROWD_BINS;
|
|
95
|
+
|
|
96
|
+
/** Width of the ramp a thinned packet fades over, a fraction of its share. */
|
|
97
|
+
export const WATER_PARTICLE_THINNING_RAMP = 0.25;
|
|
98
|
+
|
|
99
|
+
/** Fixed-point scale of a packet's steepness in its cells' sums. */
|
|
100
|
+
const WATER_PARTICLE_WEIGHT_SCALE = 1 << 20;
|
|
101
|
+
|
|
102
|
+
const STRIDE = WATER_PARTICLE_BODY_BOUNDS_STRIDE;
|
|
103
|
+
const WORDS = WATER_PARTICLE_CROWD_WORDS;
|
|
104
|
+
const NEST = WATER_PARTICLE_NEST_WORDS;
|
|
105
|
+
const BINS = WATER_PARTICLE_CROWD_BINS;
|
|
106
|
+
|
|
107
|
+
/** The step's settings: one immediate per entry point. */
|
|
108
|
+
export const WATER_PARTICLE_STEP_STRUCT = WebGPUStruct.from({ dt: f32, capacity: u32 }, 'WaterParticleStepSettings');
|
|
109
|
+
|
|
110
|
+
/** The pool's limits for the frame's finalize. */
|
|
111
|
+
export const WATER_PARTICLE_LIMITS_STRUCT = WebGPUStruct.from({ capacity: u32, table_limit: u32 }, 'WaterParticleLimits');
|
|
112
|
+
|
|
113
|
+
const update_resources = new ShaderResourceSetDescriptor();
|
|
114
|
+
update_resources.setImmediate('frame', WATER_PARTICLE_STEP_STRUCT);
|
|
115
|
+
update_resources.createGroup()
|
|
116
|
+
.addStorageBuffer('particles', WATER_PARTICLE_ARRAY_STRUCT, true)
|
|
117
|
+
.addStorageBuffer('children', COLLECTION_U32_STRUCT_READ, true)
|
|
118
|
+
.addStorageBuffer('images', WebGPUArray.from(WATER_PARTICLE_STRUCT), true)
|
|
119
|
+
.addStorageBuffer('activity', WebGPUArray.u32)
|
|
120
|
+
.addStorageBuffer('scene_database', WebGPUArray.u32)
|
|
121
|
+
.addStorageBuffer('geometries', WebGPUArray.from(GEOMETRY_METADATA_STRUCT))
|
|
122
|
+
.addStorageBuffer('blas_lookup', WebGPUArray.u32)
|
|
123
|
+
.addStorageBuffer('blas_data', WebGPUArray.from(BVH2_NODE_STRUCT))
|
|
124
|
+
.addStorageBuffer('meshlet_metadata', WebGPUArray.from(MESHLET_METADATA_STRUCT))
|
|
125
|
+
.addStorageBuffer('meshlet_data', WebGPUArray.from(f32));
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Advance every live particle of a body simulated this frame in place and count what it leaves: 0, 1, 2 (it and the
|
|
129
|
+
* mirror image it left at the outline, which goes to `images` at its index), 3 or 6 (each split into three). A paused
|
|
130
|
+
* body's particles stand still and leave themselves.
|
|
131
|
+
*/
|
|
132
|
+
const update_body = CodeChunk.from(/* language=WGSL */ `
|
|
133
|
+
@compute @workgroup_size(64)
|
|
134
|
+
fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) index: u32,
|
|
135
|
+
@builtin(subgroup_invocation_id) lane: u32, @builtin(subgroup_size) lanes: u32) {
|
|
136
|
+
let i = folded_workgroup_index(group) * 64u + index;
|
|
137
|
+
let count = min(particles.count, frame.capacity);
|
|
138
|
+
// every lane reaches the shuffles below: one past the count reads the last particle and writes nothing
|
|
139
|
+
let live = i < count;
|
|
140
|
+
var particle = particles.elements[min(i, max(count, 1u) - 1u)];
|
|
141
|
+
let node = ${waves.marshalling_method_read_field('node')}(&scene_database, particle.body);
|
|
142
|
+
let paused = (activity[particle.body] & ${WaterActivity.Active}u) == 0u;
|
|
143
|
+
let stepping = live && !paused && node != 0u && node != ~0u;
|
|
144
|
+
var scale = vec2<f32>(1.0);
|
|
145
|
+
var geometry = 0u;
|
|
146
|
+
// the axis point of its leading arc a step on, and whether it is over the body
|
|
147
|
+
var axis = vec2<f32>(0.0);
|
|
148
|
+
var hint = WaterParticleArcHint(0u, 0u, 0u);
|
|
149
|
+
if (stepping) {
|
|
150
|
+
let global = scene_read_node(&scene_database, node).global;
|
|
151
|
+
scale = max(vec2<f32>(length(global[0].xyz), length(global[2].xyz)), vec2<f32>(1e-20));
|
|
152
|
+
geometry = ${waves.marshalling_method_read_field('geometry')}(&scene_database, particle.body);
|
|
153
|
+
if (particle.reflected == 0u) {
|
|
154
|
+
axis = water_particle_leading_point(particle, frame.dt, 0u);
|
|
155
|
+
let hit = water_particle_over_body(geometry, axis / scale);
|
|
156
|
+
hint = WaterParticleArcHint(1u, select(0u, 1u, hit.t >= 0.0), hit.triangle);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// A front's particles stand in a row β apart, so the ends of one's window are its neighbours' axis points: each
|
|
160
|
+
// takes what the lanes either side found there rather than casting its own down-rays.
|
|
161
|
+
let state = hint.known | (hint.over << 1u) | (particle.body << 2u);
|
|
162
|
+
let below = max(lane, 1u) - 1u;
|
|
163
|
+
let above = min(lane + 1u, lanes - 1u);
|
|
164
|
+
let below_axis = subgroupShuffle(axis, below);
|
|
165
|
+
let below_state = subgroupShuffle(state, below);
|
|
166
|
+
let above_axis = subgroupShuffle(axis, above);
|
|
167
|
+
let above_state = subgroupShuffle(state, above);
|
|
168
|
+
if (!live) { return; }
|
|
169
|
+
var left = 1u;
|
|
170
|
+
if (stepping) {
|
|
171
|
+
if (particle.reflected == 0u) {
|
|
172
|
+
let tolerance = 2e-3 * particle.radius;
|
|
173
|
+
for (var k = 1u; k < 3u; k++) {
|
|
174
|
+
let end = water_particle_leading_point(particle, frame.dt, k);
|
|
175
|
+
var neighbour = 0u;
|
|
176
|
+
if ((below_state >> 2u) == particle.body && distance(end, below_axis) <= tolerance) { neighbour = below_state; }
|
|
177
|
+
if ((above_state >> 2u) == particle.body && distance(end, above_axis) <= tolerance) { neighbour = above_state; }
|
|
178
|
+
hint.known |= (neighbour & 1u) << k;
|
|
179
|
+
hint.over |= ((neighbour >> 1u) & 1u) << k;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
let threshold = water_particle_threshold(${waves.marshalling_method_read_field('slope_variance')}(&scene_database, particle.body),
|
|
183
|
+
count, frame.capacity);
|
|
184
|
+
let advanced = water_particle_advance(particle, frame.dt, geometry, scale,
|
|
185
|
+
${waves.marshalling_method_read_field('wind')}(&scene_database, particle.body).w, threshold, hint);
|
|
186
|
+
particle = advanced.particle;
|
|
187
|
+
left = advanced.children;
|
|
188
|
+
if (left == 2u || left == 6u) { images[i] = advanced.image; }
|
|
189
|
+
} else if (!paused) {
|
|
190
|
+
left = 0u;
|
|
191
|
+
}
|
|
192
|
+
particles.elements[i] = particle;
|
|
193
|
+
children.elements[i] = left;
|
|
194
|
+
}
|
|
195
|
+
`, [chunk_water_particle_step, chunk_scene_database_access, waves.chunk_read_field('node'), waves.chunk_read_field('slope_variance'),
|
|
196
|
+
waves.chunk_read_field('geometry'), waves.chunk_read_field('wind'), chunk_folded_workgroup_index]);
|
|
197
|
+
update_body.addExtension(WebGPUExtensionType.Subgroups);
|
|
198
|
+
export const shader_water_particles_update = ComputeShader.from({ label: 'Water / particles update', resources: update_resources,
|
|
199
|
+
body: update_body });
|
|
200
|
+
|
|
201
|
+
const split_resources = new ShaderResourceSetDescriptor();
|
|
202
|
+
split_resources.setImmediate('capacity', u32);
|
|
203
|
+
split_resources.createGroup()
|
|
204
|
+
.addStorageBuffer('particles', WATER_PARTICLE_ARRAY_STRUCT)
|
|
205
|
+
.addStorageBuffer('children', COLLECTION_U32_STRUCT_READ)
|
|
206
|
+
.addStorageBuffer('images', WebGPUArray.from(WATER_PARTICLE_STRUCT))
|
|
207
|
+
.addStorageBuffer('next', WATER_PARTICLE_ARRAY_STRUCT, true);
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Write what each particle leaves into the next array at its scanned offset, in order: the particle, then the mirror
|
|
211
|
+
* image it left; each split one's three at θ − β/3, θ, θ + β/3 with β/3 each. So a front's particles stay in a row
|
|
212
|
+
* and in order, and an image follows its packet as a front of its own. Past capacity, the tail is dropped.
|
|
213
|
+
*/
|
|
214
|
+
export const shader_water_particles_split = ComputeShader.from({ label: 'Water / particles split', resources: split_resources,
|
|
215
|
+
body: CodeChunk.from(/* language=WGSL */ `
|
|
216
|
+
@compute @workgroup_size(64)
|
|
217
|
+
fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
|
|
218
|
+
let i = folded_workgroup_index(group) * 64u + lane;
|
|
219
|
+
let count = min(particles.count, capacity);
|
|
220
|
+
if (i >= count) { return; }
|
|
221
|
+
let end = children.elements[i];
|
|
222
|
+
var start = 0u;
|
|
223
|
+
if (i > 0u) { start = children.elements[i - 1u]; }
|
|
224
|
+
let particle = particles.elements[i];
|
|
225
|
+
if (end - start == 1u) {
|
|
226
|
+
if (start < capacity) { next.elements[start] = particle; }
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
// three of each when they split
|
|
230
|
+
let split = (end - start) % 3u == 0u;
|
|
231
|
+
let each = select(1u, 3u, split);
|
|
232
|
+
for (var k = start; k < end && k < capacity; k++) {
|
|
233
|
+
var child = particle;
|
|
234
|
+
if (k - start >= each) { child = images[i]; }
|
|
235
|
+
if (split) {
|
|
236
|
+
child.dispersion = child.dispersion / 3.0;
|
|
237
|
+
child.angle += (f32((k - start) % 3u) - 1.0) * child.dispersion;
|
|
238
|
+
}
|
|
239
|
+
next.elements[k] = child;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
`, [WATER_PARTICLE_STRUCT.declaration_chunk, chunk_folded_workgroup_index]) });
|
|
243
|
+
|
|
244
|
+
const total_resources = new ShaderResourceSetDescriptor();
|
|
245
|
+
total_resources.setImmediate('capacity', u32);
|
|
246
|
+
total_resources.createGroup()
|
|
247
|
+
.addStorageBuffer('children', COLLECTION_U32_STRUCT_READ)
|
|
248
|
+
.addStorageBuffer('next', WATER_PARTICLE_ARRAY_STRUCT, true);
|
|
249
|
+
|
|
250
|
+
/** The next array's count: everything the scan placed, up to capacity. */
|
|
251
|
+
export const shader_water_particles_total = ComputeShader.from({ label: 'Water / particles total', resources: total_resources,
|
|
252
|
+
body: CodeChunk.from(/* language=WGSL */ `
|
|
253
|
+
@compute @workgroup_size(1)
|
|
254
|
+
fn main() {
|
|
255
|
+
let total = select(0u, children.elements[max(children.count, 1u) - 1u], children.count > 0u);
|
|
256
|
+
next.count = min(total, capacity);
|
|
257
|
+
}
|
|
258
|
+
`, [WATER_PARTICLE_STRUCT.declaration_chunk]) });
|
|
259
|
+
|
|
260
|
+
const emit_resources = new ShaderResourceSetDescriptor();
|
|
261
|
+
emit_resources.setImmediate('capacity', u32);
|
|
262
|
+
emit_resources.createGroup()
|
|
263
|
+
.addStorageBuffer('scene_database', WebGPUArray.u32, true)
|
|
264
|
+
.addStorageBuffer('emit_rows', COLLECTION_U32_STRUCT_READ)
|
|
265
|
+
.addStorageBuffer('steep', WebGPUArray.from(u32))
|
|
266
|
+
.addStorageBuffer('activity', WebGPUArray.u32)
|
|
267
|
+
.addStorageBuffer('particle_bounds', WebGPUArray.u32, true)
|
|
268
|
+
.addStorageBuffer('next', WATER_PARTICLE_ARRAY_APPEND_STRUCT, true);
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Each queued splash of a row on the culling pass's emit list becomes a ring
|
|
272
|
+
* (WATER_SURFACE_SIMULATION_PLAN_2026_10_06.md §5.3): packets of the splash's radius r, two radii out from its centre,
|
|
273
|
+
* their windows' arcs no longer than their envelopes, ridge height strength·r/c (a vertical velocity over the time a crest takes to cross the
|
|
274
|
+
* packet), no steeper than 0.1. The row's queue is spent. A paused body's ring stands still with its other particles,
|
|
275
|
+
* which this frame's binning does not visit, so the ring's bound is added to the body's own here.
|
|
276
|
+
*/
|
|
277
|
+
export const shader_water_particles_emit = ComputeShader.from({ label: 'Water / particles emit', resources: emit_resources,
|
|
278
|
+
body: CodeChunk.from(/* language=WGSL */ `
|
|
279
|
+
@compute @workgroup_size(64)
|
|
280
|
+
fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
|
|
281
|
+
let index = folded_workgroup_index(group) * 64u + lane;
|
|
282
|
+
if (index >= emit_rows.count) { return; }
|
|
283
|
+
let row = emit_rows.elements[index];
|
|
284
|
+
let wave = ${waves.marshalling_method_read_rw}(&scene_database, row);
|
|
285
|
+
${waves.marshalling_method_write_field('impulse_count')}(&scene_database, row, 0u);
|
|
286
|
+
if (wave.node == 0u || wave.node == ~0u || steep[row] != 0u) { return; }
|
|
287
|
+
let global = scene_read_node_rw(&scene_database, wave.node).global;
|
|
288
|
+
let scale = vec2<f32>(length(global[0].xyz), length(global[2].xyz));
|
|
289
|
+
let paused = (activity[row] & ${WaterActivity.Active}u) == 0u;
|
|
290
|
+
var held = 0.0;
|
|
291
|
+
for (var j = 0u; j < min(wave.impulse_count, ${WATER_WAVE_IMPULSE_LIMIT}u); j++) {
|
|
292
|
+
let impulse = wave.impulses[j];
|
|
293
|
+
let radius = clamp(impulse.z, 1e-3, 16.0);
|
|
294
|
+
let height = clamp(impulse.w * radius / water_particle_phase_speed(radius), -0.1 * radius, 0.1 * radius);
|
|
295
|
+
let reach = water_particle_launch_distance(radius);
|
|
296
|
+
// as many as keep each window's arc within the envelope's half-length, as the step splits them
|
|
297
|
+
let count = u32(ceil(WATER_TAU * reach / water_particle_half_length_at(radius, reach)));
|
|
298
|
+
let dispersion = WATER_TAU / f32(count);
|
|
299
|
+
let age = reach / water_particle_group_speed(radius);
|
|
300
|
+
let base = atomicAdd(&next.count, count);
|
|
301
|
+
// a ring that does not fit whole is not emitted: an arc of it would break its front's partition of unity.
|
|
302
|
+
// The slots it took up to capacity hold flat packets, which the next step drops.
|
|
303
|
+
let ridge = select(0.0, height, base + count <= capacity);
|
|
304
|
+
for (var k = 0u; k < count; k++) {
|
|
305
|
+
if (base + k >= capacity) { break; }
|
|
306
|
+
next.elements[base + k] = ${WATER_PARTICLE_STRUCT.wgsl_ref}(impulse.xy * scale, f32(k) * dispersion, age, ridge, radius, dispersion, row, 0u);
|
|
307
|
+
}
|
|
308
|
+
held += f32(count) * abs(ridge);
|
|
309
|
+
}
|
|
310
|
+
if (paused && held > 0.0) {
|
|
311
|
+
// level 0's slot, and its bit, so that the culling pass, which reads the slots only of the levels set, counts it
|
|
312
|
+
let at = row * ${STRIDE}u;
|
|
313
|
+
particle_bounds[at] |= 1u;
|
|
314
|
+
particle_bounds[at + 1u] = bitcast<u32>(bitcast<f32>(particle_bounds[at + 1u]) + held);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
`, [waves.chunk_read_rw, waves.wgsl_gen_write_field_code('impulse_count'), chunk_water_particle, chunk_scene_database_access_rw,
|
|
318
|
+
chunk_folded_workgroup_index]) });
|
|
319
|
+
|
|
320
|
+
const finalize_resources = new ShaderResourceSetDescriptor();
|
|
321
|
+
finalize_resources.setImmediate('limits', WATER_PARTICLE_LIMITS_STRUCT);
|
|
322
|
+
finalize_resources.createGroup()
|
|
323
|
+
.addStorageBuffer('next', WATER_PARTICLE_ARRAY_STRUCT, true)
|
|
324
|
+
.addStorageBuffer('offsets', COLLECTION_U32_STRUCT_READ, true)
|
|
325
|
+
.addStorageBuffer('table_command', STRUCT_DISPATCH_COMPUTE_COMMAND, true);
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* The live count, clamped to capacity (emitters append past it), and the frame's hash table size: twice the
|
|
329
|
+
* entries the particles can take, a power of two, at least ${WATER_PARTICLE_TABLE_MIN}; and the command that clears
|
|
330
|
+
* that much of the table, a thread a bucket.
|
|
331
|
+
*/
|
|
332
|
+
export const shader_water_particles_finalize = ComputeShader.from({ label: 'Water / particles finalize', resources: finalize_resources,
|
|
333
|
+
body: CodeChunk.from(/* language=WGSL */ `
|
|
334
|
+
@compute @workgroup_size(1)
|
|
335
|
+
fn main() {
|
|
336
|
+
let live = min(next.count, limits.capacity);
|
|
337
|
+
next.count = live;
|
|
338
|
+
let wanted = max(2u * ${WATER_PARTICLE_CORNERS}u * live, ${WATER_PARTICLE_TABLE_MIN}u);
|
|
339
|
+
let table = min(1u << (32u - countLeadingZeros(wanted - 1u)), limits.table_limit);
|
|
340
|
+
offsets.count = table;
|
|
341
|
+
table_command.workgroupCountX = (table + 63u) / 64u;
|
|
342
|
+
table_command.workgroupCountY = 1u;
|
|
343
|
+
table_command.workgroupCountZ = 1u;
|
|
344
|
+
}
|
|
345
|
+
`, [WATER_PARTICLE_STRUCT.declaration_chunk]) });
|
|
346
|
+
|
|
347
|
+
const clear_resources = new ShaderResourceSetDescriptor();
|
|
348
|
+
clear_resources.createGroup()
|
|
349
|
+
.addStorageBuffer('buckets', WATER_PARTICLE_BUCKETS_STRUCT, true)
|
|
350
|
+
.addStorageBuffer('bucket_amplitude', WebGPUArray.u32, true)
|
|
351
|
+
.addStorageBuffer('bucket_weight', WebGPUArray.u32, true)
|
|
352
|
+
.addStorageBuffer('bucket_nest', WebGPUArray.u32, true);
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Zero the frame's table, as far as finalize sized it: each bucket's count, largest |a| and crowd. A frame pays for the
|
|
356
|
+
* table its particles take, not for the pool's capacity; the buckets past it are never read.
|
|
357
|
+
*/
|
|
358
|
+
export const shader_water_particles_clear = ComputeShader.from({ label: 'Water / particles clear', resources: clear_resources,
|
|
359
|
+
body: CodeChunk.from(/* language=WGSL */ `
|
|
360
|
+
@compute @workgroup_size(64)
|
|
361
|
+
fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
|
|
362
|
+
let b = folded_workgroup_index(group) * 64u + lane;
|
|
363
|
+
if (b >= buckets.count) { return; }
|
|
364
|
+
atomicStore(&buckets.elements[b], 0u);
|
|
365
|
+
bucket_amplitude[b] = 0u;
|
|
366
|
+
for (var word = 0u; word < ${WORDS}u; word++) { bucket_weight[${WORDS}u * b + word] = 0u; }
|
|
367
|
+
for (var word = 0u; word < ${NEST}u; word++) { bucket_nest[${NEST}u * b + word] = 0u; }
|
|
368
|
+
}
|
|
369
|
+
`, [chunk_folded_workgroup_index]) });
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* A live particle's place in the hash this frame: its kernel at this time, its level and the cells its
|
|
373
|
+
* support overlaps (one or two a side, the level's cell being at least the support's diameter).
|
|
374
|
+
*/
|
|
375
|
+
const chunk_water_particle_cells = CodeChunk.from(/* language=WGSL */ `
|
|
376
|
+
struct WaterParticleCells {
|
|
377
|
+
entry: ${WATER_PARTICLE_ENTRY_STRUCT.wgsl_ref},
|
|
378
|
+
level: u32,
|
|
379
|
+
first: vec2<i32>,
|
|
380
|
+
span: vec2<u32>,
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
fn water_particle_cells(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}, source: u32) -> WaterParticleCells {
|
|
384
|
+
var cells: WaterParticleCells;
|
|
385
|
+
let position = water_particle_position(particle);
|
|
386
|
+
let width = water_particle_width(particle);
|
|
387
|
+
let distance = water_particle_distance(particle);
|
|
388
|
+
let half_length = water_particle_half_length_at(particle.radius, distance);
|
|
389
|
+
let support = water_particle_support(half_length, width, particle.dispersion);
|
|
390
|
+
cells.level = water_particle_level(support);
|
|
391
|
+
let size = water_particle_cell_size(cells.level);
|
|
392
|
+
cells.first = vec2<i32>(floor((position - vec2<f32>(support)) / size));
|
|
393
|
+
let last = vec2<i32>(floor((position + vec2<f32>(support)) / size));
|
|
394
|
+
cells.span = vec2<u32>(clamp(last - cells.first, vec2<i32>(0), vec2<i32>(1))) + vec2<u32>(1u);
|
|
395
|
+
cells.entry = ${WATER_PARTICLE_ENTRY_STRUCT.wgsl_ref}(position, cells.first, particle.angle, particle.amplitude, particle.radius,
|
|
396
|
+
particle.dispersion, water_particle_phase(particle), water_particle_key(particle.body, cells.level), source, distance, half_length);
|
|
397
|
+
return cells;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
fn water_particle_corner_cell(cells: WaterParticleCells, corner: u32) -> vec2<i32> {
|
|
401
|
+
return cells.first + vec2<i32>(i32(corner & 1u), i32(corner >> 1u));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
fn water_particle_has_corner(cells: WaterParticleCells, corner: u32) -> bool {
|
|
405
|
+
return all(vec2<u32>(corner & 1u, corner >> 1u) < cells.span);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// A packet's claim on its cells: its steepness, fixed point.
|
|
409
|
+
fn water_particle_weight(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}) -> u32 {
|
|
410
|
+
return u32(min(abs(particle.amplitude) / max(particle.radius, 1e-6), 1.0) * ${WATER_PARTICLE_WEIGHT_SCALE}.0);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// The front a packet belongs to: from its centre, the same for every packet of the front.
|
|
414
|
+
fn water_particle_front(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}) -> u32 {
|
|
415
|
+
return pcg3d(vec3<u32>(bitcast<u32>(particle.origin.x), bitcast<u32>(particle.origin.y), particle.body)).x;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// A number in [0, 1) that stays with a front while it travels, the same for all its packets.
|
|
419
|
+
fn water_particle_lot(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}) -> f32 {
|
|
420
|
+
return f32(water_particle_front(particle) >> 8u) * (1.0 / 16777216.0);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// The bucket of the cell holding a cell's points at a level as coarse as its own or coarser, as the sampler finds it.
|
|
424
|
+
fn water_particle_bucket_over(body: u32, level: u32, cell: vec2<i32>, over: u32, table_size: u32) -> u32 {
|
|
425
|
+
return water_particle_bucket(body, over, cell >> vec2<u32>(over - level), table_size);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// The key a crowd keeps a packet by: its front's lot over its steepness. A packet is drawn while its lot is under its
|
|
429
|
+
// share, which is its steepness times its cell's, so a crowd keeps the packets of its lowest keys, the steepest first.
|
|
430
|
+
fn water_particle_precedence(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}) -> f32 {
|
|
431
|
+
return water_particle_lot(particle) / f32(water_particle_weight(particle));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// The largest key a crowded cell's lottery can keep, from the summed steepness a point of it reads at its level and
|
|
435
|
+
// the coarser ones: its most crowded point reads at least that, and the ramp draws a lot up to its share above it.
|
|
436
|
+
fn water_particle_crowd_reach(steepness: u32) -> f32 {
|
|
437
|
+
return ${1 + WATER_PARTICLE_THINNING_RAMP} * ${WATER_PARTICLE_CELL_SHARE}.0 / f32(steepness);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// The octave of a cell's reach a key falls in, from the exponent of their ratio: [2^-(j+1), 2^-j) is bin j, the last
|
|
441
|
+
// takes everything below, and one past the reach is not counted (${BINS}u).
|
|
442
|
+
fn water_particle_crowd_bin(precedence: f32, reach: f32) -> u32 {
|
|
443
|
+
let ratio = precedence / reach;
|
|
444
|
+
if (!(ratio < 1.0)) { return ${BINS}u; }
|
|
445
|
+
return min(126u - ((bitcast<u32>(ratio) >> 23u) & 0xffu), ${BINS - 1}u);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// How much of a packet its crowd leaves drawn, from its share of the most crowded of its cells: all of it from a share
|
|
449
|
+
// of one, and below that, all of it while its lot is under its share, fading to none a ramp of that share above, and
|
|
450
|
+
// none from a share of none. So a cell keeps about its share whatever its crowd.
|
|
451
|
+
fn water_particle_visibility(share: f32, lot: f32) -> f32 {
|
|
452
|
+
if (!(share > 0.0)) { return 0.0; }
|
|
453
|
+
return clamp((share - lot) / (${WATER_PARTICLE_THINNING_RAMP} * share) + 1.0, 0.0, 1.0);
|
|
454
|
+
}
|
|
455
|
+
`, [chunk_water_particle]);
|
|
456
|
+
|
|
457
|
+
const fronts_resources = new ShaderResourceSetDescriptor();
|
|
458
|
+
fronts_resources.createGroup()
|
|
459
|
+
.addStorageBuffer('next', WATER_PARTICLE_ARRAY_STRUCT)
|
|
460
|
+
.addStorageBuffer('fronts', COLLECTION_U32_STRUCT_READ, true);
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Where each front's run of particles begins: 1 at a particle not of the front before it, else 0. A front keeps its
|
|
464
|
+
* particles in a row and in order — a ring is emitted whole, a split puts its three in its place, the dead leave no
|
|
465
|
+
* gap — so the inclusive scan of these numbers each run from 1. A mirror image follows its packet in the row: it
|
|
466
|
+
* breaks its front's run there, as a death does, and is a front of its own.
|
|
467
|
+
*/
|
|
468
|
+
export const shader_water_particles_fronts = ComputeShader.from({ label: 'Water / particles fronts', resources: fronts_resources,
|
|
469
|
+
body: CodeChunk.from(/* language=WGSL */ `
|
|
470
|
+
@compute @workgroup_size(64)
|
|
471
|
+
fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
|
|
472
|
+
let i = folded_workgroup_index(group) * 64u + lane;
|
|
473
|
+
if (i >= next.count) { return; }
|
|
474
|
+
fronts.elements[i] = select(1u, 0u, i > 0u && water_particle_same_front(next.elements[i - 1u], next.elements[i]));
|
|
475
|
+
}
|
|
476
|
+
`, [chunk_water_particle, chunk_folded_workgroup_index]) });
|
|
477
|
+
|
|
478
|
+
const runs_resources = new ShaderResourceSetDescriptor();
|
|
479
|
+
runs_resources.createGroup()
|
|
480
|
+
.addStorageBuffer('next', WATER_PARTICLE_ARRAY_STRUCT)
|
|
481
|
+
.addStorageBuffer('fronts', COLLECTION_U32_STRUCT_READ)
|
|
482
|
+
.addStorageBuffer('runs', WebGPUArray.u32, true);
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Two words a run: its first particle, and the share its crowd leaves it, none yet (the share pass lowers it).
|
|
486
|
+
*/
|
|
487
|
+
export const shader_water_particles_runs = ComputeShader.from({ label: 'Water / particles runs', resources: runs_resources,
|
|
488
|
+
body: CodeChunk.from(/* language=WGSL */ `
|
|
489
|
+
@compute @workgroup_size(64)
|
|
490
|
+
fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
|
|
491
|
+
let i = folded_workgroup_index(group) * 64u + lane;
|
|
492
|
+
if (i >= next.count) { return; }
|
|
493
|
+
let run = fronts.elements[i];
|
|
494
|
+
if (i == 0u || fronts.elements[i - 1u] != run) {
|
|
495
|
+
runs[2u * (run - 1u)] = i;
|
|
496
|
+
runs[2u * (run - 1u) + 1u] = bitcast<u32>(1e30);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
`, [WATER_PARTICLE_STRUCT.declaration_chunk, chunk_folded_workgroup_index]) });
|
|
500
|
+
|
|
501
|
+
const groups_resources = new ShaderResourceSetDescriptor();
|
|
502
|
+
groups_resources.createGroup()
|
|
503
|
+
.addStorageBuffer('next', WATER_PARTICLE_ARRAY_STRUCT)
|
|
504
|
+
.addStorageBuffer('fronts', COLLECTION_U32_STRUCT_READ)
|
|
505
|
+
.addStorageBuffer('runs', WebGPUArray.u32)
|
|
506
|
+
.addStorageBuffer('draw', WebGPUArray.u32, true);
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Two words a particle: its run, and how the frame draws it ({@link chunk_water_particle}'s water_particle_group): 1 as
|
|
510
|
+
* itself, 0 not at all, 3^g for the middle of a whole front's group of 3^g, drawn as one packet.
|
|
511
|
+
*/
|
|
512
|
+
export const shader_water_particles_groups = ComputeShader.from({ label: 'Water / particles groups', resources: groups_resources,
|
|
513
|
+
body: CodeChunk.from(/* language=WGSL */ `
|
|
514
|
+
@compute @workgroup_size(64)
|
|
515
|
+
fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
|
|
516
|
+
let i = folded_workgroup_index(group) * 64u + lane;
|
|
517
|
+
let count = next.count;
|
|
518
|
+
if (i >= count) { return; }
|
|
519
|
+
let run = fronts.elements[i] - 1u;
|
|
520
|
+
let start = runs[2u * run];
|
|
521
|
+
var end = count;
|
|
522
|
+
if (run + 1u < fronts.elements[count - 1u]) { end = runs[2u * (run + 1u)]; }
|
|
523
|
+
draw[2u * i] = run;
|
|
524
|
+
draw[2u * i + 1u] = water_particle_group(next.elements[i], end - start, i - start);
|
|
525
|
+
}
|
|
526
|
+
`, [chunk_water_particle, chunk_folded_workgroup_index]) });
|
|
527
|
+
|
|
528
|
+
const weigh_resources = new ShaderResourceSetDescriptor();
|
|
529
|
+
weigh_resources.createGroup()
|
|
530
|
+
.addStorageBuffer('next', WATER_PARTICLE_ARRAY_STRUCT)
|
|
531
|
+
.addStorageBuffer('activity', WebGPUArray.u32)
|
|
532
|
+
.addStorageBuffer('offsets', COLLECTION_U32_STRUCT_READ)
|
|
533
|
+
.addStorageBuffer('draw', WebGPUArray.u32)
|
|
534
|
+
.addStorageBuffer('bucket_weight', WebGPUArray.from(atomic_u32), true)
|
|
535
|
+
.addStorageBuffer('body_bounds', WebGPUArray.from(atomic_u32), true);
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Each packet drawn of a body simulated this frame adds its steepness to every cell its support overlaps: how crowded
|
|
539
|
+
* a cell is, for the share pass to thin it by. Per bucket ({@link WATER_PARTICLE_CROWD_WORDS}) the sum, the largest
|
|
540
|
+
* front and complement of a front entered, which are each other's complements only where one front alone was, and the
|
|
541
|
+
* entries. And its level to its body's: a point reads a cell of every level its body has.
|
|
542
|
+
*/
|
|
543
|
+
export const shader_water_particles_weigh = ComputeShader.from({ label: 'Water / particles weigh', resources: weigh_resources,
|
|
544
|
+
body: CodeChunk.from(/* language=WGSL */ `
|
|
545
|
+
@compute @workgroup_size(64)
|
|
546
|
+
fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
|
|
547
|
+
let i = folded_workgroup_index(group) * 64u + lane;
|
|
548
|
+
if (i >= next.count) { return; }
|
|
549
|
+
let drawn = draw[2u * i + 1u];
|
|
550
|
+
if (drawn == 0u) { return; }
|
|
551
|
+
let particle = water_particle_drawn(next.elements[i], drawn);
|
|
552
|
+
if ((activity[particle.body] & ${WaterActivity.Active}u) == 0u) { return; }
|
|
553
|
+
let cells = water_particle_cells(particle, i);
|
|
554
|
+
let weight = water_particle_weight(particle);
|
|
555
|
+
let front = water_particle_front(particle);
|
|
556
|
+
atomicOr(&body_bounds[particle.body * ${STRIDE}u], 1u << cells.level);
|
|
557
|
+
for (var corner = 0u; corner < ${WATER_PARTICLE_CORNERS}u; corner++) {
|
|
558
|
+
if (!water_particle_has_corner(cells, corner)) { continue; }
|
|
559
|
+
let at = ${WATER_PARTICLE_CROWD_WORDS}u * water_particle_bucket(particle.body, cells.level, water_particle_corner_cell(cells, corner), offsets.count);
|
|
560
|
+
atomicAdd(&bucket_weight[at], weight);
|
|
561
|
+
atomicMax(&bucket_weight[at + 1u], front);
|
|
562
|
+
atomicMax(&bucket_weight[at + 2u], ~front);
|
|
563
|
+
atomicAdd(&bucket_weight[at + 3u], 1u);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
`, [chunk_water_particle_cells, chunk_folded_workgroup_index]) });
|
|
567
|
+
|
|
568
|
+
const nest_resources = new ShaderResourceSetDescriptor();
|
|
569
|
+
nest_resources.createGroup()
|
|
570
|
+
.addStorageBuffer('next', WATER_PARTICLE_ARRAY_STRUCT)
|
|
571
|
+
.addStorageBuffer('activity', WebGPUArray.u32)
|
|
572
|
+
.addStorageBuffer('offsets', COLLECTION_U32_STRUCT_READ)
|
|
573
|
+
.addStorageBuffer('draw', WebGPUArray.u32)
|
|
574
|
+
.addStorageBuffer('body_bounds', WebGPUArray.u32)
|
|
575
|
+
.addStorageBuffer('bucket_weight', WebGPUArray.u32)
|
|
576
|
+
.addStorageBuffer('bucket_nest', WebGPUArray.from(atomic_u32), true);
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Each packet drawn of a body simulated this frame, its cells weighed: what a point of each of its cells reads at its
|
|
580
|
+
* level and the coarser ones, entries and summed steepness, raised onto the cell over it at every coarser level its
|
|
581
|
+
* body has, so a cell knows its most crowded point where finer levels hold packets under it; and its entry counted
|
|
582
|
+
* among its cell's by its key (water_particle_crowd_bin), for the share pass to cap a crowd by.
|
|
583
|
+
*/
|
|
584
|
+
export const shader_water_particles_nest = ComputeShader.from({ label: 'Water / particles nest', resources: nest_resources,
|
|
585
|
+
body: CodeChunk.from(/* language=WGSL */ `
|
|
586
|
+
@compute @workgroup_size(64)
|
|
587
|
+
fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
|
|
588
|
+
let i = folded_workgroup_index(group) * 64u + lane;
|
|
589
|
+
if (i >= next.count) { return; }
|
|
590
|
+
let drawn = draw[2u * i + 1u];
|
|
591
|
+
if (drawn == 0u) { return; }
|
|
592
|
+
let particle = water_particle_drawn(next.elements[i], drawn);
|
|
593
|
+
if ((activity[particle.body] & ${WaterActivity.Active}u) == 0u) { return; }
|
|
594
|
+
let cells = water_particle_cells(particle, i);
|
|
595
|
+
let precedence = water_particle_precedence(particle);
|
|
596
|
+
// its body's levels coarser than its own
|
|
597
|
+
let coarser = body_bounds[particle.body * ${STRIDE}u] & ~((2u << cells.level) - 1u);
|
|
598
|
+
for (var corner = 0u; corner < ${WATER_PARTICLE_CORNERS}u; corner++) {
|
|
599
|
+
if (!water_particle_has_corner(cells, corner)) { continue; }
|
|
600
|
+
let cell = water_particle_corner_cell(cells, corner);
|
|
601
|
+
let bucket = water_particle_bucket(particle.body, cells.level, cell, offsets.count);
|
|
602
|
+
// what a point of the cell reads at its level and the coarser ones
|
|
603
|
+
var entries = bucket_weight[${WORDS}u * bucket + 3u];
|
|
604
|
+
var steepness = bucket_weight[${WORDS}u * bucket];
|
|
605
|
+
var above = coarser;
|
|
606
|
+
for (var n = 0u; n < WATER_PARTICLE_LEVELS && above != 0u; n++) {
|
|
607
|
+
let over = ${WORDS}u * water_particle_bucket_over(particle.body, cells.level, cell, firstTrailingBit(above), offsets.count);
|
|
608
|
+
above &= above - 1u;
|
|
609
|
+
entries += bucket_weight[over + 3u];
|
|
610
|
+
steepness += bucket_weight[over];
|
|
611
|
+
}
|
|
612
|
+
// onto every cell over it
|
|
613
|
+
above = coarser;
|
|
614
|
+
for (var n = 0u; n < WATER_PARTICLE_LEVELS && above != 0u; n++) {
|
|
615
|
+
let over = ${NEST}u * water_particle_bucket_over(particle.body, cells.level, cell, firstTrailingBit(above), offsets.count);
|
|
616
|
+
above &= above - 1u;
|
|
617
|
+
atomicMax(&bucket_nest[over], entries);
|
|
618
|
+
atomicMax(&bucket_nest[over + 1u], steepness);
|
|
619
|
+
}
|
|
620
|
+
let octave = water_particle_crowd_bin(precedence, water_particle_crowd_reach(steepness));
|
|
621
|
+
if (octave < ${BINS}u) { atomicAdd(&bucket_nest[${NEST}u * bucket + 2u + octave], 1u); }
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
`, [chunk_water_particle_cells, chunk_folded_workgroup_index]) });
|
|
625
|
+
|
|
626
|
+
const share_resources = new ShaderResourceSetDescriptor();
|
|
627
|
+
share_resources.createGroup()
|
|
628
|
+
.addStorageBuffer('next', WATER_PARTICLE_ARRAY_STRUCT)
|
|
629
|
+
.addStorageBuffer('activity', WebGPUArray.u32)
|
|
630
|
+
.addStorageBuffer('offsets', COLLECTION_U32_STRUCT_READ)
|
|
631
|
+
.addStorageBuffer('bucket_weight', WebGPUArray.u32)
|
|
632
|
+
.addStorageBuffer('bucket_nest', WebGPUArray.u32)
|
|
633
|
+
.addStorageBuffer('body_bounds', WebGPUArray.u32)
|
|
634
|
+
.addStorageBuffer('draw', WebGPUArray.u32)
|
|
635
|
+
.addStorageBuffer('runs', WebGPUArray.from(atomic_u32), true);
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Each packet drawn its share of the most crowded of its cells, lowered onto its run: a run is thinned whole, by the most
|
|
639
|
+
* crowded cell any of its packets is in, so a front is never holed. A cell is crowded where its most crowded point (one
|
|
640
|
+
* of it at its level and the coarser ones, or one under it the nest pass raised) reads more than
|
|
641
|
+
* {@link WATER_PARTICLE_CELL_SHARE} entries, and a packet's share there is that many times its steepness over the summed
|
|
642
|
+
* steepness that point reads. Where that point would read more than the gather budget, the cell keeps no more of it
|
|
643
|
+
* than its own steepness's part: its keys' octaves dropped from the top until the rest fit
|
|
644
|
+
* ({@link WATER_PARTICLE_CROWD_BINS}). A cell only one front reaches does not count.
|
|
645
|
+
*/
|
|
646
|
+
export const shader_water_particles_share = ComputeShader.from({ label: 'Water / particles share', resources: share_resources,
|
|
647
|
+
body: CodeChunk.from(/* language=WGSL */ `
|
|
648
|
+
@compute @workgroup_size(64)
|
|
649
|
+
fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
|
|
650
|
+
let i = folded_workgroup_index(group) * 64u + lane;
|
|
651
|
+
if (i >= next.count) { return; }
|
|
652
|
+
let drawn = draw[2u * i + 1u];
|
|
653
|
+
if (drawn == 0u) { return; }
|
|
654
|
+
let particle = water_particle_drawn(next.elements[i], drawn);
|
|
655
|
+
if ((activity[particle.body] & ${WaterActivity.Active}u) == 0u) { return; }
|
|
656
|
+
let cells = water_particle_cells(particle, i);
|
|
657
|
+
let weight = f32(water_particle_weight(particle));
|
|
658
|
+
let coarser = body_bounds[particle.body * ${STRIDE}u] & ~((2u << cells.level) - 1u);
|
|
659
|
+
var share = 1e30;
|
|
660
|
+
for (var corner = 0u; corner < ${WATER_PARTICLE_CORNERS}u; corner++) {
|
|
661
|
+
if (!water_particle_has_corner(cells, corner)) { continue; }
|
|
662
|
+
let cell = water_particle_corner_cell(cells, corner);
|
|
663
|
+
let bucket = water_particle_bucket(particle.body, cells.level, cell, offsets.count);
|
|
664
|
+
let at = ${WORDS}u * bucket;
|
|
665
|
+
let nested = ${NEST}u * bucket;
|
|
666
|
+
if (bucket_weight[at + 1u] == ~bucket_weight[at + 2u]) { continue; }
|
|
667
|
+
// what a point of the cell reads at its level and the coarser ones
|
|
668
|
+
var entries = bucket_weight[at + 3u];
|
|
669
|
+
var steepness = bucket_weight[at];
|
|
670
|
+
var above = coarser;
|
|
671
|
+
for (var n = 0u; n < WATER_PARTICLE_LEVELS && above != 0u; n++) {
|
|
672
|
+
let over = ${WORDS}u * water_particle_bucket_over(particle.body, cells.level, cell, firstTrailingBit(above), offsets.count);
|
|
673
|
+
above &= above - 1u;
|
|
674
|
+
entries += bucket_weight[over + 3u];
|
|
675
|
+
steepness += bucket_weight[over];
|
|
676
|
+
}
|
|
677
|
+
// and what its most crowded point reads
|
|
678
|
+
let crowd = max(entries, bucket_nest[nested]);
|
|
679
|
+
let total = max(steepness, bucket_nest[nested + 1u]);
|
|
680
|
+
if (crowd <= ${WATER_PARTICLE_CELL_SHARE}u || total == 0u) { continue; }
|
|
681
|
+
share = min(share, ${WATER_PARTICLE_CELL_SHARE}.0 * weight / f32(total));
|
|
682
|
+
if (crowd <= ${WATER_PARTICLE_GATHER_BUDGET}u) { continue; }
|
|
683
|
+
// no more kept than its part of the budget: the octaves of its keys dropped from the top until the rest fit
|
|
684
|
+
let quota = ${WATER_PARTICLE_GATHER_BUDGET}.0 * f32(bucket_weight[at]) / f32(total);
|
|
685
|
+
var below = 0u;
|
|
686
|
+
for (var octave = 0u; octave < ${BINS}u; octave++) { below += bucket_nest[nested + 2u + octave]; }
|
|
687
|
+
var reach = water_particle_crowd_reach(steepness);
|
|
688
|
+
for (var octave = 0u; octave < ${BINS}u && f32(below) > quota; octave++) {
|
|
689
|
+
below -= bucket_nest[nested + 2u + octave];
|
|
690
|
+
reach = select(0.0, 0.5 * reach, octave + 1u < ${BINS}u);
|
|
691
|
+
}
|
|
692
|
+
// drawn while its lot is under the ramp above its share: while its key is under the reach left
|
|
693
|
+
share = min(share, reach * weight / ${1 + WATER_PARTICLE_THINNING_RAMP});
|
|
694
|
+
}
|
|
695
|
+
// non-negative floats order as their bits do
|
|
696
|
+
atomicMin(&runs[2u * draw[2u * i] + 1u], bitcast<u32>(share));
|
|
697
|
+
}
|
|
698
|
+
`, [chunk_water_particle_cells, chunk_folded_workgroup_index]) });
|
|
699
|
+
|
|
700
|
+
const bin_resources = new ShaderResourceSetDescriptor();
|
|
701
|
+
bin_resources.createGroup()
|
|
702
|
+
.addStorageBuffer('next', WATER_PARTICLE_ARRAY_STRUCT)
|
|
703
|
+
.addStorageBuffer('activity', WebGPUArray.u32)
|
|
704
|
+
.addStorageBuffer('buckets', WATER_PARTICLE_BUCKETS_STRUCT, true)
|
|
705
|
+
.addStorageBuffer('slots', WebGPUArray.u32, true)
|
|
706
|
+
.addStorageBuffer('bucket_amplitude', WebGPUArray.from(atomic_u32), true)
|
|
707
|
+
.addStorageBuffer('body_bounds', WebGPUArray.from(atomic_u32), true)
|
|
708
|
+
.addStorageBuffer('draw', WebGPUArray.u32)
|
|
709
|
+
.addStorageBuffer('runs', WebGPUArray.u32)
|
|
710
|
+
.addStorageBuffer('visible', WebGPUArray.from(f32), true);
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Enter each packet drawn of a body simulated this frame in every hash cell its support overlaps: count it into the
|
|
714
|
+
* cell's bucket, keep its slot there, raise the bucket's largest |a| to its own, and mark its level on its body. One
|
|
715
|
+
* atomic each: no compare-exchange loop a crowded bucket would serialize. A paused body's particles take no slot:
|
|
716
|
+
* nothing draws them this frame.
|
|
717
|
+
*
|
|
718
|
+
* Where cells are crowded (the share pass), a run is drawn by the share its most crowded cell leaves it and its
|
|
719
|
+
* front's lot: one decision for all its packets and all their cells, so neither a front nor the surface across cells
|
|
720
|
+
* is holed, and it does not change with the order particles arrive in. One left out takes no slot.
|
|
721
|
+
*/
|
|
722
|
+
export const shader_water_particles_bin = ComputeShader.from({ label: 'Water / particles bin', resources: bin_resources,
|
|
723
|
+
body: CodeChunk.from(/* language=WGSL */ `
|
|
724
|
+
fn water_particle_unslotted(i: u32) {
|
|
725
|
+
for (var corner = 0u; corner < ${WATER_PARTICLE_CORNERS}u; corner++) { slots[i * ${WATER_PARTICLE_CORNERS}u + corner] = ~0u; }
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
@compute @workgroup_size(64)
|
|
729
|
+
fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
|
|
730
|
+
let i = folded_workgroup_index(group) * 64u + lane;
|
|
731
|
+
if (i >= next.count) { return; }
|
|
732
|
+
let drawn = draw[2u * i + 1u];
|
|
733
|
+
let particle = water_particle_drawn(next.elements[i], max(drawn, 1u));
|
|
734
|
+
if (drawn == 0u || (activity[particle.body] & ${WaterActivity.Active}u) == 0u) { water_particle_unslotted(i); return; }
|
|
735
|
+
let cells = water_particle_cells(particle, i);
|
|
736
|
+
let visibility = water_particle_visibility(bitcast<f32>(runs[2u * draw[2u * i] + 1u]), water_particle_lot(particle));
|
|
737
|
+
visible[i] = visibility;
|
|
738
|
+
if (!(visibility > 0.0)) { water_particle_unslotted(i); return; }
|
|
739
|
+
atomicOr(&body_bounds[particle.body * ${STRIDE}u], 1u << cells.level);
|
|
740
|
+
for (var corner = 0u; corner < ${WATER_PARTICLE_CORNERS}u; corner++) {
|
|
741
|
+
var slot = ~0u;
|
|
742
|
+
if (water_particle_has_corner(cells, corner)) {
|
|
743
|
+
let bucket = water_particle_bucket(particle.body, cells.level, water_particle_corner_cell(cells, corner), buckets.count);
|
|
744
|
+
slot = atomicAdd(&buckets.elements[bucket], 1u);
|
|
745
|
+
// non-negative floats order as their bits do
|
|
746
|
+
atomicMax(&bucket_amplitude[bucket], bitcast<u32>(abs(particle.amplitude) * visibility));
|
|
747
|
+
}
|
|
748
|
+
slots[i * ${WATER_PARTICLE_CORNERS}u + corner] = slot;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
`, [chunk_water_particle_cells, chunk_folded_workgroup_index]) });
|
|
752
|
+
|
|
753
|
+
const scatter_resources = new ShaderResourceSetDescriptor();
|
|
754
|
+
scatter_resources.createGroup()
|
|
755
|
+
.addStorageBuffer('next', WATER_PARTICLE_ARRAY_STRUCT)
|
|
756
|
+
.addStorageBuffer('offsets', COLLECTION_U32_STRUCT_READ)
|
|
757
|
+
.addStorageBuffer('slots', WebGPUArray.u32)
|
|
758
|
+
.addStorageBuffer('visible', WebGPUArray.from(f32))
|
|
759
|
+
.addStorageBuffer('draw', WebGPUArray.u32)
|
|
760
|
+
.addStorageBuffer('staging', WebGPUArray.from(WATER_PARTICLE_ENTRY_STRUCT), true);
|
|
761
|
+
|
|
762
|
+
/** Each entry to its bucket's range, in arrival order, at the height its crowd leaves it; the rank pass orders it. */
|
|
763
|
+
export const shader_water_particles_scatter = ComputeShader.from({ label: 'Water / particles scatter', resources: scatter_resources,
|
|
764
|
+
body: CodeChunk.from(/* language=WGSL */ `
|
|
765
|
+
@compute @workgroup_size(64)
|
|
766
|
+
fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
|
|
767
|
+
let i = folded_workgroup_index(group) * 64u + lane;
|
|
768
|
+
if (i >= next.count) { return; }
|
|
769
|
+
let drawn = draw[2u * i + 1u];
|
|
770
|
+
if (drawn == 0u) { return; }
|
|
771
|
+
let particle = water_particle_drawn(next.elements[i], drawn);
|
|
772
|
+
let cells = water_particle_cells(particle, i);
|
|
773
|
+
for (var corner = 0u; corner < ${WATER_PARTICLE_CORNERS}u; corner++) {
|
|
774
|
+
let slot = slots[i * ${WATER_PARTICLE_CORNERS}u + corner];
|
|
775
|
+
if (slot == ~0u) { continue; }
|
|
776
|
+
let cell = water_particle_corner_cell(cells, corner);
|
|
777
|
+
let bucket = water_particle_bucket(particle.body, cells.level, cell, offsets.count);
|
|
778
|
+
var start = 0u;
|
|
779
|
+
if (bucket > 0u) { start = offsets.elements[bucket - 1u]; }
|
|
780
|
+
var entry = cells.entry;
|
|
781
|
+
entry.cell = cell;
|
|
782
|
+
entry.amplitude *= visible[i];
|
|
783
|
+
staging[start + slot] = entry;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
`, [chunk_water_particle_cells, chunk_folded_workgroup_index]) });
|
|
787
|
+
|
|
788
|
+
const rank_resources = new ShaderResourceSetDescriptor();
|
|
789
|
+
rank_resources.createGroup()
|
|
790
|
+
.addStorageBuffer('next', WATER_PARTICLE_ARRAY_STRUCT)
|
|
791
|
+
.addStorageBuffer('offsets', COLLECTION_U32_STRUCT_READ)
|
|
792
|
+
.addStorageBuffer('slots', WebGPUArray.u32)
|
|
793
|
+
.addStorageBuffer('draw', WebGPUArray.u32)
|
|
794
|
+
.addStorageBuffer('staging', WebGPUArray.from(WATER_PARTICLE_ENTRY_STRUCT))
|
|
795
|
+
.addStorageBuffer('entries', WebGPUArray.from(WATER_PARTICLE_ENTRY_STRUCT), true);
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Order every bucket by particle index then cell, so a gather that runs out of budget drops the same
|
|
799
|
+
* entries every frame (the particle array keeps its order from frame to frame). A bucket of more than
|
|
800
|
+
* ${WATER_PARTICLE_RANK_LIMIT} keeps its arrival order: a fixed budget of comparisons per entry.
|
|
801
|
+
*/
|
|
802
|
+
export const shader_water_particles_rank = ComputeShader.from({ label: 'Water / particles rank', resources: rank_resources,
|
|
803
|
+
body: CodeChunk.from(/* language=WGSL */ `
|
|
804
|
+
fn water_entry_before(a: ${WATER_PARTICLE_ENTRY_STRUCT.wgsl_ref}, b: ${WATER_PARTICLE_ENTRY_STRUCT.wgsl_ref}) -> bool {
|
|
805
|
+
if (a.source != b.source) { return a.source < b.source; }
|
|
806
|
+
if (a.cell.x != b.cell.x) { return a.cell.x < b.cell.x; }
|
|
807
|
+
return a.cell.y < b.cell.y;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
@compute @workgroup_size(64)
|
|
811
|
+
fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
|
|
812
|
+
let i = folded_workgroup_index(group) * 64u + lane;
|
|
813
|
+
if (i >= next.count) { return; }
|
|
814
|
+
let drawn = draw[2u * i + 1u];
|
|
815
|
+
if (drawn == 0u) { return; }
|
|
816
|
+
let particle = water_particle_drawn(next.elements[i], drawn);
|
|
817
|
+
let cells = water_particle_cells(particle, i);
|
|
818
|
+
for (var corner = 0u; corner < ${WATER_PARTICLE_CORNERS}u; corner++) {
|
|
819
|
+
let slot = slots[i * ${WATER_PARTICLE_CORNERS}u + corner];
|
|
820
|
+
if (slot == ~0u) { continue; }
|
|
821
|
+
let bucket = water_particle_bucket(particle.body, cells.level, water_particle_corner_cell(cells, corner), offsets.count);
|
|
822
|
+
var start = 0u;
|
|
823
|
+
if (bucket > 0u) { start = offsets.elements[bucket - 1u]; }
|
|
824
|
+
let end = offsets.elements[bucket];
|
|
825
|
+
let entry = staging[start + slot];
|
|
826
|
+
var rank = slot;
|
|
827
|
+
if (end - start <= ${WATER_PARTICLE_RANK_LIMIT}u) {
|
|
828
|
+
rank = 0u;
|
|
829
|
+
for (var j = start; j < end; j++) { rank += select(0u, 1u, water_entry_before(staging[j], entry)); }
|
|
830
|
+
}
|
|
831
|
+
entries[start + rank] = entry;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
`, [chunk_water_particle_cells, chunk_folded_workgroup_index]) });
|
|
835
|
+
|
|
836
|
+
const bounds_resources = new ShaderResourceSetDescriptor();
|
|
837
|
+
bounds_resources.createGroup()
|
|
838
|
+
.addStorageBuffer('next', WATER_PARTICLE_ARRAY_STRUCT)
|
|
839
|
+
.addStorageBuffer('activity', WebGPUArray.u32)
|
|
840
|
+
.addStorageBuffer('offsets', COLLECTION_U32_STRUCT_READ)
|
|
841
|
+
.addStorageBuffer('bucket_amplitude', WebGPUArray.u32)
|
|
842
|
+
.addStorageBuffer('draw', WebGPUArray.u32)
|
|
843
|
+
.addStorageBuffer('body_bounds', WebGPUArray.from(atomic_u32), true);
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Each body's bound per level: the largest of count × max |a| over the buckets its particles are in, which is
|
|
847
|
+
* at least the bucket's Σ|a|. Every particle that reaches a point is in that point's cell's bucket, and neither
|
|
848
|
+
* its height nor its horizontal displacement exceeds |a|, so the bound holds everywhere; a hash collision only
|
|
849
|
+
* adds to it.
|
|
850
|
+
*/
|
|
851
|
+
export const shader_water_particles_bounds = ComputeShader.from({ label: 'Water / particles bounds', resources: bounds_resources,
|
|
852
|
+
body: CodeChunk.from(/* language=WGSL */ `
|
|
853
|
+
@compute @workgroup_size(64)
|
|
854
|
+
fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
|
|
855
|
+
let i = folded_workgroup_index(group) * 64u + lane;
|
|
856
|
+
if (i >= next.count) { return; }
|
|
857
|
+
let drawn = draw[2u * i + 1u];
|
|
858
|
+
if (drawn == 0u) { return; }
|
|
859
|
+
let particle = water_particle_drawn(next.elements[i], drawn);
|
|
860
|
+
// a paused body keeps the bounds its particles last had
|
|
861
|
+
if ((activity[particle.body] & ${WaterActivity.Active}u) == 0u) { return; }
|
|
862
|
+
let cells = water_particle_cells(particle, i);
|
|
863
|
+
var largest = 0.0;
|
|
864
|
+
for (var corner = 0u; corner < ${WATER_PARTICLE_CORNERS}u; corner++) {
|
|
865
|
+
if (!water_particle_has_corner(cells, corner)) { continue; }
|
|
866
|
+
let bucket = water_particle_bucket(particle.body, cells.level, water_particle_corner_cell(cells, corner), offsets.count);
|
|
867
|
+
var start = 0u;
|
|
868
|
+
if (bucket > 0u) { start = offsets.elements[bucket - 1u]; }
|
|
869
|
+
largest = max(largest, f32(offsets.elements[bucket] - start) * bitcast<f32>(bucket_amplitude[bucket]));
|
|
870
|
+
}
|
|
871
|
+
// non-negative floats order as their bits do
|
|
872
|
+
atomicMax(&body_bounds[particle.body * ${STRIDE}u + 1u + cells.level], bitcast<u32>(largest));
|
|
873
|
+
}
|
|
874
|
+
`, [chunk_water_particle_cells, chunk_folded_workgroup_index]) });
|