LearnOpenGL 学习笔记 光照

文章详细介绍了OpenGL中的光照处理,包括颜色、环境光照、漫反射光照、法向量的计算和处理,以及镜面光照。强调了法向量在不等比缩放下的问题和解决方法——法线矩阵。还讨论了逐顶点和逐片元的光照渲染差异,以及冯氏着色和平滑着色的概念。此外,提到了材质、光源类型(平行光、点光源、聚光)和衰减的计算,以及如何使用贴图和多光源增强效果。
摘要由CSDN通过智能技术生成

教程网址

https://learnopengl-cn.github.io/02%20Lighting/01%20Colors/

颜色

环境光照

漫反射光照

法向量

计算漫反射的时候需要法向量乘以光线方向

光线方向是世界坐标系中的,那么法向量也需要是世界坐标系中的

但是存在一些问题

首先,法向量只是一个方向向量,不能表达空间中的特定位置。同时,法向量没有齐次坐标(顶点位置中的w分量)。这意味着,位移不应该影响到法向量。因此,如果我们打算把法向量乘以一个模型矩阵,我们就要从矩阵中移除位移部分,只选用模型矩阵左上角 3×3 的矩阵(注意,我们也可以把法向量的 w 分量设置为 0,再乘以 4×4 矩阵;这同样可以移除位移)。对于法向量,我们只希望对它实施缩放和旋转变换。

其次,如果模型矩阵执行了不等比缩放,顶点的改变会导致法向量不再垂直于表面了。因此,我们不能用这样的模型矩阵来变换法向量。下面的图展示了应用了不等比缩放的模型矩阵对法向量的影响:

每当我们应用一个不等比缩放时(注意:等比缩放不会破坏法线,因为法线的方向没被改变,仅仅改变了法线的长度,而这很容易通过标准化来修复),法向量就不会再垂直于对应的表面了,这样光照就会被破坏。

同时,可见,不等比缩放的模型矩阵应用后得到的切向量还是正确的

修复这个行为的诀窍是使用一个为法向量专门定制的模型矩阵。这个矩阵称之为法线矩阵(Normal Matrix),它使用了一些线性代数的操作来移除对法向量错误缩放的影响。

假设变换之前的切向量为 T,法向量为 N,变换之后的切向量为 T’,法向量为 N’

那么我们期望 N ′ ⋅ T ′ = N ⋅ T = 0 N' \cdot T' = N \cdot T = 0 NT=NT=0

假设模型矩阵为 M,对切向量的变换为 G

那么 N ′ ⋅ T ′ = ( G N ) ⋅ ( M T ) = ( G N ) T ∗ ( M T ) = N T G T M T N' \cdot T' = (GN) \cdot (MT) = (GN)^T * (MT) = N^TG^TMT NT=(GN)(MT)=(GN)T(MT)=NTGTMT

已知 N T T = 0 N^TT = 0 NTT=0,只要 G T M = I G^TM = I GTM=I,那么就有 N T G T M T = N T T = 0 N^TG^TMT = N^TT = 0 NTGTMT=NTT=0

所以设 G T M = I G^TM = I GTM=I,那么 G = ( M − 1 ) T G = (M^{-1})^T G=(M1)T

也就是法线矩阵被定义为「模型矩阵左上角3x3部分的逆矩阵的转置矩阵」。

注意,大部分的资源都会将法线矩阵定义为应用到模型-观察矩阵(Model-view Matrix)上的操作,但是由于我们只在世界空间中进行操作(不是在观察空间),我们只使用模型矩阵。

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

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

矩阵求逆是一项对于着色器开销很大的运算,因为它必须在场景中的每一个顶点上进行,所以应该尽可能地避免在着色器中进行求逆运算。以学习为目的的话这样做还好,但是对于一个高效的应用来说,你最好先在CPU上计算出法线矩阵,再通过uniform把它传递给着色器(就像模型矩阵一样)。

