Navigate
Search topics across all sections
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.
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
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
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
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
{/* 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.
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
});// 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);
});// Tiny render target on a large surface
const rt = new THREE.WebGLRenderTarget(64, 64);
// 64x64 texture stretched over a large plane
// Result: extremely pixelatedBest 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.