片元的测试与操作

当OpenGL在执行片断着色器内容后,会经过几个处理阶段,判断片元是否可以作为像素绘制到缓存中,以及控制绘制的方式。

比如,如果片元超出了帧缓存的矩形区域,或者它与当前帧缓存中同位置的像素相比,距离视点更远,那么正在处理的过程都会停止,片元也不会被绘制。而另一个阶段当中,片元的颜色会与当前帧缓存中的像素颜色进行混合。

片元在进入到帧缓存之需要经过许多测试过程,并且写入时可以执行一些操作。这些测试和操作大部分都可以通过glEnable()和glDisable()来分别启用和禁止。如果一个片元在某个测试过程中丢弃,那么之后所有的测试或者操作都不会再执行。这些测试和操作的发生顺序如下所示:

  1. 剪切测试(scissor test)
  2. 多重采样的片元操作。
  3. 模板测试(stencil test)
  4. 深度测试(depth test)
  5. 混合(blending)
  6. 抖动(dithering)
  7. 逻辑操作

剪切测试

我们将程序窗口中的一个矩形区域称作剪切盒(scissorbox),并且将所有的绘制操作都限制在这个区域内。 我们可以使用glScissor()来设置这个剪切盒,并且使用glEnable()开启测试。如果片元位于矩形区域内,那么它将通过剪切测试。 默认条件下,剪切矩形与窗口的大小是相等的,并且剪切测试是关闭的。如果已经开启测试,那么所有的渲染,包括窗口的清除,都被限制在剪切盒区域内(这一点与视口的设置不同,后者不会限制屏幕的清除操作)。

多重采样的片元操作

多重采样(multisampling)是一种对集合图元的边缘进行平滑的技术 - 通常也成为反走样(antialiasing)。 默认情况下,多重采样在计算片元的覆盖率时不会考虑alpha的影响。不过,如果使用glEnable()开启某个特定模式,那么片元的alpha值将被纳入到计算过程中。

模板测试

模板缓存的用途之一,就是将绘图范围限制在屏幕的特定区域。模板缓存用来进行复杂的掩模(masking)操作。

深度测试

深度其实就是该象素点在3d世界中距离摄象机的距离。深度测试决定了是否绘制较远的象素点(或较近的象素点)。

混合

如果一个输入的片元通过了所有测试,那么它就可以与颜色缓存中当前的内容通过某种方式进行合并了。

抖动

对于颜色位面数目较小的系统来说,我们可以通过对图像中的颜色进行抖动(dithering)来提升颜色的分辨率,代价是损失一定的空同分辨率。 抖动操作本身是与硬件相关的。OpenGL能做的只是允许开启或者关团这个特性。事实上,在某些机器上,如果颜色分辨率已经非常高,那么开启抖动可能不会产生任何效果。如果要开启或者关团抖动的特性,我们以将参数GL_DITHER传人glEnable()和glDisable()。默认情况下抖动是开启的。

逻辑操作

片元的最后一个操作就是逻辑操作。包括或(OR)、异或(XOR)和反转(INVERT),它作用于输人的片元数据(源)以及当前颜色缓存中的数据(目标)。这类片元操作对于位块传输(bit-blt)类型的系统是非常有用的,因为对它们来说,主要的图形操作就是将窗口中的某一处矩形数据拷贝到另外一处,或者从窗口拷贝到处理器内存,以及从内存拷贝到窗口。通常情况下,这一步拷贝操作不会将数据直接写入内存上,而是允许用户对输入的数据和己有数据之间做一次逻辑操作,然后用操作的结果替换当前已有的数据。 由于这个过程的实现代价对于硬件来说是非常低廉的,因此很多系统都允许这种做法。我们以异或(XOR)操作为例,它可以用来实现可逆的图像绘制操作,因为只要第二次使用XOR进行绘制,就可以还原原始图像。 我们可以将GL_COLOR_LOGIC_OP参数传递给glEnabIe()和glDisable()来开启和禁用逻辑搡作,否则它将保持默认的状态值,也就是GL_COPY。

