@woosh/meep-engine 3.68.0 → 3.69.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.
Files changed (190) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/editor/ecs/component/editors/ecs/Trail3DEditor.d.ts +2 -17
  3. package/editor/ecs/component/editors/ecs/Trail3DEditor.d.ts.map +1 -1
  4. package/editor/ecs/component/editors/ecs/Trail3DEditor.js +3 -18
  5. package/package.json +1 -1
  6. package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts +33 -65
  7. package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts.map +1 -1
  8. package/src/engine/graphics/ecs/trail3d/Trail3D.js +37 -245
  9. package/src/engine/graphics/ecs/trail3d/Trail3DFlags.d.ts +0 -4
  10. package/src/engine/graphics/ecs/trail3d/Trail3DFlags.js +4 -10
  11. package/src/engine/graphics/ecs/trail3d/seed_trail_stroke.d.ts +13 -10
  12. package/src/engine/graphics/ecs/trail3d/seed_trail_stroke.d.ts.map +1 -1
  13. package/src/engine/graphics/ecs/trail3d/seed_trail_stroke.js +28 -37
  14. package/src/engine/graphics/ecs/trail3d/trail_knot_count.d.ts +10 -0
  15. package/src/engine/graphics/ecs/trail3d/trail_knot_count.d.ts.map +1 -0
  16. package/src/engine/graphics/ecs/trail3d/trail_knot_count.js +27 -0
  17. package/src/engine/graphics/trail/TrailKnots.d.ts +62 -0
  18. package/src/engine/graphics/trail/TrailKnots.d.ts.map +1 -0
  19. package/src/engine/graphics/trail/TrailKnots.js +137 -0
  20. package/src/engine/graphics/trail/tube/TubeXMaterialSpec.d.ts +1 -1
  21. package/src/engine/graphics/trail/tube/TubeXMaterialSpec.js +1 -1
  22. package/src/engine/graphics3/EntityRowHold.d.ts +48 -0
  23. package/src/engine/graphics3/EntityRowHold.d.ts.map +1 -0
  24. package/src/engine/graphics3/EntityRowHold.js +128 -0
  25. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts +6 -2
  26. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts.map +1 -1
  27. package/src/engine/graphics3/GPUParticleEmitterSystem.js +11 -44
  28. package/src/engine/graphics3/Trail3DSystem.d.ts +69 -35
  29. package/src/engine/graphics3/Trail3DSystem.d.ts.map +1 -1
  30. package/src/engine/graphics3/Trail3DSystem.js +380 -81
  31. package/src/engine/physics/fluid/ecs/FluidObstacle.d.ts +17 -0
  32. package/src/engine/physics/fluid/ecs/FluidObstacle.d.ts.map +1 -1
  33. package/src/engine/physics/fluid/ecs/FluidObstacle.js +22 -2
  34. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +6 -5
  35. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts.map +1 -1
  36. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.js +14 -3
  37. package/src/engine/physics/fluid/obstacle/FLUID_OBSTACLE_RECORD_STRUCT.d.ts +9 -1
  38. package/src/engine/physics/fluid/obstacle/FLUID_OBSTACLE_RECORD_STRUCT.d.ts.map +1 -1
  39. package/src/engine/physics/fluid/obstacle/FLUID_OBSTACLE_RECORD_STRUCT.js +11 -1
  40. package/src/engine/physics/fluid/obstacle/fluid_obstacle_record_create.d.ts +5 -0
  41. package/src/engine/physics/fluid/obstacle/fluid_obstacle_record_create.d.ts.map +1 -1
  42. package/src/engine/physics/fluid/obstacle/fluid_obstacle_record_create.js +2 -0
  43. package/src/engine/physics/fluid/obstacle/fluid_obstacle_write_gpu_record.d.ts +4 -3
  44. package/src/engine/physics/fluid/obstacle/fluid_obstacle_write_gpu_record.d.ts.map +1 -1
  45. package/src/engine/physics/fluid/obstacle/fluid_obstacle_write_gpu_record.js +11 -3
  46. package/src/shade/playground/trails/index.html +50 -0
  47. package/src/shade/playground/trails/main.d.ts +2 -0
  48. package/src/shade/playground/trails/main.d.ts.map +1 -0
  49. package/src/shade/playground/trails/main.js +258 -0
  50. package/src/shade/renderer/animation/animation_curve_write_keys.d.ts +18 -0
  51. package/src/shade/renderer/animation/animation_curve_write_keys.d.ts.map +1 -0
  52. package/src/shade/renderer/animation/animation_curve_write_keys.js +34 -0
  53. package/src/shade/renderer/deferred/tonemap/ColorGrade.d.ts.map +1 -1
  54. package/src/shade/renderer/deferred/tonemap/ColorGrade.js +13 -36
  55. package/src/shade/renderer/dynamic/GPUDynamicMeshResidency.js +2 -2
  56. package/src/shade/renderer/dynamic/chunk_dynamic_mesh_avboit_vertex.d.ts +2 -13
  57. package/src/shade/renderer/dynamic/chunk_dynamic_mesh_avboit_vertex.d.ts.map +1 -1
  58. package/src/shade/renderer/dynamic/chunk_dynamic_mesh_avboit_vertex.js +6 -21
  59. package/src/shade/renderer/dynamic/graph_dynamic_mesh_avboit.d.ts +17 -6
  60. package/src/shade/renderer/dynamic/graph_dynamic_mesh_avboit.d.ts.map +1 -1
  61. package/src/shade/renderer/dynamic/graph_dynamic_mesh_avboit.js +38 -178
  62. package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_draw.d.ts +7 -9
  63. package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_draw.d.ts.map +1 -1
  64. package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_draw.js +17 -106
  65. package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_splat.d.ts +7 -17
  66. package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_splat.d.ts.map +1 -1
  67. package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_splat.js +18 -118
  68. package/src/shade/renderer/fluid/forcing/FLUID_FORCING.d.ts +14 -0
  69. package/src/shade/renderer/fluid/forcing/FLUID_FORCING.d.ts.map +1 -1
  70. package/src/shade/renderer/fluid/forcing/FLUID_FORCING.js +18 -0
  71. package/src/shade/renderer/fluid/forcing/FLUID_FORCING_APPLY_STRUCT.d.ts +3 -1
  72. package/src/shade/renderer/fluid/forcing/FLUID_FORCING_APPLY_STRUCT.d.ts.map +1 -1
  73. package/src/shade/renderer/fluid/forcing/FLUID_FORCING_APPLY_STRUCT.js +4 -1
  74. package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +9 -0
  75. package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts.map +1 -1
  76. package/src/shade/renderer/fluid/forcing/GPUFluidForcing.js +20 -4
  77. package/src/shade/renderer/fluid/forcing/chunk_fluid_forcing_cell.d.ts +11 -2
  78. package/src/shade/renderer/fluid/forcing/chunk_fluid_forcing_cell.d.ts.map +1 -1
  79. package/src/shade/renderer/fluid/forcing/chunk_fluid_forcing_cell.js +59 -2
  80. package/src/shade/renderer/fluid/forcing/shader_fluid_forcing_apply.d.ts +4 -1
  81. package/src/shade/renderer/fluid/forcing/shader_fluid_forcing_apply.d.ts.map +1 -1
  82. package/src/shade/renderer/fluid/forcing/shader_fluid_forcing_apply.js +367 -341
  83. package/src/shade/renderer/fluid/mac/shader_fluid_mac_advect_correct.d.ts +6 -2
  84. package/src/shade/renderer/fluid/mac/shader_fluid_mac_advect_correct.d.ts.map +1 -1
  85. package/src/shade/renderer/fluid/mac/shader_fluid_mac_advect_correct.js +339 -282
  86. package/src/shade/renderer/grass/press/shader_grass_press_mark.d.ts +3 -2
  87. package/src/shade/renderer/grass/press/shader_grass_press_mark.d.ts.map +1 -1
  88. package/src/shade/renderer/grass/press/shader_grass_press_mark.js +5 -3
  89. package/src/shade/renderer/rasterize/native/avboit/avboit_unlit_draw.d.ts +49 -0
  90. package/src/shade/renderer/rasterize/native/avboit/avboit_unlit_draw.d.ts.map +1 -0
  91. package/src/shade/renderer/rasterize/native/avboit/avboit_unlit_draw.js +144 -0
  92. package/src/shade/renderer/rasterize/native/avboit/avboit_unlit_splat.d.ts +71 -0
  93. package/src/shade/renderer/rasterize/native/avboit/avboit_unlit_splat.d.ts.map +1 -0
  94. package/src/shade/renderer/rasterize/native/avboit/avboit_unlit_splat.js +152 -0
  95. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_unlit_varyings.d.ts +21 -0
  96. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_unlit_varyings.d.ts.map +1 -0
  97. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_unlit_varyings.js +30 -0
  98. package/src/shade/renderer/rasterize/native/avboit/graph_avboit_unlit.d.ts +101 -0
  99. package/src/shade/renderer/rasterize/native/avboit/graph_avboit_unlit.d.ts.map +1 -0
  100. package/src/shade/renderer/rasterize/native/avboit/graph_avboit_unlit.js +306 -0
  101. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_splat.d.ts +9 -0
  102. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_splat.d.ts.map +1 -1
  103. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_splat.js +11 -4
  104. package/src/shade/renderer/trails/GPUTrailAVBOITChannel.d.ts +69 -0
  105. package/src/shade/renderer/trails/GPUTrailAVBOITChannel.d.ts.map +1 -0
  106. package/src/shade/renderer/trails/GPUTrailAVBOITChannel.js +129 -0
  107. package/src/shade/renderer/trails/GPUTrailSystem.d.ts +164 -0
  108. package/src/shade/renderer/trails/GPUTrailSystem.d.ts.map +1 -0
  109. package/src/shade/renderer/trails/GPUTrailSystem.js +725 -0
  110. package/src/shade/renderer/trails/data/TRAIL_DATABASE_SPEC.d.ts +14 -0
  111. package/src/shade/renderer/trails/data/TRAIL_DATABASE_SPEC.d.ts.map +1 -0
  112. package/src/shade/renderer/trails/data/TRAIL_DATABASE_SPEC.js +38 -0
  113. package/src/shade/renderer/trails/data/TRAIL_KNOT.d.ts +66 -0
  114. package/src/shade/renderer/trails/data/TRAIL_KNOT.d.ts.map +1 -0
  115. package/src/shade/renderer/trails/data/TRAIL_KNOT.js +154 -0
  116. package/src/shade/renderer/trails/data/TRAIL_RECORD.d.ts +19 -0
  117. package/src/shade/renderer/trails/data/TRAIL_RECORD.d.ts.map +1 -0
  118. package/src/shade/renderer/trails/data/TRAIL_RECORD.js +68 -0
  119. package/src/shade/renderer/trails/data/TRAIL_STATE.d.ts +18 -0
  120. package/src/shade/renderer/trails/data/TRAIL_STATE.d.ts.map +1 -0
  121. package/src/shade/renderer/trails/data/TRAIL_STATE.js +43 -0
  122. package/src/shade/renderer/trails/graph_trails.d.ts +58 -0
  123. package/src/shade/renderer/trails/graph_trails.d.ts.map +1 -0
  124. package/src/shade/renderer/trails/graph_trails.js +155 -0
  125. package/src/shade/renderer/trails/graph_trails_avboit.d.ts +126 -0
  126. package/src/shade/renderer/trails/graph_trails_avboit.d.ts.map +1 -0
  127. package/src/shade/renderer/trails/graph_trails_avboit.js +300 -0
  128. package/src/shade/renderer/trails/runtime/TrailFadeCurves.d.ts +56 -0
  129. package/src/shade/renderer/trails/runtime/TrailFadeCurves.d.ts.map +1 -0
  130. package/src/shade/renderer/trails/runtime/TrailFadeCurves.js +220 -0
  131. package/src/shade/renderer/trails/runtime/TrailKnotPool.d.ts +49 -0
  132. package/src/shade/renderer/trails/runtime/TrailKnotPool.d.ts.map +1 -0
  133. package/src/shade/renderer/trails/runtime/TrailKnotPool.js +162 -0
  134. package/src/shade/renderer/trails/runtime/TrailParameters.d.ts +52 -0
  135. package/src/shade/renderer/trails/runtime/TrailParameters.d.ts.map +1 -0
  136. package/src/shade/renderer/trails/runtime/TrailParameters.js +126 -0
  137. package/src/shade/renderer/trails/shaders/chunk_trail_frame_transport.d.ts +23 -0
  138. package/src/shade/renderer/trails/shaders/chunk_trail_frame_transport.d.ts.map +1 -0
  139. package/src/shade/renderer/trails/shaders/chunk_trail_frame_transport.js +57 -0
  140. package/src/shade/renderer/trails/shaders/chunk_trail_knot_alpha.d.ts +14 -0
  141. package/src/shade/renderer/trails/shaders/chunk_trail_knot_alpha.d.ts.map +1 -0
  142. package/src/shade/renderer/trails/shaders/chunk_trail_knot_alpha.js +40 -0
  143. package/src/shade/renderer/trails/shaders/chunk_trail_seed.d.ts +18 -0
  144. package/src/shade/renderer/trails/shaders/chunk_trail_seed.d.ts.map +1 -0
  145. package/src/shade/renderer/trails/shaders/chunk_trail_seed.js +64 -0
  146. package/src/shade/renderer/trails/shaders/chunk_trail_segment.d.ts +14 -0
  147. package/src/shade/renderer/trails/shaders/chunk_trail_segment.d.ts.map +1 -0
  148. package/src/shade/renderer/trails/shaders/chunk_trail_segment.js +51 -0
  149. package/src/shade/renderer/trails/shaders/chunk_trail_update.d.ts +48 -0
  150. package/src/shade/renderer/trails/shaders/chunk_trail_update.d.ts.map +1 -0
  151. package/src/shade/renderer/trails/shaders/chunk_trail_update.js +197 -0
  152. package/src/shade/renderer/trails/shaders/chunk_trail_vertex.d.ts +34 -0
  153. package/src/shade/renderer/trails/shaders/chunk_trail_vertex.d.ts.map +1 -0
  154. package/src/shade/renderer/trails/shaders/chunk_trail_vertex.js +111 -0
  155. package/src/shade/renderer/trails/shaders/shader_trail_avboit.d.ts +24 -0
  156. package/src/shade/renderer/trails/shaders/shader_trail_avboit.d.ts.map +1 -0
  157. package/src/shade/renderer/trails/shaders/shader_trail_avboit.js +78 -0
  158. package/src/shade/renderer/trails/shaders/shader_trail_avboit_segments.d.ts +23 -0
  159. package/src/shade/renderer/trails/shaders/shader_trail_avboit_segments.d.ts.map +1 -0
  160. package/src/shade/renderer/trails/shaders/shader_trail_avboit_segments.js +130 -0
  161. package/src/shade/renderer/trails/shaders/shader_trail_draw_args.d.ts +29 -0
  162. package/src/shade/renderer/trails/shaders/shader_trail_draw_args.d.ts.map +1 -0
  163. package/src/shade/renderer/trails/shaders/shader_trail_draw_args.js +78 -0
  164. package/src/shade/renderer/trails/shaders/shader_trail_seed.d.ts +18 -0
  165. package/src/shade/renderer/trails/shaders/shader_trail_seed.d.ts.map +1 -0
  166. package/src/shade/renderer/trails/shaders/shader_trail_seed.js +79 -0
  167. package/src/shade/renderer/trails/shaders/shader_trail_update.d.ts +20 -0
  168. package/src/shade/renderer/trails/shaders/shader_trail_update.d.ts.map +1 -0
  169. package/src/shade/renderer/trails/shaders/shader_trail_update.js +101 -0
  170. package/src/engine/graphics/ecs/trail3d/advance_trail.d.ts +0 -18
  171. package/src/engine/graphics/ecs/trail3d/advance_trail.d.ts.map +0 -1
  172. package/src/engine/graphics/ecs/trail3d/advance_trail.js +0 -42
  173. package/src/engine/graphics/ecs/trail3d/seed_trail_tube.d.ts +0 -14
  174. package/src/engine/graphics/ecs/trail3d/seed_trail_tube.d.ts.map +0 -1
  175. package/src/engine/graphics/ecs/trail3d/seed_trail_tube.js +0 -75
  176. package/src/engine/graphics/ecs/trail3d/trail_head_position.d.ts +0 -17
  177. package/src/engine/graphics/ecs/trail3d/trail_head_position.d.ts.map +0 -1
  178. package/src/engine/graphics/ecs/trail3d/trail_head_position.js +0 -19
  179. package/src/engine/graphics/trail/tube/TubeX.d.ts +0 -186
  180. package/src/engine/graphics/trail/tube/TubeX.d.ts.map +0 -1
  181. package/src/engine/graphics/trail/tube/TubeX.js +0 -575
  182. package/src/engine/graphics/trail/tube/simulator/TubeXFixedPhysicsSimulator.d.ts +0 -16
  183. package/src/engine/graphics/trail/tube/simulator/TubeXFixedPhysicsSimulator.d.ts.map +0 -1
  184. package/src/engine/graphics/trail/tube/simulator/TubeXFixedPhysicsSimulator.js +0 -34
  185. package/src/engine/graphics/trail/tube/tube_attributes_spec.d.ts +0 -27
  186. package/src/engine/graphics/trail/tube/tube_attributes_spec.d.ts.map +0 -1
  187. package/src/engine/graphics/trail/tube/tube_attributes_spec.js +0 -70
  188. package/src/engine/graphics3/trail/trail_tube_write_geometry.d.ts +0 -38
  189. package/src/engine/graphics3/trail/trail_tube_write_geometry.d.ts.map +0 -1
  190. package/src/engine/graphics3/trail/trail_tube_write_geometry.js +0 -119
