Configure Three.js ArrayCamera for Multiple Views

This article explains how to configure and use the THREE.ArrayCamera in Three.js to render multiple camera angles onto a single canvas. You will learn how to define sub-cameras, assign custom viewports for each, and render a multi-view scene (such as a split-screen multiplayer game or a grid of security monitors) in a single render call.

Understanding the ArrayCamera

An ArrayCamera is a container camera that holds an array of sub-cameras (usually PerspectiveCamera instances). Each sub-camera has its own viewport property, defined as a THREE.Vector4 representing (x, y, width, height) in pixel coordinates. When the renderer draws the scene using the ArrayCamera, it automatically iterates through each sub-camera and renders its view onto its designated area of the canvas.

Step-by-Step Configuration

To configure an ArrayCamera, you must set up your renderer, define the viewport dimensions for each sub-camera, and instantiate the parent array camera.

1. Initialize the Renderer and Scene

First, create your standard Three.js scene and a WebGLRenderer. Ensure the renderer is configured to handle the device pixel ratio for sharp rendering on high-resolution screens.

import * as THREE from 'three';

const scene = new THREE.Scene();

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

2. Define Sub-Cameras and Viewports

To display a 2x2 grid of four distinct views on a single canvas, divide the total canvas width and height by two. You then configure a Vector4 viewport for each sub-camera.

const AMOUNT_X = 2;
const AMOUNT_Y = 2;

// Calculate the width and height of each sub-viewport in pixels
const subWidth = (window.innerWidth / AMOUNT_X) * window.devicePixelRatio;
const subHeight = (window.innerHeight / AMOUNT_Y) * window.devicePixelRatio;
const aspect = subWidth / subHeight;

const cameras = [];

for (let y = 0; y < AMOUNT_Y; y++) {
    for (let x = 0; x < AMOUNT_X; x++) {
        // Create a standard perspective camera for each view
        const subCamera = new THREE.PerspectiveCamera(40, aspect, 0.1, 10);
        
        // Define the viewport area (x, y, width, height) in pixels
        subCamera.viewport = new THREE.Vector4(
            Math.floor(x * subWidth),
            Math.floor(y * subHeight),
            Math.ceil(subWidth),
            Math.ceil(subHeight)
        );

        // Position each sub-camera to look at the center from different angles
        subCamera.position.set((x - 0.5) * 3, (y - 0.5) * 3, 3);
        subCamera.lookAt(0, 0, 0);
        
        // Mark the camera's matrix as updated
        subCamera.updateMatrixWorld();
        
        cameras.push(subCamera);
    }
}

3. Create the ArrayCamera and Render

Instantiate the ArrayCamera by passing the array of configured sub-cameras into its constructor. You then pass this ArrayCamera to your renderer’s render loop.

// Instantiate the parent ArrayCamera
const arrayCamera = new THREE.ArrayCamera(cameras);

// Render loop
function animate() {
    requestAnimationFrame(animate);
    
    // Render the scene using the ArrayCamera
    renderer.render(scene, arrayCamera);
}

animate();

Handling Window Resizing

Because sub-camera viewports are defined using pixel values, you must recalculate the viewport coordinates whenever the browser window is resized.

```window.addEventListener(‘resize’, onWindowResize);

function onWindowResize() { const width = window.innerWidth; const height = window.innerHeight;

renderer.setSize(width, height);

const subWidth = (width / AMOUNT_X) * window.devicePixelRatio;
const subHeight = (height / AMOUNT_Y) * window.devicePixelRatio;
const aspect = subWidth / subHeight;

let index = 0;
for (let y = 0; y < AMOUNT_Y; y++) {
    for (let x = 0; x < AMOUNT_X; x++) {
        const subCamera = arrayCamera.cameras[index];
        
        subCamera.aspect = aspect;
        subCamera.updateProjectionMatrix();
        
        subCamera.viewport.set(
            Math.floor(x * subWidth),
            Math.floor(y * subHeight),
            Math.ceil(subWidth),
            Math.ceil(subHeight)
        );
        
        index++;
    }
}

} ```