LearnOpenGL 笔记(二)-光照

目录

九、颜色

创建一个光照场景

 十、基础光照-冯氏光照模型

环境光照

漫反射光照

法线矩阵

镜面光照

十一、材质

十二、光照贴图

漫反射贴图

镜面光贴图

 十三、投光物

平行光-太阳

点光源

实现衰减

聚光

手电筒

平滑/软化边缘

十四、多光源

定向光

点光源

词汇表


九、颜色

定义物体的颜色为物体从一个光源反射各个颜色分量的大小

创建一个光照场景

首先我们需要一个物体来作为被投光(Cast the light)的对象,光源。

为灯创建一个新的VAO

unsigned int lightVAO;
glGenVertexArrays(1, &lightVAO);
glBindVertexArray(lightVAO);
// 只需要绑定VBO不用再次设置VBO的数据,因为箱子的VBO数据中已经包含了正确的立方体顶点数据
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 设置灯立方体的顶点属性(对我们的灯来说仅仅只有位置数据)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

定义一个片段着色器

#version 330 core
out vec4 FragColor;

uniform vec3 objectColor;
uniform vec3 lightColor;

void main()
{
    FragColor = vec4(lightColor * objectColor, 1.0);//将光源的颜色和物体(反射的)颜色相乘
}


// 在此之前不要忘记首先 use 对应的着色器程序(来设定uniform)
lightingShader.use();
lightingShader.setVec3("objectColor", 1.0f, 0.5f, 0.31f);
lightingShader.setVec3("lightColor",  1.0f, 1.0f, 1.0f);

灯的片段着色器给灯定义了一个不变的常量白色,保证了灯的颜色一直是亮的:

#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0); // 将向量的四个分量全部设置为1.0
}

声明一个全局vec3变量来表示光源在场景的世界空间坐标中的位置

glm::vec3 lightPos(1.2f, 1.0f, 2.0f);
把灯位移到这里,然后将它缩小一点,让它不那么明显
model = glm::mat4();
model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(0.2f));

 十、基础光照-冯氏光照模型

冯氏光照模型的主要结构由3个分量组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照

 环境光照(Ambient Lighting):即使在黑暗的情况下,世界上通常也仍然有一些光亮(月亮、远处的光)漫反射光照(Diffuse Lighting):模拟光源对物体的方向性影响(Directional Impact)。它是冯氏光照模型中视觉上最显著的分量。物体的某一部分越是正对着光源,它就会越亮。镜面光照(Specular Lighting):模拟有光泽物体上面出现的亮点。

环境光照

光能够在其它的表面上反射,对一个物体产生间接的影响。考虑到这种情况的算法叫做全局照明(Global Illumination)算法。

我们使用一个很小的常量(光照)颜色,添加到物体片段的最终颜色中

void main()
{//用光的颜色乘以一个很小的常量环境因子,再乘以物体的颜色
    float ambientStrength = 0.1;
    vec3 ambient = ambientStrength * lightColor;

    vec3 result = ambient * objectColor;
    FragColor = vec4(result, 1.0);
}

漫反射光照

 图左上方有一个光源,它所发出的光线落在物体的一个片段上。

计算漫反射光照需要什么?

  • 法向量:一个垂直于顶点表面的向量。
layout (location = 1) in vec3 aNormal;
将法向量由顶点着色器传递到片段着色器
out vec3 Normal;

void main()
{
    Normal = aNormal;
}
在片段着色器中定义相应的输入变量
in vec3 Normal;
  • 定向的光线:作为光源的位置与片段的位置之间向量差的方向向量。

由于光源的位置是一个静态变量,在片段着色器中把它声明为uniform

uniform vec3 lightPos;
//在渲染循环中(渲染循环的外面也可以,因为它不会改变)更新uniform
lightingShader.setVec3("lightPos", lightPos);

片段的位置:在世界空间中进行所有的光照计算,因此我们需要一个在世界空间中的顶点位置。

out vec3 FragPos;  
      FragPos = vec3(model * vec4(aPos, 1.0));
//光的方向向量是光源位置向量与片段位置向量之间的向量差。
vec3 norm = normalize(Normal);//把法线和最终的方向向量都进行标准化
vec3 lightDir = normalize(lightPos - FragPos);

