@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.
- package/dist/animation/animationset.js +92 -30
- package/dist/animation/animationset.js.map +1 -1
- package/dist/animation/hair/gpu_hair_simulation.js +1090 -0
- package/dist/animation/hair/gpu_hair_simulation.js.map +1 -0
- package/dist/animation/joint_dynamics/controller.js +7 -5
- package/dist/animation/joint_dynamics/controller.js.map +1 -1
- package/dist/animation/joint_dynamics/convex_collider.js +320 -0
- package/dist/animation/joint_dynamics/convex_collider.js.map +1 -0
- package/dist/animation/joint_dynamics/joint_dynamics_system.js +81 -5
- package/dist/animation/joint_dynamics/joint_dynamics_system.js.map +1 -1
- package/dist/animation/morphtrack.js +3 -4
- package/dist/animation/morphtrack.js.map +1 -1
- package/dist/animation/skeleton.js +79 -25
- package/dist/animation/skeleton.js.map +1 -1
- package/dist/animation/spring/multi_chain_spring_system.js +577 -179
- package/dist/animation/spring/multi_chain_spring_system.js.map +1 -1
- package/dist/animation/spring/spring_collider.js +4 -3
- package/dist/animation/spring/spring_collider.js.map +1 -1
- package/dist/animation/spring/spring_node_pose_tracker.js +40 -0
- package/dist/animation/spring/spring_node_pose_tracker.js.map +1 -0
- package/dist/animation/spring/spring_particle.js +2 -0
- package/dist/animation/spring/spring_particle.js.map +1 -1
- package/dist/animation/spring/spring_system.js +11 -3
- package/dist/animation/spring/spring_system.js.map +1 -1
- package/dist/animation/spring_modifier.js +10 -0
- package/dist/animation/spring_modifier.js.map +1 -1
- package/dist/app/engine.js +53 -43
- package/dist/app/engine.js.map +1 -1
- package/dist/app/scriptingsystem.js +119 -6
- package/dist/app/scriptingsystem.js.map +1 -1
- package/dist/app/scriptregistry.js +3 -2
- package/dist/app/scriptregistry.js.map +1 -1
- package/dist/asset/assetmanager.js +284 -67
- package/dist/asset/assetmanager.js.map +1 -1
- package/dist/asset/loaders/zhair/zhair_format.js +288 -0
- package/dist/asset/loaders/zhair/zhair_format.js.map +1 -0
- package/dist/asset/loaders/zhair/zhair_loader.js +221 -0
- package/dist/asset/loaders/zhair/zhair_loader.js.map +1 -0
- package/dist/asset/model.js +184 -69
- package/dist/asset/model.js.map +1 -1
- package/dist/camera/camera.js +268 -44
- package/dist/camera/camera.js.map +1 -1
- package/dist/camera/perspectivecamera.js +3 -1
- package/dist/camera/perspectivecamera.js.map +1 -1
- package/dist/index.d.ts +9044 -5139
- package/dist/index.js +17 -6
- package/dist/index.js.map +1 -1
- package/dist/material/eye.js +932 -0
- package/dist/material/eye.js.map +1 -0
- package/dist/material/hair.js +917 -0
- package/dist/material/hair.js.map +1 -0
- package/dist/material/hairstrand.js +842 -0
- package/dist/material/hairstrand.js.map +1 -0
- package/dist/material/hairstrand_data.js +162 -0
- package/dist/material/hairstrand_data.js.map +1 -0
- package/dist/material/lambert.js +5 -2
- package/dist/material/lambert.js.map +1 -1
- package/dist/material/material.js +48 -10
- package/dist/material/material.js.map +1 -1
- package/dist/material/meshmaterial.js +120 -19
- package/dist/material/meshmaterial.js.map +1 -1
- package/dist/material/mixins/albedocolor.js +1 -1
- package/dist/material/mixins/albedocolor.js.map +1 -1
- package/dist/material/mixins/lightmodel/blinnphong.js +5 -3
- package/dist/material/mixins/lightmodel/blinnphong.js.map +1 -1
- package/dist/material/mixins/lightmodel/lambert.js +5 -4
- package/dist/material/mixins/lightmodel/lambert.js.map +1 -1
- package/dist/material/mixins/lightmodel/pbrblueprintmixin.js +3 -3
- package/dist/material/mixins/lightmodel/pbrblueprintmixin.js.map +1 -1
- package/dist/material/mixins/lightmodel/pbrmetallicroughness.js +5 -5
- package/dist/material/mixins/lightmodel/pbrmetallicroughness.js.map +1 -1
- package/dist/material/mixins/lightmodel/pbrspecularglossness.js +3 -3
- package/dist/material/mixins/lightmodel/pbrspecularglossness.js.map +1 -1
- package/dist/material/mixins/lit.js +106 -19
- package/dist/material/mixins/lit.js.map +1 -1
- package/dist/material/mixins/pbr/common.js +4 -4
- package/dist/material/mixins/pbr/common.js.map +1 -1
- package/dist/material/mixins/texture.js +48 -11
- package/dist/material/mixins/texture.js.map +1 -1
- package/dist/material/mtoon.js +6 -5
- package/dist/material/mtoon.js.map +1 -1
- package/dist/material/pbrblueprint.js +146 -28
- package/dist/material/pbrblueprint.js.map +1 -1
- package/dist/material/pbrblueprint_instance.js +501 -0
- package/dist/material/pbrblueprint_instance.js.map +1 -0
- package/dist/material/pbrmr.js +15 -16
- package/dist/material/pbrmr.js.map +1 -1
- package/dist/material/shader/helper.js +734 -23
- package/dist/material/shader/helper.js.map +1 -1
- package/dist/material/shader/terrain_blend.js +55 -0
- package/dist/material/shader/terrain_blend.js.map +1 -0
- package/dist/material/skin.js +192 -32
- package/dist/material/skin.js.map +1 -1
- package/dist/material/spriteblueprint.js +16 -1
- package/dist/material/spriteblueprint.js.map +1 -1
- package/dist/material/subsurfaceprofile.js +86 -23
- package/dist/material/subsurfaceprofile.js.map +1 -1
- package/dist/material/water.js +2059 -72
- package/dist/material/water.js.map +1 -1
- package/dist/posteffect/bloom.js +167 -7
- package/dist/posteffect/bloom.js.map +1 -1
- package/dist/posteffect/coloradjust.js +2 -1
- package/dist/posteffect/coloradjust.js.map +1 -1
- package/dist/posteffect/compositor.js.map +1 -1
- package/dist/posteffect/fxaa.js +2 -1
- package/dist/posteffect/fxaa.js.map +1 -1
- package/dist/posteffect/grayscale.js +3 -1
- package/dist/posteffect/grayscale.js.map +1 -1
- package/dist/posteffect/motionblur.js +664 -56
- package/dist/posteffect/motionblur.js.map +1 -1
- package/dist/posteffect/posteffect.js +12 -1
- package/dist/posteffect/posteffect.js.map +1 -1
- package/dist/posteffect/sao.js +46 -44
- package/dist/posteffect/sao.js.map +1 -1
- package/dist/posteffect/skinsss.js +368 -69
- package/dist/posteffect/skinsss.js.map +1 -1
- package/dist/posteffect/ssgi.js +16 -9
- package/dist/posteffect/ssgi.js.map +1 -1
- package/dist/posteffect/ssr.js +3 -0
- package/dist/posteffect/ssr.js.map +1 -1
- package/dist/posteffect/sss.js +121 -36
- package/dist/posteffect/sss.js.map +1 -1
- package/dist/posteffect/taa.js.map +1 -1
- package/dist/posteffect/tonemap.js +44 -1
- package/dist/posteffect/tonemap.js.map +1 -1
- package/dist/render/abuffer_oit.js +8 -2
- package/dist/render/abuffer_oit.js.map +1 -1
- package/dist/render/clipmap.js +156 -5
- package/dist/render/clipmap.js.map +1 -1
- package/dist/render/cull_visitor.js +20 -0
- package/dist/render/cull_visitor.js.map +1 -1
- package/dist/render/depthpass.js +36 -2
- package/dist/render/depthpass.js.map +1 -1
- package/dist/render/drawable_mixin.js +26 -1
- package/dist/render/drawable_mixin.js.map +1 -1
- package/dist/render/dualdepthpeeling_oit.js +3 -2
- package/dist/render/dualdepthpeeling_oit.js.map +1 -1
- package/dist/render/envlight.js +1 -1
- package/dist/render/envlight.js.map +1 -1
- package/dist/render/fbm_wavegenerator.js +6 -3
- package/dist/render/fbm_wavegenerator.js.map +1 -1
- package/dist/render/fft_wavegenerator.js +57 -15
- package/dist/render/fft_wavegenerator.js.map +1 -1
- package/dist/render/gerstner_wavegenerator.js +130 -20
- package/dist/render/gerstner_wavegenerator.js.map +1 -1
- package/dist/render/globalbindgroup_allocator.js +3 -0
- package/dist/render/globalbindgroup_allocator.js.map +1 -1
- package/dist/render/hzb.js +80 -22
- package/dist/render/hzb.js.map +1 -1
- package/dist/render/lightpass.js +6 -3
- package/dist/render/lightpass.js.map +1 -1
- package/dist/render/oit.js +29 -0
- package/dist/render/oit.js.map +1 -0
- package/dist/render/render_queue.js +39 -0
- package/dist/render/render_queue.js.map +1 -1
- package/dist/render/renderer.js +4 -0
- package/dist/render/renderer.js.map +1 -1
- package/dist/render/rendergraph/blackboard.js +1 -0
- package/dist/render/rendergraph/blackboard.js.map +1 -1
- package/dist/render/rendergraph/device_pool_allocator.js.map +1 -1
- package/dist/render/rendergraph/executor.js +2 -0
- package/dist/render/rendergraph/executor.js.map +1 -1
- package/dist/render/rendergraph/forward_plus_builder.js +379 -10
- package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
- package/dist/render/rendergraph/frame_resource_requirements.js +1 -0
- package/dist/render/rendergraph/frame_resource_requirements.js.map +1 -1
- package/dist/render/rendergraph/history_resources.js +8 -1
- package/dist/render/rendergraph/history_resources.js.map +1 -1
- package/dist/render/rendergraph/rendergraph.js.map +1 -1
- package/dist/render/rendergraph/texture_affinity_cache.js +1 -1
- package/dist/render/rendergraph/texture_affinity_cache.js.map +1 -1
- package/dist/render/rendergraph/types.js.map +1 -1
- package/dist/render/shadow_mask_pass.js +73 -8
- package/dist/render/shadow_mask_pass.js.map +1 -1
- package/dist/render/sky.js +166 -126
- package/dist/render/sky.js.map +1 -1
- package/dist/render/underwater.js +624 -0
- package/dist/render/underwater.js.map +1 -0
- package/dist/render/water_caustics.js +1380 -0
- package/dist/render/water_caustics.js.map +1 -0
- package/dist/render/weightedblended_oit.js +3 -2
- package/dist/render/weightedblended_oit.js.map +1 -1
- package/dist/scene/basesprite.js +8 -0
- package/dist/scene/basesprite.js.map +1 -1
- package/dist/scene/batchgroup.js +3 -0
- package/dist/scene/batchgroup.js.map +1 -1
- package/dist/scene/environment.js +4 -1
- package/dist/scene/environment.js.map +1 -1
- package/dist/scene/hair.js +963 -0
- package/dist/scene/hair.js.map +1 -0
- package/dist/scene/light.js +5 -1
- package/dist/scene/light.js.map +1 -1
- package/dist/scene/mesh.js +481 -47
- package/dist/scene/mesh.js.map +1 -1
- package/dist/scene/meshdrawable.js +6 -0
- package/dist/scene/meshdrawable.js.map +1 -1
- package/dist/scene/msdftext.js +3 -0
- package/dist/scene/msdftext.js.map +1 -1
- package/dist/scene/msdftextsprite.js +3 -0
- package/dist/scene/msdftextsprite.js.map +1 -1
- package/dist/scene/particlesys.js +6 -0
- package/dist/scene/particlesys.js.map +1 -1
- package/dist/scene/scene.js +3 -0
- package/dist/scene/scene.js.map +1 -1
- package/dist/scene/scene_node.js +6 -1
- package/dist/scene/scene_node.js.map +1 -1
- package/dist/scene/terrain-cm/grass.js +3 -0
- package/dist/scene/terrain-cm/grass.js.map +1 -1
- package/dist/scene/terrain-cm/grassmaterial.js +3 -0
- package/dist/scene/terrain-cm/grassmaterial.js.map +1 -1
- package/dist/scene/terrain-cm/terrain-cm.js +6 -0
- package/dist/scene/terrain-cm/terrain-cm.js.map +1 -1
- package/dist/scene/terrain-cm/texture_cache.js +309 -0
- package/dist/scene/terrain-cm/texture_cache.js.map +1 -0
- package/dist/scene/water.js +546 -4
- package/dist/scene/water.js.map +1 -1
- package/dist/shaders/atmosphere.js.map +1 -1
- package/dist/shaders/diffusion.js +54 -0
- package/dist/shaders/diffusion.js.map +1 -0
- package/dist/shaders/shadow.js +33 -21
- package/dist/shaders/shadow.js.map +1 -1
- package/dist/shaders/temporal.js +71 -33
- package/dist/shaders/temporal.js.map +1 -1
- package/dist/shaders/water.js +22 -19
- package/dist/shaders/water.js.map +1 -1
- package/dist/shaders/water_medium.js +56 -0
- package/dist/shaders/water_medium.js.map +1 -0
- package/dist/shadow/dom.js +546 -0
- package/dist/shadow/dom.js.map +1 -0
- package/dist/shadow/pcf_opt.js +3 -5
- package/dist/shadow/pcf_opt.js.map +1 -1
- package/dist/shadow/pcf_pd.js +1 -1
- package/dist/shadow/pcf_pd.js.map +1 -1
- package/dist/shadow/receiver_bias.js +41 -0
- package/dist/shadow/receiver_bias.js.map +1 -0
- package/dist/shadow/shader.js +20 -60
- package/dist/shadow/shader.js.map +1 -1
- package/dist/shadow/shadow_impl.js +89 -0
- package/dist/shadow/shadow_impl.js.map +1 -1
- package/dist/shadow/shadow_region.js +1 -1
- package/dist/shadow/shadow_region.js.map +1 -1
- package/dist/shadow/shadowmapper.js +150 -37
- package/dist/shadow/shadowmapper.js.map +1 -1
- package/dist/shapes/shape.js +11 -2
- package/dist/shapes/shape.js.map +1 -1
- package/dist/shapes/sphere.js +50 -9
- package/dist/shapes/sphere.js.map +1 -1
- package/dist/utility/blueprint/common/constants.js +112 -2
- package/dist/utility/blueprint/common/constants.js.map +1 -1
- package/dist/utility/blueprint/material/func.js +142 -49
- package/dist/utility/blueprint/material/func.js.map +1 -1
- package/dist/utility/blueprint/material/ir.js +490 -25
- package/dist/utility/blueprint/material/ir.js.map +1 -1
- package/dist/utility/blueprint/material/texture.js +898 -4
- package/dist/utility/blueprint/material/texture.js.map +1 -1
- package/dist/utility/serialization/manager.js +434 -101
- package/dist/utility/serialization/manager.js.map +1 -1
- package/dist/utility/serialization/scene/animation.js +401 -5
- package/dist/utility/serialization/scene/animation.js.map +1 -1
- package/dist/utility/serialization/scene/batch.js +4 -0
- package/dist/utility/serialization/scene/batch.js.map +1 -1
- package/dist/utility/serialization/scene/camera.js +186 -26
- package/dist/utility/serialization/scene/camera.js.map +1 -1
- package/dist/utility/serialization/scene/common.js +26 -5
- package/dist/utility/serialization/scene/common.js.map +1 -1
- package/dist/utility/serialization/scene/hair.js +1141 -0
- package/dist/utility/serialization/scene/hair.js.map +1 -0
- package/dist/utility/serialization/scene/light.js +84 -4
- package/dist/utility/serialization/scene/light.js.map +1 -1
- package/dist/utility/serialization/scene/material.js +1867 -166
- package/dist/utility/serialization/scene/material.js.map +1 -1
- package/dist/utility/serialization/scene/mesh.js +231 -14
- package/dist/utility/serialization/scene/mesh.js.map +1 -1
- package/dist/utility/serialization/scene/node.js +141 -7
- package/dist/utility/serialization/scene/node.js.map +1 -1
- package/dist/utility/serialization/scene/particle.js +4 -0
- package/dist/utility/serialization/scene/particle.js.map +1 -1
- package/dist/utility/serialization/scene/primitive.js +18 -0
- package/dist/utility/serialization/scene/primitive.js.map +1 -1
- package/dist/utility/serialization/scene/sprite.js +4 -0
- package/dist/utility/serialization/scene/sprite.js.map +1 -1
- package/dist/utility/serialization/scene/terrain.js +4 -0
- package/dist/utility/serialization/scene/terrain.js.map +1 -1
- package/dist/utility/serialization/scene/text.js +4 -0
- package/dist/utility/serialization/scene/text.js.map +1 -1
- package/dist/utility/serialization/scene/water.js +1105 -21
- package/dist/utility/serialization/scene/water.js.map +1 -1
- package/dist/utility/serialization/types.js.map +1 -1
- package/dist/utility/shprojector.js +9 -8
- package/dist/utility/shprojector.js.map +1 -1
- package/dist/values.js +49 -2
- package/dist/values.js.map +1 -1
- package/package.json +3 -3
- package/dist/render/rendergraph/frame_graph_context.js +0 -41
- package/dist/render/rendergraph/frame_graph_context.js.map +0 -1
- package/dist/shaders/ssgi.js +0 -63
- package/dist/shaders/ssgi.js.map +0 -1
- package/dist/shaders/ssgi_reprojection.js +0 -118
- 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 */
|
|
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.
|
|
20
|
-
this.
|
|
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
|
-
/**
|
|
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
|
|
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.
|
|
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.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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,
|
|
398
|
+
device.setBindGroup(0, bindGroup);
|
|
50
399
|
this.drawFullscreenQuad();
|
|
51
400
|
}
|
|
52
|
-
/** @internal */
|
|
53
|
-
if (!MotionBlur.
|
|
54
|
-
MotionBlur.
|
|
55
|
-
vertex
|
|
56
|
-
|
|
57
|
-
this
|
|
58
|
-
this
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
this.$
|
|
63
|
-
|
|
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.
|
|
69
|
-
this.
|
|
70
|
-
this.
|
|
71
|
-
this.
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
this.
|
|
79
|
-
this.$l.
|
|
80
|
-
this.$l.
|
|
81
|
-
this.$
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
this.$l.
|
|
85
|
-
|
|
86
|
-
this.$
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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.
|
|
101
|
-
MotionBlur.
|
|
102
|
-
|
|
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
|