目前我们使用的所有光照都来自于一个单独的光源。它的效果不错,但是在真实世界,我们有多种类型的光,它们每个表现都不同。一个光源把光投射到物体上,叫做投光。

我们将讨论几种不同的投光类型。分别是定向光(directional light),点光(point light),聚光灯(Spotlight)。

定向光(Directional Light)

当一个光源很远的时候,来自光源的每条光线接近于平行。这看起来就像所有的光线来自于同一个方向,无论物体和观察者在哪儿。当一个光源被设置为无限远时,它被称为定向光(也被成为平行光),因为所有的光线都有着同一个方向。它会独立于光源的位置。

我们知道的定向光源的一个好例子是,太阳。在下面的图片里,来自于太阳的所有的光线都被定义为平行光:

因为所有的光线都是平行的,对于场景中的每个物体光的方向都保持一致,物体和光源的位置保持怎样的关系都无所谓。由于光的方向向量保持一致,光照计算会和场景中的其他物体相似。

通过定义一个光的方向向量,可以模拟这样一个定向光,而不是使用光的位置向量。

为灯光属性定义一个direction,并用它计算灯光向量。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Light
{    
    vec3 direction;
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};
...
void main()
{
    vec3 lightDir = normalize(-light.direction);
    ...
}

光照计算需要光的方向是从片段指向光源,而我们通常定义的定向光方向都是从光源出发,所以要更换它的方向,并且执行标准化操作。

注意: 我们目前把光的位置和方向向量传递为vec3,当然你也可以把所有的向量设置为vec4。当定义位置向量为vec4的时候,把w元素设置为1.0非常重要,这样平移和投影才会合理的被应用。然而,当定义一个方向向量为vec4时,我们并不想让平移发挥作用(因为它们除了代表方向,其他什么也不是)所以我们把w元素设置为0.0。

这可以作为简单检查光的类型的方法:若w元素等于1.0,则我们现在所拥有的是光的位置向量;若w等于0.0,则有一个光的方向向量:

1
2
if(lightVector.w == 0.0) // 执行定向光照计算
else if(lightVector.w == 1.0) // 执行顶点光照计算

定点光(Point Light)

定向光作为全局光可以照亮整个场景,但是另一方面除了定向光,我们通常也需要几个定点光,在场景里发亮。点光是一个在时间里有位置的光源,它向所有方向发光,光线随距离增加逐渐变暗。想象灯泡和火炬作为投光物,它们可以扮演点光的角色。

之前我们已经使用了最简单的点光。我们有一个有位置的光源,它从自身的位置向所有方向发出光线。然而,这个我们定义的光源所模拟光线的从不会衰减。

  • 衰减(Attenuation)

    随着光线穿越更远的距离相应地减少亮度,通常被称为衰减(Attenuation)。 一种随着距离减少亮度的方式是使用线性等式。这样的线性方程,可以使远处的物体更暗。然而,线性方程效果会有点假。在真实世界,通常光在近处时非常亮,但是一个光源的亮度,开始的时候减少的非常快,之后随着距离的增加,减少的速度会慢下来。我们需要一种不同的方程来减少光的亮度。

    幸运的是一些聪明人已经早就把它想到了。下面的方程把一个片段的光的亮度除以一个已经计算出来的衰减值,这个值根据光源的远近得到:

    公式中,I是当前片段的光的亮度,d代表片段到光的距离。为了计算衰减,我们定义三个项:常数项Kc,一次项Kl,二次项Kq。

    常数项Kc:通常是1,这样可以保证分母值比1大,因为当比1小反而会增大亮度。 一次项Kl:与距离相乘,会以线性的方式减少亮度。 二次项Kq:与距离的平方相乘,设置一个亮度的二次衰减。

    最终效果就是光在近距离时,非常亮,但是距离变远亮度迅速降低,最后亮度降低速度再次变慢。下面的图展示了在100以内的范围,这样的衰减效果。

    那么,我们该把这三个值设成什么样的值呢。正确值的设置由很多因素决定:环境、你希望光所覆盖的距离范围、光的类型等。大多数场合,这是经验的问题,也要适度调整。

