Base-面试问题汇总
1.PreZ带来的优势,同时会有哪些消耗?
Pre-Z(Depth Prepass)先只绘制深度,后续 Base Pass / GBuffer Pass 再使用 ZTest Equal 或 ZTest LEqual。它的核心价值是提前建立稳定的深度缓冲,让被遮挡的片元尽量在执行昂贵的 Fragment Shader 之前被 Early-Z 丢弃。
优势:
- 减少 Fragment Shader 的 Overdraw,材质越复杂、屏幕覆盖越大,收益通常越明显。
- 为 Hi-Z、GPU Occlusion Culling、Decal、SSAO 等后续功能提前提供深度。
- 深度结果与 Base Pass 分离后,后续 Pass 的 Early-Z 行为更稳定。
- 对大量不透明遮挡物、复杂 GBuffer Shader 的场景比较有效。
消耗:
- 同一批物体至少多绘制一次,增加 Draw Call、Command 提交和状态切换。
- 顶点变换、蒙皮、裁剪和光栅化会重复执行,几何压力变大。
- 多一次深度写入和后续读取,会消耗显存带宽。
- AlphaTest 材质必须在 Pre-Z 中执行相同的透明裁剪,否则预写深度会与 Base Pass 不一致;这可能需要采样纹理,削弱“只写深度”的低成本优势。
- 在 TBDR 移动 GPU 上,额外 Pass 可能增加 Binning、Tile Load/Store,甚至导致 Tile 数据提前写回外部显存,因此不一定划算。
是否使用不能只看 Overdraw。场景如果几何极多但 Fragment Shader 很轻,Pre-Z 可能得不偿失;如果像素开销高、遮挡严重,通常更容易获得收益。常见折中是只让大型遮挡物或复杂材质进入 Pre-Z,而不是无脑 Full Prepass。
详细笔记:Base-渲染管线
2.TBDR架构的Binning和Tile过程。HSR是什么?
TBDR(Tile-Based Deferred Rendering)大致分成两个阶段。
第一阶段:Binning。
- 执行顶点处理,得到裁剪空间或屏幕空间三角形。
- 计算每个图元覆盖哪些 Tile。
- 不立刻执行完整像素着色,而是把图元引用、插值所需数据等写入各 Tile 的 Primitive List。
这里的 Deferred 指的是“延迟到 Tile 阶段再光栅化和着色”,不是 Deferred Shading。
第二阶段:Tile Rendering。
- GPU 把一个 Tile 的 Color、Depth、Stencil Attachment 加载或初始化到片上内存。
- 读取该 Tile 的 Primitive List,进行光栅化、深度模板测试和 Fragment Shader。
- Tile 内的中间结果尽量停留在片上高速存储中。
- Tile 完成后才 Resolve / Store 到外部显存。
HSR(Hidden Surface Removal,隐藏面消除)利用 Tile 内完整的几何与深度信息,尽可能在执行 Fragment Shader 前排除最终不可见的片元。它减少 Overdraw 和无效着色,是 TBDR 的重要收益之一。
但不要把 HSR 理解成任何情况下都能完美消除 Overdraw。discard/clip、修改深度、Shader Side Effect、复杂透明混合等情况会让最终可见性必须等 Fragment Shader 执行后才能确定,从而降低 HSR 效率;具体行为也依赖 GPU 架构。
详细笔记:Base-TBDR架构
3.SinglePassFetch是什么?
SinglePassFetch 的底层思路是 Framebuffer Fetch / Subpass Input:在同一个 Render Pass 内,Fragment Shader 直接读取当前像素位置上前一步写入的 Attachment 内容,再继续计算。
在 TBDR GPU 上,这份数据可能仍位于 Tile 的片上内存,因此可以把多个原本需要“写 RT → 再采样 RT”的 Pass 合并,减少中间 Render Texture、外部显存带宽和 Tile Store/Load。
它的关键限制是:
- 一般只能读取当前像素对应的旧值,不能像普通 Texture 一样任意采样邻居像素。
- 前后步骤必须处于兼容的 Render Pass / Subpass 中,并使用相同 Attachment。
- Blit、CopyColor、切换 Render Target 或某些不兼容的 RenderFeature 会打断合并。
- SSAO、Blur、TAA 等需要邻域采样、历史缓冲或不同分辨率的算法通常不能只靠 Fetch 完成。
- Framebuffer Fetch、Vulkan Subpass、Metal Tile Shader 的接口和支持范围不同,需要平台能力检测与回退路径。
它适合当前像素上的颜色分级、简单混合、某些 Deferred Lighting 和多层材质合成,不适合把所有后处理强行塞进一个 Pass。
详细笔记:Base-TBDR架构
Khronos Vulkan Specification:Render Pass
4.遇到的兼容性问题有哪些?如何解决?
这道题最好回答真实项目案例。一个完整案例应该包含:哪个平台出错、根因是什么、如何定位、怎么回退、如何防止再次发生。如果暂时没有特别典型的个人案例,可以从下面几类总结。
Shader 与坐标约定
- DirectX / OpenGL 的 NDC 深度范围、UV 原点、Render Texture Y 翻转不同。
- Reversed-Z、投影矩阵和深度纹理解码不一致。
- 法线贴图绿色通道、切线空间手性不同。
half、float、矩阵行列和编译器优化在不同后端表现不同。
解决方式:优先使用引擎提供的坐标与采样函数;对平台差异集中封装;用纯色、深度、法线等调试视图逐级验证,不在业务 Shader 中到处散落平台宏。
图形 API 与硬件能力
- MRT 数量、纹理格式、MSAA、UAV / RWTexture、Framebuffer Fetch、Indirect Draw 支持不同。
- 移动端不支持某种 HDR 或深度格式,或者格式可创建但不可过滤、不可混合。
- Vulkan、Metal、DX11、DX12 的资源状态、同步和 Render Pass 规则不同。
解决方式:运行前查询 Capability;准备格式降级、Pass 拆分和 CPU Fallback;不要仅根据设备型号硬编码能力。
驱动与编译器
- Shader 在某个驱动编译失败、出现精度错误或被错误优化。
- 未初始化变量、越界访问、资源未绑定在某个平台“碰巧能跑”,换平台后暴露。
解决方式:先保证代码符合规范;保存最小复现;查看目标平台编译产物;使用 RenderDoc、Nsight、Xcode GPU Capture、Arm Performance Studio 等工具;必要时针对明确的驱动问题做小范围黑名单,而不是整个平台关闭功能。
引擎版本与内容资源
- SRP / RenderGraph 接口升级导致自定义 Pass 注入点变化。
- AssetBundle、Shader Variant、纹理压缩格式在不同平台丢失或不匹配。
解决方式:锁定并记录引擎与包版本;建立真机设备矩阵和自动化冒烟测试;把高风险 Feature 做成可远程关闭的开关。
面试时不要只说“加宏解决”。更重要的是说明如何发现差异、如何选择正确回退,以及回退后画质、性能和维护成本的变化。
5.float和half使用场景的区分?
half 通常表示 FP16,float 通常表示 FP32。选择依据不是变量名字,而是数值范围、允许误差、目标 GPU 的原生支持和最终瓶颈。
适合 half 的数据:
- 0~1 范围内的颜色、Mask、Metallic、Roughness。
- 范围受控的 UV、局部坐标和材质参数。
- 普通特效、粒子和允许少量误差的光照中间值。
- 某些归一化方向,但高光和多次运算仍需检查精度。
建议保留 float 的数据:
- 大范围 World Position、View Position 和深度重建。
- 矩阵变换、几何判断、阴影坐标。
- 长循环累加、积分以及误差会逐步放大的计算。
- HDR 高亮值、很大或很小的参数、对稳定性敏感的计算。
half 可能提高 FP16 算术吞吐、减少寄存器压力、提高 Occupancy,但不保证更快。部分桌面 GPU 或 API 会把它提升为 FP32;变量写成 half 也不代表 Constant Buffer、StructuredBuffer 或纹理自动变成 16bit 存储。最终要看目标平台的 Shader ISA、寄存器数和性能计数器。
详细笔记:Base-GPU硬件架构基础
Unity:Use 16-bit precision in shaders
6.DOTS的理解?Jobs和Burst的理解?
DOTS(Data-Oriented Technology Stack)不是单独一个多线程 API,而是一套数据导向技术栈,常见组成包括 ECS、C# Job System、Burst Compiler 和 NativeContainer。
- ECS:按组件数据组织实体。相同 Archetype 的数据以 Chunk 形式紧凑存放,减少对象跳转,提高 Cache Locality,并方便批量遍历。
- Job System:把工作拆成带明确读写依赖的 Job,调度到 Worker Thread。它解决的是多核并行和任务依赖。
- Burst:把受支持的 C# 子集编译为优化后的 Native Code,进行内联、常量折叠、SIMD 向量化等。它解决的是单个 Job 的执行效率。
- NativeContainer:提供可被 Job/Burst 使用的原生内存容器,并附带读写与生命周期安全检查。
一句话可以回答:ECS 负责数据布局,Jobs 负责把工作分给多个核心,Burst 负责让每个核心上的代码跑得更快。
DOTS 适合大量同构实体、批量数学计算和数据依赖清晰的系统,不适合为了“技术先进”重写所有 GameObject 逻辑。任务太小、频繁 Schedule 后立即 Complete、每帧复制 GameObject 数据,收益都可能被调度和同步成本抵消。
7.DrawIndirect的细节,为什么是间接?
普通 Draw Call 的顶点数、索引数、实例数等参数由 CPU 直接写进命令。Indirect Draw 则让 Draw Command 指向一个 GPU Buffer,真正的绘制参数由 GPU Command Processor 在执行时从 Buffer 中读取,所以叫“间接绘制”。
以 Indexed Indirect Draw 为例,参数通常包含:
1 | |
典型 GPU Driven 流程:
1 | |
它减少的是 CPU 回读和逐对象提交。GPU 可以在不知道最终可见数量的情况下,直接把剔除结果交给后续绘制。
需要注意:
- Indirect 不等于自动完成 GPU Culling,剔除和可见列表仍要自己生成。
- Compute 写参数后,Draw 读取前必须有正确的资源状态转换和同步屏障。
- Arguments Buffer 的结构、Stride、对齐和 API 支持必须匹配。
- 可见实例数据要紧凑,否则 Vertex Shader 仍会做大量随机读取。
- Unity 通常仍需要提供一个整体 Bounds,Bounds 过小会被 CPU 侧整批剔除,过大则降低粗粒度剔除效率。
- Indirect 主要降低 CPU Submission 瓶颈;如果瓶颈已经在 Fragment Shader 或带宽,DrawIndirect 不会自动提高帧率。
Unity:Graphics.RenderMeshIndirect
Khronos Vulkan Specification:Drawing Commands
8.RenderGraph的理解?一般项目值得开吗,它最大的消耗在哪里?
RenderGraph 是对一帧 Render Pass 和资源依赖的声明式描述。代码不再手动决定每张临时 RT 什么时候创建、切换和释放,而是声明:
- 这个 Pass 读取什么资源。
- 写入什么资源。
- 资源需要什么格式和用途。
- 哪些结果最终会被后续 Pass 或屏幕使用。
RenderGraph 根据依赖关系建立 DAG,并有机会自动完成:
- 剔除结果无人使用的 Pass。
- 计算临时资源生命周期并进行内存复用 / Aliasing。
- 插入资源状态转换和同步。
- 合并兼容的 Native Render Pass / Subpass。
- 减少移动端不必要的 Attachment Load / Store。
一般项目值得使用吗?
Unity 6 的新 URP 项目通常值得使用,也是管线发展的主要方向。它对 Pass 多、临时 RT 多、跨平台以及 TBDR 带宽敏感的项目更有价值。
老项目则不要只为了“开一个选项”强行迁移。大量自定义 Renderer Feature、依赖全局状态或隐式执行顺序的代码需要重构,应该先迁移高带宽、高维护成本的部分,再通过 Frame Debugger 和真机 Profiler 验证。
最大消耗在哪里?
RenderGraph 本身会有 CPU 侧建图、依赖分析、资源生命周期计算和命令记录成本,但通常不是 GPU 大头。更现实的成本是:
- 接入与维护成本:旧 Pass 要改成显式资源声明,不能再依赖隐藏的全局状态。
- 错误的 Pass 设计:频繁拆分 Pass、强制全局纹理、读取上一阶段结果,会阻止 Pass Culling、资源复用和 Native Pass 合并。
- Transient Resource 峰值:资源生命周期重叠过多时仍会产生很高显存峰值;RenderGraph 只能看见并优化正确声明出来的依赖。
- 调试复杂度:资源可能被复用或 Pass 被裁掉,需要借助 RenderGraph Viewer、Frame Debugger 和 GPU Capture 理解最终执行结果。
所以它不是“开了就免费变快”,而是让引擎有足够信息进行全局调度。写法不符合 RenderGraph 思路时,收益会很有限。
Unity:Introduction to the render graph system in URP
9.PBR整体理解。Diffuse为什么要除以Pi,Specular为什么不除以Pi?
PBR 的核心不是某个固定 Shader,而是在统一材质模型中满足能量守恒、菲涅尔效应和微表面统计规律。实时渲染通常把反射分成 Diffuse 与 Cook-Torrance Specular:
1 | |
Diffuse 为什么除以 π?
Lambert 假设漫反射向所有观察方向均匀分布。如果 BRDF 直接写 BaseColor,在整个半球积分后会多出:
1 | |
因此使用 BaseColor / π,半球积分后总反射能量才回到 BaseColor,不会凭空增加能量。
Specular 为什么不再统一除以 π?
Cook-Torrance 镜面项已经通过 D 的归一化、几何投影关系以及分母 4(N·L)(N·V) 完成能量与面积变换,不能再机械地整体除一次 π。
这不代表 Specular 公式里永远没有 π。例如 GGX 的 D 项本身通常包含 π:
1 | |
所以准确说法是:Lambert 的 1/π 是半球归一化;微表面 Specular 的归一化已经分布在 D、G 和几何变换中,不额外统一除以 π。
详细笔记:Base-PBR
Google Filament:Physically Based Rendering
10.菲涅尔的基础反射率是什么?(PBR里有说明)
基础反射率 F0 表示光线垂直入射,也就是观察方向接近法线方向时,表面的镜面反射比例。Schlick 菲涅尔近似为:
1 | |
对于非金属,如果光从折射率约为 1 的空气进入折射率为 n 的材质:
1 | |
普通塑料、玻璃等介质的折射率常在 1.3~1.6 左右,对应 F0 大约 0.02~0.06。实时渲染经常用 0.04 作为普通非金属的默认值。
金属的情况不同。它的折射率包含复数部分,而且随波长变化,所以 F0 较高并带有颜色;Metallic 工作流通常使用 Base Color 表示金属的有色 F0,同时把金属 Diffuse 压到接近 0。
随着观察角度接近掠射角,大多数材质的菲涅尔反射会趋近 1。F0 不是“高光亮度滑块”,而是材质在法线入射时的物理反射属性。
详细笔记:Base-PBR
11.TAA原理。为什么要转换颜色空间?为什么要采样镜头周围更近的MotionVector?
TAA(Temporal Anti-Aliasing)通过多帧积累增加采样数。典型流程是:
- 每帧给投影矩阵加入亚像素 Jitter,让同一像素在不同帧覆盖不同采样位置。
- 当前帧正常渲染颜色、深度和 Motion Vector。
- 根据 Motion Vector 把当前像素重投影到上一帧,采样 History Color。
- 用当前帧邻域颜色构造合法范围,对 History 做 Clamp / Clip,拒绝不可信历史。
- 根据运动、遮挡、亮度变化等计算权重,混合 Current 与 History。
难点不在“把两帧 lerp”,而在于判断历史样本是否仍对应当前表面。遮挡显露、透明物、粒子、反射、动画和 Motion Vector 缺失都会导致 Ghosting。
为什么转换颜色空间?
很多实现会把 RGB 转到 YCoCg 等亮度/色度空间,再做邻域包围盒和 History Clipping。原因是 RGB 三个通道相关性较强,轴对齐的 RGB Min/Max Box 对颜色分布拟合较差;拆分亮度和色度后,异常历史颜色更容易被限制,同时减少色相偏移。
另一类实现会在 Tone Mapping 前后使用可逆的亮度压缩,让 HDR 极亮像素不会在历史混合中占据过大权重。颜色空间转换不是 TAA 的硬性步骤,具体选择服务于更稳定的邻域统计和历史权重。
为什么从周围像素选择更近深度对应的 Motion Vector?
在物体边缘,一个像素及其邻域可能同时包含前景和背景。若直接使用中心像素的背景 Motion Vector,重投影可能把背景 History 拉到前景上,形成拖影。
常见做法是在当前像素周围检查深度,选择离相机最近表面的 Motion Vector。前景通常是遮挡关系的主导者,用它重投影更不容易把已被遮挡的背景历史带到前面。但这只是一种边缘启发式,细线、粒子、透明物和快速显露区域仍需要 Disocclusion 检测与 History Rejection。
Epic:Anti-Aliasing and Upscaling in Unreal Engine
Playdead:Temporal Reprojection Anti-Aliasing
12.FS的数据转移到VS,会起到什么优化作用?
主要收益来自降低计算频率。Fragment Shader 的执行次数通常接近屏幕覆盖片元数,并且会受到 Overdraw、MSAA、Helper Lane 等影响;Vertex Shader 通常每个提交顶点执行一次。如果某个结果可以在顶点上计算,再插值到片元,就能把大量逐片元 ALU 转成少量逐顶点 ALU。
例如一个模型有 1 万个顶点,却覆盖 100 万个片元,把几条适合插值的计算从 FS 移到 VS,理论执行次数可能下降两个数量级。
适合迁移的内容:
- 线性或变化平缓的数据,例如部分 UV 变换、顶点颜色、简单光照参数。
- 与每个三角形内部位置近似线性相关的中间量。
- 对精度要求不高、模型顶点密度足够的效果。
不适合直接迁移的内容:
- 高光、归一化、幂函数等强非线性结果,因为
interpolate(f(x))通常不等于f(interpolate(x))。 - 依赖屏幕导数
ddx/ddy或隐式 Texture LOD 的采样。VS 没有普通 FS Quad 导数,通常只能显式SampleLevel。 - 高频法线、逐像素阴影、屏幕空间效果。
- 低模上需要精细逐像素变化的计算。
代价是增加 VS 输出和插值器占用,可能提高参数带宽;顶点非常密集、片元很少时也未必划算。常见折中是:在 VS 计算便宜且可插值的中间量,在 FS 重新归一化并完成非线性部分,而不是把整个光照结果搬到 VS。
详细笔记:Base-渲染管线
详细笔记:Base-GPU硬件架构基础
13.GPU架构。L1、L2Cache、寄存器、都是什么?
不同厂商命名和层级不完全一致,但可以用“离执行单元越近,容量通常越小、延迟越低”理解。
1 | |
Register
寄存器保存每个 Shader Thread 的局部变量,是最靠近 ALU 的存储,延迟最低。它不是普通 Cache,也通常不能由程序像数组一样随意管理。
寄存器总量由一个 SM / CU 内的所有活跃 Wave 共享。单线程使用过多寄存器会降低同时驻留的 Warp/Wave 数量,也就是降低 Occupancy;严重时变量还可能 Spill 到更慢的 Local Memory。
L1 Cache
L1 通常位于每个 SM / CU 附近,服务该计算单元上的线程,容量较小、速度较快。不同架构可能把 L1 Data Cache、Texture Cache、Shared Memory 的部分硬件组合或统一,因此不能把某一代 NVIDIA 的结构直接套到 Mali、Adreno 或 AMD 上。
它主要缓存近期访问的数据。连续访问、相邻线程访问相邻地址更容易合并请求并提高命中率;随机访问会造成更多 Cache Miss。
L2 Cache
L2 通常由整个 GPU 的多个 SM / CU 共享,容量比 L1 大,延迟也更高,是片上缓存与外部显存之间的重要中间层。不同计算单元交换数据、Render Target、Texture 和 Buffer 访问都可能经过 L2,但一致性和具体路径由架构与 API 决定。
Shared Memory / LDS 与 Texture Cache
- Shared Memory / LDS 是程序显式管理的 Workgroup 片上存储,适合线程组内数据复用。
- Texture Cache 针对纹理的空间局部性、过滤和特定访问模式优化。
- Constant / Uniform Cache 针对一个 Wave 中多个线程读取相同常量优化。
GPU 隐藏内存延迟不只靠 Cache,还会在一个 Warp/Wave 等待纹理或显存时切换到其他可执行 Warp/Wave。因此性能取决于 Cache 命中率、访存合并、寄存器压力和 Occupancy 的共同作用。
详细笔记:Base-GPU硬件架构基础