pmndrs / postprocessing

A post processing library for three.js.
zlib License
2.32k stars 208 forks source link

Effect and EffectPass are not completely cleaned up after calling the dispose method #648

Closed Steve245270533 closed 3 weeks ago

Steve245270533 commented 1 month ago

Description of the bug

When destroying the entire three context, two methods removeToneMappingPass, removeSceneFadePass are called in the destroy method of the PostProcessingEffect class, both methods call the Effect and EffectPass's dispose method, but by printing renderer.info to see that there are still uncleaned programs

To reproduce

export class PostProcessingEffect implements IPostProcessingEffect {
  private composer: EffectComposer
  private renderer: ICore["renderer"]
  private camera: ICore["camera"]
  private scene: ICore["scene"]
  private sizes: ISizes

  constructor(core: ICore, sizes: ISizes) {
    this.renderer = core.renderer
    this.camera = core.camera
    this.scene = core.scene
    this.sizes = sizes

    this.composer = new EffectComposer(this.renderer)
    this.composer.setSize(this.sizes.width, this.sizes.height)
    this.setupRenderPass()
    this.setupToneMappingEffectPass(core.config.tone_mapping)
    this.setupSceneFadePass()
  }

  private render_pass!: RenderPass
  private setupRenderPass() {
    this.render_pass = new RenderPass(this.scene, this.camera)
    this.composer.addPass(this.render_pass)
  }

  private removeRenderPass() {
    this.removePass(this.render_pass)
    this.render_pass.dispose()
  }

  addPass(pass: Pass, index?: number) {
    this.composer.addPass(pass, index)
    this.ensureToneMappingLast()
  }

  removePass(pass: Pass) {
    this.composer.removePass(pass)
  }

  /**
   * ToneMappingEffect
   */
  private tone_mapping_effect_pass!: EffectPass
  private tone_mapping_effect!: ToneMappingEffect

  private setupToneMappingEffectPass(tone_mapping_mode: ToneMapping) {
    this.tone_mapping_effect = new ToneMappingEffect({ mode: tone_mapping_mode })
    this.tone_mapping_effect_pass = new EffectPass(this.camera, this.tone_mapping_effect)
  }

  private ensureToneMappingLast() {
    const tone_mapping_effect_pass_index = this.composer.passes.findIndex(pass => (pass as EffectPass)?.effects?.some(e => e instanceof ToneMappingEffect))
    if (tone_mapping_effect_pass_index === -1) {
      this.composer.addPass(this.tone_mapping_effect_pass)
    }
    else if (tone_mapping_effect_pass_index !== this.composer.passes.length - 1) {
      this.composer.removePass(this.tone_mapping_effect_pass)
      this.composer.addPass(this.tone_mapping_effect_pass)
    }
  }

  private removeToneMappingPass() {
    this.removePass(this.tone_mapping_effect_pass)
    this.tone_mapping_effect.dispose()
    this.tone_mapping_effect_pass.dispose()
  }

  /**
   * FadeEffect
   */
  private scene_fade_effect!: IFadeEffect
  private scene_fade_pass!: EffectPass
  private scene_fade_animation: ReturnType<typeof gsap.to> | undefined
  private setupSceneFadePass() {
    this.scene_fade_effect = new FadeEffect()
    this.scene_fade_pass = new EffectPass(this.camera, this.scene_fade_effect)
    this.addPass(this.scene_fade_pass)
  }

  setSceneFadeIn(duration: number = 2): Promise<void> {
    return new Promise((resolve) => {
      this.scene_fade_animation?.kill()
      this.scene_fade_animation = gsap.to(this.scene_fade_effect, {
        opacity: 0,
        duration,
        ease: "linear",
        onComplete: () => {
          resolve()
        },
      })
    })
  }

  setSceneFadeOut(duration: number = 2): Promise<void> {
    return new Promise((resolve) => {
      this.scene_fade_animation = gsap.to(this.scene_fade_effect, {
        opacity: 1,
        duration,
        ease: "linear",
        onComplete: () => {
          resolve()
        },
      })
    })
  }

  private removeSceneFadePass() {
    this.removePass(this.scene_fade_pass)
    this.scene_fade_effect.dispose()
    this.scene_fade_pass.dispose()
  }

  /**
   * Vignette Effect
   */
  private vignette_effect: IVignetteEffect | undefined
  private vignette_pass: EffectPass | undefined
  addVignettePass(params?: VignetteEffectParams) {
    this.removeVignettePass()
    this.vignette_effect = new VignetteEffect(params)
    this.vignette_pass = new EffectPass(this.camera, this.vignette_effect)
    this.addPass(this.vignette_pass)
  }

  updateVignettePass(params: Omit<VignetteEffectParams, "blendFunction">) {
    if (!this.vignette_effect)
      return

    const { technique, offset, darkness, center } = params

    this.vignette_effect.technique = technique ?? this.vignette_effect.technique
    this.vignette_effect.offset = offset ?? this.vignette_effect.offset
    this.vignette_effect.darkness = darkness ?? this.vignette_effect.darkness
    this.vignette_effect.center = center ?? this.vignette_effect.center
  }

  removeVignettePass() {
    if (this.vignette_effect && this.vignette_pass) {
      this.removePass(this.vignette_pass)
      this.vignette_effect.dispose()
      this.vignette_pass.dispose()
    }
  }

  resize() {
    this.camera.aspect = this.sizes.aspect
    this.camera.updateProjectionMatrix()
    this.composer.setSize(this.sizes.width, this.sizes.height)
    this.renderer.setPixelRatio(this.sizes.device_pixel_ratio)
  }

  update() {
    this.composer.render()
  }

  destroy() {
    this.removeToneMappingPass()
    this.removeSceneFadePass()
    this.removeVignettePass()
    this.removeRenderPass()
    this.composer.removeAllPasses()
    this.composer.dispose()
  }
}

Expected behavior

renderer.info.programs should be cleared

Screenshots

image

Library versions used

Desktop

Mobile

vanruesc commented 1 month ago

renderer.info.programs should be cleared

Programs and other disposable resources will be recreated automatically if you call render after calling dispose.

Your reproduction steps are incomplete. How exactly are you disposing things? Have you debugged your code and checked whether dispose is being called on the respective resources? Please provide a complete and minimal reproduction of the issue instead of code snippets.

Steve245270533 commented 1 month ago

renderer.info.programs should be cleared

Programs and other disposable resources will be recreated automatically if you call render after calling dispose.

Your reproduction steps are incomplete. How exactly are you disposing things? Have you debugged your code and checked whether dispose is being called on the respective resources? Please provide a complete and minimal reproduction of the issue instead of code snippets.

This is the smallest scene I restored. It seems that any Effect added cannot be cleaned up normally. stackblitz-demo

vanruesc commented 1 month ago

Thanks for the example :+1:

This does indeed look like a bug. The dispose method in Pass doesn't dispose the fullscreen material because that property is defined as a getter/setter which doesn't get picked up by Object.keys. I'll work on a fix.

In the meantime, you can manually dispose those materials as shown here: vitejs-vite-als1ht

Note that the remaining geometry is the fullscreen triangle that is shared by all postprocessing passes. The lifetime of that mesh is basically tied to the WebGL context and it's being reused when needed so it shouldn't do any harm. That being said, I'll check if it can be disposed somehow.

Steve245270533 commented 1 month ago

Thank you very much. Currently, manually disposing of these passes materials is effective.