可见性剔除:遮挡、视锥与 Portal 的层级设计
一、看不见的,就不该画
实时渲染的其一是条铁律是"不可见的物体不渲染"。一个开放场景里可能有十万物件,但玩家同一时刻只能看到其中一小部分。把全部物件都送进管线,是带宽与顶点处理的双重浪费。可见性剔除(Visibility Culling)就是一套把"看不见的"在最早阶段剔掉的层级体系,是大规模场景帧率的基石。
剔除不是单一技术,而是分层递进:先在最粗粒度砍掉整个区域,再逐层细化到单个物体。每一层都面向不同规模的浪费,组合使用才能把绘制调用压到最低。
二、层级剔除的数据流
典型的层级剔除从粗到细依次为:视锥剔除、遮挡剔除、Portal 剔除。这一过程如同一个漏斗,从场景全部物件开始,逐级过滤直至提交绘制的可见集。
视锥剔除最简单:不在相机视野四棱锥内的直接丢弃,遮挡剔除更聪明:被前景大物体完全挡住的也被丢弃。Portal 剔除面向室内:通过门洞(Portal)判断哪些房间可能可见,房间外的直接跳过。三层漏斗逐级收窄可见集。
三、生产级剔除查询与可见集构建实现
下面是一段 C++ 示例,展示如何用八叉树做视锥剔除,并把可见集交给绘制。
#include<vector>#include<cmath>structAABB{floatminx,miny,minz,maxx,maxy,maxz;};structFrustumPlanes{floatp[6][4];};// 6 个平面: ax+by+cz+d// 球体与平面的有符号距离,<=0 表示在背面staticinlinefloatDistToPlane(constfloatp[4],floatx,floaty,floatz){returnp[0]*x+p[1]*y+p[2]*z+p[3];}// 用包围球做保守剔除:球完全在任一平面背面则剔除boolIsVisible(constFrustumPlanes&f,floatcx,floatcy,floatcz,floatr){for(inti=0;i<6;++i){if(DistToPlane(f.p[i],cx,cy,cz)<-r)returnfalse;// 保守:宁可多画不可误剔}returntrue;}classCuller{public:std::vector<int>Cull(constFrustumPlanes&f,conststd::vector<AABB>&boxes){std::vector<int>vis;vis.reserve(boxes.size()/4);for(inti=0;i<(int)boxes.size();++i){auto&b=boxes[i];floatcx=(b.minx+b.maxx)*0.5f,cy=(b.miny+b.maxy)*0.5f,cz=(b.minz+b.maxz)*0.5f;floatr=std::sqrtf((b.maxx-b.minx)*(b.maxx-b.minx)+(b.maxy-b.miny)*(b.maxy-b.miny)+(b.maxz-b.minz)*(b.maxz-b.minz))*0.5f;if(IsVisible(f,cx,cy,cz,r))vis.push_back(i);}returnvis;// 返回可见索引集,交由绘制提交}};这段代码的关键契约:剔除必须保守——用包围球做测试,宁可多画不可误剔。一旦错误剔除了本应可见的物体,玩家会看到物体凭空消失,比多画几帧更不可接受。生产环境应在八叉树或 BVH 上做层级剔除,先砍整棵子树再细化到叶,避免逐个物体裸测;包围体更新需在物体移动时及时重算,否则剔除会基于过期包围盒失效。
在工程落地中,遮挡剔除常采用软硬结合的策略。硬件遮挡查询适合GPU密集型场景,但对室内复杂结构大多力不从心。此时需要软件遮挡体或预计算遮挡信息来补足。门户剔除则专门针对房间与走廊的连通关系,玩家未穿过门就绝不加载门外内容,对室内关卡收益极高。近年GPU驱动剔除把可见性判断下沉到绘制命令本身,由GPU决定哪些绘制调用真正执行。进一步削减了CPU端的剔除开销与提交负担。
四、保守误剔、遮挡精度与 CPU 开销的代价
剔除的最大风险是"误剔":遮挡剔除若用不精确的遮挡体(如用粗糙的盒代替真实遮挡物),可能把本可见的物体判为被挡。导致穿帮。因此遮挡剔除常需保守的遮挡体或硬件遮挡查询(GPU 回读),但硬件查询本身有延迟与带宽成本,需权衡。
Portal 剔除依赖手工放置的门洞,对自动化大世界不友好,且 Portal 维护是额外美术工作量。CPU 侧剔除本身也有开销:每帧遍历场景做可见性测试,物件极多时这部分 CPU 时间不可忽略。需借助空间索引(八叉树、BVH)把复杂度从 O(N) 降到接近 O(log N)。收尾是"过度剔除"导致远处弹出的问题:剔除太激进,物件在转入视野瞬间才加载,会有明显 pop-in,需用淡入或预加载缓冲。
所以落地建议:剔除必须保守、用空间索引加速、遮挡用保守体或硬件查询;Portal 配合手工放置,远处用预加载缓冲防 pop-in。
五、总结
可见性剔除通过视锥、遮挡、Portal 的层级漏斗把不可见物体在最早期剔除,是大规模场景帧率的基石。其风险是保守不足导致的误剔穿帮、遮挡精度与硬件查询成本的权衡、以及 CPU 侧测试的复杂度。工程落地须坚持保守剔除(宁可多画),借助八叉树或 BVH 把测试复杂度降到对数级,遮挡剔除用保守遮挡体或硬件查询。并用预加载缓冲抑制远处 pop-in。包围体须在物体移动时及时更新,避免基于过期数据失效。