聚光灯(Spotlight)

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

OpenGL中的聚光灯用世界空间位置、一个方向和一个指定了聚光灯半径的切光角来表示。我们计算的每个片段,如果片段在聚光灯的切光方向之间(就是在圆锥体内),我们就会把片段照亮。下面的图可以让你明白聚光灯是如何工作的:

  • LightDir:从片段指向光源的向量。
  • SpotDir:聚光灯所指向的方向。
  • φ:定义聚光灯半径的切光角。每个落在这个角度之外的,聚光灯都不会照亮。
  • θ:LightDir向量和SpotDir向量之间的角度。θ值应该比φ值小,这样才会在聚光灯内。

所以我们大致要做的是,计算LightDir向量和SpotDir向量的点乘,然后在和遮光角φ对比。

  • 平滑/软化边缘

    为创建聚光灯的平滑边,我们希望去模拟的聚光灯有一个内圆锥和外圆锥。我们可以把内圆锥设置为前面定义的圆锥,我们希望外圆锥从内边到外边逐步的变暗。
    为创建外圆锥,我们简单定义另一个余弦值,它代表聚光灯的方向向量和外圆锥的向量(等于它的半径)的角度。然后,如果片段在内圆锥和外圆锥之间,就会给它计算出一个0.0到1.0之间的亮度。如果片段在内圆锥以内这个亮度就等于1.0,如果在外面就是0.0。


参考 LearnOpenGL-CN 投光物

前面我们为一个物体定义了一个材质,但是现实中物体通常不会只有这么一种材质,一个物体每个部分都可能有多种材质属性。 前面的材质系统除了对最简单的模型外都是不够的,所以我们需要扩展前面的系统,我们要介绍diffuse和specular贴图。它们允许你对一个物体的diffuse(而对于简洁的ambient成分来说,它们几乎总是是一样的)和specular成分能够有更精确的影响。

漫反射贴图

我们希望通过某种方式对每个原始像素独立设置diffuse颜色。这与纹理的道理是一样的。 使用一张图片包裹住物体,我们为每个原始像素索引独立颜色值。在有光的场景里,通常叫做漫反射贴图(Diffuse texture),因为这个纹理图像表现了所有物体的diffuse颜色。

我们把纹理储存为sampler2D,并在Material结构体中。我们使用diffuse贴图替代之前定义的vec3类型的diffuse颜色。

下面是一张带钢圈的木箱贴图:

我们也要移除amibient材质颜色向量,因为ambient颜色绝大多数情况等于diffuse颜色,所以不需要分别去储存它:

1
2
3
4
5
6
7
8
struct Material
{
    sampler2D diffuse;
    vec3 specular;
    float shininess;
};
...
in vec2 TexCoords;

然后我们简单地从纹理采样,来获得原始像素的diffuse颜色值:

1
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));

同样,不要忘记把ambient材质的颜色设置为diffuse材质的颜色:

1
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));

镜面贴图

如果我们的物体是个木箱子,我们知道木头是不应该有镜面高光的。通过把物体设置specular材质设置为vec3(0.0f)来修正它。但是如果木箱贴图带铁边,这将会不再显示镜面高光,我们知道钢铁是会显示一些镜面高光的。我们会想要控制物体部分地显示镜面高光,它带有修改了的亮度。

我们同样用一个纹理贴图,来获得镜面高光。这意味着我们需要生成一个黑白(或者你喜欢的颜色)纹理来定义specular亮度,把它应用到物体的每个部分。下面是一个specular贴图的例子:

一个specular高光的亮度可以通过图片中每个纹理的亮度来获得。specular贴图的每个像素可以显示为一个颜色向量,比如:黑色代表颜色向量vec3(0.0f),灰色是vec3(0.5f)。在片段着色器中,我们采样相应的颜色值,把它乘以光的specular亮度。像素越“白”,乘积的结果越大,物体的specualr部分越亮。

