Skip to main content

光照与材质

光照模型(Lighting Model)

面试题:经典光照模型(Phong)由哪几部分组成?

冯氏光照模型(Phong)把物体表面颜色拆成三部分叠加:

  1. 环境光(Ambient):模拟全局间接光,给所有表面一个基础亮度,与方向无关。ambient = 环境光强 × 材质色
  2. 漫反射(Diffuse):粗糙表面对光的均匀散射,亮度取决于光线方向 L 与法线 N 的夹角diffuse = max(dot(N, L), 0) × 光强 × 材质色(兰伯特余弦定律)。
  3. 镜面反射(Specular):光滑表面的高光,取决于反射方向 R 与视线方向 V 的夹角specular = pow(max(dot(R, V), 0), shininess)

Phong vs Blinn-Phong:Blinn-Phong 用「半程向量 H = normalize(L + V)」与法线点乘代替 R·V,计算更快、高光更自然,是实时渲染常用近似。

Three.js 光源类型

光源说明是否产生阴影
AmbientLight环境光,均匀照亮,无方向
DirectionalLight平行光(如太阳),方向一致
PointLight点光源(如灯泡),向四周发散,有衰减
SpotLight聚光灯,锥形范围
HemisphereLight半球光,模拟天空/地面环境色
RectAreaLight矩形区域光,模拟窗户/灯管

材质对比

面试题:Three.js 常见材质有什么区别?性能如何排序?

材质光照特点性能
MeshBasicMaterial不受光纯色/纹理,恒定亮度最快
MeshLambertMaterial漫反射逐顶点计算光照,无高光
MeshPhongMaterialPhong逐片元计算,有高光
MeshStandardMaterialPBR基于物理,metalness/roughness较慢
MeshPhysicalMaterialPBR 增强额外支持清漆、透射、镜面等最慢

选型原则:不需要光照(如 UI、辉光)用 Basic;要写实用 Standard/Physical;性能吃紧且要求不高用 Lambert/Phong。

PBR(基于物理的渲染)

面试题:什么是 PBR?为什么比 Phong 更真实?

  • PBR(Physically Based Rendering)用物理参数而非经验参数描述材质,核心是 金属度(metalness)粗糙度(roughness)
  • 遵循能量守恒(反射的光不会超过入射光),在不同光照环境下都保持一致、可信的表现。
  • 通常配合 环境贴图(Environment Map)/ IBL(基于图像的光照) 来提供真实的环境反射,金属材质尤其依赖它。
  • 常见贴图:albedo(基础色)、normal(法线)、metalnessMap、roughnessMap、aoMap(环境光遮蔽)、emissive(自发光)。

阴影机制与性能陷阱

面试题:实时阴影(Shadow Mapping)的底层原理是什么?为什么开启阴影会导致严重的性能下降?

1. 底层原理:Shadow Mapping 算法

Three.js 底层默认使用 Shadow Mapping(阴影贴图) 算法来实现实时阴影。这个算法的核心在于“两次渲染”机制:

  1. 第一阶段(深度图渲染 - Depth Pass)
    • 将相机的视角“瞬移”到光源的位置(这就是为什么代码里要配置 light.shadow.camera)。
    • 以光源的视角将整个场景渲染一遍。但这次渲染不计算颜色,而是只计算离光源最近的物体的深度值(Z值),并将这些值记录到一张名为阴影贴图 (Shadow Map) 的离屏纹理中。
  2. 第二阶段(正式渲染 - Color Pass)
    • 将相机切回真实的玩家视角,正常渲染场景。
    • 在片元着色器(Fragment Shader)计算每个像素时,将该像素的坐标反向转换回“光源坐标系”。
    • 关键比对:将算出的深度值,与刚才阴影贴图(Shadow Map)里记录的该位置的深度值进行比对。
    • 如果当前像素的深度 大于 贴图里的深度,说明在光源和该像素之间有其他物体挡着了,该像素就在阴影中(压暗颜色);否则就在光照下。

2. 为什么开启阴影极度消耗性能?

一旦面试官问到性能,你必须从渲染管线的角度指出开启阴影的“致命开销”:

  1. Draw Call 数量直接翻倍(甚至翻 N 倍)
    • 因为 Shadow Mapping 需要“两次渲染”,如果你的场景有 1000 个 Mesh,正常渲染是 1000 次 Draw Call。一旦开启了阴影(castShadow = true),系统必须先以光源视角再画一遍这 1000 个物体。总 Draw Call 会瞬间飙升到 2000 次!
    • 如果场景里有 3 个投射阴影的光源,那就要以 3 个光源视角各画一遍深度图,总 Draw Call 会变成 4000 次!这在移动端浏览器上是灾难性的。
  2. GPU 显存占用与采样带宽激增
    • 阴影贴图本质上是一张极高分辨率的纹理(为了让阴影边缘不出现马赛克,通常需要将 light.shadow.mapSize 设为 2048x2048 甚至 4096x4096)。这不仅生吃巨大的显存,而且在第二阶段渲染时,片元着色器必须高频去采样这张庞大的贴图,给 GPU 带宽带来极大压力。
  3. 软阴影计算(PCF / PCSS)开销
    • 原生的 Shadow Map 边缘是硬锯齿状的。为了实现柔和的软阴影(Soft Shadow),Three.js 默认会在着色器里进行多次周边像素采样模糊(PCF)。采样的点数越多,GPU 的运算负担越重。

