一句话:光打到光滑表面上,像台球撞墙一样按固定角度弹开。只有当你的眼睛正好在弹开的方向上,才能看到它。
金属的亮点、湿地面的倒影、湖面远处的反光、镜子里的人,都属于镜面反射。
在游戏和图形学里,"镜面反射"通常指两件相关的事:
| 叫法 | 反射的是什么 | 例子 |
|---|---|---|
| 镜面高光(Specular Highlight) | 光源本身 | 苹果表面的白色亮点、头盔上的光斑 |
| 镜面反射 / 环境反射(Reflection) | 周围的整个环境 | 镜子、湖面倒影、车漆上映出的天空 |
两者原理相同。高光可以看成"只反射了一盏灯",环境反射则是"把四面八方都反射了"。
一、先分清:漫反射 vs 镜面反射
拿手电筒分别照一张白纸和一面镜子:
白纸(漫反射) 镜子(镜面反射) 光 光 ↘ ↘ ↖ ↑ ↗ ↘ → ← 向四面八方散开 ────●──── ═══════════ ↗ ← 只往一个方向弹 👁 只有站在这里才看得到| 漫反射 | 镜面反射 | |
|---|---|---|
| 光往哪走 | 各个方向均匀散开 | 集中在反射方向附近 |
| 换个角度看 | 亮度不变 | 亮点会移动,甚至消失 |
| 颜色来自 | 物体本身(红苹果是红的) | 主要是光的颜色(红苹果的高光是白的) |
| 和什么有关 | 光源方向 | 光源方向+ 眼睛方向 |
可以自己试一下:盯着桌上的杯子左右晃头,杯子的颜色不变(漫反射),杯子上的亮点却跟着你移动(镜面反射)。镜面反射依赖视角,这是它和漫反射最本质的区别。
二、物理原理
1. 反射定律:入射角 = 反射角
N(法线) L ↑ R ↖ │ ↗ ↖ │ ↗ θ │ θ ═════════●═════════L是指向光源的方向,N是法线,反射方向是:
R = 2(N·L)N − LGLSL 自带reflect(I, N)。注意它的I是入射方向(从光源射向表面),也就是−L,方向反了结果就错。
2. 微表面:为什么高光有大有小
真正的镜子极其光滑,所有光都朝同一个方向弹走,所以你能看到清晰的倒影。
大多数表面在显微镜下是凹凸不平的,可以看成无数块朝向各异的"小镜子"(微表面):
光滑表面(粗糙度低) 粗糙表面(粗糙度高) ───────────── ╱╲╱╲_╱╲╱‾╲╱ 反射光集中 → 高光小而亮 反射光散开 → 高光大而暗淡 例:镜子、抛光金属、湿地面 例:磨砂塑料、橡胶、干燥的石头粗糙度(Roughness)就是在描述这些小镜子有多乱。现代引擎里,美术调材质最常动的参数之一就是它。
3. 菲涅尔效应:越斜着看越像镜子
站在湖边:
- 低头看脚下的水,几乎是透明的,能看见水底的石头;
- 抬头看远处的湖面,像一面镜子,映着天空和对岸的山。
这就是菲涅尔效应(Fresnel):视线越贴着表面(掠射角),反射越强。常用 Schlick 近似:
F = F0 + (1 − F0) × (1 − cosθ)^5 θ 是视线与法线的夹角以水为例(F0 ≈ 0.02):
| 视线与法线的夹角 | 反射比例 | 看到的 |
|---|---|---|
| 0°(正对着看) | 约 2% | 几乎看透水面 |
| 80° | 约 40% | 倒影明显 |
| 89°(贴着水面) | 约 92% | 几乎是镜子 |
这个效应不只出现在水面上:木桌、塑料、皮肤,甚至一张纸,斜着看都会变亮。游戏里物体边缘泛起的一圈亮光,很大一部分来自它。
三、经典模型:Phong 与 Blinn-Phong
早期 GPU 算力有限,用简单的经验公式模拟高光,现在的风格化游戏和移动端仍在大量使用。
1. Phong:看反射方向离眼睛有多近
高光 = (R · V)^n V 是指向眼睛的方向R 和 V 越接近,点积越接近 1,高光越亮。
2. Blinn-Phong:用半程向量,更快也更好
H = normalize(L + V) 光线方向和视线方向的"中间方向" 高光 = (N · H)^n意思是:如果某块小镜子的法线正好是 H,它就能把光反射进眼睛。所以 N 越接近 H,高光越强。
vec3 H = normalize(L + V); float spec = pow(max(dot(N, H), 0.0), shininess); vec3 color = diffuse * max(dot(N, L), 0.0) + specColor * spec;选它而不是 Phong 有两个原因:计算更便宜,而且 Phong 在掠射角时R·V会小于 0,高光被突然切断;Blinn-Phong 没有这个问题,效果更接近真实。
同样大小的高光,Blinn-Phong 的指数要比 Phong 大约 2~4 倍,从 Phong 迁移过来时要重新调参数。
3. 指数 n:高光的"锐利度"
用数字看 n 的作用:
| N 与 H 的夹角 | n = 8(塑料) | n = 128(抛光金属) |
|---|---|---|
| 0° | 1.00 | 1.00 |
| 10° | 0.89 | 0.15 |
| 20° | 0.61 | 0.0004 |
n 小,偏离 20° 还有 61% 的亮度,高光是一大片模糊的光晕;n 大,偏离 10° 就只剩 15%,高光是一个锐利的小亮点。n 越大,表面越光滑。
四、现代做法:PBR(基于物理的渲染)
Blinn-Phong 是"看着像就行"的经验公式,有两个明显问题:调参全靠感觉,同一组参数换个光照环境就不对了;能量也不守恒,高光调亮时物体整体可能比照到它的光还亮。
现在主流引擎(Unity HDRP/URP、Unreal)使用Cook-Torrance 微表面模型,高光由三项相乘:
D × F × G 高光 = ───────────────────── 4 (N·L) (N·V)| 项 | 名称 | 回答的问题 | 形象理解 |
|---|---|---|---|
| D | 法线分布(常用 GGX) | 有多少小镜子正好朝向 H? | 决定高光的大小和形状 |
| F | 菲涅尔 | 这个角度反射多少光? | 斜着看更亮 |
| G | 几何遮蔽 | 有多少光被旁边的凸起挡住了? | 粗糙表面在掠射角时会变暗 |
float D_GGX(float NoH, float roughness) { float a = roughness * roughness; float a2 = a * a; float d = NoH * NoH * (a2 - 1.0) + 1.0; return a2 / (3.14159 * d * d); } vec3 F_Schlick(vec3 F0, float VoH) { return F0 + (1.0 - F0) * pow(1.0 - VoH, 5.0); }GGX 比 Blinn-Phong 多了一条长长的"拖尾":高光中心之外还有一圈柔和的光晕,更接近真实材质。
金属和非金属的区别
PBR 里最重要的一个参数是金属度(Metallic),它决定了 F0(正对着看时的反射率):
| 材质 | F0 | 高光颜色 | 漫反射 |
|---|---|---|---|
| 塑料、木头、石头、皮肤(非金属) | 约 0.04(4%) | 白色(光源的颜色) | 有,物体颜色来自这里 |
| 金 | (1.00, 0.71, 0.29) | 金色 | 几乎没有 |
| 银 | (0.95, 0.93, 0.88) | 近白色 | 几乎没有 |
| 铁 | (0.56, 0.57, 0.58) | 灰色 | 几乎没有 |
结论:非金属的颜色在漫反射里,高光是白的;金属没有漫反射,颜色全在高光(反射)里。金子之所以是金色,是因为它反射出来的光是金色的。
同时,PBR 保证能量守恒:被镜面反射走的光,不会再参与漫反射。所以表面越光滑,高光越集中、越亮,但总能量不变。
五、反射整个环境:从高光到倒影
上面只计算了几盏灯的高光。但真实的镜面反射应该映出周围所有东西:天空、墙壁、旁边的角色。
这个问题难在:要知道反射方向 R 上有什么,相当于要从表面再"看"一次场景。游戏里有几种不同成本的做法:
1. 环境贴图(Cubemap)
提前在某个位置拍一张六个面的全景图,着色时沿 R 方向直接查:
vec3 R = reflect(-V, N); vec3 env = textureLod(envCubemap, R, roughness * maxMip).rgb;和上一篇的纹理过滤直接相关:粗糙的表面反射是模糊的,所以预先把 Cubemap 卷积模糊,存在各级 Mip 里,粗糙度越高取越模糊的级别。这就是 PBR 中"预过滤环境贴图 + BRDF 查找表"(Split-Sum 近似)的核心思路。
2. 反射探针(Reflection Probe)
在场景里放很多个 Cubemap 拍摄点,物体使用离自己最近的那个,还可以用盒体投影(Box Projection)修正室内的反射位置。
3. 平面反射(Planar Reflection)
专门用于镜子、平静水面。把摄像机沿反射平面翻转到"镜子背后",把场景再渲染一遍:
真实摄像机 📷 ↘ ══════════════●══════════ ← 镜子 / 水面 ↗ 镜像摄像机 📷(在平面下方,倒着看)画面完全正确,但每多一面镜子,场景就要多渲染一次。所以游戏里能照出人的镜子往往很少,而且常常和场景的其他部分隔开。
4. 屏幕空间反射(SSR)
利用当前帧已经渲染好的颜色和深度,沿 R 方向在屏幕上步进,找到和深度缓冲相交的位置,取那里的颜色。
- 优点:便宜,任意形状的表面都能用,动态物体也能反射;
- 致命缺点:屏幕上没有的东西,反射里也没有。
5. 光线追踪反射
真正从表面沿 R 发射光线,在整个场景中求交。最准确,但最昂贵,需要硬件支持,通常还要降低分辨率并配合降噪。
对比
| 方法 | 准确度 | 动态物体 | 成本 | 典型用途 |
|---|---|---|---|---|
| Cubemap / 反射探针 | 低(位置不准) | 通常不能 | 很低 | 几乎所有物体的基础反射 |
| 平面反射 | 高 | 能 | 高(多渲染一遍) | 镜子、平静的水面 |
| SSR | 中 | 能 | 中 | 湿地面、光滑地板 |
| 光线追踪 | 最高 | 能 | 很高 | 高端 PC / 主机 |
实际游戏通常混合使用:先用 SSR,SSR 找不到的地方退回反射探针,最远处退回天空盒。
六、案例
案例 1:高光为什么总是白色的
现象:红色塑料球的高光是白色,金戒指的高光却是金色。
原因:塑料是非金属,F0 约 0.04 且无色,反射的是光源本身的颜色;金是金属,F0 本身带颜色。
常见错误:把非金属的高光颜色设成物体颜色,结果看起来像金属;或者金属材质设置了漫反射颜色,看起来像涂了漆的塑料。
案例 2:背光面也有高光
现象:灯在物体背后,物体朝向摄像机的一面却出现了高光。
原因:只计算了N·H,没有检查光是否照到了这一面。
修复:高光乘以N·L > 0的判断,或者直接乘以max(N·L, 0)。
案例 3:远处的金属表面闪烁
现象:远处的金属栏杆、细密的法线贴图上,高光像雪花一样闪。
原因:高光走样。高光非常锐利,一个像素覆盖的区域里法线变化很大,有时采样点正好落在亮点上,有时落不到。上一篇讲过,法线贴图 Mipmap 后向量会变短,也会加剧这个问题。
修复:根据法线的变化程度提高粗糙度(Toksvig、Specular AA),再配合 TAA。
案例 4:地板倒影在屏幕边缘消失
现象:光滑地板上的倒影,往下看时上方物体的倒影突然被截断,或者边缘一片模糊。
原因:SSR 只能反射屏幕上可见的东西,物体移出画面,它的倒影也就没了。
修复:在屏幕边缘逐渐淡出 SSR,交给反射探针接管;高端方案改用光线追踪。
案例 5:室内物体反射出天空
现象:地下室里的金属桶,表面映着蓝天白云。
原因:附近没有放反射探针,物体使用了默认的天空盒作为环境反射。
修复:在室内放置反射探针,并设置合适的影响范围和盒体投影。
案例 6:湖面看起来像一块塑料
现象:水面无论从什么角度看,反射强度都一样,显得很假。
原因:没有加菲涅尔项。
修复:按视角计算 Fresnel,近处透明、远处反光,水面立刻显得真实。
案例 7:金属在没有环境光的场景里发黑
现象:在空旷场景里只放一盏方向光,金属球除了一个亮点,其余全是黑的。
原因:金属几乎没有漫反射,它的外观全靠反射周围环境。周围什么都没有,它就是黑的。
修复:提供天空盒或反射探针。这也是为什么调 PBR 材质时,场景里一定要有环境光照。
案例 8:镜子里的角色和本人不同步
现象:平面反射里,角色的动作慢一帧,或者特效没出现在镜子里。
原因:反射相机在主相机之前渲染,用的还是上一帧的数据;或者某些特效没有加入反射相机的渲染层。
修复:调整更新顺序,检查反射相机的剔除层设置。
七、总结
光打到表面 │ ├─ 散向各个方向 ──▶ 漫反射:颜色来自物体,视角无关 │ └─ 按反射定律弹开 ──▶ 镜面反射:视角相关 ├─ 粗糙度:小镜子有多乱 → 高光的大小 ├─ 菲涅尔:越斜着看越亮 ├─ 金属度:反射光有没有颜色 └─ 反射什么 ├─ 只有光源 → 高光(Blinn-Phong / GGX) └─ 整个环境 → Cubemap / 平面反射 / SSR / 光追| 问题 | 答案 |
|---|---|
| 镜面反射是什么 | 光按反射定律集中往一个方向弹开 |
| 和漫反射的区别 | 依赖视角,亮点会随眼睛移动 |
| 高光大小由什么决定 | 粗糙度(微表面的混乱程度),经典模型中是指数 n |
| 为什么斜着看更亮 | 菲涅尔效应 |
| 金属和非金属 | 非金属高光是白的、颜色在漫反射;金属高光带颜色、没有漫反射 |
| 经典模型 | Blinn-Phong:(N·H)^n |
| 现代模型 | Cook-Torrance:D(分布)× F(菲涅尔)× G(遮蔽) |
| 环境反射方案 | Cubemap、反射探针、平面反射、SSR、光线追踪 |
| 常见坑 | 背光高光、高光闪烁、SSR 边缘消失、室内反射天空、缺少菲涅尔、金属发黑 |
一句话:漫反射告诉你物体是什么颜色,镜面反射告诉你物体有多光滑,以及它周围有什么。高光是它映出的灯,倒影是它映出的整个世界。