@zephyr3d/scene 0.9.25 → 0.9.27

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 (299) hide show
  1. package/dist/animation/animationset.js +92 -30
  2. package/dist/animation/animationset.js.map +1 -1
  3. package/dist/animation/hair/gpu_hair_simulation.js +1090 -0
  4. package/dist/animation/hair/gpu_hair_simulation.js.map +1 -0
  5. package/dist/animation/joint_dynamics/controller.js +7 -5
  6. package/dist/animation/joint_dynamics/controller.js.map +1 -1
  7. package/dist/animation/joint_dynamics/convex_collider.js +320 -0
  8. package/dist/animation/joint_dynamics/convex_collider.js.map +1 -0
  9. package/dist/animation/joint_dynamics/joint_dynamics_system.js +81 -5
  10. package/dist/animation/joint_dynamics/joint_dynamics_system.js.map +1 -1
  11. package/dist/animation/morphtrack.js +3 -4
  12. package/dist/animation/morphtrack.js.map +1 -1
  13. package/dist/animation/skeleton.js +79 -25
  14. package/dist/animation/skeleton.js.map +1 -1
  15. package/dist/animation/spring/multi_chain_spring_system.js +577 -179
  16. package/dist/animation/spring/multi_chain_spring_system.js.map +1 -1
  17. package/dist/animation/spring/spring_collider.js +4 -3
  18. package/dist/animation/spring/spring_collider.js.map +1 -1
  19. package/dist/animation/spring/spring_node_pose_tracker.js +40 -0
  20. package/dist/animation/spring/spring_node_pose_tracker.js.map +1 -0
  21. package/dist/animation/spring/spring_particle.js +2 -0
  22. package/dist/animation/spring/spring_particle.js.map +1 -1
  23. package/dist/animation/spring/spring_system.js +11 -3
  24. package/dist/animation/spring/spring_system.js.map +1 -1
  25. package/dist/animation/spring_modifier.js +10 -0
  26. package/dist/animation/spring_modifier.js.map +1 -1
  27. package/dist/app/engine.js +53 -43
  28. package/dist/app/engine.js.map +1 -1
  29. package/dist/app/scriptingsystem.js +119 -6
  30. package/dist/app/scriptingsystem.js.map +1 -1
  31. package/dist/app/scriptregistry.js +3 -2
  32. package/dist/app/scriptregistry.js.map +1 -1
  33. package/dist/asset/assetmanager.js +284 -67
  34. package/dist/asset/assetmanager.js.map +1 -1
  35. package/dist/asset/loaders/zhair/zhair_format.js +288 -0
  36. package/dist/asset/loaders/zhair/zhair_format.js.map +1 -0
  37. package/dist/asset/loaders/zhair/zhair_loader.js +221 -0
  38. package/dist/asset/loaders/zhair/zhair_loader.js.map +1 -0
  39. package/dist/asset/model.js +184 -69
  40. package/dist/asset/model.js.map +1 -1
  41. package/dist/camera/camera.js +268 -44
  42. package/dist/camera/camera.js.map +1 -1
  43. package/dist/camera/perspectivecamera.js +3 -1
  44. package/dist/camera/perspectivecamera.js.map +1 -1
  45. package/dist/index.d.ts +9044 -5139
  46. package/dist/index.js +17 -6
  47. package/dist/index.js.map +1 -1
  48. package/dist/material/eye.js +932 -0
  49. package/dist/material/eye.js.map +1 -0
  50. package/dist/material/hair.js +917 -0
  51. package/dist/material/hair.js.map +1 -0
  52. package/dist/material/hairstrand.js +842 -0
  53. package/dist/material/hairstrand.js.map +1 -0
  54. package/dist/material/hairstrand_data.js +162 -0
  55. package/dist/material/hairstrand_data.js.map +1 -0
  56. package/dist/material/lambert.js +5 -2
  57. package/dist/material/lambert.js.map +1 -1
  58. package/dist/material/material.js +48 -10
  59. package/dist/material/material.js.map +1 -1
  60. package/dist/material/meshmaterial.js +120 -19
  61. package/dist/material/meshmaterial.js.map +1 -1
  62. package/dist/material/mixins/albedocolor.js +1 -1
  63. package/dist/material/mixins/albedocolor.js.map +1 -1
  64. package/dist/material/mixins/lightmodel/blinnphong.js +5 -3
  65. package/dist/material/mixins/lightmodel/blinnphong.js.map +1 -1
  66. package/dist/material/mixins/lightmodel/lambert.js +5 -4
  67. package/dist/material/mixins/lightmodel/lambert.js.map +1 -1
  68. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js +3 -3
  69. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js.map +1 -1
  70. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js +5 -5
  71. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js.map +1 -1
  72. package/dist/material/mixins/lightmodel/pbrspecularglossness.js +3 -3
  73. package/dist/material/mixins/lightmodel/pbrspecularglossness.js.map +1 -1
  74. package/dist/material/mixins/lit.js +106 -19
  75. package/dist/material/mixins/lit.js.map +1 -1
  76. package/dist/material/mixins/pbr/common.js +4 -4
  77. package/dist/material/mixins/pbr/common.js.map +1 -1
  78. package/dist/material/mixins/texture.js +48 -11
  79. package/dist/material/mixins/texture.js.map +1 -1
  80. package/dist/material/mtoon.js +6 -5
  81. package/dist/material/mtoon.js.map +1 -1
  82. package/dist/material/pbrblueprint.js +146 -28
  83. package/dist/material/pbrblueprint.js.map +1 -1
  84. package/dist/material/pbrblueprint_instance.js +501 -0
  85. package/dist/material/pbrblueprint_instance.js.map +1 -0
  86. package/dist/material/pbrmr.js +15 -16
  87. package/dist/material/pbrmr.js.map +1 -1
  88. package/dist/material/shader/helper.js +734 -23
  89. package/dist/material/shader/helper.js.map +1 -1
  90. package/dist/material/shader/terrain_blend.js +55 -0
  91. package/dist/material/shader/terrain_blend.js.map +1 -0
  92. package/dist/material/skin.js +192 -32
  93. package/dist/material/skin.js.map +1 -1
  94. package/dist/material/spriteblueprint.js +16 -1
  95. package/dist/material/spriteblueprint.js.map +1 -1
  96. package/dist/material/subsurfaceprofile.js +86 -23
  97. package/dist/material/subsurfaceprofile.js.map +1 -1
  98. package/dist/material/water.js +2059 -72
  99. package/dist/material/water.js.map +1 -1
  100. package/dist/posteffect/bloom.js +167 -7
  101. package/dist/posteffect/bloom.js.map +1 -1
  102. package/dist/posteffect/coloradjust.js +2 -1
  103. package/dist/posteffect/coloradjust.js.map +1 -1
  104. package/dist/posteffect/compositor.js.map +1 -1
  105. package/dist/posteffect/fxaa.js +2 -1
  106. package/dist/posteffect/fxaa.js.map +1 -1
  107. package/dist/posteffect/grayscale.js +3 -1
  108. package/dist/posteffect/grayscale.js.map +1 -1
  109. package/dist/posteffect/motionblur.js +664 -56
  110. package/dist/posteffect/motionblur.js.map +1 -1
  111. package/dist/posteffect/posteffect.js +12 -1
  112. package/dist/posteffect/posteffect.js.map +1 -1
  113. package/dist/posteffect/sao.js +46 -44
  114. package/dist/posteffect/sao.js.map +1 -1
  115. package/dist/posteffect/skinsss.js +368 -69
  116. package/dist/posteffect/skinsss.js.map +1 -1
  117. package/dist/posteffect/ssgi.js +16 -9
  118. package/dist/posteffect/ssgi.js.map +1 -1
  119. package/dist/posteffect/ssr.js +3 -0
  120. package/dist/posteffect/ssr.js.map +1 -1
  121. package/dist/posteffect/sss.js +121 -36
  122. package/dist/posteffect/sss.js.map +1 -1
  123. package/dist/posteffect/taa.js.map +1 -1
  124. package/dist/posteffect/tonemap.js +44 -1
  125. package/dist/posteffect/tonemap.js.map +1 -1
  126. package/dist/render/abuffer_oit.js +8 -2
  127. package/dist/render/abuffer_oit.js.map +1 -1
  128. package/dist/render/clipmap.js +156 -5
  129. package/dist/render/clipmap.js.map +1 -1
  130. package/dist/render/cull_visitor.js +20 -0
  131. package/dist/render/cull_visitor.js.map +1 -1
  132. package/dist/render/depthpass.js +36 -2
  133. package/dist/render/depthpass.js.map +1 -1
  134. package/dist/render/drawable_mixin.js +26 -1
  135. package/dist/render/drawable_mixin.js.map +1 -1
  136. package/dist/render/dualdepthpeeling_oit.js +3 -2
  137. package/dist/render/dualdepthpeeling_oit.js.map +1 -1
  138. package/dist/render/envlight.js +1 -1
  139. package/dist/render/envlight.js.map +1 -1
  140. package/dist/render/fbm_wavegenerator.js +6 -3
  141. package/dist/render/fbm_wavegenerator.js.map +1 -1
  142. package/dist/render/fft_wavegenerator.js +57 -15
  143. package/dist/render/fft_wavegenerator.js.map +1 -1
  144. package/dist/render/gerstner_wavegenerator.js +130 -20
  145. package/dist/render/gerstner_wavegenerator.js.map +1 -1
  146. package/dist/render/globalbindgroup_allocator.js +3 -0
  147. package/dist/render/globalbindgroup_allocator.js.map +1 -1
  148. package/dist/render/hzb.js +80 -22
  149. package/dist/render/hzb.js.map +1 -1
  150. package/dist/render/lightpass.js +6 -3
  151. package/dist/render/lightpass.js.map +1 -1
  152. package/dist/render/oit.js +29 -0
  153. package/dist/render/oit.js.map +1 -0
  154. package/dist/render/render_queue.js +39 -0
  155. package/dist/render/render_queue.js.map +1 -1
  156. package/dist/render/renderer.js +4 -0
  157. package/dist/render/renderer.js.map +1 -1
  158. package/dist/render/rendergraph/blackboard.js +1 -0
  159. package/dist/render/rendergraph/blackboard.js.map +1 -1
  160. package/dist/render/rendergraph/device_pool_allocator.js.map +1 -1
  161. package/dist/render/rendergraph/executor.js +2 -0
  162. package/dist/render/rendergraph/executor.js.map +1 -1
  163. package/dist/render/rendergraph/forward_plus_builder.js +379 -10
  164. package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
  165. package/dist/render/rendergraph/frame_resource_requirements.js +1 -0
  166. package/dist/render/rendergraph/frame_resource_requirements.js.map +1 -1
  167. package/dist/render/rendergraph/history_resources.js +8 -1
  168. package/dist/render/rendergraph/history_resources.js.map +1 -1
  169. package/dist/render/rendergraph/rendergraph.js.map +1 -1
  170. package/dist/render/rendergraph/texture_affinity_cache.js +1 -1
  171. package/dist/render/rendergraph/texture_affinity_cache.js.map +1 -1
  172. package/dist/render/rendergraph/types.js.map +1 -1
  173. package/dist/render/shadow_mask_pass.js +73 -8
  174. package/dist/render/shadow_mask_pass.js.map +1 -1
  175. package/dist/render/sky.js +166 -126
  176. package/dist/render/sky.js.map +1 -1
  177. package/dist/render/underwater.js +624 -0
  178. package/dist/render/underwater.js.map +1 -0
  179. package/dist/render/water_caustics.js +1380 -0
  180. package/dist/render/water_caustics.js.map +1 -0
  181. package/dist/render/weightedblended_oit.js +3 -2
  182. package/dist/render/weightedblended_oit.js.map +1 -1
  183. package/dist/scene/basesprite.js +8 -0
  184. package/dist/scene/basesprite.js.map +1 -1
  185. package/dist/scene/batchgroup.js +3 -0
  186. package/dist/scene/batchgroup.js.map +1 -1
  187. package/dist/scene/environment.js +4 -1
  188. package/dist/scene/environment.js.map +1 -1
  189. package/dist/scene/hair.js +963 -0
  190. package/dist/scene/hair.js.map +1 -0
  191. package/dist/scene/light.js +5 -1
  192. package/dist/scene/light.js.map +1 -1
  193. package/dist/scene/mesh.js +481 -47
  194. package/dist/scene/mesh.js.map +1 -1
  195. package/dist/scene/meshdrawable.js +6 -0
  196. package/dist/scene/meshdrawable.js.map +1 -1
  197. package/dist/scene/msdftext.js +3 -0
  198. package/dist/scene/msdftext.js.map +1 -1
  199. package/dist/scene/msdftextsprite.js +3 -0
  200. package/dist/scene/msdftextsprite.js.map +1 -1
  201. package/dist/scene/particlesys.js +6 -0
  202. package/dist/scene/particlesys.js.map +1 -1
  203. package/dist/scene/scene.js +3 -0
  204. package/dist/scene/scene.js.map +1 -1
  205. package/dist/scene/scene_node.js +6 -1
  206. package/dist/scene/scene_node.js.map +1 -1
  207. package/dist/scene/terrain-cm/grass.js +3 -0
  208. package/dist/scene/terrain-cm/grass.js.map +1 -1
  209. package/dist/scene/terrain-cm/grassmaterial.js +3 -0
  210. package/dist/scene/terrain-cm/grassmaterial.js.map +1 -1
  211. package/dist/scene/terrain-cm/terrain-cm.js +6 -0
  212. package/dist/scene/terrain-cm/terrain-cm.js.map +1 -1
  213. package/dist/scene/terrain-cm/texture_cache.js +309 -0
  214. package/dist/scene/terrain-cm/texture_cache.js.map +1 -0
  215. package/dist/scene/water.js +546 -4
  216. package/dist/scene/water.js.map +1 -1
  217. package/dist/shaders/atmosphere.js.map +1 -1
  218. package/dist/shaders/diffusion.js +54 -0
  219. package/dist/shaders/diffusion.js.map +1 -0
  220. package/dist/shaders/shadow.js +33 -21
  221. package/dist/shaders/shadow.js.map +1 -1
  222. package/dist/shaders/temporal.js +71 -33
  223. package/dist/shaders/temporal.js.map +1 -1
  224. package/dist/shaders/water.js +22 -19
  225. package/dist/shaders/water.js.map +1 -1
  226. package/dist/shaders/water_medium.js +56 -0
  227. package/dist/shaders/water_medium.js.map +1 -0
  228. package/dist/shadow/dom.js +546 -0
  229. package/dist/shadow/dom.js.map +1 -0
  230. package/dist/shadow/pcf_opt.js +3 -5
  231. package/dist/shadow/pcf_opt.js.map +1 -1
  232. package/dist/shadow/pcf_pd.js +1 -1
  233. package/dist/shadow/pcf_pd.js.map +1 -1
  234. package/dist/shadow/receiver_bias.js +41 -0
  235. package/dist/shadow/receiver_bias.js.map +1 -0
  236. package/dist/shadow/shader.js +20 -60
  237. package/dist/shadow/shader.js.map +1 -1
  238. package/dist/shadow/shadow_impl.js +89 -0
  239. package/dist/shadow/shadow_impl.js.map +1 -1
  240. package/dist/shadow/shadow_region.js +1 -1
  241. package/dist/shadow/shadow_region.js.map +1 -1
  242. package/dist/shadow/shadowmapper.js +150 -37
  243. package/dist/shadow/shadowmapper.js.map +1 -1
  244. package/dist/shapes/shape.js +11 -2
  245. package/dist/shapes/shape.js.map +1 -1
  246. package/dist/shapes/sphere.js +50 -9
  247. package/dist/shapes/sphere.js.map +1 -1
  248. package/dist/utility/blueprint/common/constants.js +112 -2
  249. package/dist/utility/blueprint/common/constants.js.map +1 -1
  250. package/dist/utility/blueprint/material/func.js +142 -49
  251. package/dist/utility/blueprint/material/func.js.map +1 -1
  252. package/dist/utility/blueprint/material/ir.js +490 -25
  253. package/dist/utility/blueprint/material/ir.js.map +1 -1
  254. package/dist/utility/blueprint/material/texture.js +898 -4
  255. package/dist/utility/blueprint/material/texture.js.map +1 -1
  256. package/dist/utility/serialization/manager.js +434 -101
  257. package/dist/utility/serialization/manager.js.map +1 -1
  258. package/dist/utility/serialization/scene/animation.js +401 -5
  259. package/dist/utility/serialization/scene/animation.js.map +1 -1
  260. package/dist/utility/serialization/scene/batch.js +4 -0
  261. package/dist/utility/serialization/scene/batch.js.map +1 -1
  262. package/dist/utility/serialization/scene/camera.js +186 -26
  263. package/dist/utility/serialization/scene/camera.js.map +1 -1
  264. package/dist/utility/serialization/scene/common.js +26 -5
  265. package/dist/utility/serialization/scene/common.js.map +1 -1
  266. package/dist/utility/serialization/scene/hair.js +1141 -0
  267. package/dist/utility/serialization/scene/hair.js.map +1 -0
  268. package/dist/utility/serialization/scene/light.js +84 -4
  269. package/dist/utility/serialization/scene/light.js.map +1 -1
  270. package/dist/utility/serialization/scene/material.js +1867 -166
  271. package/dist/utility/serialization/scene/material.js.map +1 -1
  272. package/dist/utility/serialization/scene/mesh.js +231 -14
  273. package/dist/utility/serialization/scene/mesh.js.map +1 -1
  274. package/dist/utility/serialization/scene/node.js +141 -7
  275. package/dist/utility/serialization/scene/node.js.map +1 -1
  276. package/dist/utility/serialization/scene/particle.js +4 -0
  277. package/dist/utility/serialization/scene/particle.js.map +1 -1
  278. package/dist/utility/serialization/scene/primitive.js +18 -0
  279. package/dist/utility/serialization/scene/primitive.js.map +1 -1
  280. package/dist/utility/serialization/scene/sprite.js +4 -0
  281. package/dist/utility/serialization/scene/sprite.js.map +1 -1
  282. package/dist/utility/serialization/scene/terrain.js +4 -0
  283. package/dist/utility/serialization/scene/terrain.js.map +1 -1
  284. package/dist/utility/serialization/scene/text.js +4 -0
  285. package/dist/utility/serialization/scene/text.js.map +1 -1
  286. package/dist/utility/serialization/scene/water.js +1105 -21
  287. package/dist/utility/serialization/scene/water.js.map +1 -1
  288. package/dist/utility/serialization/types.js.map +1 -1
  289. package/dist/utility/shprojector.js +9 -8
  290. package/dist/utility/shprojector.js.map +1 -1
  291. package/dist/values.js +49 -2
  292. package/dist/values.js.map +1 -1
  293. package/package.json +3 -3
  294. package/dist/render/rendergraph/frame_graph_context.js +0 -41
  295. package/dist/render/rendergraph/frame_graph_context.js.map +0 -1
  296. package/dist/shaders/ssgi.js +0 -63
  297. package/dist/shaders/ssgi.js.map +0 -1
  298. package/dist/shaders/ssgi_reprojection.js +0 -118
  299. package/dist/shaders/ssgi_reprojection.js.map +0 -1