@@ -1,341 +1,367 @@
1
- import { chunk_fluid_effector_record_resolve } from "../../../../engine/physics/fluid/effector/record/chunk_fluid_effector_record_resolve.js";
2
- import { FLUID_EFFECTOR_RECORD_STRUCT } from "../../../../engine/physics/fluid/effector/record/FLUID_EFFECTOR_RECORD_STRUCT.js";
3
- import { FLUID_OBSTACLE_RECORD_STRUCT } from "../../../../engine/physics/fluid/obstacle/FLUID_OBSTACLE_RECORD_STRUCT.js";
4
- import { WebGPUExtensionType } from "../../../descriptor/WebGPUExtensionType.js";
5
- import { COLLECTION_U32_STRUCT_READ } from "../../gpu_primitive/COLLECTION_U32_STRUCT_READ.js";
6
- import { TRANSFORMS_TABLE } from "../../scene/chunk_scene_database_access.js";
7
- import { chunk_folded_workgroup_index } from "../../shader/chunk/grid/chunk_folded_workgroup_index.js";
8
- import { chunk_grid3d_to_index } from "../../shader/chunk/grid/chunk_grid3d_to_index.js";
9
- import { chunk_grid_tile_window } from "../../shader/chunk/grid/chunk_grid_tile_window.js";
10
- import { chunk_index_to_grid3d } from "../../shader/chunk/grid/chunk_index_to_grid3d.js";
11
- import { ComputeShader } from "../../shader/ComputeShader.js";
12
- import { CodeChunk } from "../../shader/compiler/CodeChunk.js";
13
- import { ShaderResourceSetDescriptor } from "../../shader/resource/ShaderResourceSetDescriptor.js";
14
- import { f32, u32 } from "../../shader/type/WEBGPU_WGSL_PRIMITIVE_TYPES.js";
15
- import { WebGPUArray } from "../../shader/type/WebGPUArray.js";
16
- import { chunk_fluid_forcing_cell } from "./chunk_fluid_forcing_cell.js";
17
- import {
18
- FLUID_EFFECTOR_DESCRIPTOR,
19
- FLUID_OBSTACLE_DESCRIPTOR,
20
- FLUID_OBSTACLE_PLANE_DESCRIPTOR
21
- } from "./FLUID_DATABASE_SPEC.js";
22
- import { FLUID_FORCING_BRICK } from "./FLUID_FORCING.js";
23
- import { FLUID_FORCING_APPLY_STRUCT } from "./FLUID_FORCING_APPLY_STRUCT.js";
24
-
25
- /**
26
- * Cells of the block a forcing workgroup tests for solids: its brick and the layer before it on every
27
- * axis, whose cells pin the brick's −faces.
28
- * @type {number}
29
- */
30
- const SOLID_BLOCK = FLUID_FORCING_BRICK + 1;
31
-
32
- const resources = new ShaderResourceSetDescriptor();
33
-
34
- resources.createGroup()
35
- .addUniform("settings", FLUID_FORCING_APPLY_STRUCT)
36
- .addStorageBuffer("fluid_database", WebGPUArray.from(u32), false)
37
- .addStorageBuffer("scene_database", WebGPUArray.from(u32), false)
38
- .addStorageBuffer("list", COLLECTION_U32_STRUCT_READ, false)
39
- .addStorageBuffer("survivors", WebGPUArray.from(u32), false)
40
- .addStorageBuffer("leaf_rows", WebGPUArray.from(u32), false)
41
- // the cascade's solid flags, window-local
42
- .addStorageBuffer("solid", WebGPUArray.from(u32), true)
43
- // the tick's forcing: u faces, then v from settings.forcing_v, then w from settings.forcing_w
44
- .addStorageBuffer("forcing", WebGPUArray.from(f32), true)
45
- // the cascade's persistent faces, toroidal, where a pinned face takes its wall's velocity
46
- .addStorageTexture("out_u", "r32float", "3d", "write-only")
47
- .addStorageTexture("out_v", "r32float", "3d", "write-only")
48
- .addStorageTexture("out_w", "r32float", "3d", "write-only");
49
-
50
- const body = CodeChunk.from(
51
- //language=WGSL
52
- `
53
- const SOLID_BLOCK: u32 = ${SOLID_BLOCK}u;
54
- const SOLID_BLOCK_CELLS: u32 = ${SOLID_BLOCK ** 3}u;
55
-
56
- var<workgroup> fluid_forcing_obstacles: array<${FLUID_OBSTACLE_RECORD_STRUCT.wgsl_ref}, FLUID_FORCING_OBSTACLE_LIMIT>;
57
- var<workgroup> fluid_forcing_globals: array<mat4x4<f32>, FLUID_FORCING_OBSTACLE_LIMIT>;
58
- var<workgroup> fluid_forcing_effectors: array<${FLUID_EFFECTOR_RECORD_STRUCT.wgsl_ref}, FLUID_FORCING_EFFECTOR_LIMIT>;
59
- // the slot of the obstacle each cell of the block is solid in, or FLUID_FORCING_NONE
60
- var<workgroup> fluid_forcing_solid_slot: array<u32, SOLID_BLOCK_CELLS>;
61
-
62
- fn fluid_forcing_obstacle(slot: u32) -> ${FLUID_OBSTACLE_RECORD_STRUCT.wgsl_ref} {
63
- return fluid_forcing_obstacles[slot];
64
- }
65
-
66
- fn fluid_forcing_obstacle_global(slot: u32) -> mat4x4<f32> {
67
- return fluid_forcing_globals[slot];
68
- }
69
-
70
- fn fluid_forcing_effector(slot: u32) -> ${FLUID_EFFECTOR_RECORD_STRUCT.wgsl_ref} {
71
- return fluid_forcing_effectors[slot];
72
- }
73
-
74
- fn shape_record_plane(index: u32) -> vec4<f32> {
75
- return ${FLUID_OBSTACLE_PLANE_DESCRIPTOR.marshalling_method_read_field('plane')}(&fluid_database, index);
76
- }
77
-
78
- // Solid test using the pre-normalised rotation columns stored in fluid_forcing_globals[slot]
79
- fn fluid_forcing_solid_prenorm(x: vec3<f32>, count: u32) -> u32 {
80
- var found = FLUID_FORCING_NONE;
81
-
82
- for (var slot = 0u; slot < FLUID_FORCING_OBSTACLE_LIMIT; slot++) {
83
- if (slot >= count) {
84
- break;
85
- }
86
-
87
- let g = fluid_forcing_globals[slot];
88
- let offset = x - g[3].xyz;
89
- let node = vec3<f32>(
90
- dot(g[0].xyz, offset),
91
- dot(g[1].xyz, offset),
92
- dot(g[2].xyz, offset),
93
- );
94
-
95
- let q = fluid_forcing_obstacles[slot].pose_rotation;
96
- let local = quat_rotate_v3(vec4<f32>(-q.xyz, q.w), node - fluid_forcing_obstacles[slot].pose_translation);
97
- let d = shape_record_signed_distance(fluid_forcing_obstacles[slot].shape_kind, fluid_forcing_obstacles[slot].shape_parameters, local);
98
-
99
- if (d <= fluid_forcing_obstacles[slot].inflation) {
100
- found = slot;
101
- break;
102
- }
103
- }
104
-
105
- return found;
106
- }
107
-
108
- // the slot a brick-local cell (-1 to 3 on each axis) is solid in
109
- fn fluid_forcing_block_slot(local: vec3<i32>) -> u32 {
110
- return fluid_forcing_solid_slot[grid3d_to_index(vec3<u32>(local + vec3<i32>(1)), vec2<u32>(SOLID_BLOCK, SOLID_BLOCK))];
111
- }
112
-
113
- fn fluid_forcing_face_world(face: vec3<f32>) -> vec3<f32> {
114
- return fluid_forcing_world(settings.cells.corner, settings.cell_size, face);
115
- }
116
-
117
- // A face between the cells whose solids are minus and plus: pinned when either is solid, and then it
118
- // takes the wall's velocity in the persistent faces, the solid cell's obstacle's, the + side's when
119
- // both are; otherwise its forcing is what the effectors deposit there.
120
- fn fluid_forcing_face_u(face: vec3<i32>, minus: u32, plus: u32, effector_count: u32) {
121
- let index = grid3d_to_index(vec3<u32>(face), settings.faces_u.size.xy);
122
- let wall = select(minus, plus, plus != FLUID_FORCING_NONE);
123
-
124
- if (wall != FLUID_FORCING_NONE) {
125
- let x = fluid_forcing_face_world(vec3<f32>(face) - vec3<f32>(0.5, 0.0, 0.0));
126
- let velocity = fluid_forcing_wall_velocity(wall, x);
127
-
128
- textureStore(out_u, grid_tile_window_storage(face, settings.faces_u.corner_storage, settings.faces_u.period), vec4<f32>(velocity.x, 0.0, 0.0, 0.0));
129
- forcing[index] = 0.0;
130
- } else if (effector_count > 0u) {
131
- let x = fluid_forcing_face_world(vec3<f32>(face) - vec3<f32>(0.5, 0.0, 0.0));
132
- forcing[index] = fluid_forcing_deposit(x, settings.dt, settings.cell_size, effector_count).x;
133
- } else {
134
- forcing[index] = 0.0;
135
- }
136
- }
137
-
138
- fn fluid_forcing_face_v(face: vec3<i32>, minus: u32, plus: u32, effector_count: u32) {
139
- let index = settings.forcing_v + grid3d_to_index(vec3<u32>(face), settings.faces_v.size.xy);
140
- let wall = select(minus, plus, plus != FLUID_FORCING_NONE);
141
-
142
- if (wall != FLUID_FORCING_NONE) {
143
- let x = fluid_forcing_face_world(vec3<f32>(face) - vec3<f32>(0.0, 0.5, 0.0));
144
- let velocity = fluid_forcing_wall_velocity(wall, x);
145
-
146
- textureStore(out_v, grid_tile_window_storage(face, settings.faces_v.corner_storage, settings.faces_v.period), vec4<f32>(velocity.y, 0.0, 0.0, 0.0));
147
- forcing[index] = 0.0;
148
- } else if (effector_count > 0u) {
149
- let x = fluid_forcing_face_world(vec3<f32>(face) - vec3<f32>(0.0, 0.5, 0.0));
150
- forcing[index] = fluid_forcing_deposit(x, settings.dt, settings.cell_size, effector_count).y;
151
- } else {
152
- forcing[index] = 0.0;
153
- }
154
- }
155
-
156
- fn fluid_forcing_face_w(face: vec3<i32>, minus: u32, plus: u32, effector_count: u32) {
157
- let index = settings.forcing_w + grid3d_to_index(vec3<u32>(face), settings.faces_w.size.xy);
158
- let wall = select(minus, plus, plus != FLUID_FORCING_NONE);
159
-
160
- if (wall != FLUID_FORCING_NONE) {
161
- let x = fluid_forcing_face_world(vec3<f32>(face) - vec3<f32>(0.0, 0.0, 0.5));
162
- let velocity = fluid_forcing_wall_velocity(wall, x);
163
-
164
- textureStore(out_w, grid_tile_window_storage(face, settings.faces_w.corner_storage, settings.faces_w.period), vec4<f32>(velocity.z, 0.0, 0.0, 0.0));
165
- forcing[index] = 0.0;
166
- } else if (effector_count > 0u) {
167
- let x = fluid_forcing_face_world(vec3<f32>(face) - vec3<f32>(0.0, 0.0, 0.5));
168
- forcing[index] = fluid_forcing_deposit(x, settings.dt, settings.cell_size, effector_count).z;
169
- } else {
170
- forcing[index] = 0.0;
171
- }
172
- }
173
-
174
- @compute @workgroup_size(${FLUID_FORCING_BRICK}, ${FLUID_FORCING_BRICK}, ${FLUID_FORCING_BRICK})
175
- fn main(
176
- @builtin(workgroup_id) workgroup_id: vec3<u32>,
177
- @builtin(local_invocation_id) local: vec3<u32>,
178
- @builtin(local_invocation_index) lane: u32,
179
- ) {
180
- let entry = folded_workgroup_index(workgroup_id);
181
-
182
- if (entry >= list.count) {
183
- return;
184
- }
185
-
186
- let query = list.elements[entry];
187
- let base = query * FLUID_FORCING_SURVIVOR_STRIDE;
188
- let header = survivors[base];
189
- let obstacle_count = header & 0xffffu;
190
- let effector_count = header >> 16u;
191
- let origin = vec3<i32>(index_to_grid3d(query - settings.query_base, settings.bricks.xy) * FLUID_FORCING_BRICK);
192
- let brick_local = vec3<i32>(local);
193
- let cell = origin + brick_local;
194
- let flags = grid3d_to_index(vec3<u32>(cell), settings.cells.size.xy);
195
-
196
- // listed only to clear what an earlier tick left: fluid, and no forcing to read
197
- if (obstacle_count + effector_count == 0u) {
198
- solid[flags] = 0u;
199
-
200
- return;
201
- }
202
-
203
- // the brick's survivors into workgroup memory, posed: obstacles on lanes 0..23, effectors on 32..47
204
- if (lane < obstacle_count) {
205
- let row = leaf_rows[survivors[base + 1u + lane]];
206
- let record = ${FLUID_OBSTACLE_DESCRIPTOR.marshalling_method_read}(&fluid_database, row);
207
- let g = ${TRANSFORMS_TABLE.marshalling_method_read_field('global')}(&scene_database, record.node);
208
-
209
- fluid_forcing_obstacles[lane] = record;
210
- fluid_forcing_globals[lane] = mat4x4<f32>(
211
- vec4<f32>(g[0].xyz * (1.0 / length(g[0].xyz)), 0.0),
212
- vec4<f32>(g[1].xyz * (1.0 / length(g[1].xyz)), 0.0),
213
- vec4<f32>(g[2].xyz * (1.0 / length(g[2].xyz)), 0.0),
214
- g[3]
215
- );
216
- }
217
-
218
- if (lane >= 32u && lane - 32u < effector_count) {
219
- let slot = lane - 32u;
220
- let row = leaf_rows[survivors[base + 1u + FLUID_FORCING_OBSTACLE_LIMIT + slot]];
221
- let record = ${FLUID_EFFECTOR_DESCRIPTOR.marshalling_method_read}(&fluid_database, row);
222
-
223
- fluid_forcing_effectors[slot] = fluid_effector_record_resolve(
224
- record,
225
- ${TRANSFORMS_TABLE.marshalling_method_read_field('global')}(&scene_database, record.node),
226
- ${TRANSFORMS_TABLE.marshalling_method_read_field('prev_global')}(&scene_database, record.node),
227
- settings.inverse_frame_dt
228
- );
229
- }
230
-
231
- workgroupBarrier();
232
-
233
- var own = FLUID_FORCING_NONE;
234
- var minus_x = FLUID_FORCING_NONE;
235
- var minus_y = FLUID_FORCING_NONE;
236
- var minus_z = FLUID_FORCING_NONE;
237
-
238
- // the solid test at every cell centre of the brick and the layer before it; a cell before the
239
- // window is fluid, as every pass of the step reads it
240
- if (obstacle_count > 0u) {
241
- for (var round = 0u; round < 2u; round++) {
242
- let k = round * FLUID_FORCING_BRICK_CELLS + lane;
243
-
244
- if (k < SOLID_BLOCK_CELLS) {
245
- let block_cell = origin - vec3<i32>(1) + vec3<i32>(index_to_grid3d(k, vec2<u32>(SOLID_BLOCK, SOLID_BLOCK)));
246
-
247
- var slot = FLUID_FORCING_NONE;
248
-
249
- if (all(block_cell >= vec3<i32>(0))) {
250
- slot = fluid_forcing_solid_prenorm(fluid_forcing_face_world(vec3<f32>(block_cell)), obstacle_count);
251
- }
252
-
253
- fluid_forcing_solid_slot[k] = slot;
254
- }
255
- }
256
-
257
- workgroupBarrier();
258
-
259
- own = fluid_forcing_block_slot(brick_local);
260
- minus_x = fluid_forcing_block_slot(brick_local - vec3<i32>(1, 0, 0));
261
- minus_y = fluid_forcing_block_slot(brick_local - vec3<i32>(0, 1, 0));
262
- minus_z = fluid_forcing_block_slot(brick_local - vec3<i32>(0, 0, 1));
263
- }
264
-
265
- solid[flags] = FLUID_FORCED_FLAG | select(0u, FLUID_SOLID_FLAG, own != FLUID_FORCING_NONE);
266
-
267
- // the cell's -x, -y and -z faces
268
- fluid_forcing_face_u(cell, minus_x, own, effector_count);
269
- fluid_forcing_face_v(cell, minus_y, own, effector_count);
270
- fluid_forcing_face_w(cell, minus_z, own, effector_count);
271
-
272
- // the +boundary face planes, owned by the window's last cells; the cell beyond is fluid
273
- let last = vec3<i32>(settings.cells.size) - vec3<i32>(1);
274
-
275
- if (cell.x == last.x) {
276
- fluid_forcing_face_u(cell + vec3<i32>(1, 0, 0), own, FLUID_FORCING_NONE, effector_count);
277
- }
278
- if (cell.y == last.y) {
279
- fluid_forcing_face_v(cell + vec3<i32>(0, 1, 0), own, FLUID_FORCING_NONE, effector_count);
280
- }
281
- if (cell.z == last.z) {
282
- fluid_forcing_face_w(cell + vec3<i32>(0, 0, 1), own, FLUID_FORCING_NONE, effector_count);
283
- }
284
- }
285
- `, [
286
- chunk_folded_workgroup_index,
287
- chunk_index_to_grid3d,
288
- chunk_grid3d_to_index,
289
- chunk_grid_tile_window,
290
- chunk_fluid_forcing_cell,
291
- chunk_fluid_effector_record_resolve,
292
- FLUID_OBSTACLE_DESCRIPTOR.chunk_read,
293
- FLUID_EFFECTOR_DESCRIPTOR.chunk_read,
294
- FLUID_OBSTACLE_PLANE_DESCRIPTOR.chunk_read_field('plane'),
295
- TRANSFORMS_TABLE.chunk_read_field('global'),
296
- TRANSFORMS_TABLE.chunk_read_field('prev_global'),
297
- ]);
298
-
299
- body.addExtension(WebGPUExtensionType.Subgroups);
300
-
301
- /**
302
- * Q3 of a tick, the forcing (GPU_FLUID_CASCADES_PROPOSAL_2026_10_16.md §10), for one stepping
303
- * cascade: one workgroup of 4×4×4 for each brick its query listed, dispatched indirectly from Q2's
304
- * command, the brick found by `folded_workgroup_index`.
305
- *
306
- * 1. **The survivors, posed:** the brick's obstacle records with their nodes' `global` (rotation
307
- * columns pre-normalised), and its effector records brought to the world
308
- * (`fluid_effector_record_resolve` with the node's `global` and `prev_global`), into workgroup
309
- * memory; one barrier.
310
- * 2. **The solid test** (`fluid_forcing_solid_prenorm`) at the centre of every cell of the brick and
311
- * the layer before it on each axis (125 cells, two rounds), recording the solid's obstacle; a cell
312
- * before the window is fluid, as the step reads it. One barrier.
313
- * 3. **Per cell:** its solid flags, `FLUID_FORCED_FLAG` with `FLUID_SOLID_FLAG` where solid; then each
314
- * of its −x, −y, −z faces, and the window's last cells their +boundary faces: a face with a solid
315
- * cell on either side is pinned, as every pass of the step will find it from the flags, and takes
316
- * the wall's velocity (`fluid_forcing_wall_velocity`, the solid cell's obstacle, the + side's when
317
- * both are) **in the cascade's persistent faces**, before the step reads them, with forcing 0;
318
- * any other face's forcing is the effectors' deposit over `dt_c` on `h_c`
319
- * (`fluid_forcing_deposit`).
320
- *
321
- * Writing the wall's velocity into the persistent faces is what makes a face that just became pinned
322
- * carry the wall, never the fluid value it held: advection reads it as a boundary value and every pass
323
- * carries it through. A brick listed only because its flags held an earlier tick's has no survivors
324
- * and writes its cells' flags to 0, which is also what leaves no reader of this tick's forcing there.
325
- *
326
- * Per invocation: two solid tests of at most 24 fields (768 hull planes at worst), three to six faces
327
- * of at most 16 kernels. Workgroup memory: 24 records and matrices, 16 records, 125 slots, 4,724
328
- * bytes.
329
- *
330
- * Bindings: `settings` ({@link FLUID_FORCING_APPLY_STRUCT}); `fluid_database` and `scene_database`;
331
- * `list`, `survivors` and `leaf_rows` from the tick's query and the frame's leaf list; `solid`, the
332
- * cascade's flags; `forcing`, the tick's; `out_u`, `out_v`, `out_w`, the cascade's persistent faces.
333
- * Seven storage buffers and three storage textures.
334
- *
335
- * @type {ComputeShader}
336
- */
337
- export const shader_fluid_forcing_apply = ComputeShader.from({
338
- label: "Fluid / forcing / apply",
339
- resources,
340
- body,
341
- });
1
+ import { chunk_fluid_effector_record_resolve } from "../../../../engine/physics/fluid/effector/record/chunk_fluid_effector_record_resolve.js";
2
+ import { FLUID_EFFECTOR_RECORD_STRUCT } from "../../../../engine/physics/fluid/effector/record/FLUID_EFFECTOR_RECORD_STRUCT.js";
3
+ import { FLUID_OBSTACLE_RECORD_STRUCT } from "../../../../engine/physics/fluid/obstacle/FLUID_OBSTACLE_RECORD_STRUCT.js";
4
+ import { WebGPUExtensionType } from "../../../descriptor/WebGPUExtensionType.js";
5
+ import { COLLECTION_U32_STRUCT_READ } from "../../gpu_primitive/COLLECTION_U32_STRUCT_READ.js";
6
+ import { TRANSFORMS_TABLE } from "../../scene/chunk_scene_database_access.js";
7
+ import { chunk_folded_workgroup_index } from "../../shader/chunk/grid/chunk_folded_workgroup_index.js";
8
+ import { chunk_grid3d_to_index } from "../../shader/chunk/grid/chunk_grid3d_to_index.js";
9
+ import { chunk_grid_tile_window } from "../../shader/chunk/grid/chunk_grid_tile_window.js";
10
+ import { chunk_index_to_grid3d } from "../../shader/chunk/grid/chunk_index_to_grid3d.js";
11
+ import { ComputeShader } from "../../shader/ComputeShader.js";
12
+ import { CodeChunk } from "../../shader/compiler/CodeChunk.js";
13
+ import { ShaderResourceSetDescriptor } from "../../shader/resource/ShaderResourceSetDescriptor.js";
14
+ import { f32, u32 } from "../../shader/type/WEBGPU_WGSL_PRIMITIVE_TYPES.js";
15
+ import { WebGPUArray } from "../../shader/type/WebGPUArray.js";
16
+ import { chunk_fluid_forcing_cell } from "./chunk_fluid_forcing_cell.js";
17
+ import {
18
+ FLUID_EFFECTOR_DESCRIPTOR,
19
+ FLUID_OBSTACLE_DESCRIPTOR,
20
+ FLUID_OBSTACLE_PLANE_DESCRIPTOR
21
+ } from "./FLUID_DATABASE_SPEC.js";
22
+ import { FLUID_FORCING_BRICK } from "./FLUID_FORCING.js";
23
+ import { FLUID_FORCING_APPLY_STRUCT } from "./FLUID_FORCING_APPLY_STRUCT.js";
24
+
25
+ /**
26
+ * Cells of the block a forcing workgroup tests for solids: its brick and the layer before it on every
27
+ * axis, whose cells pin the brick's −faces.
28
+ * @type {number}
29
+ */
30
+ const SOLID_BLOCK = FLUID_FORCING_BRICK + 1;
31
+
32
+ const resources = new ShaderResourceSetDescriptor();
33
+
34
+ resources.createGroup()
35
+ .addUniform("settings", FLUID_FORCING_APPLY_STRUCT)
36
+ .addStorageBuffer("fluid_database", WebGPUArray.from(u32), false)
37
+ .addStorageBuffer("scene_database", WebGPUArray.from(u32), false)
38
+ .addStorageBuffer("list", COLLECTION_U32_STRUCT_READ, false)
39
+ .addStorageBuffer("survivors", WebGPUArray.from(u32), false)
40
+ .addStorageBuffer("leaf_rows", WebGPUArray.from(u32), false)
41
+ // the cascade's solid flags, window-local
42
+ .addStorageBuffer("solid", WebGPUArray.from(u32), true)
43
+ // the tick's forcing: u faces, then v from settings.forcing_v, then w from settings.forcing_w,
44
+ // then the cells' drag from settings.forcing_porous
45
+ .addStorageBuffer("forcing", WebGPUArray.from(f32), true)
46
+ // the cascade's persistent faces, toroidal, where a pinned face takes its wall's velocity
47
+ .addStorageTexture("out_u", "r32float", "3d", "write-only")
48
+ .addStorageTexture("out_v", "r32float", "3d", "write-only")
49
+ .addStorageTexture("out_w", "r32float", "3d", "write-only");
50
+
51
+ const body = CodeChunk.from(
52
+ //language=WGSL
53
+ `
54
+ const SOLID_BLOCK: u32 = ${SOLID_BLOCK}u;
55
+ const SOLID_BLOCK_CELLS: u32 = ${SOLID_BLOCK ** 3}u;
56
+
57
+ var<workgroup> fluid_forcing_obstacles: array<${FLUID_OBSTACLE_RECORD_STRUCT.wgsl_ref}, FLUID_FORCING_OBSTACLE_LIMIT>;
58
+ var<workgroup> fluid_forcing_globals: array<mat4x4<f32>, FLUID_FORCING_OBSTACLE_LIMIT>;
59
+ var<workgroup> fluid_forcing_effectors: array<${FLUID_EFFECTOR_RECORD_STRUCT.wgsl_ref}, FLUID_FORCING_EFFECTOR_LIMIT>;
60
+ // the slot of the obstacle each cell of the block is solid in, or FLUID_FORCING_NONE
61
+ var<workgroup> fluid_forcing_solid_slot: array<u32, SOLID_BLOCK_CELLS>;
62
+
63
+ fn fluid_forcing_obstacle(slot: u32) -> ${FLUID_OBSTACLE_RECORD_STRUCT.wgsl_ref} {
64
+ return fluid_forcing_obstacles[slot];
65
+ }
66
+
67
+ fn fluid_forcing_obstacle_global(slot: u32) -> mat4x4<f32> {
68
+ return fluid_forcing_globals[slot];
69
+ }
70
+
71
+ fn fluid_forcing_effector(slot: u32) -> ${FLUID_EFFECTOR_RECORD_STRUCT.wgsl_ref} {
72
+ return fluid_forcing_effectors[slot];
73
+ }
74
+
75
+ fn shape_record_plane(index: u32) -> vec4<f32> {
76
+ return ${FLUID_OBSTACLE_PLANE_DESCRIPTOR.marshalling_method_read_field('plane')}(&fluid_database, index);
77
+ }
78
+
79
+ // Solid test using the pre-normalised rotation columns stored in fluid_forcing_globals[slot]
80
+ fn fluid_forcing_solid_prenorm(x: vec3<f32>, count: u32) -> u32 {
81
+ var found = FLUID_FORCING_NONE;
82
+
83
+ for (var slot = 0u; slot < FLUID_FORCING_OBSTACLE_LIMIT; slot++) {
84
+ if (slot >= count) {
85
+ break;
86
+ }
87
+
88
+ if (fluid_forcing_obstacles[slot].drag != 0.0) {
89
+ continue;
90
+ }
91
+
92
+ let g = fluid_forcing_globals[slot];
93
+ let offset = x - g[3].xyz;
94
+ let node = vec3<f32>(
95
+ dot(g[0].xyz, offset),
96
+ dot(g[1].xyz, offset),
97
+ dot(g[2].xyz, offset),
98
+ );
99
+
100
+ let q = fluid_forcing_obstacles[slot].pose_rotation;
101
+ let local = quat_rotate_v3(vec4<f32>(-q.xyz, q.w), node - fluid_forcing_obstacles[slot].pose_translation);
102
+ let d = shape_record_signed_distance(fluid_forcing_obstacles[slot].shape_kind, fluid_forcing_obstacles[slot].shape_parameters, local);
103
+
104
+ if (d <= fluid_forcing_obstacles[slot].inflation) {
105
+ found = slot;
106
+ break;
107
+ }
108
+ }
109
+
110
+ return found;
111
+ }
112
+
113
+ // the slot a brick-local cell (-1 to 3 on each axis) is solid in
114
+ fn fluid_forcing_block_slot(local: vec3<i32>) -> u32 {
115
+ return fluid_forcing_solid_slot[grid3d_to_index(vec3<u32>(local + vec3<i32>(1)), vec2<u32>(SOLID_BLOCK, SOLID_BLOCK))];
116
+ }
117
+
118
+ fn fluid_forcing_face_world(face: vec3<f32>) -> vec3<f32> {
119
+ return fluid_forcing_world(settings.cells.corner, settings.cell_size, face);
120
+ }
121
+
122
+ // A face between the cells whose solids are minus and plus: pinned when either is solid, and then it
123
+ // takes the wall's velocity in the persistent faces, the solid cell's obstacle's, the + side's when
124
+ // both are; otherwise its forcing is what the effectors deposit there.
125
+ fn fluid_forcing_face_u(face: vec3<i32>, minus: u32, plus: u32, effector_count: u32) {
126
+ let index = grid3d_to_index(vec3<u32>(face), settings.faces_u.size.xy);
127
+ let wall = select(minus, plus, plus != FLUID_FORCING_NONE);
128
+
129
+ if (wall != FLUID_FORCING_NONE) {
130
+ let x = fluid_forcing_face_world(vec3<f32>(face) - vec3<f32>(0.5, 0.0, 0.0));
131
+ let velocity = fluid_forcing_wall_velocity(wall, x);
132
+
133
+ textureStore(out_u, grid_tile_window_storage(face, settings.faces_u.corner_storage, settings.faces_u.period), vec4<f32>(velocity.x, 0.0, 0.0, 0.0));
134
+ forcing[index] = 0.0;
135
+ } else if (effector_count > 0u) {
136
+ let x = fluid_forcing_face_world(vec3<f32>(face) - vec3<f32>(0.5, 0.0, 0.0));
137
+ forcing[index] = fluid_forcing_deposit(x, settings.dt, settings.cell_size, effector_count).x;
138
+ } else {
139
+ forcing[index] = 0.0;
140
+ }
141
+ }
142
+
143
+ fn fluid_forcing_face_v(face: vec3<i32>, minus: u32, plus: u32, effector_count: u32) {
144
+ let index = settings.forcing_v + grid3d_to_index(vec3<u32>(face), settings.faces_v.size.xy);
145
+ let wall = select(minus, plus, plus != FLUID_FORCING_NONE);
146
+
147
+ if (wall != FLUID_FORCING_NONE) {
148
+ let x = fluid_forcing_face_world(vec3<f32>(face) - vec3<f32>(0.0, 0.5, 0.0));
149
+ let velocity = fluid_forcing_wall_velocity(wall, x);
150
+
151
+ textureStore(out_v, grid_tile_window_storage(face, settings.faces_v.corner_storage, settings.faces_v.period), vec4<f32>(velocity.y, 0.0, 0.0, 0.0));
152
+ forcing[index] = 0.0;
153
+ } else if (effector_count > 0u) {
154
+ let x = fluid_forcing_face_world(vec3<f32>(face) - vec3<f32>(0.0, 0.5, 0.0));
155
+ forcing[index] = fluid_forcing_deposit(x, settings.dt, settings.cell_size, effector_count).y;
156
+ } else {
157
+ forcing[index] = 0.0;
158
+ }
159
+ }
160
+
161
+ fn fluid_forcing_face_w(face: vec3<i32>, minus: u32, plus: u32, effector_count: u32) {
162
+ let index = settings.forcing_w + grid3d_to_index(vec3<u32>(face), settings.faces_w.size.xy);
163
+ let wall = select(minus, plus, plus != FLUID_FORCING_NONE);
164
+
165
+ if (wall != FLUID_FORCING_NONE) {
166
+ let x = fluid_forcing_face_world(vec3<f32>(face) - vec3<f32>(0.0, 0.0, 0.5));
167
+ let velocity = fluid_forcing_wall_velocity(wall, x);
168
+
169
+ textureStore(out_w, grid_tile_window_storage(face, settings.faces_w.corner_storage, settings.faces_w.period), vec4<f32>(velocity.z, 0.0, 0.0, 0.0));
170
+ forcing[index] = 0.0;
171
+ } else if (effector_count > 0u) {
172
+ let x = fluid_forcing_face_world(vec3<f32>(face) - vec3<f32>(0.0, 0.0, 0.5));
173
+ forcing[index] = fluid_forcing_deposit(x, settings.dt, settings.cell_size, effector_count).z;
174
+ } else {
175
+ forcing[index] = 0.0;
176
+ }
177
+ }
178
+
179
+ @compute @workgroup_size(${FLUID_FORCING_BRICK}, ${FLUID_FORCING_BRICK}, ${FLUID_FORCING_BRICK})
180
+ fn main(
181
+ @builtin(workgroup_id) workgroup_id: vec3<u32>,
182
+ @builtin(local_invocation_id) local: vec3<u32>,
183
+ @builtin(local_invocation_index) lane: u32,
184
+ ) {
185
+ let entry = folded_workgroup_index(workgroup_id);
186
+
187
+ if (entry >= list.count) {
188
+ return;
189
+ }
190
+
191
+ let query = list.elements[entry];
192
+ let base = query * FLUID_FORCING_SURVIVOR_STRIDE;
193
+ let header = survivors[base];
194
+ let obstacle_count = header & 0xffffu;
195
+ let effector_count = header >> 16u;
196
+ let origin = vec3<i32>(index_to_grid3d(query - settings.query_base, settings.bricks.xy) * FLUID_FORCING_BRICK);
197
+ let brick_local = vec3<i32>(local);
198
+ let cell = origin + brick_local;
199
+ let flags = grid3d_to_index(vec3<u32>(cell), settings.cells.size.xy);
200
+
201
+ // listed only to clear what an earlier tick left: fluid, and no forcing to read
202
+ if (obstacle_count + effector_count == 0u) {
203
+ solid[flags] = 0u;
204
+
205
+ return;
206
+ }
207
+
208
+ // the brick's survivors into workgroup memory, posed: obstacles on lanes 0..23, effectors on 32..47
209
+ if (lane < obstacle_count) {
210
+ let row = leaf_rows[survivors[base + 1u + lane]];
211
+ let record = ${FLUID_OBSTACLE_DESCRIPTOR.marshalling_method_read}(&fluid_database, row);
212
+ let g = ${TRANSFORMS_TABLE.marshalling_method_read_field('global')}(&scene_database, record.node);
213
+
214
+ fluid_forcing_obstacles[lane] = record;
215
+ fluid_forcing_globals[lane] = mat4x4<f32>(
216
+ vec4<f32>(g[0].xyz * (1.0 / length(g[0].xyz)), 0.0),
217
+ vec4<f32>(g[1].xyz * (1.0 / length(g[1].xyz)), 0.0),
218
+ vec4<f32>(g[2].xyz * (1.0 / length(g[2].xyz)), 0.0),
219
+ g[3]
220
+ );
221
+ }
222
+
223
+ if (lane >= 32u && lane - 32u < effector_count) {
224
+ let slot = lane - 32u;
225
+ let row = leaf_rows[survivors[base + 1u + FLUID_FORCING_OBSTACLE_LIMIT + slot]];
226
+ let record = ${FLUID_EFFECTOR_DESCRIPTOR.marshalling_method_read}(&fluid_database, row);
227
+
228
+ fluid_forcing_effectors[slot] = fluid_effector_record_resolve(
229
+ record,
230
+ ${TRANSFORMS_TABLE.marshalling_method_read_field('global')}(&scene_database, record.node),
231
+ ${TRANSFORMS_TABLE.marshalling_method_read_field('prev_global')}(&scene_database, record.node),
232
+ settings.inverse_frame_dt
233
+ );
234
+ }
235
+
236
+ workgroupBarrier();
237
+
238
+ var own = FLUID_FORCING_NONE;
239
+ var minus_x = FLUID_FORCING_NONE;
240
+ var minus_y = FLUID_FORCING_NONE;
241
+ var minus_z = FLUID_FORCING_NONE;
242
+
243
+ // the solid test at every cell centre of the brick and the layer before it; a cell before the
244
+ // window is fluid, as every pass of the step reads it
245
+ if (obstacle_count > 0u) {
246
+ for (var round = 0u; round < 2u; round++) {
247
+ let k = round * FLUID_FORCING_BRICK_CELLS + lane;
248
+
249
+ if (k < SOLID_BLOCK_CELLS) {
250
+ let block_cell = origin - vec3<i32>(1) + vec3<i32>(index_to_grid3d(k, vec2<u32>(SOLID_BLOCK, SOLID_BLOCK)));
251
+
252
+ var slot = FLUID_FORCING_NONE;
253
+
254
+ if (all(block_cell >= vec3<i32>(0))) {
255
+ slot = fluid_forcing_solid_prenorm(fluid_forcing_face_world(vec3<f32>(block_cell)), obstacle_count);
256
+ }
257
+
258
+ fluid_forcing_solid_slot[k] = slot;
259
+ }
260
+ }
261
+
262
+ workgroupBarrier();
263
+
264
+ own = fluid_forcing_block_slot(brick_local);
265
+ minus_x = fluid_forcing_block_slot(brick_local - vec3<i32>(1, 0, 0));
266
+ minus_y = fluid_forcing_block_slot(brick_local - vec3<i32>(0, 1, 0));
267
+ minus_z = fluid_forcing_block_slot(brick_local - vec3<i32>(0, 0, 1));
268
+ }
269
+
270
+ // the drag of the porous obstacles on a cell that is not solid, over the step
271
+ var porous = 0u;
272
+
273
+ if (obstacle_count > 0u && own == FLUID_FORCING_NONE) {
274
+ let drag = fluid_forcing_porous(fluid_forcing_face_world(vec3<f32>(cell)), settings.cell_size, obstacle_count);
275
+
276
+ if (drag.x > 0.0) {
277
+ let at = settings.forcing_porous + flags * FLUID_FORCING_POROUS_WORDS;
278
+
279
+ forcing[at] = drag.x * settings.dt;
280
+ forcing[at + 1u] = drag.y;
281
+ forcing[at + 2u] = drag.z;
282
+ forcing[at + 3u] = drag.w;
283
+
284
+ porous = FLUID_POROUS_FLAG;
285
+ }
286
+ }
287
+
288
+ solid[flags] = FLUID_FORCED_FLAG | select(0u, FLUID_SOLID_FLAG, own != FLUID_FORCING_NONE) | porous;
289
+
290
+ // the cell's -x, -y and -z faces
291
+ fluid_forcing_face_u(cell, minus_x, own, effector_count);
292
+ fluid_forcing_face_v(cell, minus_y, own, effector_count);
293
+ fluid_forcing_face_w(cell, minus_z, own, effector_count);
294
+
295
+ // the +boundary face planes, owned by the window's last cells; the cell beyond is fluid
296
+ let last = vec3<i32>(settings.cells.size) - vec3<i32>(1);
297
+
298
+ if (cell.x == last.x) {
299
+ fluid_forcing_face_u(cell + vec3<i32>(1, 0, 0), own, FLUID_FORCING_NONE, effector_count);
300
+ }
301
+ if (cell.y == last.y) {
302
+ fluid_forcing_face_v(cell + vec3<i32>(0, 1, 0), own, FLUID_FORCING_NONE, effector_count);
303
+ }
304
+ if (cell.z == last.z) {
305
+ fluid_forcing_face_w(cell + vec3<i32>(0, 0, 1), own, FLUID_FORCING_NONE, effector_count);
306
+ }
307
+ }
308
+ `, [
309
+ chunk_folded_workgroup_index,
310
+ chunk_index_to_grid3d,
311
+ chunk_grid3d_to_index,
312
+ chunk_grid_tile_window,
313
+ chunk_fluid_forcing_cell,
314
+ chunk_fluid_effector_record_resolve,
315
+ FLUID_OBSTACLE_DESCRIPTOR.chunk_read,
316
+ FLUID_EFFECTOR_DESCRIPTOR.chunk_read,
317
+ FLUID_OBSTACLE_PLANE_DESCRIPTOR.chunk_read_field('plane'),
318
+ TRANSFORMS_TABLE.chunk_read_field('global'),
319
+ TRANSFORMS_TABLE.chunk_read_field('prev_global'),
320
+ ]);
321
+
322
+ body.addExtension(WebGPUExtensionType.Subgroups);
323
+
324
+ /**
325
+ * Q3 of a tick, the forcing (GPU_FLUID_CASCADES_PROPOSAL_2026_10_16.md §10), for one stepping
326
+ * cascade: one workgroup of 4×4×4 for each brick its query listed, dispatched indirectly from Q2's
327
+ * command, the brick found by `folded_workgroup_index`.
328
+ *
329
+ * 1. **The survivors, posed:** the brick's obstacle records with their nodes' `global` (rotation
330
+ * columns pre-normalised), and its effector records brought to the world
331
+ * (`fluid_effector_record_resolve` with the node's `global` and `prev_global`), into workgroup
332
+ * memory; one barrier.
333
+ * 2. **The solid test** (`fluid_forcing_solid_prenorm`) at the centre of every cell of the brick and
334
+ * the layer before it on each axis (125 cells, two rounds), recording the solid's obstacle; a cell
335
+ * before the window is fluid, as the step reads it. One barrier.
336
+ * 3. **Per cell:** its solid flags, `FLUID_FORCED_FLAG` with `FLUID_SOLID_FLAG` where solid, and
337
+ * `FLUID_POROUS_FLAG` where a porous obstacle drags on a cell that is not solid, whose drag over the
338
+ * step and the velocity it drags toward go into the forcing buffer's porous block
339
+ * (`fluid_forcing_porous`, the drag times `dt_c`); then each
340
+ * of its −x, −y, −z faces, and the window's last cells their +boundary faces: a face with a solid
341
+ * cell on either side is pinned, as every pass of the step will find it from the flags, and takes
342
+ * the wall's velocity (`fluid_forcing_wall_velocity`, the solid cell's obstacle, the + side's when
343
+ * both are) **in the cascade's persistent faces**, before the step reads them, with forcing 0;
344
+ * any other face's forcing is the effectors' deposit over `dt_c` on `h_c`
345
+ * (`fluid_forcing_deposit`).
346
+ *
347
+ * Writing the wall's velocity into the persistent faces is what makes a face that just became pinned
348
+ * carry the wall, never the fluid value it held: advection reads it as a boundary value and every pass
349
+ * carries it through. A brick listed only because its flags held an earlier tick's has no survivors
350
+ * and writes its cells' flags to 0, which is also what leaves no reader of this tick's forcing there.
351
+ *
352
+ * Per invocation: two solid tests of at most 24 fields (768 hull planes at worst), three to six faces
353
+ * of at most 16 kernels. Workgroup memory: 24 records and matrices, 16 records, 125 slots, 4,724
354
+ * bytes.
355
+ *
356
+ * Bindings: `settings` ({@link FLUID_FORCING_APPLY_STRUCT}); `fluid_database` and `scene_database`;
357
+ * `list`, `survivors` and `leaf_rows` from the tick's query and the frame's leaf list; `solid`, the
358
+ * cascade's flags; `forcing`, the tick's; `out_u`, `out_v`, `out_w`, the cascade's persistent faces.
359
+ * Seven storage buffers and three storage textures.
360
+ *
361
+ * @type {ComputeShader}
362
+ */
363
+ export const shader_fluid_forcing_apply = ComputeShader.from({
364
+ label: "Fluid / forcing / apply",
365
+ resources,
366
+ body,
367
+ });