在漫反射光照部分,光照表现并没有问题,这是因为我们没有对物体进行任何缩放操作,所以我们并不真的需要使用一个法线矩阵,而是仅以模型矩阵乘以法线就可以。但是如果你会进行不等比缩放,使用法线矩阵去乘以法向量就是必须的了。

镜面光照

法向与视线方向求 cos

逐顶点和逐片元

在光照着色器的早期,开发者曾经在顶点着色器中实现冯氏光照模型。在顶点着色器中做光照的优势是,相比片段来说,顶点要少得多,因此会更高效,所以(开销大的)光照计算频率会更低。然而,顶点着色器中的最终颜色值是仅仅只是那个顶点的颜色值,片段的颜色值是由插值光照颜色所得来的。结果就是这种光照看起来不会非常真实,除非使用了大量顶点。

在顶点着色器中实现的冯氏光照模型叫做 Gouraud 着色(Gouraud Shading),而不是冯氏着色(Phong Shading)。记住,由于插值,这种光照看起来有点逊色。冯氏着色能产生更平滑的光照效果。

逐片元的 phong shading

顶点着色器

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;

out vec3 FragPos;
out vec3 Normal;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    FragPos = vec3(model * vec4(aPos, 1.0));
    Normal = mat3(transpose(inverse(model))) * aNormal;  
    
    gl_Position = projection * view * vec4(FragPos, 1.0);
}

片元着色器

#version 330 core
out vec4 FragColor;

in vec3 Normal;  
in vec3 FragPos;  
  
uniform vec3 lightPos; 
uniform vec3 viewPos; 
uniform vec3 lightColor;
uniform vec3 objectColor;

void main()
{
    // ambient
    float ambientStrength = 0.1;
    vec3 ambient = ambientStrength * lightColor;
  	
    // diffuse 
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(lightPos - FragPos);
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = diff * lightColor;
    
    // specular
    float specularStrength = 0.5;
    vec3 viewDir = normalize(viewPos - FragPos);
    vec3 reflectDir = reflect(-lightDir, norm);  
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
    vec3 specular = specularStrength * spec * lightColor;  
        
    vec3 result = (ambient + diffuse + specular) * objectColor;
    FragColor = vec4(result, 1.0);
} 
逐顶点的 gound shading

其实就是照抄 phong shading 就好了

顶点着色器

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;

out vec3 FragPos;
out vec3 Normal;
out vec4 VertColor;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

uniform vec3 lightPos; 
uniform vec3 viewPos; 
uniform vec3 lightColor;
uniform vec3 objectColor;

void main()
{
    vec3 FragPos = vec3(model * vec4(aPos, 1.0));
    vec3 Normal = mat3(transpose(inverse(model))) * aNormal;  
    
    // ambient
    float ambientStrength = 0.1;
    vec3 ambient = ambientStrength * lightColor;
  	
    // diffuse 
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(lightPos - FragPos);
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = diff * lightColor;
    
    // specular
    float specularStrength = 0.5;
    vec3 viewDir = normalize(viewPos - FragPos);
    vec3 reflectDir = reflect(-lightDir, norm);  
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
    vec3 specular = specularStrength * spec * lightColor;  
        
    vec3 result = (ambient + diffuse + specular) * objectColor;
    
    VertColor = vec4(result, 1.0);

    gl_Position = projection * view * vec4(FragPos, 1.0);
}

片元着色器

#version 330 core
out vec4 FragColor;

in vec4 VertColor;  

void main()
{
    FragColor = VertColor;
} 

结果

在这里插入图片描述

这里在三角形接缝的地方出现了直线的高光

其实就是因为顶点着色器的输出变量到片元着色器的输入变量的时候是插值的

不然,想想也知道,顶点就那么几个顶点,却要映射到片元里面各个像素,所以肯定要插值

那么这个高光插值的时候,就是两个三角形各自的顶点算出来颜色之后插值,那么这种情况下,如果高光在两个三角形那个共享边的两个顶点上,插值出来的接缝上的高光就会更加明显

