投光物
简单来说投光物就是光源。投光物可分为定向光和点光源,同时还讨论了聚光效果。
平行光
当假设光源位于无限远的某处,平行光就是与光源位置无关的无数条相互平行的光线,也称为定向光。最容易理解的就是太阳,太阳距离地球足够远,可以近似的将太阳光视为是平行光。
由于光线都是平行的,所以光源的位置就没那么重要了。因此我们可以定义一个光线方向向量,这个
方向向量就足以表示所有的光线。
不再需要光源的位置,需要一个光线的方向。
struct Light {
// vec3 position; // 使用定向光就不再需要了
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
...
void main()
{
vec3 lightDir = normalize(-light.direction);
...
}
一般习惯定义光线方向为照射方向,但对于光照计算需求的是相反方向,则对光线方向进行了取反并标准化。
进行计算,这里使用十个不同位置的箱子。
glm::vec3 cubePositions[] = {
glm::vec3( 0.0f, 0.0f, 0.0f),
glm::vec3( 2.0f, 5.0f, -15.0f),
glm::vec3(-1.5f, -2.2f, -2.5f),
glm::vec3(-3.8f, -2.0f, -12.3f),
glm::vec3( 2.4f, -0.4f, -3.5f),
glm::vec3(-1.7f, 3.0f, -7.5f),
glm::vec3( 1.3f, -2.0f, -2.5f),
glm::vec3( 1.5f, 2.0f, -2.5f),
glm::vec3( 1.5f, 0.2f, -1.5f),
glm::vec3(-1.3f, 1.0f, -1.5f)
};
对每个箱子都生成了一个不同的模型矩阵,每个模型矩阵都包含了对应的局部-世界坐标变换
for(unsigned int i = 0; i < 10; i++)
{
glm::mat4 model;
model = glm::translate(model, cubePositions[i]);
float angle = 20.0f * i;
model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
lightingShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
定义光线的方向
lightingShader.setVec3("light.direction", -0.2f, -1.0f, -0.3f);
效果图
点光源
前两节都是关于点光源的描述,但有一些缺陷,就是光强度并不会衰减,而正常情况离光源越远,应该越暗才对。因此在此引入衰减。
随着光线传播距离的增长逐渐削减光的强度通常叫做衰减。随距离减少光强度的一种方式是使用一个线性方程。这样的方程能够随着距离的增长线性地减少光的强度,但效果看起来会比较假。现实世界中,在离光源越近的地方光照强度越大,衰减越快,离光源越远的地方光照强度越小,衰减越慢。因此这是一个线性方程无法解决的问题,所以需要一个公式来解决此问题
d代表了片段距光源的距离。为了计算衰减值,定义3个项:常数项Kc、一次项Kl和二次项Kq。
常数项通常保持为1.0,它的主要作用是保证分母永远不会比1小,否则的话在某些距离上它反而会增加强度,这肯定不是我们想要的效果。
一次项会与距离值相乘,以线性的方式减少强度。
二次项会与距离的平方相乘,让光源以二次递减的方式减少强度。二次项在距离比较小的时候影响会比一次项小很多,但当距离值比较大的时候它就会比一次项更大了。
公式带来的效果是,光在近距离时亮度很高,但随着距离变远亮度迅速降低,最后会以更慢的速度减少亮度。下面这张图显示了在100的距离内衰减的效果:
随着距离增长,它的强度明显减弱,并缓慢地在距离大约100的时候强度接近0。
对于这三个值得设置,可参考Ogre3D的Wiki,
对于我们的计算,32-100距离基本够用了。
实现衰减
在片段着色器中定义公式的三个常数项
struct Light
{
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 reflection;
float constant;
float linear;
float quadratic;
};
在OpenGL中设置这些项:我们希望光源能够覆盖50的距离,所以我们会使用表格中对应的常数项、一次项和二次项:
lightingShader.setFloat("light.constant", 1.0f);
lightingShader.setFloat("light.linear", 0.09f);
lightingShader.setFloat("light.quadratic", 0.032f);
在片段着色器中实现衰减公式计算,之后再分别乘以环境光、漫反射、镜面反射分量即可。
float distance = length(light.position - FragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
ambient *= attenuation;
diffuse *= attenuation;
reflection *= attenuation;
由于此处是单光源,所以我们也希望衰减环境光照。正常情况下环境光不应该衰减。
效果图
聚光
聚光是位于环境中某个位置的光源,它只朝一个特定方向而不是所有方向照射光线。这样的结果就是只有在聚光方向的特定半径内的物体才会被照亮,其它的物体都会保持黑暗。聚光很好的例子就是路灯或手电筒。
在openGL中聚光是用一个世界坐标的光源位置、一个光照方向以及一个切光角来表示的,切光角指定了聚光的半径,原理如图
LightDir表示的是片段指向光源的向量。
SpotDir表示的是聚光中心所指的方向。
phiϕ表示的是聚光半径的切光角,在这个角之外的物体将不会被此光源照亮。
Thetaθ 表示的是LightDir与SpotDir之间的夹角,在聚光内部θ<ϕ。
计算LightDir与SpotDir向量的点积,即可得到夹角的余弦,通过与切光角ϕ 的余弦进行比较,即可判断片段是否落入切光角内部,即光照可到达的地方。
此处进行余弦处理后,两方的大小发生了变化(cos0=1,角度越大。余弦值越小),所以进行比较时则判断θ > ϕ?
在片段着色器中定义聚光的位置,方向,切光角。
struct Light {
vec3 position;
vec3 direction;
float cutOff;
...
};
设置这些值
lightingShader.setVec3("light.position", camera.Position);
lightingShader.setVec3("light.direction", camera.Front);
lightingShader.setFloat("light.cutOff", glm::cos(glm::radians(12.5f)));
计算theta值,和切光角对比判断是否在聚光内
float theta = dot(lightDir, normalize(-light.direction));
if(theta > light.cutOff)
{
// 执行光照计算
}
else // 否则,使用环境光,让场景在聚光之外时不至于完全黑暗
color = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0);
效果图
这仍看起来有些假,主要是因为聚光有一圈硬边。当一个片段遇到聚光圆锥的边缘时,它会完全变暗,没有一点平滑的过渡。一个真实的聚光将会在边缘处逐渐减少亮度。
平滑/软化边缘
从上图可发现边缘会有一个僵硬的光圈,我们希望是一个平滑的过度。
我们利用一个外圆锥,在内圆锥与外圆锥之间光照强度逐渐削弱,就可以实现边缘的平滑与软化。外圆锥的设置与内圆锥基本一致,在设置在内圆锥内强度为1.0,外圆锥外强度为0.0,之间逐渐递减,可利用如下公式来实现。
假设内圆锥切角为25°,外圆锥切角为35°,则通过一些实例:
可以看出我们是在内外圆锥之间进行插值,θ 在内外圆锥之间逐渐递减,在内圆锥内为大于1的值,在外圆锥外为小于0的值,由此限制在0-1之间。
用计算出的强度值乘以光照分量就可以得出内外圆锥之间的光照分量值。
float theta = dot(lightDir, normalize(-light.direction));
float epsilon = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
...
// 将不对环境光做出影响,让它总是能有一点光
diffuse *= intensity;
specular *= intensity;
...
clamp是限制值在0-1之间的内置函数
效果图:
片段着色器
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
in vec3 Normal;
in vec3 FragPos;
struct Material
{
sampler2D diffuse;
sampler2D reflection;
float shininess;
};
struct Light
{
vec3 position;
vec3 direction;
float cutOff;
float outerCutOff;
vec3 ambient;
vec3 diffuse;
vec3 reflection;
float constant;
float linear;
float quadratic;
};
uniform vec3 viewPos;
uniform Material material;
uniform Light light;
void main()
{
vec3 lightDir = normalize(light.position - FragPos);
// ambient
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
// diffuse
vec3 normal = normalize(Normal);
float diff = max(dot(normal, lightDir), 0.0);
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));;
// specular
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, normal);
float ref = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
vec3 reflection = light.reflection * ref * vec3(texture(material.reflection,TexCoords));
float theta = dot(lightDir, -light.direction);
float epsilon = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
diffuse *= intensity;
reflection *= intensity;
float distance = length(light.position - FragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
//ambient *= attenuation;
diffuse *= attenuation;
reflection *= attenuation;
vec3 result = ambient + diffuse + reflection;
FragColor = vec4(result, 1.0);
}
Application.cpp
lightingShader.use();
lightingShader.setVec3("light.position", camera.Position);
lightingShader.setVec3("light.direction", camera.Front);
lightingShader.setFloat("light.cutOff", glm::cos(glm::radians(12.5f)));
lightingShader.setFloat("light.outerCutOff", glm::cos(glm::radians(17.5f)));
lightingShader.setVec3("viewPos", camera.Position);
lightingShader.setVec3("light.ambient", 0.2f, 0.2f, 0.2f);
lightingShader.setVec3("light.diffuse", 0.5f, 0.5f, 0.5f);
lightingShader.setVec3("light.reflection", 1.0f, 1.0f, 1.0f);
lightingShader.setFloat("light.constant", 1.0f);
lightingShader.setFloat("light.linear", 0.09f);
lightingShader.setFloat("light.quadratic", 0.0032f);
// material properties
lightingShader.setFloat("material.shininess", 64.0f);