我们目前使用的光照都来自于空间中的一个点。它能给我们不错的效果,但现实世界中,我们有很多种类的光照,每种的表现都不同。将光投射(Cast)到物体的光源叫做投光物(Light Caster)。在这一节中,我们将会讨论几种不同类型的投光物。学会模拟不同种类的光源是又一个能够进一步丰富场景的工具。
我们首先将会讨论定向光(Directional Light),接下来是点光源(Point Light),它是我们之前学习的光源的拓展,最后我们将会讨论聚光(Spotlight)。在下一节中我们将讨论如何将这些不同种类的光照类型整合到一个场景之中。
当一个光源处于很远的地方时,来自光源的每条光线就会近似于互相平行。不论物体和/或者观察者的位置,看起来好像所有的光都来自于同一个方向。当我们使用一个假设光源处于无限远处的模型时,它就被称为定向光,因为它的所有光线都有着相同的方向,它与光源的位置是没有关系的。
定向光非常好的一个例子就是太阳。太阳距离我们并不是无限远,但它已经远到在光照计算中可以把它视为无限远了。所以来自太阳的所有光线将被模拟为平行光线,我们可以在下图看到:
因为所有的光线都是平行的,所以物体与光源的相对位置是不重要的,因为对场景中每一个物体光的方向都是一致的。由于光的位置向量保持一致,场景中每个物体的光照计算将会是类似的。
我们可以定义一个光线方向向量而不是位置向量来模拟一个定向光。着色器的计算基本保持不变,但这次我们将直接使用光的direction向量而不是通过direction来计算lightDir向量。
glm::vec3 lightDir = glm::vec3(0.0f, 0.0f, -1.0f);
...
struct UniformBufferObject {
...
glm::vec3 viewPos;
float foo = 0.0f;
glm::vec3 lightDirect;
};
首先定义一个全局的光照方向,之后在UniformBufferObject中添加新的数据lightDirect,此数据之前添加了float foo = 0.0f为了适应vulkan内存对齐。
接下來我們更新更新顶点着色器,
...
layout(binding = 0) uniform UniformBufferObject {
...
vec3 viewPos;
vec3 lightDirect;
} ubo;
...
layout(location = 13) out vec3 lightDirect;
...
void main() {
...
lightDirect= ubo.lightDirect;
}
再下一步我们更新片元着色器,
#version 450
#extension GL_ARB_separate_shader_objects : enable
layout(binding = 1) uniform sampler2D texSampler;
layout(location = 0) in vec3 fragColor;
layout(location = 1) in vec3 fragNormal;
layout(location = 2) in vec2 fragTexCoord;
layout(location = 3) in vec3 fragBaseLight;
layout(location = 4) in float ambientStrength;
layout(location = 5) in vec3 lightPos;
layout(location = 6) in vec3 fragPos;
layout(location = 7) in float specularStrength;
layout(location = 8) in vec3 viewPos;
//Material
layout(location = 9) in vec3 m_ambient;
layout(location = 10) in vec3 m_diffuse;
layout(location = 11) in vec3 m_specular;
layout(location = 12) in float m_shininess;
layout(location = 13) in vec3 lightDirect;
layout(location = 0) out vec4 outColor;
void main() {
...
// Ambient Light
vec3 ambient = ambientStrength * fragBaseLight * lightAmbient ;
// Diffuse Light
vec3 norm = normalize(fragNormal);
//vec3 lightDir = normalize(lightPos - fragPos);
vec3 lightDir = normalize(-lightDirect);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * fragBaseLight * lightDiffuse;
vec3 result = ambient + diffuse ;
outColor = texture(texSampler, fragTexCoord)* vec4(result, 1.0);
...
}
注意我们首先对light.direction向量取反。我们目前使用的光照计算需求一个从片段至光源的光线方向,但人们更习惯定义定向光为一个从光源出发的全局方向。所以我们需要对全局光照方向向量取反来改变它的方向,它现在是一个指向光源的方向向量了。而且,记得对向量进行标准化,假设输入向量为一个单位向量是很不明智的。
定向光对于照亮整个场景的全局光源是非常棒的,但除了定向光之外我们也需要一些分散在场景中的点光源(Point Light)。点光源是处于世界中某一个位置的光源,它会朝着所有方向发光,但光线会随着距离逐渐衰减。想象作为投光物的灯泡和火把,它们都是点光源。
在之前的教程中,我们一直都在使用一个(简化的)点光源。我们在给定位置有一个光源,它会从它的光源位置开始朝着所有方向散射光线。然而,我们定义的光源模拟的是永远不会衰减的光线,这看起来像是光源亮度非常的强。在大部分的3D模拟中,我们都希望模拟的光源仅照亮光源附近的区域而不是整个场景。
首先我們在片元着色器中加上镜面光照:
void main() {
...
// Specular Lighting
vec3 viewDir = normalize(viewPos - fragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), m_shininess);
vec3 specular = spec * fragBaseLight * lightSpecular * lightSpecular;
vec3 result = ambient + diffuse + specular ;
outColor = texture(texSampler, fragTexCoord)* vec4(result, 1.0);
}
编译运行,可以看到:
调整一下角度,再次观察:
你会注意到在最后面的箱子和在灯面前的箱子都以相同的强度被照亮,并没有定义一个公式来将光随距离衰减。我们希望在后排的箱子与前排的箱子相比仅仅是被轻微地照亮。
随着光线传播距离的增长逐渐削减光的强度通常叫做衰减(Attenuation)。随距离减少光强度的一种方式是使用一个线性方程。这样的方程能够随着距离的增长线性地减少光的强度,从而让远处的物体更暗。然而,这样的线性方程通常会看起来比较假。在现实世界中,灯在近处通常会非常亮,但随着距离的增加光源的亮度一开始会下降非常快,但在远处时剩余的光强度就会下降的非常缓慢了。所以,我们需要一个不同的公式来减少光的强度。
幸运的是一些聪明的人已经帮我们解决了这个问题。下面这个公式根据片段距光源的距离计算了衰减值,之后我们会将它乘以光的强度向量:
在这里d代表了片段距光源的距离。接下来为了计算衰减值,我们定义3个(可配置的)项:常数项Kc、一次项Kl和二次项Kq。
由于二次项的存在,光线会在大部分时候以线性的方式衰退,直到距离变得足够大,让二次项超过一次项,光的强度会以更快的速度下降。这样的结果就是,光在近距离时亮度很高,但随着距离变远亮度迅速降低,最后会以更慢的速度减少亮度。下面这张图显示了在100的距离内衰减的效果:
你可以看到光在近距离的时候有着最高的强度,但随着距离增长,它的强度明显减弱,并缓慢地在距离大约100的时候强度接近0。这正是我们想要的。
选择正确的值:
但是,该对这三个项设置什么值呢?正确地设定它们的值取决于很多因素:环境、希望光覆盖的距离、光的类型等。在大多数情况下,这都是经验的问题,以及适量的调整。下面这个表格显示了模拟一个(大概)真实的,覆盖特定半径(距离)的光源时,这些项可能取的一些值。第一列指定的是在给定的三项时光所能覆盖的距离。这些值是大多数光源很好的起始点,它们由Ogre3D的Wiki所提供:
你可以看到,常数项Kc在所有的情况下都是1.0。一次项Kl为了覆盖更远的距离通常都很小,二次项Kq甚至更小。尝试对这些值进行实验,看看它们在你的实现中有什么效果。在我们的环境中,32到100的距离对大多数的光源都足够了。
为了实现衰减,在片段着色器中我们还需要三个额外的值:也就是公式中的常数项、一次项和二次项。由于这三个系数我们很少修改,大部分场景此组合固定值效果最佳,所以我们和光照相关属性类似硬编码在片元着色器中.
void main() {
...
float constant = 1.0f;
float linear = 0.09f;
float quadratic = 0.032f;
...
}
在片段着色器中实现衰减还是比较直接的:我们根据公式计算衰减值,之后再分别乘以环境光、漫反射和镜面光分量。
我们仍需要公式中距光源的距离,还记得我们是怎么计算一个向量的长度的吗?我们可以通过获取片段和光源之间的向量差,并获取结果向量的长度作为距离项。我们可以使用GLSL内建的length函数来完成这一点:
void main() {
...
float distance = length(lightPos - fragPos);
float attenuation = 1.0 / (constant + linear * distance + quadratic * (distance * distance));
...
}
接下来,我们将包含这个衰减值到光照计算中,将它分别乘以环境光、漫反射和镜面光颜色。
void main() {
...
vec3 result = (ambient + diffuse + specular) * attenuation ;
outColor = texture(texSampler, fragTexCoord)* vec4(result, 1.0);
...
}
再次运行,并调整视角:
你可以看到,只有前排的箱子被照亮的,距离最近的箱子是最亮的。后排的箱子一点都没有照亮,因为它们离光源实在是太远了。
点光源就是一个能够配置位置和衰减的光源。它是我们光照工具箱中的又一个光照类型。
我们要讨论的最后一种类型的光是聚光(Spotlight)。聚光是位于环境中某个位置的光源,它只朝一个特定方向而不是所有方向照射光线。这样的结果就是只有在聚光方向的特定半径内的物体才会被照亮,其它的物体都会保持黑暗。聚光很好的例子就是路灯或手电筒。
Vulkan中聚光是用一个世界空间位置、一个方向和一个切光角(Cutoff Angle)来表示的,切光角指定了聚光的半径(译注:是圆锥的半径不是距光源距离那个半径)。对于每个片段,我们会计算片段是否位于聚光的切光方向之间(也就是在锥形内),如果是的话,我们就会相应地照亮片段。下面这张图会让你明白聚光是如何工作的:
所以我们要做的就是计算LightDir向量和SpotDir向量之间的点积(还记得它会返回两个单位向量夹角的余弦值吗?),并将它与切光角ϕ值对比。你现在应该了解聚光究竟是什么了,下面我们将以手电筒的形式创建一个聚光。
手电筒(Flashlight)是一个位于观察者位置的聚光,通常它都会瞄准玩家视角的正前方。基本上说,手电筒就是普通的聚光,但它的位置和方向会随着玩家的位置和朝向不断更新。
所以,在片段着色器中我们需要的值有聚光的位置向量(来计算光的方向向量)、聚光的方向向量和一个切光角。我们可以将它们储存在UniformBufferObject 结构体中:
struct UniformBufferObject {
...
glm::vec3 lightDirect;
float foo2 = 0.0f;
glm::vec3 flashPos;
float outerCutOff;
glm::vec3 flashDir;
float flashCutOff ;
};
在updateUniformBuffer函数中更新这些数据:
void updateUniformBuffer() {
...
ubo.flashPos = camera.Position;
ubo.flashDir = camera.Forward;
ubo.flashCutOff = glm::cos(glm::radians(12.5f));
ubo.outerCutOff = glm::cos(glm::radians(14.5f));
...
}
接下來更新顶点着色器和片元着色器
layout(binding = 0) uniform UniformBufferObject {
...
vec3 flashPos;
float outerCutOff;
vec3 flashDir;
float flashCutOff ;
} ubo;
...
layout(location = 14) out vec3 flashPos;
layout(location = 15) out vec3 flashDir;
layout(location = 16) out float flashCutOff;
layout(location = 17) out float outerCutOff;
...
void main() {
...
flashPos= ubo.flashPos;
flashDir= ubo.flashDir;
flashCutOff= ubo.flashCutOff;
outerCutOff= ubo.outerCutOff;
}
layout(location = 14) in vec3 flashPos;
layout(location = 15) in vec3 flashDir;
layout(location = 16) in float flashCutOff;
layout(location = 17) in float outerCutOff;
你可以看到,我们并没有给切光角设置一个角度值,反而是用角度值计算了一个余弦值,将余弦结果传递到片段着色器中。这样做的原因是在片段着色器中,我们会计算LightDir和SpotDir向量的点积,这个点积返回的将是一个余弦值而不是角度值,所以我们不能直接使用角度值和余弦值进行比较。为了获取角度值我们需要计算点积结果的反余弦,这是一个开销很大的计算。所以为了节约一点性能开销,我们将会计算切光角对应的余弦值,并将它的结果传入片段着色器中。由于这两个角度现在都由余弦角来表示了,我们可以直接对它们进行比较而不用进行任何开销高昂的计算。
接下来就是计算θ值,并将它和切光角ϕ对比,来决定是否在聚光的内部:
void main() {
vec3 lightAmbient = vec3( 0.2f, 0.2f, 0.2f);
vec3 lightDiffuse = vec3( 0.5f, 0.5f, 0.5f);
vec3 lightSpecular = vec3( 1.0f, 1.0f, 1.0f);
float theta = dot(normalize(lightPos - fragPos), normalize(-flashDir));
if(theta > flashCutOff) // 执行光照计算
{
// Ambient Lighting
vec3 ambient = ambientStrength * fragBaseLight * lightAmbient ;
// Diffuse Lighting
vec3 norm = normalize(fragNormal);
// lightDir = normalize(lightPos - fragPos);
vec3 lightDir = normalize(-lightDirect);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * fragBaseLight * lightDiffuse;
// Specular Lighting
vec3 viewDir = normalize(viewPos - fragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), m_shininess);
vec3 specular = spec * fragBaseLight * lightSpecular * lightSpecular;
float constant = 1.0f;
float linear = 0.09f;
float quadratic = 0.032f;
float distance = length(lightPos - fragPos);
float attenuation = 1.0 / (constant + linear * distance + quadratic * (distance * distance));
vec3 result =ambient + (diffuse + specular) * attenuation ;
outColor = texture(texSampler, fragTexCoord)* vec4(result, 1.0);
}
else // 否则,使用环境光,让场景在聚光之外时不至于完全黑暗
outColor = vec4(lightAmbient * vec3(texture(texSampler, fragTexCoord)), 1.0);
}
我们首先计算了lightDir和取反的direction向量(取反的是因为我们想让向量指向光源而不是从光源出发)之间的点积。记住要对所有的相关向量标准化。
运行程序,你将会看到一个聚光,它仅会照亮聚光圆锥内的片段。看起来像是这样的:
但这仍看起来有些假,主要是因为聚光有一圈硬边。当一个片段遇到聚光圆锥的边缘时,它会完全变暗,没有一点平滑的过渡。一个真实的聚光将会在边缘处逐渐减少亮度。
为了创建一种看起来边缘平滑的聚光,我们需要模拟聚光有一个内圆锥(Inner Cone)和一个外圆锥(Outer Cone)。我们可以将内圆锥设置为上一部分中的那个圆锥,但我们也需要一个外圆锥,来让光从内圆锥逐渐减暗,直到外圆锥的边界。
为了创建一个外圆锥,我们只需要再定义一个余弦值来代表聚光方向向量和外圆锥向量(等于它的半径)的夹角。然后,如果一个片段处于内外圆锥之间,将会给它计算出一个0.0到1.0之间的强度值。如果片段在内圆锥之内,它的强度就是1.0,如果在外圆锥之外强度值就是0.0。
我们可以用下面这个公式来计算这个值:
这里ϵ(Epsilon)是内(ϕ)和外圆锥(γ)之间的余弦值差(ϵ=ϕ−γ)。最终的I值就是在当前片段聚光的强度。
很难去表现这个公式是怎么工作的,所以我们用一些实例值来看看:
你可以看到,我们基本是在内外余弦值之间根据θ插值。如果你仍不明白发生了什么,不必担心,只需要记住这个公式就好了,在你更聪明的时候再回来看看。
我们现在有了一个在聚光外是负的,在内圆锥内大于1.0的,在边缘处于两者之间的强度值了。如果我们正确地约束(Clamp)这个值,在片段着色器中就不再需要if-else了,我们能够使用计算出来的强度值直接乘以光照分量:
void main() {
...
// spotlight (soft edges)
float theta = dot(normalize(lightPos - fragPos), normalize(-flashDir));
float epsilon = (flashCutOff - outerCutOff);
float intensity = clamp((theta - outerCutOff) / epsilon, 0.0, 1.0);
// 将不对环境光做出影响,让它总是能有一点光
diffuse *= intensity;
specular *= intensity;
...
注意我们使用了clamp函数,它把第一个参数约束(Clamp)在了0.0到1.0之间。这保证强度值不会在[0, 1]区间之外。
确定你将outerCutOff值添加到了UniformBufferObject结构体之中,并在程序中设置它的值。下面的图片中,我们使用的内切光角是12.5,外切光角是14.5:
你可以在上述的更新数据代码中找到对应的更新。
运行,可以看到:
这样看起来就好多了。稍微对内外切光角实验一下,尝试创建一个更能符合你需求的聚光。
这样的手电筒/聚光类型的灯光非常适合恐怖游戏,结合定向光和点光源,环境就会开始被照亮了。在下一节的教程中,我们将会结合我们至今讨论的所有光照和技巧。
顶点着色器
#version 450
#extension GL_ARB_separate_shader_objects : enable
layout(binding = 0) uniform UniformBufferObject {
mat4 model;
mat4 view;
mat4 proj;
vec3 baseLight;
float ambientStrength;
vec3 lightPos;
float specularStrength ;
vec3 viewPos;
vec3 lightDirect;
vec3 flashPos;
float outerCutOff;
vec3 flashDir;
float flashCutOff ;
} ubo;
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inColor;
layout(location = 2) in vec3 inNormal;
layout(location = 3) in vec2 inTexCoord;
layout(location = 4) in vec3 inM_ambient;
layout(location = 5) in vec3 inM_diffuse;
layout(location = 6) in vec3 inM_specular;
layout(location = 7) in float inM_shininess;
layout(location = 0) out vec3 fragColor;
layout(location = 1) out vec3 fragNormal;
layout(location = 2) out vec2 fragTexCoord;
layout(location = 3) out vec3 fragBaseLight;
layout(location = 4) out float ambientStrength;
layout(location = 5) out vec3 lightPos;
layout(location = 6) out vec3 fragPos;
layout(location = 7) out float specularStrength;
layout(location = 8) out vec3 viewPos;
//Material
layout(location = 9) out vec3 m_ambient;
layout(location = 10) out vec3 m_diffuse;
layout(location = 11) out vec3 m_specular;
layout(location = 12) out float m_shininess;
layout(location = 13) out vec3 lightDirect;
layout(location = 14) out vec3 flashPos;
layout(location = 15) out vec3 flashDir;
layout(location = 16) out float flashCutOff;
layout(location = 17) out float outerCutOff;
layout(push_constant) uniform PushConsts {
vec3 objPos;
} pushConsts;
void main() {
gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition + pushConsts.objPos, 1.0);
fragPos =vec3( ubo.model * vec4(inPosition+ pushConsts.objPos, 1.0));;
fragColor = inColor;
fragNormal = mat3(transpose(inverse(ubo.model))) *inNormal;
fragTexCoord = inTexCoord;
fragBaseLight = ubo.baseLight;
ambientStrength= ubo.ambientStrength;
lightPos=ubo.lightPos;
specularStrength=ubo.specularStrength;
viewPos=ubo.viewPos;
m_ambient = inM_ambient;
m_diffuse = inM_diffuse;
m_specular = inM_specular;
m_shininess = inM_shininess;
lightDirect= ubo.lightDirect;
flashPos= ubo.flashPos;
flashDir= ubo.flashDir;
flashCutOff= ubo.flashCutOff;
outerCutOff= ubo.outerCutOff;
}
片元着色器
#version 450
#extension GL_ARB_separate_shader_objects : enable
layout(binding = 1) uniform sampler2D texSampler;
layout(location = 0) in vec3 fragColor;
layout(location = 1) in vec3 fragNormal;
layout(location = 2) in vec2 fragTexCoord;
layout(location = 3) in vec3 fragBaseLight;
layout(location = 4) in float ambientStrength;
layout(location = 5) in vec3 lightPos;
layout(location = 6) in vec3 fragPos;
layout(location = 7) in float specularStrength;
layout(location = 8) in vec3 viewPos;
//Material
layout(location = 9) in vec3 m_ambient;
layout(location = 10) in vec3 m_diffuse;
layout(location = 11) in vec3 m_specular;
layout(location = 12) in float m_shininess;
layout(location = 13) in vec3 lightDirect;
layout(location = 14) in vec3 flashPos;
layout(location = 15) in vec3 flashDir;
layout(location = 16) in float flashCutOff;
layout(location = 17) in float outerCutOff;
layout(location = 0) out vec4 outColor;
void main() {
vec3 lightAmbient = vec3( 0.2f, 0.2f, 0.2f);
vec3 lightDiffuse = vec3( 0.5f, 0.5f, 0.5f);
vec3 lightSpecular = vec3( 1.0f, 1.0f, 1.0f);
// Ambient Lighting
vec3 ambient = ambientStrength * fragBaseLight * lightAmbient ;
// Diffuse Lighting
vec3 norm = normalize(fragNormal);
// lightDir = normalize(lightPos - fragPos);
vec3 lightDir = normalize(-lightDirect);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * fragBaseLight * lightDiffuse;
// Specular Lighting
vec3 viewDir = normalize(viewPos - fragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), m_shininess);
vec3 specular = spec * fragBaseLight * lightSpecular * lightSpecular;
// spotlight (soft edges)
float theta = dot(normalize(lightPos - fragPos), normalize(-flashDir));
float epsilon = (flashCutOff - outerCutOff);
float intensity = clamp((theta - outerCutOff) / epsilon, 0.0, 1.0);
// 将不对环境光做出影响,让它总是能有一点光
diffuse *= intensity;
specular *= intensity;
// attenuation
float constant = 1.0f;
float linear = 0.09f;
float quadratic = 0.032f;
float distance = length(lightPos - fragPos);
float attenuation = 1.0 / (constant + linear * distance + quadratic * (distance * distance));
vec3 result =ambient + (diffuse + specular) * attenuation ;
outColor = texture(texSampler, fragTexCoord)* vec4(result, 1.0);
//outColor = vec4(fragTexCoord, 0.0, 1.0);
//outColor = vec4(fragColor * texture(texSampler, fragTexCoord).rgb, 1.0);
//outColor = texture(texSampler, fragTexCoord)* vec4(fragBaseLight, 1.0);
}