重新自学学习openGL 之光照投光物
概念
将光投射(Cast)到物体的光源叫做投光物(Light Caster)
分类
- 定向光(Directional Light)
- 点光源(Point Light)
- 聚光(Spotlight)
定向光
当一个光源处于很远的地方时,来自光源的每条光线就会近似于互相平行。不论物体和/或者观察者的位置,看起来好像所有的光都来自于同一个方向。当我们使用一个假设光源处于无限远处的模型时,它就被称为定向光,因为它的所有光线都有着相同的方向,它与光源的位置是没有关系的。
定向光非常好的一个例子就是太阳。太阳距离我们并不是无限远,但它已经远到在光照计算中可以把它视为无限远了。所以来自太阳的所有光线将被模拟为平行光线,我们可以在下图看到:
定向光
因为所有的光线都是平行的,所以物体与光源的相对位置是不重要的,因为对场景中每一个物体光的方向都是一致的。由于光的位置向量保持一致,场景中每个物体的光照计算将会是类似的。
我们可以定义一个光线方向向量而不是位置向量来模拟一个定向光。着色器的计算基本保持不变,但这次我们将直接使用光的direction
向量而不是通过direction
来计算lightDir
向量。
struct Light{
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
...
void main(){
vec3 lightDir = normalize(-light.direction);
...
}
注意我们首先对light.direction向量取反。我们目前使用的光照计算需求一个从片段至光源的光线方向,但人们更习惯定义定向光为一个从光源出发的全局方向。所以我们需要对全局光照方向向量取反来改变它的方向,它现在是一个指向光源的方向向量了。而且,记得对向量进行标准化,假设输入向量为一个单位向量是很不明智的。
最终的lightDir向量将和以前一样用在漫反射和镜面光计算中。
最终结果如下图
我们一直将光的位置和位置向量定义为vec3,但一些人会喜欢将所有的向量都定义为vec4。当我们将位置向量定义为一个vec4时,很重要的一点是要将w分量设置为1.0,这样变换和投影才能正确应用。然而,当我们定义一个方向向量为vec4的时候,我们不想让位移有任何的效果(因为它仅仅代表的是方向),所以我们将w分量设置为0.0。
方向向量就会像这样来表示:vec4(0.2f, 1.0f, 0.3f, 0.0f)。这也可以作为一个快速检测光照类型的工具:你可以检测w分量是否等于1.0,来检测它是否是光的位置向量;w分量等于0.0,则它是光的方向向量,这样就能根据这个来调整光照计算了:
if(lightVector.w == 0.0) // 注意浮点数据类型的误差
// 执行定向光照计算
else if(lightVector.w == 1.0)
// 根据光源的位置做光照计算(与上一节一样)
你知道吗:这正是旧OpenGL(固定函数式)决定光源是定向光还是位置光源(Positional Light Source)的方法,并根据它来调整光照。
点光源
定向光对于照亮整个场景的全局光源是非常棒的,但除了定向光之外我们也需要一些分散在场景中的点光源(Point Light)。点光源是处于世界中某一个位置的光源,它会朝着所有方向发光,但光线会随着距离逐渐衰减。想象作为投光物的灯泡和火把,它们都是点光源。
在之前的demo中,我们一直都在使用一个(简化的)点光源。我们在给定位置有一个光源,它会从它的光源位置开始朝着所有方向散射光线。但是,以上demo中定义的光源都是不会衰减的光源。在大部分的3D模拟中,我们都希望模拟的光源仅照亮光源附近的区域而不是整个场景。
如果在场景中有十个箱子,我们使用不会衰减的光源.那么不管箱子距离光源是否很远,都是会被照亮的. 如果想让箱子被照亮的强度随着光源的远近进行变化.我们需要在shader中重新定义这样的衰减公式才可以.
衰减
随着光线传播距离的增长逐渐削减光的强度通常叫做衰减(Attenuation)。
随距离减少光强度的一种方式是使用一个线性方程。这样的方程能够随着距离的增长线性地减少光的强度,从而让远处的物体更暗。然而,这样的线性方程通常会看起来比较假。在现实世界中,灯在近处通常会非常亮,但随着距离的增加光源的亮度一开始会下降非常快,但在远处时剩余的光强度就会下降的非常缓慢了。所以,我们需要一个不同的公式来减少光的强度。
幸运的是一些聪明的人已经帮我们解决了这个问题。下面这个公式根据片段距光源的距离计算了衰减值,之后我们会将它乘以光的强度向量:
image.png
在这里d
代表了片段距光源的距离。接下来为了计算衰减值,我们定义3个(可配置的)项:常数项Kc、一次项Kl和二次项Kq。
- 常数项通常保持为1.0,它的主要作用是保证分母永远不会比1小,否则的话在某些距离上它反而会增加强度,这肯定不是我们想要的效果。
- 一次项会与距离值相乘,以线性的方式减少强度。
- 二次项会与距离的平方相乘,让光源以二次递减的方式减少强度。二次项在距离比较小的时候影响会比一次项小很多,但当距离值比较大的时候它就会比一次项更大了。
由于二次项的存在,光线会在大部分时候以线性的方式衰退,直到距离变得足够大,让二次项超过一次项,光的强度会以更快的速度下降。这样的结果就是,光在近距离时亮度很高,但随着距离变远亮度迅速降低,最后会以更慢的速度减少亮度。下面这张图显示了在100的距离内衰减的效果:
image
你可以看到光在近距离的时候有着最高的强度,但随着距离增长,它的强度明显减弱,并缓慢地在距离大约100的时候强度接近0。这正是我们想要的。
公式的具体使用
但是,该对这三个项设置什么值呢?正确地设定它们的值取决于很多因素:环境、希望光覆盖的距离、光的类型等。在大多数情况下,这都是经验的问题,以及适量的调整。下面这个表格显示了模拟一个(大概)真实的,覆盖特定半径(距离)的光源时,这些项可能取的一些值。第一列指定的是在给定的三项时光所能覆盖的距离。这些值是大多数光源很好的起始点,它们由Ogre3D的Wiki所提供:
距离 | 常数项Kc | 一次项Kl | 二次项Kq |
---|---|---|---|
7 | 1.0 | 0.7 | 1.8 |
13 | 1.0 | 0.35 | 0.44 |
20 | 1.0 | 0.22 | 0.20 |
32 | 1.0 | 0.14 | 0.07 |
50 | 1.0 | 0.09 | 0.032 |
65 | 1.0 | 0.07 | 0.017 |
100 | 1.0 | 0.045 | 0.0075 |
160 | 1.0 | 0.027 | 0.0028 |
200 | 1.0 | 0.022 | 0.0019 |
325 | 1.0 | 0.014 | 0.0007 |
600 | 1.0 | 0.007 | 0.0002 |
3250 | 1.0 | 0.0014 | 0.000007 |
你可以看到,常数项Kc在所有的情况下都是1.0。一次项Kl为了覆盖更远的距离通常都很小,二次项Kq甚至更小。尝试对这些值进行实验,看看它们在你的实现中有什么效果。在我们的环境中,32到100的距离对大多数的光源都足够了。
点光源衰减编码
为了实现衰减,在片段着色器中我们还需要三个额外的值:也就是公式中的常数项、一次项和二次项。
struct Light {
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant;
float linear;
float quadratic;
};
shader 对点光源的处理
void main(){
// 环境光
vec3 diffuseT =vec3(texture2D(material.diffuse,v_texture));
vec3 specularT =vec3(texture2D(material.specular,v_texture));
vec3 ambient = light.ambient * diffuseT;
// 漫反射
vec3 norm = normalize(normal);
vec3 lightDir = normalize(light.position - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = light.diffuse * (diff * diffuseT);
// 镜面光
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess*128.0);
vec3 specular = light.specular * (spec * specularT);
float distance = length(light.position - FragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance +
light.quadratic * (distance * distance));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
vec3 result = ambient + diffuse + specular;
gl_FragColor =vec4(result, 1.0);;
}
给shader绑定以及传值
//绑定
weakSelf.bindObject->uniforms[PL_UniformLocationLightPos] = glGetUniformLocation(self.shader.program, "light.position");
weakSelf.bindObject->uniforms[PL_UniformLocationLightAmbient] = glGetUniformLocation(self.shader.program, "light.ambient");
weakSelf.bindObject->uniforms[PL_UniformLocationLightSpecular] = glGetUniformLocation(self.shader.program, "light.specular");
weakSelf.bindObject->uniforms[PL_UniformLocationLightTDiffuse] = glGetUniformLocation(self.shader.program, "light.diffuse");
weakSelf.bindObject->uniforms[PL_UniformLocationLightConstant] = glGetUniformLocation(self.shader.program, "light.constant");
weakSelf.bindObject->uniforms[PL_UniformLocationLightLinear] = glGetUniformLocation(self.shader.program, "light.linear");
weakSelf.bindObject->uniforms[PL_UniformLocationLightQuadratic] = glGetUniformLocation(self.shader.program, "light.quadratic");
传值
PL_Light light;
light.position = GLKVector3Make(1.2f, 1.0f, 2.0f);
light.ambient =GLKVector3Make(0.2,0.2,0.2);
light.diffuse =GLKVector3Make(0.5,0.5,0.5);
light.specular =GLKVector3Make(1.0,1.0,1.0);
light.constant = 1.0;
light.linear = 0.09f;
light.quadratic = 0.032f;
glUniform3fv(self.bindObject->uniforms[PL_UniformLocationLightPos], 1, &light.position);
glUniform3fv(self.bindObject->uniforms[PL_UniformLocationLightAmbient], 1, &light.ambient);
glUniform3fv(self.bindObject->uniforms[PL_UniformLocationLightSpecular], 1, &light.specular);
glUniform3fv(self.bindObject->uniforms[PL_UniformLocationLightTDiffuse], 1, &light.diffuse);
glUniform1fv(self.bindObject->uniforms[PL_UniformLocationLightConstant], 1, &light.constant);
glUniform1fv(self.bindObject->uniforms[PL_UniformLocationLightLinear], 1, &light.linear);
glUniform1fv(self.bindObject->uniforms[PL_UniformLocationLightQuadratic], 1, &light.quadratic);
结果如下
点光源就是一个能够配置位置和衰减的光源。它是我们光照工具箱中的又一个光照类型。
聚光
聚光是位于环境中某个位置的光源,它只朝一个特定方向而不是所有方向照射光线。这样的结果就是只有在聚光方向的特定半径内的物体才会被照亮,其它的物体都会保持黑暗。聚光很好的例子就是路灯或手电筒。
OpenGL中聚光是用一个世界空间位置、一个方向和一个切光角(Cutoff Angle)来表示的,切光角指定了聚光的半径(译注:是圆锥的半径不是距光源距离那个半径)。对于每个片段,我们会计算片段是否位于聚光的切光方向之间(也就是在锥形内),如果是的话,我们就会相应地照亮片段。下面这张图会让你明白聚光是如何工作的:
-
LightDir
:从片段指向光源的向量。 -
SpotDir
:聚光所指向的方向。 -
Phiϕ
:指定了聚光半径的切光角。落在这个角度之外的物体都不会被这个聚光所照亮。 -
Thetaθ
:LightDir向量和SpotDir向量之间的夹角。在聚光内部的话θ值应该比ϕ值小。
上面的图需要看的仔细点
LightDir
指的是图中黑线
φ
指的是红线和蓝线之间的夹角
,这个角度需要我们设置
lightDi
r 和spotDir
相乘 结果是θ的余弦值
,因此这里我们最好传入到片段着色器一个切光角的余弦值进行比较.余弦值越大说明角度越小
SpotDir
是我们传入的光的方向
LightDir 可以用光源位置和顶点位置求值
vec3 lightDir = normalize(light.position - FragPos);
SpotDir
这个需要我们直接指定聚光灯的方向
Thetaθ
可以通过上面的LightDir
和SpotDir
值求得
float theta = dot(lightDir, normalize(-light.direction));
Phiϕ
需要我们指定.
因此,我们给片段着色器传入 光的位置放方向以及 Phiϕ 就可以计算出 我们需要的聚光效果了
所以我们要做的就是计算LightDir向量和SpotDir向量之间的点积(还记得它会返回两个单位向量夹角的余弦值吗?
),并将它与切光角ϕ
值对比。
手电筒
手电筒(Flashlight)是一个位于观察者位置的聚光,通常它都会瞄准玩家视角的正前方。基本上说,手电筒就是普通的聚光,但它的位置和方向会随着玩家的位置和朝向不断更新。
从上面的分析中,聚光需要 光源的位置和方向以及切光角.我们可以重新定义光照为
struct Light{
vec3 position;
vec3 direction;
float cutOff;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant;
float linear;
float quadratic;
};
uniform Light light;
传值
FL_Light light;
light.position = GLKVector3Make(0.0f, 0.0f, 3.0f);
light.ambient =GLKVector3Make(0.2,0.2,0.2);
light.diffuse =GLKVector3Make(0.5,0.5,0.5);
light.specular =GLKVector3Make(1.0,1.0,1.0);
light.constant = 1.0;
light.linear = 0.09f ;
light.quadratic = 0.032f;
// 0.0f, 0.0f, 3.0f
const float YAW = -90.0f;
const float PITCH = 0.0f;
GLKVector3 front;
front.x = cos(radians(YAW)) * cos(radians(PITCH));
front.y = sin(radians(PITCH));
front.z = sin(radians(YAW)) * cos(radians(PITCH));
front = GLKVector3Normalize(front);
light.direction = front;
light.cutOff = cos(12.5*M_PI/180);
glUniform3fv(self.bindObject->uniforms[FL_UniformLocationLightPos], 1, &light.position);
glUniform3fv(self.bindObject->uniforms[FL_UniformLocationLightAmbient], 1, &light.ambient);
glUniform3fv(self.bindObject->uniforms[FL_UniformLocationLightSpecular], 1, &light.specular);
glUniform3fv(self.bindObject->uniforms[FL_UniformLocationLightTDiffuse], 1, &light.diffuse);
glUniform1fv(self.bindObject->uniforms[FL_UniformLocationLightConstant], 1, &light.constant);
glUniform1fv(self.bindObject->uniforms[FL_UniformLocationLightLinear], 1, &light.linear);
glUniform1fv(self.bindObject->uniforms[FL_UniformLocationLightQuadratic], 1, &light.quadratic);
glUniform3fv(self.bindObject->uniforms[FL_UniformLocationLightDirection], 1, &light.direction);
glUniform1fv(self.bindObject->uniforms[FL_UniformLocationLightCutOff], 1, &light.cutOff);
从前面的知识我们知道,我们不需要给切光角传入一个角度值,而用角度值计算了一个余弦值,将余弦结果传递到片段着色器中。这样做的原因是在片段着色器中,我们会计算LightDir和SpotDir向量的点积,这个点积返回的将是一个余弦值而不是角度值,所以我们不能直接使用角度值和余弦值进行比较。为了获取角度值我们需要计算点积结果的反余弦,这是一个开销很大的计算。所以为了节约一点性能开销,我们将会计算切光角对应的余弦值,并将它的结果传入片段着色器中。由于这两个角度现在都由余弦角来表示了,我们可以直接对它们进行比较而不用进行任何开销高昂的计算。
接下来就是计算θ值,并将它和切光角ϕ对比,来决定是否在聚光的内部:
float theta = dot(lightDir, normalize(-light.direction));
if(theta > light.cutOff)
{
// 执行光照计算
}
else // 否则,使用环境光,让场景在聚光之外时不至于完全黑暗
color = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0);
我们首先计算了lightDir和取反的direction向量(取反的是因为我们想让向量指向光源而不是从光源出发)之间的点积。记住要对所有的相关向量标准化。
运行结果如图你可能奇怪为什么在if条件中使用的是 > 符号而不是 < 符号。theta不应该比光的切光角更小才是在聚光内部吗?这并没有错,但不要忘记角度值现在都由余弦值来表示的。一个0度的角度表示的是1.0的余弦值,而一个90度的角度表示的是0.0的余弦值,你可以在下图中看到:
image
你现在可以看到,余弦值越接近1.0,它的角度就越小。这也就解释了为什么theta要比切光值更大了。切光值目前设置为12.5的余弦,约等于0.9978,所以在0.9979到1.0内的<var style="box-sizing: border-box; font-style: normal; font-family: "Courier New", Courier, monospace; color: rgb(34, 34, 119);">theta</var>值才能保证片段在聚光内,从而被照亮。
shader 编码
precision lowp float;
attribute vec3 beginPostion; ///开始位置
attribute vec2 a_texture; //纹理贴图
attribute vec3 a_normal; //法向量
uniform mat4 u_mvpMatrix;
uniform mat4 u_model;
uniform mat4 u_inverModel;
varying lowp vec3 normal;
varying lowp vec3 FragPos;
varying lowp vec2 v_texture;
void main(){
gl_Position =u_mvpMatrix *u_model* vec4(beginPostion, 1.0);
FragPos = vec3(u_model * vec4(beginPostion, 1.0));
normal = mat3(u_inverModel) * a_normal;;
v_texture = a_texture;
}
precision mediump float;
uniform vec3 viewPos;
varying lowp vec3 normal;
varying lowp vec3 FragPos;
varying lowp vec2 v_texture;
struct Light{
vec3 position;
vec3 direction;
float cutOff;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant;
float linear;
float quadratic;
};
uniform Light light;
struct Material{
sampler2D diffuse;
sampler2D specular;
float shininess;
};
uniform Material material;
void main()
{
vec3 lightDir = normalize(light.position - FragPos);
// check if lighting is inside the spotlight cone
float theta = dot(lightDir, normalize(-light.direction));
if(theta > light.cutOff) // remember that we're working with angles as cosines instead of degrees so a '>' is used.
{
// ambient
vec3 ambient = light.ambient * texture2D(material.diffuse, v_texture).rgb;
// diffuse
vec3 norm = normalize(normal);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = light.diffuse * diff * texture2D(material.diffuse, v_texture).rgb;
// specular
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
vec3 specular = light.specular * spec * texture2D(material.specular, v_texture).rgb;
// attenuation
float distance = length(light.position - FragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
// ambient *= attenuation; // remove attenuation from ambient, as otherwise at large distances the light would be darker inside than outside the spotlight due the ambient term in the else branche
diffuse *= attenuation;
specular *= attenuation;
vec3 result = ambient + diffuse + specular;
gl_FragColor = vec4(result, 1.0);
}
else
{
// else, use ambient light so scene isn't completely dark outside the spotlight.
gl_FragColor = vec4(light.ambient * texture2D(material.diffuse, v_texture).rgb, 1.0);
}
}
平滑/软化边缘
上边实现的聚光灯看起来有点假,主要是因为聚光有一圈硬边。当一个片段遇到聚光圆锥的边缘时,它会完全变暗,没有一点平滑的过渡。一个真实的聚光将会在边缘处逐渐减少亮度。
为了创建一种看起来边缘平滑的聚光,我们需要模拟聚光有一个内圆锥(Inner Cone)和一个外圆锥(Outer Cone)。我们可以将内圆锥设置为上一部分中的那个圆锥,但我们也需要一个外圆锥,来让光从内圆锥逐渐减暗,直到外圆锥的边界。
为了创建一个外圆锥,我们只需要再定义一个余弦值来代表聚光方向向量和外圆锥向量(等于它的半径)的夹角。然后,如果一个片段处于内外圆锥之间,将会给它计算出一个0.0到1.0之间的强度值。如果片段在内圆锥之内,它的强度就是1.0,如果在外圆锥之外强度值就是0.0。
我们可以用下面这个公式来计算这个值:
这里ϵ(Epsilon)是内(ϕ)和外圆锥(γ)之间的余弦值差(ϵ=ϕ−γ)。最终的I值就是在当前片段聚光的强度。
我们可以这样理解
基本模型
这里在 θ 在r~ϕ 之间移动,值正好是 0~1 ,正好是余弦值的变化.
r~ϕ 是内边距到外边距的变化根据光照需要逐渐变弱
而余弦值正好符号上述条件, 在0~1 之间值随着 θ的变大正好是变小的 .因此我们可以用余弦值来表示光照强度变化
我们再举例说明下,看下列实例:
θ | θ(角度) | ϕ(内光切) | ϕ(角度) | γ(外光切) | γ(角度) | ϵ | I |
---|---|---|---|---|---|---|---|
0.87 | 30 | 0.91 | 25 | 0.82 | 35 | 0.91 - 0.82 = 0.09 | 0.87 - 0.82 / 0.09 = 0.56 |
0.9 | 26 | 0.91 | 25 | 0.82 | 35 | 0.91 - 0.82 = 0.09 | 0.9 - 0.82 / 0.09 = 0.89 |
0.97 | 14 | 0.91 | 25 | 0.82 | 35 | 0.91 - 0.82 = 0.09 | 0.97 - 0.82 / 0.09 = 1.67 |
0.83 | 34 | 0.91 | 25 | 0.82 | 35 | 0.91 - 0.82 = 0.09 0 | .83 - 0.82 / 0.09 = 0.11 |
0.64 | 50 | 0.91 | 25 | 0.82 | 35 | 0.91 - 0.82 = 0.09 0 | .64 - 0.82 / 0.09 = -2.0 |
0.966 | 15 | 0.9978 | 12.5 | 0.953 | 17.5 | 0.966 - 0.953 = 0.0448 | 0.966 - 0.953 / 0.0448 = 0.29 |
你可以看到,我们基本是在内外余弦值之间根据θ
插值
我们现在有了一个在聚光外是负的,在内圆锥内大于1.0的,在边缘处于两者之间的强度值了。如果我们正确地约束(Clamp)这个值,在片段着色器中就不再需要if-else了,我们能够使用计算出来的强度值直接乘以光照分量:
precision lowp float;
attribute vec3 beginPostion; ///开始位置
attribute vec2 a_texture; //纹理贴图
attribute vec3 a_normal; //法向量
uniform mat4 u_mvpMatrix;
uniform mat4 u_model;
uniform mat4 u_inverModel;
varying lowp vec3 normal;
varying lowp vec3 FragPos;
varying lowp vec2 v_texture;
void main(){
gl_Position =u_mvpMatrix *u_model* vec4(beginPostion, 1.0);
FragPos = vec3(u_model * vec4(beginPostion, 1.0));
normal = mat3(u_inverModel) * a_normal;;
v_texture = a_texture;
}
// spotlight (soft edges)
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函数,它把第一个参数约束(Clamp)在了0.0到1.0之间。这保证强度值不会在[0, 1]区间之外。
最终效果如图
光滑 shader 编码
precision mediump float;
uniform vec3 viewPos;
varying lowp vec3 normal;
varying lowp vec3 FragPos;
varying lowp vec2 v_texture;
struct Light{
vec3 position;
vec3 direction;
float cutOff;
float outerCutOff;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant;
float linear;
float quadratic;
};
uniform Light light;
struct Material{
sampler2D diffuse;
sampler2D specular;
float shininess;
};
uniform Material material;
void main()
{
// ambient
vec3 ambient = light.ambient * texture2D(material.diffuse, v_texture).rgb;
// diffuse
vec3 norm = normalize(normal);
vec3 lightDir = normalize(light.position - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = light.diffuse * diff * texture2D(material.diffuse, v_texture).rgb;
// specular
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
vec3 specular = light.specular * spec * texture2D(material.specular, v_texture).rgb;
// spotlight (soft edges)
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;
// attenuation
float distance = length(light.position - FragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
// ambient *= attenuation; // remove attenuation from ambient, as otherwise at large distances the light would be darker inside than outside the spotlight due the ambient term in the else branche
diffuse *= attenuation;
specular *= attenuation;
vec3 result = ambient + diffuse + specular;
gl_FragColor = vec4(result, 1.0);
}