N8AO failing to render when using OrthographicCamera in R3F

I am using @react-three/postprocessing with the N8AO component. While the ambient occlusion renders perfectly in Perspective mode, it completely disappears when switching to an OrthographicCamera.

Code Snippet:

// Effects.tsx
import { EffectComposer, N8AO, SMAA } from '@react-three/postprocessing';

export function Effects() {
  return (
    <EffectComposer multisampling={0} enableNormalPass>
      <N8AO
        denoiseRadius={16}
        denoiseSamples={8}
        intensity={5}
        color="#6C7C7C"
      />
      <SMAA />
    </EffectComposer>
  );
}

// App.tsx
<Canvas>
  {ortho ? (
    <OrthographicCamera 
      makeDefault 
      position={[1000, 1000, 1000]} 
      near={1} 
      far={5000} 
    />
  ) : (
    <PerspectiveCamera makeDefault position={[0, 100, 200]} />
  )}

  <mesh castShadow receiveShadow>
    <boxGeometry args={[10, 10, 10]} />
    <meshStandardMaterial color="gray" />
  </mesh>

  <Effects />
</Canvas>

This looks like a classic post-processing + camera type mismatch issue in R3F.

N8AO (like most screen-space ambient occlusion passes) is heavily dependent on depth and normal reconstruction. It assumes a perspective projection where depth behaves in a non-linear way that the shader is designed around.

When you switch to an OrthographicCamera, that assumption breaks. In ortho view, depth is linear and doesn’t encode perspective foreshortening the same way, so the SSAO algorithm often ends up with either flat results or completely fails to pick up occlusion detail.

That’s why it works fine in PerspectiveCamera but disappears in OrthographicCamera, the input data it relies on is fundamentally different.

In practice, people usually solve this in one of a few ways:

Stick to PerspectiveCamera when SSAO is important, even for “2D-like” scenes, since you can fake orthographic feel with a narrow FOV.

Disable or replace SSAO when switching to orthographic view, especially in UI-like or isometric modes.

Use a custom AO approach that doesn’t rely purely on screen-space depth reconstruction, though that gets expensive quickly.

Sometimes also tweaking enableNormalPass or depth resolution helps, but in most cases it won’t fully fix orthographic incompatibility.

So this isn’t really a bug in your code, it’s more a limitation of how screen-space AO is designed. It’s basically built around perspective depth cues, and orthographic projection removes the very thing it needs to calculate occlusion properly.

If your project needs both camera modes, the most stable approach is usually to conditionally toggle N8AO off when ortho is active and maybe replace it with a subtle baked or fake shading solution instead.

N8AOPass is designed to be as easy to use as possible. It works with logarithmic depth buffers and orthographic cameras, supports materials with custom vertex displacement and alpha clipping, and automatically detects the presence of these things so you do not need to deal with user-side configuration.

~ n8ao

for me it works in both Ortho cam and Perspective cam:

the problem is mostly about the camera setup not the N8AO itself.

when you use an OrthographicCamera at position [1000, 1000, 1000] with near={1} far={5000} you get a massive depth range for a tiny 10 unit box. N8AO reconstructs world positions from the depth buffer and with ortho cams that depth is linear instead of hyperbolic like perspective. that huge range basically washes out the AO completely.

also without setting zoom on the ortho camera the box is probably like 2 pixels on screen so you wont see any AO anyway.

fix it like this:

<OrthographicCamera 
  makeDefault 
  position={[50, 50, 50]} 
  zoom={30}
  near={0.1} 
  far={300} 
/>

and tune aoRadius to match your scene scale. default might not fit when switching between perspective and ortho:

<N8AO 
  aoRadius={3}
  denoiseRadius={16} 
  denoiseSamples={8} 
  intensity={5} 
  color="#6C7C7C" 
/>

also a single floating box in empty space barely shows AO. add a ground plane underneath so you actually see contact shadows:

<mesh receiveShadow rotation-x={-Math.PI / 2} position-y={-5}>
  <planeGeometry args={[50, 50]} />
  <meshStandardMaterial color="white" />
</mesh>