@react-three/postprocessing 3.0.4 → 3.1.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/LICENSE +21 -21
- package/README.md +80 -81
- package/dist/EffectComposer.d.ts +14 -6
- package/dist/EffectGroup.d.ts +9 -0
- package/dist/Selection.d.ts +8 -8
- package/dist/createEffectComponent.d.ts +30 -0
- package/dist/effects/ASCII.d.ts +29 -7
- package/dist/effects/Autofocus.d.ts +8 -7
- package/dist/effects/Bloom.d.ts +10 -5
- package/dist/effects/BrightnessContrast.d.ts +15 -5
- package/dist/effects/ChromaticAberration.d.ts +30 -7
- package/dist/effects/ColorAverage.d.ts +7 -8
- package/dist/effects/ColorDepth.d.ts +8 -5
- package/dist/effects/Depth.d.ts +13 -5
- package/dist/effects/DepthOfField.d.ts +7 -18
- package/dist/effects/DotScreen.d.ts +15 -5
- package/dist/effects/FXAA.d.ts +11 -5
- package/dist/effects/Glitch.d.ts +17 -29
- package/dist/effects/GodRays.d.ts +7 -19
- package/dist/effects/Grid.d.ts +8 -8
- package/dist/effects/HueSaturation.d.ts +15 -5
- package/dist/effects/LUT.d.ts +4 -3
- package/dist/effects/LensFlare.d.ts +52 -11
- package/dist/effects/Noise.d.ts +8 -5
- package/dist/effects/Outline.d.ts +9 -25
- package/dist/effects/Pixelation.d.ts +3 -2
- package/dist/effects/Ramp.d.ts +58 -10
- package/dist/effects/SMAA.d.ts +10 -5
- package/dist/effects/SSAO.d.ts +7 -30
- package/dist/effects/ScanlineEffect.d.ts +13 -5
- package/dist/effects/SelectiveBloom.d.ts +5 -10
- package/dist/effects/Sepia.d.ts +13 -5
- package/dist/effects/ShockWave.d.ts +12 -5
- package/dist/effects/Texture.d.ts +7 -8
- package/dist/effects/TiltShift.d.ts +10 -5
- package/dist/effects/TiltShift2.d.ts +44 -8
- package/dist/effects/ToneMapping.d.ts +9 -7
- package/dist/effects/Vignette.d.ts +19 -5
- package/dist/effects/Water.d.ts +17 -5
- package/dist/index.d.ts +15 -11
- package/dist/index.js +1841 -45
- package/dist/index.js.map +1 -1
- package/dist/passes/DepthPicking.d.ts +11 -0
- package/dist/{effects → passes}/N8AO.d.ts +5 -2
- package/dist/util.d.ts +19 -17
- package/dist/wrapEffect.d.ts +12 -0
- package/package.json +74 -67
- package/src/EffectComposer.tsx +379 -200
- package/src/EffectGroup.tsx +74 -0
- package/src/Selection.tsx +86 -46
- package/src/createEffectComponent.tsx +71 -0
- package/src/effects/ASCII.tsx +183 -135
- package/src/effects/Autofocus.tsx +122 -153
- package/src/effects/Bloom.tsx +34 -6
- package/src/effects/BrightnessContrast.tsx +7 -4
- package/src/effects/ChromaticAberration.tsx +22 -5
- package/src/effects/ColorAverage.tsx +4 -15
- package/src/effects/ColorDepth.tsx +25 -4
- package/src/effects/Depth.tsx +6 -4
- package/src/effects/DepthOfField.tsx +109 -89
- package/src/effects/DotScreen.tsx +7 -4
- package/src/effects/FXAA.tsx +6 -4
- package/src/effects/Glitch.tsx +32 -40
- package/src/effects/GodRays.tsx +104 -16
- package/src/effects/Grid.tsx +27 -21
- package/src/effects/HueSaturation.tsx +7 -4
- package/src/effects/LUT.tsx +25 -26
- package/src/effects/LensFlare.tsx +765 -611
- package/src/effects/Noise.tsx +16 -4
- package/src/effects/Outline.tsx +84 -117
- package/src/effects/Pixelation.tsx +20 -15
- package/src/effects/Ramp.tsx +239 -150
- package/src/effects/SMAA.tsx +20 -4
- package/src/effects/SSAO.tsx +152 -41
- package/src/effects/ScanlineEffect.tsx +7 -7
- package/src/effects/SelectiveBloom.tsx +98 -126
- package/src/effects/Sepia.tsx +6 -4
- package/src/effects/ShockWave.tsx +32 -4
- package/src/effects/Texture.tsx +28 -23
- package/src/effects/TiltShift.tsx +32 -4
- package/src/effects/TiltShift2.tsx +156 -90
- package/src/effects/ToneMapping.tsx +19 -6
- package/src/effects/Vignette.tsx +7 -4
- package/src/effects/Water.tsx +50 -35
- package/src/index.ts +43 -40
- package/src/passes/DepthPicking.tsx +63 -0
- package/src/passes/N8AO.tsx +105 -0
- package/src/tests/Autofocus.test.tsx +38 -0
- package/src/tests/Bloom.test.tsx +76 -0
- package/src/tests/ChromaticAberration.test.tsx +81 -0
- package/src/tests/ColorDepth.test.tsx +71 -0
- package/src/tests/DepthOfField.test.tsx +130 -0
- package/src/tests/DepthPicking.test.tsx +250 -0
- package/src/tests/EffectComposer.test.tsx +1404 -0
- package/src/tests/EffectGroup.test.tsx +337 -0
- package/src/tests/Glitch.test.tsx +56 -0
- package/src/tests/GodRays.test.tsx +101 -0
- package/src/tests/Grid.test.tsx +34 -0
- package/src/tests/LUT.test.tsx +64 -0
- package/src/tests/N8AO.test.tsx +92 -0
- package/src/tests/Outline.test.tsx +211 -0
- package/src/tests/SSAO.test.tsx +140 -0
- package/src/tests/Selection.test.tsx +324 -0
- package/src/tests/SelectiveBloom.test.tsx +166 -0
- package/src/tests/ShockWave.test.tsx +95 -0
- package/src/tests/TiltShift.test.tsx +76 -0
- package/src/tests/createEffectComponent.test.tsx +286 -0
- package/src/tests/effects.smoke.test.tsx +238 -0
- package/src/tests/test-utils.tsx +96 -0
- package/src/tests/wrapEffect.test.tsx +209 -0
- package/src/util.tsx +221 -55
- package/src/wrapEffect.tsx +123 -0
- package/src/EffectComposer.test.tsx +0 -126
- package/src/effects/N8AO.tsx +0 -81
package/src/util.tsx
CHANGED
|
@@ -1,55 +1,221 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
export
|
|
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
|
-
|
|
1
|
+
import { useThree, type Instance, type ReactThreeFiber } from '@react-three/fiber'
|
|
2
|
+
import type { Selection as PPSelection } from 'postprocessing'
|
|
3
|
+
import { use, useCallback, useEffect, useLayoutEffect, useMemo, useRef, type Ref, type RefObject } from 'react'
|
|
4
|
+
import { Group, Object3D, Vector2, Vector3, type Vector2Tuple, type Vector3Tuple } from 'three'
|
|
5
|
+
import { selectionContext } from './Selection'
|
|
6
|
+
|
|
7
|
+
// Stable reference for array-typed props defaulting to "nothing" - `= []`
|
|
8
|
+
// as a default parameter allocates a new array on every call, which is
|
|
9
|
+
// enough to retrigger any effect that depends on it.
|
|
10
|
+
export const EMPTY_ARRAY: never[] = []
|
|
11
|
+
|
|
12
|
+
export const resolveRef = <T,>(ref: T | RefObject<T>) =>
|
|
13
|
+
typeof ref === 'object' && ref != null && 'current' in ref ? ref.current : ref
|
|
14
|
+
|
|
15
|
+
// Merges a local ref with a caller-supplied ref (object or callback form).
|
|
16
|
+
// Clears localRef inside the callback ref's own returned cleanup, since
|
|
17
|
+
// React 19 never re-invokes it with null once it returns one.
|
|
18
|
+
export function useMergeRefs<T>(
|
|
19
|
+
localRef: RefObject<T | null>,
|
|
20
|
+
outerRef: Ref<T> | undefined
|
|
21
|
+
): (instance: T | null) => void | (() => void) {
|
|
22
|
+
return useCallback(
|
|
23
|
+
(instance: T | null) => {
|
|
24
|
+
localRef.current = instance
|
|
25
|
+
if (typeof outerRef !== 'function') {
|
|
26
|
+
if (outerRef) outerRef.current = instance
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
const cleanup = outerRef(instance)
|
|
30
|
+
if (typeof cleanup !== 'function') return
|
|
31
|
+
return () => {
|
|
32
|
+
localRef.current = null
|
|
33
|
+
cleanup()
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
[localRef, outerRef]
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Reads a <group>'s direct r3f children, filtered by type - transparent to
|
|
41
|
+
// non-host wrapper components, since r3f's instance tree already is.
|
|
42
|
+
export function readGroupChildren<T>(group: Group, filter: (object: unknown) => object is T): T[] {
|
|
43
|
+
const groupInstance = (group as Group & { __r3f: Instance<Group> }).__r3f
|
|
44
|
+
return groupInstance ? groupInstance.children.map((child) => child.object).filter(filter) : []
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Diffs `next` against what's stored in `ref`, updating it only when the
|
|
48
|
+
// contents actually changed. Returns whether it changed.
|
|
49
|
+
export function updateIfChanged<T>(ref: RefObject<T[]>, next: T[]): boolean {
|
|
50
|
+
const previous = ref.current
|
|
51
|
+
if (next.length === previous.length && next.every((item, i) => item === previous[i])) return false
|
|
52
|
+
ref.current = next
|
|
53
|
+
return true
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Keeps `selection` synced with whichever mode is active: the manual
|
|
57
|
+
// `selection` prop, or the declarative Selection/Select API (context wins).
|
|
58
|
+
export function useSelectionSync(
|
|
59
|
+
effect: { selection: PPSelection },
|
|
60
|
+
selection: Object3D | Object3D[] | RefObject<Object3D | null> | RefObject<Object3D | null>[],
|
|
61
|
+
selectionLayer: number
|
|
62
|
+
): void {
|
|
63
|
+
const invalidate = useThree((state) => state.invalidate)
|
|
64
|
+
const api = use(selectionContext)
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
effect.selection.layer = selectionLayer
|
|
68
|
+
invalidate()
|
|
69
|
+
}, [effect, invalidate, selectionLayer])
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (api) return
|
|
73
|
+
const resolved = (Array.isArray(selection) ? selection.map((o) => resolveRef(o)) : [resolveRef(selection)]).filter(
|
|
74
|
+
(o): o is Object3D => o != null
|
|
75
|
+
)
|
|
76
|
+
if (!resolved.length) return
|
|
77
|
+
|
|
78
|
+
effect.selection.set(resolved)
|
|
79
|
+
invalidate()
|
|
80
|
+
return () => {
|
|
81
|
+
effect.selection.clear()
|
|
82
|
+
invalidate()
|
|
83
|
+
}
|
|
84
|
+
}, [effect, selection, api, invalidate])
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (api && api.enabled && api.selected?.length) {
|
|
88
|
+
effect.selection.set(api.selected)
|
|
89
|
+
invalidate()
|
|
90
|
+
return () => {
|
|
91
|
+
effect.selection.clear()
|
|
92
|
+
invalidate()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}, [api, effect.selection, invalidate])
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// r3f never disposes <primitive> objects, so they must dispose themselves.
|
|
99
|
+
export const useDispose = <T extends { dispose?: () => void }>(instance: T): void => {
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
return () => {
|
|
102
|
+
instance?.dispose?.()
|
|
103
|
+
}
|
|
104
|
+
}, [instance])
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Reads a plain or r3f-pierced ("a-b-c") key off an object, matching what
|
|
108
|
+
// applyProps below can write - a single reader that works for both shapes.
|
|
109
|
+
export function readPierced(instance: object, key: string): unknown {
|
|
110
|
+
let target: unknown = instance
|
|
111
|
+
for (const part of key.split('-')) {
|
|
112
|
+
if (target == null) return undefined
|
|
113
|
+
target = (target as Record<string, unknown>)[part]
|
|
114
|
+
}
|
|
115
|
+
return target
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Writes a plain or r3f-pierced ("a-b-c") key, mirroring readPierced.
|
|
119
|
+
// Deliberately a plain assignment, not r3f's applyProps: these instances
|
|
120
|
+
// are built with `new`, not r3f's reconciler, so applyProps' Color/Vector
|
|
121
|
+
// coercion never applies to them. Callers wrap values that need coercion.
|
|
122
|
+
export function applyPierced(instance: object, key: string, value: unknown): void {
|
|
123
|
+
const parts = key.split('-')
|
|
124
|
+
let target: unknown = instance
|
|
125
|
+
for (let idx = 0; idx < parts.length - 1; idx++) {
|
|
126
|
+
if (target == null) return
|
|
127
|
+
target = (target as Record<string, unknown>)[parts[idx]]
|
|
128
|
+
}
|
|
129
|
+
if (target == null) return
|
|
130
|
+
;(target as Record<string, unknown>)[parts[parts.length - 1]] = value
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Applies live-mutable properties onto an instance built via `new` (not
|
|
134
|
+
// r3f's reconciler, so r3f's own prop diffing/reset never runs on it).
|
|
135
|
+
// Falls back to the constructor-time default when a value is `undefined`.
|
|
136
|
+
// Only calls `set` when the resolved value actually changed since the last
|
|
137
|
+
// apply - some setters have side effects beyond storing the value (e.g.
|
|
138
|
+
// OutlineEffect's `multisampling` disposes its render target on every set).
|
|
139
|
+
export function useLiveDefaults<T extends object>(
|
|
140
|
+
instance: T | RefObject<T | null> | null,
|
|
141
|
+
values: Record<string, unknown>,
|
|
142
|
+
keys: Iterable<string>,
|
|
143
|
+
get: (instance: T, key: string) => unknown = readPierced,
|
|
144
|
+
set: (instance: T, key: string, value: unknown) => void = applyPierced
|
|
145
|
+
): void {
|
|
146
|
+
const snapshotRef = useRef<{ instance: T; defaults: Map<string, unknown>; applied: Map<string, unknown> } | null>(
|
|
147
|
+
null
|
|
148
|
+
)
|
|
149
|
+
const invalidate = useThree((state) => state.invalidate)
|
|
150
|
+
|
|
151
|
+
useLayoutEffect(() => {
|
|
152
|
+
const resolved = resolveRef(instance)
|
|
153
|
+
if (!resolved) return
|
|
154
|
+
if (snapshotRef.current?.instance !== resolved) {
|
|
155
|
+
snapshotRef.current = { instance: resolved, defaults: new Map(), applied: new Map() }
|
|
156
|
+
}
|
|
157
|
+
const { defaults, applied } = snapshotRef.current
|
|
158
|
+
let changed = false
|
|
159
|
+
|
|
160
|
+
for (const key of keys) {
|
|
161
|
+
if (!defaults.has(key)) {
|
|
162
|
+
// Seed `applied` too, not just `defaults`, so an unchanged key
|
|
163
|
+
// skips `set` even on this first pass (avoids re-triggering
|
|
164
|
+
// setters with side effects, e.g. multisampling's dispose).
|
|
165
|
+
const current = get(resolved, key)
|
|
166
|
+
defaults.set(key, current)
|
|
167
|
+
applied.set(key, current)
|
|
168
|
+
}
|
|
169
|
+
const next = values[key] !== undefined ? values[key] : defaults.get(key)
|
|
170
|
+
if (Object.is(applied.get(key), next)) continue
|
|
171
|
+
set(resolved, key, next)
|
|
172
|
+
applied.set(key, next)
|
|
173
|
+
changed = true
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// These instances are mutated directly (not via r3f's reconciler), so
|
|
177
|
+
// r3f never sees the change - without this, frameloop="demand" would
|
|
178
|
+
// never repaint after a live prop update.
|
|
179
|
+
if (changed) invalidate()
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const useVector2 = (props: Record<string, unknown>, key: string): Vector2 => {
|
|
184
|
+
const value = props[key] as ReactThreeFiber.Vector2 | undefined
|
|
185
|
+
|
|
186
|
+
return useMemo(() => {
|
|
187
|
+
if (typeof value === 'number') {
|
|
188
|
+
return new Vector2(value, value)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (value instanceof Vector2) {
|
|
192
|
+
return value
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (value) {
|
|
196
|
+
return new Vector2(...(value as Vector2Tuple))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return new Vector2()
|
|
200
|
+
}, [value])
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export const useVector3 = (props: Record<string, unknown>, key: string): Vector3 => {
|
|
204
|
+
const value = props[key] as ReactThreeFiber.Vector3 | undefined
|
|
205
|
+
|
|
206
|
+
return useMemo(() => {
|
|
207
|
+
if (typeof value === 'number') {
|
|
208
|
+
return new Vector3(value, value, value)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (value instanceof Vector3) {
|
|
212
|
+
return value
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (value) {
|
|
216
|
+
return new Vector3(...(value as Vector3Tuple))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return new Vector3()
|
|
220
|
+
}, [value])
|
|
221
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { extend, useThree } from '@react-three/fiber'
|
|
2
|
+
import type { BlendFunction } from 'postprocessing'
|
|
3
|
+
import { useMemo, type ExoticComponent, type JSX, type Ref } from 'react'
|
|
4
|
+
import type { EffectConstructor } from './createEffectComponent'
|
|
5
|
+
|
|
6
|
+
export type { EffectConstructor }
|
|
7
|
+
|
|
8
|
+
// Handles three ConstructorParameters<T> shapes: required first param
|
|
9
|
+
// (P), optional first param (Partial<P> — some effects in postprocessing
|
|
10
|
+
// type inner fields as required even though the whole object is
|
|
11
|
+
// omittable), and no params at all ({})
|
|
12
|
+
type FirstConstructorParam<T extends EffectConstructor> =
|
|
13
|
+
ConstructorParameters<T> extends [infer P, ...any[]]
|
|
14
|
+
? P
|
|
15
|
+
: ConstructorParameters<T> extends [(infer P)?, ...any[]]
|
|
16
|
+
? Partial<P>
|
|
17
|
+
: {}
|
|
18
|
+
|
|
19
|
+
export type EffectProps<T extends EffectConstructor> = FirstConstructorParam<T> & {
|
|
20
|
+
ref?: Ref<InstanceType<T>>
|
|
21
|
+
blendFunction?: BlendFunction
|
|
22
|
+
opacity?: number
|
|
23
|
+
args?: ConstructorParameters<T>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let i = 0
|
|
27
|
+
|
|
28
|
+
const components = new WeakMap<EffectConstructor, ExoticComponent<any> | string>()
|
|
29
|
+
|
|
30
|
+
const identityTokens = new WeakMap<object, number>()
|
|
31
|
+
let nextIdentityToken = 0
|
|
32
|
+
|
|
33
|
+
// Same object in, same token out — lets otherwise-opaque values (textures,
|
|
34
|
+
// refs, any class instance) still change the fingerprint when swapped for
|
|
35
|
+
// a different instance, without walking into them.
|
|
36
|
+
function identityOf(value: object): number {
|
|
37
|
+
let token = identityTokens.get(value)
|
|
38
|
+
if (token === undefined) {
|
|
39
|
+
token = nextIdentityToken++
|
|
40
|
+
identityTokens.set(value, token)
|
|
41
|
+
}
|
|
42
|
+
return token
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Walks props into a JSON-safe fingerprint for the args memo below. Two
|
|
46
|
+
// things JSON.stringify can't be trusted with here:
|
|
47
|
+
// - it throws on cycles (refs, or any three.js object with a back-
|
|
48
|
+
// reference), so cycles are cut off with a WeakSet guard instead.
|
|
49
|
+
// - it calls a value's own toJSON first if present, which for e.g.
|
|
50
|
+
// THREE.Texture re-encodes the whole image to a base64 data URL on
|
|
51
|
+
// every single render (measured ~39ms for a 512x512 texture) just to
|
|
52
|
+
// throw the result away. Reading properties directly sidesteps that.
|
|
53
|
+
// - reading properties directly instead means typed arrays need their
|
|
54
|
+
// own case: Object.keys() on one returns every numeric index, so
|
|
55
|
+
// walking a texture's backing buffer element-by-element is even
|
|
56
|
+
// slower (~340ms for the same texture) than the toJSON it replaces.
|
|
57
|
+
// Identity is enough — same buffer means unchanged.
|
|
58
|
+
function fingerprint(value: unknown, seen: WeakSet<object>): unknown {
|
|
59
|
+
if (value === null || typeof value !== 'object') return value
|
|
60
|
+
if (seen.has(value)) return '[Circular]'
|
|
61
|
+
|
|
62
|
+
if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
|
|
63
|
+
return identityOf(value)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
seen.add(value)
|
|
67
|
+
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
return value.map((item) => fingerprint(item, seen))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const out: Record<string, unknown> = {}
|
|
73
|
+
for (const key of Object.keys(value).sort()) {
|
|
74
|
+
out[key] = fingerprint((value as Record<string, unknown>)[key], seen)
|
|
75
|
+
}
|
|
76
|
+
return out
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function stableStringify(value: unknown): string {
|
|
80
|
+
return JSON.stringify(fingerprint(value, new WeakSet()))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function wrapEffect<T extends EffectConstructor>(
|
|
84
|
+
effect: T,
|
|
85
|
+
defaults?: EffectProps<T>
|
|
86
|
+
): (props: EffectProps<T>) => JSX.Element {
|
|
87
|
+
return function WrappedEffect({
|
|
88
|
+
ref,
|
|
89
|
+
blendFunction = defaults?.blendFunction,
|
|
90
|
+
opacity = defaults?.opacity,
|
|
91
|
+
...props
|
|
92
|
+
}) {
|
|
93
|
+
let Component = components.get(effect)
|
|
94
|
+
|
|
95
|
+
if (!Component) {
|
|
96
|
+
const key = `@react-three/postprocessing/${effect.name}-${i++}`
|
|
97
|
+
extend({ [key]: effect })
|
|
98
|
+
components.set(effect, (Component = key))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const camera = useThree((state) => state.camera)
|
|
102
|
+
|
|
103
|
+
const args = useMemo(
|
|
104
|
+
() => [
|
|
105
|
+
...((defaults?.args as unknown as any[]) ?? []),
|
|
106
|
+
...((props.args as unknown as any[]) ?? [{ ...defaults, ...props }]),
|
|
107
|
+
],
|
|
108
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
109
|
+
[stableStringify(props)]
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<Component
|
|
114
|
+
camera={camera}
|
|
115
|
+
blendMode-blendFunction={blendFunction}
|
|
116
|
+
blendMode-opacity-value={opacity}
|
|
117
|
+
{...props}
|
|
118
|
+
args={args}
|
|
119
|
+
ref={ref}
|
|
120
|
+
/>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import * as React from 'react'
|
|
2
|
-
import * as THREE from 'three'
|
|
3
|
-
import { vi, describe, it, expect } from 'vitest'
|
|
4
|
-
import { extend, createRoot, act } from '@react-three/fiber'
|
|
5
|
-
import { EffectComposer } from './EffectComposer'
|
|
6
|
-
import { EffectComposer as EffectComposerImpl, RenderPass, Pass, Effect, EffectPass } from 'postprocessing'
|
|
7
|
-
|
|
8
|
-
// Let React know that we'll be testing effectful components
|
|
9
|
-
declare global {
|
|
10
|
-
var IS_REACT_ACT_ENVIRONMENT: boolean
|
|
11
|
-
}
|
|
12
|
-
global.IS_REACT_ACT_ENVIRONMENT = true
|
|
13
|
-
|
|
14
|
-
// Create virtual R3F root for testing
|
|
15
|
-
extend(THREE as any)
|
|
16
|
-
const root = createRoot({
|
|
17
|
-
style: {} as CSSStyleDeclaration,
|
|
18
|
-
addEventListener: (() => {}) as any,
|
|
19
|
-
removeEventListener: (() => {}) as any,
|
|
20
|
-
width: 1280,
|
|
21
|
-
height: 800,
|
|
22
|
-
clientWidth: 1280,
|
|
23
|
-
clientHeight: 800,
|
|
24
|
-
getContext: (() =>
|
|
25
|
-
new Proxy(
|
|
26
|
-
{},
|
|
27
|
-
{
|
|
28
|
-
get(_target, prop) {
|
|
29
|
-
switch (prop) {
|
|
30
|
-
case 'getParameter':
|
|
31
|
-
return () => 'WebGL 2' // GL_VERSION
|
|
32
|
-
case 'getExtension':
|
|
33
|
-
return () => ({}) // EXT_blend_minmax
|
|
34
|
-
case 'getContextAttributes':
|
|
35
|
-
return () => ({ alpha: true })
|
|
36
|
-
case 'getShaderPrecisionFormat':
|
|
37
|
-
return () => ({ rangeMin: 1, rangeMax: 1, precision: 1 })
|
|
38
|
-
default:
|
|
39
|
-
return () => {}
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
}
|
|
43
|
-
)) as any,
|
|
44
|
-
} satisfies Partial<HTMLCanvasElement> as HTMLCanvasElement)
|
|
45
|
-
root.configure({ frameloop: 'never' })
|
|
46
|
-
|
|
47
|
-
const EFFECT_SHADER = 'mainImage() {}'
|
|
48
|
-
|
|
49
|
-
describe('EffectComposer', () => {
|
|
50
|
-
it('should merge effects together', async () => {
|
|
51
|
-
const composerRef = React.createRef<EffectComposerImpl>()
|
|
52
|
-
|
|
53
|
-
const effectA = new Effect('A', EFFECT_SHADER)
|
|
54
|
-
const effectB = new Effect('B', EFFECT_SHADER)
|
|
55
|
-
const effectC = new Effect('C', EFFECT_SHADER)
|
|
56
|
-
const passA = new Pass()
|
|
57
|
-
const passB = new Pass()
|
|
58
|
-
|
|
59
|
-
// Forward order
|
|
60
|
-
await act(async () =>
|
|
61
|
-
root.render(
|
|
62
|
-
<EffectComposer ref={composerRef}>
|
|
63
|
-
{/* EffectPass(effectA, effectB) */}
|
|
64
|
-
<primitive object={effectA} />
|
|
65
|
-
<primitive object={effectB} />
|
|
66
|
-
{/* PassA */}
|
|
67
|
-
<primitive object={passA} />
|
|
68
|
-
{/* EffectPass(effectC) */}
|
|
69
|
-
<primitive object={effectC} />
|
|
70
|
-
{/* PassB */}
|
|
71
|
-
<primitive object={passB} />
|
|
72
|
-
</EffectComposer>
|
|
73
|
-
)
|
|
74
|
-
)
|
|
75
|
-
expect(composerRef.current!.passes.map((p) => p.constructor)).toStrictEqual([
|
|
76
|
-
RenderPass,
|
|
77
|
-
EffectPass,
|
|
78
|
-
Pass,
|
|
79
|
-
EffectPass,
|
|
80
|
-
Pass,
|
|
81
|
-
])
|
|
82
|
-
// @ts-expect-error
|
|
83
|
-
expect((composerRef.current!.passes[1] as EffectPass).effects).toStrictEqual([effectA, effectB])
|
|
84
|
-
expect(composerRef.current!.passes[2]).toBe(passA)
|
|
85
|
-
// @ts-expect-error
|
|
86
|
-
expect((composerRef.current!.passes[3] as EffectPass).effects).toStrictEqual([effectC])
|
|
87
|
-
expect(composerRef.current!.passes[4]).toBe(passB)
|
|
88
|
-
|
|
89
|
-
// NOTE: instance children ordering is unstable until R3F v9, so we remount from scratch
|
|
90
|
-
await act(async () => root.render(null))
|
|
91
|
-
|
|
92
|
-
// Reverse order
|
|
93
|
-
await act(async () =>
|
|
94
|
-
root.render(
|
|
95
|
-
<EffectComposer ref={composerRef}>
|
|
96
|
-
{/* PassB */}
|
|
97
|
-
<primitive object={passB} />
|
|
98
|
-
{/* EffectPass(effectC) */}
|
|
99
|
-
<primitive object={effectC} />
|
|
100
|
-
{/* PassA */}
|
|
101
|
-
<primitive object={passA} />
|
|
102
|
-
{/* EffectPass(effectB, effectA) */}
|
|
103
|
-
<primitive object={effectB} />
|
|
104
|
-
<primitive object={effectA} />
|
|
105
|
-
</EffectComposer>
|
|
106
|
-
)
|
|
107
|
-
)
|
|
108
|
-
expect(composerRef.current!.passes.map((p) => p.constructor)).toStrictEqual([
|
|
109
|
-
RenderPass,
|
|
110
|
-
Pass,
|
|
111
|
-
EffectPass,
|
|
112
|
-
Pass,
|
|
113
|
-
EffectPass,
|
|
114
|
-
])
|
|
115
|
-
expect(composerRef.current!.passes[1]).toBe(passB)
|
|
116
|
-
// @ts-expect-error
|
|
117
|
-
expect((composerRef.current!.passes[2] as EffectPass).effects).toStrictEqual([effectC])
|
|
118
|
-
expect(composerRef.current!.passes[3]).toBe(passA)
|
|
119
|
-
// @ts-expect-error
|
|
120
|
-
expect((composerRef.current!.passes[4] as EffectPass).effects).toStrictEqual([effectB, effectA])
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
it.skip('should split convolution effects', async () => {
|
|
124
|
-
await act(async () => root.render(null))
|
|
125
|
-
})
|
|
126
|
-
})
|
package/src/effects/N8AO.tsx
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
// From https://github.com/N8python/n8ao
|
|
2
|
-
// https://twitter.com/N8Programs/status/1660996748485984261
|
|
3
|
-
|
|
4
|
-
import { Ref, forwardRef, useLayoutEffect, useMemo } from 'react'
|
|
5
|
-
/* @ts-ignore */
|
|
6
|
-
import { N8AOPostPass } from 'n8ao'
|
|
7
|
-
import { useThree, ReactThreeFiber, applyProps } from '@react-three/fiber'
|
|
8
|
-
|
|
9
|
-
export type N8AOProps = {
|
|
10
|
-
aoRadius?: number
|
|
11
|
-
distanceFalloff?: number
|
|
12
|
-
intensity?: number
|
|
13
|
-
quality?: 'performance' | 'low' | 'medium' | 'high' | 'ultra'
|
|
14
|
-
aoSamples?: number
|
|
15
|
-
denoiseSamples?: number
|
|
16
|
-
denoiseRadius?: number
|
|
17
|
-
color?: ReactThreeFiber.Color
|
|
18
|
-
halfRes?: boolean
|
|
19
|
-
depthAwareUpsampling?: boolean
|
|
20
|
-
screenSpaceRadius?: boolean
|
|
21
|
-
renderMode?: 0 | 1 | 2 | 3 | 4
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export const N8AO = /* @__PURE__ */ forwardRef<N8AOPostPass, N8AOProps>(
|
|
25
|
-
(
|
|
26
|
-
{
|
|
27
|
-
halfRes,
|
|
28
|
-
screenSpaceRadius,
|
|
29
|
-
quality,
|
|
30
|
-
depthAwareUpsampling = true,
|
|
31
|
-
aoRadius = 5,
|
|
32
|
-
aoSamples = 16,
|
|
33
|
-
denoiseSamples = 4,
|
|
34
|
-
denoiseRadius = 12,
|
|
35
|
-
distanceFalloff = 1,
|
|
36
|
-
intensity = 1,
|
|
37
|
-
color,
|
|
38
|
-
renderMode = 0,
|
|
39
|
-
},
|
|
40
|
-
ref: Ref<N8AOPostPass>
|
|
41
|
-
) => {
|
|
42
|
-
const { camera, scene } = useThree()
|
|
43
|
-
const effect = useMemo(() => new N8AOPostPass(scene, camera), [camera, scene])
|
|
44
|
-
|
|
45
|
-
// TODO: implement dispose upstream; this effect has memory leaks without
|
|
46
|
-
useLayoutEffect(() => {
|
|
47
|
-
applyProps(effect.configuration, {
|
|
48
|
-
color,
|
|
49
|
-
aoRadius,
|
|
50
|
-
distanceFalloff,
|
|
51
|
-
intensity,
|
|
52
|
-
aoSamples,
|
|
53
|
-
denoiseSamples,
|
|
54
|
-
denoiseRadius,
|
|
55
|
-
screenSpaceRadius,
|
|
56
|
-
renderMode,
|
|
57
|
-
halfRes,
|
|
58
|
-
depthAwareUpsampling,
|
|
59
|
-
})
|
|
60
|
-
}, [
|
|
61
|
-
screenSpaceRadius,
|
|
62
|
-
color,
|
|
63
|
-
aoRadius,
|
|
64
|
-
distanceFalloff,
|
|
65
|
-
intensity,
|
|
66
|
-
aoSamples,
|
|
67
|
-
denoiseSamples,
|
|
68
|
-
denoiseRadius,
|
|
69
|
-
renderMode,
|
|
70
|
-
halfRes,
|
|
71
|
-
depthAwareUpsampling,
|
|
72
|
-
effect,
|
|
73
|
-
])
|
|
74
|
-
|
|
75
|
-
useLayoutEffect(() => {
|
|
76
|
-
if (quality) effect.setQualityMode(quality.charAt(0).toUpperCase() + quality.slice(1))
|
|
77
|
-
}, [effect, quality])
|
|
78
|
-
|
|
79
|
-
return <primitive ref={ref} object={effect} />
|
|
80
|
-
}
|
|
81
|
-
)
|