镜面反射光照模型Phong和Blinn-Phong
Phong
Phong(1975) - 最早的高光模型,也是BRDF的一种。这里需要说明一点,它不是基于物理的光照模型,而是经验模型,Blinn-Phong也是如此。
核心原理及公式
Phong 模型基于完美镜面反射假设: 完美镜面反射指的是:
- 表面完全光滑(理想化假设)
- 光线遵循反射定律:入射角 = 反射角
- 反射方向唯一确定:给定入射方向 L,反射方向 R 只有一个 理论上的完美镜面反射:只有 V = R 时才能看到高光 其他方向强度 = 0。 Phong 在反射上做了妥协,它允许控制高光的范围,也就是(R·V)^α中的α来控制宽度
1
Specular = Ks × (R · V)^α × LightColor
- Ks: 高光颜色/强度
- R: 反射光方向
- V: 视线方向
- α (shininess): 高光锐利度
对于高光部分:
- ✅ 表面完全光滑
- ✅ 完美镜面反射
- ✅ 反射方向由反射定律确定
- ❌ 但允许反射有”宽度”(通过 shininess 控制) 图片来自https://learnopengl.com/Lighting/Basic-Lighting
从图像上来看,当 V 与 R 完全重合时,高光最强(R·V = 1),反之就会衰减,这里其实就是求反射向量R在相机向量V上的投影,也就是R·V作为权重来作为高光衰减范围,使用Shininess(α)作为幂来控制高光集中度,Shininess表示光泽度,Shininess越大表示物体越光滑,高光越集中衰减范围越小。
ReflectDir计算
这里需要说明一点,此处光向量是物体指向光源,而非物理上的光源指向物体 下方是手动计算方法
当然引擎中一般是带有这个函数的,所以可以使用自带函数
Blinn-Phong
1975年:Phong Bui Tuong 提出 Phong 模型 1977年:Jim Blinn 提出改进版 → Blinn-Phong
公式
1
2
H = normalize(L + V)
specular = Ks × (N·H)^α × LightColor
为什么有Blinn-Phong
问题 1:因为计算开销问题
1
2
3
// Phong 需要计算反射向量
float3 R = reflect(-L, N); // 需要:R = 2(N·L)N - L
float spec = pow(dot(R, V), shininess);
问题 2:数值不稳定
1
float RdotV = dot(R, V);
当视角接近掠射角(Grazing angle)时:
1
2
3
4
5
6
7
8
9
V (几乎平行于表面)
↗
/
/_____ 表面
↑ N
R 和 V 的夹角可能 > 90°
→ dot(R, V) < 0
→ 需要 clamp,但会出现硬边(hard edge)
问题 3:能量不守恒(后来发现)
Phong 模型在掠射角下能量分布不符合物理规律(这在 1977 年还不是主要关注点)。
Blinn-Phong 的核心创新
革命性思想:半程向量(Halfway Vector) Jim Blinn 的洞察: 与其计算 “R 是否接近 V” 不如计算 “N 是否接近 H” 半程向量就是L+V,视角向量和光线向量相加 %E5%90%91%E9%87%8F.gif)
Phong vs Blinn-Phong 详细对比
| 特性 | Phong | Blinn-Phong |
|---|---|---|
| 提出年份 | 1975 | 1977 |
| 核心计算 | R·V | N·H |
| 关键向量 | 反射向量 R | 半程向量 H |
| 计算开销 | 较高(~8 ALU) | 较低(~6 ALU) |
| 性能 | 基准 | 快 20-30% |
| 数值稳定性 | 较差(掠射角) | 较好 |
| 高光形状 | 稍大、柔和 | 稍小、锐利 |
| Shininess 对应 | α | ~4α |
| 现代引擎采用 | 较少 | 广泛(Unity, UE 默认) |
| 硬件友好 | 一般 | 更好 |
Unity代码参考
properties
添加高光颜色,强度,光泽度控制
1
2
3
4
5
6
7
_SpecularColor("SpecularColor",color) = (1,1,1,1)
_SpecularIntensity("SpecularIntensity", Range(0,50)) = 1
_SpecularShininess("SpecularShininess", Range(0,10)) = 2
----------
half4 _SpecularColor;
half _SpecularIntensity;
half _SpecularShininess;
appdata
1
2
3
4
5
6
7
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL; //法线
float4 tangent : TANGENT; //切线
float2 uv : TEXCOORD0;
};
v2f
1
2
3
4
5
6
7
8
9
10
struct v2f
{
float4 pos : SV_POSITION;
float4 positionWS : TEXCOORD0;
float3 normalWS : TEXCOORD1; //法线
float3 tangentWS : TEXCOORD2; //切线
float3 bitangentWS : TEXCOORD3; //副切线
float2 texcoord : TEXCOORD4;
UNITY_SHADOW_COORDS(5)
};
vert 顶点着色器
处理数据参考
1
2
3
4
5
6
7
8
9
10
11
12
13
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.positionWS = mul(unity_ObjectToWorld, v.vertex);
o.normalWS = UnityObjectToWorldNormal(v.normal);
o.tangentWS = UnityObjectToWorldDir(v.tangent.xyz);
//* v.tangent.w处理平台差异
o.bitangentWS = cross(o.normalWS, o.tangentWS) * v.tangent.w;
o.texcoord = TRANSFORM_TEX(v.uv,_MainTex);
TRANSFER_SHADOW(o);
return o;
}
frag片元着色器
关键代码
PhongSpecular,注意这里取的是光向量的反向,其它的套公式就行
1
2
3
4
5
//Specular
//phong
half3 reflectDir = reflect(-lightWS, pixelNormal);
half phongItem = max(0, dot(reflectDir, viewDir));
half3 phongSpecular = _SpecularColor.xyz * _SpecularIntensity * pow(phongItem, _SpecularShininess) * lightColor;
Blinn-phongSpecular,改为计算半程向量和NoH
1
2
3
4
//Blinn-phong
half3 halfDir = normalize(lightWS + viewDir);
half blinnPhongItem = max(0, dot(pixelNormal, halfDir));
half3 blinnPhongSpecular = _SpecularColor.xyz * _SpecularIntensity * pow(blinnPhongItem, _SpecularShininess) * lightColor;
完整代码参考
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
float4 frag(v2f i) : SV_Target
{
//vectors
float3 positionWS = i.positionWS;
float3 normalWS = normalize(i.normalWS);
float3 tangentWS = normalize(i.tangentWS);
float3 bitangentWS = normalize(i.bitangentWS);
float3x3 TBN = float3x3(tangentWS, bitangentWS, normalWS);
half3 lightWS = normalize(_WorldSpaceLightPos0.xyz);
float3 viewDir = normalize(_WorldSpaceCameraPos - positionWS);
//tex
float3 lightColor = _LightColor0.xyz;
float4 baseCol = tex2D(_MainTex, i.texcoord);
float3 normalMapData = UnpackNormal(tex2D(_NormalMap, i.texcoord)).xyz;
normalMapData.xy *= _NormalMapScale;
//halfLambet
float3 pixelNormal = normalize(mul(normalMapData, TBN));
float noL = max(0, dot(pixelNormal, lightWS));
float halfLambet = noL*0.5 + 0.5;
//Specular
//phong
half3 reflectDir = reflect(-lightWS, pixelNormal);
half phongItem = max(0, dot(reflectDir, viewDir));
half3 phongSpecular = _SpecularColor.xyz * _SpecularIntensity * pow(phongItem, _SpecularShininess) * lightColor;
//Blinn-phong
half3 halfDir = normalize(lightWS + viewDir);
half blinnPhongItem = max(0, dot(pixelNormal, halfDir));
half3 blinnPhongSpecular = _SpecularColor.xyz * _SpecularIntensity * pow(blinnPhongItem, _SpecularShininess) * lightColor;
//Shadow
float shadow = SHADOW_ATTENUATION(i);
//output
float3 finalColor = baseCol *halfLambet * lightColor *shadow;
return float4(finalColor, 1);
}
Enjoy Reading This Article?
Here are some more articles you might like to read next: