腾娱UE客户端开发一二面汇总
- 轮次
- 一面+二面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
一面(图形学)
- VAO、VBO、EBO 分别是什么?
- 讲一下渲染管线。
- 深度测试是怎么做的?
- Phong 和 Blinn-Phong 有什么区别?
- PBR 和 Cook-Torrance 的核心是什么?
- Shadow Map 是如何生成的?
- PCF 是怎么做的?
- SSAO 的原理是什么?
- 还知道哪些 AO?
- MSAA 是什么原理?和 SSAA 相比呢?
- 各向异性过滤是做什么的?
- 还了解过其它哪些渲染知识?
- 项目中最有成就感的部分是什么?
- C++ 的智能指针和 UE 标准库的智能指针有区别吗?
- 了解过哪些寻路算法?
- 项目中用过哪些树?为什么用树?
- 哈希冲突如何解决?
- 怎么用的 AI?
二面
- 学计算机的,怎么没考虑 AI 方向?
- 为什么对游戏感兴趣?平时自己玩游戏吗?
- 除了二游还玩过哪些游戏?各玩了多久?
- 最近最常用的角色组合是哪一种?
- 有了解过原神的整个客户端吗?
- 有了解过客户端大概分什么方向吗?
- 学习过程中遇到什么大的困难挑战,是怎么度过的?
- 你对游戏行业的加班怎么看?
- 怎么看待 AI?
- 行业里有种说法是有了 AI 就不需要初级程序员,你怎么看?
- 你希望加入一个什么样的团队?对负责人和工作环境有什么想法?
- 未来 5 年的人生规划?
- 自己最大的缺点是什么?
- 估算一下 2027、2028、2029 年厦门正面迎接台风的概率有多大?
《参考解析》
1. 渲染管线的阶段划分:应用阶段在 CPU 上做剔除、排序、骨骼动画与合批,把可见物体的顶点数据整理好;几何阶段在 GPU 上跑顶点着色器(做 MVP 变换到裁剪空间)、可选细分与几何着色器、透视除法与视口变换,再做背面剔除和裁剪;光栅化阶段把三角形离散成片元并做插值;像素阶段跑片元着色器算着色,然后依次是深度测试、模板测试、混合,最后写入帧缓冲。现代管线还会插入计算着色器做 GPU 剔除、cluster 光照和后期处理;理解这条链路的价值在于知道每个问题的开销落在哪一段——draw call 过多是 CPU 提交瓶颈,片元着色器复杂是填充率瓶颈,几何量过大则是顶点或光栅化瓶颈。
2. VAO、VBO、EBO 的分工:VBO 是显存里的顶点缓冲,存位置、法线、UV 这些顶点属性数据;VAO 是”顶点数组对象”,记录的是状态——每个属性槽用哪个 VBO、数据格式(几个分量、类型、是否归一化)、偏移量和步长,以及绑定的是哪个 EBO,绑定一次 VAO 之后画不同网格只要切换 VAO 而不用重新设置一堆 glVertexAttribPointer。EBO(也叫 IBO)是索引缓冲,存放顶点的下标,让多个三角形复用同一批顶点,省显存也提高顶点缓存命中率。三者的关系是 VAO 引用 VBO 和 EBO,所以绑定 VAO 之后不能解绑 EBO,否则 VAO 里记录的索引缓冲就丢了。
3. 深度测试:开启深度测试后,每个片元带着插值出来的深度值与深度缓冲中同位置的已有值做比较(默认 GL_LESS,即更近才通过),通过则写入颜色并按需更新深度缓冲。关键是深度缓冲的精度与分布:透视投影下深度值是非线性分布的,绝大部分精度集中在近平面附近,所以近平面设得太小(比如 0.0001)会导致远处出现 z-fighting,而深度缓冲位数(D24S8 还是 D32F)也直接决定精度。除此之外还要注意深度写入的开关(半透明物体必须关闭深度写入、保持测试,并配合排序绘制)、提前深度测试(early-z)在有 discard 或写入深度时会失效,以及必要时用反向 Z 或对数深度缓冲改善远距离精度。
4. Phong 与 Blinn-Phong:两者结构一样,都是环境光 + 漫反射 + 镜面反射,区别只在镜面项的高光向量怎么取。Phong 用反射向量 R 与视线方向 V 的点积,需要先算 reflect(L, N);Blinn-Phong 改用半程向量 H = normalize(L + V),比较 N 与 H。差别有三点:一是 Blinn-Phong 少一次反射计算、更便宜;二是高光的形状不同,Blinn-Phong 的高光在掠射角下更宽也更”软”,Phong 在视线与反射方向接近垂直时会出现高光突然消失的截断;三是 Blinn-Phong 的高光在低模大平面上更平滑,因此早期固定管线和很多移动端方案都默认用它。两者都不是能量守恒的,高光强度与粗糙度不挂钩,这也是 PBR 取代它们的原因。
5. PBR 与 Cook-Torrance:PBR 的目标是让材质在任意光照下都自洽,核心是三条:基于微表面理论把表面看成大量微小镜面,用统计分布描述朝向;满足能量守恒,反射出去的能量不超过入射能量,漫反射部分要乘 1 - F;用金属度-粗糙度工作流,金属没有漫反射、非金属的 F0 固定为 0.04。Cook-Torrance 的镜面项由三部分相乘:D 是法线分布函数(GGX/Trowbridge-Reitz,决定高光形状和长尾)、G 是几何遮蔽项(Smith 形式,处理微表面自遮挡)、F 是菲涅尔项(Schlick 近似,掠射角反射增强)。三项除以 4(N·L)(N·V) 做归一化,再叠加 Lambert 漫反射,最后进 IBL 用预积分的辐照度贴图和 BRDF LUT 处理环境光。
6. Shadow Map 的生成:从光源视角渲染一遍场景,只写深度不写颜色,得到一张深度图(方向光用正交投影、聚光灯用透视投影),这就是 shadow map。主渲染时把世界坐标变换到光源空间,比较当前片元深度与 shadow map 中采样到的深度:更远就说明被遮挡、处于阴影中。实现细节决定质量:要按视锥体裁剪光源空间,方向光常用 CSM(级联阴影)把视锥分成几段各用一张图,否则近处锯齿、远处糊;要加深度偏移或法线偏移治 shadow acne(自阴影条纹),偏移过大会产生 peter-panning(影子与物体分离);光源空间边界要有 border clamp 并对超出范围的处理,避免边缘漏光。
7. PCF 做软阴影:PCF(Percentage Closer Filtering)不是对深度做模糊,而是对”比较结果”做平均。做法是在 shadow map 上以当前片元的投影位置为中心取一圈采样点(3×3、5×5 或按硬件支持用双线性 PCF),每个点各自做一次深度比较得到 0 或 1,最后把这批结果平均,得到 0~1 的可见度,边缘就有了渐变。采样数越多越平滑但越贵,所以常见优化是 poisson disk 抖动采样、依据光源大小和距离动态调整采样半径(contact hardening)、或者用 VSM/ESM 把深度换成可线性过滤的表示以便一次采样完成滤波。它只能软化边缘,遮挡关系本身仍受 shadow map 分辨率限制。
8. SSAO 的原理:SSAO 用屏幕空间的深度和法线近似遮蔽关系,不依赖场景几何。对每个片元,把法线方向围成的半球用若干随机方向采样(采样点按深度重建到视图空间),统计有多少采样点比实际几何更靠近、即被遮挡,按遮挡比例算遮蔽因子,最后乘到环境光项上。为了降噪,随机旋转向量按屏幕 4×4 分块复用,再对结果做一次模糊。它的近似性带来几个典型伪影:平面自遮蔽(需要 bias 和法线朝向剔除)、深度不连续处的黑边(halo,需要按深度差拒绝采样)、以及薄物体漏光;提高采样数、加双边模糊或改用 HBAO/GTAO 能明显改善。
9. 其它 AO 方案:HBAO 在半球内按多个方向做射线步进,找最大水平角,遮挡更符合直觉、也更贵;GTAO 把可见性写成地平线角度的解析积分,加上多次弹射近似,质量与开销平衡得最好,是目前主流方案(UE 里也有对应实现);VXAO 在体素化场景里做锥体追踪,能得到长距离遮蔽,但需要体素化和额外内存,多用于离线或高端管线;SSDO 在 SSAO 基础上加了单次间接反弹,能给画面带上邻近物体的颜色;还有基于深度图的地面实况遮蔽(ground truth ao)离线烘焙方案,用于静态场景。
10. MSAA 与 SSAA:SSAA 是超采样,把画面按更高分辨率(比如 2×2)渲染再降采样,等于在每个像素里放多个完整样本,着色开销按倍数上涨,效果最好但最贵。MSAA 只对几何边缘做多采样:每个像素维护多个覆盖样本和深度样本,三角形覆盖测试在样本级判定,但片元着色器每个像素只跑一次(或按需跑多次 shading),最后按覆盖率把颜色解析到单样本,因此只解决几何锯齿,对 shader 内部产生的高频细节(高光、纹理、透明、后期)无能为力。工程上常用 MSAA + 后处理 AA 组合,或者在延迟渲染里因为 G-Buffer 带宽问题改用 FXAA/TAA 这类后处理方案。
11. 各向异性过滤:它是纹理过滤的改进,针对的是”视线与表面接近平行”时的问题。这时一个屏幕像素投影到纹理上是一个被拉长的椭圆(各向异性),三线性 mipmap 只能选一层、按最长边取 mip 就会过度模糊。各向异性过滤沿纹理的主要压缩方向取多个采样(2×、4×、8×、16×,就是通常说的 4x/8x/16x),按比例与各向同性过滤结果合成,从而在斜视地面上保留远处细节。所以它的典型表现就是地面、路面、墙面的远处纹理更清晰,代价是纹理采样带宽上升,倍数越高越贵,而且只影响纹理、不改变几何锯齿。
12. 延迟渲染:延迟渲染先把场景的几何信息写进 G-Buffer(位置或深度、法线、albedo、粗糙度、金属度等),光照阶段再用全屏 pass 逐像素读取 G-Buffer 算光照。好处是光照开销与光源数量解耦、只对可见像素计算,几百个动态光源也能扛,且材质与光照解耦便于统一 PBR。代价是 G-Buffer 的显存与带宽开销很大(多张 RT),无法直接做 MSAA,透明物体要单独走前向,且不支持逐材质不同的着色模型。Tiled/Clustered 的混合管线(UE 的 mobile 与桌面路径都属此类)加上 forward+ 的思路,就是在前向渲染里按屏幕分块剔除光源,兼得两者优点。
13. UE 智能指针与 std 智能指针的区别:UE 自己实现了一套(TUniquePtr、TSharedPtr、TSharedRef、TWeakPtr),语义对标 std 的 unique/shared/weak。差异在实现和配套上:TSharedPtr 用侵入式引用计数,线程安全模式要显式声明(ThreadSafe 或 NotThreadSafe),TWeakPtr 用 Pin() 取回强引用,TSharedRef 表达不可为空的引用。但 UObject 体系不走这一套:UObject 由引擎 GC 按引用图做标记-清除,裸指针用 TObjectPtr 表达可被追踪的引用,UPROPERTY() 决定它是否参与扫描;本质是”非 UObject 用智能指针、UObject 用 GC”两套内存管理模型并存。
14. A 寻路与空间树*:A* 是带启发的最短路径搜索,代价函数 f(n) = g(n) + h(n),g 是起点到当前的实际代价,h 是到终点的估计代价;h 必须可采纳(不高估)才能保证最优,常用欧氏距离或八方向下的对角距离(octile)。实现上用开放列表(优先队列,按 f 排序)加关闭列表,节点记录父指针回溯路径。工程里要解决的还有:网格太细时用导航网格(NavMesh)把多边形当作节点、路径再做漏斗算法拉直;动态障碍用分层寻路或局部避障(RVO/ORCA);大规模群体用 HPA* 先粗后细;性能上要注意优先队列的实现和启发权重(加权 A* 可以牺牲最优性换速度)。四叉树/八叉树是把空间递归八分的结构,用于视锥剔除、碰撞粗筛和邻近查询,把 O(n) 的暴力遍历降到 O(log n) 量级。
15. 哈希冲突的解决方式:主流是两类。拉链法把同一个桶里的元素挂在链表上,冲突多了把链表转成红黑树(Java 的 HashMap 就是这么做的),实现简单、删除方便,缺点是节点分散导致缓存不友好、指针开销大。开放寻址法在探测序列上找下一个空槽,线性探测、二次探测、双重哈希都是常见变体,缓存局部性好、内存紧凑,但删除要留墓碑标记、负载因子高时性能断崖式下滑,因此通常控制装载因子在 0.7 左右并在超限时 rehash。工程上的补充手段还有:用高质量哈希函数(如带随机种子的 SipHash)防碰撞攻击、Robin Hood 哈希平衡探测长度、Cuckoo 哈希用两张表保证最坏 O(1) 查询,以及给哈希表预设容量避免运行期反复扩容。