Phong模型与Blinn-Phong模型
需要预先学习:
1. Phong 反射模型
着色与渲染管线 中给出了 Blinn-Phong 反射模型的数学形式,这篇笔记把它的前身——Phong 反射模型(Phong reflection model)——与 Blinn-Phong 放在一起对比,并分别给出对应的 GLSL 实现。
两个模型都是局部光照模型(local illumination model):每个片元独立计算颜色,只依赖光源、表面材质与观察方向,不考虑阴影与光线的多次反弹。它们共享同一个框架,把片元的输出颜色分解为三个分量之和:
| 分量 | 名称 | 模拟的现象 | 是否依赖观察方向 |
|---|---|---|---|
| 环境光(ambient) | 光线多次反弹形成的间接光照 | 否 | |
| 漫反射(diffuse) | 粗糙表面向各个方向均匀散射 | 否 | |
| 高光(specular) | 光滑表面向镜像方向集中反射 | 是 |
两者的区别只在第三项——高光的计算方式:Phong 用反射向量与视线的夹角,Blinn-Phong 用半程向量与法线的夹角。
注:着色与渲染管线 中的公式还包含点光源的距离衰减项 ,这里把它吸收进光源颜色
lightColor,以便聚焦模型本身。距离衰减与光源类型有关,属于另一个话题。
1.1 环境光
环境光(ambient)是间接光照的粗糙近似:光线在场景中反弹多次之后,近似均匀地照亮物体的每个角落。它与方向无关,用一个常数表示:
其中 是环境光系数, 是环境光强度。GLSL 里一行就能算出来:
vec3 ambient = ambientStrength * lightColor;
环境光保证背光面不至于全黑,但代价是抹平了明暗对比——强度过高时画面会整体发灰。
1.2 漫反射
漫反射(diffuse reflection)描述粗糙表面的散射:光打进表面后向四面八方均匀射出,因此漫反射分量与观察方向无关——绕着物体走一圈,看到的漫反射颜色不会变。
表面接收到的光量由 Lambert 余弦定律决定(推导见着色与渲染管线):光倾斜照射时,单位面积接收的能量随夹角余弦衰减,而 恰好就是这个余弦:
- :表面法线,单位向量
- :从片元指向光源的方向,单位向量
- :漫反射系数,即物体的固有色
不可省略:当光源位于表面背面()时,负的点积没有物理意义,钳制为 0 表示背面接收不到光照。
1.3 高光
高光(specular highlight)模拟光滑表面的镜面反射:光集中在镜像方向附近反射,视线越接近镜像方向,看到的高光越亮。
第一步,由光的方向求反射方向。反射向量满足"入射角等于反射角":
第二步,用 与视线方向 的夹角余弦衡量"接近镜像的程度",再乘上指数 :
- :从片元指向摄像机的方向,单位向量
- :高光系数,控制高光的颜色与强度,常取偏白的颜色
- :高光指数(shininess),控制高光的集中程度

