@effect-motion/three 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DepthAwareDof.d.ts +58 -0
- package/dist/DepthAwareDof.js +331 -0
- package/dist/PostProcessing.d.ts +1 -0
- package/dist/PostProcessing.js +1 -0
- package/package.json +56 -56
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Node } from "three/webgpu";
|
|
2
|
+
import type { Pass } from "./PostProcessing.js";
|
|
3
|
+
/** A float parameter: a plain number or a `uniform()` node. */
|
|
4
|
+
export type FloatParam = number | (Node & {
|
|
5
|
+
value: number;
|
|
6
|
+
});
|
|
7
|
+
export interface DepthAwareDofOptions {
|
|
8
|
+
focusDistance: FloatParam;
|
|
9
|
+
aperture: FloatParam;
|
|
10
|
+
maxBlurPx?: FloatParam;
|
|
11
|
+
/**
|
|
12
|
+
* Gather taps per pixel for the near and far fields (default 64 / 48). Fewer
|
|
13
|
+
* taps cost proportionally less GPU time for more sample noise; the blur
|
|
14
|
+
* shape and edges are unchanged. Fixed when the node is built.
|
|
15
|
+
*/
|
|
16
|
+
taps?: {
|
|
17
|
+
readonly near: number;
|
|
18
|
+
readonly far: number;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** The output node, with its parameters as uniforms (set `.value`). */
|
|
22
|
+
export type DepthAwareDofNode = Node & {
|
|
23
|
+
focusDistance: Node & {
|
|
24
|
+
value: number;
|
|
25
|
+
};
|
|
26
|
+
aperture: Node & {
|
|
27
|
+
value: number;
|
|
28
|
+
};
|
|
29
|
+
maxBlurPx: Node & {
|
|
30
|
+
value: number;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
/** Screen px per world unit on the focus plane (perspective camera). */
|
|
34
|
+
export declare function pxPerUnitAtFocus(camera: {
|
|
35
|
+
fov: number;
|
|
36
|
+
zoom: number;
|
|
37
|
+
}, focusDistance: number, height: number): number;
|
|
38
|
+
/**
|
|
39
|
+
* Physical thin-lens CoC radius (px) at view distance d = −viewZ: a lens of
|
|
40
|
+
* radius `aperture` focused at `focusDistance` images the point as a disc of
|
|
41
|
+
* world radius aperture · |d − focus| / d on the focus plane, projected by
|
|
42
|
+
* `pxPerUnit` ({@link pxPerUnitAtFocus}). Near blur grows without bound as
|
|
43
|
+
* d → 0; far blur saturates at aperture · pxPerUnit.
|
|
44
|
+
*/
|
|
45
|
+
export declare function cocRadiusPx(d: number, focusDistance: number, aperture: number, pxPerUnit: number): number;
|
|
46
|
+
/**
|
|
47
|
+
* Depth-aware gather DOF on a continuous per-pixel signed CoC s (px, < 0 near).
|
|
48
|
+
*
|
|
49
|
+
* - Near field: scatter-as-gather over a disc sized by the dilated near-CoC
|
|
50
|
+
* tile max, so blurred foreground spreads over sharp pixels behind it. Each
|
|
51
|
+
* near tap covers R²/N px and spreads over π c², giving coverage α.
|
|
52
|
+
* - Far field: disc of the pixel's own CoC; a tap reaches with min(s_q, s_p),
|
|
53
|
+
* so sharp (s ≈ 0) neighbours mask themselves out and the rest is
|
|
54
|
+
* renormalized — the hidden background is filled from its visible part.
|
|
55
|
+
* - Hidden surfaces come from a depth-peeled second layer: behind near pixels,
|
|
56
|
+
* and behind a missed front tap across a depth jump.
|
|
57
|
+
*/
|
|
58
|
+
export declare function depthAwareDof(pass: Pass, { focusDistance, aperture, maxBlurPx, taps: { near: nearTaps, far: farTaps }, }: DepthAwareDofOptions): DepthAwareDofNode;
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Depth-aware depth of field for three.js WebGPU: a drop-in alternative to
|
|
3
|
+
* three's `dof()` with a physical thin-lens circle of confusion (CoC).
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Ported verbatim (algorithm and constants) from the standalone
|
|
7
|
+
* `DepthAwareDofNode.ts` prototype; only typing and lint conventions differ,
|
|
8
|
+
* plus one fix: the near gather reaches nearR + 0.5 and the centre tap's
|
|
9
|
+
* coverage no longer double-counts, so opaque near surfaces at a few px of
|
|
10
|
+
* CoC stay opaque.
|
|
11
|
+
* Exposed as `PostProcessing.depthAwareDof`.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* const scenePass = PostProcessing.pass(scene, camera);
|
|
15
|
+
* const dofNode = PostProcessing.depthAwareDof(scenePass, {
|
|
16
|
+
* focusDistance: 500, // world units along the view axis
|
|
17
|
+
* aperture: 12.5, // lens radius, world units
|
|
18
|
+
* });
|
|
19
|
+
* const pipeline = PostProcessing.makePipeline(renderer, dofNode);
|
|
20
|
+
* dofNode.focusDistance.value = 300; // parameters are uniforms: change any time
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Parameters (each a number or a float `uniform()`; exposed as uniforms on the
|
|
24
|
+
* returned node):
|
|
25
|
+
* - `focusDistance`: distance of the sharp plane, world units.
|
|
26
|
+
* - `aperture`: lens radius, world units. A point at distance d blurs to a
|
|
27
|
+
* disc of radius aperture · |d − focus| / d · H / (2 · focus · tan(fov / 2))
|
|
28
|
+
* px (H = render height in px), see {@link cocRadiusPx}.
|
|
29
|
+
* - `maxBlurPx` (default 30): CoC radius cap in render-target px. Keep it
|
|
30
|
+
* ≤ (REACH − 1) · TILE = 48, the reach of the near-field tile dilation.
|
|
31
|
+
* The uncapped CoC scales with H, so it looks the same at any pixel ratio;
|
|
32
|
+
* the cap does not (at DPR 2, 30 caps at 15 CSS px). It is not scaled by
|
|
33
|
+
* DPR internally because 30 · 2 would pass the 48 px limit.
|
|
34
|
+
* - `taps` (default near 64 / far 48, plain numbers fixed at build): the gather
|
|
35
|
+
* cost is about linear in them; halving trades GPU time for sample noise.
|
|
36
|
+
*
|
|
37
|
+
* Requirements: WebGPURenderer + RenderPipeline, a PerspectiveCamera, opaque
|
|
38
|
+
* geometry. Resize, pixel ratio and camera near/far/fov/zoom changes are
|
|
39
|
+
* picked up every frame; no rebuild is needed. The node takes over the given
|
|
40
|
+
* pass's rendering (it renders under the node's context), so give it a pass
|
|
41
|
+
* of its own.
|
|
42
|
+
*
|
|
43
|
+
* Known limits:
|
|
44
|
+
* - Transparent surfaces are not handled (the CoC comes from the depth buffer).
|
|
45
|
+
* - The scene is rendered twice per frame: the second, depth-peeled pass finds
|
|
46
|
+
* surfaces hidden behind near / sharp objects (≈ 20–50% of the effect cost).
|
|
47
|
+
* Both passes share their render objects, so the second costs draw calls,
|
|
48
|
+
* not a per-object rebuild.
|
|
49
|
+
* - A nearer blurred surface still composites with a hard step over a surface
|
|
50
|
+
* whose own CoC is > 1 px (a depth-gap split would fix it).
|
|
51
|
+
*/
|
|
52
|
+
import * as TSL from "three/tsl";
|
|
53
|
+
import { FloatType, HalfFloatType, NearestFilter, NodeUpdateType, Vector3, } from "three/webgpu";
|
|
54
|
+
// Shallow signatures: full three/tsl overloads hang tsc.
|
|
55
|
+
const t = TSL;
|
|
56
|
+
/** Near-CoC tile size (px) and dilation reach (tiles): covers ≥ (REACH − 1) · TILE px. */
|
|
57
|
+
const TILE = 16;
|
|
58
|
+
const REACH = 4;
|
|
59
|
+
/** A source never spreads over less than ~one pixel (π·0.5² ≈ 0.8 px²). */
|
|
60
|
+
const MIN_AREA = 0.25;
|
|
61
|
+
/** Relative view-depth gap a fragment needs to count as behind the first layer. */
|
|
62
|
+
const PEEL_EPS = 1e-3;
|
|
63
|
+
const GOLDEN = Math.PI * (3 - Math.sqrt(5));
|
|
64
|
+
/** Screen px per world unit on the focus plane (perspective camera). */
|
|
65
|
+
export function pxPerUnitAtFocus(camera, focusDistance, height) {
|
|
66
|
+
return (height /
|
|
67
|
+
(2 * focusDistance * (Math.tan((camera.fov * Math.PI) / 360) / camera.zoom)));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Physical thin-lens CoC radius (px) at view distance d = −viewZ: a lens of
|
|
71
|
+
* radius `aperture` focused at `focusDistance` images the point as a disc of
|
|
72
|
+
* world radius aperture · |d − focus| / d on the focus plane, projected by
|
|
73
|
+
* `pxPerUnit` ({@link pxPerUnitAtFocus}). Near blur grows without bound as
|
|
74
|
+
* d → 0; far blur saturates at aperture · pxPerUnit.
|
|
75
|
+
*/
|
|
76
|
+
export function cocRadiusPx(d, focusDistance, aperture, pxPerUnit) {
|
|
77
|
+
return ((aperture * Math.abs(d - focusDistance)) / d) * pxPerUnit;
|
|
78
|
+
}
|
|
79
|
+
const asUniform = (p) => (typeof p === "number" ? t.uniform(p) : p);
|
|
80
|
+
function packedTarget(node, scale = 1, type = HalfFloatType) {
|
|
81
|
+
const tex = t.rtt(node, null, null, { type });
|
|
82
|
+
tex.setResolutionScale(scale);
|
|
83
|
+
tex.updateBeforeType = NodeUpdateType.FRAME; // once per frame, not per consumer
|
|
84
|
+
tex.renderTarget.texture.minFilter = NearestFilter;
|
|
85
|
+
tex.renderTarget.texture.magFilter = NearestFilter;
|
|
86
|
+
return tex;
|
|
87
|
+
}
|
|
88
|
+
/** Vogel disc tap i of n at radius R: [screen offset px, radius px]. */
|
|
89
|
+
function vogel(i, n, radius, rot) {
|
|
90
|
+
// Unit-disc taps precomputed (x, y, r): no per-tap sqrt / sincos.
|
|
91
|
+
const taps = Array.from({ length: n }, (_, k) => {
|
|
92
|
+
const r = Math.sqrt((k + 0.5) / n);
|
|
93
|
+
return new Vector3(r * Math.cos(k * GOLDEN), r * Math.sin(k * GOLDEN), r);
|
|
94
|
+
});
|
|
95
|
+
const tap = t.uniformArray(taps, "vec3").element(i);
|
|
96
|
+
const off = t.vec2(tap.x.mul(rot.x).sub(tap.y.mul(rot.y)), tap.x.mul(rot.y).add(tap.y.mul(rot.x)));
|
|
97
|
+
return [off.mul(radius), tap.z.mul(radius)];
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Depth-aware gather DOF on a continuous per-pixel signed CoC s (px, < 0 near).
|
|
101
|
+
*
|
|
102
|
+
* - Near field: scatter-as-gather over a disc sized by the dilated near-CoC
|
|
103
|
+
* tile max, so blurred foreground spreads over sharp pixels behind it. Each
|
|
104
|
+
* near tap covers R²/N px and spreads over π c², giving coverage α.
|
|
105
|
+
* - Far field: disc of the pixel's own CoC; a tap reaches with min(s_q, s_p),
|
|
106
|
+
* so sharp (s ≈ 0) neighbours mask themselves out and the rest is
|
|
107
|
+
* renormalized — the hidden background is filled from its visible part.
|
|
108
|
+
* - Hidden surfaces come from a depth-peeled second layer: behind near pixels,
|
|
109
|
+
* and behind a missed front tap across a depth jump.
|
|
110
|
+
*/
|
|
111
|
+
export function depthAwareDof(pass, { focusDistance, aperture, maxBlurPx = 30, taps: { near: nearTaps, far: farTaps } = { near: 64, far: 48 }, }) {
|
|
112
|
+
const scenePass = pass["~three.pass"];
|
|
113
|
+
const focus = asUniform(focusDistance);
|
|
114
|
+
const lensRadius = asUniform(aperture);
|
|
115
|
+
const maxBlur = asUniform(maxBlurPx);
|
|
116
|
+
const camera = scenePass.camera;
|
|
117
|
+
// CoC px per unit of |d − focus| / d, as a fraction of maxBlur. Per render:
|
|
118
|
+
// follows resize / pixel ratio (the pass target) and camera fov / zoom.
|
|
119
|
+
const cocScale = t
|
|
120
|
+
.uniform(0)
|
|
121
|
+
.onRenderUpdate(() => (lensRadius.value *
|
|
122
|
+
pxPerUnitAtFocus(camera, focus.value, scenePass.renderTarget.height)) /
|
|
123
|
+
maxBlur.value);
|
|
124
|
+
const side = (d, far) => t.min(t
|
|
125
|
+
.max(far ? d.sub(focus) : focus.sub(d), 0)
|
|
126
|
+
.div(d)
|
|
127
|
+
.mul(cocScale), 1);
|
|
128
|
+
const beauty = scenePass.getTextureNode();
|
|
129
|
+
const distance = t.negate(scenePass.getViewZNode());
|
|
130
|
+
const signedCoc = (d) => side(d, true).sub(side(d, false)).mul(maxBlur);
|
|
131
|
+
const signed = signedCoc(distance);
|
|
132
|
+
const packed = packedTarget(t.vec4(beauty.rgb, signed));
|
|
133
|
+
// Second depth layer: re-render, discarding fragments at or in front of the
|
|
134
|
+
// first layer, so hidden surfaces behind near / sharp objects are known.
|
|
135
|
+
// Fragments within 1 px CoC of the first layer are the same surface (e.g. a
|
|
136
|
+
// Line2's overlapping segment caps) and are peeled too, else it counts twice.
|
|
137
|
+
// The front pass can't sample its own depth attachment, so the peel reads a
|
|
138
|
+
// float copy (exact: same size, nearest).
|
|
139
|
+
const frontDepth = packedTarget(scenePass.getTextureNode("depth").sample(t.screenUV), 1, FloatType);
|
|
140
|
+
// Front pass and peel render with ONE context node, the peel test switched
|
|
141
|
+
// by a uniform: three keys render objects by context node, so two nodes
|
|
142
|
+
// would rebuild every scene object's render object twice a frame (CPU-bound
|
|
143
|
+
// with a few hundred entities). Giving the peel render objects of its own
|
|
144
|
+
// instead made output depend on earlier frames (see the render-history test
|
|
145
|
+
// in packages/renderer); shared ones are the front pass's, rendered anyway.
|
|
146
|
+
const back = t.pass(scenePass.scene, scenePass.camera);
|
|
147
|
+
const peeling = t.uniform(0);
|
|
148
|
+
const getOutput = (output) => {
|
|
149
|
+
// A plain texture node, and a new one per material: the rtt node would
|
|
150
|
+
// trigger the copy nested inside the front render, and three caches a
|
|
151
|
+
// texture node's binding across materials, so after a resize only the
|
|
152
|
+
// first render object sharing it would rebind (the rest keep sampling
|
|
153
|
+
// the destroyed texture).
|
|
154
|
+
const frontZ = t.perspectiveDepthToViewZ(t.texture(frontDepth.renderTarget.texture).sample(t.screenUV).x, t.cameraNear, t.cameraFar);
|
|
155
|
+
// Rasterized depth, not positionView: Line2's positionView is its unit
|
|
156
|
+
// quad. fragCoord is declared by the screenUV lookup above.
|
|
157
|
+
const fragZ = t.perspectiveDepthToViewZ(t.builtin("fragCoord.z"), t.cameraNear, t.cameraFar);
|
|
158
|
+
t.Discard(peeling
|
|
159
|
+
.greaterThan(0.5)
|
|
160
|
+
.and(fragZ
|
|
161
|
+
.greaterThan(frontZ.mul(1 + PEEL_EPS))
|
|
162
|
+
.or(t
|
|
163
|
+
.abs(signedCoc(t.negate(fragZ)).sub(signedCoc(t.negate(frontZ))))
|
|
164
|
+
.lessThan(1))));
|
|
165
|
+
return output;
|
|
166
|
+
};
|
|
167
|
+
// Same merge as PassNode's own contextNode: the renderer's flow data first.
|
|
168
|
+
// ponytail: three 0.185 skips a render object's uniform / texture updates
|
|
169
|
+
// unless its MATERIAL holds nodes (NodeMaterialObserver.containsNode), and
|
|
170
|
+
// nodes injected through getOutput don't count, so plain materials would
|
|
171
|
+
// keep a stale `peeling` and the depth copy's destroyed texture (what broke
|
|
172
|
+
// the earlier separate-peel attempt). A context `modelViewMatrix` counts; set
|
|
173
|
+
// to three's own default it changes no shader. Drop it once three checks
|
|
174
|
+
// context nodes too.
|
|
175
|
+
let shared;
|
|
176
|
+
const renderWith = (p, peel) => {
|
|
177
|
+
const updateBefore = p.updateBefore.bind(p);
|
|
178
|
+
p.updateBefore = (frame) => {
|
|
179
|
+
const renderer = frame.renderer;
|
|
180
|
+
const base = renderer.contextNode;
|
|
181
|
+
if (shared?.base !== base || shared.version !== base.version) {
|
|
182
|
+
const node = t.context({
|
|
183
|
+
modelViewMatrix: t.mediumpModelViewMatrix,
|
|
184
|
+
...base.getFlowContextData(),
|
|
185
|
+
getOutput,
|
|
186
|
+
});
|
|
187
|
+
shared = { base, version: base.version, node };
|
|
188
|
+
}
|
|
189
|
+
renderer.contextNode = shared.node;
|
|
190
|
+
peeling.value = peel;
|
|
191
|
+
const result = updateBefore(frame);
|
|
192
|
+
peeling.value = 0;
|
|
193
|
+
renderer.contextNode = base;
|
|
194
|
+
return result;
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
renderWith(scenePass, 0);
|
|
198
|
+
renderWith(back, 1);
|
|
199
|
+
// `packed` and the depth copy first so the front pass renders (once per
|
|
200
|
+
// frame) and is copied before the peel; otherwise the peel would trigger
|
|
201
|
+
// them nested, inside the peel context.
|
|
202
|
+
const backPacked = packedTarget(t.vec4(packed
|
|
203
|
+
.sample(t.uv())
|
|
204
|
+
.a.add(frontDepth.sample(t.uv()).x)
|
|
205
|
+
.mul(0)
|
|
206
|
+
.add(back.getTextureNode().rgb), signedCoc(t.negate(back.getViewZNode()))));
|
|
207
|
+
const texel = t.vec2(1).div(t.vec2(t.textureSize(packed)));
|
|
208
|
+
// Near CoC max per TILE² block (two 4×4 max reductions: short serial loops
|
|
209
|
+
// keep the low-res passes from being latency-bound), then dilated below.
|
|
210
|
+
const maxDown = (src, value, scale) => {
|
|
211
|
+
const srcTexel = t.vec2(1).div(t.vec2(t.textureSize(src)));
|
|
212
|
+
return packedTarget(t.Fn(() => {
|
|
213
|
+
const m = t.float(0).toVar();
|
|
214
|
+
t.Loop(4, 4, ({ i, j }) => {
|
|
215
|
+
const off = t.vec2(t.float(i), t.float(j)).sub(1.5);
|
|
216
|
+
m.assign(t.max(m, value(src.sample(t.uv().add(off.mul(srcTexel))))));
|
|
217
|
+
});
|
|
218
|
+
return m;
|
|
219
|
+
})(), scale);
|
|
220
|
+
};
|
|
221
|
+
const tileMax = maxDown(maxDown(packed, (q) => t.negate(q.a), 1 / 4), (q) => q.x, 1 / TILE);
|
|
222
|
+
const tileTexel = t.vec2(1).div(t.vec2(t.textureSize(tileMax)));
|
|
223
|
+
const nearTile = packedTarget(t.Fn(() => {
|
|
224
|
+
const m = t.float(0).toVar();
|
|
225
|
+
const n = 2 * REACH + 1;
|
|
226
|
+
t.Loop(n, n, ({ i, j }) => {
|
|
227
|
+
const off = t.vec2(t.float(i), t.float(j)).sub(REACH);
|
|
228
|
+
m.assign(t.max(m, tileMax.sample(t.uv().add(off.mul(tileTexel))).x));
|
|
229
|
+
});
|
|
230
|
+
return m;
|
|
231
|
+
})(), 1 / TILE);
|
|
232
|
+
const output = t
|
|
233
|
+
.Fn(() => {
|
|
234
|
+
const uv = t.uv();
|
|
235
|
+
const center = packed.sample(uv);
|
|
236
|
+
// Per-pixel pattern rotation (interleaved gradient noise): trades the
|
|
237
|
+
// tap pattern's banding for fine noise.
|
|
238
|
+
const px = uv.mul(t.vec2(t.textureSize(packed)));
|
|
239
|
+
const ign = t.fract(t.fract(px.x.mul(0.06711056).add(px.y.mul(0.00583715))).mul(52.9829189));
|
|
240
|
+
const angle = ign.mul(2 * Math.PI);
|
|
241
|
+
const rot = t.vec2(t.cos(angle), t.sin(angle));
|
|
242
|
+
const sP = center.a;
|
|
243
|
+
// Near field
|
|
244
|
+
const nearR = nearTile.sample(uv).x;
|
|
245
|
+
// A tap's reach ramps out at r = c + 0.5, so gather that far: cut at
|
|
246
|
+
// nearR, the ramp's outer half is lost and α ≈ 1 − 0.25 / c inside an
|
|
247
|
+
// opaque near surface (see-through at a few px of CoC).
|
|
248
|
+
const gatherR = nearR.add(0.5);
|
|
249
|
+
const tapArea = gatherR.mul(gatherR).div(nearTaps);
|
|
250
|
+
const spread = (c) => t.max(t.max(c.mul(c), tapArea), MIN_AREA);
|
|
251
|
+
// Centre tap: a near pixel's own colour counts in its near average.
|
|
252
|
+
// Only a surface blurred by more than 1 px counts as near: a barely-near
|
|
253
|
+
// one is in focus (CoC < 1 px), so nearer blur composites over it via the
|
|
254
|
+
// far path instead of being normalized together with its centre tap.
|
|
255
|
+
// ponytail: a hard 1 px cut; a [0.5, 1.5] ramp scored slightly worse.
|
|
256
|
+
const nearP = sP.lessThan(-1);
|
|
257
|
+
const cP = t.max(t.negate(sP), 0);
|
|
258
|
+
const w0 = t.select(nearP, t.float(1).div(spread(cP)), 0);
|
|
259
|
+
const nearSum = center.rgb.mul(w0).toVar();
|
|
260
|
+
const nearW = w0.toVar();
|
|
261
|
+
// The centre tap's coverage is only what the disc taps under-sample:
|
|
262
|
+
// spaced for gatherR, they resolve p's own disc (area ∝ cP²) when
|
|
263
|
+
// gatherR ≈ cP but miss it when a bigger nearby CoC dilated the tile.
|
|
264
|
+
// A flat extra term would double-count and bulge α at edges.
|
|
265
|
+
const alphaSum = w0
|
|
266
|
+
.mul(t.max(tapArea.sub(cP.mul(cP).div(nearTaps)), 0))
|
|
267
|
+
.toVar();
|
|
268
|
+
t.If(nearR.greaterThan(0.5), () => {
|
|
269
|
+
t.Loop(nearTaps, ({ i }) => {
|
|
270
|
+
const [off, r] = vogel(i, nearTaps, gatherR, rot);
|
|
271
|
+
const q = packed.sample(uv.add(off.mul(texel)));
|
|
272
|
+
const c = t.negate(q.a);
|
|
273
|
+
const isNear = c.greaterThan(0);
|
|
274
|
+
const reach = t.clamp(c.sub(r).add(0.5), 0, 1);
|
|
275
|
+
const w = t.select(isNear, reach.div(spread(c)), 0);
|
|
276
|
+
nearSum.addAssign(q.rgb.mul(w));
|
|
277
|
+
nearW.addAssign(w);
|
|
278
|
+
alphaSum.addAssign(w.mul(tapArea));
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
const alpha = t.min(alphaSum, 1);
|
|
282
|
+
const nearColor = nearSum.div(t.max(nearW, 1e-6));
|
|
283
|
+
// Far / focus field of the scene with near surfaces peeled away: per
|
|
284
|
+
// tap, the front layer if it reaches p, else the layer hidden behind it.
|
|
285
|
+
// Layer 2 can itself be near (a line behind a nearer disc): it is then
|
|
286
|
+
// blurred by its own CoC |sB| too, near layer-2 taps included.
|
|
287
|
+
const bP = t.select(nearP, backPacked.sample(uv), center);
|
|
288
|
+
const sB = bP.a;
|
|
289
|
+
const cB = t.abs(sB);
|
|
290
|
+
const farColor = bP.rgb.toVar();
|
|
291
|
+
t.If(cB.greaterThan(0.5), () => {
|
|
292
|
+
const farArea = t.max(cB.mul(cB).div(farTaps), MIN_AREA);
|
|
293
|
+
const spreadF = (c) => t.max(c.mul(c), farArea);
|
|
294
|
+
const sum = bP.rgb.div(spreadF(cB)).toVar();
|
|
295
|
+
const wSum = t.float(1).div(spreadF(cB)).toVar();
|
|
296
|
+
t.Loop(farTaps, ({ i }) => {
|
|
297
|
+
const [off, r] = vogel(i, farTaps, cB, rot);
|
|
298
|
+
const quv = uv.add(off.mul(texel));
|
|
299
|
+
const q1 = packed.sample(quv);
|
|
300
|
+
const c1 = t.min(t.abs(q1.a), cB);
|
|
301
|
+
// In front of p's hidden surface (left to the near field): any near
|
|
302
|
+
// tap, or when that surface is itself near, a clearly nearer one.
|
|
303
|
+
const inFront = q1.a.lessThan(t.select(sB.lessThan(0), sB.sub(1), 0));
|
|
304
|
+
const reach1 = t.select(inFront, 0, t.clamp(c1.sub(r).add(0.5), 0, 1));
|
|
305
|
+
const w1 = reach1.div(spreadF(c1));
|
|
306
|
+
sum.addAssign(q1.rgb.mul(w1));
|
|
307
|
+
wSum.addAssign(w1);
|
|
308
|
+
// A missed front tap only reveals layer 2 across a depth jump; on a
|
|
309
|
+
// continuous surface (s changes slowly with r) the ray hits it anyway.
|
|
310
|
+
const jump = t.select(inFront, 1, t.clamp(cB.sub(c1).div(t.max(r, 1)).sub(0.25).mul(4), 0, 1));
|
|
311
|
+
const miss = t.float(1).sub(reach1).mul(jump);
|
|
312
|
+
t.If(miss.greaterThan(0), () => {
|
|
313
|
+
const q2 = backPacked.sample(quv);
|
|
314
|
+
const c2 = t.min(t.abs(q2.a), cB);
|
|
315
|
+
const reach2 = t.select(q2.a.lessThan(0).and(sB.greaterThan(0)), 0, t.clamp(c2.sub(r).add(0.5), 0, 1));
|
|
316
|
+
const w2 = miss.mul(reach2).div(spreadF(c2));
|
|
317
|
+
sum.addAssign(q2.rgb.mul(w2));
|
|
318
|
+
wSum.addAssign(w2);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
farColor.assign(sum.div(t.max(wSum, 1e-6)));
|
|
322
|
+
});
|
|
323
|
+
return t.vec4(t.mix(farColor, nearColor, alpha), 1);
|
|
324
|
+
})()
|
|
325
|
+
.toInspector("Composite");
|
|
326
|
+
return Object.assign(output, {
|
|
327
|
+
focusDistance: focus,
|
|
328
|
+
aperture: lensRadius,
|
|
329
|
+
maxBlurPx: maxBlur,
|
|
330
|
+
});
|
|
331
|
+
}
|
package/dist/PostProcessing.d.ts
CHANGED
|
@@ -109,4 +109,5 @@ export declare const makePipeline: (renderer: Renderer.Renderer, outputNode: unk
|
|
|
109
109
|
* renders the passes the graph depends on and applies the output transform.
|
|
110
110
|
*/
|
|
111
111
|
export declare const render: (self: RenderPipeline) => Effect.Effect<void, ThreeException>;
|
|
112
|
+
export { cocRadiusPx, type DepthAwareDofNode, type DepthAwareDofOptions, depthAwareDof, type FloatParam, pxPerUnitAtFocus, } from "./DepthAwareDof.js";
|
|
112
113
|
export { uniform };
|
package/dist/PostProcessing.js
CHANGED
|
@@ -99,4 +99,5 @@ export const makePipeline = (renderer, outputNode) => {
|
|
|
99
99
|
* renders the passes the graph depends on and applies the output transform.
|
|
100
100
|
*/
|
|
101
101
|
export const render = (self) => wrap("RenderPipeline.render", () => self["~three.renderPipeline"].render());
|
|
102
|
+
export { cocRadiusPx, depthAwareDof, pxPerUnitAtFocus, } from "./DepthAwareDof.js";
|
|
102
103
|
export { uniform };
|
package/package.json
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
2
|
+
"name": "@effect-motion/three",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Bindings-only Effect wrapper over three.js for effect-motion",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/julia-script/effect-motion.git",
|
|
10
|
+
"directory": "packages/three"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/julia-script/effect-motion#readme",
|
|
13
|
+
"bugs": "https://github.com/julia-script/effect-motion/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"effect",
|
|
16
|
+
"motion",
|
|
17
|
+
"three",
|
|
18
|
+
"webgpu",
|
|
19
|
+
"motion-graphics"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./*": {
|
|
27
|
+
"types": "./dist/*.d.ts",
|
|
28
|
+
"default": "./dist/*.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc -p tsconfig.build.json",
|
|
36
|
+
"dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
|
|
37
|
+
"test": "vitest run --passWithNoTests",
|
|
38
|
+
"check": "tsc --noEmit"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@types/three": "^0.185.1",
|
|
45
|
+
"three": "^0.185.1",
|
|
46
|
+
"webgpu": "^0.4.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"effect": ">=4.0.0-rc.115"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^26.1.1",
|
|
53
|
+
"effect": "4.0.0-rc.115",
|
|
54
|
+
"typescript": "^7.0.2",
|
|
55
|
+
"vitest": "^4.1.10"
|
|
56
|
+
}
|
|
57
|
+
}
|