How to Create a Three.js Mini-Map Using Viewports
Rendering a picture-in-picture (PiP) mini-map in Three.js is a
powerful way to provide users with spatial orientation in 3D
environments. This article explains how to configure multiple viewports
using a single WebGLRenderer and two separate cameras—one
for the main view and another positioned overhead to capture the
mini-map. You will learn how to set up the cameras, define the viewport
and scissor dimensions, and render both views efficiently in a single
animation loop.
The Dual Viewport Concept
Instead of creating two separate canvas elements or running multiple
renderer instances, you can partition a single HTML5 canvas. This is
achieved using the setViewport and setScissor
methods of the Three.js WebGLRenderer.
By enabling the scissor test, you instruct the renderer to clear and draw only within a specific boundary. This allows you to render the main 3D scene across the entire screen, and then render an overhead “map” view in a smaller, isolated corner of the same canvas.
Step 1: Set Up the Cameras
You need two cameras to make this work: 1. Main
Camera: Usually a PerspectiveCamera representing
the user’s field of view. 2. Mini-Map Camera: Usually
an OrthographicCamera positioned directly above the player
or scene, pointing downwards.
// Main camera for the primary view
const mainCamera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
mainCamera.position.set(0, 5, 10);
// Mini-map camera looking top-down
const mapWidth = 200;
const mapHeight = 200;
const mapCamera = new THREE.OrthographicCamera(-10, 10, 10, -10, 1, 1000);
mapCamera.position.set(0, 50, 0); // Position high above the scene
mapCamera.lookAt(0, 0, 0); // Look straight downStep 2: Configure the Renderer for Scissoring
By default, Three.js renders to the entire canvas size. To render a smaller picture-in-picture view without clearing the main scene, you must enable the scissor test.
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Enable the scissor test
renderer.setScissorTest(true);Step 3: Implement the Render Loop
In your animation loop, you will define two distinct rendering passes. First, define the viewport and scissor region for the main screen. Second, define the viewport and scissor region for the mini-map in the top-right corner, then render the scene again with the top-down camera.
function animate() {
requestAnimationFrame(animate);
// Update object positions, controls, or physics here
// e.g., keep the mini-map camera centered over the player
// mapCamera.position.x = player.position.x;
// mapCamera.position.z = player.position.z;
const width = window.innerWidth;
const height = window.innerHeight;
// 1. Render the Main View (Full Screen)
renderer.setViewport(0, 0, width, height);
renderer.setScissor(0, 0, width, height);
renderer.setClearColor(0x111111, 1); // Dark background for main scene
renderer.clear(); // Clear color and depth buffers
renderer.render(scene, mainCamera);
// 2. Render the Mini-Map View (Top-Right Corner)
const miniMapSize = 200; // Size of the mini-map in pixels
const margin = 20; // Offset from the screen edges
const mapX = width - miniMapSize - margin;
const mapY = height - miniMapSize - margin;
renderer.setViewport(mapX, mapY, miniMapSize, miniMapSize);
renderer.setScissor(mapX, mapY, miniMapSize, miniMapSize);
renderer.setClearColor(0x222222, 1); // Slightly lighter background for the map
renderer.clearDepth(); // Clear only the depth buffer so the map draws on top
renderer.render(scene, mapCamera);
}
animate();Step 4: Handling Window Resizing
When the window resizes, you must update the main camera’s aspect
ratio and the renderer’s size. Because the mini-map viewport
calculations in the render loop use dynamic
window.innerWidth and window.innerHeight
values, the mini-map will automatically adjust its position to stay
pinned to the corner.
window.addEventListener('resize', () => {
const width = window.innerWidth;
const height = window.innerHeight;
mainCamera.aspect = width / height;
mainCamera.updateProjectionMatrix();
renderer.setSize(width, height);
});