第七课:光照模型入门——冯氏光照模型之漫反射
各位同学,今天我们来聊聊光照。说实话,光照是让3D场景“活”起来的关键一步。没有光照,你的模型就是一堆扁平的多边形,毫无生气。
我个人最早接触OpenGL时,第一个被震撼到的效果就是加上漫反射光照的那一刻。原本灰蒙蒙的立方体,突然有了立体感,像被真实的光线照亮了一样。嗯,那种感觉,至今难忘。
为什么需要光照模型?
现实世界中,我们能看到物体,是因为光线照射到物体表面,然后反射到我们的眼睛里。计算机要模拟这个过程,就得有一套数学公式——这就是光照模型。
冯氏光照模型(Phong Lighting Model)是最经典、最基础的光照模型。它把光照拆成三个分量:
- 环境光(Ambient):模拟场景中无处不在的微弱光线,让物体不至于全黑
- 漫反射(Diffuse):模拟光线照射到粗糙表面后向各个方向均匀反射
- 镜面光(Specular):模拟光滑表面的高光反射
今天我们先聚焦漫反射。为什么?因为它是三个分量中最直观、也最出效果的。你想想看,一个物体被光照射,正面亮、侧面暗、背面全黑——这就是漫反射在起作用。
核心概念:漫反射的强度取决于光线方向与表面法线方向的夹角。夹角越小,光照越强;夹角越大,光照越弱。
漫反射的数学原理
说白了,漫反射的计算就一个公式:
diffuse = max(dot(normal, lightDir), 0.0) * lightColor
这里:
normal是表面法线(单位向量)lightDir是从表面指向光源的方向(单位向量)dot是点积运算,计算两个向量的夹角余弦值max(..., 0.0)确保结果不为负数(背面不发光)
我在项目中遇到过一个问题:法线没有归一化,结果光照效果忽明忽暗,排查了半天才发现是向量长度不对。所以,记得一定要归一化!
在片段着色器中实现漫反射
好,理论说完了,我们直接上代码。下面是一个完整的片段着色器,实现了漫反射光照:
// 片段着色器 - 漫反射光照
#version 300 es
precision mediump float;
// 从顶点着色器传来的数据
in vec3 vNormal; // 插值后的法线
in vec3 vFragPos; // 插值后的片段位置
// 统一变量
uniform vec3 uLightPos; // 光源位置
uniform vec3 uLightColor; // 光源颜色
uniform vec3 uViewPos; // 观察者位置(暂时不用)
uniform vec3 uObjectColor;// 物体颜色
out vec4 fragColor;
void main() {
// 1. 归一化法线
vec3 normal = normalize(vNormal);
// 2. 计算光线方向(从片段指向光源)
vec3 lightDir = normalize(uLightPos - vFragPos);
// 3. 计算漫反射强度
float diff = max(dot(normal, lightDir), 0.0);
// 4. 计算最终颜色
vec3 diffuse = diff * uLightColor * uObjectColor;
// 5. 加上一点环境光,避免背面全黑
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * uLightColor * uObjectColor;
vec3 result = ambient + diffuse;
fragColor = vec4(result, 1.0);
}
这段代码我解释一下关键点:
- 法线归一化:顶点着色器传过来的法线是插值后的,长度可能不是1,必须重新归一化
- 光线方向:用光源位置减去片段位置,得到从片段指向光源的向量
- 点积取正:用
max(dot, 0.0)确保背光面为0,不会出现负光照 - 环境光:我习惯加一个很小的环境光(0.1倍),这样背光面不会完全漆黑
小技巧:调试时可以把环境光强度设成0.5,这样你能清楚看到模型的轮廓。等漫反射效果调好了,再改回0.1。
顶点着色器的配合
片段着色器需要法线和片段位置,这些数据从哪来?从顶点着色器传过来。看代码:
// 顶点着色器
#version 300 es
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProjection;
out vec3 vNormal;
out vec3 vFragPos;
void main() {
// 计算世界空间中的位置
vec4 worldPos = uModel * vec4(aPos, 1.0);
vFragPos = worldPos.xyz;
// 转换法线(使用法线矩阵)
mat3 normalMatrix = transpose(inverse(mat3(uModel)));
vNormal = normalMatrix * aNormal;
gl_Position = uProjection * uView * worldPos;
}
这里有个坑,我必须要提醒你:
注意:法线不能直接用模型矩阵变换!如果模型有非均匀缩放(比如X轴拉伸2倍,Y轴不变),直接变换会导致法线方向错误。必须用法线矩阵——也就是模型矩阵的逆转置矩阵的左上角3x3部分。
我曾经在这个问题上栽过跟头。一个简单的立方体,旋转后光照效果完全不对,阴影位置飘忽不定。查了两天资料才发现是法线变换的问题。嗯,从那以后,我写光照代码第一件事就是检查法线矩阵。
知识体系总览
下面这张图总结了漫反射光照的完整流程:
常见问题与避坑指南
根据我的经验,初学者在实现漫反射时最容易遇到这几个问题:
| 问题 | 现象 | 解决方案 |
|---|---|---|
| 法线未归一化 | 光照强度不均匀,闪烁 | 在片段着色器中调用normalize() |
| 法线变换错误 | 旋转模型时光照异常 | 使用法线矩阵(逆转置矩阵) |
| 光线方向反了 | 背光面反而亮 | 检查lightPos - fragPos的方向 |
| 忘记环境光 | 背光面全黑 | 加一个0.1~0.2的环境光分量 |
调试技巧:如果你不确定光照计算是否正确,可以先把漫反射颜色直接设为法线方向的可视化(fragColor = vec4(normal * 0.5 + 0.5, 1.0))。这样你能直观看到法线方向是否正确,排除法线问题后再调光照。
好了,漫反射光照就讲到这里。代码不多,但背后的原理值得反复琢磨。你动手试试,把一个立方体加上漫反射光照,旋转它,观察不同面的亮度变化——相信我,那种从平面到立体的转变,会让你上瘾的。
公众号:蓝海资料掘金营,微信deep3321