Skip to main content

15. 面试真题与避坑指南

学完了前面的章节,你已经具备了 WebGL 的硬实力。但在面试中,面试官往往会考察你对底层原理的理解深度以及解决问题的经验。

以下是图形学/WebGL 岗位高频出现的面试题。


🟢 基础概念类

1. 渲染管线 (Rendering Pipeline) 的流程是什么?

考察点:宏观把控能力。 参考答案

  • CPU 阶段:提交 Draw Call,准备数据。
  • 顶点着色器 (Vertex Shader):计算顶点位置。
  • 图元装配 (Primitive Assembly):连点成线/面,裁剪 (Clipping)。
  • 光栅化 (Rasterization):将三角形离散化为像素片段。
  • 片段着色器 (Fragment Shader):计算每个像素的颜色。
  • 测试与混合 (Tests & Blending):深度测试 (Z-Test)、Alpha 混合。

cpu 阶段详细过程如下:

  • 矩阵计算:在 CPU 端计算所有变换矩阵(Model、View、Projection)
    • modelMatrix = mesh.matrixWorld(根据 position/rotation/scale 计算)
    • viewMatrix = camera.matrixWorldInverse(相机逆变换)
    • projectionMatrix = camera.projectionMatrix(根据 FOV/aspect 计算)
  • 数据准备:将矩阵作为 uniform 传递给 GPU
    • 通过 gl.uniformMatrix4fv() 或 Three.js 的 material.uniforms 设置
    • 矩阵数据(16 个 float,64 字节)被写入 GPU 的 uniform 缓冲区
  • Draw Call 提交:调用 gl.drawArrays()gl.drawElements()
    • 此时 GPU 已经准备好所有数据:顶点缓冲区、uniform 矩阵、shader 程序
    • Draw Call 只是告诉 GPU:"用这些数据开始绘制"

完整流程示例(Three.js 内部简化版):

// ========== CPU 阶段:准备数据 ==========
function render(scene, camera) {
// 1. 更新所有对象的矩阵(CPU 计算)
scene.updateMatrixWorld(); // 计算每个 mesh.matrixWorld
camera.updateMatrixWorld(); // 计算 camera.matrixWorld
camera.updateProjectionMatrix(); // 计算 projectionMatrix

// 2. 对于每个要渲染的对象
for (const mesh of scene.children) {
// 2.1 获取计算好的矩阵(CPU 端已完成)
const modelMatrix = mesh.matrixWorld; // 16 个数字
const viewMatrix = camera.matrixWorldInverse; // 16 个数字
const projectionMatrix = camera.projectionMatrix; // 16 个数字

// 2.2 将矩阵作为 uniform 传递给 GPU(写入 GPU 内存)
gl.useProgram(shaderProgram);
gl.uniformMatrix4fv(
gl.getUniformLocation(shaderProgram, "modelMatrix"),
false, // 不转置(WebGL 使用列主序)
modelMatrix.elements, // 16 个 float 数组
);
gl.uniformMatrix4fv(
gl.getUniformLocation(shaderProgram, "viewMatrix"),
false,
viewMatrix.elements,
);
gl.uniformMatrix4fv(
gl.getUniformLocation(shaderProgram, "projectionMatrix"),
false,
projectionMatrix.elements,
);

// 2.3 绑定顶点缓冲区(attribute 数据)
gl.bindBuffer(gl.ARRAY_BUFFER, mesh.geometry.attributes.position.buffer);
gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);

// 2.4 提交 Draw Call(告诉 GPU 开始绘制)
gl.drawArrays(gl.TRIANGLES, 0, mesh.geometry.attributes.position.count);
// ↑ 这一步只是发送一个命令,矩阵数据已经在步骤 2.2 中准备好了
}
}

// ========== GPU 阶段:执行 Shader ==========
// 顶点着色器(在 GPU 上执行,每个顶点执行一次)
// uniform mat4 modelMatrix; // ← 这些 uniform 已经在 Draw Call 前设置好了
// uniform mat4 viewMatrix;
// uniform mat4 projectionMatrix;
// attribute vec3 position;
//
// void main() {
// gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
// }

关键理解

  • 矩阵计算在 CPUupdateMatrixWorld() 等函数在 CPU 端计算矩阵
  • 矩阵传递在 Draw Call 之前gl.uniformMatrix4fv() 将矩阵写入 GPU 的 uniform 缓冲区
  • Draw Call 只是触发gl.drawArrays() 告诉 GPU "开始用这些数据绘制"
  • GPU 读取 uniform:顶点着色器执行时,直接从 uniform 缓冲区读取矩阵数据

2. attribute, uniform, varying 的区别?

