中级OpenGL教程 006:高光反射原理与 Shader 实现
- [Bilibili 同步视频](#Bilibili 同步视频)
- [一、何为高光反射?✨ 光的镜面 "反弹" 艺术](#一、何为高光反射?✨ 光的镜面 “反弹” 艺术)
- [二、高光反射的核心逻辑 🎯 夹角决定亮度](#二、高光反射的核心逻辑 🎯 夹角决定亮度)
- [三、高光反射 Shader 计算全流程 💻 从向量到像素](#三、高光反射 Shader 计算全流程 💻 从向量到像素)
-
- [1. 第一步:计算光线反射方向(Light Reflection)](#1. 第一步:计算光线反射方向(Light Reflection))
- [2. 第二步:计算观察方向(View Direction)](#2. 第二步:计算观察方向(View Direction))
- [3. 第三步:点乘计算高光基础系数](#3. 第三步:点乘计算高光基础系数)
- [4. 第四步:负值截断(Max 函数优化)](#4. 第四步:负值截断(Max 函数优化))
- [四、关键参数详解 📍 让高光更真实](#四、关键参数详解 📍 让高光更真实)
-
- [1. 单位化(Normalize)的必要性](#1. 单位化(Normalize)的必要性)
- [2. 世界坐标(World Position)的计算](#2. 世界坐标(World Position)的计算)
- [3. Uniform 变量的使用](#3. Uniform 变量的使用)
- [五、总结 📝 高光反射核心速记](#五、总结 📝 高光反射核心速记)
Bilibili 同步视频
在三维渲染的世界里,光与物体的交互是构建真实感画面的核心。上一章节我们深入探索了漫反射(Diffuse)的底层逻辑与代码实现,理解了光线在物体表面被均匀散射的视觉效果。而今天,我们将解锁光影渲染的另一关键模块 ------高光反射,它是让物体呈现金属质感、玻璃光泽、漆面高亮的核心密码,也是真实感渲染中不可或缺的一环。
一、何为高光反射?✨ 光的镜面 "反弹" 艺术
我们先回归物理本质:当光线照射到物体表面时,一部分光被物体吸收转化为其他能量,另一部分光则未被吸收,直接在物体表面发生反弹 ,这种遵循入射角 = 出射角 规律的反射形式,就是镜面反射,而高光反射正是镜面反射在渲染中的具象呈现。
在现实世界中,不存在绝对光滑的物体表面:
-
绝对光滑的表面,光线会沿唯一反射方向射出,仅在特定角度能看到亮光;
-
日常物体表面存在细微粗糙纹理,光线会在主反射方向周边形成发散式反射,最终在视觉上呈现出中心高亮、边缘渐弱的圆形高光斑点。
这也就解释了:高光的强弱,完全由观察方向与光线主反射方向的夹角决定。夹角越小,视线越贴近反射方向,高光越明亮;夹角越大,高光越暗淡,直至消失。
二、高光反射的核心逻辑 🎯 夹角决定亮度
我们为高光反射建立基础向量模型,明确三个核心要素:
-
法线向量(Normal):物体表面像素的垂直方向,是反射计算的基准;
-
入射光方向(Light Direction):光源照射到物体表面的方向;
-
观察方向(View Direction):摄像机指向当前渲染像素的方向;
-
反射方向(Light Reflection):光线经表面反弹后的主方向。
基于物理规律,我们得出高光反射的核心结论:
高光强度 ∝ 观察方向与反射方向的夹角余弦值(cosθ)
👉 θ 越小 → cosθ 越大 → 高光越亮
👉 θ 越大 → cosθ 越小 → 高光越弱
在 0°~90° 范围内,余弦值完美匹配高光的衰减规律,这也是我们用 cosθ 量化高光强度的核心原因。
三、高光反射 Shader 计算全流程 💻 从向量到像素
高光反射的计算逻辑与漫反射同源,核心是计算光线因观察偏差产生的损耗,最终通过 GLSL 内置函数与向量运算实现,全程无需复杂数学推导。
1. 第一步:计算光线反射方向(Light Reflection)
GLSL 提供了开箱即用的 reflect() 内置函数,只需传入入射光方向 和法线方向,即可自动算出反射方向:
glsl
// 计算反射方向:reflect(入射光方向, 法线方向)
// 注意:入射光方向为 光源→物体 方向
vec3 lightReflect = reflect(lightDir, normal);
✅ 性能说明:reflect() 是 GPU 原生优化函数,无额外性能开销,1 个时钟周期即可完成计算。
2. 第二步:计算观察方向(View Direction)
观察方向是摄像机指向像素的向量,需通过世界坐标计算:
glsl
// 观察方向 = 像素世界坐标 - 摄像机世界坐标
vec3 viewDir = worldPos - cameraPos;
// 关键:向量必须单位化(归一化)
viewDir = normalize(viewDir);
✅ 关键细节:
-
cameraPos:所有像素共享摄像机位置,作为uniform变量传入 Shader; -
worldPos:顶点着色器计算顶点世界坐标,经片元着色器自动插值得到像素级世界坐标; -
向量必须
normalize():非单位向量会导致点乘结果超出 [0,1] 范围,破坏光照计算。
3. 第三步:点乘计算高光基础系数
反射方向与观察方向的夹角需用点乘(dot)计算,且必须反转观察方向,避免计算钝角余弦值:
glsl
// 反转观察方向 + 点乘计算 cosθ
float specularBase = dot(normalize(lightReflect), normalize(-viewDir));
4. 第四步:负值截断(Max 函数优化)
当视线与反射方向夹角大于 90° 时,点乘结果会为负数,此时应无高光,用 max() 函数截断:
glsl
// 负数归 0,正数保留原值
float specular = max(specularBase, 0.0);
✅ 性能对比:max() 比 clamp() 少 1 次参数判断,移动端渲染更高效。
四、关键参数详解 📍 让高光更真实
1. 单位化(Normalize)的必要性
所有方向向量(LightDir、Normal、ViewDir、LightReflect)必须单位化:
-
非单位向量点乘结果会大于 1 或小于 -1,导致高光过曝或计算异常;
-
单位化后向量长度为 1,点乘结果严格落在 [-1,1],符合光照计算规则。
2. 世界坐标(World Position)的计算
像素世界坐标无需手动计算,由顶点着色器传递后自动插值:
glsl
// 顶点着色器:计算顶点世界坐标
worldPos = vec3(modelMatrix * vec4(position, 1.0));
OpenGL 会自动完成三角形面片内的像素插值,无需开发者干预,降低开发成本。
3. Uniform 变量的使用
cameraPos 作为全局变量,用 uniform 传递:
glsl
// 片元着色器声明
uniform vec3 cameraPos;
所有像素共享同一摄像机位置,减少数据传输,提升渲染帧率。
五、总结 📝 高光反射核心速记
-
本质:光线在物体表面的镜面反弹,夹角决定高光强弱;
-
核心公式 :
Specular = max(dot(反射方向, -观察方向), 0.0); -
关键步骤:求反射方向 → 求观察方向 → 点乘计算 → 负值截断;
-
必做操作 :所有方向向量单位化,世界坐标自动插值。

下一章节,我们将基于这套理论,编写完整的高光反射 Shader 代码,把理论转化为可运行的渲染效果,让 3D 物体真正拥有 "光泽感"。