diff --git a/.storybook/stories/MemoryLeak.stories.tsx b/.storybook/stories/MemoryLeak.stories.tsx
new file mode 100644
index 0000000..341aebc
--- /dev/null
+++ b/.storybook/stories/MemoryLeak.stories.tsx
@@ -0,0 +1,118 @@
+import { Box, Stats, useTexture } from '@react-three/drei'
+import type { Meta, StoryObj } from '@storybook/react'
+import React, { useEffect, useRef, useState } from 'react'
+import * as THREE from 'three'
+import { BackSide } from 'three'
+
+import { useFrame, useThree } from '@react-three/fiber'
+import { EffectComposer, LensFlare } from '../../src'
+import { Setup } from '../Setup'
+
+const meta = {
+ title: 'MemoryLeak',
+ component: LensFlare,
+ decorators: [
+ (Story) => (
+
+
+ {Story()}
+
+ ),
+ ],
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const WithPostprocessing: Story = {
+ render: (args) => (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ ),
+ args: {},
+}
+
+export const WithoutPostprocessing: Story = {
+ render: (args) => (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ ),
+ args: {},
+}
+
+function CameraSwitcher() {
+ const { camera, set } = useThree()
+ const camRef = useRef(new THREE.OrthographicCamera())
+ const camDef = useRef(camera)
+
+ const keySPressedCount = useKeyPressedCount('c')
+
+ const switchCamera = () => {
+ const newcam = camera === camDef.current ? camRef.current : camDef.current
+ set(() => ({ camera: newcam }))
+
+ // log memory usage
+ // if ('memory' in performance) {
+ // console.log(((performance as any).memory.usedJSHeapSize / 1024 / 1024).toFixed(0))
+ // }
+ }
+
+ useFrame(() => {
+ if (keySPressedCount % 2 === 1) {
+ switchCamera()
+ }
+ })
+
+ return null
+}
+
+function useKeyPressedCount(key: string) {
+ const [count, setCount] = useState(0)
+
+ useEffect(() => {
+ const handler = (e: KeyboardEvent) => {
+ if (e.key === key) {
+ setCount((prev) => prev + 1)
+ }
+ }
+
+ window.addEventListener('keydown', handler)
+
+ return () => window.removeEventListener('keydown', handler)
+ }, [])
+
+ return count
+}
+
+function SkyBox() {
+ const texture = useTexture('digital_painting_golden_hour_sunset.jpg')
+
+ return (
+
+
+
+
+ )
+}
diff --git a/package.json b/package.json
index 1fc7f28..9e8c28c 100644
--- a/package.json
+++ b/package.json
@@ -49,7 +49,7 @@
"buffer": "^6.0.3",
"maath": "^0.6.0",
"n8ao": "^1.6.6",
- "postprocessing": "^6.32.1",
+ "postprocessing": "^6.35.2",
"three-stdlib": "^2.23.4"
},
"devDependencies": {
diff --git a/src/EffectComposer.tsx b/src/EffectComposer.tsx
index cc3db74..2cba122 100644
--- a/src/EffectComposer.tsx
+++ b/src/EffectComposer.tsx
@@ -1,14 +1,6 @@
import type { TextureDataType } from 'three'
import { HalfFloatType, NoToneMapping } from 'three'
-import React, {
- forwardRef,
- useMemo,
- useEffect,
- useLayoutEffect,
- createContext,
- useRef,
- useImperativeHandle,
-} from 'react'
+import React, { forwardRef, useMemo, useEffect, createContext, useRef, useImperativeHandle } from 'react'
import { useThree, useFrame, useInstanceHandle } from '@react-three/fiber'
import {
EffectComposer as EffectComposerImpl,
@@ -32,7 +24,7 @@ export const EffectComposerContext = createContext<{
resolutionScale?: number
}>(null!)
-export type EffectComposerProps = {
+export type EffectComposerProps = {
enabled?: boolean
children: JSX.Element | JSX.Element[]
depthBuffer?: boolean
@@ -74,10 +66,18 @@ export const EffectComposer = React.memo(
const scene = _scene || defaultScene
const camera = _camera || defaultCamera
- const [composer, normalPass, downSamplingPass] = useMemo(() => {
+ const composer = useRef()
+ const normalPass = useRef()
+ const downSamplingPass = useRef()
+
+ const group = useRef(null)
+ const instance = useInstanceHandle(group)
+
+ useEffect(() => {
const webGL2Available = isWebGL2Available()
+
// Initialize composer
- const effectComposer = new EffectComposerImpl(gl, {
+ composer.current = new EffectComposerImpl(gl, {
depthBuffer,
stencilBuffer,
multisampling: multisampling > 0 && webGL2Available ? multisampling : 0,
@@ -85,55 +85,26 @@ export const EffectComposer = React.memo(
})
// Add render pass
- effectComposer.addPass(new RenderPass(scene, camera))
+ composer.current.addPass(new RenderPass(scene, camera))
// Create normal pass
- let downSamplingPass = null
- let normalPass = null
if (enableNormalPass) {
- normalPass = new NormalPass(scene, camera)
- normalPass.enabled = false
- effectComposer.addPass(normalPass)
+ normalPass.current = new NormalPass(scene, camera)
+ normalPass.current.enabled = false
+ composer.current.addPass(normalPass.current)
if (resolutionScale !== undefined && webGL2Available) {
- downSamplingPass = new DepthDownsamplingPass({ normalBuffer: normalPass.texture, resolutionScale })
- downSamplingPass.enabled = false
- effectComposer.addPass(downSamplingPass)
+ downSamplingPass.current = new DepthDownsamplingPass({
+ normalBuffer: normalPass.current.texture,
+ resolutionScale,
+ })
+ downSamplingPass.current.enabled = false
+ composer.current.addPass(downSamplingPass.current)
}
}
- return [effectComposer, normalPass, downSamplingPass]
- }, [
- camera,
- gl,
- depthBuffer,
- stencilBuffer,
- multisampling,
- frameBufferType,
- scene,
- enableNormalPass,
- resolutionScale,
- ])
-
- useEffect(() => composer?.setSize(size.width, size.height), [composer, size])
- useFrame(
- (_, delta) => {
- if (enabled) {
- const currentAutoClear = gl.autoClear
- gl.autoClear = autoClear
- if (stencilBuffer && !autoClear) gl.clearStencil()
- composer.render(delta)
- gl.autoClear = currentAutoClear
- }
- },
- enabled ? renderPriority : 0
- )
-
- const group = useRef(null)
- const instance = useInstanceHandle(group)
- useLayoutEffect(() => {
const passes: Pass[] = []
- if (group.current && instance.current && composer) {
+ if (group.current && instance.current && composer.current) {
const children = instance.current.objects as unknown[]
for (let i = 0; i < children.length; i++) {
@@ -158,18 +129,50 @@ export const EffectComposer = React.memo(
}
}
- for (const pass of passes) composer?.addPass(pass)
+ for (const pass of passes) composer.current?.addPass(pass)
- if (normalPass) normalPass.enabled = true
- if (downSamplingPass) downSamplingPass.enabled = true
+ if (normalPass.current) normalPass.current.enabled = true
+ if (downSamplingPass.current) downSamplingPass.current.enabled = true
}
return () => {
- for (const pass of passes) composer?.removePass(pass)
- if (normalPass) normalPass.enabled = false
- if (downSamplingPass) downSamplingPass.enabled = false
+ for (const pass of passes) composer.current?.removePass(pass)
+ if (normalPass.current) normalPass.current.enabled = false
+ if (downSamplingPass.current) downSamplingPass.current.enabled = false
+ normalPass.current?.dispose()
+ downSamplingPass.current?.dispose()
+ composer.current?.dispose()
+
+ composer.current = undefined
+ normalPass.current = undefined
+ downSamplingPass.current = undefined
}
- }, [composer, children, camera, normalPass, downSamplingPass, instance])
+ }, [
+ camera,
+ gl,
+ depthBuffer,
+ stencilBuffer,
+ multisampling,
+ frameBufferType,
+ scene,
+ enableNormalPass,
+ resolutionScale,
+ instance,
+ ])
+
+ useEffect(() => composer.current?.setSize(size.width, size.height), [size])
+ useFrame(
+ (_, delta) => {
+ if (enabled) {
+ const currentAutoClear = gl.autoClear
+ gl.autoClear = autoClear
+ if (stencilBuffer && !autoClear) gl.clearStencil()
+ composer.current?.render(delta)
+ gl.autoClear = currentAutoClear
+ }
+ },
+ enabled ? renderPriority : 0
+ )
// Disable tone mapping because threejs disallows tonemapping on render targets
useEffect(() => {
@@ -182,7 +185,14 @@ export const EffectComposer = React.memo(
// Memoize state, otherwise it would trigger all consumers on every render
const state = useMemo(
- () => ({ composer, normalPass, downSamplingPass, resolutionScale, camera, scene }),
+ () => ({
+ composer: composer.current!,
+ normalPass: normalPass.current!,
+ downSamplingPass: downSamplingPass.current!,
+ resolutionScale,
+ camera,
+ scene,
+ }),
[composer, normalPass, downSamplingPass, resolutionScale, camera, scene]
)
diff --git a/src/effects/LensFlare.tsx b/src/effects/LensFlare.tsx
index ee5bac6..62c9036 100644
--- a/src/effects/LensFlare.tsx
+++ b/src/effects/LensFlare.tsx
@@ -47,13 +47,13 @@ const LensFlareShader = {
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;}
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;}
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);}
- 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);}
- 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;}
+ 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);}
+ 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;}
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);}
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);}
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); }}
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);}
- 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));}
+ 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));}
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);}
`,
}
@@ -183,6 +183,6 @@ export const LensFlare = forwardRef(
}
}, [effect, viewport])
- return
+ return
}
)
diff --git a/yarn.lock b/yarn.lock
index de8cf38..95d6497 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8394,10 +8394,10 @@ postcss@^8.4.23:
picocolors "^1.0.0"
source-map-js "^1.0.2"
-postprocessing@^6.32.1:
- version "6.32.1"
- resolved "https://registry.yarnpkg.com/postprocessing/-/postprocessing-6.32.1.tgz#a91fa4101246620e12113cded7028d9e4b504845"
- integrity sha512-GiUv5vN/QCWnPJ3DdYPYn/4V1amps94T/0jFPSUL40KfaLCkfE9yPudzTtJJQjs168QNpwkmnvYF9RcP3HiAWA==
+postprocessing@^6.35.2:
+ version "6.35.2"
+ resolved "https://registry.yarnpkg.com/postprocessing/-/postprocessing-6.35.2.tgz#7a7b42f7d3cc21cd2fded2505af645bf62276716"
+ integrity sha512-yGmidrVzA1dSEmExYGgWOGcRvyOVahvurNo9iuzOonRCY6f1hnJe6/HMVSnKV9ppjLtCTqzZOI9iz8CACkmijw==
potpack@^1.0.1:
version "1.0.2"