//对norm和lightDir向量进行点乘,计算光源对当前片段实际的漫发射影响
float diff = max(dot(norm, lightDir), 0.0);
//如果两个向量之间的角度大于90度,点乘的结果就会变成负数

//结果值再乘以光的颜色,得到漫反射分量
vec3 diffuse = diff * lightColor;

环境光分量和漫反射分量,我们把它们相加,然后把结果乘以物体的颜色
vec3 result = (ambient + diffuse) * objectColor;
FragColor = vec4(result, 1.0);

法线矩阵

法向量只是一个方向向量,不能表达空间中的特定位置。同时,法向量没有齐次坐标(顶点位置中的w分量)。这意味着,位移不应该影响到法向量。

其次,如果模型矩阵执行了不等比缩放,顶点的改变会导致法向量不再垂直于表面了。

 法线矩阵(Normal Matrix),它使用了一些线性代数的操作来移除对法向量错误缩放的影响。

在顶点着色器中,我们可以使用inverse和transpose函数自己生成这个法线矩阵,这两个函数对所有类型矩阵都有效。(注意我们还要把被处理过的矩阵强制转换为3×3矩阵,来保证它失去了位移属性以及能够乘以vec3的法向量。)

Normal = mat3(transpose(inverse(model))) * aNormal;

镜面光照

镜面光照也是依据光的方向向量和物体的法向量来决定的,但是它也依赖于观察方向,例如玩家是从什么方向看着这个片段的。镜面光照是基于光的反射特性。

 观察向量是镜面光照附加的一个变量,我们可以使用观察者世界空间位置和片段的位置来计算它。

uniform vec3 viewPos;//观察者的世界空间坐标,简单地使用摄像机对象的位置坐标代替

lightingShader.setVec3("viewPos", camera.Position);


//镜面强度
float specularStrength = 0.5;

//计算视线方向向量,和对应的沿着法线轴的反射向量
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
//reflect函数要求第一个向量是从光源指向片段位置的向量,所以取反

//计算镜面分量
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);//32是高光的反光度(Shininess)
vec3 specular = specularStrength * spec * lightColor;

加到环境光分量和漫反射分量里,再用结果乘以物体的颜色
vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);

十一、材质

在OpenGL中模拟多种类型的物体,我们必须为每个物体分别定义一个材质(Material)属性。

材质颜色(Material Color):环境光照(Ambient Lighting)、漫反射光照(Diffuse Lighting)和镜面光照(Specular Lighting)。

创建一个结构体(Struct)来储存物体的材质属性,声明一个uniform变量。

struct Material {
    vec3 ambient;//通常这是和物体颜色相同的颜色
    vec3 diffuse;//在漫反射光照下物体的颜色
    vec3 specular;//镜面光照对物体的颜色影响
    float shininess;//影响镜面高光的散射/半径
}; 

uniform Material material;

void main()
{    
    // 环境光
    vec3 ambient = lightColor * material.ambient;

    // 漫反射 
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(lightPos - FragPos);
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = lightColor * (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 = lightColor * (spec * material.specular);  

    vec3 result = ambient + diffuse + specular;
    FragColor = vec4(result, 1.0);
}

//对每个单独的uniform进行设置,但这次要带上结构体名的前缀
lightingShader.setVec3("material.ambient",  1.0f, 0.5f, 0.31f);
lightingShader.setVec3("material.diffuse",  1.0f, 0.5f, 0.31f);
lightingShader.setVec3("material.specular", 0.5f, 0.5f, 0.5f);
lightingShader.setFloat("material.shininess", 32.0f);

修改光源的漫反射和镜面光强度

struct Light {
    vec3 position;//

    vec3 ambient;//环境光照通常会设置为一个比较低的强度
    vec3 diffuse;//漫反射分量通常设置为光所具有的颜色
    vec3 specular;//镜面光分量通常会保持为vec3(1.0),以最大强度发光
};

uniform Light light;

十二、光照贴图

引入漫反射镜面光贴图(Map)

漫反射贴图

使用一张覆盖物体的图像,让我们能够逐片段索引其独立的颜色值。它通常叫做一个漫反射贴图(Diffuse Map)。

在着色器中使用漫反射贴图的方法和纹理教程中是完全一样的。但这次我们会将纹理储存为Material结构体中的一个sampler2D。将之前定义的vec3漫反射颜色向量替换为漫反射贴图。

struct Material {
    sampler2D diffuse;
    vec3      specular;
    float     shininess;
}; 
...
in vec2 TexCoords;
//从纹理中采样片段的漫反射颜色值
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));


