首页
/ three.js FrustumArray 指南:面向 ArrayCamera 多视角渲染的“任意相机可见”视锥剔除

three.js FrustumArray 指南:面向 ArrayCamera 多视角渲染的“任意相机可见”视锥剔除

2026-09-06 19:12:52作者:廉彬冶Miranda

视锥剔除(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;

}

实现要点:

  1. 复用模块级临时矩阵 _projScreenMatrix,对每台子相机求 projectionMatrix × matrixWorldInverse,得到“投影-视图”合成矩阵;
  2. 对象池中对应槽位不存在时按需 new Frustum(),否则复用旧实例;
  3. 把合成矩阵连同子相机自身的 coordinateSystemreversedDepth 传给 Frustum.setFromProjectionMatrix() 生成 6 个平面;
  4. 最后把 _count 同步为子相机数量,链式返回 this

注意最后一步“截断”逻辑:若本次传入的子相机少于上次,多出来的旧视锥并不会被清除,只是不再参与判定(循环只走到 _count)。这也是文档中所有相交方法“只检查 this._count 个缓存视锥”的原因。

坐标系统与 reversedDepth 到底影响什么? 视线锥平面的推导方法 src/math/Frustum.js:左右上下四个侧面(planes[0..3])通过投影矩阵元素的加/减组合提取;而近/远平面(planes[4]、planes[5])会随参数变化:

  • 启用 reversedDepth(反向深度缓冲)时,near/far 平面的构造顺序对调;
  • 未启用时,near 平面的构造在 WebGLCoordinateSystemWebGPUCoordinateSystem 下使用不同的矩阵元素组合(第 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 标志自动分流——普通相机走单个 _frustumArrayCamera 则走模块级复用的 _frustumArray(第 45 行 const _frustumArray = /*@__PURE__*/ new FrustumArray())。

第 2 步——逐物体判定(第 3271-3299 行):

SpriteMesh / 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.jsisArrayCamera 时先 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 以获得正确的近平面提取。

源码与测试位置指引

从当前仓库的测试目录看,单元测试集中在单个视锥的 test/unit/src/math/Frustum.tests.js(覆盖 setFromProjectionMatrixcontainsPointintersectsSphere 等),而 FrustumArray 作为其上的“多相机包装层”,没有独立测试文件——其正确性主要由渲染器多相机路径的行为保证,可据此推断:若要在未来补充测试,最直接的验证方式是构造含两台已知视锥的 ArrayCamera,断言物体位于其一之内时的相交结果。总体而言,理解 FrustumArray 就等于理解了 three.js 多视角可见性判定的全部要点:一次构建、按“任一命中”聚合、复用对象池以支撑高性能多相机渲染。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391