跳到主要内容
腾讯题库 · Q11

原题 Q11 · 当前收录 10 / 49

学习状态 未开始

软遮挡与可见性

Shader occlusion · shader 对应课程:Shader 与材质

面试官问

PDF 第 26 页

如何在 Shader 中实现“动态遮挡剔除”(Soft Occlusion Culling)?请说明其与传统遮挡查询的区别。(冷门题,理清逻辑)

学习进度

0/9 已勾

只保存在这台浏览器;勾满 9 项即视为本题完成。

先把题目说成人话

一张半透明烟雾片插进地面时,交界处会像被剪刀硬切开;一个任务标记缓慢走到墙后时,产品又可能希望整块标记一起退场;等它稳定地完全不可见,性能目标还要求不再提交绘制。这三件事都表现为“看不见”,却分别发生在片元交界、整个对象的过渡状态和提交前的可见性清单,不能共用一条深度差公式冒充全部答案。

“软遮挡剔除”并不是含义唯一的标准术语(Soft Occlusion Culling),必须先澄清题目具体指什么。用同一屏幕位置的前后表面距离柔化相交边界叫深度淡出(Depth Fade);让一个对象共用同一可见权重叫整物体遮挡渐隐(Object Occlusion Fade);提交绘制前保留哪些对象的清单叫可见列表(Visibility List)。透明度只是画面权重(Alpha),本身不等于剔除。

这是虚构教学故事,不是个人项目经历。

场景:先让问题变得看得见

同一镜头里放两个测试物:左边的烟雾片穿过地面,右边的任务标记从墙边移动到墙后。左边要消掉相交硬缝,右边要让整块标记平稳退场;标记完全藏稳以后,还要确认绘制命令真的消失。

(右脑)画面:把镜头想成一张有前后层次的舞台。左边,薄烟穿进地板,每一小片烟只需测量自己离地板交界还有多远,越靠近接缝越透明;所以同一张烟片上有的地方清楚、有的地方淡。右边,一整块路牌绕到墙后,观众需要看到路牌整体按一个旋钮退场,不能让每一小块各自算远近而碎成斑驳切片。路牌完全藏稳后,仓库管理员才把整件道具从搬运清单划掉。接缝柔化、整件渐隐和停止搬运是三道相邻却独立的门。

(左脑)对表:烟雾交界路径逐片元比较 scene depth 与 particle fragment depth,得到每片元 Alpha;整物体路径先由包围体或遮挡查询获得对象级可见证据,再把一个时间连续的权重传给整个 Renderer;真正剔除则在提交前从可见列表移除对象或实例。标准深度测试若先拒绝墙后片元,片元 Shader 没有机会执行透墙渐隐,因此这种效果需要专用 Pass 与明确深度状态。两份深度须按投影和 reversed-Z 规则线性化到同一视图空间长度,fade distance 使用相同单位;对象级证据则要注明包围体、相机、来源帧与保守偏置。Alpha 下降是画面事实,Draw 和顶点工作是否消失必须由 Frame Debugger 与 Profiler 证明。

小明:先形成一个完整猜测

小明把烟雾和任务标记都乘上同一份片元深度差。烟雾与地面的硬缝确实柔和了,于是他以为整套方案已经成立。

把标记移到墙后,反例立刻出现:各片元只知道自己与当前场景深度的差,可能在墙边形成局部切片,却不能天然给整个标记一个统一的遮挡比例。即使透明度接近零,Draw Call、顶点处理和很多片元工作仍可能发生。这个公式能解决片元交界,不是整物体状态机,也不是性能剔除。

小红:把猜测放回现实检查

小红先单独验收烟雾交界:场景深度与烟雾片元深度线性化到同一视图空间长度后再求差,反向 Z、投影类型和深度纹理可用性都按当前管线处理;过渡距离用明确的世界或视图长度单位。这个结果只能声明“交界淡出正确”。

再验收整块标记:她用对象包围体得到可见性证据,把结果经过进入、退出时间与迟滞处理后写成全对象共享权重;只有权重已经退到不可见、查询也连续稳定时,才从列表移除。这样墙边过渡不会碎成逐片元切片,也不会在一次不确定查询后误杀对象。

若证据来自 CullingGroup,包围球会参与相机视锥和 Unity 已烘焙的静态遮挡判断;动态物体可以成为被测目标,却不会因此自动成为遮挡物。相机完成本次剔除前查询到的是最近一次已完成的状态,业务必须按可能滞后一帧来设计,不能把它当同步的当前片元答案。

若动态遮挡物必须参与,则可评估 Hi-Z:先用前一帧深度金字塔,或先绘制当前帧遮挡物的深度预通过再建金字塔,然后才测试候选包围体。屏幕包围范围、金字塔层级、近深度、偏置和历史确认都要保守;来源顺序不清或只测包围体中心,都可能把本来可见的对象错误剔除。