材质

当描述一个表面时,我们可以分别为三个光照分量定义一个材质颜色(Material Color):环境光照(Ambient Lighting)、漫反射光照(Diffuse Lighting)和镜面光照(Specular Lighting)。通过为每个分量指定一个颜色,我们就能够对表面的颜色输出有细粒度的控制了。现在,我们再添加一个反光度(Shininess)分量,结合上述的三个颜色,我们就有了全部所需的材质属性了:

#version 330 core
struct Material {
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
    float shininess;
}; 

uniform Material material;

ambient 材质向量定义了在环境光照下这个表面反射的是什么颜色,通常与表面的颜色相同。diffuse 材质向量定义了在漫反射光照下表面的颜色。漫反射颜色(和环境光照一样)也被设置为我们期望的物体颜色。specular 材质向量设置的是表面上镜面高光的颜色(或者甚至可能反映一个特定表面的颜色)。最后,shininess 影响镜面高光的散射/半径。

物体过亮的原因是环境光、漫反射和镜面光这三个颜色对任何一个光源都全力反射。光源对环境光、漫反射和镜面光分量也分别具有不同的强度。

前面的章节中,我们通过使用一个强度值改变环境光和镜面光强度的方式解决了这个问题。我们想做类似的事情,但是这次是要为每个光照分量分别指定一个强度向量。

struct Light {
    vec3 position;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};

uniform Light light;

就是说由光源里面的属性来控制 phong 三个光照项的亮度

那么其实光源的三个属性也可以定义光源颜色

片元着色器中的计算变为:

vec3 ambient  = light.ambient * material.ambient;
vec3 diffuse  = light.diffuse * (diff * material.diffuse);
vec3 specular = light.specular * (spec * material.specular);

vec3 result = (ambient + diffuse + specular) * objectColor;

光照贴图

漫反射贴图

phong shading 的 diffuse 部分改为对一个贴图采样

镜面光贴图

phong shading 的 specular 部分改为对一个贴图采样

投光物

平行光

对于平行光,phong shading 中光的方向不再使用点到光源的位置相减来计算,而是已知平行光的指向,

旧 OpenGL(固定函数式)决定光源是定向光还是位置光源(Positional Light Source)的方法

if(lightVector.w == 0.0) // 注意浮点数据类型的误差
  // 执行定向光照计算
else if(lightVector.w == 1.0)
  // 根据光源的位置做光照计算(与上一节一样)

点光源

之前章节用的,但是之前没设置衰减

点光源就是一个能够配置位置和衰减的光源

衰减

光的衰减在现实世界中一般是随着距离非线性的

在这里插入图片描述
在这里d代表了片段距光源的距离。接下来为了计算衰减值,我们定义3个(可配置的)项:常数项Kc、一次项Kl和二次项Kq

  • 常数项通常保持为1.0,它的主要作用是保证分母永远不会比1小,否则的话在某些距离上它反而会增加强度,这肯定不是我们想要的效果。

  • 一次项会与距离值相乘,以线性的方式减少强度。

  • 二次项会与距离的平方相乘,让光源以二次递减的方式减少强度。二次项在距离比较小的时候影响会比一次项小很多,但当距离值比较大的时候它就会比一次项更大了。

由于二次项的存在,光线会在大部分时候以线性的方式衰退,直到距离变得足够大,让二次项超过一次项,光的强度会以更快的速度下降。这样的结果就是,光在近距离时亮度很高,但随着距离变远亮度迅速降低,最后会以更慢的速度减少亮度。

但是,该对这三个项设置什么值呢?正确地设定它们的值取决于很多因素:环境、希望光覆盖的距离、光的类型等。在大多数情况下,这都是经验的问题,以及适量的调整。下面这个表格显示了模拟一个(大概)真实的,覆盖特定半径(距离)的光源时,这些项可能取的一些值。第一列指定的是在给定的三项时光所能覆盖的距离。这些值是大多数光源很好的起始点,它们由Ogre3D的Wiki所提供:

