three.js ArrayCamera 深度解析:用一组预定义相机高效渲染多视角与 VR 场景
ArrayCamera 是 three.js 中用于「一次渲染调用覆盖多个视口」的相机类型:它把一组 PerspectiveCamera 子相机聚合为一个渲染入口,让 WebGL 渲染器只执行一次场景遍历、投影与光照设置,随后为每个子相机分别绘制其 viewport 区域。读完本文,你将掌握 ArrayCamera 的构造方式与全部公开属性、子相机 viewport 的必选配置要求、渲染管线内部对 isArrayCamera 的处理流程,以及官方 6×6 相机阵列示例中可复制的多视口实战代码。
继承体系与定位
按照官方 API 文档 ArrayCamera,该类的继承链为:
EventDispatcher → Object3D → Camera → PerspectiveCamera → ArrayCamera
这与源码 src/cameras/ArrayCamera.js 中 class 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)。整个渲染管线靠它识别多视口相机,例如:
- 通用渲染器选择视锥剔除策略:src/renderers/common/Renderer.js 中
if ( camera.isArrayCamera ) { _frustumArray.setFromArrayCamera( camera ); }; - TSL 节点访问器按数组展开相机矩阵:src/nodes/accessors/Camera.js 中
if ( camera.isArrayCamera && camera.cameras.length > 0 )。
.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
ArrayCameraalways has an array of sub cameras. It's mandatory to define for each sub camera theviewportproperty 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 );
} );
两个容易踩坑的细节:
- viewport 用物理像素。示例中
WIDTH / HEIGHT都乘了window.devicePixelRatio,因为viewport对应的是实际绘制缓冲区的像素坐标,忘记乘devicePixelRatio会导致高 DPI 屏幕上视口只占左上角一小块; - 子相机位置是相对聚合并自行 lookAt 的。父
ArrayCamera的position.z = 3只是聚合体的基准位姿,每个子相机通过自身的position+lookAt( 0, 0, 0 )决定观察方向。
渲染管线内部的完整调用链
把上述分散的证据串起来,一次 renderer.render( scene, arrayCamera ) 的实际流程是:
- 通用渲染器检测到
camera.isArrayCamera,用FrustumArray.setFromArrayCamera为全部子相机缓存视锥(src/renderers/common/Renderer.js); - WebGL 后端执行一次场景遍历
projectObject、物体排序与阴影贴图渲染(src/renderers/WebGLRenderer.js); - 若场景含透射(transmission)对象,会先按子相机逐一遍历透射 pass(L1751-L1761),再渲染背景;
- 循环调用
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.md、src/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,默认 false;true 时走 OVR_multiview2 硬件多视口 |
src/nodes/accessors/Camera.js#L86、src/core/RenderTarget.js#L43-L44 |
| 必选配置 | 每个子相机必须设置 viewport(Vector4,物理像素) |
官方文档、examples/webgl_camera_array.html#L47 |
| 视锥剔除 | FrustumArray 缓存全部子相机视锥,任一相交即可见 |
src/math/FrustumArray.js#L55-L76 |
ArrayCamera 的价值在于把「多视角渲染」从 N 次独立的完整渲染流程,收敛为「一次场景准备 + N 次视口绘制」:VR 双目、分屏预览、多视角调试等场景都可以基于它构建,且只需一次 renderer.render 调用即可完成整帧输出。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
