@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.
Files changed (114) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +80 -81
  3. package/dist/EffectComposer.d.ts +14 -6
  4. package/dist/EffectGroup.d.ts +9 -0
  5. package/dist/Selection.d.ts +8 -8
  6. package/dist/createEffectComponent.d.ts +30 -0
  7. package/dist/effects/ASCII.d.ts +29 -7
  8. package/dist/effects/Autofocus.d.ts +8 -7
  9. package/dist/effects/Bloom.d.ts +10 -5
  10. package/dist/effects/BrightnessContrast.d.ts +15 -5
  11. package/dist/effects/ChromaticAberration.d.ts +30 -7
  12. package/dist/effects/ColorAverage.d.ts +7 -8
  13. package/dist/effects/ColorDepth.d.ts +8 -5
  14. package/dist/effects/Depth.d.ts +13 -5
  15. package/dist/effects/DepthOfField.d.ts +7 -18
  16. package/dist/effects/DotScreen.d.ts +15 -5
  17. package/dist/effects/FXAA.d.ts +11 -5
  18. package/dist/effects/Glitch.d.ts +17 -29
  19. package/dist/effects/GodRays.d.ts +7 -19
  20. package/dist/effects/Grid.d.ts +8 -8
  21. package/dist/effects/HueSaturation.d.ts +15 -5
  22. package/dist/effects/LUT.d.ts +4 -3
  23. package/dist/effects/LensFlare.d.ts +52 -11
  24. package/dist/effects/Noise.d.ts +8 -5
  25. package/dist/effects/Outline.d.ts +9 -25
  26. package/dist/effects/Pixelation.d.ts +3 -2
  27. package/dist/effects/Ramp.d.ts +58 -10
  28. package/dist/effects/SMAA.d.ts +10 -5
  29. package/dist/effects/SSAO.d.ts +7 -30
  30. package/dist/effects/ScanlineEffect.d.ts +13 -5
  31. package/dist/effects/SelectiveBloom.d.ts +5 -10
  32. package/dist/effects/Sepia.d.ts +13 -5
  33. package/dist/effects/ShockWave.d.ts +12 -5
  34. package/dist/effects/Texture.d.ts +7 -8
  35. package/dist/effects/TiltShift.d.ts +10 -5
  36. package/dist/effects/TiltShift2.d.ts +44 -8
  37. package/dist/effects/ToneMapping.d.ts +9 -7
  38. package/dist/effects/Vignette.d.ts +19 -5
  39. package/dist/effects/Water.d.ts +17 -5
  40. package/dist/index.d.ts +15 -11
  41. package/dist/index.js +1841 -45
  42. package/dist/index.js.map +1 -1
  43. package/dist/passes/DepthPicking.d.ts +11 -0
  44. package/dist/{effects → passes}/N8AO.d.ts +5 -2
  45. package/dist/util.d.ts +19 -17
  46. package/dist/wrapEffect.d.ts +12 -0
  47. package/package.json +74 -67
  48. package/src/EffectComposer.tsx +379 -200
  49. package/src/EffectGroup.tsx +74 -0
  50. package/src/Selection.tsx +86 -46
  51. package/src/createEffectComponent.tsx +71 -0
  52. package/src/effects/ASCII.tsx +183 -135
  53. package/src/effects/Autofocus.tsx +122 -153
  54. package/src/effects/Bloom.tsx +34 -6
  55. package/src/effects/BrightnessContrast.tsx +7 -4
  56. package/src/effects/ChromaticAberration.tsx +22 -5
  57. package/src/effects/ColorAverage.tsx +4 -15
  58. package/src/effects/ColorDepth.tsx +25 -4
  59. package/src/effects/Depth.tsx +6 -4
  60. package/src/effects/DepthOfField.tsx +109 -89
  61. package/src/effects/DotScreen.tsx +7 -4
  62. package/src/effects/FXAA.tsx +6 -4
  63. package/src/effects/Glitch.tsx +32 -40
  64. package/src/effects/GodRays.tsx +104 -16
  65. package/src/effects/Grid.tsx +27 -21
  66. package/src/effects/HueSaturation.tsx +7 -4
  67. package/src/effects/LUT.tsx +25 -26
  68. package/src/effects/LensFlare.tsx +765 -611
  69. package/src/effects/Noise.tsx +16 -4
  70. package/src/effects/Outline.tsx +84 -117
  71. package/src/effects/Pixelation.tsx +20 -15
  72. package/src/effects/Ramp.tsx +239 -150
  73. package/src/effects/SMAA.tsx +20 -4
  74. package/src/effects/SSAO.tsx +152 -41
  75. package/src/effects/ScanlineEffect.tsx +7 -7
  76. package/src/effects/SelectiveBloom.tsx +98 -126
  77. package/src/effects/Sepia.tsx +6 -4
  78. package/src/effects/ShockWave.tsx +32 -4
  79. package/src/effects/Texture.tsx +28 -23
  80. package/src/effects/TiltShift.tsx +32 -4
  81. package/src/effects/TiltShift2.tsx +156 -90
  82. package/src/effects/ToneMapping.tsx +19 -6
  83. package/src/effects/Vignette.tsx +7 -4
  84. package/src/effects/Water.tsx +50 -35
  85. package/src/index.ts +43 -40
  86. package/src/passes/DepthPicking.tsx +63 -0
  87. package/src/passes/N8AO.tsx +105 -0
  88. package/src/tests/Autofocus.test.tsx +38 -0
  89. package/src/tests/Bloom.test.tsx +76 -0
  90. package/src/tests/ChromaticAberration.test.tsx +81 -0
  91. package/src/tests/ColorDepth.test.tsx +71 -0
  92. package/src/tests/DepthOfField.test.tsx +130 -0
  93. package/src/tests/DepthPicking.test.tsx +250 -0
  94. package/src/tests/EffectComposer.test.tsx +1404 -0
  95. package/src/tests/EffectGroup.test.tsx +337 -0
  96. package/src/tests/Glitch.test.tsx +56 -0
  97. package/src/tests/GodRays.test.tsx +101 -0
  98. package/src/tests/Grid.test.tsx +34 -0
  99. package/src/tests/LUT.test.tsx +64 -0
  100. package/src/tests/N8AO.test.tsx +92 -0
  101. package/src/tests/Outline.test.tsx +211 -0
  102. package/src/tests/SSAO.test.tsx +140 -0
  103. package/src/tests/Selection.test.tsx +324 -0
  104. package/src/tests/SelectiveBloom.test.tsx +166 -0
  105. package/src/tests/ShockWave.test.tsx +95 -0
  106. package/src/tests/TiltShift.test.tsx +76 -0
  107. package/src/tests/createEffectComponent.test.tsx +286 -0
  108. package/src/tests/effects.smoke.test.tsx +238 -0
  109. package/src/tests/test-utils.tsx +96 -0
  110. package/src/tests/wrapEffect.test.tsx +209 -0
  111. package/src/util.tsx +221 -55
  112. package/src/wrapEffect.tsx +123 -0
  113. package/src/EffectComposer.test.tsx +0 -126
  114. package/src/effects/N8AO.tsx +0 -81