考察点:GLSL 基础。 参考答案

  • attribute:每个顶点独有的数据(如位置、法线)。仅在 Vertex Shader。
  • uniform:全局常量(如时间、变换矩阵)。所有着色器共享。
  • varying:从 Vertex 传递给 Fragment 的插值数据(如 UV)。

3. 什么是 MVP 矩阵?

考察点:数学基础。 参考答案

  • Model (模型矩阵):物体自身的变换(本地坐标 -> 世界坐标)。
  • View (视图矩阵):相机的变换(世界坐标 -> 相机/观察坐标)。
  • Projection (投影矩阵):透视或正交投影(相机坐标 -> 裁剪坐标)。
  • 顺序:gl_Position = P * V * M * position

🟡 进阶原理类

4. 深度测试 (Z-Test) 是什么?

考察点:3D 遮挡关系。 参考答案

  • GPU 会维护一个深度缓冲区 (Depth Buffer),存储每个像素当前的深度值 (Z 值)。
  • 当绘制新像素时,比较新像素的 Z 值与缓冲区里的 Z 值。
  • 如果新像素“更近”,则覆盖颜色并更新深度;否则丢弃。

5. 为什么会有“ZFighting” (深度冲突)?怎么解决?

考察点:实战经验。 现象:两个重叠平面闪烁。 解决

  1. 稍微偏移其中一个平面的位置。
  2. 使用 polygonOffset
  3. 使用对数深度缓存 (Logarithmic Depth Buffer) 提高精度。

6. 什么是 Draw Call?为什么要减少它?

考察点:性能优化。 参考答案

  • 定义:Draw Call 是 CPU 调用图形 API (如 gl.drawArrays) 命令 GPU 进行一次绘制的操作。
  • 为什么慢(CPU 瓶颈)
    • 并不是“传数据”本身慢,而是准备工作慢。
    • 每次调用 Draw Call,CPU 的驱动程序需要做:
      1. 校验状态:检查 Shader、Buffer、Texture 是否匹配且合法。
      2. 切换状态:告诉 GPU 切换到新的渲染状态(比如从不透明模式切换到半透明模式)。
      3. 发送命令:将指令写入 Command Buffer 供 GPU 读取。
    • 比喻:就像你要运送 1000 块砖。
      • 多 Draw Call:每次只拿 1 块砖,跑 1000 趟(CPU 累死在路上,GPU 经常闲着等砖)。
      • 少 Draw Call (Batching):用卡车装 1000 块砖,跑 1 趟(CPU 轻松,GPU 满载工作)。
  • 优化:使用 Instancing (实例化渲染)合并几何体 (Merge Geometries),尽可能一次 Draw Call 画更多东西。

7. [必考] renderOrderdepthTest 的优先级?(为什么我的透视效果失效了?)

考察点:渲染管线控制与深度机制。这是面试中非常容易被问倒的“陷阱题”。

常见误区:认为 renderOrder 高的物体一定显示在前面,或者是 renderOrder 优先级高于 depthTest

标准答案(核心逻辑)

不存在谁优先级高的问题,它们管的是不同的事:

  • renderOrder:决定时间顺序(谁先动手)。
  • depthTest:决定动作有效性(动手时,能否覆盖已有的颜色)。

深度解析

  1. 场景一:透视失效(经典陷阱)

    • 设置:物体 A(远处,renderOrder: 999depthTest: true),物体 B(近处,默认设置)。
    • 结果:A 被 B 挡住,画不上去!
    • 原因:虽然 A 后画(renderOrder 大),但当它画的时候,GPU 拿它的深度(远)和缓存里 B 的深度(近)比较,测试失败,A 的像素被丢弃。renderOrder 在这里毫无作用。
  2. 场景二:实现“透视/X 光”效果(正确做法)

    • 必须同时满足两点:
      1. 后画renderOrder 大):保证最后轮到它执行。
      2. 无视规则depthTest: false):保证不管之前画了什么(比如墙壁),它都能霸道地覆盖上去。
  3. 进阶追问:Three.js 真的没有 Early Z-Test 吗?

    • :Three.js 不缺少 Early Z-Test(这是 GPU 硬件功能,只要满足条件就会自动开启)。
    • 但是:Three.js 默认优先按“材质”排序(减少 CPU 切换开销),而不是按“距离”排序(从近到远)。
    • 后果:这可能导致“先画远处的,再画近处的”,导致 Early Z-Test 失效(近处物体覆盖远处物体,发生了 Overdraw)。
    • 优化:在填充率(Fill Rate)瓶颈严重的情况下,可以手动设置 renderOrder 强迫不透明物体“从近到远”绘制,手动激活 Early Z 的最大潜力。

8. 独显电脑流畅,集显电脑卡顿,是 CPU 还是 GPU 瓶颈?