在这里插入图片描述

聚光

我们要讨论的最后一种类型的光是聚光(Spotlight)。聚光是位于环境中某个位置的光源,它只朝一个特定方向而不是所有方向照射光线。这样的结果就是只有在聚光方向的特定半径内的物体才会被照亮,其它的物体都会保持黑暗。聚光很好的例子就是路灯或手电筒。

OpenGL中聚光是用一个世界空间位置、一个方向和一个切光角(Cutoff Angle)来表示的,切光角指定了聚光的半径(译注:是圆锥的半径不是距光源距离那个半径)。对于每个片段,我们会计算片段是否位于聚光的切光方向之间(也就是在锥形内),如果是的话,我们就会相应地照亮片段。下面这张图会让你明白聚光是如何工作的:

  • LightDir:从片段指向光源的向量。

  • SpotDir:聚光所指向的方向。

  • Phi ϕ:指定了聚光半径的切光角。落在这个角度之外的物体都不会被这个聚光所照亮。

  • Theta θ:LightDir 向量和 SpotDir 向量之间的夹角。在聚光内部的话 θ 值应该比 ϕ 值小。

所以我们要做的就是计算 LightDir 向量和 SpotDir 向量之间的点积(还记得它会返回两个单位向量夹角的余弦值吗?),并将它与切光角 ϕ 值对比。你现在应该了解聚光究竟是什么了,下面我们将以手电筒的形式创建一个聚光。

手电筒

手电筒(Flashlight)是一个位于观察者位置的聚光,通常它都会瞄准玩家视角的正前方。基本上说,手电筒就是普通的聚光,但它的位置和方向会随着玩家的位置和朝向不断更新。

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

切光角实际上用切光角的余弦来记录

我们只知道 LightDir 向量和 SpotDir 向量之间的点积,也就是 LightDir 向量和 SpotDir 向量之间的夹角 θ 的余弦值,如果要用反三角函数求角度再和切光角比较,那么就是额外的耗费

不如直接比较余弦值,余弦值大的角度小,那么只要 LightDir 向量和 SpotDir 向量之间的夹角的余弦值大于 cutOff 就说明点在聚光之内,可以被照亮

平滑/软化边缘

使用大于号判断会留下硬边缘

为了创建一种看起来边缘平滑的聚光,我们需要模拟聚光有一个内圆锥(Inner Cone)和一个外圆锥(Outer Cone)。我们可以将内圆锥设置为上一部分中的那个圆锥,但我们也需要一个外圆锥,来让光从内圆锥逐渐减暗,直到外圆锥的边界。

也就是定义两个 cutOff,然后,如果一个片段处于内外圆锥之间,将会给它计算出一个 0.0 到 1.0 之间的强度值。如果片段在内圆锥之内,它的强度就是 1.0,如果在外圆锥之外强度值就是 0.0。

这样就能得到柔和的边缘

多光源

假设只有一个定向光,和多个点光源,那么片元着色器会是

out vec4 FragColor;

void main()
{
  // 定义一个输出颜色值
  vec3 output;
  // 将定向光的贡献加到输出中
  output += someFunctionToCalculateDirectionalLight();
  // 对所有的点光源也做相同的事情
  for(int i = 0; i < nr_of_point_lights; i++)
    output += someFunctionToCalculatePointLight();
  // 也加上其它的光源(比如聚光)
  output += someFunctionToCalculateSpotLight();

  FragColor = vec4(output, 1.0);
}

为了简洁代码,需要抽象定向光,处理定向光的函数,点光源数组,处理点光源的函数

函数内部都是 phong shading

定向光

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);
}

定义完了光源与函数,最终的片元着色器:

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);
}

设置数组中元素的属性:

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

可以定义一个点光源类,让它来为你设置 uniform 值,但最后仍然是,一个光源有多少个 uniform 属性,对着色程序就要设置多少次 uniform

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值