//将要用的纹理单元赋值到material.diffuse这个uniform采样器
lightingShader.setInt("material.diffuse", 0);
...
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, diffuseMap);

顶点数据现在包含了顶点位置、法向量和立方体顶点处的纹理坐标。

layout (location = 2) in vec2 aTexCoords;
...
out vec2 TexCoords;

void main()
{
    ...
    TexCoords = aTexCoords;
}

镜面光贴图

专门用于镜面高光的纹理贴图,生成一个黑白的(如果你想得话也可以是彩色的)纹理,来定义物体每部分的镜面光强度。镜面高光的强度可以通过图像每个像素的亮度来获取。

                   

                     镜面光贴图                                                   漫反射贴图

黑色代表颜色向量vec3(0.0),灰色代表颜色向量vec3(0.5)。在片段着色器中,我们接下来会取样对应的颜色值并将它乘以光源的镜面强度。一个像素越「白」,乘积就会越大,物体的镜面光分量就会越亮。

lightingShader.setInt("material.specular", 1);
...
glActiveTexture(GL_TEXTURE1);//绑定到合适的纹理单元
glBindTexture(GL_TEXTURE_2D, specularMap);

//更新片段着色器的材质属性
struct Material {
    sampler2D diffuse;
    sampler2D specular;//new
    float     shininess;
};

vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));

 十三、投光物

将光投射(Cast)到物体的光源叫做投光物(Light Caster)。

平行光-太阳

当一个光源处于很远的地方时,来自光源的每条光线就会近似于互相平行。当我们使用一个假设光源处于无限远处的模型时,它就被称为定向光,因为它的所有光线都有着相同的方向,它与光源的位置是没有关系的。

 定义一个光线方向向量而不是位置向量来模拟一个定向光。直接使用光的direction向量

struct Light {
    // vec3 position; // 使用定向光就不再需要了
    vec3 direction;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};
...
void main()
{
  vec3 lightDir = normalize(-light.direction);
//vec3 lightDir = normalize(lightPos - FragPos);不用这个
  ...
}

lightingShader.setVec3("light.direction", -0.2f, -1.0f, -0.3f);

点光源

点光源是处于世界中某一个位置的光源,它会朝着所有方向发光,但光线会随着距离逐渐衰减。

随着光线传播距离的增长逐渐削减光的强度通常叫做衰减(Attenuation)。 在现实世界中,灯在近处通常会非常亮,但随着距离的增加光源的亮度一开始会下降非常快,但在远处时剩余的光强度就会下降的非常缓慢了。

d代表了片段距光源的距离。

实现衰减

 为了实现衰减,在片段着色器中我们还需要三个额外的值:也就是公式中的常数项、一次项和二次项。它们最好储存在之前定义的Light结构体中。

struct Light {
    vec3 position;  

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;

    float constant;
    float linear;
    float quadratic;
};
//根据公式计算衰减值
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;

lightingShader.setFloat("light.constant",  1.0f);
lightingShader.setFloat("light.linear",    0.09f);
lightingShader.setFloat("light.quadratic", 0.032f);//表格给出

聚光

聚光是位于环境中某个位置的光源,它只朝一个特定方向而不是所有方向照射光线。这样的结果就是只有在聚光方向的特定半径内的物体才会被照亮,其它的物体都会保持黑暗。

 OpenGL中聚光是用一个世界空间位置、一个方向和一个切光角(Cutoff Angle)来表示的,切光角指定了聚光的半径。计算LightDir向量和SpotDir向量之间的点积

  • LightDir:从片段指向光源的向量。
  • SpotDir:聚光所指向的方向。
  • \phi:聚光半径的切光角。落在这个角度之外的物体都不会被这个聚光所照亮。
  • \theta:LightDir向量和SpotDir向量之间的夹角