由于箱子几乎是由木头组成,木头作为一个材质不会有镜面高光,整个不透部分的diffuse纹理被用黑色覆盖:黑色部分不会包含任何specular高光。箱子的铁边有一个修改的specular亮度,它自身更容易受到镜面高光影响,木纹部分则不会。

使用Photoshop之类的工具,剪切一些部分,非常容易变换一个diffuse纹理为specular图片,以增加亮度/对比度的方式,可以把这个部分变换为黑色或白色。

同样要更新片断着色器中的材质:

1
2
3
4
5
6
struct Material
{
    sampler2D diffuse;
    sampler2D specular;
    float shininess;
};

计算specular:

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

你也可以在specular贴图里使用颜色,不单单为每个原始像素设置specular亮度,同时也设置specular高光的颜色。从真实角度来说,specular的颜色基本是由光源自身决定的,所以它不会生成真实的图像(这就是为什么图片通常是黑色和白色的:我们只关心亮度)。


参考 LearnOpenGL-CN 光照贴图

定义材质:

在真实世界里,每个物体会对光产生不同的反应。每个物体对光的反射强度不一样,对镜面高光也有不同的反应。有些物体不会散射(Scatter)很多光却会反射(Reflect)很多光,结果看起来就有一个较小的高光点(Highlight),有些物体散射了很多,它们就会产生一个半径更大的高光。

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

当描述物体的时候,我们可以使用3种光照元素:环境光照(Ambient Lighting)、漫反射光照(Diffuse Lighting)、镜面光照(Specular Lighting)定义一个材质颜色。再加上一个镜面高光亮度,这是我们需要的所有材质属性。在片段着色器中,我们创建一个结构体(Struct),来储存物体的材质属性:

1
2
3
4
5
6
7
8
struct Material
{
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
    float shininess;
};
uniform Material material;

ambient:定义了在环境光照下这个物体反射的是什么颜色,通常这是和物体颜色相同的颜色。 diffuse:定义了在漫反射光照照下物体的颜色。漫反射颜色被设置为(和环境光照一样)我们需要的物体颜色。 specular:设置的是物体受到的镜面光照的影响的颜色(或者可能是反射一个物体特定的镜面高光颜色)。 shininess:影响镜面高光的散射/半径。

这四个元素定义了一个物体的材质,通过它们我们能够模拟很多真实世界的材质。 这里有一个列表devernay.free.fr展示了几种材质属性,这些材质属性模拟外部世界的真实材质。

设置材质:

我们在片段着色器定义了一个uniform的材质结构体,所以我们使用材质的属性来计算光照。

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;
    color = vec4(result, 1.0f);
}

然后在程序中传递uniform的值:

1
2
3
4
glUniform3f(glGetUniformLocation(objectShader.Program, "material.ambient"), 1.0f, 0.5f, 0.31f);
glUniform3f(glGetUniformLocation(objectShader.Program, "material.diffuse"), 1.0f, 0.5f, 0.31f);
glUniform3f(glGetUniformLocation(objectShader.Program, "material.specular"), 0.5f, 0.5f, 0.5f);
glUniform1f(glGetUniformLocation(objectShader.Program, "material.shininess"), 32.0f);

光的属性:

如果你这样设置运行后,会发现物体非常亮。这是因为环境、漫反射和镜面三个颜色任何一个光源都会去全力反射。而之前们是使用了一个强度值来改变了环境光和镜面反射的强度。 我们定义一个光源的强度向量,来减小环境光的影响:

1
vec3 result = vec3(0.1f) * material.ambient;

我们可以用同样的方式影响光源diffuse和specular的强度。

由此,与材质类似,我们同样可以定义光源的属性:

1
2
3
4
5
6
7
8
struct Light
{
    vec3 position;
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};
uniform Light light;

一个光源的ambient、diffuse和specular光都有不同的亮度。 ambient:通常设置为一个比较低的亮度,因为我们不希望环境色太过显眼。 diffuse:通常设置为我们希望光所具有的颜色,经常是一个明亮的白色。 specular:通常被设置为vec3(1.0f)类型的全强度发光。 们同样把光的位置添加到结构体中。

