镜面反射光照模型Phong和Blinn-Phong

Phong

Phong(1975) - 最早的高光模型,也是BRDF的一种。这里需要说明一点,它不是基于物理的光照模型,而是经验模型,Blinn-Phong也是如此。

核心原理及公式

Phong 模型基于完美镜面反射假设: 完美镜面反射指的是:

  1. 表面完全光滑(理想化假设)
  2. 光线遵循反射定律:入射角 = 反射角
  3. 反射方向唯一确定:给定入射方向 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,视角向量和光线向量相加 图片

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:

  • URP - RendererFeature :ScreenSpaceOutline
  • 平滑法线处理 - 八面体映射
  • Lv.3 Unity主线:一个简单的PBRShader
  • 理论支线:直接光漫反射与GGX高光的混合问题
  • 理论支线:PBR - 基于图像的照明( image based lighting-IBL)
  • # #