冯氏光照模型分为环境光、漫反射光和镜面光三部分:环境光=环境光强度×物体颜色,不依赖向量;漫反射光∝max(法线·光源方向,0),遵循兰伯特定律;镜面光∝max(法线·半程向量,0)的高次幂,体现高光锐度。

冯氏光照三部分怎么分别计算
环境光最简单,就是直接用材质颜色乘以环境光强度,不依赖任何向量;漫反射需要物体表面法线 normal 和指向光源的单位向量 lightDir,用点积取余弦值再 clamp 到 [0,1];镜面光还要引入观察方向 viewDir 和反射方向,或者更常用的是用半程向量 halfDir(normalize(lightDir + viewDir)),再做高次幂衰减。
关键不是公式本身,而是所有向量必须在同一个空间里运算。最容易出错的是:法线从模型空间传入,但 lightDir 和 viewDir 在世界空间或视图空间计算——结果点积全是错的。统一到世界空间最稳妥,前提是法线也用法线矩阵(transpose(inverse(model)))变换。
常见错误现象:gl_Position 用了 model 矩阵,但法线只乘了 model 矩阵(没转置逆),导致漫反射随旋转剧烈闪烁;或者光照方向硬编码为 vec3(0.0, 1.0, 0.0) 却没考虑它是否和法线同空间,结果光照永远“照歪”。
顶点着色器里该传什么给片元着色器
不要在片元着色器里重新算世界坐标或光源方向——那是顶点着色器该干的活。顶点着色器要输出:变换后的世界空间顶点位置、归一化的世界空间法线、世界空间光源位置(或方向)、以及摄像机位置(用于算 viewDir)。
注意三点:
- 法线必须用法线矩阵变换,不能直接用 model 矩阵(除非模型无缩放)
- 光源位置如果用
vec3表示方向光(如太阳),就传normalize(worldLightPos - worldFragPos)的结果到片元着色器;如果是点光源,传世界空间光源坐标,让片元着色器自己减 - 插值会导致向量长度不为 1,所以片元着色器里对
normal、lightDir、viewDir都必须调用normalize()
片元着色器里怎么组合三部分光
环境光是常量项,漫反射是 Lambert 项,镜面光是 Phong/Blinn 项。标准写法是:
vec3 ambient = ambientStrength * lightColor; vec3 norm = normalize(Normal); vec3 lightDir = normalize(LightPos - FragPos); float diff = max(dot(norm, lightDir), 0.0); vec3 diffuse = diff * lightColor; vec3 viewDir = normalize(ViewPos - FragPos); vec3 halfDir = normalize(lightDir + viewDir); float spec = pow(max(dot(norm, halfDir), 0.0), 32.0); vec3 specular = spec * lightColor; vec3 result = (ambient + diffuse + specular) * objectColor;
注意:32.0 是镜面高光的 shininess 参数,值越大高光越小越锐利;改成 8.0 就会变“发散”。这个值不是随便定的——它直接影响视觉质感,且和美术资源约定有关。别用整数 32,浮点写法 32.0 能避免某些驱动编译警告。
容易踩的坑:漏掉 max(..., 0.0) 导致负值参与幂运算,产生 NaN;或者把 objectColor 错放在 ambient 计算里(应最后统一乘);还有把 LightPos 当作方向直接点积,没减 FragPos。
为什么光照看起来“平”或者“糊成一片”
大概率是法线贴图没生效,或者模型本身法线数据有问题。检查 glVertexAttribPointer 是否正确绑定了法线属性(index、size、type、stride、offset),尤其注意 stride 是否和顶点结构体对齐(比如 float[3] 位置 + float[3] 法线,stride 应为 6 * sizeof(float))。
另一个常见原因是深度测试或背面剔除干扰了光照计算顺序——光照本身不依赖绘制顺序,但如果片段被提前丢弃(比如 glEnable(GL_CULL_FACE) 且法线朝向反了),你就看不到漫反射结果。临时关掉 GL_CULL_FACE 和 GL_DEPTH_TEST 排查一下。
真正难调的是多光源叠加时的衰减。冯氏模型本身不包含距离衰减,如果点光源不手动除以距离平方,离得近就过曝,远了就看不见。加一句 float distance = length(LightPos - FragPos); float attenuation = 1.0 / (1.0 + 0.09 * distance + 0.032 * distance * distance); 再乘到 diffuse/specular 上,效果才真实。这个二次衰减系数不是标准值,得根据场景尺度反复试。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











