上一篇基础光照阐述了opengl中颜色的定义,使用以及冯氏光照模型。本篇继续讲和光照相关的材质、光照贴图、投光物等知识。
1、材质
现实世界里,不同的材质在光照下有不同的表现,比如,金属制品在阳光下闪闪发光,但木头就不会,所以我们需要定义物体材质做区分
根据冯氏光照模型,材质也会用以下三个分量来定义:
- 环境光照
- 漫反射光照
- 镜面光照
再加上反光度,这样就可以定义材质了:
struct Material {
vec3 ambient;
vec3 diffuse;
vec3 specular;
float shininess;
};
uniform Material material;
在片段着色器中定义如上结构体,并且定义结构体uniform类型变量,那该怎么给这种类型的uniform变量赋值呢?
objectShader.setVec3("material.ambient", 1.0f, 0.5f, 0.31f);
2、光照
既然材质都已经按冯氏光照模型三个维度定义了,光照也是需要按冯氏模型维度定义的:
struct Light {
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
uniform Light light;
在计算颜色输出时,需要光源的位置信息,所以光源结构体中也增加了光源的位置信息。
这样在计算最终颜色输出时,比如漫反射输出,则用材质的漫反射颜色乘以光源的漫反射颜色即可,一般写法为:
void main()
{
vec3 ambient = light.ambient * material.ambient;
vec3 norm = normalize(normal);
vec3 lightDir = normalize(light.position - fragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = light.diffuse * (diff * material.diffuse);
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 * material.specular);
vec3 result = ambient + diffuse + specular;
FragColor = vec4(result, 1.0);
}
3、光照贴图
目前光照效果只使用了颜色,如果要使用纹理怎么办呢?因为真实场景中,肯定是纹理使用得更多,更真实
纹理代表着一个buf,这个buf可能代表着一张图片,或者一段视频中的yuv某个分量数据,可以认为就是无数颜色的合集,那材质能用颜色,也可以用纹理,所以材质可以这么定义:
struct Material {
sampler2D diffuse;
sampler2D specular;
float shininess;
};
uniform Material material;
所以,现在需要我们给uniform变量赋值,然后再来计算最终颜色输出:
//设置材质纹理id,纹理id就是0,1这类
objectShader.setInt("material.diffuse", 0);
objectShader.setInt("material.specular", 1);
//然后计算最终颜色输出,使用texture函数计算颜色值
void main()
{
vec3 ambient = light.ambient * (texture(material.diffuse, texCoords)).rgb;
vec3 norm = normalize(normal);
vec3 lightDir = normalize(light.position - fragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = light.diffuse * diff * (texture(material.diffuse, texCoords)).rgb;
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 * (texture(material.specular, texCoords)).rgb;
vec3 result = ambient + diffuse + specular;
FragColor = vec4(result, 1.0);
}
4、平行光
光源有多种类型,平行光是其中一种。
- 平行光,比如说太阳,光照强度足够强,距离足够远
- 点光源,比如说灯泡,理论上点光源只能照亮点光源附近的地方,无法照亮远处
- 聚光,类似于手电筒,在光的范围内可照亮物体,范围之外则无法照亮物体
还记得上一节中,光源结构体有个position变量,平行光是不需要光源的位置的,我们只需要光的方向,所以针对平行光,position变direction
struct Light {
// vec3 position;
// 使用定向光就不再需要了
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
//计算光广告,也需要改一改
vec3 lightDir = normalize(-light.direction);
5、点光源
一开始学习的示例就是点光源,但有一个特性,点光源没有考虑到。就是衰减,理论上离点光源位置越远,就会越暗,那怎么计算距离,并且计算光亮的程度呢?
glsl中有函数,可以计算距离:
float distance = length(light.position - fragPos);
衰减系数如上述公式,d既是距离,公式里这几个参数一般按如下方式挑选:
| 距离 | 常数项 | 一次项 | 二次项 |
|---|---|---|---|
| 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 |
综上,点光源结构体应该这么定义:
struct Light {
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
//下边3个成员分别是常量、一次项系数、二次项系数
float constant;
float linear;
float quadratic;
};
float attenuation = 1.0/(light.constant + light.linear * distance + light.quadratic * distance * distance);
距离衰减系数计算如上,最后把漫反射以及镜面结果分别自乘这个系数即可。
6、聚光
还有一种光源像手电筒一样,它能射出一定范围内的光线。
LightDir:从片段指向光源的向量。SpotDir:聚光所指向的方向。Phi:ϕ 指定了聚光半径的切光角。落在这个角度之外的物体都不会被这个聚光所照亮。Thetaθ:LightDir向量和SpotDir向量之间的夹角。在聚光内部的话θ值应该比ϕ值小
所以,光源结构体就要这么定义了:
struct Light {
vec3 position;
vec3 direction;
float cutOff;
float outCutOff;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant;
float linear;
float quadratic;
};
position是光源位置,direction是聚光源的指向,上图就是正向下
float theta = dot(lightDir, normalize(-light.direction));
if(theta > light.cutOff)
{
// 执行光照计算
}
else // 否则,使用环境光,让场景在聚光之外时不至于完全黑暗
color = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0);
但这么做会有个问题,很突兀,比如这样:
所以,为了解决这个问题,需要做边缘平滑工作。
为了创建一种看起来边缘平滑的聚光,我们需要模拟聚光有一个内圆锥(Inner Cone)和一个外圆锥(Outer Cone)。我们可以将内圆锥设置为上一部分中的那个圆锥,但我们也需要一个外圆锥,来让光从内圆锥逐渐减暗,直到外圆锥的边界。
为了创建一个外圆锥,我们只需要再定义一个余弦值来代表聚光方向向量和外圆锥向量(等于它的半径)的夹角。然后,如果一个片段处于内外圆锥之间,将会给它计算出一个0.0到1.0之间的强度值。如果片段在内圆锥之内,它的强度就是1.0,如果在外圆锥之外强度值就是0.0。
我们可以用下面这个公式来计算这个值:
void main()
{
vec3 lightDir = normalize(light.position - fragPos);
float theta = dot(lightDir, normalize(-light.direction));
float epsilon = light.cutOff - light.outCutOff;
float intensity = clamp((theta - light.outCutOff)/epsilon, 0.0, 1.0);
float distance = length(light.position - fragPos);
float attenuation = 1.0/(light.constant + light.linear * distance + light.quadratic * distance * distance);
vec3 ambient = light.ambient * (texture(material.diffuse, texCoords)).rgb;
// ambient = ambient * attenuation;
vec3 norm = normalize(normal);
//如果传的是方向值 ,就直接计算光方向的单位向量
// vec3 lightDir = normalize(-light.direction);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = light.diffuse * diff * (texture(material.diffuse, texCoords)).rgb;
// diffuse = diffuse * attenuation;
diffuse *= intensity;
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 * (texture(material.specular, texCoords)).rgb;
// specular = specular * attenuation;
specular *= intensity;
vec3 result = ambient + diffuse + specular;
FragColor = vec4(result, 1.0);
}