3. 在 Three.js 中的基础配置

// 1. 渲染器全局开启阴影计算,并可设置软阴影类型 (如 PCFSoftShadowMap)
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

// 2. 光源开启投射
light.castShadow = true;
// 关键优化:限制光源阴影相机的视野范围,越紧凑阴影越清晰且省性能
light.shadow.camera.near = 0.5;
light.shadow.camera.far = 50;

// 3. 物体参与阴影投射和接收
mesh.castShadow = true; // 这个物体会挡住光,投下阴影
ground.receiveShadow = true; // 这个地面会显示别人投下来的阴影

常见问题

  • 阴影表面出现诡异的条纹斑点(Shadow Acne):因为浮点数精度误差导致物体错误地遮挡了自己。解决办法是微调 light.shadow.bias = -0.0001(给深度加一点偏移)。
  • 优化方案:在静态场景(如建筑漫游)中,坚决不用实时阴影!必须在 Blender 里把阴影烘焙(Bake)死在贴图上,前端直接当成普通纹理加载,零性能损耗。

纹理与采样原理

  • UV 坐标:把 2D 纹理映射到 3D 表面的坐标系统(范围 0~1)。U 代表水平方向,V 代表垂直方向。

1. Mipmap(多级渐远纹理)

面试题:如果把一张 4K 的高清贴图贴在一个距离相机非常远、在屏幕上只有 10x10 像素的物体上,会发生什么?

  • 问题(摩尔纹与闪烁):如果没有 Mipmap,GPU 在渲染那 10x10 个像素时,只能在 4K 贴图里极其“跳跃”地随机抽取 100 个像素点。当你稍微移动相机,抽取的像素就会剧烈变化,导致远处的纹理疯狂闪烁,出现密集的网格状波纹(摩尔纹 / Moiré pattern)。
  • Mipmap 的解法:在加载纹理时,GPU 会提前生成一系列逐渐缩小的图(比如 2048、1024、512...直到 1x1)。当物体离相机远时,GPU 会自动选用那张小分辨率的贴图进行采样。
  • 代价:Mipmap 会额外多占用约 33% 的显存,但换来了极大的远景抗锯齿提升和采样性能优化(采样小图更快)。Three.js 默认开启。

2. 纹理过滤(Texture Filtering)

当纹理被放大(贴近相机)或缩小(远离相机)时,GPU 应该如何决定屏幕像素的颜色?这由 magFilter(放大过滤)和 minFilter(缩小过滤)决定。

  • NearestFilter(最近邻过滤)
    • 原理:简单粗暴,直接选取离目标位置最近的那一个像素的颜色。
    • 表现:当相机贴近物体时,能看到明显的马赛克/大方块
    • 应用场景:《我的世界 (Minecraft)》风格的体素游戏、2D 像素风游戏。在这种场景下,必须手动将材质的 magFilter 设为 THREE.NearestFilter,否则像素边缘会被模糊掉。
  • LinearFilter(双线性插值过滤)
    • 原理:提取目标位置周围的 4 个像素,按距离权重混合出一个平滑的颜色。
    • 表现:当相机贴近时,马赛克消失,取而代之的是平滑的模糊感
    • 应用场景:绝大多数写实 3D 游戏和 Web3D 项目的标准配置(Three.js 默认值)。

3. GPU 纹理压缩(KTX2 / Basis)

面试题:我把贴图存成了 100KB 的 JPG 格式,为什么一加载到场景里,显存就爆了?

  • 传统格式的陷阱:PNG 或 JPG 是为了网络传输和硬盘存储而设计的压缩算法(类似 ZIP)。GPU 硬件在渲染时是不认识 JPG 的。浏览器必须在 CPU 中把这 100KB 的 JPG 解码成最原始的 RGBA 像素矩阵(位图)后,再推送到 GPU 显存里。一张 2048x2048 的图,无论 JPG 有多小,解压到显存中必定占用 2048 * 2048 * 4 Bytes = 16MB 显存!
  • 硬件级纹理压缩(Block Compression):KTX2 / Basis 格式采用的是 GPU 原生支持的块压缩算法(如 ASTC, ETC, DXT)。这种格式的贴图在下载后,不需要在 CPU 解码,直接以压缩状态塞进 GPU 显存。GPU 在渲染采样时可以通过硬件电路瞬间解压读取。
  • 优势:显存占用和 GPU 带宽消耗通常能降低到原来的 1/4 到 1/8,彻底解决 WebGL 在移动端浏览器经常遇到的 OOM(内存溢出)崩溃问题。