考察点:性能瓶颈定位能力。 参考答案

  • 通常是 GPU 瓶颈(算力或带宽),而不是 CPU。
  • 原因
    1. 算力不足:集显的 Shader 执行核心比独显少得多,复杂的片元着色器(Fragment Shader)跑不动。
    2. 带宽共享:集显没有独立显存,使用系统内存(RAM)。系统内存的速度(几十 GB/s)远低于独立显存(几百 GB/s),且要和 CPU 抢带宽。
  • 如何快速验证(排除法)
    1. 缩小浏览器窗口:如果窗口变小,帧率瞬间提升,说明是 Fill Rate(填充率)/ Shader 计算 瓶颈(GPU 锅)。
    2. 把 Shader 换成纯色输出:如果帧率提升,说明原来的 Shader 逻辑太重(GPU 锅)。
    3. 如果怎么缩小窗口都很卡:那才可能是 顶点太多Draw Call 太多(CPU 或 顶点处理瓶颈)。

🔴 刁钻/数学类

9. 点积 (Dot) 和叉积 (Cross) 的几何意义?

考察点:图形学数学直觉。 参考答案

  • Dot: $A \cdot B = |A||B|\cos\theta$。判断方向相似度。用于光照(光线与法线夹角)。
  • Cross: 得到垂直于 A 和 B 所在平面的向量。用于计算法线。

10. 为什么矩阵乘法顺序很重要?( A B != B A )

考察点:矩阵性质。 参考答案

  • 矩阵代表变换。
  • “先旋转再平移”和“先平移再旋转”,结果截然不同。
  • 旋转是绕原点进行的。如果你先平移了,物体就离开了原点,再旋转就会绕着远处的大圈转。

11. 如何判断一个点在三角形内部?

考察点:光栅化原理。 参考答案

  • 叉积法:沿着三角形三条边顺时针走,如果点都在三条边的右侧,则在内部。
  • 重心坐标法 (Barycentric Coordinates):$P = uA + vB + wC$,如果 $u,v,w \ge 0$ 且 $u+v+w=1$。

12. 什么是万向节死锁 (Gimbal Lock)?如何解决?

考察点:3D 旋转的数学原理。 参考答案

  • 现象:使用欧拉角 (Euler Angles, 如 x, y, z 旋转) 时,当中间轴旋转 90 度时,第一个轴和第三个轴重合,导致失去一个自由度。
  • 解决:使用 四元数 (Quaternion) 进行旋转计算。Three.js 内部大部分旋转计算都是用四元数完成的。

13. 为什么 discard 操作会影响性能?

考察点:GPU 硬件特性 (Early-Z)。 参考答案

  • 现代 GPU 有一个优化叫 Early-Z:在执行复杂的 Fragment Shader 之前,先检查深度。如果被遮挡了,直接跳过,不计算颜色。
  • 如果你在 Shader 里写了 discard(比如做噪声溶解),GPU 就无法提前知道这个像素会不会被丢弃,被迫关闭 Early-Z 优化
  • 后果:所有被遮挡的像素也会跑一遍完整的 Shader,导致严重的过度绘制 (Overdraw)。

🔵 前沿/工程类

14. PBR (Physically Based Rendering) 的核心参数是什么?

考察点:现代渲染标准。 参考答案

  • Albedo (反照率):物体的固有色(不包含光照信息)。
  • Metallic (金属度):0 是绝缘体(塑料、木头),1 是金属。金属几乎没有漫反射,只有镜面反射,且反射带颜色。
  • Roughness (粗糙度):表面微观的平整程度。越粗糙,高光越散。
  • AO (Ambient Occlusion):环境光遮蔽,缝隙里的阴影。

15. 常见的 WebGL 性能优化手段有哪些?

考察点:综合能力。 参考答案

  1. CPU 侧:减少 Draw Call (Instancing, Merging),使用对象池减少 GC。
  2. GPU 侧
    • 纹理压缩 (使用 .ktx2 / .dds 代替 .png)。
    • LOD (Level of Detail):远处用低模,近处用高模。
    • 视锥体剔除 (Frustum Culling):不画视野外的物体 (Three.js 自动做)。
  3. Shader 侧
    • 把计算从 Fragment Shader 移到 Vertex Shader (如 Fog, 简单的光照)。
    • 避免在 Shader 里写复杂的 if-else 或循环。
    • 使用 lowpmediump 精度。

💡 面试避坑指南

  1. 不要背代码:面试官不会让你手写具体的 GLSL 算法,而是看思路。比如问“怎么做水面”,你应该回答“用正弦波控制顶点高度,用菲涅尔方程做反射”。
  2. 承认盲区:图形学博大精深(PBR、光线追踪、后处理)。如果问到不懂的,诚实说“这块我还没深入研究,但我熟悉基础的管线”,比强行胡扯要好。
  3. 展示作品:图形学是视觉的。一个炫酷的 Shader Demo 链接胜过千言万语。