@@ -2,31 +2,180 @@ import { AbstractPostEffect, PostEffectLayer } from './posteffect.js';
2
2
  import { linearToGamma } from '../shaders/misc.js';
3
3
  import { fetchSampler } from '../utility/misc.js';
4
4
  import { Vector2 } from '@zephyr3d/base';
5
+ import { FrameResources } from '../render/rendergraph/blackboard.js';
5
6
 
7
+ /** Default for {@link MotionBlur.maxBlurLength}. @internal */ const DEFAULT_MAX_BLUR_LENGTH = 40;
8
+ /**
9
+ * Sample spacing along a streak, in pixels. The taps are not dithered, so this
10
+ * is all that stands between a long streak and ghosting.
11
+ * @internal
12
+ */ const TARGET_STEP_PX = 3.3;
13
+ /**
14
+ * Cost ceiling, and the one place the spacing above is allowed to slip. Past
15
+ * `MAX_WALK_STEPS * TARGET_STEP_PX` per direction the taps thin out and long
16
+ * streaks ghost, which caps the usable {@link MotionBlur.maxBlurLength} at
17
+ * roughly 210 px on a fully biased shutter, twice that on a centred one.
18
+ * @internal
19
+ */ const MIN_WALK_STEPS = 4;
20
+ const MAX_WALK_STEPS = 64;
21
+ /**
22
+ * Velocity tile size, which is also the furthest a point may throw its streak:
23
+ * NeighborMax dilates across one tile, so anything that can reach a pixel has
24
+ * to live within a tile of it. A centred shutter spends that reach both ways
25
+ * and affords twice the tile size; a fully biased one spends it all one way.
26
+ * @internal
27
+ */ function tileSizeFor(maxBlurLength, shutterBias) {
28
+ return Math.max(2, Math.ceil(maxBlurLength * Math.max(shutterBias, 1 - shutterBias)));
29
+ }
30
+ /**
31
+ * Steps for a walk spanning `spanPx` pixels, at {@link TARGET_STEP_PX} stride.
32
+ * Per direction rather than per pair: the bias splits the streak unevenly, and
33
+ * a direction with no share must take no steps (see the walk).
34
+ * @internal
35
+ */ function walkStepsFor(spanPx) {
36
+ if (spanPx <= 0) {
37
+ return 0;
38
+ }
39
+ return Math.min(MAX_WALK_STEPS, Math.max(MIN_WALK_STEPS, Math.ceil(spanPx / TARGET_STEP_PX)));
40
+ }
41
+ /**
42
+ * Sentinel velocity emitted by materials opting out of temporal reuse
43
+ * ({@link MeshMaterial.disableTAA} writes `(6e4, 6e4)`). Read literally it
44
+ * would smear those pixels across the screen. Same threshold
45
+ * `temporalResolve()` tests against.
46
+ * @internal
47
+ */ const MV_SENTINEL = 5e4;
48
+ /**
49
+ * View-space depth gap past which two samples count as separate surfaces.
50
+ * Small on purpose: the test is meant to be near-binary, with the ramp only
51
+ * softening near-coplanar cases.
52
+ * @internal
53
+ */ const SOFT_Z_EXTENT = 0.01;
54
+ /** Half the separation between the two walks per direction. @internal */ const RIBBON_WIDTH = 3;
55
+ /** Velocity tiles keep the sign of the motion, so a signed float format. @internal */ const TILE_FORMAT = 'rgba16f';
56
+ const FUNC_PROCESS_VELOCITY = 'Z_mbProcessVelocity';
57
+ /**
58
+ * Raw motion vector to the velocity the filter works with: sentinel rejected,
59
+ * scaled by strength, clamped to the longest reconstructible streak. Every
60
+ * pass must apply exactly this, or the dilated and per-pixel velocities
61
+ * disagree and the weights break.
62
+ * @internal
63
+ */ function processVelocity(scope, raw, texSize, strength, maxLength) {
64
+ const pb = scope.$builder;
65
+ pb.func(FUNC_PROCESS_VELOCITY, [
66
+ pb.vec2('raw'),
67
+ pb.vec2('texSize'),
68
+ pb.float('strength'),
69
+ pb.float('maxLength')
70
+ ], function() {
71
+ this.$l.sentinel = pb.and(pb.greaterThanEqual(this.raw.x, MV_SENTINEL), pb.greaterThanEqual(this.raw.y, MV_SENTINEL));
72
+ this.$l.v = this.$choice(this.sentinel, pb.vec2(0), pb.mul(this.raw, this.strength));
73
+ this.$l.len = pb.length(pb.mul(this.v, this.texSize));
74
+ // Branchless clamp: the factor is 1 while the velocity is within budget.
75
+ this.$return(pb.mul(this.v, pb.div(this.maxLength, pb.max(this.len, this.maxLength))));
76
+ });
77
+ return scope[FUNC_PROCESS_VELOCITY](raw, texSize, strength, maxLength);
78
+ }
6
79
  /**
7
80
  * The motion blur post effect
81
+ *
82
+ * @remarks
83
+ * The reconstruction filter of McGuire et al., "A Reconstruction Filter for
84
+ * Plausible Motion Blur" (2012), with the tap path walking the velocity field
85
+ * rather than a straight line so that rotating geometry does not tear.
86
+ *
87
+ * Velocity is dilated over tiles first (TileMax, then NeighborMax) so a static
88
+ * pixel still blurs when fast geometry passes near it. That dilation is what
89
+ * makes moving *objects* streak over the background: reading only the velocity
90
+ * under the current pixel reproduces camera motion, where the field is smooth
91
+ * screen-wide, but leaves moving objects with hard edges.
92
+ *
8
93
  * @public
9
94
  */ class MotionBlur extends AbstractPostEffect {
95
+ static _programTileMaxH = null;
96
+ static _bindgroupTileMaxH = null;
97
+ static _programTileMaxV = null;
98
+ static _bindgroupTileMaxV = null;
99
+ static _programNeighborMax = null;
100
+ static _bindgroupNeighborMax = null;
10
101
  static _programMotionBlur = null;
11
102
  static _bindgroupMotionBlur = null;
12
103
  /** @internal */ _intensity;
13
- /** @internal */ _aspect;
104
+ /** @internal */ _shutterBias;
105
+ /** @internal */ _maxBlurLength;
106
+ /** @internal */ _texSize;
14
107
  /**
15
108
  * Creates an instance of tonemap post effect
16
109
  */ constructor(){
17
110
  super();
18
111
  this._intensity = 1;
19
- this._aspect = new Vector2();
20
- this._layer = PostEffectLayer.transparent;
112
+ this._shutterBias = 0.5;
113
+ this._maxBlurLength = DEFAULT_MAX_BLUR_LENGTH;
114
+ this._texSize = new Vector2();
115
+ // End layer, so the display chain runs after the TAA resolve (see
116
+ // Camera.setupPostEffects). Motion vectors are unaffected: the blackboard
117
+ // handle is final before either post chain is built.
118
+ this._layer = PostEffectLayer.end;
21
119
  }
22
- /** Motion blur strength */ get strength() {
120
+ /**
121
+ * Motion blur strength, the fraction of the frame interval the shutter is
122
+ * open. 1 blurs over a whole frame of movement (a 360 degree shutter), 0.5
123
+ * over half of it (the cinematic 180 degree shutter), 0 disables the effect.
124
+ *
125
+ * @remarks
126
+ * Motion vectors already carry one frame of displacement, so this is a plain
127
+ * dimensionless factor and must not be scaled by frame time again. The
128
+ * result is capped by {@link MotionBlur.maxBlurLength} whatever the strength.
129
+ */ get strength() {
23
130
  return this._intensity;
24
131
  }
25
132
  set strength(val) {
26
133
  this._intensity = val;
27
134
  }
135
+ /**
136
+ * How much of the streak lies ahead of where the object is now, as a fraction
137
+ * of its length. Clamped to [0, 1].
138
+ *
139
+ * - `0` trails the streak entirely behind, which is what a shutter closing
140
+ * on the current frame produces and reads as the object dragging a tail.
141
+ * - `0.5` (the default) centres it, as McGuire et al. describe: nothing lags,
142
+ * at the cost of blurring into where the object has not been yet.
143
+ * - `1` puts it entirely ahead. Offered for symmetry, rarely wanted.
144
+ *
145
+ * @remarks
146
+ * This slides the streak without lengthening it;
147
+ * {@link MotionBlur.maxBlurLength} is honoured at any bias, paid for with a
148
+ * coarser tile grid towards the extremes.
149
+ */ get shutterBias() {
150
+ return this._shutterBias;
151
+ }
152
+ set shutterBias(val) {
153
+ this._shutterBias = Math.min(1, Math.max(0, val));
154
+ }
155
+ /**
156
+ * Longest streak the filter will reconstruct, in pixels at render
157
+ * resolution. Velocity beyond this is clamped: the direction survives, the
158
+ * length does not.
159
+ *
160
+ * @remarks
161
+ * This is the ceiling {@link MotionBlur.strength} runs into: an object
162
+ * crossing `V` pixels per frame stops responding once `V * strength` reaches
163
+ * it. The clamp is per pixel, so fast objects saturate first and pushing
164
+ * strength further only flattens the fast/slow difference. Raise this instead.
165
+ *
166
+ * Cost is linear in it - 28 samples per pixel at the default 40, 100 at 160,
167
+ * since step counts are derived from the length to keep the sampling dense.
168
+ * The quality limit comes sooner though: a tile is as wide as the reach, so
169
+ * raising this spreads the dilated velocity further and a fast object starts
170
+ * dragging nearby slow geometry into its blur.
171
+ */ get maxBlurLength() {
172
+ return this._maxBlurLength;
173
+ }
174
+ set maxBlurLength(val) {
175
+ this._maxBlurLength = Math.max(1, val);
176
+ }
28
177
  /** {@inheritDoc AbstractPostEffect.requireLinearDepthTexture} */ requireLinearDepthTexture() {
29
- return false;
178
+ return true;
30
179
  }
31
180
  /** {@inheritDoc AbstractPostEffect.requireDepthAttachment} */ requireDepthAttachment() {
32
181
  return false;
@@ -34,75 +183,534 @@ import { Vector2 } from '@zephyr3d/base';
34
183
  /** {@inheritDoc AbstractPostEffect.requireMotionVectorTexture} */ requireMotionVectorTexture() {
35
184
  return true;
36
185
  }
37
- /** {@inheritDoc AbstractPostEffect.apply} */ apply(ctx, inputColorTexture, sceneDepthTexture, srgbOutput) {
186
+ /** {@inheritDoc AbstractPostEffect.setup} */ setup(s) {
187
+ const { graph, ctx, blackboard } = s;
188
+ const motionVectorHandle = blackboard.get(FrameResources.MotionVector);
189
+ const linearDepthHandle = blackboard.get(FrameResources.LinearDepth);
190
+ if (!motionVectorHandle || !linearDepthHandle || this._intensity <= 0) {
191
+ // Without velocity or depth there is nothing to reconstruct from; the
192
+ // legacy path degrades to a pass-through.
193
+ return this._setupFromApply(s);
194
+ }
195
+ // Tile size follows the requested streak length, and is what every pass
196
+ // this frame sizes itself against. Read once here so the tile textures, the
197
+ // shader loop bounds and the velocity clamp cannot disagree.
198
+ const tileSize = tileSizeFor(this._maxBlurLength, this._shutterBias);
199
+ const tileCountX = Math.max(1, Math.ceil(s.width / tileSize));
200
+ const tileCountY = Math.max(1, Math.ceil(s.height / tileSize));
201
+ // 1. TileMax, separable: rows then columns. max() is separable, so the two
202
+ // passes together cover the tile far more cheaply than a 2D reduction.
203
+ const tileMaxHHandle = graph.addPass('MotionBlur:TileMaxH', (builder)=>{
204
+ builder.read(motionVectorHandle);
205
+ for (const dep of s.dependencies){
206
+ builder.read(dep);
207
+ }
208
+ const out = builder.createTexture({
209
+ format: TILE_FORMAT,
210
+ sizeMode: 'absolute',
211
+ width: tileCountX,
212
+ height: s.height,
213
+ label: 'MotionBlur:tileMaxH'
214
+ });
215
+ builder.setExecute((rg)=>{
216
+ const device = ctx.device;
217
+ device.pushDeviceStates();
218
+ try {
219
+ this._tileMaxH(device, rg.getTexture(motionVectorHandle), rg.getTexture(out), tileCountX, tileSize);
220
+ } finally{
221
+ device.popDeviceStates();
222
+ }
223
+ });
224
+ return out;
225
+ });
226
+ const tileMaxHandle = graph.addPass('MotionBlur:TileMaxV', (builder)=>{
227
+ builder.read(tileMaxHHandle);
228
+ const out = builder.createTexture({
229
+ format: TILE_FORMAT,
230
+ sizeMode: 'absolute',
231
+ width: tileCountX,
232
+ height: tileCountY,
233
+ label: 'MotionBlur:tileMax'
234
+ });
235
+ builder.setExecute((rg)=>{
236
+ const device = ctx.device;
237
+ device.pushDeviceStates();
238
+ try {
239
+ this._tileMaxV(device, rg.getTexture(tileMaxHHandle), rg.getTexture(out), tileCountY, tileSize);
240
+ } finally{
241
+ device.popDeviceStates();
242
+ }
243
+ });
244
+ return out;
245
+ });
246
+ // 2. NeighborMax: max over the 3x3 tile neighbourhood, so a background-only
247
+ // tile learns about the fast geometry beside it and the streak may cross
248
+ // the silhouette.
249
+ const neighborMaxHandle = graph.addPass('MotionBlur:NeighborMax', (builder)=>{
250
+ builder.read(tileMaxHandle);
251
+ const out = builder.createTexture({
252
+ format: TILE_FORMAT,
253
+ sizeMode: 'absolute',
254
+ width: tileCountX,
255
+ height: tileCountY,
256
+ label: 'MotionBlur:neighborMax'
257
+ });
258
+ builder.setExecute((rg)=>{
259
+ const device = ctx.device;
260
+ device.pushDeviceStates();
261
+ try {
262
+ this._neighborMax(device, rg.getTexture(tileMaxHandle), rg.getTexture(out));
263
+ } finally{
264
+ device.popDeviceStates();
265
+ }
266
+ });
267
+ return out;
268
+ });
269
+ // 3. Reconstruction.
270
+ return graph.addPass('PostEffect:MotionBlur', (builder)=>{
271
+ builder.read(s.input);
272
+ builder.read(motionVectorHandle);
273
+ builder.read(linearDepthHandle);
274
+ builder.read(neighborMaxHandle);
275
+ for (const dep of s.dependencies){
276
+ builder.read(dep);
277
+ }
278
+ for (const binding of s.historyReads){
279
+ builder.read(binding.handle);
280
+ }
281
+ const output = s.createOutput(builder);
282
+ builder.setExecute((rg)=>{
283
+ const device = ctx.device;
284
+ device.pushDeviceStates();
285
+ try {
286
+ device.setFramebuffer(output.framebuffer ? rg.getFramebuffer(output.framebuffer) : null);
287
+ // A framebuffer bind may be a backend no-op when the requested target is already
288
+ // current. Never let a previous multi-resolution effect (for example Bloom) leak its
289
+ // smaller viewport/scissor into this fullscreen pass.
290
+ device.setViewport(null);
291
+ device.setScissor(null);
292
+ this._reconstruct(ctx, rg.getTexture(s.input), rg.getTexture(motionVectorHandle), rg.getTexture(linearDepthHandle), rg.getTexture(neighborMaxHandle), output.srgbOutput);
293
+ } finally{
294
+ device.popDeviceStates();
295
+ }
296
+ });
297
+ return output.color;
298
+ });
299
+ }
300
+ /**
301
+ * {@inheritDoc AbstractPostEffect.apply}
302
+ *
303
+ * @remarks
304
+ * Reconstruction needs the dilated tiles that {@link MotionBlur.setup} builds
305
+ * and this single-pass entry cannot. Only reached without motion vectors,
306
+ * where no blur is the right answer anyway.
307
+ */ apply(ctx, inputColorTexture, _sceneDepthTexture, srgbOutput) {
308
+ this.passThrough(ctx, inputColorTexture, srgbOutput);
309
+ }
310
+ /** @internal */ _tileMaxH(device, motionVectorTexture, target, tileCountX, tileSize) {
311
+ this._prepareTileMax(device);
312
+ device.setFramebuffer([
313
+ target
314
+ ]);
315
+ device.setViewport(null);
316
+ device.setScissor(null);
317
+ const bindGroup = MotionBlur._bindgroupTileMaxH;
318
+ bindGroup.setTexture('srcTexture', motionVectorTexture, fetchSampler('clamp_nearest_nomip'));
319
+ this._texSize.setXY(motionVectorTexture.width, motionVectorTexture.height);
320
+ bindGroup.setValue('srcSize', this._texSize);
321
+ bindGroup.setValue('tileCount', tileCountX);
322
+ bindGroup.setValue('tileSize', tileSize);
323
+ bindGroup.setValue('strength', this._intensity);
324
+ bindGroup.setValue('maxBlurLength', this._maxBlurLength);
325
+ bindGroup.setValue('flip', this.needFlip(device) ? 1 : 0);
326
+ device.setProgram(MotionBlur._programTileMaxH);
327
+ device.setBindGroup(0, bindGroup);
328
+ this.drawFullscreenQuad();
329
+ }
330
+ /** @internal */ _tileMaxV(device, srcTexture, target, tileCountY, tileSize) {
331
+ this._prepareTileMax(device);
332
+ device.setFramebuffer([
333
+ target
334
+ ]);
335
+ device.setViewport(null);
336
+ device.setScissor(null);
337
+ const bindGroup = MotionBlur._bindgroupTileMaxV;
338
+ bindGroup.setTexture('srcTexture', srcTexture, fetchSampler('clamp_nearest_nomip'));
339
+ this._texSize.setXY(srcTexture.width, srcTexture.height);
340
+ bindGroup.setValue('srcSize', this._texSize);
341
+ bindGroup.setValue('tileCount', tileCountY);
342
+ bindGroup.setValue('tileSize', tileSize);
343
+ bindGroup.setValue('flip', this.needFlip(device) ? 1 : 0);
344
+ device.setProgram(MotionBlur._programTileMaxV);
345
+ device.setBindGroup(0, bindGroup);
346
+ this.drawFullscreenQuad();
347
+ }
348
+ /** @internal */ _neighborMax(device, srcTexture, target) {
349
+ this._prepareNeighborMax(device);
350
+ device.setFramebuffer([
351
+ target
352
+ ]);
353
+ device.setViewport(null);
354
+ device.setScissor(null);
355
+ const bindGroup = MotionBlur._bindgroupNeighborMax;
356
+ bindGroup.setTexture('srcTexture', srcTexture, fetchSampler('clamp_nearest_nomip'));
357
+ this._texSize.setXY(srcTexture.width, srcTexture.height);
358
+ bindGroup.setValue('srcSize', this._texSize);
359
+ bindGroup.setValue('flip', this.needFlip(device) ? 1 : 0);
360
+ device.setProgram(MotionBlur._programNeighborMax);
361
+ device.setBindGroup(0, bindGroup);
362
+ this.drawFullscreenQuad();
363
+ }
364
+ /** @internal */ _reconstruct(ctx, inputColorTexture, motionVectorTexture, linearDepthTexture, neighborMaxTexture, srgbOutput) {
38
365
  const device = ctx.device;
39
- this._prepare(device);
40
- this._aspect.setXY(inputColorTexture.width / inputColorTexture.height, 1);
41
- MotionBlur._bindgroupMotionBlur.setTexture('inputTexture', inputColorTexture, fetchSampler('clamp_nearest_nomip'));
42
- MotionBlur._bindgroupMotionBlur.setTexture('motionVectorTexture', ctx.motionVectorTexture, fetchSampler('clamp_nearest_nomip'));
43
- MotionBlur._bindgroupMotionBlur.setValue('flip', this.needFlip(device) ? 1 : 0);
44
- MotionBlur._bindgroupMotionBlur.setValue('srgbOut', srgbOutput ? 1 : 0);
45
- MotionBlur._bindgroupMotionBlur.setValue('frameDeltaTime', device.frameInfo.elapsedFrame);
46
- MotionBlur._bindgroupMotionBlur.setValue('intensity', this._intensity);
47
- MotionBlur._bindgroupMotionBlur.setValue('aspect', this._aspect);
366
+ this._prepareReconstruct(device);
367
+ const bindGroup = MotionBlur._bindgroupMotionBlur;
368
+ const nearest = fetchSampler('clamp_nearest_nomip');
369
+ // Taps land on sub-pixel offsets, so colour is filtered. Velocity and depth
370
+ // stay point-sampled: interpolating either across a silhouette invents a
371
+ // surface at neither depth, moving at neither speed.
372
+ bindGroup.setTexture('inputTexture', inputColorTexture, fetchSampler('clamp_linear_nomip'));
373
+ bindGroup.setTexture('motionVectorTexture', motionVectorTexture, nearest);
374
+ bindGroup.setTexture('linearDepthTexture', linearDepthTexture, nearest);
375
+ bindGroup.setTexture('neighborMaxTexture', neighborMaxTexture, nearest);
376
+ this._texSize.setXY(motionVectorTexture.width, motionVectorTexture.height);
377
+ bindGroup.setValue('texSize', this._texSize);
378
+ bindGroup.setValue('strength', this._intensity);
379
+ bindGroup.setValue('maxBlurLength', this._maxBlurLength);
380
+ bindGroup.setValue('shutterBias', this._shutterBias);
381
+ // Split the streak between the two directions and give each the steps its
382
+ // own share needs. The strides are resolved here rather than in the shader
383
+ // so that a direction with no share divides by nothing.
384
+ const spanBack = this._maxBlurLength * this._shutterBias;
385
+ const spanFwd = this._maxBlurLength * (1 - this._shutterBias);
386
+ const stepsBack = walkStepsFor(spanBack);
387
+ const stepsFwd = walkStepsFor(spanFwd);
388
+ bindGroup.setValue('stepsBack', stepsBack);
389
+ bindGroup.setValue('stepsFwd', stepsFwd);
390
+ bindGroup.setValue('dtBack', stepsBack > 0 ? this._shutterBias / stepsBack : 0);
391
+ bindGroup.setValue('dtFwd', stepsFwd > 0 ? (1 - this._shutterBias) / stepsFwd : 0);
392
+ // Linear depth is stored normalized against the far plane; the filter
393
+ // compares depths in view-space units so the extent above is scene units.
394
+ bindGroup.setValue('cameraFar', ctx.camera.getFarPlane());
395
+ bindGroup.setValue('flip', this.needFlip(device) ? 1 : 0);
396
+ bindGroup.setValue('srgbOut', srgbOutput ? 1 : 0);
48
397
  device.setProgram(MotionBlur._programMotionBlur);
49
- device.setBindGroup(0, MotionBlur._bindgroupMotionBlur);
398
+ device.setBindGroup(0, bindGroup);
50
399
  this.drawFullscreenQuad();
51
400
  }
52
- /** @internal */ _prepare(device) {
53
- if (!MotionBlur._programMotionBlur) {
54
- MotionBlur._programMotionBlur = device.buildRenderProgram({
55
- vertex (pb) {
56
- this.flip = pb.int().uniform(0);
57
- this.$inputs.pos = pb.vec2().attrib('position');
58
- this.$outputs.uv = pb.vec2();
401
+ /** @internal */ _prepareTileMax(device) {
402
+ if (!MotionBlur._programTileMaxH) {
403
+ MotionBlur._programTileMaxH = device.buildRenderProgram({
404
+ vertex: fullscreenVertex,
405
+ fragment (pb) {
406
+ this.srcTexture = pb.tex2D().uniform(0);
407
+ this.srcSize = pb.vec2().uniform(0);
408
+ this.tileCount = pb.float().uniform(0);
409
+ this.strength = pb.float().uniform(0);
410
+ this.maxBlurLength = pb.float().uniform(0);
411
+ this.tileSize = pb.float().uniform(0);
412
+ this.$outputs.outColor = pb.vec4();
59
413
  pb.main(function() {
60
- this.$builtins.position = pb.vec4(this.$inputs.pos, 0, 1);
61
- this.$outputs.uv = pb.add(pb.mul(this.$inputs.pos.xy, 0.5), pb.vec2(0.5));
62
- this.$if(pb.notEqual(this.flip, 0), function() {
63
- this.$builtins.position.y = pb.neg(this.$builtins.position.y);
414
+ // Destination is one texel per tile column but keeps the source
415
+ // height, so the row index maps straight through.
416
+ this.$l.tileX = pb.floor(pb.mul(this.$inputs.uv.x, this.tileCount));
417
+ this.$l.srcY = pb.floor(pb.mul(this.$inputs.uv.y, this.srcSize.y));
418
+ this.$l.baseX = pb.mul(this.tileX, this.tileSize);
419
+ this.$l.best = pb.vec2(0);
420
+ this.$l.bestLen = pb.float(0);
421
+ this.$for(pb.float('i'), 0, this.tileSize, function() {
422
+ this.$l.srcUV = pb.div(pb.add(pb.vec2(pb.add(this.baseX, this.i), this.srcY), pb.vec2(0.5)), this.srcSize);
423
+ // The sampler clamps, so a partial tile at the right edge just
424
+ // re-reads its last texel, which cannot change a maximum.
425
+ this.$l.raw = pb.textureSampleLevel(this.srcTexture, this.srcUV, 0).xy;
426
+ this.$l.v = processVelocity(this, this.raw, this.srcSize, this.strength, this.maxBlurLength);
427
+ this.$l.len = pb.dot(this.v, this.v);
428
+ this.$if(pb.greaterThan(this.len, this.bestLen), function() {
429
+ this.bestLen = this.len;
430
+ this.best = this.v;
431
+ });
64
432
  });
433
+ this.$outputs.outColor = pb.vec4(this.best, 0, 1);
65
434
  });
66
- },
435
+ }
436
+ });
437
+ MotionBlur._programTileMaxH.name = '@MotionBlurTileMaxH';
438
+ MotionBlur._bindgroupTileMaxH = device.createBindGroup(MotionBlur._programTileMaxH.bindGroupLayouts[0]);
439
+ }
440
+ if (!MotionBlur._programTileMaxV) {
441
+ MotionBlur._programTileMaxV = device.buildRenderProgram({
442
+ vertex: fullscreenVertex,
67
443
  fragment (pb) {
68
- this.srgbOut = pb.int().uniform(0);
69
- this.frameDeltaTime = pb.float().uniform(0);
70
- this.inputTexture = pb.tex2D().uniform(0);
71
- this.motionVectorTexture = pb.tex2D().uniform(0);
72
- this.intensity = pb.float().uniform(0);
73
- this.aspect = pb.vec2().uniform(0);
444
+ this.srcTexture = pb.tex2D().uniform(0);
445
+ this.srcSize = pb.vec2().uniform(0);
446
+ this.tileCount = pb.float().uniform(0);
447
+ this.tileSize = pb.float().uniform(0);
74
448
  this.$outputs.outColor = pb.vec4();
75
449
  pb.main(function() {
76
- this.$l.color = pb.vec3();
77
- this.$l.velocity = pb.textureSampleLevel(this.motionVectorTexture, this.$inputs.uv, 0).xy;
78
- this.velocity = pb.div(pb.mul(this.velocity, 0.5), this.aspect);
79
- this.$l.sourceSample = pb.textureSampleLevel(this.inputTexture, this.$inputs.uv, 0);
80
- this.$l.color = this.sourceSample.rgb;
81
- this.$if(pb.greaterThan(pb.dot(this.velocity, this.velocity), 1e-7), function() {
82
- this.velocity = pb.mul(this.velocity, this.intensity, this.frameDeltaTime);
83
- this.$l.uv0 = pb.clamp(pb.sub(this.$inputs.uv, this.velocity), pb.vec2(0), pb.vec2(1));
84
- this.$l.uv1 = pb.clamp(pb.add(this.$inputs.uv, this.velocity), pb.vec2(0), pb.vec2(1));
85
- const NUM_SAMPLES = 17;
86
- this.$for(pb.float('i'), 0, NUM_SAMPLES, function() {
87
- this.$l.t = pb.div(this.i, NUM_SAMPLES - 1);
88
- this.color = pb.add(this.color, pb.textureSampleLevel(this.inputTexture, pb.mix(this.uv0, this.uv1, this.t), 0).rgb);
450
+ // Source already holds one texel per tile column, so the column
451
+ // index maps straight through and only rows are reduced here.
452
+ this.$l.srcX = pb.floor(pb.mul(this.$inputs.uv.x, this.srcSize.x));
453
+ this.$l.tileY = pb.floor(pb.mul(this.$inputs.uv.y, this.tileCount));
454
+ this.$l.baseY = pb.mul(this.tileY, this.tileSize);
455
+ this.$l.best = pb.vec2(0);
456
+ this.$l.bestLen = pb.float(0);
457
+ this.$for(pb.float('i'), 0, this.tileSize, function() {
458
+ this.$l.srcUV = pb.div(pb.add(pb.vec2(this.srcX, pb.add(this.baseY, this.i)), pb.vec2(0.5)), this.srcSize);
459
+ // Already processed by the horizontal pass, so taken verbatim.
460
+ this.$l.v = pb.textureSampleLevel(this.srcTexture, this.srcUV, 0).xy;
461
+ this.$l.len = pb.dot(this.v, this.v);
462
+ this.$if(pb.greaterThan(this.len, this.bestLen), function() {
463
+ this.bestLen = this.len;
464
+ this.best = this.v;
89
465
  });
90
- this.color = pb.mul(this.color, 1 / NUM_SAMPLES);
91
466
  });
92
- //this.color = pb.vec3(this.velocity, 0);
93
- this.$if(pb.notEqual(this.srgbOut, 0), function() {
94
- this.color = linearToGamma(this, this.color);
95
- });
96
- this.$outputs.outColor = pb.vec4(this.color, this.sourceSample.a);
467
+ this.$outputs.outColor = pb.vec4(this.best, 0, 1);
97
468
  });
98
469
  }
99
470
  });
100
- MotionBlur._programMotionBlur.name = '@MotionBlur';
101
- MotionBlur._bindgroupMotionBlur = device.createBindGroup(MotionBlur._programMotionBlur.bindGroupLayouts[0]);
102
- console.log(MotionBlur._programMotionBlur.getShaderSource('fragment'));
471
+ MotionBlur._programTileMaxV.name = '@MotionBlurTileMaxV';
472
+ MotionBlur._bindgroupTileMaxV = device.createBindGroup(MotionBlur._programTileMaxV.bindGroupLayouts[0]);
473
+ }
474
+ }
475
+ /** @internal */ _prepareNeighborMax(device) {
476
+ if (MotionBlur._programNeighborMax) {
477
+ return;
478
+ }
479
+ MotionBlur._programNeighborMax = device.buildRenderProgram({
480
+ vertex: fullscreenVertex,
481
+ fragment (pb) {
482
+ this.srcTexture = pb.tex2D().uniform(0);
483
+ this.srcSize = pb.vec2().uniform(0);
484
+ this.$outputs.outColor = pb.vec4();
485
+ pb.main(function() {
486
+ this.$l.best = pb.vec2(0);
487
+ this.$l.bestLen = pb.float(0);
488
+ // Unrolled: the 3x3 offsets are compile-time constants, so this
489
+ // avoids a nested dynamic loop for nine taps.
490
+ for(let dy = -1; dy <= 1; dy++){
491
+ for(let dx = -1; dx <= 1; dx++){
492
+ const nName = `n${dy + 1}${dx + 1}`;
493
+ const lenName = `len${dy + 1}${dx + 1}`;
494
+ this.$l[nName] = pb.textureSampleLevel(this.srcTexture, pb.add(this.$inputs.uv, pb.div(pb.vec2(dx, dy), this.srcSize)), 0).xy;
495
+ this.$l[lenName] = pb.dot(this[nName], this[nName]);
496
+ this.$if(pb.greaterThan(this[lenName], this.bestLen), function() {
497
+ this.bestLen = this[lenName];
498
+ this.best = this[nName];
499
+ });
500
+ }
501
+ }
502
+ this.$outputs.outColor = pb.vec4(this.best, 0, 1);
503
+ });
504
+ }
505
+ });
506
+ MotionBlur._programNeighborMax.name = '@MotionBlurNeighborMax';
507
+ MotionBlur._bindgroupNeighborMax = device.createBindGroup(MotionBlur._programNeighborMax.bindGroupLayouts[0]);
508
+ }
509
+ /** @internal */ _prepareReconstruct(device) {
510
+ if (MotionBlur._programMotionBlur) {
511
+ return;
103
512
  }
513
+ MotionBlur._programMotionBlur = device.buildRenderProgram({
514
+ vertex: fullscreenVertex,
515
+ fragment (pb) {
516
+ this.inputTexture = pb.tex2D().uniform(0);
517
+ this.motionVectorTexture = pb.tex2D().uniform(0);
518
+ this.linearDepthTexture = pb.tex2D().uniform(0);
519
+ this.neighborMaxTexture = pb.tex2D().uniform(0);
520
+ this.texSize = pb.vec2().uniform(0);
521
+ this.strength = pb.float().uniform(0);
522
+ this.maxBlurLength = pb.float().uniform(0);
523
+ this.shutterBias = pb.float().uniform(0);
524
+ this.stepsBack = pb.float().uniform(0);
525
+ this.stepsFwd = pb.float().uniform(0);
526
+ this.dtBack = pb.float().uniform(0);
527
+ this.dtFwd = pb.float().uniform(0);
528
+ this.cameraFar = pb.float().uniform(0);
529
+ this.srgbOut = pb.int().uniform(0);
530
+ this.$outputs.outColor = pb.vec4();
531
+ // Coverage of a streak of length `len` at a point `dist` away. The half
532
+ // pixel floor keeps a static sample from dividing by zero.
533
+ pb.func('Z_mbCone', [
534
+ pb.float('dist'),
535
+ pb.float('len')
536
+ ], function() {
537
+ this.$return(pb.clamp(pb.sub(1, pb.div(this.dist, pb.max(this.len, 0.5))), 0, 1));
538
+ });
539
+ // Coverage of a streak that passes over the point rather than ending
540
+ // on it, used when both samples are blurry.
541
+ pb.func('Z_mbCylinder', [
542
+ pb.float('dist'),
543
+ pb.float('len')
544
+ ], function() {
545
+ this.$l.r = pb.max(this.len, 0.5);
546
+ this.$return(pb.sub(1, pb.smoothStep(pb.mul(this.r, 0.95), pb.mul(this.r, 1.05), this.dist)));
547
+ });
548
+ // 1 when za is in front of zb, 0 when clearly behind it.
549
+ pb.func('Z_mbSoftDepthCompare', [
550
+ pb.float('za'),
551
+ pb.float('zb')
552
+ ], function() {
553
+ this.$return(pb.clamp(pb.sub(1, pb.div(pb.sub(this.za, this.zb), SOFT_Z_EXTENT)), 0, 1));
554
+ });
555
+ pb.func('Z_mbLinearDepth', [
556
+ pb.vec2('uv')
557
+ ], function() {
558
+ // Motion vectors are unavailable on WebGL, so this effect never runs
559
+ // against the RGBA-packed depth variant that backend uses.
560
+ this.$return(pb.mul(pb.textureSampleLevel(this.linearDepthTexture, this.uv, 0).r, this.cameraFar));
561
+ });
562
+ // Velocity to advance the walk by: the field where there is one, else the
563
+ // dilated vector. That fallback is what starts a walk at all - a
564
+ // background pixel has no velocity of its own to leave on.
565
+ pb.func('Z_mbFlowAt', [
566
+ pb.vec2('uv'),
567
+ pb.vec2('vFallback')
568
+ ], function() {
569
+ this.$l.v = processVelocity(this, pb.textureSampleLevel(this.motionVectorTexture, this.uv, 0).xy, this.texSize, this.strength, this.maxBlurLength);
570
+ this.$l.len = pb.length(pb.mul(this.v, this.texSize));
571
+ this.$return(this.$choice(pb.greaterThan(this.len, 0.5), this.v, this.vFallback));
572
+ });
573
+ // One tap of the reconstruction filter, returning the weighted colour
574
+ // in rgb and the weight in a.
575
+ pb.func('Z_mbTap', [
576
+ pb.vec2('sampleUV'),
577
+ pb.float('dist'),
578
+ pb.float('zC'),
579
+ pb.float('lenC')
580
+ ], function() {
581
+ this.$l.zS = this.Z_mbLinearDepth(this.sampleUV);
582
+ this.$l.vS = processVelocity(this, pb.textureSampleLevel(this.motionVectorTexture, this.sampleUV, 0).xy, this.texSize, this.strength, this.maxBlurLength);
583
+ this.$l.lenS = pb.length(pb.mul(this.vS, this.texSize));
584
+ // f: the sample sits in front of the centre, so its own blur is
585
+ // allowed to spill onto the centre. b: the centre is in front and
586
+ // blurry, so it gathers whatever is behind it.
587
+ this.$l.f = this.Z_mbSoftDepthCompare(this.zS, this.zC);
588
+ this.$l.b = this.Z_mbSoftDepthCompare(this.zC, this.zS);
589
+ this.$l.alpha = pb.add(pb.mul(this.f, this.Z_mbCone(this.dist, this.lenS)), pb.mul(this.b, this.Z_mbCone(this.dist, this.lenC)), pb.mul(this.Z_mbCylinder(this.dist, this.lenS), this.Z_mbCylinder(this.dist, this.lenC), 2));
590
+ this.$l.colorS = pb.textureSampleLevel(this.inputTexture, this.sampleUV, 0).rgb;
591
+ this.$return(pb.vec4(pb.mul(this.colorS, this.alpha), this.alpha));
592
+ });
593
+ pb.main(function() {
594
+ this.$l.uv = this.$inputs.uv;
595
+ this.$l.sourceSample = pb.textureSampleLevel(this.inputTexture, this.uv, 0);
596
+ this.$l.color = this.sourceSample.rgb;
597
+ // Dilated tile velocity: non-zero even where the pixel is static but
598
+ // fast geometry passes close by.
599
+ this.$l.vN = pb.textureSampleLevel(this.neighborMaxTexture, this.uv, 0).xy;
600
+ this.$l.lenN = pb.length(pb.mul(this.vN, this.texSize));
601
+ this.$if(pb.greaterThan(this.lenN, 0.5), function() {
602
+ this.$l.vC = processVelocity(this, pb.textureSampleLevel(this.motionVectorTexture, this.uv, 0).xy, this.texSize, this.strength, this.maxBlurLength);
603
+ // Floored at half a pixel, as the paper floors the stored velocity:
604
+ // a sharp centre needs a large but finite self weight.
605
+ this.$l.lenC = pb.max(pb.length(pb.mul(this.vC, this.texSize)), 0.5);
606
+ this.$l.zC = this.Z_mbLinearDepth(this.uv);
607
+ // The sharper this pixel is, the more of its own colour it keeps.
608
+ this.$l.weight = pb.div(1, this.lenC);
609
+ this.$l.sum = pb.mul(this.sourceSample.rgb, this.weight);
610
+ // Four walks leave the pixel: backwards into the points arriving
611
+ // here during the shutter, forwards into the ones this pixel's
612
+ // motion carries it onto, each doubled across the streak.
613
+ //
614
+ // They follow the velocity field rather than a straight line. A
615
+ // point Y covers this pixel only if X - Y runs along Y's own
616
+ // velocity; translation makes that locus a line, self-rotation
617
+ // curves it into an arc, and a straight walk cuts across the arc,
618
+ // catching a different set of points at every pixel - the crack a
619
+ // spinning object shows. Re-reading the field each step follows the
620
+ // arc, and reduces to the straight line when the field is uniform.
621
+ //
622
+ // The doubling averages over silhouette tangency, where a lone walk
623
+ // loses several taps at once for a one-pixel shift. Its offset is
624
+ // perpendicular to the motion, which a blur must not smear along,
625
+ // so it stays as narrow as the tangency allows.
626
+ //
627
+ // shutterBias splits the streak between the two directions, each
628
+ // getting steps in proportion. A direction with no share must take
629
+ // none: zero-length steps park every tap on the centre at distance
630
+ // zero, where cone, cylinder and the depth test all peak, spending
631
+ // the heaviest weight in the filter on the pixel's own colour.
632
+ this.$l.nrmN = pb.normalize(this.vN);
633
+ this.$l.lane = pb.div(pb.mul(pb.vec2(pb.neg(this.nrmN.y), this.nrmN.x), RIBBON_WIDTH), this.texSize);
634
+ this.$l.posBack = this.uv;
635
+ this.$l.posFwd = this.uv;
636
+ this.$l.posBack2 = this.uv;
637
+ this.$l.posFwd2 = this.uv;
638
+ // Arc length walked, in pixels: the weights compare it against a
639
+ // sample speed, so it must follow the curve, not the chord.
640
+ this.$l.arcBack = pb.float(0);
641
+ this.$l.arcFwd = pb.float(0);
642
+ this.$l.arcBack2 = pb.float(0);
643
+ this.$l.arcFwd2 = pb.float(0);
644
+ // Direction carried between steps, seeded with the dilated vector.
645
+ // Carried rather than re-seeded so that leaving the geometry again
646
+ // does not kink the path mid-arc.
647
+ this.$l.flowBack = this.vN;
648
+ this.$l.flowFwd = this.vN;
649
+ this.$l.flowBack2 = this.vN;
650
+ this.$l.flowFwd2 = this.vN;
651
+ // The lanes separate with distance rather than running parallel:
652
+ // coincident at the pixel, where sharpness shows, and a few pixels
653
+ // apart far along the streak, where the tangency needs averaging.
654
+ this.$for(pb.float('i'), 0, this.stepsBack, function() {
655
+ this.$l.spread = pb.mul(this.lane, pb.div(pb.add(this.i, 1), this.stepsBack));
656
+ this.flowBack = this.Z_mbFlowAt(this.posBack, this.flowBack);
657
+ this.$l.stepBack = pb.mul(this.flowBack, pb.neg(this.dtBack));
658
+ this.posBack = pb.clamp(pb.add(this.posBack, this.stepBack), pb.vec2(0), pb.vec2(1));
659
+ this.arcBack = pb.add(this.arcBack, pb.length(pb.mul(this.stepBack, this.texSize)));
660
+ this.$l.tapBack = this.Z_mbTap(pb.add(this.posBack, this.spread), this.arcBack, this.zC, this.lenC);
661
+ this.flowBack2 = this.Z_mbFlowAt(this.posBack2, this.flowBack2);
662
+ this.$l.stepBack2 = pb.mul(this.flowBack2, pb.neg(this.dtBack));
663
+ this.posBack2 = pb.clamp(pb.add(this.posBack2, this.stepBack2), pb.vec2(0), pb.vec2(1));
664
+ this.arcBack2 = pb.add(this.arcBack2, pb.length(pb.mul(this.stepBack2, this.texSize)));
665
+ this.$l.tapBack2 = this.Z_mbTap(pb.sub(this.posBack2, this.spread), this.arcBack2, this.zC, this.lenC);
666
+ this.sum = pb.add(this.sum, this.tapBack.rgb, this.tapBack2.rgb);
667
+ this.weight = pb.add(this.weight, this.tapBack.a, this.tapBack2.a);
668
+ });
669
+ this.$for(pb.float('i'), 0, this.stepsFwd, function() {
670
+ this.$l.spread = pb.mul(this.lane, pb.div(pb.add(this.i, 1), this.stepsFwd));
671
+ this.flowFwd = this.Z_mbFlowAt(this.posFwd, this.flowFwd);
672
+ this.$l.stepFwd = pb.mul(this.flowFwd, this.dtFwd);
673
+ this.posFwd = pb.clamp(pb.add(this.posFwd, this.stepFwd), pb.vec2(0), pb.vec2(1));
674
+ this.arcFwd = pb.add(this.arcFwd, pb.length(pb.mul(this.stepFwd, this.texSize)));
675
+ this.$l.tapFwd = this.Z_mbTap(pb.add(this.posFwd, this.spread), this.arcFwd, this.zC, this.lenC);
676
+ this.flowFwd2 = this.Z_mbFlowAt(this.posFwd2, this.flowFwd2);
677
+ this.$l.stepFwd2 = pb.mul(this.flowFwd2, this.dtFwd);
678
+ this.posFwd2 = pb.clamp(pb.add(this.posFwd2, this.stepFwd2), pb.vec2(0), pb.vec2(1));
679
+ this.arcFwd2 = pb.add(this.arcFwd2, pb.length(pb.mul(this.stepFwd2, this.texSize)));
680
+ this.$l.tapFwd2 = this.Z_mbTap(pb.sub(this.posFwd2, this.spread), this.arcFwd2, this.zC, this.lenC);
681
+ this.sum = pb.add(this.sum, this.tapFwd.rgb, this.tapFwd2.rgb);
682
+ this.weight = pb.add(this.weight, this.tapFwd.a, this.tapFwd2.a);
683
+ });
684
+ this.color = pb.div(this.sum, pb.max(this.weight, 1e-5));
685
+ });
686
+ this.$if(pb.notEqual(this.srgbOut, 0), function() {
687
+ this.color = linearToGamma(this, this.color);
688
+ });
689
+ this.$outputs.outColor = pb.vec4(this.color, this.sourceSample.a);
690
+ });
691
+ }
692
+ });
693
+ MotionBlur._programMotionBlur.name = '@MotionBlur';
694
+ MotionBlur._bindgroupMotionBlur = device.createBindGroup(MotionBlur._programMotionBlur.bindGroupLayouts[0]);
104
695
  }
105
696
  }
697
+ /**
698
+ * Shared fullscreen quad vertex stage. `uv` is derived from the attribute and
699
+ * so is unaffected by the flip, which keeps every pass in this effect writing
700
+ * and sampling the same orientation on both backends.
701
+ * @internal
702
+ */ function fullscreenVertex(pb) {
703
+ this.flip = pb.int().uniform(0);
704
+ this.$inputs.pos = pb.vec2().attrib('position');
705
+ this.$outputs.uv = pb.vec2();
706
+ pb.main(function() {
707
+ this.$builtins.position = pb.vec4(this.$inputs.pos, 0, 1);
708
+ this.$outputs.uv = pb.add(pb.mul(this.$inputs.pos.xy, 0.5), pb.vec2(0.5));
709
+ this.$if(pb.notEqual(this.flip, 0), function() {
710
+ this.$builtins.position.y = pb.neg(this.$builtins.position.y);
711
+ });
712
+ });
713
+ }
106
714
 
107
715
  export { MotionBlur };
108
716
  //# sourceMappingURL=motionblur.js.map