光线投射器(Light Casters)
目前我们的渲染场景都只是应用单一来源的光源,现实世界中往往存在多种类型的光源。一个投射光线到物体上的光源我们称为光线投射器(light caster)。下面我们讨论几种光线投射器。
1. 定向光源
- 当一个光源建模为无限远,则称为定向光源(directional light),因为它的光线拥有相同的方向(平行),且独立于光源的位置。下图展示一种定向光源——太阳光:(图片取自书中)
定向光源——太阳光 - 因为光的方向矢量保持不变,因此对于场景中的每个对象的光源计算都是相似的。我们可以通过定义光的方向矢量而不是位置矢量来建模定向光源。这样对于定向光源,在着色器中我们无需通过位置矢量计算光的方向矢量而是直接使用。
struct Light{
// vec3 position; // 对于方向光源,不需要位置矢量
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
...
void main()
{
vec3 lightDir = normalize(-light.direction); // 直接取反
// 下面直接使用lightDir计算扩散光和镜面光
...
}
- 为了展示定向光源的效果,我使用坐标系统章节中产生多个立方体的方法,关键代码如下:
for (unsigned int i = 0; i < 10; i++)
{
glm::mat4 model = glm::mat4(1.0f);
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));
objectShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
- 代码中直接设置光源的方向矢量。
objectShader.setVec3("light.direction", -0.2f, -1.0f, -0.3f);
-
渲染效果
定向光源效果
2. 点光源
-
点光源(point light)是在世界空间中指定位置向所有方向照射的光源,光线会随距离变暗(如灯光)。(图片取自书中)
点光源 - 光线随照射距离逐渐降低强度的过程我们通常称为衰减(attenuation)。最简单的模拟衰减过程我们可以使用线性等式,但是线性等式的效果并不逼真。现实世界中,光的亮度在近距离内会快速衰减,超过一定距离之后衰减会变得缓慢很多。下面我们使用前人提出的公式来进行衰减模拟:
其中,代表片元到光源的距离。要计算衰减值我们定义了(可配置的)3项:常数项,线性项和二次项。- 常数项一般设置为1.0,主要用于保证分母不小于1。
- 线性项与距离相乘,以线性的方式降低光的强度。
- 二次项与距离象限相乘,使光源进行二次衰减。与线性项相比,二次项在近距离的影响很小,但随着距离增加影响逐渐变大。
- 上面公式的衰减效果。(图片取自书中)
衰减 - 下面给出上述公式各项的常用值设置
距离 | 常数项 | 线性项 | 二次项 |
---|---|---|---|
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.00007 |
- 要实现光的衰减,我们在片元着色器添加公式中对应的3个项,并将原先定向光源时的方向矢量修改回位置矢量。
struct Light{
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant;
float linear;
float quadratic;
};
- 在片元着色器中完成片元与光源距离和衰减值的计算
// 使用GLSL内置函数length
float distance = length(light.position - FragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
- 将计算后的衰减值分别乘以3个光分量值
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
- 代码中设置各项的值(对照上方的表格,我们大概想覆盖范围为50)
objectShader.setFloat("light.constant", 1.0f);
objectShader.setFloat("light.linear", 0.09f);
objectShader.setFloat("light.quadratic", 0.032f);
-
渲染效果
点光源效果
3. 聚光灯(spotlight)
- 聚光灯是处于场景中某个位置向指定方向投射光线的光源。结果就是只有在聚光灯特定半径范围的物体才会被照亮。
- 在OpenGL中,聚光灯由其世界空间的位置,方向和一个指定其半径的截断角度(cutoff angle)所定义。见下图展示:(图片取自书中)
聚光灯-
LightDir
:从片元指向光源位置的矢量。
-
-
SpotDir
:聚光灯指向的矢量。
-
-
Phi
:截断角度,指定了聚光灯的照射半径,处于该角度外的物体将不会被照亮。
-
-
Theta
:LightDir
和SpotDir
矢量之间的角度。
-
- 闪光灯(flashlight)是一种处于观察者位置的聚光灯,一般指向观察者视角的正前方。闪光灯的位置和方向随着观察者的位置和朝向的变化而变化。下面我们使用闪光灯这种类型的聚光灯来展示场景的渲染效果。
- 对于处于聚光灯下物体的片元颜色的计算,我们需要聚光灯的位置矢量,方向矢量和cutoff角度,我们在片元着色器的
Light
结构添加。
struct Light{
vec3 position;
vec3 direction;
float cutOff;
...
};
- 对于聚光灯,我们需要计算角度并与cutoff角度值进行比较,判断片元是否处于聚光灯的光线椎体内。注意,下面的计算使用的是和的
cos
值进行比较。
float theta = dot(lightDir, normalize(-light.direction));
if(theta > light.cutOff)
{
// 完成光源计算
}
else
{
FragColor = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0);
}
- 代码中设置聚光灯参数的值。
objectShader.setVec3("light.position", camera.Position);
objectShader.setVec3("light.direction", camera.Front);
objectShader.setFloat("light.cutOff", glm::cos(glm::radians(12.5f)));
-
渲染效果
聚光灯效果
4. 聚光灯边缘平滑
- 从上面的渲染效果可以看出边缘界限十分清晰,现实中,聚光灯的边缘应该是逐步缓慢变暗的。要平滑聚光灯的边缘,我们需模拟一个具有内外光线椎体的聚光灯,光线从内锥体到外椎体之间逐渐变暗。
- 对于具有内外光锥的聚光灯,我们需要再定义一个
cos
值来代表光线方向矢量和外光锥之间的角度。如果片元处于内外光锥之间,它的强度值应该处于1.0到0.0之间,如果处于内光锥内则为1.0,处于外光锥外则为0.0。要计算这样的值我们可以使用如下公式:
其中,是内光锥截断角度和外光锥截断角度之差()。而就是当前片元的聚光灯强度值。 - 对于上述公式,很难想象各个参数如何影响最终的渲染效果,下面我们给出一些样本值。
角度 | (内cutoff) | 角度 | (外cutoff) | 角度 | |||
---|---|---|---|---|---|---|---|
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.996 - 0.953 = 0.0448 |
0.966 - 0.953 / 0.0448 = 0.29 |
- 从上面的表格我们大致可以推测出聚光灯的强度值,当处于聚光灯外时值为负,当处于内光锥内时值大于1.0,而当处于内外光锥之间,取值则处于1.0到0.0之间。在片元着色器的计算中,我们使用
clamp
函数,将强度值限制到0.0和1.0之间,这样我们可以取消前面的if-else
判断,将强度值直接应用于光分量。
struct Light{
vec3 position;
vec3 direction;
float cutOff;
float outerCutOff;
...
};
int main()
{
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;
...
}
- 代码中设置cutoff值
objectShader.setFloat("light.cutOff", glm::cos(glm::radians(12.5f)));
objectShader.setFloat("light.outerCutOff", glm::cos(glm::radians(17.5f)));
-
渲染效果
聚光灯边缘平滑效果
网友评论