的影响很直观:余弦值的 次方会加速衰减, 越大,视线稍微偏离镜像方向高光就迅速变暗——高光小而锐利,表面显得光滑; 越小,高光大而模糊,表面显得粗糙:
| 高光观感 | 对应材质 | |
|---|---|---|
| 2 | 又大又模糊 | 粗糙的塑料 |
| 32 | 适中,常用默认值 | 一般光滑表面 |
| 128 | 小而锐利 | 金属、抛光表面 |
注:标准公式的高光项不包含 ,当光源位于表面背面()而 时,仍然会产生高光。实践中可以把高光项再乘上一个 ,保证背光面彻底没有高光。
1.4 GLSL 实现
写代码前先约定向量的方向与空间。所有方向向量都以片元为起点、归一化,并且位于同一个坐标空间(这里选择世界空间):
| 变量 | 含义 | 求法 |
|---|---|---|
normal |
片元法线 | 顶点法线插值后归一化 |
lightDir |
片元指向光源 | normalize(lightPos - FragPos) |
viewDir |
片元指向摄像机 | normalize(viewPos - FragPos) |
注意:方向向量不归一化就参与点积,得到的余弦值是错的——点积的几何意义建立在单位向量之上。
下面把三个分量各写成一个函数,再写一个总装函数。它们依赖的全局 uniform 声明如下:
uniform vec3 lightPos; // 点光源位置(世界空间)
uniform vec3 viewPos; // 摄像机位置(世界空间)
uniform vec3 lightColor; // 光源颜色(光强已吸收进来)
uniform float ambientStrength; // 环境光强度系数,即公式中的 k_a * I_a
uniform vec3 specColor; // 高光系数 k_s
uniform float shininess; // 高光指数 p
环境光函数:
/*
* 计算环境光贡献
*
* 参数:
* strength 环境光强度系数,取值 0.0 ~ 1.0
* lightColor 光源颜色
*
* 返回:
* 环境光对该片元的 RGB 颜色贡献
*/
vec3 calcAmbient(float strength, vec3 lightColor)
{
// 环境光与方向无关,直接按强度缩放光源颜色
return strength * lightColor;
}
漫反射函数:
/*
* 计算漫反射贡献(Lambert 余弦定律)
*
* 参数:
* normal 片元法线,已归一化,与 lightDir 位于同一坐标空间
* lightDir 从片元指向光源的方向,已归一化
* albedo 漫反射系数 k_d,即物体的固有色
* lightColor 光源颜色
*
* 返回:
* 漫反射对该片元的 RGB 颜色贡献
*/
vec3 calcDiffuse(vec3 normal, vec3 lightDir, vec3 albedo, vec3 lightColor)
{
// n 点乘 l:法线与光方向的夹角余弦,衡量单位面积接收的光能
// max(0.0, ...):光源在表面背面时余弦为负,钳制为 0
float diff = max(dot(normal, lightDir), 0.0);
// albedo 决定表面散射出的颜色,与观察方向无关
return diff * albedo * lightColor;
}
高光函数(Phong 版):
/*
* 计算高光贡献(Phong 模型)
*
* 参数:
* normal 片元法线,已归一化
* lightDir 从片元指向光源的方向,已归一化
* viewDir 从片元指向摄像机的方向,已归一化
* specColor 高光系数 k_s,高光的颜色与强度
* lightColor 光源颜色
* shininess 高光指数 p,越大高光越集中
*
* 返回:
* 高光对该片元的 RGB 颜色贡献
*/
vec3 calcSpecularPhong(vec3 normal, vec3 lightDir, vec3 viewDir,
vec3 specColor, vec3 lightColor, float shininess)
{
// reflect(I, N) 返回入射向量 I 关于法线 N 的反射向量
// 第一个参数是从光源指向片元的入射方向,与 lightDir 恰好相反,
// 所以传 -lightDir
vec3 reflectDir = reflect(-lightDir, normal);
// r 点乘 v:反射方向与观察方向的夹角余弦,越接近 1 高光越亮
// pow(..., shininess):指数衰减,p 越大高光越集中
float spec = pow(max(dot(reflectDir, viewDir), 0.0), shininess);
return spec * specColor * lightColor;
}
注意:GLSL 的内置函数
reflect(I, N)要求第一个参数是指向表面的入射向量(从光源指向片元)。而lightDir的约定方向恰好相反(片元指向光源),所以必须传-lightDir,否则反射向量会指向表面内部。
总装函数:
/*
* Phong 光照:环境光 + 漫反射 + 高光三项求和
*
* 参数:
* normal 片元法线(世界空间,已归一化)
* fragPos 片元位置(世界空间)
* albedo 漫反射系数 k_d(物体固有色)
*
* 返回:
* 该片元的最终颜色(RGB)
*
* 依赖全局 uniform:lightPos、viewPos、lightColor、
* ambientStrength、specColor、shininess
*/
vec3 phongLighting(vec3 normal, vec3 fragPos, vec3 albedo)
{
// 片元指向光源 / 摄像机的方向向量
vec3 lightDir = normalize(lightPos - fragPos);
vec3 viewDir = normalize(viewPos - fragPos);
vec3 ambient = calcAmbient(ambientStrength, lightColor);
vec3 diffuse = calcDiffuse(normal, lightDir, albedo, lightColor);
vec3 specular = calcSpecularPhong(normal, lightDir, viewDir,
specColor, lightColor, shininess);
return ambient + diffuse + specular;
}
2. Blinn-Phong 模型
Blinn-Phong 模型由 Jim Blinn 于 1977 年提出,它完全保留了 Phong 的环境光与漫反射,只修改高光项的计算方式。修改的原因是 Phong 高光存在一个肉眼可见的缺陷。
2.1 Phong 高光的缺陷
Phong 高光项里的 在掠射视角下会出问题。考虑光源低角度掠过表面、视线接近水平的情形:
当 与 的夹角超过 90° 时,反射向量 与 的夹角只会更大——点积为负,被钳制成 0。问题在于这个归零是突变的:视线越过这条界线的瞬间,高光从某个亮度直接跌落到 0,在平坦表面上留下一道生硬的亮暗分界。高光越大( 越小),这道分界越明显。
2.2 半程向量
Blinn 的方案是绕开反射向量,改用半程向量(halfway vector)—— 与 的角平分线方向:

