首页
/ three.js ArrayCamera 深度解析:用一组预定义相机高效渲染多视角与 VR 场景

three.js ArrayCamera 深度解析:用一组预定义相机高效渲染多视角与 VR 场景

2026-09-06 15:17:49作者:滑思眉Philip

ArrayCamera 是 three.js 中用于「一次渲染调用覆盖多个视口」的相机类型:它把一组 PerspectiveCamera 子相机聚合为一个渲染入口,让 WebGL 渲染器只执行一次场景遍历、投影与光照设置,随后为每个子相机分别绘制其 viewport 区域。读完本文,你将掌握 ArrayCamera 的构造方式与全部公开属性、子相机 viewport 的必选配置要求、渲染管线内部对 isArrayCamera 的处理流程,以及官方 6×6 相机阵列示例中可复制的多视口实战代码。

three.js 官方 webgl_camera_array 示例截图:6×6 相机阵列从不同位置观察同一场景

继承体系与定位

按照官方 API 文档 ArrayCamera,该类的继承链为:

EventDispatcher → Object3D → Camera → PerspectiveCamera → ArrayCamera

这与源码 src/cameras/ArrayCamera.jsclass ArrayCamera extends PerspectiveCamera 的定义一致,单测 test/unit/src/cameras/ArrayCamera.tests.js 也用 object instanceof PerspectiveCamera 明确验证了这一点。

文档给出的核心定位是:

This type of camera can be used in order to efficiently render a scene with a predefined set of cameras. This is an important performance aspect for rendering VR scenes.(这种相机类型可以用一组预定义的相机高效地渲染场景,这是渲染 VR 场景时重要的性能要素。)

其效率来源可以从渲染器源码结构看到:在 src/renderers/WebGLRenderer.js 中,当传入的相机满足 camera.isArrayCamera 时,渲染器先完成一次 projectObject(场景图遍历与投影)、一次光照 setup,然后才进入按子相机逐个 renderScene 的循环。也就是说,多视角共享了场景遍历、排序、阴影贴图生成等高开销阶段,这正是其优于「分别调用多次 renderer.render(scene, cameraN)」的原因。

构造函数

官方文档定义:

new ArrayCamera( array : Array.<PerspectiveCamera> )
  • array:一个透视子相机数组(Array.<PerspectiveCamera>),默认值为 []

对应实现位于 src/cameras/ArrayCamera.js

constructor( array = [] ) {

    super();

    this.isArrayCamera = true;
    this.isMultiViewCamera = false;
    this.cameras = array;

}

构造函数只有三件事:调用父类 PerspectiveCamera 的无参构造、置位两个只读标志、把传入的数组直接赋给 cameras。可以推断,ArrayCamera 本体并不持有独立的视锥或投影矩阵——真正参与视口投影的是每个子相机,聚合体的职责是向渲染器声明「这是一组需要按子相机逐个绘制的相机」。

公开属性

.cameras : Array.<PerspectiveCamera>

透视子相机数组,构造时传入、渲染时被遍历。从 src/renderers/WebGLRenderer.js 的用法看,每个子相机按顺序绘制,且绘制范围由该子相机自身的 viewport 决定:

for ( let i = 0, l = cameras.length; i < l; i ++ ) {

    const camera2 = cameras[ i ];

    renderScene( currentRenderList, scene, camera2, camera2.viewport );

}

.isArrayCamera : boolean(readonly)

类型测试标志,默认 true(见 src/cameras/ArrayCamera.js)。整个渲染管线靠它识别多视口相机,例如:

.isMultiViewCamera : boolean(readonly)

标记该相机是否用于多视口(multiview)渲染,默认 false(见 src/cameras/ArrayCamera.js)。这个标志区分了两条技术路径:

  • false(默认):软件级多视口。渲染器在单次场景遍历后,循环切换视口与投影矩阵逐个绘制,兼容性最好,webgl_camera_array 示例走的就是这条路;
  • true:硬件级多视口。此时会借助 WebGL 的 OVR_multiview2 扩展。从 src/nodes/accessors/Camera.js 可以看到,TSL 在此情况下用内建变量 gl_ViewID_OVR 索引矩阵数组,而非相机下标:camera.isMultiViewCamera ? builtin( 'gl_ViewID_OVR' ) : cameraIndex。与之配套的渲染目标配置是 src/core/RenderTarget.js 中的 multiview: false 选项(「Whether this target is used for multiview rendering (WebGL OVR_multiview2 extension)」)。

