0 Preface
本节笔记对应的内容为投光物,部分内容参考自傅老師/OpenGL教學 第二章。
前面的课程我们实现的光照是一种类似于点光源的照明,但是又不存在衰减半径。它能给我们不错的效果,但现实世界中,我们有很多种类的光照,每种的表现都不同。将光投射(Cast)到物体的光源叫做投光物(Light Caster)。在这一节中,我们将会讨论几种不同类型的投光物。学会模拟不同种类的光源是又一个能够进一步丰富场景的工具。
我们首先将会讨论定向光(Directional Light),接下来是点光源(Point Light),它是我们之前学习的光源的拓展,最后我们将会讨论聚光(Spotlight)。
1 Directional Light
当一个光源处于很远的地方时,来自光源的每条光线就会近似于互相平行。当我们使用一个假设光源放置于无限远处时,它就被称为定向光,因为它的所有光线都有着相同的方向,它与光源的位置是没有关系的。
定向光非常好的一个例子就是太阳。太阳距离我们并不是无限远,但它已经远到在光照计算中可以把它视为无限远了。所以来自太阳的所有光线将被模拟为平行光线,我们可以在下图看到:
因为所有的光线都是平行的,所以物体与光源的相对位置是不重要的,因为对场景中每一个物体光的方向都是一致的。由于光的位置向量保持一致,场景中每个物体的光照计算将会是一致的。
我们可以定义一个光线方向向量而不是位置向量来模拟一个定向光。着色器的计算基本保持不变,但这次我们将直接通过外部传入的光的lightDir向量而不是通过direction来计算lightDir向量。
uniform vec3 lightDir;
void main() {
// vec3 lightDir = normalize(lightPos - FragPos);
vec3 reflectVec = reflect(-lightDir, Normal);
vec3 cameraVec = normalize(cameraPos - FragPos);
...
}
注意我们首先对light.direction向量取反。我们目前使用的光照计算需求一个从片段至光源的光线方向,但人们更习惯定义定向光为一个从光源出发的全局方向。所以我们需要对全局光照方向向量取反来改变它的方向,它现在是一个指向光源的方向向量了。而且,记得对向量进行标准化,假设输入向量为一个单位向量是很不明智的。
最终的lightDir向量将和以前一样用在漫反射和镜面光计算中。
我们现在需要封装一下平行光部分的代码,加入LightDirectional类:
LightDirectional.h:
#ifndef _LIGHTDIRECTIONAL_H_
#define _LIGHTDIRECTIONAL_H_
#include <glm/glm.hpp>
#include <glm/gtx/rotate_vector.hpp>
class LightDirectional
{
public:
LightDirectional(glm::vec3 _position, glm::vec3 _angles, glm::vec3 _color = glm::vec3(1.0f, 1.0f, 1.0f));
~LightDirectional();
glm::vec3 position;
glm::vec3 angles;
glm::vec3 direction = glm::vec3(0, 0, 1.0f);
glm::vec3 color;
void UpdateDirection();
};
#endif
通过传入针对各坐标轴的旋转角计算目标向量
LightDirectional.cpp:
#include "LightDirectional.h"
LightDirectional::LightDirectional(glm::vec3 _position, glm::vec3 _angles, glm::vec3 _color)
: position(_position), angles(_angles), color(_color)
{
UpdateDirection();
}
LightDirectional::~LightDirectional()
{
}
void LightDirectional::UpdateDirection()
{
direction = glm::vec3(0, 0, 1.0f); // pointing to z(forward)
direction = glm::rotateZ(direction, angles.z);
direction = glm::rotateX(direction, angles.x);
direction = glm::rotateY(direction, angles.y);
direction = -1.0f * direction;
}
传入光源位置、颜色及光源方向:
#include "LightDirectional.h"
......
#pragma region Light Declare
LightDirectional light (glm::vec3(10.0f, 10.0f, -5.0f),glm::vec3(glm::radians(45.0f),0,0));
#pragma endregion
......
glUniform3f(glGetUniformLocation(myShader->ID, "lightPos"), light.position.x,light.position.y,light.position.z);
glUniform3f(glGetUniformLocation(myShader->ID, "lightColor"), light.color.x,light.color.y,light.color.z);
glUniform3f(glGetUniformLocation(myShader->ID, "lightDir"), light.direction.x, light.direction.y, light.direction.z);
观察结果可以发现光源都来自一个方向
2 Point lights
定向光对于照亮整个场景的全局光源是非常棒的,但除了定向光之外我们也需要一些分散在场景中的点光源(Point Light)。点光源是处于世界中某一个位置的光源,它会朝着所有方向发光,但光线会随着距离逐渐衰减。想象作为投光物的灯泡和火把,它们都是点光源。
在之前的教程中,我们一直都在使用一个(简化的)点光源。我们在给定位置有一个光源,它会从它的光源位置开始朝着所有方向散射光线。然而,我们定义的光源模拟的是永远不会衰减的光线,这看起来像是光源亮度非常的强。在大部分的3D模拟中,我们都希望模拟的光源仅照亮光源附近的区域而不是整个场景。
Attenuation(衰减)
随着光线传播距离的增长逐渐削减光的强度通常叫做衰减(Attenuation)。随距离减少光强度的一种方式是使用一个线性方程。这样的方程能够随着距离的增长线性地减少光的强度,从而让远处的物体更暗。然而,这样的线性方程通常会看起来比较假。在现实世界中,灯在近处通常会非常亮,但随着距离的增加光源的亮度一开始会下降非常快,但在远处时剩余的光强度就会下降的非常缓慢了。所以,我们需要一个不同的公式来减少光的强度。
下面这个公式根据片段距光源的距离计算了衰减值,之后我们会将它乘以光的强度向量:
\[F_{att} = \frac{1.0}{K_{c} + K_{l} \ast d + K_{q} \ast d^{2}}\]
在这里\(d\)代表了片元距光源的距离。接下来为了计算衰减值,我们定义3个(可配置的)项:常数constant项\(K_{c}\)、一次linear项\(K_{l}\)和二次quadratic项\(K_{q}\)。
- 常数项通常保持为1.0,它的主要作用是保证分母永远不会比1小,否则的话在某些距离上它反而会增加强度,这肯定不是我们想要的效果。
- 一次项会与距离值相乘,以线性的方式减少强度。
- 二次项会与距离的平方相乘,让光源以二次递减的方式减少强度。二次项在距离比较小的时候影响会比一次项小很多,但当距离值比较大的时候它就会比一次项更大了。
由于二次项的存在,光线会在大部分时候以线性的方式衰退,直到距离变得足够大,让二次项超过一次项,光的强度会以更快的速度下降。这样的结果就是,光在近距离时亮度很高,但随着距离变远亮度迅速降低,最后会以更慢的速度减少亮度。下面这张图显示了在100的距离内衰减的效果:
你可以看到光在近距离的时候有着最高的强度,但随着距离增长,它的强度明显减弱,并缓慢地在距离大约100的时候强度接近0。这正是我们想要的。
Choosing the right values
但是,该对这三个项设置什么值呢?正确地设定它们的值取决于很多因素:环境、希望光覆盖的距离、光的类型等。这些值一般都是经验值。下表显示了这些项可能取的一些值。第一列指定的是当前参数下光所能覆盖的距离。该数据由Ogre3D的Wiki所提供:
距离 | 常数项 | 一次项 | 二次项 |
---|---|---|---|
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 |
你可以看到,常数项\(K_{c}\)在所有的情况下都是1.0。一次项\(K_{l}\)为了覆盖更远的距离通常都很小,二次项\(K_{q}\)甚至更小。尝试对这些值进行实验,看看它们在你的实现中有什么效果。在我们的环境中,32到100的距离对大多数的光源都足够了。
Implementing attenuation
为了实现衰减,在片段着色器中我们还需要三个额外的值:也就是公式中的常数项、一次项和二次项。现在新增LightPoint类:
LightPoint.h
#ifndef _LIGHTPOINT_H_
#define _LIGHTPOINT_H_
#include <glm/glm.hpp>
#include <glm/gtx/rotate_vector.hpp>
class LightPoint
{
public:
LightPoint(glm::vec3 _position, glm::vec3 _angles, glm::vec3 _color = glm::vec3(1.0f, 1.0f, 1.0f));
~LightPoint();
glm::vec3 position;
glm::vec3 angles;
glm::vec3 direction = glm::vec3(0, 0, 1.0f);
glm::vec3 color;
float constant;
float linear;
float quadratic;
};
#endif
我们希望光源能够覆盖50的距离,所以我们会使用表格中对应的常数项、一次项和二次项:
LightPoint.cpp
#include "LightPoint.h"
LightPoint::LightPoint(glm::vec3 _position, glm::vec3 _angles, glm::vec3 _color)
: position(_position), angles(_angles), color(_color)
{
constant = 1.0f;
linear = 0.09f;
quadratic = 0.032f;
}
片元着色器中新增LightPoint类,并且为了视觉效果,只对diffuse和specular做衰减,完整shader代码如下:
#version 330 core
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;
struct Material {
vec3 ambient;
sampler2D diffuse;
sampler2D specular;
float shininess;
};
struct LightPoint {
float constant;
float linear;
float quadratic;
};
uniform Material material;
uniform LightPoint lightP;
uniform vec3 objColor;
uniform vec3 ambientColor;
uniform vec3 lightPos;
uniform vec3 lightDirUniform;
uniform vec3 lightColor;
uniform vec3 cameraPos;
out vec4 FragColor;
void main() {
float dist = length(lightPos - FragPos);
float attenuation = 1.0 / (lightP.constant + lightP.linear * dist +
lightP.quadratic * (dist * dist));
vec3 lightDir = normalize(lightPos - FragPos);
vec3 reflectVec = reflect(-lightDir, Normal);
vec3 cameraVec = normalize(cameraPos - FragPos);
// specular
float specularAmount =
pow(max(dot(reflectVec, cameraVec), 0), material.shininess);
// vec3 specular = material.specular * specularAmount * lightColor;
vec3 specular =
texture(material.specular, TexCoord).rgb * specularAmount * lightColor;
// diffuse
// vec3 diffuse = material.diffuse * max(dot(lightDir, Normal), 0) *
// lightColor;
vec3 diffuse = texture(material.diffuse, TexCoord).rgb *
max(dot(lightDir, Normal), 0) * lightColor;
// ambient
vec3 ambient = texture(material.diffuse, TexCoord).rgb * ambientColor;
FragColor =
vec4((ambient + (diffuse + specular) * attenuation) * objColor, 1.0f);
}
主函数中新增传入上述三参数
glUniform3f(glGetUniformLocation(testShader->ID, "lightPos"), light.position.x, light.position.y, light.position.z);
glUniform3f(glGetUniformLocation(testShader->ID, "lightDirUniform"), light.direction.x, light.direction.y, light.direction.z);
glUniform3f(glGetUniformLocation(testShader->ID, "lightColor"), light.color.r, light.color.g, light.color.b);
glUniform1f(glGetUniformLocation(testShader->ID, "lightP.constant"), light.constant);
glUniform1f(glGetUniformLocation(testShader->ID, "lightP.linear"), light.linear);
glUniform1f(glGetUniformLocation(testShader->ID, "lightP.quadratic"), light.quadratic);
实现效果如下,可见光强随距离有明显衰减,远处的箱子颜色较暗:
3 Spotlight
我们要讨论的最后一种类型的光是聚光(Spotlight)。聚光是位于环境中某个位置的光源,它只朝一个特定方向而不是所有方向照射光线。这样的结果就是只有在聚光方向的特定半径内的物体才会被照亮,其它的物体都会保持黑暗。聚光很好的例子就是路灯或手电筒。
OpenGL中聚光是用一个世界空间位置、一个方向和一个切光角(Cutoff Angle)来表示的,切光角指定了聚光圆锥的半径。对于每个片元,我们会计算片元是否位于聚光的切光方向之间(也就是在锥形内),如果是的话,我们就会相应地照亮片段。下面这张图会让你明白聚光是如何工作的:
LightDir
:从片段指向光源的向量。SpotDir
:聚光所指向的方向。Phi
\(\phi\):指定了聚光半径的切光角。落在这个角度之外的物体都不会被这个聚光所照亮。Theta
\(\theta\):LightDir向量和SpotDir向量之间的夹角。在聚光内部的话\(\theta\)值应该比\(\phi\)值小。
所以我们要做的就是计算LightDir向量和SpotDir向量之间的点积(返回两个单位向量夹角的余弦值),并将它与切光角\(\phi\)值对比。
新建LightSpot类,LightSpot.h:
#ifndef _LIGHTSPOT_H_
#define _LIGHTSPOT_H_
#include <glm/glm.hpp>
#include <glm/gtx/rotate_vector.hpp>
class LightSpot
{
public:
LightSpot(glm::vec3 _position, glm::vec3 _angles, glm::vec3 _color = glm::vec3(1.0f, 1.0f, 1.0f));
~LightSpot();
glm::vec3 position;
glm::vec3 angles;
glm::vec3 direction = glm::vec3(0, 0, 1.0f);
glm::vec3 color;
float cosPhy = 0.9f;
void UpdateDirection();
};
#endif
UpdateDiection函数的实现与DirectionalLight中相同。
fragment Shader:
#version 330 core
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;
struct Material {
vec3 ambient;
sampler2D diffuse;
sampler2D specular;
float shininess;
};
struct LightPoint {
float constant;
float linear;
float quadratic;
};
struct LightSpot {
float cosPhy;
};
uniform Material material;
uniform LightPoint lightP;
uniform LightSpot lightS;
uniform vec3 objColor;
uniform vec3 ambientColor;
uniform vec3 lightPos;
uniform vec3 lightDirUniform;
uniform vec3 lightColor;
uniform vec3 cameraPos;
out vec4 FragColor;
void main() {
vec3 lightDir = normalize(lightPos - FragPos);
vec3 reflectVec = reflect(-lightDir, Normal);
vec3 cameraVec = normalize(cameraPos - FragPos);
// specular
float specularAmount =
pow(max(dot(reflectVec, cameraVec), 0), material.shininess);
// vec3 specular = material.specular * specularAmount * lightColor;
vec3 specular =
texture(material.specular, TexCoord).rgb * specularAmount * lightColor;
// diffuse
// vec3 diffuse = material.diffuse * max(dot(lightDir, Normal), 0) *
// lightColor;
vec3 diffuse = texture(material.diffuse, TexCoord).rgb *
max(dot(lightDir, Normal), 0) * lightColor;
// ambient
vec3 ambient = texture(material.diffuse, TexCoord).rgb * ambientColor;
float cosTheta = dot(normalize(FragPos - lightPos), -1 * lightDirUniform);
if (cosTheta > lightS.cosPhy) {
// inside
FragColor = vec4((ambient + (diffuse + specular)) * objColor, 1.0f);
} else {
// outside
FragColor = vec4((ambient)*objColor, 1.0f);
}
}
main文件中传入cosPhy的值
glUniform1f(glGetUniformLocation(testShader->ID, "lightS.cosPhy"), light.cosPhy);
上图效果仍看起来有些假,主要是因为聚光有一圈硬边。当一个片段遇到聚光圆锥的边缘时,它会完全变暗,没有一点平滑的过渡。一个真实的聚光将会在边缘处逐渐减少亮度。
Smooth/Soft edges(平滑过渡)
为了创建一种看起来边缘平滑的聚光,我们需要模拟聚光有一个内圆锥(Inner Cone)和一个外圆锥(Outer Cone)。我们可以将内圆锥设置为上一部分中的那个圆锥,但我们也需要一个外圆锥,来让光从内圆锥逐渐减暗,直到外圆锥的边界。
为了创建一个外圆锥,我们只需要再定义一个余弦值来代表聚光方向向量和外圆锥向量(等于它的半径)的夹角。然后,如果一个片段处于内外圆锥之间,将会给它计算出一个0.0到1.0之间的强度值。如果片段在内圆锥之内,它的强度就是1.0,如果在外圆锥之外强度值就是0.0。
我们可以用下面这个公式来计算这个值:
\[I = \frac{\theta −\gamma }{\epsilon }\]
这里\(\epsilon \)(Epsilon)是内(\(\phi\))和外圆锥(\(\gamma\))之间的余弦值差(\(\epsilon = \phi −\gamma \))。最终的\(I\)值就是在当前片段聚光的强度。
LightSpot.h文件中新增cosPhyOutter
float cosPhyInner = 0.9f;
float cosPhyOutter = 0.85f;
fragmentSource中:
float spotRatio;
if (cosTheta > lightS.cosPhyInner) {
// inside
spotRatio = 1.0f;
} else if (cosTheta > lightS.cosPhyOutter) {
// middle
spotRatio = 1 - (cosTheta - lightS.cosPhyInner) /
(lightS.cosPhyOutter - lightS.cosPhyInner);
} else {
// outside
spotRatio = 0;
}
FragColor = vec4((ambient + diffuse + specular) * spotRatio * objColor, 1.0f);
这样看起来光线就好多了。
「如果这篇文章对你有用,请随意打赏」
如果这篇文章对你有用,请随意打赏
使用微信扫描二维码完成支付

comments powered by Disqus