three.js FrustumArray 指南:面向 ArrayCamera 多视角渲染的“任意相机可见”视锥剔除
视锥剔除(frustum culling)是 three.js 加速渲染的基石:摄像机视锥外的物体会被跳过,避免无谓的绘制开销。而 FrustumArray 正是把这一机制从“单摄像机”推广到“多摄像机”的关键工具——它为 ArrayCamera 中每一台子相机各缓存一个视锥,只要物体被其中任意一台相机看到即视为可见。本文基于 FrustumArray.html.md 的 API 文档,结合源码与渲染器内部调用链,完整讲解构造、属性、方法与底层原理,并给出可用于多视角/多窗口场景的实战剔除方案。
为什么要引入 FrustumArray:单视锥在多视角场景中的局限
普通 Frustum 只封装一套由 6 个平面(Plane)构成的视锥,用于判断物体是否落在“一台相机”的视野内。但在 VR、多窗口拼接屏、分屏/画中画等场景中,three.js 使用 ArrayCamera——它是 PerspectiveCamera 的子类,内部维护一个子相机数组:
this.isArrayCamera = true; // 类型标识,见 src/cameras/ArrayCamera.js
this.cameras = array; // 子相机列表(通常每个子相机带 viewport 决定渲染的屏幕区域)
单台子相机的视锥无法覆盖整个 ArrayCamera:一个物体可能不在左眼的视锥里,却在右眼的视锥里。若只用某一台子相机的视锥做剔除,就会出现“被错误裁剪掉”的可见物体。因此需要“对数组中的每一台相机各算一个视锥,只要命中任意一个就保留”的判定逻辑——这正是 FrustumArray 的定位:
FrustumArray is used to determine if an object is visible in at least one camera from an array of cameras. This is particularly useful for multi-view renderers.
构造函数与内部存储
const fa = new THREE.FrustumArray();
从 src/math/FrustumArray.js 的源码(第 11-47 行)可以看到,构造时除了初始化一个公开属性外,还会建立两块“私有”存储,用于运行时缓存视锥:
this._frustums = []:Frustum实例的对象池。当连续渲染不同子相机数量的ArrayCamera时,多余的空闲实例会被保留复用,避免频繁重新分配(源码注释明确指出该设计意图);this._count = 0:当前实际使用的视锥数量(即本次渲染中子相机的个数)。
this._frustums = []; // Frustum 对象池,可多于正在使用的数量
this._count = 0; // 当前正在使用的视锥个数
这两个字段在文档中标注为 private(
_前缀),但copy()/clone()等内部方法会读取它们,属于实现层面的细节,不建议外部直接操作。
属性:.coordinateSystem
.coordinateSystem : WebGLCoordinateSystem | WebGPUCoordinateSystem
- 含义:本次视锥提取所采用的坐标系统;
- 默认值:
WebGLCoordinateSystem。
它和单视锥 Frustum 的坐标系统参数一一对应,作用是在“从投影矩阵反解视锥平面”时决定近裁剪面(near plane)的取法——见下文 setFromArrayCamera 一节。两套坐标系统分别对应 WebGL 渲染器与 WebGPU 渲染器(后者的 NDC 深度范围为 [0, 1],与 WebGL 的 [-1, 1] 不同)。
方法详解
.setFromArrayCamera( cameraArray : ArrayCamera ) : FrustumArray
这是 FrustumArray 的“输入口”与核心方法,语义为:对给定数组相机的每一台子相机,计算并缓存一个视锥。文档特别强调:渲染期间,在调用各类相交判断方法之前,每帧必须先调用一次本方法。
源码实现(src/math/FrustumArray.js):
setFromArrayCamera( cameraArray ) {
const cameras = cameraArray.cameras;
const frustums = this._frustums;
for ( let i = 0; i < cameras.length; i ++ ) {
const camera = cameras[ i ];
_projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
if ( frustums[ i ] === undefined ) frustums[ i ] = new Frustum();
frustums[ i ].setFromProjectionMatrix( _projScreenMatrix, camera.coordinateSystem, camera.reversedDepth );
}
this._count = cameras.length;
return this;
}
实现要点:
- 复用模块级临时矩阵
_projScreenMatrix,对每台子相机求projectionMatrix × matrixWorldInverse,得到“投影-视图”合成矩阵; - 对象池中对应槽位不存在时按需
new Frustum(),否则复用旧实例; - 把合成矩阵连同子相机自身的
coordinateSystem、reversedDepth传给Frustum.setFromProjectionMatrix()生成 6 个平面; - 最后把
_count同步为子相机数量,链式返回this。
注意最后一步“截断”逻辑:若本次传入的子相机少于上次,多出来的旧视锥并不会被清除,只是不再参与判定(循环只走到 _count)。这也是文档中所有相交方法“只检查 this._count 个缓存视锥”的原因。
坐标系统与 reversedDepth 到底影响什么? 视线锥平面的推导方法 src/math/Frustum.js:左右上下四个侧面(planes[0..3])通过投影矩阵元素的加/减组合提取;而近/远平面(planes[4]、planes[5])会随参数变化:
- 启用
reversedDepth(反向深度缓冲)时,near/far 平面的构造顺序对调; - 未启用时,near 平面的构造在
WebGLCoordinateSystem与WebGPUCoordinateSystem下使用不同的矩阵元素组合(第 118-124 行),若传入未知坐标系统则抛出THREE.Frustum.setFromProjectionMatrix(): Invalid coordinate system: ...。
FrustumArray 本身不做这些推导,它只是把每台子相机自己的设定原样透传给底层的 Frustum。因此 WebGPU 渲染路径下应把 coordinateSystem 设置为 WebGPUCoordinateSystem,而 reversedDepth 则取决于子相机是否启用了反向深度。
相交判断方法族:命中“任一”子视锥即返回 true
以下方法语义完全一致,只是被测试的几何体类型不同:遍历全部缓存子视锥,只要任一子视锥命中即返回 true,全部未命中才返回 false。文档均注明前置条件:必须先 setFromArrayCamera。
| 方法 | 参数 | 判定对象 | 典型用途 |
|---|---|---|---|
.containsPoint( point : Vector3 ) |
空间点 | 点是否落在任一视锥内部 | 最小粒度可见性判断 |
.intersectsSphere( sphere : Sphere ) |
包围球 | 球是否与任一视锥相交 | 通用包围体剔除 |
.intersectsBox( box : Box3 ) |
轴对齐包围盒 | 盒是否与任一视锥相交 | 八叉树/BVH 剔除 |
.intersectsObject( object : Object3D ) |
3D 物体 | 物体包围球是否与任一视锥相交 | 物体级可见性(默认路径) |
.intersectsSprite( sprite : Sprite ) |
精灵 | 精灵是否与任一视锥相交 | 精灵剔除 |
它们的实现结构高度统一,以 intersectsObject 为例(src/math/FrustumArray.js):
intersectsObject( object ) {
const frustums = this._frustums;
for ( let i = 0; i < this._count; i ++ ) {
if ( frustums[ i ].intersectsObject( object ) ) return true;
}
return false;
}
需要澄清的一个语义细节:intersectsObject 实际上委托给各子 Frustum 的同名方法,而单个 Frustum.intersectsObject 是用物体的 boundingSphere(包围球)做测试的,因此文档表述为“3D object’s bounding sphere”。若想使用精确包围盒,则应调用 intersectsBox。
.copy( frustumArray : FrustumArray ) : FrustumArray 与 .clone() : FrustumArray
copy():把源实例的coordinateSystem、_frustums中“正在使用的部分”(到源_count为止)逐平面拷贝到当前实例,并同步_count,链式返回自身(src/math/FrustumArray.js)。它同样会按需扩充对象池,但不会拷贝源对象池中多余的空闲实例;clone():内部即new FrustumArray().copy( this )(第 220-224 行)。
两者组合可用于“保存某次渲染的视锥快照”,供后续离线判定使用。
渲染器内部调用链:FrustumArray 如何服务于多视角渲染
FrustumArray 在 three.js 中的真实使用场景集中在渲染管线的可见性收集(visibility culling)阶段。以 WebGL/WebGPU 共用的 src/renderers/common/Renderer.js 为例:
第 1 步——每帧准备视锥(第 977-987 行):
_projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
if ( camera.isArrayCamera ) {
_frustumArray.setFromArrayCamera( camera );
} else {
_frustum.setFromProjectionMatrix( _projScreenMatrix, camera.coordinateSystem, camera.reversedDepth );
}
也就是说:渲染器根据 camera.isArrayCamera 标志自动分流——普通相机走单个 _frustum,ArrayCamera 则走模块级复用的 _frustumArray(第 45 行 const _frustumArray = /*@__PURE__*/ new FrustumArray())。
第 2 步——逐物体判定(第 3271-3299 行):
对 Sprite 与 Mesh / Line / Points 分支,渲染器统一取
const frustum = camera.isArrayCamera ? _frustumArray : _frustum;
if ( ! object.frustumCulled || object.intersectsFrustum( frustum ) ) { ... }
其中 object.intersectsFrustum( frustum ) 的方法签名明确接受 Frustum | FrustumArray 两种类型(src/core/Object3D.js),并在 Mesh/Line/Points/Sprite 等子类中被覆写,分别派发到 intersectsObject(或精灵路径的 intersectsSphere/精灵专属逻辑)——最终汇入上面这套“命中任意子视锥”的判定。
同样的模式也出现在实例化渲染路径中:src/objects/BatchedMesh.js 在 isArrayCamera 时先 frustum.setFromArrayCamera( camera ),再对每个实例做剔除,实现多视角下的实例级可见性管理。
可见:使用 ArrayCamera 渲染时,默认的 frustumCulled 剔除就已经自动基于 FrustumArray 工作,开发者通常无需手动触碰该类。
手动使用示例:多视角下的可见性查询
尽管渲染器默认路径已覆盖 ArrayCamera 的标准渲染流程,但当你想在渲染之外做自己的可见性决策(例如多窗口 HUD 元素是否绘制、自定义 LOD 切换、拼接屏分区预加载、把 BatchedMesh 中某些实例按视角启停),就需要手动操作 FrustumArray。下面给出一个可直接运行的参考流程:
import * as THREE from 'three';
// 1) 构造 ArrayCamera,并为其每台子相机指定 viewport(决定渲染区域)
const sub1 = new THREE.PerspectiveCamera( 60, 1.0, 0.1, 1000 );
const sub2 = new THREE.PerspectiveCamera( 60, 1.0, 0.1, 1000 );
sub1.viewport = new THREE.Vector4( 0, 0, 960, 1080 ); // 左半屏
sub2.viewport = new THREE.Vector4( 960, 0, 960, 1080 ); // 右半屏
const cameraArray = new THREE.ArrayCamera( [ sub1, sub2 ] );
// 2) 每帧开始:确保矩阵最新,再一次性构建全部子视锥
scene.updateMatrixWorld();
cameraArray.updateMatrixWorld();
const frustumArray = new THREE.FrustumArray();
frustumArray.setFromArrayCamera( cameraArray );
// 3) 之后可按需做“任意相机可见”判断,无需重新 setFromArrayCamera
if ( frustumArray.intersectsObject( someMesh ) ) {
// someMesh 至少被 sub1 或 sub2 中的一台看到
}
if ( frustumArray.intersectsBox( regionBox3 ) ) {
// 该包围盒区域进入了某台子相机视野
}
if ( frustumArray.containsPoint( worldPoint ) ) {
// 世界坐标点位于某个子视锥内部
}
使用时要特别注意文档反复强调的约定:
setFromArrayCamera每帧只调用一次(放在所有相交查询之前),它缓存的是“这一帧”子相机的视锥状态。若相机矩阵中途变化而未重新调用,查询结果是过期状态;- 查询方法逐个遍历,时间复杂度与子相机数量成正比,这点在多子相机(如分屏几十个视口)场景下值得留意;
- 对象池会保留历史分配,因此同一个
FrustumArray实例跨帧复用更省内存;若使用 WebGPU 渲染,请记得把coordinateSystem设为WebGPUCoordinateSystem以获得正确的近平面提取。
源码与测试位置指引
- API 文档原文:docs/pages/FrustumArray.html.md(HTML 渲染版见 docs/pages/FrustumArray.html)
- 核心实现:src/math/FrustumArray.js
- 底层单视锥与平面提取算法:src/math/Frustum.js
- 多相机容器类型:src/cameras/ArrayCamera.js
- 渲染器内部的自动分流与调用:src/renderers/common/Renderer.js
intersectsFrustum的可接收类型声明:src/core/Object3D.js- 实例化渲染中的运用:src/objects/BatchedMesh.js
- 坐标系统常量(
WebGLCoordinateSystem/WebGPUCoordinateSystem):src/constants.js
从当前仓库的测试目录看,单元测试集中在单个视锥的 test/unit/src/math/Frustum.tests.js(覆盖 setFromProjectionMatrix、containsPoint、intersectsSphere 等),而 FrustumArray 作为其上的“多相机包装层”,没有独立测试文件——其正确性主要由渲染器多相机路径的行为保证,可据此推断:若要在未来补充测试,最直接的验证方式是构造含两台已知视锥的 ArrayCamera,断言物体位于其一之内时的相交结果。总体而言,理解 FrustumArray 就等于理解了 three.js 多视角可见性判定的全部要点:一次构建、按“任一命中”聚合、复用对象池以支撑高性能多相机渲染。
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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00