package/src/util.tsx CHANGED
@@ -1,55 +1,221 @@
1
- import React, { RefObject } from 'react'
2
- import { Vector2 } from 'three'
3
- import * as THREE from 'three'
4
- import { type ReactThreeFiber, type ThreeElement, extend, useThree } from '@react-three/fiber'
5
- import type { Effect, Pass, BlendFunction } from 'postprocessing'
6
-
7
- export const resolveRef = <T,>(ref: T | React.RefObject<T>) =>
8
- typeof ref === 'object' && ref != null && 'current' in ref ? ref.current : ref
9
-
10
- export type EffectConstructor = new (...args: any[]) => Effect | Pass
11
-
12
- export type EffectProps<T extends EffectConstructor> = ThreeElement<T> &
13
- ConstructorParameters<T>[0] & {
14
- blendFunction?: BlendFunction
15
- opacity?: number
16
- }
17
-
18
- let i = 0
19
- const components = new WeakMap<EffectConstructor, React.ExoticComponent<any> | string>()
20
-
21
- export const wrapEffect = <T extends EffectConstructor>(effect: T, defaults?: EffectProps<T>) =>
22
- /* @__PURE__ */ function Effect({ blendFunction = defaults?.blendFunction, opacity = defaults?.opacity, ...props }) {
23
- let Component = components.get(effect)
24
- if (!Component) {
25
- const key = `@react-three/postprocessing/${effect.name}-${i++}`
26
- extend({ [key]: effect })
27
- components.set(effect, (Component = key))
28
- }
29
-
30
- const camera = useThree((state) => state.camera)
31
- const args = React.useMemo(
32
- () => [...(defaults?.args ?? []), ...(props.args ?? [{ ...defaults, ...props }])],
33
- // eslint-disable-next-line react-hooks/exhaustive-deps
34
- [JSON.stringify(props)]
35
- )
36
-
37
- return (
38
- <Component
39
- camera={camera}
40
- blendMode-blendFunction={blendFunction}
41
- blendMode-opacity-value={opacity}
42
- {...props}
43
- args={args}
44
- />
45
- )
46
- }
47
-
48
- export const useVector2 = (props: Record<string, unknown>, key: string): THREE.Vector2 => {
49
- const value = props[key] as ReactThreeFiber.Vector2 | undefined
50
- return React.useMemo(() => {
51
- if (typeof value === 'number') return new THREE.Vector2(value, value)
52
- else if (value) return new THREE.Vector2(...(value as THREE.Vector2Tuple))
53
- else return new THREE.Vector2()
54
- }, [value])
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
- })
@@ -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
- )