几何意义:若 与 对齐,则 与 关于 对称——视线恰好位于镜像方向上。于是高光项改为:
为什么这能修复缺陷: 是 与 的角平分线,它与两者的夹角都只有 、 夹角的一半。即使 与 的夹角超过 90°, 与 的夹角依然可以很小, 连续变化——高光平滑衰减到 0,不会出现生硬的截断。
2.3 GLSL 实现
与 Phong 的差别只是高光函数:
/*
* 计算高光贡献(Blinn-Phong 模型)
*
* 参数与返回值同 calcSpecularPhong:
* normal 片元法线,已归一化
* lightDir 从片元指向光源的方向,已归一化
* viewDir 从片元指向摄像机的方向,已归一化
* specColor 高光系数 k_s
* lightColor 光源颜色
* shininess 高光指数 p
*/
vec3 calcSpecularBlinnPhong(vec3 normal, vec3 lightDir, vec3 viewDir,
vec3 specColor, vec3 lightColor, float shininess)
{
// 半程向量:l 与 v 相加后归一化,即两者的角平分线方向
vec3 halfwayDir = normalize(lightDir + viewDir);
// n 点乘 h:法线与半程向量的夹角余弦
// n 与 h 对齐时,v 恰好位于光的镜像反射方向上
float spec = pow(max(dot(normal, halfwayDir), 0.0), shininess);
return spec * specColor * lightColor;
}
总装时把 phongLighting 中对 calcSpecularPhong 的调用换成 calcSpecularBlinnPhong,其余一行都不用改。
2.4 模型对比
| Phong | Blinn-Phong | |
|---|---|---|
| 高光核心量 | ||
| 关键 GLSL 调用 | reflect(-lightDir, normal) |
normalize(lightDir + viewDir) |
| 与 夹角超 90° | 高光突变截断,留下生硬分界 | 平滑衰减,无截断 |
| 相近观感的 shininess | 约取 |
同样的 值下,两个模型的高光大小并不一致: 与 的衰减速度不同——光正对表面时,Blinn-Phong 的高光大约是 Phong 的两倍宽;掠射角下则自然地变小变暗,更接近真实材质的行为。要得到与 Phong 接近的高光观感,Blinn-Phong 的 通常取 2~4 倍:Phong 用 32 时,Blinn-Phong 取 64~128。
3. 完整着色器示例
3.1 坐标空间与法线矩阵
光照计算要求所有向量位于同一个坐标空间,这里选世界空间:lightPos、viewPos 本来就用世界坐标描述,片元位置 FragPos 由 model 矩阵变换得到。
法线的变换有一个陷阱:不能直接用 mat3(model) 变换法线。旋转和等比缩放没问题,但非等比缩放会破坏法线与表面的垂直关系。正确做法是用正规矩阵(normal matrix,model 矩阵左上 3×3 的逆转置):
Normal = mat3(transpose(inverse(model))) * aNormal;
注:逆矩阵再转置能保证变换后的法线仍然垂直于变换后的表面。每个顶点都算一次
inverse开销不小,如果模型的变换不含非等比缩放,直接用mat3(model)也可以。
3.2 顶点着色器
#version 330 core
layout (location = 0) in vec3 aPos; // 顶点位置(模型空间)
layout (location = 1) in vec3 aNormal; // 顶点法线(模型空间)
uniform mat4 model; // 模型变换矩阵
uniform mat4 view; // 观察矩阵
uniform mat4 projection; // 投影矩阵
out vec3 FragPos; // 片元位置(世界空间)
out vec3 Normal; // 片元法线(世界空间)
void main()
{
// MVP 变换,得到裁剪空间坐标
gl_Position = projection * view * model * vec4(aPos, 1.0);
// 位置变换到世界空间
FragPos = vec3(model * vec4(aPos, 1.0));
// 法线用正规矩阵变换(见上一节的说明)
Normal = mat3(transpose(inverse(model))) * aNormal;
}
3.3 片段着色器
完整的 Blinn-Phong 片段着色器,可以直接使用。把 calcSpecularBlinnPhong 换成 calcSpecularPhong 就是 Phong 版本:
#version 330 core
in vec3 FragPos; // 从顶点着色器插值而来:世界空间位置
in vec3 Normal; // 从顶点着色器插值而来:世界空间法线
uniform vec3 lightPos; // 点光源位置(世界空间)
uniform vec3 viewPos; // 摄像机位置(世界空间)
uniform vec3 lightColor; // 光源颜色
uniform float ambientStrength; // 环境光强度系数 k_a * I_a
uniform vec3 specColor; // 高光系数 k_s
uniform float shininess; // 高光指数 p
uniform vec3 albedo; // 漫反射系数 k_d(物体固有色)
out vec4 FragColor; // 输出的片元颜色
vec3 calcAmbient(float strength, vec3 lightColor)
{
return strength * lightColor;
}
vec3 calcDiffuse(vec3 normal, vec3 lightDir, vec3 albedo, vec3 lightColor)
{
float diff = max(dot(normal, lightDir), 0.0);
return diff * albedo * lightColor;
}
vec3 calcSpecularBlinnPhong(vec3 normal, vec3 lightDir, vec3 viewDir,
vec3 specColor, vec3 lightColor, float shininess)
{
vec3 halfwayDir = normalize(lightDir + viewDir);
float spec = pow(max(dot(normal, halfwayDir), 0.0), shininess);
return spec * specColor * lightColor;
}
void main()
{
// 插值会破坏单位长度,先归一化再参与点积
vec3 normal = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
vec3 viewDir = normalize(viewPos - FragPos);
vec3 ambient = calcAmbient(ambientStrength, lightColor);
vec3 diffuse = calcDiffuse(normal, lightDir, albedo, lightColor);
vec3 specular = calcSpecularBlinnPhong(normal, lightDir, viewDir,
specColor, lightColor, shininess);
FragColor = vec4(ambient + diffuse + specular, 1.0);
}
3.4 uniform 的设置
C++ 侧在渲染循环中把这些 uniform 传进去。材质参数初始化一次即可,viewPos、lightPos 随摄像机与光源移动,每帧更新:
glUseProgram(shaderProgram);
// 材质参数(初始化时设置一次)
glUniform3f(glGetUniformLocation(shaderProgram, "lightColor"), 1.0f, 1.0f, 1.0f);
glUniform1f(glGetUniformLocation(shaderProgram, "ambientStrength"), 0.1f);
glUniform3f(glGetUniformLocation(shaderProgram, "specColor"), 0.5f, 0.5f, 0.5f);
glUniform1f(glGetUniformLocation(shaderProgram, "shininess"), 64.0f);
glUniform3f(glGetUniformLocation(shaderProgram, "albedo"), 1.0f, 0.5f, 0.31f);
// 每帧更新
glUniform3fv(glGetUniformLocation(shaderProgram, "viewPos"), 1, &cameraPos[0]);
glUniform3fv(glGetUniformLocation(shaderProgram, "lightPos"), 1, &lightPos[0]);
注:把
albedo换成纹理采样(texture(material.diffuse, TexCoords))就是带贴图的光照,思路与纹理中"应用纹理"一节完全一致。



