参考:https://learnopengl-cn.github.io/02 Lighting/02 Basic Lighting/
本贴意义:本贴在 learnOpengl 教程的基础上增加了 Dear-imgui,可以方便地调节一些关键参数来观看不同的光照效果
颜色
颜色由红绿蓝(RGB)三个分量组成,在OpenGL中范围为0~1。
现实生活中我们看到某一物体的颜色并不是物体真正的颜色,而是它反射的颜色。
上图中物体吸收了大部分颜色,仅反射和自己颜色相近的光(强度较高),所有的反射光组合在一起就是我们感知的颜色。
如何表示这种反射的光线呢?
光的表示是RGB,每个分量表示RGB的强度,因此将颜色直接相乘就可以看作其强度相乘。
lightColor 的强度越高(接近1),toyColor 就越能保持自身的颜色强度。
lightColor 的强度越低(接近0),toyColor 自身的颜色就越会被吸收(抵消)
而且物体颜色的各分量强度不会高于光的各分量强度(0.33f, 0.42f, 0.18f),这也就保证了颜色的近似相近。
冯氏光照模型
冯氏光照模型(Phong Lighting Model) 由三个分量组成
- 环境光(Ambient) : 由环境光线照射所看到的颜色
- 漫反射光(Diffuse) : 由光源照射所看到的颜色
- 镜面反射光(Specular) : 模拟光泽物体上的亮点,趋近于光源颜色
先看下模型本身的颜色,模型加载教程参考 https://learnopengl-cn.github.io/03 Model Loading/01 Assimp/
环境光(ambient)
环境光的添加比较简单,使用一个很小的常量乘(光照)颜色,添加到物体片段的最终颜色中,这样子的话即便场景中没有直接的光源也能看起来存在有一些发散的光。
测试代码如下,其中 ambientRatio
是环境光照系数
vec4 objColor = texture(texture_diffuse1, TexCoords);
vec4 objColor = texture(texture_diffuse1, TexCoords);
// 物体颜色
vec3 TexColor = vec3(objColor.x, objColor.y, objColor.z);
// 环境光
vec3 ambient = lightColor * ambientRatio;
result = ambient * TexColor;
FragColor = vec4(result, 1.0);
漫反射光照(diffuse)
为了让物体看起来有明暗变化,需要添加漫反射光线(可以类比窗帘)
当光线落到物体上时,我们需要测量光线接触物体的角度,以点光源为例,下图画出水平面上的光线分布情况
当光线垂直射向物体时,物体最明亮;而当光线斜射向物体时,随着角度的增大,物体会变得越来越暗;
(参考一个手电筒垂直照向一个平面,然后越来越近,周围越来越暗)
如图,有了光源到片元之间的方向向量,又有片元的法向量,我们就可以用点乘方便地表示这个夹角大小。
两个单位向量的夹角越小,它们点乘的结果越倾向于1。当两个向量的夹角为90度的时候,点乘会变为0。这同样适用于θ,θ越大,光对片段颜色的影响就应该越小。
顶点着色器示例:
void main()
{
TexCoords = aTexCoords;
FragPos = vec3(model * transform * vec4(aPos, 1.0));
gl_Position = projection * view * vec4(FragPos, 1.0);
Normal = aNormal;
}
片元着色器示例:
void main()
{
vec4 objColor = texture(texture_diffuse1, TexCoords);
// 物体颜色
vec3 TexColor = vec3(objColor.x, objColor.y, objColor.z);
// 环境光
vec3 ambient = lightColor * ambientRatio;
// 漫反射
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0);
vec3 diffuse = diff * lightColor;
vec3 result = (ambient + diffuse) * TexColor;
FragColor = vec4(result, 1.0);
}
值得注意的是 环境光 与 漫反射 的光之间 是相加(混合)而 不是相乘(反射)
镜面光照(specular)
镜面反射除了和光的方向向量以及物体的法向量相关外,还和观察方向有关。
我们通过反射法向量周围光的方向来计算反射向量。然后我们计算反射向量和视线方向的角度差,如果夹角越小,那么镜面光的影响就会越大。它的作用效果就是,当我们去看光被物体所反射的那个方向的时候,我们会看到一个高光。
准备
一般来说模型传到 shader 中的基本信息有:位置,贴图坐标,以及法线。而位置和法线都有局部坐标系和世界坐标系之分。我们在 shader 中接收的均是局部坐标系下的位置以及法线,想要在世界坐标空间下计算就必须将其转换到世界坐标空间中。
法向量
由于法线是一个向量,正常情况下我们可以用 model
矩阵将点的位置以及法向量变换到世界坐标空间下。使用齐次坐标就可以方便地做到这一点,点的位置(vec4(aPos, 1.0)), 法向量(vec4(aNormal, 0.0))。
但是如图,当缩放矩阵包含非均匀比例时,可能会出现右图的情况,结果就是 : 变换后的法线不再垂直于面
如何解决这个问题呢?
由于一个顶点的切向量
T
T
T 和法向量
N
N
N 一定是垂直的,
T
⋅
N
=
0
T·N = 0
T⋅N=0,变换后的
T
1
T^1
T1 和
N
1
N^1
N1 也满足这个条件。
给定一个矩阵
M
M
M,
T
1
=
M
T
T^1 = MT
T1=MT ; 假设法向量
N
N
N 的变换矩阵为
G
G
G,则可得
N
1
⋅
T
1
=
(
G
N
)
⋅
(
M
T
)
=
0
N^1 · T^1 = (GN) · (MT) = 0
N1⋅T1=(GN)⋅(MT)=0
将向量的点积转换为矩阵的乘积
(
G
N
)
⋅
(
M
T
)
=
(
G
N
)
T
(
M
T
)
=
N
T
G
T
M
T
(GN) · (MT) = (GN)^T(MT) = N^TG^TMT
(GN)⋅(MT)=(GN)T(MT)=NTGTMT
由于
N
T
T
=
0
N^TT=0
NTT=0, 如果
G
T
M
=
I
G^TM=I
GTM=I, 则
N
T
G
T
M
T
=
0
N^TG^TMT=0
NTGTMT=0, 可知
G
=
(
M
−
1
)
T
G=(M^{-1})^T
G=(M−1)T。
因此,使法向量正确地旋转的旋转矩阵为顶点 旋转矩阵的逆矩阵的转置矩阵,如果矩阵
M
M
M为正交矩阵,那么
M
−
1
=
M
T
M^{-1} = M^T
M−1=MT
shader中使用inverse
和transpose
处理,代码如下。
Normal = mat3(transpose(inverse(model))) * aNormal;
注意: 逆矩阵的开销还是很大的,建议在 cpu 中执行,通过 uniform 传递给着色器。
镜面
本文选择在世界坐标系下进行光照计算,但不少人习惯在观察空间下计算,看个人爱好吧
使用相机位置作为观察者位置
首先需要设置一个镜面强度(Specular Intensity)变量
float specularStrength = 0.5;
然后得到视线方向向量,以及反射向量
vec3 viewDir = normalize(viewPos - FragPos);
// 注意 lightDir 取反
vec3 reflectDir = reflect(-lightDir, norm);
接着计算镜面分量
float spec = pow(max(dot(viewDir, reflectDir), 0.0), shininess);
vec3 specular = specularStrength * spec * lightColor;
shininess 是高光的反光度,物体反光度越高,反射光的能力就越强,高光点就越小。
接着加到环境光分量和漫反射分量里,然后再乘以物体的颜色。
vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);
光照着色器早期是在顶点着色器中实现冯氏光照模型,称为Ground着色(Ground Shading),而不是冯氏着色(Phong Shading)