我:用检查结果修正模型

我把方案拆成三条不互相冒名的数据链:

  • 片元交界层:同空间深度差产生 0–1 的逐片元权重,只给 soft particle、贴花或相交表面柔化硬缝。
  • 整物体过渡层:对象级可见证据经过时间平滑与迟滞,生成全 Renderer 或全实例共享权重,让标记作为整体渐隐。
  • 提交剔除层:对象稳定不可见且已经退出过渡后,由 CullingGroup、BatchRendererGroup 或 Compute/Indirect 流程移出绘制列表,才解决 Draw、顶点与后续片元成本。

三层可以协作,但证据、空间和时序各自验收。只需要粒子交界柔边时就明确叫 depth fade 或 soft particle;只做整物体渐隐时也不能声称构建了遮挡剔除。

(右脑)画面修正:小明的一把软尺放到烟雾与地板接缝处很合适,搬到整块标记上却把它切成深浅不一的碎片;小红因此给路牌另装一个全体共用的退场旋钮,并把仓库门留在最后。画面先用对象级遮挡证据平稳转动旋钮,连续确认整件道具藏稳后,仓库门才停止搬运;若查询只是暂时不确定,就继续保守显示。三道门各留下自己的证据,既不会把柔边误叫剔除,也不会为了省一次绘制让本来可见的标记突然丢失。

(左脑)严格对表:片元交界层先把 scene depth 与 particle fragment depth 线性化,计算同单位 depthGap,用有界 smoothstep(0, fadeDistance, depthGap) 得到逐片元 visibility,再选择 Blend、dither 或 clip。整物体层不复用这份局部差值,而是把包围体可见结果或保守遮挡覆盖证据转成一个有时间滤波、迟滞和回退策略的对象权重。CullingGroup 路径记录包围球、相机、静态遮挡烘焙与上一完成状态;Hi-Z 路径记录深度来自前一帧还是当前帧遮挡物预通过,并对投影包围范围、层级和偏置做保守测试。对象只有在整物体渐隐完成且不可见证据连续稳定后才从列表移除。验收分别记录交界、整物体连续性、误杀/迟滞以及 Draw 和实例数;只通过 Alpha 截图不能宣称性能剔除完成。

老师解惑

若题目指粒子与场景交界的深度淡出,核心关系可以写成伪 HLSL;它不是整物体遮挡渐隐公式:

float sceneEyeDepth = LinearEyeDepth(sceneRawDepth);
float fragEyeDepth  = inputEyeDepth;
float depthGap      = sceneEyeDepth - fragEyeDepth;
float visibility    = smoothstep(0.0, fadeDistance, depthGap);

color.a *= visibility;

具体的深度采样与线性化函数必须换成当前 Unity 管线提供的 API。

  • 软隐藏:改变 Alpha、dither 或 clip 阈值;它解决过渡,不保证减少提交。
  • 整物体遮挡渐隐:以对象级可见证据驱动一份共享权重;逐片元深度差只能形成局部交界,不能自动代表整个对象被遮挡多少。
  • CullingGroup:按包围球给出相机视锥和已烘焙静态遮挡下的可见状态;动态物体不是由它自动纳入的遮挡物,查询时还要处理最近完成结果的时序。
  • 传统遮挡查询:硬件查询通常先提交包围体或代理几何,再异步返回通过深度测试的样本信息;读取结果可能滞后,必须处理上一帧结果与可见性切换。它只是可见性证据来源之一,不等同于 Unity 烘焙遮挡、CPU 包围体测试或 Hi-Z 剔除的全部路径。
  • GPU Hi-Z 剔除:用前一帧深度金字塔,或当前帧遮挡物深度预通过后生成的金字塔,保守测试实例投影包围体,再把可见实例写入 Indirect 参数;这是 Compute/绘制流程,不是一个普通片元 Shader 单独完成的。
  • 时间延迟:Occlusion Query 读取结果可能滞后;软过渡可以遮住状态切换,但不能篡改可见性判断。

工具分工

  • Unity Frame Debugger:确认被“淡没”的对象是否仍有 Draw。
  • Unity Profiler(CPU/GPU Usage):分别检查提交、顶点和片元成本是否真的下降。
  • Scene Depth Debug:核对深度是否在线性化后的同一空间比较。
  • RenderDoc:检查深度输入、透明覆盖和 clip/Blend 状态。

记忆钩子

先问“软”是柔和消失还是实际不画:Alpha 能藏住画面,只有可见列表才能真正省下绘制。

这是教学场景,不是目标项目的真实捕获或个人经历。

下一步:验证这条因果

Shader 内的片元淡出仍保留对象提交;提交前整物体剔除才会让后续阶段消失。

教学边界:网页只画阶段分账,不替代 Unity Occlusion Culling、Frame Debugger 或目标设备捕获。

进入教学 Lab:「先看清什么在消失,再决定怎样让它消失」