Help Needed: Screen Space Reflections (SSR) Integration with React Three Fiber & Postprocessing

Hi everyone,

I’m currently working on a project using React Three Fiber and react-three-postprocessing for postprocessing effects. I’m using the EffectComposer from this package to handle effects, but I’ve noticed that Screen Space Reflections (SSR) is no longer exported or available in the latest versions.

Previously, packages like screen-space-reflections and realism-effects offered SSR solutions, but unfortunately, they are incompatible with my current Next.js setup and recent React Three Fiber versions. Downgrading the packages isn’t a feasible option for me.

I’m aware that it’s possible to extend the Effect class from the underlying postprocessing library to create custom effects, and I want to integrate SSR effect seamlessly into EffectComposer react-three-postprocesing pipeline—just like other postprocessing effects Bloom, tonemapping etc.

However, I’m not an expert in creating SSR or screen-space reflection shaders, so I’m reaching out to see if anyone:

  • Has experience creating custom SSR effects for React Three Fiber’s postprocessing pipeline,
  • Could share example code or resources for implementing SSR as a custom Effect,
  • Or knows of any viable SSR implementations compatible with the latest React Three Fiber and Next.js setup.

current setup:

'use client';

import { useThree } from '@react-three/fiber';
import { Bloom, EffectComposer, N8AO, Outline, SMAA, ToneMapping } from '@react-three/postprocessing';
import { BlendFunction, KernelSize } from 'postprocessing'
import { useSelectionStore } from '@/store/useSelectionStore';
import { useAdvancedRenderingStore } from '@/store/useAdvancedRenderingStore';
import { useControls } from 'leva';
import { MyCustomEffect } from '../effects/myCustomEffect';

export function PostProcessing() {
  const { selectedMeshes } = useSelectionStore();
  const { size } = useThree();
  const { 
    bloomIntensity, 
    bloomThreshold, 
    bloomSmoothing,
    toneMappingMiddleGrey,
    toneMappingMaxLuminance,
    toneMappingAdaptationRate,
    toneMappingAverageLuminance,
    toneMappingMode
  } = useAdvancedRenderingStore();

   const getToneMappingMode = () => {
    switch (toneMappingMode) {
      case 'Linear':
        return BlendFunction.NORMAL;
      case 'Reinhard':
        return BlendFunction.SCREEN;
      case 'Cineon':
        return BlendFunction.OVERLAY;
      case 'ACES':
      default:
        return BlendFunction.NORMAL;
    }
  };

  const { middleGrey, maxLuminance, adaptionRate, averageLuminance, opacity } = useControls({
    middleGrey: {
      min: 0,
      max: 5,
      value: toneMappingMiddleGrey,
      step: 0.1
    },
    adaptionRate: {
      min: 0,
      max: 1,
      value: toneMappingAdaptationRate,
      step: 0.1
    },
    opacity: {
      min: 0,
      max: 1,
      value: 0.5,
      step: 0.1
    },
    maxLuminance: {
      min: 0,
      max: 64,
      value: toneMappingMaxLuminance,
      step: 1
    },
    averageLuminance: {
      min: 0,
      max: 64,
      value: toneMappingAverageLuminance,
      step: 1
    },
  });
  
  return (
    <EffectComposer autoClear={false} multisampling={0}>
      {/**my custom SSR Effect */}
      <MyCustomEffect/>  
      <SMAA />
      <Outline
        selection={selectedMeshes}
        selectionLayer={10}
        blendFunction={BlendFunction.SUBTRACT}
        edgeStrength={300.0}
        pulseSpeed={0.0}
        visibleEdgeColor={0xff0000}
        hiddenEdgeColor={0xff0000}
        width={size.width}
        height={size.height}
        kernelSize={KernelSize.HUGE}
        blur={false}
        xRay={true}
      />
  
      <Bloom
        intensity={bloomIntensity}
        luminanceThreshold={bloomThreshold}
        luminanceSmoothing={bloomSmoothing}
        mipmapBlur={true}
        levels={4}
        kernelSize={KernelSize.LARGE}
      />

      {/* <N8AO aoRadius={50} distanceFalloff={0.2} intensity={8} screenSpaceRadius halfRes /> */}

      <ToneMapping middleGrey={middleGrey} maxLuminance={maxLuminance} opacity={opacity} blendFunction={getToneMappingMode()}/>
    </EffectComposer>
  );
}

CustomEffect template:

import { SSRBlurShader, SSRDepthShader, SSRShader } from 'three/examples/jsm/shaders/SSRShader.js';
import { CopyShader } from 'three/examples/jsm/shaders/CopyShader.js';
import { Effect } from 'postprocessing';
import { Uniform } from 'three';


const fragmentShader = `
  uniform float param; // Example uniform
  void mainUv(inout vec2 uv) {}
  void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
    // outputColor = vec4(inputColor.rgb * param, 1.0);
    // outputColor = inputColor;
    outputColor = vec4(1.0, 0.0, 0.0, 1.0);
  }
`;


class MyCustomEffectImpl extends Effect {
  constructor({ param = 0.1 } = {}) {
    super('MyCustomEffect', fragmentShader, {
      uniforms: new Map([
        ['param', new Uniform(param)],
      ]),
    });
  }

}

import { forwardRef, useMemo } from 'react';

interface MyCustomEffectProps {
  param?: number
}

export const MyCustomEffect = forwardRef<
  Effect,
  MyCustomEffectProps
>(({ param = 0.1, ...props }, ref) => {
  const effect = useMemo(
    () => new MyCustomEffectImpl({ param }),
    [param]
  )

  return <primitive ref={ref} object={effect} dispose={null} {...props} />
})