手电筒

手电筒就是普通的聚光,但它的位置和方向会随着玩家的位置和朝向不断更新。

struct Light {
    vec3  position;
    vec3  direction;
    float cutOff;
    ...
};

lightingShader.setFloat("light.cutOff",   glm::cos(glm::radians(12.5f)));
//用角度值计算了一个余弦值
//在片段着色器中,我们会计算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);

平滑/软化边缘

为了创建一种看起来边缘平滑的聚光,我们需要模拟聚光有一个内圆锥(Inner Cone)和一个外圆锥(Outer Cone)。为了创建一个外圆锥,我们只需要再定义一个余弦值来代表聚光方向向量和外圆锥向量(等于它的半径)的夹角。然后,如果一个片段处于内外圆锥之间,将会给它计算出一个0.0到1.0之间的强度值。如果片段在内圆锥之内,它的强度就是1.0,如果在外圆锥之外强度值就是0.0。

float theta     = dot(lightDir, normalize(-light.direction));
float epsilon   = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);   
//clamp函数,它把第一个参数约束(Clamp)在了0.0到1.0之间 
...
// 将不对环境光做出影响,让它总是能有一点光
diffuse  *= intensity;
specular *= intensity;

十四、多光源

为了在场景中使用多个光源,我们希望将光照计算封装到GLSL函数中,我们对每个光照类型都创建一个不同的函数:定向光、点光源和聚光。

void main()
{
    // 属性
    vec3 norm = normalize(Normal);
    vec3 viewDir = normalize(viewPos - FragPos);

    // 第一阶段:定向光照
    vec3 result = CalcDirLight(dirLight, norm, viewDir);
    // 第二阶段:点光源
    for(int i = 0; i < NR_POINT_LIGHTS; i++)
        result += CalcPointLight(pointLights[i], norm, FragPos, viewDir);    
    // 第三阶段:聚光
    //result += CalcSpotLight(spotLight, norm, FragPos, viewDir);    

    FragColor = vec4(result, 1.0);
}

定向光

struct DirLight {
    vec3 direction;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};  
uniform DirLight dirLight;


vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir)
{
    vec3 lightDir = normalize(-light.direction);
    // 漫反射着色
    float diff = max(dot(normal, lightDir), 0.0);
    // 镜面光着色
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    // 合并结果
    vec3 ambient  = light.ambient  * vec3(texture(material.diffuse, TexCoords));
    vec3 diffuse  = light.diffuse  * diff * vec3(texture(material.diffuse, TexCoords));
    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
    return (ambient + diffuse + specular);
}

点光源

struct PointLight {
    vec3 position;

    float constant;
    float linear;
    float quadratic;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};  
#define NR_POINT_LIGHTS 4
uniform PointLight pointLights[NR_POINT_LIGHTS];
//使用了预处理指令来定义了我们场景中点光源的数量

vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
    vec3 lightDir = normalize(light.position - fragPos);
    // 漫反射着色
    float diff = max(dot(normal, lightDir), 0.0);
    // 镜面光着色
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    // 衰减
    float distance    = length(light.position - fragPos);
    float attenuation = 1.0 / (light.constant + light.linear * distance + 
                 light.quadratic * (distance * distance));    
    // 合并结果
    vec3 ambient  = light.ambient  * vec3(texture(material.diffuse, TexCoords));
    vec3 diffuse  = light.diffuse  * diff * vec3(texture(material.diffuse, TexCoords));
    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
    ambient  *= attenuation;
    diffuse  *= attenuation;
    specular *= attenuation;
    return (ambient + diffuse + specular);
}

设置点光源的uniform值,点光源的uniform现在是一个PointLight的数组了

lightingShader.setFloat("pointLights[0].constant", 1.0f);

词汇表

  • 冯氏光照模型(Phong Lighting Model):一个通过计算环境光,漫反射,和镜面光分量的值来估计真实光照的模型。
  • 法线矩阵(Normal Matrix):一个3x3矩阵,或者说是没有平移的模型(或者模型-观察)矩阵。
  • 衰减(Attenuation):光随着距离减少强度的过程。
  • 聚光(Spotlight):一个被定义为在某一个方向上的锥形的光源。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值