WebGL实战篇(十)—— 光照Ⅱ——点光源与聚光灯

lxf2023-07-06 11:40:01

本文正在参加「」

前言

在上一篇文章WebGL实战篇(九)—— 光照Ⅰ - 编程 ()中,我们介绍了在平行光中实现的 Phong 基本光照模型。

之前介绍的平行光作为照亮整个场景的全局光源来说是非常棒的选择,但是除了平行光之外我们也需要一些分散在场景中的其他光源,想想一下在洞穴中墙壁上的火把或者是城市夜晚中的路灯,都是很典型的点光源。除了点光源之外,还有一些其他的光源类型。今天我们为大家再介绍两种常见的光照模型:“点光源”与“聚光灯”。

点光源

点光源,顾名思义,就是用一个点来表示该光源所处的位置。点光源不同于平行光在场景中任意位置的强度都相同,我们认为点光源的辐射范围是有限的。当物体不在点光源的辐射范围内时,则该物体不被该光源所影响。如果物体处于点光源的辐射范围内,我们还需要考虑点光源在其范围内的衰减情况。换句话说,我们希望离光源越近的物体被照的约亮,离光源越远的物体则偏暗一些。

衰减

随着光线传播距离的增长逐渐削减光的强度通常叫做衰减(Attenuation)。我们可以通过很多方式来表示这样的一种情况,比如使用线性方程等,不过使用线性方程来解决这一问题时会让其看起来非常的“假”。我们通常采用非线性的变化来表示光照的衰减。幸运的是已经有人帮我们解决了这个问题,我们通常采用下面这个公式来计算光源的衰减:

Atten=1.0Kc+Kld+Kqd2Atten = \frac{1.0}{K_c + K_l d + K_q d^2}

其中:

  • KcK_c 表示常数项,通常为 1
  • KlK_l 表示一次项,它与距离相乘,以线性的方式减少强度
  • KqK_q 表示二次项,它与距离的二次方相乘,让光源以二次方递减的速度减少强度。当光源与被照射物之间的距离比较小时,一次项的影响比较大,但是当距离比较大的时候,则是二次项的影响更大了。下图显示了在 13 距离内的衰减效果。

WebGL实战篇(十)—— 光照Ⅱ——点光源与聚光灯

但是,对于一个某个辐射范围的点光源的这三个系数该如何确定呢?比如说:我创建了一个辐射范围为 100 的点光源,那么这个点光源所对应的 Kc,Kl,KqK_c, K_l, K_q 都分别是多少呢?

Ogre 这个开源的 3D 引擎中,提供了这样的参数参照:

距离常数项一次项二次项
71.00.71.8
131.00.350.44
201.00.220.20
321.00.140.07
501.00.090.032
651.00.070.017
1001.00.0450.0075
1601.00.0270.0028
2001.00.0220.0019
3251.00.0140.0007
6001.00.0070.0002
32501.00.00140.000007

点光源实现

现在我们将之前的平行光的光照改为点光源的光照。我们不需要改变顶点着色器中的内容,我们仅仅只需要改变片元着色器中的内容。

首先,对于物体表面的每个点来说,光源的方向会变得不一样,如下图所示。我们需要根据光源的位置和物体表面的位置来计算光照方向。另外,我们还需要计算光源到物体表面的距离。最后根据Kc,Kl,KqK_c, K_l, K_q的值来计算衰减度。

WebGL实战篇(十)—— 光照Ⅱ——点光源与聚光灯

uniform vec3 u_coefficient; //[!code ++]
// ......
float kc = u*coefficient[0]; //[!code ++]
float kl = u_coefficient[1]; //[!code ++]
float kq = u_coefficient[2]; //[!code ++]
// ......
vec3 lightDir = normalize(u_lightDir); //[!code --]
vec3 lightDir = normalize(u_lightPos - v_worldPos); //[!code ++]
float dis = distance(u_lightPos, v_worldPos); //[!code ++]
float atten = 1.0 / (kc + kl * dis + kq _ dis _ dis); //[!code ++]
vec3 color = ambient + (diffuse + specular) * atten;

最终,点光源实现的 Phong 光照模型如下:

聚光灯

接下来我们要讨论的光源类型是聚光灯。聚光灯与点光源类似,它同样是位于某个位置的光源,但是它与点光源不同的是:聚光灯不会向空间的所有方向发射光线,聚光灯只会向空间中的特定方向发射光线。也就是说,只有特定区域的物体才会被聚光灯所照亮,你可以想象一下舞台上的照射在演员身上的追光灯或者是黑夜中的手电筒,它们都是典型的聚光灯。

我们要描述聚光灯,我们不仅仅需要聚光灯的位置,还需要聚光灯的照射方向,还需要聚光灯的“聚光半径”。这里我说“聚光半径”可能有一点歧义。其实我想表达的是被照射的物体表面与聚光灯形成的连线与聚光灯照射方向形成的夹角θ\thetaθ。如下图所示:

其中:

  • lightDir: 表示物体表面到光源的方向
  • spotLightDir: 表示聚光灯的照射方向

θ\theta表示 lightDir 与 spotLightDir 之间的夹角。如果这个夹角大于某个值,则不被照亮,这正是聚光灯的特性。所以我们需要引入一个变量 cutoff表示聚光灯能照亮物体的最大范围,一般我们用夹角的余弦值表示。因为我们在 GLSL 中可以通过点乘的方式很方便的计算余弦值。

我们可以写入下面的代码

float LdotS = dot(-lightDir, normalize(u_spotDir));
float m = 1.0;
if (LdotS < cutoff) {
    m = 0.1;
}
vec3 color = ambient + (diffuse + specular) * atten * m;

结果如下:

WebGL实战篇(十)—— 光照Ⅱ——点光源与聚光灯

不过,我们使用 if条件判断来决定两部分的光照强度的话会产生一个硬边。我们更希望的是在有光和无光的部分能够产生一个比较平滑的过渡,所以,我们单单靠一个夹角来判断聚光灯照亮范围是不够的,我们还需要引入另一个角度 ϕ\phi。如下图所示:

WebGL实战篇(十)—— 光照Ⅱ——点光源与聚光灯

我们期望夹角小于ϕ\phi时,物体的表面被聚光灯完全照亮,夹角处于 ϕ\phiθ\theta 之间时,则有一段从亮到暗的过渡,夹角大于 θ\theta 时,则不被聚光灯照亮。在 GLSL 中正好有一个函数对应这种情况,就是 smoothstep(min, max, x)。其表示,如果 x < min 时,则返回 0,如果 x > max,则返回 1。如果处于其中,则在 0~1 之间进行插值处理。我们可以写出下面的代码:

float m = smoothstep(u_cutoff[0], u_cutoff[1], LdotS) * 0.8 + 0.2;
vec3 color = ambient + (diffuse + specular) * atten * m;

上面,我们进行了 *0.8 + 0.2 的运算,是因为我们想让聚光灯外的亮度没有那么的黑,所以将 0~1 的返回值范围调整为了 0.2~1 之间。

总结

本文介绍了点光源和聚光灯这两种常见的光源形式,再加上之前我们使用的平行光,我们已经学习了三种光源了。恰当的使用光源可以给场景带来不错的氛围效果。

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!