对绝大多数多视口需求(分屏、鱼眼阵列、调试视图),保持默认的 false 即可。

关键前提:为每个子相机设置 viewport

文档明确指出:

An instance of ArrayCamera always has an array of sub cameras. It's mandatory to define for each sub camera the viewport property which determines the part of the viewport that is rendered with this camera.(ArrayCamera 实例始终持有一个子相机数组;必须为每个子相机定义 viewport 属性,它决定用该相机渲染视口的哪一部分。)

viewport 是一个 Vector4(x, y, width, height,像素坐标)。这一约束在渲染器源码中直接体现:renderScene( currentRenderList, scene, camera2, camera2.viewport )src/renderers/WebGLRenderer.js)把 camera2.viewport 作为绘制范围传入。此外,渲染状态(如反转深度缓冲、坐标系、投影矩阵更新)也会同步到全部子相机,见 src/renderers/WebGLRenderer.js 中对 camera.cameras 的逐项遍历。

视锥剔除:FrustumArray 的作用

多相机场景下,一个物体只要被任意一个子相机看到就需要渲染。为此 three.js 提供了专门的 src/math/FrustumArray.js

  • setFromArrayCamera( cameraArray )L55-L76):为 ArrayCamera 的每个子相机计算并缓存一个视锥;
  • intersectsObject / intersectsSprite / intersectsSphere / intersectsBox / containsPoint:只要对象与任一缓存视锥相交即判定可见。

通用渲染器在每帧投影阶段调用它(src/renderers/common/Renderer.js),而在对象遍历与剔除环节统一采用 camera.isArrayCamera ? _frustumArray : _frustum 的选择逻辑(如 src/renderers/WebGLRenderer.js)。这保证多视口渲染不会退化成「每个对象都全量绘制」。

官方示例实战:6×6 相机阵列

仓库内置示例 examples/webgl_camera_array.html(WebGPU 版本见 examples/webgpu_camera_array.html)展示了典型的多视口布局:36 个从不同位置看向原点的子相机,各自占据 1/6 × 1/6 的屏幕区域。核心代码可直接复用:

import * as THREE from 'three';

const AMOUNT = 6;
const ASPECT_RATIO = window.innerWidth / window.innerHeight;

// 每个子视口的像素尺寸(乘以 devicePixelRatio 以匹配物理像素)
const WIDTH  = ( window.innerWidth  / AMOUNT ) * window.devicePixelRatio;
const HEIGHT = ( window.innerHeight / AMOUNT ) * window.devicePixelRatio;

const cameras = [];
for ( let y = 0; y < AMOUNT; y ++ ) {
    for ( let x = 0; x < AMOUNT; x ++ ) {

        const subcamera = new THREE.PerspectiveCamera( 40, ASPECT_RATIO, 0.1, 10 );
        // 必选:定义该子相机渲染的视口区域(Vector4: x, y, width, height)
        subcamera.viewport = new THREE.Vector4(
            Math.floor( x * WIDTH ), Math.floor( y * HEIGHT ),
            Math.ceil( WIDTH ),     Math.ceil( HEIGHT )
        );
        // 把相机阵列排布成环形,全部看向原点
        subcamera.position.x = ( x / AMOUNT ) - 0.5;
        subcamera.position.y = 0.5 - ( y / AMOUNT );
        subcamera.position.z = 1.5;
        subcamera.position.multiplyScalar( 2 );
        subcamera.lookAt( 0, 0, 0 );
        subcamera.updateMatrixWorld();
        cameras.push( subcamera );
    }
}

const camera = new THREE.ArrayCamera( cameras );
camera.position.z = 3;

// ... 构建 scene / renderer 后:
renderer.setAnimationLoop( animate );

function animate() {
    // 渲染器内部会为 36 个子相机逐视口绘制,仅需一次 render 调用
    renderer.render( scene, camera );
}

(以上代码节选自 examples/webgl_camera_array.html,阴影灯光与背景、圆柱网格设置略。)

窗口缩放时,viewport 与子相机 aspect 必须一起更新,否则子视口会错位或拉伸(examples/webgl_camera_array.html):

