跳到主要内容
3D Playground
WebGL 与着色器第 11 / 12 节

手写光照:Lambert、Phong 与 Fresnel

进阶WebGL 2

三个向量

几乎所有经典光照模型都只围绕三个单位向量展开,它们都在世界空间中计算:

  • N:表面法线。插值后长度会变短,片元着色器里要重新 normalize。
  • L:从表面指向光源。
  • V:从表面指向相机(cameraPosition - vWorld)。

Lambert 漫反射

粗糙表面把光均匀地散向各个方向,亮度只取决于光线入射角的余弦:

glsl
float NdotL = max(dot(N, L), 0.0);
vec3 color = uAmbient * uAlbedo + uAlbedo * uLightColor * NdotL;

max(..., 0.0) 保证背光面不会出现负光。环境光项是对周围间接光的粗略近似。

Phong 高光

光滑表面会把光反射到镜面方向。Phong 先求出反射方向 R,再看它与视线有多接近:

glsl
vec3 R = reflect(-L, N);
float spec = pow(max(dot(R, V), 0.0), uShininess);

指数(shininess)越大,高光越小越锐利。

Blinn-Phong 高光

Blinn 改用 L 与 V 的半程向量 H,比较它与法线的夹角。计算更省,掠射角下的高光也更自然。OpenGL 固定管线时代的默认光照就是它。同样的锐度大约需要 Phong 指数的 4 倍:

glsl
vec3 H = normalize(L + V);
float spec = pow(max(dot(N, H), 0.0), uShininess * 4.0);

Fresnel 边缘光

现实中,几乎所有材质在掠射角下都会变得更反光(菲涅尔效应)。用 1 - dot(N, V) 来近似:正对相机时为 0,轮廓边缘处趋近 1。乘方控制边缘的宽窄:

glsl
float rim = pow(1.0 - max(dot(N, V), 0.0), uRimPower);

它让物体从背景中“跳”出来,常用于角色描边和科幻效果。

Gamma

光照要在线性空间里计算,最后再用 pow(color, 1.0 / 2.2) 编码给显示器。跳过这一步,暗部会显得过黑,过渡也会变得生硬。

核心 API

dotreflectnormalizepowcameraPositionTHREE.ShaderMaterialTHREE.TorusKnotGeometry

动手试试

  1. 1关掉 orbit light,把光固定住,在 Phong 与 Blinn-Phong 之间切换,对比掠射角下的高光形状。
  2. 2把 shininess 调到 1 和 128,感受“塑料”与“抛光金属”的差别。
  3. 3加一个 toon 模式:用 floor(NdotL * 3.0) / 3.0 把漫反射量化成色阶。
运行中