I’ve been working on building 3D scenes and environments in the browser using Three.JS. As part of those, I make pretty heavy use of the pmndrs postprocessing library for post-processing and effects.

I’ve also implemented a custom godrays effect that works with postprocesing called three-good-godrays. It creates a custom pass that is added to the postprocessing EffectComposer which renders volumetric screen-space godrays by reading the depth buffer and shadow map for a light.

The Problem

For one of my scenes, the postprocessing chain had gotten pretty long with several different effects in use. At some point, I started seeing errors like this in the JS console:

[.WebGL-0x16c00334d00]GL ERROR :GL_INVALID_OPERATION : glDrawArrays: Source and destination textures of the draw are the same.

In addition, my godrays effect stopped working. I used the Spector.JS browser extension to debug the WebGL rendering sequence for one of my frames and I saw that the shaders were getting launched, but nothing seemed to be getting rendered into the destination buffers.

The Cause

As I mentioned previously, the shaders used internally by three-good-godrays and some of the other effects in my pipeline needed access to scene depth information. The Passes added to the EffectComposer have a needsDepthTexture attribute that they can set to indicate that they need access to the depth buffer. If set, the EffectComposer will call their setDepthTexture() method and provide them a texture containing it.

Internally, EffectComposer has two buffers that it swaps back and forth between when rendering the passes. I looked at the code and the logic looks roughly like this:

input_buffer, output_buffer = build_buffers()

if any_pass_needs_depth_texture:
  input_buffer.bind_depth_texture(build_depth_texture())

for i, fx_pass in enumerate(passes):
  is_last = i == len(passes) - 1
  render_to_screen = is_last or pass.render_to_screen
  # Passing None indicates that the pass should render to the canvas framebuffer
  # which puts its output directly onto the screen
  fx_pass.render(input_buffer, None if is_last else output_buffer)

  # needs_swap defaults to true
  if pass.needs_swap and not render_to_screen:
    output_buffer, input_buffer = input_buffer, output_buffer

When the depth texture is first initialized, it gets set on the input buffer. Since these buffers swap back and forth each frame, it’s possible that the depth buffer will be attached to the output buffer while processing the effect. If the pass makes use of the depth texture as input for some shader by passing it as a uniform or similar, it will cause the WebGL error I pasted before and cause the pass to fail to render.

This is a known bug/limitation of postprocessing. There are multiple issues about this:

They plan to address it in a release of version 7, which is not yet out at the time of writing this.

The Fix

Luckily, it’s possible to work around this issue. I had to update three-good-godrays to detect and handle case where the provided depth texture is the same as the one bound to the provided output buffer.

If it is the same, then I allocate an additional framebuffer the same size of the depth texture, run a CopyPass to copy the contents of the depth texture into it, and then bind that copied buffer as the input for the sceneDepth uniform of the shader instead. This fixes the “Source and destination texture sof the draw are the same” error and allows the pass to render without issue.

I made that change in this commit. I do hope that pmndrs postprocessing gets that v7 rework; it was very hard to figure out what was causing this and debugging it took several hours.