然后更新片段着色器:

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

然后设置uniform的光源属性值:

1
2
3
glUniform3f(glGetUniformLocation(objectShader.Program, "light.ambient"), 0.2f, 0.2f, 0.2f);
glUniform3f(glGetUniformLocation(objectShader.Program, "light.diffuse"), 0.5f, 0.5f, 0.5f);
glUniform3f(glGetUniformLocation(objectShader.Program, "light.specular"), 1.0f, 1.0f, 1.0f);

参考 LearnOpenGL-CN 材质

Phong光照模型

现实世界的光照是非常复杂的,我们目前是没有能力完全模拟的。因此OpenGL的光照仅使用了简化的模型并基于对现实的估计来进行模拟。这些光照模型都是基于我们对光的物理特性的理解。 其中一个模型被称为冯氏光照模型(Phong Lighting Model)。冯氏光照模型的主要结构由3个元素组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。这些光照元素看起来像下面这样:

  • 环境光照(Ambient):即使在黑暗的情况下,世界上也仍然有一些光亮(月亮、一个来自远处的光),所以物体永远不会是完全黑暗的。我们使用环境光照来模拟这种情况,也就是无论如何永远都给物体一些颜色。
  • 漫反射光照(Diffuse):模拟一个发光物对物体的方向性影响(Directional Impact)。它是冯氏光照模型最显著的组成部分。面向光源的一面比其他面会更亮。
  • 镜面光照(Specular):模拟有光泽物体上面出现的亮点。镜面光照的颜色,相比于物体的颜色更倾向于光的颜色。

环境光照(Ambient Lighting)

通常我们周围有很多光源,即使它们有些不是那么明显。光有一个特性是,可以向很多方向发散和反射(Reflect)到其他表面,一个物体的光照可能受到一个非直射光的影响。考虑到这种情况的算法叫做全局照明(Global Illumination)算法,但是这种算法既开销高昂又极其复杂。

我们会使用一种简化的全局照明模型,叫做环境光照。我们使用一个很小的常量光颜色添加进物体片段的最终颜色里,使其就算没有直射光也始终存在着一些发散的光。

1
2
3
4
5
6
7
void main()
{
    float ambientStrength = 0.1f;
    vec3 ambient = ambientStrength * lightColor;
    vec3 result = ambient * objectColor;
    color = vec4(result, 1.0f);
}

漫反射光照(Diffuse Lighting)

环境光本身不提供最明显的光照效果,但是漫反射光照会对物体产生显著的视觉影响。漫反射光使物体上与光线排布越近的片段越能从光源处获得更多的亮度。为了更好的理解漫反射光照,请看下图:

左上方有一个光源,它所发出的光线落在物体的一个片段上。我们需要测量这个光线与它所接触片段之间的角度。如果光线垂直于物体表面,这束光对物体的影响会最大化(更亮)。为了测量光线和片段的角度,我们使用法向量(Normal Vector)。

我们知道两个单位向量的角度越小,它们点乘的结果越倾向于1。当两个向量的角度是90度的时候,点乘会变为0。这同样适用于θ,θ越大,光对片段颜色的影响越小。

