Navigate

Search topics across all sections

GitHub
Particles & Lines

Render Targets

Normally, the renderer draws your scene directly to the screen. But what if you could render to a texture instead? That is what render targets do. They let you capture a scene as an image and display it on any surface — creating mirrors, portals, security cameras, minimaps, and picture-in-picture effects.

You set up a render target and render a portal scene to it. The texture appears on your surface, but the main scene disappears — the screen is blank. You check the portal scene and it renders perfectly. The problem? You forgot to call gl.setRenderTarget(null) after the off-screen render.

terminal
Main scene disappears. Only the render target content is visible, or the screen is entirely black.

Real-world

Think of render targets as picture-in-picture on your TV.

Your main screen shows the live game. But in the corner, a small window shows a different camera angle — maybe a security feed, or a rear-view mirror, or a minimap. That small window is rendered separately and then displayed as an image within the main image.

In Three.js terms: you set up a second camera looking at a second scene (or the same scene from a different angle). You render that view into a texture instead of to the screen. Then you slap that texture onto any surface — a TV screen, a mirror, a magic portal, anything.

The key insight: a render target is just a texture that gets re-painted every frame instead of being loaded from an image file.

How Render Targets Work

The rendering pipeline with an off-screen pass.

Create Target

WebGLRenderTarget with width x height

Render to Target

gl.setRenderTarget(rt) then gl.render()

Reset to Screen

gl.setRenderTarget(null)

Use as Texture

rt.texture on any material's map prop

Building a Portal TV Step by Step

Let us render a mini scene onto a surface — like a TV showing a live camera feed from another world.

Step 1 -- Create the render target and camera

PortalTV.tsxTSX
import * as THREE from "three";
import { useMemo } from "react";

const renderTarget = useMemo(() => {
  return new THREE.WebGLRenderTarget(512, 512, {
    format: THREE.RGBAFormat,
    stencilBuffer: false,
  });
}, []);

const portalCamera = useMemo(() => {
  const cam = new THREE.PerspectiveCamera(50, 1, 0.1, 100);
  cam.position.set(0, 0, 3);
  return cam;
}, []);

The render target is a GPU texture buffer — 512 by 512 pixels. We also create a separate camera for the portal scene. This camera determines what the portal "sees."

Step 2 -- Create a separate scene with createPortal

PortalTV.tsxTSX
import { createPortal } from "@react-three/fiber";

const portalScene = useMemo(
  () => new THREE.Scene(), []
);

// In your JSX, portal content lives in a
// separate scene graph:
{createPortal(
  <>
    <ambientLight intensity={0.5} />
    <mesh>
      <icosahedronGeometry />
      <meshStandardMaterial color="hotpink" />
    </mesh>
  </>,
  portalScene
)}

createPortal renders React children into a different Three.js scene. This keeps the portal content completely separate from your main scene, avoiding infinite loops.

Step 3 -- Render the portal scene to the texture every frame

PortalTV.tsxTSX
import { useFrame } from "@react-three/fiber";

useFrame(({ gl }) => {
  // 1. Render portal scene to the texture
  gl.setRenderTarget(renderTarget);
  gl.render(portalScene, portalCamera);

  // 2. CRITICAL: reset so main scene goes to screen
  gl.setRenderTarget(null);
});

Every frame, we redirect the renderer to paint the portal scene into our texture. Then we reset to null so the main scene renders to the screen as usual. Forgetting this reset is the number one render target bug.

Step 4 -- Display the texture on a surface

PortalTV.tsxTSX
{/* The TV screen showing the portal */}
<mesh position={[0, 1, 0]}>
  <planeGeometry args={[2, 1.5]} />
  <meshBasicMaterial map={renderTarget.texture} />
</mesh>

renderTarget.texture is a live texture that updates every frame. We use it as the map on a material, just like any static texture. The surface now shows a live view of the portal scene.

What you just learned

WebGLRenderTarget creates an off-screen texture buffer that the renderer can paint into instead of the screen.

Always call gl.setRenderTarget(null) after your off-screen render — otherwise the main scene renders to the texture too.

createPortal keeps portal content in a separate scene graph, preventing infinite recursion in mirrors.

renderTarget.texture is a live texture that updates every frame — use it as a map on any material.

Question

A render target essentially doubles your rendering work — you render the portal scene plus the main scene every frame. What if you need four security camera feeds? That is five full scene renders per frame. How would you manage the performance cost?

Think about it...

You set up a render target and your portal scene renders correctly. But the main scene is gone — the screen is completely black. What is the most likely cause?

Try These Challenges

Put what you learned into practice. Try each challenge in the demo above using the Leva controls, then check the solution.

Try This!

Beginner

Set resolution to 64 — pixel art TV!

Try This!

Beginner

Set resolution to 1024 — crispy

Try This!

Beginner

Toggle portal — blank screen

These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.

1
Render target shows a blank black texture
Forgot to reset the render target after rendering
Don't do this
Portal.tsxTSX
useFrame(({ gl, scene, camera }) => {
  // Render to the texture
  gl.setRenderTarget(renderTarget);
  gl.render(portalScene, portalCamera);
  // FORGOT to set render target back to null!
  // The main scene now renders to the texture too
  // Screen shows nothing or the wrong scene
});
After rendering to a render target, the GPU is still pointed at that texture buffer. If you do not call gl.setRenderTarget(null), the main scene also renders into the texture instead of the screen. Always reset to null when done.
2
Infinite recursion when rendering a mirror
The mirror scene includes the mirror itself
Don't do this
Mirror.tsxTSX
// The mirror material uses the render target texture
// But the scene that renders INTO the target
// also contains the mirror mesh — infinite loop!
useFrame(({ gl, scene, camera }) => {
  gl.setRenderTarget(renderTarget);
  gl.render(scene, mirrorCamera); // renders mirror too!
  gl.setRenderTarget(null);
});
If the mirror is part of the scene being rendered to its own texture, you get infinite recursion (or at best, a visual artifact). Either hide the mirror mesh before the off-screen render, or use R3F's createPortal to keep the portal scene separate.
3
Render target texture looks blurry or pixelated
Resolution is too low for the display size
Don't do this
RenderTarget.tsxTSX
// Tiny render target on a large surface
const rt = new THREE.WebGLRenderTarget(64, 64);
// 64x64 texture stretched over a large plane
// Result: extremely pixelated
The render target resolution determines the texture quality. 64x64 stretched over a large surface looks blocky. 512x512 is a good balance. For mirrors and portals where quality matters, go to 1024. Each doubling quadruples the pixel count and rendering cost.

Best Practices

Always reset to null

Call gl.setRenderTarget(null) immediately after your off-screen render. Make it a habit — this is the single most common render target bug.

Use createPortal for separate scenes

R3F's createPortal renders React children into a separate Three.js scene, keeping portal content isolated and preventing recursive rendering issues.

Balance resolution vs performance

512x512 is a solid default for most portals. Use 256 for distant or small surfaces. Only go to 1024 for large, close-up mirrors. Each step up quadruples the pixel count.

Skip frames for distant portals

Not every render target needs to update every frame. For a security camera far away, updating every 2nd or 3rd frame cuts the cost without a noticeable visual difference.