three.js ArrayCamera 深度解析:用一组预定义相机高效渲染多视角与 VR 场景
2026/9/7 14:08:44 网站建设 项目流程

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

【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js

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 ofArrayCameraalways 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 ); } );

两个容易踩坑的细节:

  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 → ArrayCameradocs/pages/ArrayCamera.html.md、src/cameras/ArrayCamera.js
构造参数array : Array.<PerspectiveCamera>,默认[]src/cameras/ArrayCamera.js#L21
.cameras子相机数组,渲染时被逐个绘制src/renderers/WebGLRenderer.js#L1765-L1771
.isArrayCamerareadonly,默认true,管线分支判定标志src/cameras/ArrayCamera.js#L32
.isMultiViewCamerareadonly,默认falsetrue时走OVR_multiview2硬件多视口src/nodes/accessors/Camera.js#L86、src/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调用即可完成整帧输出。

【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询