Skip to content

Commit 8757d24

Browse files
committed
fix: WIP fix for memory leak
1 parent 29fda1b commit 8757d24

File tree

5 files changed

+197
-69
lines changed

5 files changed

+197
-69
lines changed
+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { Box, Stats, useTexture } from '@react-three/drei'
2+
import type { Meta, StoryObj } from '@storybook/react'
3+
import React, { useEffect, useRef, useState } from 'react'
4+
import * as THREE from 'three'
5+
import { BackSide } from 'three'
6+
7+
import { useFrame, useThree } from '@react-three/fiber'
8+
import { EffectComposer, LensFlare } from '../../src'
9+
import { Setup } from '../Setup'
10+
11+
const meta = {
12+
title: 'MemoryLeak',
13+
component: LensFlare,
14+
decorators: [
15+
(Story) => (
16+
<Setup cameraPosition={new THREE.Vector3(8, 1, 10)} cameraFov={50}>
17+
<Stats showPanel={2} />
18+
{Story()}
19+
</Setup>
20+
),
21+
],
22+
} satisfies Meta<typeof LensFlare>
23+
24+
export default meta
25+
type Story = StoryObj<typeof meta>
26+
27+
export const WithPostprocessing: Story = {
28+
render: (args) => (
29+
<>
30+
<color attach="background" args={['#303035']} />
31+
32+
<CameraSwitcher />
33+
34+
<directionalLight intensity={3} position={[-25, 60, -60]} />
35+
36+
<Box />
37+
38+
<SkyBox />
39+
40+
<EffectComposer multisampling={0}>
41+
<LensFlare {...args} />
42+
</EffectComposer>
43+
</>
44+
),
45+
args: {},
46+
}
47+
48+
export const WithoutPostprocessing: Story = {
49+
render: (args) => (
50+
<>
51+
<color attach="background" args={['#303035']} />
52+
53+
<CameraSwitcher />
54+
55+
<directionalLight intensity={3} position={[-25, 60, -60]} />
56+
57+
<Box />
58+
59+
<SkyBox />
60+
</>
61+
),
62+
args: {},
63+
}
64+
65+
function CameraSwitcher() {
66+
const { camera, set } = useThree()
67+
const camRef = useRef(new THREE.OrthographicCamera())
68+
const camDef = useRef(camera)
69+
70+
const keySPressedCount = useKeyPressedCount('c')
71+
72+
const switchCamera = () => {
73+
const newcam = camera === camDef.current ? camRef.current : camDef.current
74+
set(() => ({ camera: newcam }))
75+
76+
// log memory usage
77+
// if ('memory' in performance) {
78+
// console.log(((performance as any).memory.usedJSHeapSize / 1024 / 1024).toFixed(0))
79+
// }
80+
}
81+
82+
useFrame(() => {
83+
if (keySPressedCount % 2 === 1) {
84+
switchCamera()
85+
}
86+
})
87+
88+
return null
89+
}
90+
91+
function useKeyPressedCount(key: string) {
92+
const [count, setCount] = useState(0)
93+
94+
useEffect(() => {
95+
const handler = (e: KeyboardEvent) => {
96+
if (e.key === key) {
97+
setCount((prev) => prev + 1)
98+
}
99+
}
100+
101+
window.addEventListener('keydown', handler)
102+
103+
return () => window.removeEventListener('keydown', handler)
104+
}, [])
105+
106+
return count
107+
}
108+
109+
function SkyBox() {
110+
const texture = useTexture('digital_painting_golden_hour_sunset.jpg')
111+
112+
return (
113+
<mesh userData={{ lensflare: 'no-occlusion' }} scale={[-1, 1, 1]} castShadow={false} receiveShadow={false}>
114+
<sphereGeometry args={[50, 32, 32]} />
115+
<meshBasicMaterial toneMapped={false} map={texture} side={BackSide} />
116+
</mesh>
117+
)
118+
}

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"buffer": "^6.0.3",
5050
"maath": "^0.6.0",
5151
"n8ao": "^1.6.6",
52-
"postprocessing": "^6.32.1",
52+
"postprocessing": "^6.35.2",
5353
"three-stdlib": "^2.23.4"
5454
},
5555
"devDependencies": {

src/EffectComposer.tsx

+70-60
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
import type { TextureDataType } from 'three'
22
import { HalfFloatType, NoToneMapping } from 'three'
3-
import React, {
4-
forwardRef,
5-
useMemo,
6-
useEffect,
7-
useLayoutEffect,
8-
createContext,
9-
useRef,
10-
useImperativeHandle,
11-
} from 'react'
3+
import React, { forwardRef, useMemo, useEffect, createContext, useRef, useImperativeHandle } from 'react'
124
import { useThree, useFrame, useInstanceHandle } from '@react-three/fiber'
135
import {
146
EffectComposer as EffectComposerImpl,
@@ -32,7 +24,7 @@ export const EffectComposerContext = createContext<{
3224
resolutionScale?: number
3325
}>(null!)
3426

35-
export type EffectComposerProps = {
27+
export type EffectComposerProps = {
3628
enabled?: boolean
3729
children: JSX.Element | JSX.Element[]
3830
depthBuffer?: boolean
@@ -74,66 +66,45 @@ export const EffectComposer = React.memo(
7466
const scene = _scene || defaultScene
7567
const camera = _camera || defaultCamera
7668

77-
const [composer, normalPass, downSamplingPass] = useMemo(() => {
69+
const composer = useRef<EffectComposerImpl | undefined>()
70+
const normalPass = useRef<NormalPass | undefined>()
71+
const downSamplingPass = useRef<DepthDownsamplingPass | undefined>()
72+
73+
const group = useRef(null)
74+
const instance = useInstanceHandle(group)
75+
76+
useEffect(() => {
7877
const webGL2Available = isWebGL2Available()
78+
7979
// Initialize composer
80-
const effectComposer = new EffectComposerImpl(gl, {
80+
composer.current = new EffectComposerImpl(gl, {
8181
depthBuffer,
8282
stencilBuffer,
8383
multisampling: multisampling > 0 && webGL2Available ? multisampling : 0,
8484
frameBufferType,
8585
})
8686

8787
// Add render pass
88-
effectComposer.addPass(new RenderPass(scene, camera))
88+
composer.current.addPass(new RenderPass(scene, camera))
8989

9090
// Create normal pass
91-
let downSamplingPass = null
92-
let normalPass = null
9391
if (enableNormalPass) {
94-
normalPass = new NormalPass(scene, camera)
95-
normalPass.enabled = false
96-
effectComposer.addPass(normalPass)
92+
normalPass.current = new NormalPass(scene, camera)
93+
normalPass.current.enabled = false
94+
composer.current.addPass(normalPass.current)
9795
if (resolutionScale !== undefined && webGL2Available) {
98-
downSamplingPass = new DepthDownsamplingPass({ normalBuffer: normalPass.texture, resolutionScale })
99-
downSamplingPass.enabled = false
100-
effectComposer.addPass(downSamplingPass)
96+
downSamplingPass.current = new DepthDownsamplingPass({
97+
normalBuffer: normalPass.current.texture,
98+
resolutionScale,
99+
})
100+
downSamplingPass.current.enabled = false
101+
composer.current.addPass(downSamplingPass.current)
101102
}
102103
}
103104

104-
return [effectComposer, normalPass, downSamplingPass]
105-
}, [
106-
camera,
107-
gl,
108-
depthBuffer,
109-
stencilBuffer,
110-
multisampling,
111-
frameBufferType,
112-
scene,
113-
enableNormalPass,
114-
resolutionScale,
115-
])
116-
117-
useEffect(() => composer?.setSize(size.width, size.height), [composer, size])
118-
useFrame(
119-
(_, delta) => {
120-
if (enabled) {
121-
const currentAutoClear = gl.autoClear
122-
gl.autoClear = autoClear
123-
if (stencilBuffer && !autoClear) gl.clearStencil()
124-
composer.render(delta)
125-
gl.autoClear = currentAutoClear
126-
}
127-
},
128-
enabled ? renderPriority : 0
129-
)
130-
131-
const group = useRef(null)
132-
const instance = useInstanceHandle(group)
133-
useLayoutEffect(() => {
134105
const passes: Pass[] = []
135106

136-
if (group.current && instance.current && composer) {
107+
if (group.current && instance.current && composer.current) {
137108
const children = instance.current.objects as unknown[]
138109

139110
for (let i = 0; i < children.length; i++) {
@@ -158,18 +129,50 @@ export const EffectComposer = React.memo(
158129
}
159130
}
160131

161-
for (const pass of passes) composer?.addPass(pass)
132+
for (const pass of passes) composer.current?.addPass(pass)
162133

163-
if (normalPass) normalPass.enabled = true
164-
if (downSamplingPass) downSamplingPass.enabled = true
134+
if (normalPass.current) normalPass.current.enabled = true
135+
if (downSamplingPass.current) downSamplingPass.current.enabled = true
165136
}
166137

167138
return () => {
168-
for (const pass of passes) composer?.removePass(pass)
169-
if (normalPass) normalPass.enabled = false
170-
if (downSamplingPass) downSamplingPass.enabled = false
139+
for (const pass of passes) composer.current?.removePass(pass)
140+
if (normalPass.current) normalPass.current.enabled = false
141+
if (downSamplingPass.current) downSamplingPass.current.enabled = false
142+
normalPass.current?.dispose()
143+
downSamplingPass.current?.dispose()
144+
composer.current?.dispose()
145+
146+
composer.current = undefined
147+
normalPass.current = undefined
148+
downSamplingPass.current = undefined
171149
}
172-
}, [composer, children, camera, normalPass, downSamplingPass, instance])
150+
}, [
151+
camera,
152+
gl,
153+
depthBuffer,
154+
stencilBuffer,
155+
multisampling,
156+
frameBufferType,
157+
scene,
158+
enableNormalPass,
159+
resolutionScale,
160+
instance,
161+
])
162+
163+
useEffect(() => composer.current?.setSize(size.width, size.height), [size])
164+
useFrame(
165+
(_, delta) => {
166+
if (enabled) {
167+
const currentAutoClear = gl.autoClear
168+
gl.autoClear = autoClear
169+
if (stencilBuffer && !autoClear) gl.clearStencil()
170+
composer.current?.render(delta)
171+
gl.autoClear = currentAutoClear
172+
}
173+
},
174+
enabled ? renderPriority : 0
175+
)
173176

174177
// Disable tone mapping because threejs disallows tonemapping on render targets
175178
useEffect(() => {
@@ -182,7 +185,14 @@ export const EffectComposer = React.memo(
182185

183186
// Memoize state, otherwise it would trigger all consumers on every render
184187
const state = useMemo(
185-
() => ({ composer, normalPass, downSamplingPass, resolutionScale, camera, scene }),
188+
() => ({
189+
composer: composer.current!,
190+
normalPass: normalPass.current!,
191+
downSamplingPass: downSamplingPass.current!,
192+
resolutionScale,
193+
camera,
194+
scene,
195+
}),
186196
[composer, normalPass, downSamplingPass, resolutionScale, camera, scene]
187197
)
188198

src/effects/LensFlare.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,13 @@ const LensFlareShader = {
4747
float rShp(vec2 p, int N){float f;float a=atan(p.x,p.y)+.2;float b=6.28319/float(N);f=smoothstep(.5,.51, cos(floor(.5+a/b)*b-a)*length(p.xy)* 2.0 -ghostScale);return f;}
4848
vec3 drC(vec2 p, float zsi, float dCy, vec3 clr, vec3 clr2, float ams2, vec2 esom){float l = length(p + esom*(ams2*2.))+zsi/2.;float l2 = length(p + esom*(ams2*4.))+zsi/3.;float c = max(0.01-pow(length(p + esom*ams2), zsi*ghostScale), 0.0)*10.;float c1 = max(0.001-pow(l-0.3, 1./40.)+sin(l*20.), 0.0)*3.;float c2 = max(0.09/pow(length(p-esom*ams2/.5)*1., .95), 0.0)/20.;float s = max(0.02-pow(rShp(p*5. + esom*ams2*5. + dCy, 6) , 1.), 0.0)*1.5;clr = cos(vec3(0.44, .24, .2)*8. + ams2*4.)*0.5+.5;vec3 f = c*clr;f += c1*clr;f += c2*clr;f += s*clr;return f-0.01;}
4949
vec4 geLC(float x){return vec4(vec3(mix(mix(mix(mix(mix(mix(mix(mix(mix(mix(mix(mix(mix(mix(mix(vec3(0., 0., 0.),vec3(0., 0., 0.), smoothstep(0.0, 0.063, x)),vec3(0., 0., 0.), smoothstep(0.063, 0.125, x)),vec3(0.0, 0., 0.), smoothstep(0.125, 0.188, x)),vec3(0.188, 0.131, 0.116), smoothstep(0.188, 0.227, x)),vec3(0.31, 0.204, 0.537), smoothstep(0.227, 0.251, x)),vec3(0.192, 0.106, 0.286), smoothstep(0.251, 0.314, x)),vec3(0.102, 0.008, 0.341), smoothstep(0.314, 0.392, x)),vec3(0.086, 0.0, 0.141), smoothstep(0.392, 0.502, x)),vec3(1.0, 0.31, 0.0), smoothstep(0.502, 0.604, x)),vec3(.1, 0.1, 0.1), smoothstep(0.604, 0.643, x)),vec3(1.0, 0.929, 0.0), smoothstep(0.643, 0.761, x)),vec3(1.0, 0.086, 0.424), smoothstep(0.761, 0.847, x)),vec3(1.0, 0.49, 0.0), smoothstep(0.847, 0.89, x)),vec3(0.945, 0.275, 0.475), smoothstep(0.89, 0.941, x)),vec3(0.251, 0.275, 0.796), smoothstep(0.941, 1.0, x))),1.0);}
50-
float diTN(vec2 p){vec2 f = fract(p);f = (f * f) * (3.0 - (2.0 * f));float n = dot(floor(p), vec2(1.0, 157.0));vec4 a = fract(sin(vec4(n + 0.0, n + 1.0, n + 157.0, n + 158.0)) * 43758.5453123);return mix(mix(a.x, a.y, f.x), mix(a.z, a.w, f.x), f.y);}
51-
float fbm(vec2 p){const mat2 m = mat2(0.80, -0.60, 0.60, 0.80);float f = 0.0;f += 0.5000*diTN(p); p = m*p*2.02;f += 0.2500*diTN(p); p = m*p*2.03;f += 0.1250*diTN(p); p = m*p*2.01;f += 0.0625*diTN(p);return f/0.9375;}
50+
float diTN(vec2 p){vec2 f = fract(p);f = (f * f) * (3.0 - (2.0 * f));float n = dot(floor(p), vec2(1.0, 157.0));vec4 a = fract(sin(vec4(n + 0.0, n + 1.0, n + 157.0, n + 158.0)) * 43758.5453123);return mix(mix(a.x, a.y, f.x), mix(a.z, a.w, f.x), f.y);}
51+
float fbm(vec2 p){const mat2 m = mat2(0.80, -0.60, 0.60, 0.80);float f = 0.0;f += 0.5000*diTN(p); p = m*p*2.02;f += 0.2500*diTN(p); p = m*p*2.03;f += 0.1250*diTN(p); p = m*p*2.01;f += 0.0625*diTN(p);return f/0.9375;}
5252
vec4 geLS(vec2 p){vec2 pp = (p - vec2(0.5)) * 2.0;float a = atan(pp.y, pp.x);vec4 cp = vec4(sin(a * 1.0), length(pp), sin(a * 13.0), sin(a * 53.0));float d = sin(clamp(pow(length(vec2(0.5) - p) * 0.5 + haloScale /2., 5.0), 0.0, 1.0) * 3.14159);vec3 c = vec3(d) * vec3(fbm(cp.xy * 16.0) * fbm(cp.zw * 9.0) * max(max(max(max(0.5, sin(a * 1.0)), sin(a * 3.0) * 0.8), sin(a * 7.0) * 0.8), sin(a * 9.0) * 10.6));c *= vec3(mix(2.0, (sin(length(pp.xy) * 256.0) * 0.5) + 0.5, sin((clamp((length(pp.xy) - 0.875) / 0.1, 0.0, 1.0) + 0.0) * 2.0 * 3.14159) * 1.5) + 0.5) * 0.3275;return vec4(vec3(c * 1.0), d);}
5353
vec4 geLD(vec2 p){p.xy += vec2(fbm(p.yx * 3.0), fbm(p.yx * 2.0)) * 0.0825;vec3 o = vec3(mix(0.125, 0.25, max(max(smoothstep(0.1, 0.0, length(p - vec2(0.25))),smoothstep(0.4, 0.0, length(p - vec2(0.75)))),smoothstep(0.8, 0.0, length(p - vec2(0.875, 0.125))))));o += vec3(max(fbm(p * 1.0) - 0.5, 0.0)) * 0.5;o += vec3(max(fbm(p * 2.0) - 0.5, 0.0)) * 0.5;o += vec3(max(fbm(p * 4.0) - 0.5, 0.0)) * 0.25;o += vec3(max(fbm(p * 8.0) - 0.75, 0.0)) * 1.0;o += vec3(max(fbm(p * 16.0) - 0.75, 0.0)) * 0.75;o += vec3(max(fbm(p * 64.0) - 0.75, 0.0)) * 0.5;return vec4(clamp(o, vec3(0.15), vec3(1.0)), 1.0);}
5454
vec4 txL(sampler2D tex, vec2 xtC){if(((xtC.x < 0.) || (xtC.y < 0.)) || ((xtC.x > 1.) || (xtC.y > 1.))){return vec4(0.0);}else{return texture(tex, xtC); }}
5555
vec4 txD(sampler2D tex, vec2 xtC, vec2 dir, vec3 ditn) {return vec4(txL(tex, (xtC + (dir * ditn.r))).r,txL(tex, (xtC + (dir * ditn.g))).g,txL(tex, (xtC + (dir * ditn.b))).b,1.0);}
56-
vec4 strB(){vec2 aspXtc = vec2(1.0) - (((vxtC - vec2(0.5)) * vec2(1.0)) + vec2(0.5)); vec2 xtC = vec2(1.0) - vxtC; vec2 ghvc = (vec2(0.5) - xtC) * 0.3 - lensPosition; vec2 ghNm = normalize(ghvc * vec2(1.0)) * vec2(1.0);vec2 haloVec = normalize(ghvc) * 0.6;vec2 hlNm = ghNm * 0.6;vec2 texelSize = vec2(1.0) / vec2(iResolution.xy);vec3 ditn = vec3(-(texelSize.x * 1.5), 0.2, texelSize.x * 1.5);vec4 c = vec4(0.0);for (int i = 0; i < 8; i++) {vec2 offset = xtC + (ghvc * float(i));c += txD(lensDirtTexture, offset, ghNm, ditn) * pow(max(0.0, 1.0 - (length(vec2(0.5) - offset) / length(vec2(0.5)))), 10.0);}vec2 uyTrz = xtC + hlNm; return (c * geLC((length(vec2(0.5) - aspXtc) / length(vec2(haloScale))))) +(txD(lensDirtTexture, uyTrz, ghNm, ditn) * pow(max(0.0, 1.0 - (length(vec2(0.5) - uyTrz) / length(vec2(0.5)))), 10.0));}
56+
vec4 strB(){vec2 aspXtc = vec2(1.0) - (((vxtC - vec2(0.5)) * vec2(1.0)) + vec2(0.5)); vec2 xtC = vec2(1.0) - vxtC; vec2 ghvc = (vec2(0.5) - xtC) * 0.3 - lensPosition; vec2 ghNm = normalize(ghvc * vec2(1.0)) * vec2(1.0);vec2 haloVec = normalize(ghvc) * 0.6;vec2 hlNm = ghNm * 0.6;vec2 texelSize = vec2(1.0) / vec2(iResolution.xy);vec3 ditn = vec3(-(texelSize.x * 1.5), 0.2, texelSize.x * 1.5);vec4 c = vec4(0.0);for (int i = 0; i < 8; i++) {vec2 offset = xtC + (ghvc * float(i));c += txD(lensDirtTexture, offset, ghNm, ditn) * pow(max(0.0, 1.0 - (length(vec2(0.5) - offset) / length(vec2(0.5)))), 10.0);}vec2 uyTrz = xtC + hlNm; return (c * geLC((length(vec2(0.5) - aspXtc) / length(vec2(haloScale))))) +(txD(lensDirtTexture, uyTrz, ghNm, ditn) * pow(max(0.0, 1.0 - (length(vec2(0.5) - uyTrz) / length(vec2(0.5)))), 10.0));}
5757
void mainImage(vec4 v,vec2 r,out vec4 i){vec2 g=r-.5;g.y*=iResolution.y/iResolution.x;vec2 l=lensPosition*.5;l.y*=iResolution.y/iResolution.x;vec3 f=mLs(g,l)*20.*colorGain/256.;if(aditionalStreaks){vec3 o=vec3(.9,.2,.1),p=vec3(.3,.1,.9);for(float n=0.;n<10.;n++)f+=drC(g,pow(rnd(n*2e3)*2.8,.1)+1.41,0.,o+n,p+n,rnd(n*20.)*3.+.2-.5,lensPosition);}if(secondaryGhosts){vec3 n=vec3(0);n+=rHx(g,-lensPosition*.25,ghostScale*1.4,vec3(.25,.35,0));n+=rHx(g,lensPosition*.25,ghostScale*.5,vec3(1,.5,.5));n+=rHx(g,lensPosition*.1,ghostScale*1.6,vec3(1));n+=rHx(g,lensPosition*1.8,ghostScale*2.,vec3(0,.5,.75));n+=rHx(g,lensPosition*1.25,ghostScale*.8,vec3(1,1,.5));n+=rHx(g,-lensPosition*1.25,ghostScale*5.,vec3(.5,.5,.25));n+=fpow(1.-abs(distance(lensPosition*.8,g)-.7),.985)*colorGain/2100.;f+=n;}if(starBurst){vxtC=g+.5;vec4 n=geLD(g);float o=1.-clamp(0.5,0.,.5)*2.;n+=mix(n,pow(n*2.,vec4(2))*.5,o);float s=(g.x+g.y)*(1./6.);vec2 d=mat2(cos(s),-sin(s),sin(s),cos(s))*vxtC;n+=geLS(d)*2.;f+=clamp(n.xyz*strB().xyz,.01,1.);}i=enabled?vec4(mix(f,vec3(0),opacity)+v.xyz,v.w):vec4(v);}
5858
`,
5959
}
@@ -183,6 +183,6 @@ export const LensFlare = forwardRef<LensFlareEffect, LensFlareProps>(
183183
}
184184
}, [effect, viewport])
185185

186-
return <primitive ref={ref} object={effect} dispose={null} />
186+
return <primitive ref={ref} object={effect} />
187187
}
188188
)

yarn.lock

+4-4
Original file line numberDiff line numberDiff line change
@@ -8394,10 +8394,10 @@ postcss@^8.4.23:
83948394
picocolors "^1.0.0"
83958395
source-map-js "^1.0.2"
83968396

8397-
postprocessing@^6.32.1:
8398-
version "6.32.1"
8399-
resolved "https://registry.yarnpkg.com/postprocessing/-/postprocessing-6.32.1.tgz#a91fa4101246620e12113cded7028d9e4b504845"
8400-
integrity sha512-GiUv5vN/QCWnPJ3DdYPYn/4V1amps94T/0jFPSUL40KfaLCkfE9yPudzTtJJQjs168QNpwkmnvYF9RcP3HiAWA==
8397+
postprocessing@^6.35.2:
8398+
version "6.35.2"
8399+
resolved "https://registry.yarnpkg.com/postprocessing/-/postprocessing-6.35.2.tgz#7a7b42f7d3cc21cd2fded2505af645bf62276716"
8400+
integrity sha512-yGmidrVzA1dSEmExYGgWOGcRvyOVahvurNo9iuzOonRCY6f1hnJe6/HMVSnKV9ppjLtCTqzZOI9iz8CACkmijw==
84018401

84028402
potpack@^1.0.1:
84038403
version "1.0.2"

0 commit comments

Comments
 (0)