PBR subsurface scattering becomes black with an intermediate RenderTargetTexture and Frame Graph

Hi,

I am trying to use PBR subsurface scattering with a post-processing path that renders the scene to an intermediate RenderTargetTexture and then imports/copies that texture through Frame Graph.

Playground:

Tested with Babylon.js Playground 9.18.2.

Reproduction:

  1. Run the Playground.
  2. Press 1: direct scene render.
  3. Press 2: render to an intermediate RTT and copy it through Frame Graph.
  4. Press 3: render to the RTT, apply Frame Graph image processing, and copy it to the backbuffer.

There are two spheres:

  • left: regular PBRMaterial
  • right: the same material with subSurface.isScatteringEnabled = true

Actual result:

  • Mode 1: both spheres render normally.
  • Modes 2 and 3: the regular PBR sphere renders normally, but the scattering sphere becomes almost black.
  • The same behavior occurs in both WebGPU and WebGL2.
  • There are no console warnings or errors.
  • The repro does not use Electron, PMX/babylon-mmd, a custom MaterialPlugin, or application code.

Expected result:
The final SSS-composited color should be available to the intermediate RTT / Frame Graph path, or there should be a documented supported way to feed the composited output into Frame Graph.

Questions:

  1. Is PrePass SSS expected to be composited into a user RenderTargetTexture?
  2. Is importing/copying that RTT into Frame Graph a supported path?
  3. Should this render pass instead be built entirely inside Frame Graph, for example with an ObjectRenderer task?
  4. If this path is supported, which public texture or task should be used to preserve the SSS result?

Environment:

  • Babylon.js Playground: 9.18.2
  • Backends: WebGPU and WebGL2
  • OS: Windows 11 Pro 25H2 (OS build 26200.8875)
  • Browser: Google Chrome 151.0.7922.72 (Official Build, 64-bit)
  • GPU: NVIDIA GeForce RTX 3070
  • GPU driver: NVIDIA 595.95
  • CPU: 11th Gen Intel Core i9-11900K @ 3.50 GHz
  • RAM: 64 GB

Thank you.


cc @Evgeni_Popov (just be patient he is on vacation)

Thank you! No rush at all — I’ll be happy to wait. Hope he enjoys his vacation :slight_smile:

I investigated this. The Frame Graph copy is not the source of the problem.

The RTT gets its own PrePassRenderTarget, but that target is left disabled when the prepass is required only by a material (as is the case for PBR SSS). The material therefore writes the diffuse scattering contribution to the irradiance attachment, but the SSS composition pass is not run for the user RTT. Frame Graph then correctly copies the incomplete color, which explains the nearly black sphere.

Here is a corrected Playground with a temporary workaround:

The pass-through post-process opts the RTT into prepass rendering and writes the SSS-composited result to it. The Playground also keeps image processing in the correct color space: mode 2 applies it before the raw Frame Graph copy, while mode 3 keeps the RTT linear and applies image processing once in Frame Graph.

To answer the questions:

  1. Yes, PrePass SSS is intended to be composited into a user RenderTargetTexture. The RTT prepass hooks and composition output path already exist; the RTT-specific target is simply not enabled in this case.
  2. Yes, importing and copying that RTT through Frame Graph is supported. Once the RTT contains the composed result, Frame Graph preserves it correctly.
  3. No, using a FrameGraphObjectRendererTask is not currently a workaround for this issue. It does not run the legacy RTT prepass hooks that perform the SSS composition.
  4. There is currently no dedicated public Frame Graph SSS texture or task. The intended texture is the user RTT after prepass composition.

So this is an engine-side PrePass/RTT activation issue rather than a Frame Graph import/copy issue.

Thank you so much for the detailed investigation and the corrected Playground! The explanation matches exactly what I was seeing in my app — I’ll apply the pass-through workaround and report back if anything unexpected comes up.

Also good to know about ObjectRendererTask not running the RTT prepass hooks — that saves me from a wrong path. Thanks again!

The SSS composition timing might be the issue here. Since PrePass rendering happens before your RTT capture, the composed output might not be making it into the texture. Have you tried checking if the SSS pass result is available as a separate texture you could manually composite into the RTT after the Frame Graph pass?