本文档旨在为高级图形开发工程师提供一份深度的面试指南。不同于基础的 API 问答,本指南侧重于底层原理、图形学算法、GPU 架构以及工程化最佳实践。
一、 图形学基础与数学原理 (Q1-Q15)
Q1: 请从底层矩阵变换的角度,详述从“局部坐标”到“屏幕像素”的完整流水线。
答案要点:
- MVP 变换:
- Model Matrix (模型矩阵):
Local Space->World Space。包含平移、旋转、缩放。 - View Matrix (视图矩阵):
World Space->View/Camera Space。相机的逆变换,将世界转换到相机视野下。 - Projection Matrix (投影矩阵):
View Space->Clip Space(裁剪空间)。进行透视除法前的准备,定义视锥体。
- Model Matrix (模型矩阵):
- 透视除法 (Perspective Divide):将
Clip Space坐标(x, y, z, w)除以w,得到 NDC (标准化设备坐标),范围[-1, 1]。 - 视口变换 (Viewport Transform):
NDC->Screen Space。映射到 Canvas 的像素坐标(x, y)。 - 光栅化 (Rasterization):GPU 将三角形离散化为片元 (Fragment),并进行插值。
Q2: 欧拉角 (Euler Angles) 与四元数 (Quaternion) 的底层区别及应用场景?
答案要点:
- 欧拉角:
- 原理:按顺序绕 X、Y、Z 轴旋转。
- 缺陷:万向节死锁 (Gimbal Lock)。当中间轴旋转 90° 时,导致第一轴与第三轴重合,丢失一个自由度。
- 四元数:
- 原理:使用
(x, y, z, w)表示旋转,本质是复数在 4D 空间的扩展。 - 优势:无死锁、Slerp (球形插值) 平滑、矩阵转换效率高。
- 原理:使用
- 实践:Three.js 中
Object3D.rotation是欧拉角,但底层Object3D.quaternion才是核心。骨骼动画、飞行模拟必须使用四元数。
Q3: 什么是法线矩阵 (Normal Matrix)?为什么计算法线不能直接使用模型矩阵?
答案要点:
- 问题:如果模型矩阵包含非均匀缩放(如 x 轴放大 2 倍),直接使用模型矩阵变换法线,会导致法线不再垂直于表面。
- 原理:法线矩阵是模型矩阵左上角 3x3 子矩阵的逆转置矩阵 (
(M^-1)^T)。 - Three.js:在 Shader 中通常通过
normalMatrix传入,用于将法线从 View Space 正确变换。
Q4: 深度缓冲 (Z-Buffer) 的工作原理是什么?什么是 Z-Fighting?
答案要点:
- 原理:GPU 维护一个深度纹理,存储每个像素当前的深度值 (0.0~1.0)。
- 深度测试:新片元的深度值 vs 缓冲中的值。如果新值更近(通常
<),则通过测试并更新缓冲;否则丢弃。 - Z-Fighting (深度冲突):
- 现象:两个面距离极近,因浮点数精度限制,深度值交替占优,导致闪烁。
- 解决:
- 增大两面距离。
- 调整相机
near和far平面(near越远,far越近,精度越高)。 - 使用 对数深度缓冲 (Logarithmic Depth Buffer)。
- 设置
polygonOffset。
Q5: 视锥体剔除 (Frustum Culling) 的算法逻辑是怎样的?
答案要点:
- 核心:判断物体的包围体(AABB 包围盒或包围球)是否与视锥体(由 6 个平面组成的截断金字塔)相交。
- 流程:
- 更新物体的世界矩阵。
- 计算物体的包围球 (
BoundingSphere) 并应用世界变换。 - 遍历视锥体的 6 个平面,判断球体是否在所有平面的“内侧”。
- 优化:Three.js 使用层级剔除,先剔除父节点,若父节点不可见,子节点直接跳过。
Q6: 什么是齐次坐标 (Homogeneous Coordinates)?为什么 w 分量很重要?
答案要点:
- 定义:用 4 维向量
(x, y, z, w)表示 3D 坐标。 - 作用:
- 统一变换:使得平移(Translation)可以用矩阵乘法表示(线性变换)。
- 透视投影:
w分量存储了顶点的深度信息(距离相机的远近)。在透视除法中,x/w, y/w实现了“近大远小”的效果。
Q7: 射线检测 (Raycasting) 的数学原理?如何优化大量物体的检测性能?
答案要点:
- 原理:
- Unproject:将屏幕坐标 (NDC) 逆变换回世界空间,得到射线原点和方向。
- Intersection:计算射线与包围球/盒的交点(快速),若相交再计算与三角形的交点(慢速,重心坐标插值)。
- 优化:
- BVH (Bounding Volume Hierarchy):使用
three-mesh-bvh库构建空间索引,将复杂度从 O(N) 降至 O(logN)。 - 层级检测:只检测特定
Layers的物体。 - 粗略检测:先检测包围球,命中后再检测几何体。
- BVH (Bounding Volume Hierarchy):使用
Q8: 什么是 Draw Call?为什么它是性能瓶颈?
答案要点:
- 定义:CPU 通知 GPU 绘制一个图元列表的命令(如
glDrawElements)。 - 瓶颈原因:
- CPU 开销:每次 Draw Call 前,CPU 需要设置渲染状态(Shader, Texture, Buffer 绑定),验证数据,提交命令。
- 状态切换:频繁切换材质和几何体导致 GPU 流水线停顿。
- 优化:Instancing(实例化)、Merging(合并)、Texture Atlas(纹理图集)。
Q9: 解释 UV 映射 (UV Mapping) 及其在纹理采样中的作用。
答案要点:
- 定义:将 2D 纹理坐标
(u, v)(范围 0~1) 映射到 3D 模型的顶点上。 - 采样:Fragment Shader 使用插值后的 UV 坐标,通过
texture2D(map, uv)读取纹理颜色。 - Wrap Mode:决定 UV 超出 0~1 范围时的行为(Repeat, ClampToEdge, MirroredRepeat)。
Q10: 什么是切线空间 (Tangent Space)?法线贴图为何是蓝紫色的?
答案要点:
- 切线空间:基于模型表面的局部坐标系。由 Normal (法线), Tangent (切线), Bitangent (副切线) 组成。
- 法线贴图:存储的是切线空间下的法线扰动向量。
- 平坦表面的法线是
(0, 0, 1)(指向外)。 - 映射到 RGB 空间即
(0.5, 0.5, 1.0),呈现浅蓝色。
- 平坦表面的法线是
- 优势:法线贴图可以复用于变形的网格(如骨骼动画),因为它是相对于表面的。
二、 Three.js 核心架构与资源管理 (Q11-Q25)
Q11: 深入解析 Three.js 的渲染循环 (Render Loop) 与 requestAnimationFrame。
答案要点:
- rAF:浏览器提供的 API,通常以 60fps 运行,在后台标签页自动暂停以省电。
- Three.js 流程:
renderer.render(scene, camera)被调用。- Update Matrix:遍历场景图,更新所有物体的
matrixWorld。 - Frustum Culling:剔除视锥体外的物体。
- Sort:不透明物体按从近到远排序(减少 Overdraw),透明物体从远到近排序(混合正确)。
- Render:按顺序提交 Draw Call。
Q12: Three.js 的资源管理机制:dispose() 到底做了什么?
答案要点:
- JS vs GPU:JS 的 GC 无法回收 GPU 显存。
- dispose():
- Geometry:删除 WebGLBuffer (
gl.deleteBuffer)。 - Texture:删除 WebGLTexture (
gl.deleteTexture)。 - Material:清理关联的 ShaderProgram 引用。
- RenderTarget:删除 Framebuffer。
- Geometry:删除 WebGLBuffer (
- 最佳实践:切换场景时,必须递归遍历 Scene,手动 dispose 所有资源,否则会导致严重的内存泄漏。
Q13: BufferGeometry 的底层数据结构:InterleavedBuffer 有什么优势?
答案要点:
- 普通 BufferAttribute:Position, Normal, UV 分别存放在独立的 ArrayBuffer 中。
- InterleavedBuffer (交错缓冲):所有数据紧凑存放在同一个 Buffer 中(如
P,P,P,N,N,N,U,V...)。 - 优势:提高 GPU 缓存命中率 (Cache Locality)。当 GPU 读取顶点位置时,法线和 UV 数据已经在缓存行中,减少内存带宽消耗。
Q14: 什么是 UniformBufferObject (UBO)?Three.js 如何支持?
答案要点:
- 问题:全局变量(如相机矩阵、光照信息)在每个 Shader 中都需要重复上传,开销大。
- UBO:在 GPU 显存中开辟一块共享区域,所有 Shader 都可以读取。
- 优势:只需上传一次数据,切换 Shader 时无需重新绑定这些 Uniforms,大幅降低 CPU 开销。
- Three.js:在 WebGL 2.0 环境下自动使用 UBO 管理全局 Uniforms。
Q15: ShaderMaterial 与 RawShaderMaterial 的底层区别?
答案要点:
- ShaderMaterial:
- 预处理:Three.js 会自动注入内置 Uniforms (
modelViewMatrix,projectionMatrix) 和 Attributes。 - Chunks:支持
#include <common>等代码片段复用。 - Defines:根据材质属性(如
map,fog)自动添加#define USE_MAP。
- 预处理:Three.js 会自动注入内置 Uniforms (
- RawShaderMaterial:
- 纯净:不注入任何代码。开发者完全掌控 GLSL。
- 用途:编写不依赖 Three.js 系统的独立 Shader,或移植其他引擎的 Shader。
Q16: onBeforeCompile 的作用及使用场景?
答案要点:
- 痛点:想要微调标准材质(如 MeshStandardMaterial)的效果(如增加顶点波浪),但不想重写几千行 PBR Shader 代码。
- 机制:在 Shader 编译前,以字符串形式拦截并修改 GLSL 代码。
- 操作:使用
shader.vertexShader.replace('#include <begin_vertex>', myCustomCode)注入自定义逻辑。
Q17: 什么是 WebGLRenderTarget?FBO 在后处理中的应用?
答案要点:
- FBO (Frame Buffer Object):允许渲染到纹理而不是屏幕。
- Ping-Pong 技术:在后处理(如高斯模糊)中,需要多次 Pass。通常使用两个 RenderTarget (Read/Write) 交替读写,避免读写同一纹理导致的未定义行为。
- 应用:GPGPU 粒子系统、动态环境映射、阴影贴图生成。
Q18: 解释 InstancedMesh 的实现原理与限制。
答案要点:
- 原理:利用
ANGLE_instanced_arrays扩展(WebGL 2 原生)。- Geometry:只上传一份顶点数据。
- Attributes:上传一份包含所有实例变换矩阵(位置/旋转/缩放)的 Buffer。
- Draw:调用
glDrawElementsInstanced一次性绘制。
- 限制:所有实例必须共享同一个 Geometry 和 Material。
- Frustum Culling:默认只检测整体包围球。若需单独剔除,需自行实现计算逻辑。
Q19: 什么是 MRT (Multiple Render Targets)?
答案要点:
- 定义:一次 Draw Call 同时输出到多个纹理(Color Attachments)。
- 应用:延迟渲染 (Deferred Rendering)。
- 在 G-Buffer Pass 中,同时输出 Position, Normal, Albedo, Roughness 到不同的纹理。
- 在 Lighting Pass 中,读取这些纹理进行光照计算。
Q20: 纹理压缩 (Texture Compression) 的格式选择策略?
答案要点:
- 原理:GPU 硬件直接支持解码,无需 CPU 解压,显存占用通常是原始图片的 1/4 或 1/8。
- 格式:
- ASTC:最新标准,质量高,支持移动端和桌面端(覆盖面广)。
- ETC2:Android/iOS 通用。
- BC7/DXT:桌面端 PC 专用。
- Basis Universal (.basis/.ktx2):一种中间格式,运行时转码为当前 GPU 支持的格式,最佳跨平台方案。
三、 渲染管线与 PBR 材质 (Q21-Q35)
Q21: 详述 PBR (Physically Based Rendering) 的能量守恒与微表面理论。
答案要点:
- 能量守恒:出射光能量 <= 入射光能量。
Diffuse + Specular <= 1.0。 - 微表面理论:表面由无数微小的镜面组成。
- Roughness (粗糙度):决定微表面的朝向离散程度。越粗糙,高光越扩散。
- Fresnel (菲涅尔效应):视线与表面越平行,反射率越高(水面、玻璃侧面反光强)。
- BRDF:双向反射分布函数,Three.js 使用 Cook-Torrance 模型。
Q22: Metalness (金属度) 工作流的本质区别是什么?
答案要点:
- 非金属 (Dielectric):
- 漫反射颜色 = Albedo。
- 高光反射率 (F0) 固定为 0.04 (4%)。
- 金属 (Conductor):
- 没有漫反射 (Diffuse = 0)。所有光都被吸收或反射。
- 高光反射率 = Albedo 颜色 (60%~90%)。
- Shader 实现:通过
mix()函数根据 metalness 在这两套逻辑间插值。
Q23: 什么是 Tone Mapping (色调映射)?为什么 HDR 渲染必不可少?
答案要点:
- 问题:物理光照计算的结果范围是
[0, ∞)(HDR),而显示器只能显示[0, 1](LDR)。直接截断会导致亮部过曝死白。 - 解决:使用映射曲线将高动态范围压缩到
[0, 1]。 - 算法:
Reinhard:简单压缩,色彩较灰。ACESFilmic:电影工业标准,对比度高,保留亮部细节和色彩饱和度(Three.js 推荐)。
Q24: Gamma 校正 (Gamma Correction) 的全流程是怎样的?
答案要点:
- 输入:纹理图片通常是 sRGB (非线性) 存储的。
- 计算:Shader 中必须将 sRGB 解码为 Linear 空间进行物理计算 (
pow(color, 2.2))。 - 输出:最终颜色输出到屏幕前,必须编码回 sRGB (
pow(color, 1/2.2))。 - Three.js:设置
renderer.outputColorSpace = THREE.SRGBColorSpace自动处理输出编码。
Q25: 阴影贴图 (Shadow Map) 的 Bias 问题与 Peter Panning 现象。
答案要点:
- Shadow Acne (痤疮):由于深度图精度不够,物体表面错误地遮挡了自己,产生条纹。
- 解决:增加
bias,将阴影深度稍微推远。
- 解决:增加
- Peter Panning (悬浮):
bias过大,导致阴影与物体根部脱离,像彼得潘一样悬浮。- 解决:使用
normalBias(沿法线方向偏移),比单纯的深度偏移效果更好。
- 解决:使用
Q26: 什么是 IBL (Image Based Lighting)?它如何提升真实感?
答案要点:
- 原理:将环境贴图(HDR)视为一个巨大的球形光源。
- 组成:
- Irradiance Map:漫反射环境光(模糊的环境图)。
- Radiance Map:镜面反射环境光(不同粗糙度的多级 Mipmap)。
- 作用:提供复杂的环境反射和漫反射,是 PBR 材质看起来“真实”的关键,尤其是金属材质。
Q27: 解释 AO (Ambient Occlusion) 环境光遮蔽的原理。
答案要点:
- 目的:模拟角落、缝隙等光线难以到达区域的阴影,增强体积感。
- 类型:
- Static AO:烘焙在纹理中 (
aoMap)。 - SSAO (Screen Space AO):基于屏幕深度缓冲的实时后处理算法,计算开销较大。
- Static AO:烘焙在纹理中 (
Q28: 透明物体渲染的排序问题 (Transparency Sorting)。
答案要点:
- 机制:透明物体不写深度 (Depth Write Off),只进行深度测试。必须按从远到近的顺序渲染,才能正确混合颜色。
- 问题:当透明物体相互穿插,或形状复杂(凹多面体)时,基于中心的排序会失效,导致渲染错误。
- 解决:
- Depth Peeling:多遍渲染剥离深度(开销大)。
- OIT (Order Independent Transparency):与顺序无关的透明技术(如 Weighted Blended)。
- Alpha Test:对于树叶等,直接 discard 透明像素,当作不透明物体渲染。
Q29: 什么是延迟渲染 (Deferred Rendering)?与前向渲染 (Forward) 的对比。
答案要点:
- Forward:每个物体对每个光源计算一次光照。复杂度
O(NumObjects * NumLights)。光源多时性能崩塌。 - Deferred:
- G-Buffer Pass:渲染几何信息(位置、法线、颜色)到纹理。
- Lighting Pass:对屏幕每个像素计算光照。复杂度
O(ScreenWidth * Height * NumLights)。
- 优缺:延迟渲染支持成百上千光源,但难以处理透明物体和 MSAA。
Q30: 什么是 Light Probe (光照探针) 和 Lightmap (光照贴图)?
答案要点:
- Lightmap:将静态光照(阴影、GI)烘焙到纹理上。效果最好,但物体不能动。
- Light Probe:在空间中布置探针点,预计算球谐函数 (Spherical Harmonics)。动态物体经过时,插值周围探针的光照数据。性能极高,适合动态物体。
四、 性能优化与工程化 (Q31-Q45)
Q31: 如何系统性地分析 Three.js 的性能瓶颈?
答案要点:
- CPU 瓶颈:
- 现象:帧率低,但 GPU 占用不高。
- 原因:Draw Calls 太多、几何体更新频繁、物理计算过重。
- 工具:Chrome Performance Tab。
- GPU 瓶颈:
- 现象:分辨率降低后帧率显著提升。
- 原因:Fragment Shader 过重(像素填充率)、纹理过大、后处理太多。
- 工具:Spector.js, RenderDoc。
Q32: 减少 Draw Calls 的终极方案:Merge vs Instancing vs Batching。
答案要点:
- Merge:合并几何体。适合完全静态、材质相同的物体。缺点是内存占用增加,无法单独剔除。
- Instancing:实例化。适合网格相同、材质相同但变换不同的物体(草地、树木)。性能最佳。
- Batching (BatchedMesh):Three.js 新特性。允许合并不同几何体,且能动态更新每个子物体的矩阵。比 Merge 更灵活,比 Instancing 适用范围更广。
Q33: 纹理优化的最佳实践:尺寸、格式与 Mipmap。
答案要点:
- POT (Power of Two):宽高为 2 的幂次方(如 1024),是启用 Mipmap 的前提(WebGL 1)。
- Mipmap:以 33% 的额外显存换取渲染性能和画质(减少摩尔纹)。
- Atlas (图集):将多张小纹理合并为一张大图,减少材质切换和 Draw Calls。
- 各向异性过滤:只在地面等大角度倾斜表面开启,消耗 GPU 性能。
Q34: 几何体优化:LOD (Level of Detail) 与 Draco 压缩。
答案要点:
- LOD:根据距离动态切换高/中/低模。减少远处的顶点着色器开销。
- Draco:Google 的几何体压缩算法。
- 优点:GLB 体积减少 50%~90%。
- 代价:加载时需要 WASM 解码,增加 CPU 初始化时间。适合网络带宽瓶颈场景。
Q35: 什么是 OffscreenCanvas?如何在 WebWorker 中渲染?
答案要点:
- 原理:将 Canvas 的控制权转移给 WebWorker。
- 优势:渲染循环与主线程(UI、DOM 操作)分离。即使主线程卡顿(如 React 复杂更新),3D 动画依然流畅。
- 限制:DOM 事件(鼠标点击)需要从主线程转发给 Worker。
Q36: 内存泄漏排查:如何彻底销毁一个 Three.js 场景?
答案要点:
- 递归遍历:
scene.traverse()。 - 清理资源:
geometry.dispose()material.dispose()material.map.dispose()(以及 normalMap, roughnessMap 等所有贴图)
- 移除引用:
scene.remove(object),并将变量置为null。 - 渲染器:
renderer.dispose(),renderer.forceContextLoss()。
Q37: 移动端 H5 3D 项目的特定优化策略。
答案要点:
- DPR 限制:
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))。避免在 3x 屏上渲染过高分辨率。 - 精度:Shader 中使用
mediump或lowp。 - 后处理:尽量少用或不用 Bloom、DOF 等昂贵特效。
- 阴影:使用烘焙贴图代替实时阴影。
Q38: 什么是 Shader 编译卡顿 (Shader Compilation Jitter)?如何解决?
答案要点:
- 现象:游戏开始或新物体出现时,画面卡顿一下。
- 原因:GPU 首次遇到新 Shader 需要编译。
- 解决:预编译 (Pre-compiling)。
- 在 Loading 阶段,将所有材质强制渲染一次(哪怕是渲染到 1x1 的不可见 RenderTarget),强制 GPU 提前编译 Shader。
- 使用
renderer.compile(scene, camera)API。
Q39: 什么是 Object Pooling (对象池)?
答案要点:
- 场景:粒子发射、子弹射击等高频创建/销毁。
- 问题:频繁
new和 GC 会导致帧率波动。 - 实现:预先创建数组
[bullet1, bullet2...]。使用时标记active=true,用完后标记active=false并重置位置,而不是销毁。
Q40: 如何优化大型场景的加载速度?
答案要点:
- 增量加载:先加载低模或包围盒,用户走近时流式加载高模。
- 二进制格式:使用 GLB 代替 GLTF/OBJ。
- 并行加载:使用
Promise.all并发请求资源。 - Service Worker:缓存静态资源。
五、 WebGPU 与前沿技术 (Q41-Q50)
Q41: WebGPU 相比 WebGL 的核心架构变革是什么?
答案要点:
- 无状态 (Stateless):WebGL 是状态机(全局状态容易错乱),WebGPU 使用 Pipeline State Object (PSO),预先定义好所有状态,切换开销极低。
- 低开销:更薄的驱动层,直接映射 Vulkan/Metal/DX12。
- 多线程:支持在 Worker 线程录制
CommandEncoder,主线程只负责提交。
Q42: Compute Shader (计算着色器) 为什么是革命性的?
答案要点:
- 通用计算:不再局限于画三角形。可以处理任意数据。
- 并行性:利用 GPU 的数千个核心进行物理模拟(布料、流体)、群组行为(Boids)、剔除计算。
- Storage Buffer:允许读写巨大的结构化数据,打破了 WebGL 纹理尺寸的限制。
Q43: Three.js 的 TSL (Three Shading Language) 解决了什么问题?
答案要点:
- 跨后端:开发者用 JS 写节点逻辑,Three.js 自动编译为 WGSL (WebGPU) 或 GLSL (WebGL)。
- 模块化:基于节点的 Shader 编写,比字符串拼接更易维护、复用和 Tree-shaking。
- 未来:是 Three.js 全面转向 WebGPU 的基石。
Q44: 什么是混合渲染 (Hybrid Rendering)?
答案要点:
- 结合 光栅化 (Rasterization) 和 光线追踪 (Ray Tracing)。
- 利用光栅化快速渲染几何体,利用光线追踪计算精确的反射、折射和软阴影。
- WebGPU 目前暂未正式开放光追扩展,但未来可期。
Q45: 什么是 Gaussian Splatting (高斯泼溅)?
答案要点:
- 原理:用数百万个带颜色的 3D 高斯椭球体表示场景。
- 优势:从照片重建 3D 场景的速度和质量远超传统摄影测量,且渲染速度极快(实时)。
- Three.js:已有第三方加载器支持
.splat文件渲染。
六、 进阶数学与算法 (Q46-Q60)
Q46: 如何判断点在三角形内部?(重心坐标)
答案要点:
- 重心坐标 (Barycentric Coordinates):三角形内任意一点 P 可表示为
P = uA + vB + wC,其中u+v+w=1且u,v,w >= 0。 - 应用:射线检测交点计算、属性插值(颜色、法线)。
Q47: 什么是 SDF (Signed Distance Field) 符号距离场?
答案要点:
- 定义:空间中任意一点到最近物体表面的距离。内部为负,外部为正。
- 应用:
- Raymarching:渲染体积云、分形几何。
- 字体渲染:MSDF (Multi-channel SDF) 实现无限放大不失真的文本。
Q48: 什么是 OBB (Oriented Bounding Box)?如何进行碰撞检测?
答案要点:
- OBB:有向包围盒,随物体旋转,比 AABB 更紧凑。
- 分离轴定理 (SAT):如果能找到一个轴,使得两个凸多面体在该轴上的投影不重叠,则它们不相交。检测 OBB 需要测试 15 个轴。
Q49: 四元数的 Slerp (球面线性插值) 原理。
答案要点:
- Lerp:线性插值,路径是弦,会导致旋转速度不均匀。
- Slerp:沿 4D 球面的大圆弧插值。保证角速度恒定,是 3D 旋转动画的标准插值方法。
Q50: 什么是噪声算法 (Perlin Noise / Simplex Noise)?
答案要点:
- 特点:伪随机、连续、平滑。
- 应用:程序化生成地形(高度图)、云层纹理、水面波纹、溶解特效。
Q51: 什么是八叉树 (Octree)?在 3D 中的应用?
答案要点:
- 结构:将 3D 空间递归划分为 8 个子立方体。
- 应用:
- 视锥体剔除:快速剔除大片不可见区域。
- 碰撞检测:只检测物体所在区域的三角形,大幅提升物理引擎性能。
Q52: 什么是骨骼动画的蒙皮 (Skinning) 算法?
答案要点:
- 矩阵调色板:将所有骨骼的变换矩阵传入 Shader。
- 顶点计算:
FinalPos = sum(BoneMatrix[i] * InitialPos * Weight[i])。 - 限制:通常限制每个顶点最多受 4 根骨骼影响(Attributes 限制)。
Q53: 什么是切线空间法线贴图 (Tangent Space Normal Mapping)?
答案要点:
- TBN 矩阵:由 Tangent, Bitangent, Normal 组成的矩阵,用于将切线空间的法线变换到世界空间。
- 计算:在 Fragment Shader 中构建 TBN 矩阵,解码法线贴图颜色,应用变换。
Q54: 什么是 Shadow Mapping 的 PCF (Percentage-Closer Filtering)?
答案要点:
- 原理:不只采样阴影图中对应的一个点,而是采样周围(如 3x3 或 5x5)区域。
- 结果:计算平均遮挡值,产生边缘柔和的阴影,消除锯齿。
Q55: 什么是 BRDF (双向反射分布函数)?
答案要点:
- 定义:描述光线射入表面后,向各个方向反射的比例。
f(l, v)。 - PBR:Cook-Torrance BRDF 包含漫反射项和镜面反射项(DFG 项:分布、菲涅尔、几何遮蔽)。
Q56: 什么是 HDR 的 IBL (Image Based Lighting) 卷积?
答案要点:
- 漫反射卷积:预计算 Irradiance Map,解决漫反射积分。
- 镜面反射卷积:预计算 Prefiltered Environment Map (Split Sum Approximation),解决不同粗糙度的镜面反射。
Q57: 什么是 SSAO (Screen Space Ambient Occlusion)?
答案要点:
- 原理:在屏幕空间,根据深度缓冲,采样像素周围的随机点。如果周围点大多深度更浅(被遮挡),则该像素变暗。
- 优点:与场景复杂度无关,实时性好。
Q58: 什么是 FXAA (Fast Approximate Anti-Aliasing)?
答案要点:
- 原理:后处理滤镜。检测图像中的边缘(高对比度区域),沿边缘方向进行模糊混合。
- 优缺:极快,但会导致画面整体轻微模糊,无法处理子像素移动闪烁。
Q59: 什么是 TAA (Temporal Anti-Aliasing)?
答案要点:
- 原理:利用上一帧的渲染结果。通过抖动投影矩阵,每帧采样像素内的不同位置,混合历史帧。
- 优缺:抗锯齿效果极佳,但会有“鬼影” (Ghosting) 现象,需要运动向量 (Motion Vectors) 进行重投影修正。
Q60: 什么是 Bloom (辉光) 的实现流程?
答案要点:
- 阈值提取:提取画面中亮度超过阈值的像素。
- 高斯模糊:对提取的亮部进行多次降采样和高斯模糊。
- 混合:将模糊后的亮部叠加回原图。
七、 综合实战与架构设计 (Q61-Q75)
Q61: 设计一个 3D 地图系统的架构(如智慧城市)。
答案要点:
- 瓦片化 (Tiling):将地图切分为金字塔瓦片(类似 Google Maps),按需加载。
- 坐标精度:解决浮点数抖动(RTC - Relative To Center),将渲染原点移动到相机附近。
- 数据压缩:建筑物使用 ExtrudeGeometry 合并,或 GPU Instancing。
- 流式加载:LRU 缓存策略,卸载不可见瓦片。
Q62: 如何实现一个高性能的粒子系统(如 100 万粒子)?
答案要点:
- CPU 方案:不可行。
- WebGL 方案:GPGPU。使用 DataTexture 存储位置/速度,Fragment Shader 更新物理状态,Vertex Shader 读取纹理渲染。
- WebGPU 方案:Compute Shader 更新 Storage Buffer,Render Pipeline 直接绘制。
Q63: 如何实现 3D 模型的拆解/爆炸效果?
答案要点:
- 预处理:确保模型由独立的 Mesh 或独立的 Face 组成。
- 动画:计算每个部件的包围盒中心,沿中心向外插值位置。
- Shader:在 Vertex Shader 中根据
attribute center和uniform progress偏移顶点。
Q64: 如何实现“传送门”效果?
答案要点:
- 渲染到纹理:在传送门位置放置一个摄像机,渲染目标场景到 RenderTarget。
- 投影纹理:将 RenderTarget 作为纹理贴在传送门 Mesh 上,使用屏幕空间坐标采样。
- 递归:如果传送门互相可见,需要递归渲染(通常限制次数)。
Q65: 如何实现体积云 (Volumetric Clouds)?
答案要点:
- Raymarching:在 Fragment Shader 中沿视线步进。
- 噪声采样:采样 3D 噪声纹理(Perlin/Worley)模拟密度。
- 光照计算:Beer's Law (比尔定律) 计算光线穿透密度的衰减。
Q66: 如何优化骨骼动画的 CPU 开销?
答案要点:
- GPU Skinning:Three.js 默认已做。
- 动画烘焙 (Baking):将骨骼变换预计算并存储到浮点纹理中 (VAT - Vertex Animation Texture)。Vertex Shader 直接读取纹理,移除骨骼计算逻辑。
Q67: 如何实现贴花 (Decal) 系统(如弹孔)?
答案要点:
- 投影几何体:裁剪目标物体与贴花包围盒相交的三角形,投影 UV。
- 深度偏移:
polygonOffset防止 Z-Fighting。 - 延迟贴花:在延迟渲染管线中,通过投影矩阵直接修改 G-Buffer。
Q68: 如何实现无限大地形?
答案要点:
- 网格复用:使用环形缓冲区 (Toroidal Array) 复用 Grid Mesh。
- 顶点位移:Vertex Shader 中根据世界坐标采样高度图/噪声。
- LOD:近处高密度网格,远处低密度。
Q69: 如何实现 3D 拾取(不使用 Raycaster)?
答案要点:
- GPU Picking:
- 为每个物体分配唯一的颜色 ID。
- 在离屏 RenderTarget 中渲染 ID 颜色。
- 读取鼠标位置像素的颜色,解码为 ID。
- 优势:性能与物体数量无关,适合海量物体。
Q70: 如何处理透明物体的复杂排序(如头发)?
答案要点:
- Alpha Test:不透明部分直接写入深度。
- 多Pass:先渲染背面,再渲染正面。
- OIT:加权混合 OIT (Weighted Blended OIT),不依赖排序的近似算法。
Q71: 什么是 DrawIndirect (间接绘制)?
答案要点:
- 原理:绘制参数(顶点数、实例数)存储在 GPU Buffer 中,而不是由 CPU 指定。
- 应用:GPU 剔除 (GPU Culling)。Compute Shader 计算可见性,修改 Draw 参数,渲染管线直接执行。实现零 CPU 剔除开销。
Q72: 如何实现动态切割模型?
答案要点:
- CSG (Constructive Solid Geometry):布尔运算库(如
three-bvh-csg)。 - Stencil:利用模板缓冲遮罩实现视觉上的切割(模型未真正改变)。
- Shader Discard:根据世界坐标 discard 像素(空心效果)。
Q73: 什么是 Virtual Texture (虚拟纹理) / SVT?
答案要点:
- 原理:将超大纹理切片,只加载当前视野可见的切片到显存。
- 应用:超精细地形、巨型场景。
Q74: 如何实现即时全局光照 (Real-time GI)?
答案要点:
- Light Propagation Volumes (LPV)。
- VXGI (Voxel Cone Tracing)。
- SSGI (Screen Space GI):基于屏幕空间的近似,Three.js 扩展示例中有实现。
Q75: 什么是 Cluster Lighting (集群光照)?
答案要点:
- 问题:Forward Rendering 限制光源数量。
- 解决:将视锥体划分为 3D 网格 (Clusters)。Compute Shader 计算每个 Cluster 受哪些光源影响。Fragment Shader 只计算当前 Cluster 内的光源。支持成千上万光源。
八、 补充深度问题 (Q76-Q100)
Q76: 解释 Projection Matrix 中的 Near 和 Far 对深度精度的影响。
答案要点:
- 非线性深度:深度缓冲是非线性的,大部分精度集中在 Near 平面附近。
- 影响:Near 太小或 Far 太大,会导致远处物体精度极低,产生 Z-Fighting。
- 建议:Near 尽可能大,Far 尽可能小,或使用反向 Z-Buffer (Reversed Z)。
Q77: 什么是 Early-Z 和 Z-Prepass?
答案要点:
- Early-Z:在 Fragment Shader 执行前进行深度测试,若失败则跳过 Shader,节省性能。
- Z-Prepass:先只渲染深度(不计算颜色),生成完善的深度缓冲。再进行颜色渲染,最大化利用 Early-Z。
Q78: 什么是 Mipmap 的 Bias?
答案要点:
- 控制采样 Mipmap 的层级。负值会让纹理更清晰(但可能闪烁),正值更模糊。
Q79: 什么是 Texture Atlas 的 Bleeding (出血) 问题?
答案要点:
- 现象:采样时读取到了相邻子图的像素,导致边缘出现杂色。
- 解决:子图之间保留 Padding(间隙)。
Q80: 什么是 Equirectangular Projection?
答案要点:
- 将球体表面展开为 2:1 矩形的投影方式。常用于 360 度全景图。
Q81: 什么是 MorphTarget 的 Normal 变换问题?
答案要点:
- 变形时,顶点位置变了,法线也需要随之插值,否则光照会错误。Three.js 自动处理,但需确保导出时包含 MorphNormals。
Q82: 什么是 SkinnedMesh 的 Bind Pose?
答案要点:
- 骨骼绑定时的初始姿态。所有骨骼变换都是相对于 Bind Pose 的逆矩阵计算的。
Q83: 什么是 Animation 的 Blending Mode (Additive vs Override)?
答案要点:
- Override:完全覆盖(如 走路 -> 跑步)。
- Additive:叠加(如 跑步 + 射击动作)。
Q84: 什么是 Quaternion 的 Slerp vs Nlerp?
答案要点:
- Slerp:球面插值,角速度恒定,计算重。
- Nlerp:线性插值后归一化,角速度不匀,但计算快。小角度可用 Nlerp 近似。
Q85: 什么是 AABB 的合并算法?
答案要点:
min = min(box1.min, box2.min),max = max(box1.max, box2.max)。用于构建 BVH。
Q86: 什么是 Plane 的 Hessian Normal Form?
答案要点:
Ax + By + Cz + D = 0,其中(A,B,C)是法线。用于视锥体剔除计算点到平面的距离。
Q87: 什么是 Gamma Space 和 Linear Space 的混合错误?
答案要点:
- 如果在 sRGB 空间做光照混合(如
0.5 + 0.5),结果物理不正确,亮度偏暗。必须在 Linear 空间计算。
Q88: 什么是 Premultiplied Alpha 的混合公式?
答案要点:
Blend = Src + Dst * (1 - SrcAlpha)。无需乘 SrcAlpha,因为已经乘过了。
Q89: 什么是 WebGL 的 Context Loss (上下文丢失)?
答案要点:
- 原因:GPU 崩溃、驱动更新、占用过高。
- 处理:监听
webglcontextlost事件,取消动画循环;监听webglcontextrestored,重新创建所有 WebGL 资源。
Q90: 什么是 High-DPI (Retina) 渲染的性能陷阱?
答案要点:
- 像素数是 4 倍。Fragment Shader 开销激增。
- 策略:对于复杂 3D 场景,限制
pixelRatio为 1.5 或 2,或者动态调整。
Q91: WebGPU 的 BindGroup 是什么?
答案要点:
- 将一组资源(Buffer, Texture, Sampler)打包绑定。
- 减少 CPU 提交绑定的频率,类似 Vulkan 的 Descriptor Set。
Q92: WebGPU 的 WGSL 语言特点?
答案要点:
- Rust 风格语法。
- 强类型。
- 内置矩阵、向量运算。
Q93: 什么是 Render Bundle (WebGPU)?
答案要点:
- 预录制的渲染命令包。
- 可以在每一帧重复执行,无需重新遍历场景图录制命令,极大降低 CPU 开销。
Q94: 什么是 Timestamp Query (WebGPU)?
答案要点:
- 精确测量 GPU 执行命令的时间(纳秒级)。用于性能分析。
Q95: 什么是 Reverse Z (反向 Z)?
答案要点:
- Near=1.0, Far=0.0。配合浮点深度缓冲。
- 大幅提升远处的深度精度,彻底解决 Z-Fighting。WebGPU 推荐做法。
Q96: 什么是 Mesh Shader (未来技术)?
答案要点:
- 取代传统的 Vertex/Geometry Shader 管线。
- 以 Meshlet(小网格块)为单位进行处理和剔除,几何处理能力呈指数级提升。
Q97: 什么是 Variable Rate Shading (VRS)?
答案要点:
- 对画面不同区域使用不同的着色分辨率。
- 例如:中心区域全分辨率,边缘区域 1/2 分辨率。提升性能且视觉损失小。
Q98: 什么是 Nanite (UE5) 的核心原理?Web 能实现吗?
答案要点:
- 原理:虚拟几何体。实时流式加载 Meshlet,根据屏幕误差动态选择 LOD,软件光栅化微小三角形。
- Web:WebGPU Compute Shader 可以实现简化版。
Q99: 什么是 Lumen (UE5) 的核心原理?
答案要点:
- 原理:基于 SDF 的软件光线追踪 + 屏幕空间探针。
- Web:硬件光追普及前,SDF Tracing 是 Web 端实现动态 GI 的可行路径。
Q100: 作为图形工程师,如何保持技术敏感度?
答案要点:
- 关注 SIGGRAPH 论文。
- 关注 Three.js / Babylon.js GitHub 提交记录。
- 学习原生图形 API (Vulkan/DX12/WebGPU)。
- ShaderToy 练习。