点乘返回一个标量,我们可以用它计算光线对片段颜色的影响,基于不同片段所朝向光源的方向的不同,这些片段被照亮的情况也不同。

  • 计算法向量(Normal Vector) 法向量是垂直于顶点表面的向量。由于顶点自身并没有表面,我们利用顶点周围的顶点计算出这个顶点的表面。可以使用叉乘来计算所有的顶点法线,但是由于3D立方体不是一个复杂的形状,所以我们可以简单的把法线数据手动添加到顶点数据中。

  • 计算光线向量 光线向量只需要使用光源位置与顶点位置相减即可。光源位置是固定的。顶点位置在顶点着色器中将顶点坐标与模型坐标相乘转换到世界坐标,再传递到fragment shader使用即可。

  • 计算漫反射光 已经计算出物体法向量norm,光线向量lightDir,则

    float diff = max(dot(norm, lightDir), 0.0); vec3 diffuse = diff * lightColor;

    先计算散射因子diff,因为若两个向量夹角大于90度,点乘会变成负数,负的颜色是没有实际定义的,所以为了避免,使用max来确保大于0。

    最终颜色为

    vec3 result = (ambient + diffuse) * objectColor; color = vec4(result, 1.0f);

  • 正规矩阵: 我们都是在世界空间坐标中进行计算的,所以,法向量也要转换为世界空间坐标,但是这不是简单地把它乘以一个模型矩阵就能搞定的。

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

    其次,如果模型矩阵执行了不等比缩放,法向量就不再垂直于表面了。因此,我们不能用这样的模型矩阵去乘以法向量。下面的图展示了应用了不等比缩放的矩阵对法向量的影响:

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

    修复这个行为的诀窍是使用另一个为法向量专门定制的模型矩阵。这个矩阵称之为正规矩阵(Normal Matrix)

    正规矩阵被定义为“模型矩阵左上角的逆矩阵的转置矩阵”。注意,定义正规矩阵的大多资源就像应用到模型观察矩阵(Model-view Matrix)上的操作一样,但是由于我们只在世界空间工作(而不是在观察空间),我们只使用模型矩阵。

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

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

    注意: 对于着色器来说,逆矩阵是一种开销比较大的操作,因此,在着色器应该尽量避免逆操作,因为它们必须为你场景中的每个顶点进行这样的处理。在绘制之前,最好用CPU计算出正规矩阵,然后通过uniform把值传递给着色器(和模型矩阵一样)。

镜面光照(Specular Lighting)

和环境光照一样,镜面光照同样依据光的方向向量和物体的法向量,但是不同的是它会依据观察方向。镜面光照根据光的反射特性。如果我们想象物体表面像一面镜子一样,那么,无论我们从哪里去看那个表面所反射的光,镜面光照都会达到最大化。你可以从下面的图片看到效果:

我们计算反射向量和视线方向的角度,如果之间的角度越小,那么镜面光的作用就会越大。它的作用效果就是,当我们去看光被物体所反射的那个方向的时候,我们会看到一个高光。

观察向量可以使用观察者世界空间位置(Viewer’s World Space Position)和片段的位置来计算。之后,我们计算镜面光亮度,用它乘以光的颜色,在用它加上作为之前计算的光照颜色。 也可以在观察空间(View Space)进行光照计算,这样观察者的坐标永远是(0, 0, 0)。

先定义一个高光强度:

1
float specularStrength = 0.5f;

然后计算视线方向和沿法线轴的反射向量:

1
2
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);

reflect函数要求的第一个是从光源指向片段位置的向量,第二个参数要求是一个法向量。

计算镜面亮度:

1
2
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;

我们先计算视线方向与反射方向的点乘(确保它不是负值),然后得到它的32次幂。这个32是高光的发光值(Shininess)。一个物体的发光值越高,反射光的能力越强,散射得越少,高光点越小。在下面的图片里,你会看到不同发光值对视觉(效果)的影响:

最后一件事情是把它添加到环境光颜色和散射光颜色里,然后再乘以物体颜色:

1
2
vec3 result = (ambient + diffuse + specular) * objectColor;
color = vec4(result, 1.0f);

Gouraud着色

早期的光照着色器,开发者在顶点着色器中实现冯氏光照。这样的优势是,相比片段来说,顶点要少得多,因此会更高效。然而,顶点着色器中的颜色值是只是顶点的颜色值,片段的颜色值是它与周围的颜色值的插值。结果就是这种光照看起来不会非常真实,除非使用了大量顶点。 在顶点着色器中实现的冯氏光照模型叫做Gouraud着色,而不是冯氏着色。由于插值,这种光照连起来有点逊色。冯氏着色能产生更平滑的光照效果。


参考: LearnOpenGL-CN 光照基础