window.addEventListener( 'resize', () => {

    const ASPECT_RATIO = window.innerWidth / window.innerHeight;
    const WIDTH  = ( window.innerWidth  / AMOUNT ) * window.devicePixelRatio;
    const HEIGHT = ( window.innerHeight / AMOUNT ) * window.devicePixelRatio;

    camera.aspect = ASPECT_RATIO;
    camera.updateProjectionMatrix();

    for ( let y = 0; y < AMOUNT; y ++ ) {
        for ( let x = 0; x < AMOUNT; x ++ ) {

            const subcamera = camera.cameras[ AMOUNT * y + x ];
            subcamera.viewport.set(
                Math.floor( x * WIDTH ),
                Math.floor( y * HEIGHT ),
                Math.ceil( WIDTH ),
                Math.ceil( HEIGHT ) );
            subcamera.aspect = ASPECT_RATIO;
            subcamera.updateProjectionMatrix();
        }
    }

    renderer.setSize( window.innerWidth, window.innerHeight );
} );

两个容易踩坑的细节:

  1. viewport 用物理像素。示例中 WIDTH / HEIGHT 都乘了 window.devicePixelRatio,因为 viewport 对应的是实际绘制缓冲区的像素坐标,忘记乘 devicePixelRatio 会导致高 DPI 屏幕上视口只占左上角一小块;
  2. 子相机位置是相对聚合并自行 lookAt 的。父 ArrayCameraposition.z = 3 只是聚合体的基准位姿,每个子相机通过自身的 position + lookAt( 0, 0, 0 ) 决定观察方向。

渲染管线内部的完整调用链

把上述分散的证据串起来,一次 renderer.render( scene, arrayCamera ) 的实际流程是:

  1. 通用渲染器检测到 camera.isArrayCamera,用 FrustumArray.setFromArrayCamera 为全部子相机缓存视锥(src/renderers/common/Renderer.js);
  2. WebGL 后端执行一次场景遍历 projectObject、物体排序与阴影贴图渲染(src/renderers/WebGLRenderer.js);
  3. 若场景含透射(transmission)对象,会先按子相机逐一遍历透射 pass(L1751-L1761),再渲染背景;
  4. 循环调用 renderScene( currentRenderList, scene, camera2, camera2.viewport ),每个子相机只绘制自己的 viewport 区域(L1765-L1771)。

WebGPU 后端同样识别 isArrayCamera(如 src/renderers/webgpu/WebGPUBackend.js),因此示例提供了 WebGL / WebGPU 两套等价页面。

测试验证与行为边界

单元测试 test/unit/src/cameras/ArrayCamera.tests.js 覆盖了三条可回归的行为:

  • new ArrayCamera() instanceof PerspectiveCamera === true(继承断言);
  • 无参实例化成功(cameras 取默认值 [],构造不抛错);
  • 实例上 isArrayCamera === true(类型标志断言)。

需要注意的边界:cameras 为空数组时,ArrayCamera 依然通过 isArrayCamera 分支进入多视口渲染逻辑,但循环内没有子相机可绘制——因此实际使用中务必传入非空子相机数组,并为每个子相机设置 viewport,否则画面不会有任何输出。

小结

项目 说明 依据
继承链 EventDispatcher → Object3D → Camera → PerspectiveCamera → ArrayCamera docs/pages/ArrayCamera.html.mdsrc/cameras/ArrayCamera.js
构造参数 array : Array.<PerspectiveCamera>,默认 [] src/cameras/ArrayCamera.js#L21
.cameras 子相机数组,渲染时被逐个绘制 src/renderers/WebGLRenderer.js#L1765-L1771
.isArrayCamera readonly,默认 true,管线分支判定标志 src/cameras/ArrayCamera.js#L32
.isMultiViewCamera readonly,默认 falsetrue 时走 OVR_multiview2 硬件多视口 src/nodes/accessors/Camera.js#L86src/core/RenderTarget.js#L43-L44
必选配置 每个子相机必须设置 viewportVector4,物理像素) 官方文档、examples/webgl_camera_array.html#L47
视锥剔除 FrustumArray 缓存全部子相机视锥,任一相交即可见 src/math/FrustumArray.js#L55-L76

ArrayCamera 的价值在于把「多视角渲染」从 N 次独立的完整渲染流程,收敛为「一次场景准备 + N 次视口绘制」:VR 双目、分屏预览、多视角调试等场景都可以基于它构建,且只需一次 renderer.render 调用即可完成整帧输出。

登录后查看全文
热门项目推荐
相关项目推荐