从零实现 GLSL 反射光照:环境光 + 平行光(漫反射 + 高光)
AI 盛行的今天,不知道手写 shader 光照还有没有必要,但不管了,学了就记录一下——过程还挺有意思的。
反射需要什么?
反射离不开两样东西:光线和物体。常见的光源有三种:环境光、平行光、点光源。其中环境光不参与真正的"反射计算"(它是模拟场景中各种间接光叠加后的一个近似常量),所以最简单,先从它开始。
vec3 ambientLight(vec3 lightColor, float lightIntensity) {
return lightColor * lightIntensity;
}
传入光线颜色和强度,直接相乘就行。接入 fragment shader:
uniform vec3 uColor; // three.js 里传入的自定义颜色,随便设置个即可
varying vec3 vNormal;
varying vec3 vPosition;
#include "../light/ambientLight.glsl"
void main()
{
vec3 color = uColor;
// 光线
vec3 light = vec3(.0);
light += ambientLight(vec3(1.0), .02);
color *= light;
// Final color
gl_FragColor = vec4(color, 1.0);
#include <tonemapping_fragment>
#include <colorspace_fragment>
}

这时候可能有同学会问:环境光不需要传法线和位置,那顶部声明的 vNormal、vPosition 是干什么用的?——别急,重头戏在后面:平行光。
平行光:漫反射与高光
平行光的特点是:所有光线的方向完全一致,和光源的具体位置无关,只关心一个统一的传播方向(想象太阳光,光源离得极远,到达地表时可以认为所有光线互相平行)。
反射的计算可以拆成两部分:漫反射和高光。
- 漫反射:均匀渐变的明暗过渡,模拟粗糙、无光泽的表面(比如粉笔、布料)。
- 高光:一个集中的亮斑,会随视角移动,模拟光滑、有光泽表面的镜面反光点(比如塑料、金属)。
漫反射
这个原理有个专业名词叫兰伯特漫反射(Lambertian diffuse),细节不展开,记住结论就行:用法线和光照方向的点积,决定漫反射强度。
补充一个关键性质:如果两个向量都是单位向量,那么 dot(a, b) = cos(θ),结果落在 -1~1 之间——夹角越小越接近 1,夹角越大(方向越不同)越接近 -1。
对应到物理直觉:物体表面法线和光线方向越平行,光越亮;法线垂直于光线方向时,基本就不可见了。这正是漫反射的写法:
float shading = dot(normalize(normal), lightDirection);
shading = max(.0, shading); // 负数没有意义,最暗就是 0
加上颜色和强度:
vec3 directionalLight(vec3 lightColor, float lightIntensity, vec3 normal, vec3 lightPosition) {
vec3 lightDirection = normalize(lightPosition); // 从表面指向光源的方向
float shading = dot(normalize(normal), lightDirection);
shading = max(.0, shading);
return lightColor * lightIntensity * shading;
}

高光
高光的位置和视角(相机)、法线、光源方向都有关系,好在这部分有现成的公式可以直接调用:reflect(I, N)。
先看视线向量的计算:
vec3 viewDirection = normalize(vPosition - cameraPosition);
vPosition - cameraPosition 是"从相机指向表面点"的方向。
再看反射向量:
vec3 lightReflection = reflect(-lightDirection, normal);
这里有个容易搞混的地方:lightDirection 是"从表面指向光源"的方向,但 reflect(I, N) 要求传入的 I 是入射光线的传播方向,也就是"从光源射向表面"——正好和 lightDirection 相反,所以要取负号 -lightDirection,才是真正意义上"打过来的那束光"。
reflect 内部其实就是一行几何公式:
R = I - 2 * dot(N, I) * N
不是黑盒,是可以手推的。
拿到反射方向后,用同样的点积思路判断"反射光线是否正对着相机":
float specular = dot(lightReflection, -viewDirection); // 从表面指向相机的方向
specular = max(0.0, specular);
specular = pow(specular, specularPower); // 指数越大,高光点越集中越锐利
-viewDirection 才是"从表面指向相机"的方向,和 lightReflection 做点积,值越接近 1 说明反射光线越对准相机,高光越强。

完整代码
// directionalLight.glsl
vec3 directionalLight(
vec3 lightColor,
float lightIntensity,
vec3 normal,
vec3 lightPosition,
vec3 viewDirection,
float specularPower
) {
vec3 lightDirection = normalize(lightPosition);
vec3 lightReflection = reflect(-lightDirection, normal);
float shading = dot(normal, lightDirection);
shading = max(.0, shading);
float specular = dot(lightReflection, -viewDirection);
specular = max(0.0, specular);
specular = pow(specular, specularPower);
return lightColor * lightIntensity * (shading + specular);
}
// fragment.glsl
uniform vec3 uColor;
varying vec3 vNormal;
varying vec3 vPosition;
#include "../light/ambientLight.glsl"
#include "../light/directionalLight.glsl"
void main()
{
vec3 normal = normalize(vNormal);
vec3 viewDirection = normalize(vPosition - cameraPosition);
vec3 color = uColor;
vec3 light = vec3(.0);
light += ambientLight(vec3(1.0), .02);
light += directionalLight(
vec3(.1, .1, 1.0),
1.0,
normal,
vec3(.0, .0, 3.0),
viewDirection,
20.0
);
color *= light;
gl_FragColor = vec4(color, 1.0);
#include <tonemapping_fragment>
#include <colorspace_fragment>
}
小结
到这里,我们已经实现了光照模型里的三个基础分量:环境光(整体基础亮度)、漫反射(和视角无关的表面明暗)、高光(和视角强相关的镜面亮斑)。三者叠加,就是一个基本能打的光照效果了。本来还想记录点光源,篇幅差不多了,留到下个篇幅
https://threejs-journey.com/lessons/lights-shading-shaders 我是跟着journey学的

浙公网安备 33010602011771号