Web编程技术营地
研究、演示、创新

同时渲染多种光源

撰写时间:2026-07-26

修订时间:2026-07-29

概述

多光源的光效,环境光在场景中单独设置,不需累加;而漫反射光高光则需要将各个光源的光效逐一相加。代码基本结构为:

各种光源,不管其具体类型,均需要下面的属性:

类型属性名称含义
光源自身属性uLightColor光源颜色
uLightIntensity控制光强的因子
计算光效辅助属性uCameraPos相机位置,用于计算观察方向向量
uRoughness材质粗糙度
uMetallic材质金属度

方向光需要以下属性:

属性名称含义备注
uLightDir光源的方向向量可在客户端先计算出并归一化后再传入着色器
客户端可自行决定是否需在光源位置渲染该光源的网格物体

点光需要以下属性:

属性名称含义额外功效
uLightPos光源的位置,可进一步用于计算出逐顶点的从表面指向光源的方向向量可在该位置渲染出代表该光源的网格物体
uLightAttenuation光强的衰减因子

聚光需要以下属性:

属性名称含义额外功效
uLightPos光源的位置,可进一步用于计算出逐顶点的从表面指向光源的方向向量可在该位置渲染出代表该光源的网格物体
uLightAttenuation光强的衰减因子
uSpotConeDir聚光光源指向照射目标的方向向量
uInnerCutoff聚光内锥角余弦值
uOuterCutoff聚光外锥角余弦值

当应用只有一种光源时,可在着色器中直接设置相应的属性名称。但当同时使用多个光源时,着色器中将出现各种各样的属性名,而有些属性名并不属于特定的光源(例如,在方向光中,若为其设置uLightAttenuation属性将造成混乱)。因此,在着色器中如何为各种光源有效地管理它们的属性名称,是多光源应用首先应考虑的问题。此问题可通过在着色器中使用struct结构体来解决。

其次,在着色器中如何使用类型为struct的数组,也是一个需考虑多方因素的问题。

下面各节,将一步步来实现这些内容。

使用结构体的方向光

首先,在片断着色器中声明struct

对以上代码,需注意:

第一,在WebGL 2.0环境下,GLSL ES不支持enum关键字。因此可通过常量声明来实现其功能。

第二,GLSL不提供任何形式的运行时类型信息(RTTI),也没有typeof, typeid反射机制。因此,需在LightProps中添加type属性,以明确光源类型。

第三,虽然我们可为方向光点光聚光分别声明3种结构体,但标准工业做法并非如此,而是声明一个包含了type属性的通用结构体,并将所有光源属性都收拢在一起。

其次,修改calcDirectLightings函数为:

在判断光源类型后,使用uLight.direction, uLight.color, uLight.intensity来分别替换原来的属性名。这样,此分支将完全专注于只提取与方向光有关的属性值来参与计算。

最后,在客户端中:

需注意的是,在使用结构体后,结构体成员在GLSL中将被展平,因此在客户端必须使用结构体名称.属性名称的方式来调用glgetUniformLocation方法才能正确地取出结构体的各属性在program中的位置。

调试技巧:在着色器中的uniform变量,只有在着色器的代码中实际使用了其值,才能有效地返回其在program中的位置。一般情况下没有什么问题。但如果发现返回的位置为null,第一时间应检查着色器代码,该变量除了被声明外,是否还被实际使用了。

运行应用

扩展结构体以支持点光

片断着色器中将结构体LightProps代码扩展为:

修改calcDirectLightings函数为:

使用uLight.position, uLight.attenuation, uLight.color, uLight.intensity来分别替换原来的属性名。

客户端代码:

运行应用

扩展结构体以支持聚光

片断着色器中将结构体LightProps代码扩展为:

修改calcDirectLightings函数为:

使用uLight.position, uLight.attenuation, uLight.color, uLight.intensity, uLight.coneDirection, uLight.innerCutoff, uLight.outerCutoff来分别替换原来的属性名。

客户端代码:

运行应用

同时渲染多光源

上面各节的代码,均仅各自实现了一种光源,本节中,我们让应用程序能同时渲染多种光源。

简单原始的整合

在片断着色器中声明一个用于存储各种光源属性的数组。

GLSL不支持动态数组,因此需在编译阶段事先声明数组的大小。上面先通过宏MAX_LIGHTS,将最大光源数量限制为10,然后将其指定为数组uLights的数组长度。uLightCount为实际使用的光源数量。

修改函数calcDirectLightings内容为:

上面代码未作任何优化或重构,先实现最原始的整合。

MAX_LIGHTS的值进行遍历,但仅在索引值小于uLightCount时才计算光照。每种光照的效果值均累加进totalLight中,最后返回该值。

运行应用,先了解场景中渲染了哪些内容,以便更好地把握下面的代码作用。

客户端initLightings函数代码如下:

先构建一个全局的环境光,再指定使用3盏灯光。

先加入方向光

在客户端向着色器的数组的属性赋值时,使用array[index].propName的形式。

对于每一个灯光,我们均将其渲染为一个方向向量,以便更好地看到光照效果。

加入点光

点光为红色。点光本应向四面八方照射,但因其存在衰减的性质,我们只取其中一条光束,让其穿透圆球,这样可方便地看出光线在该方向上的衰减范围。

上面代码中,当光束从(2, 5, -2)照射至(1, 0, 1)时,就已经衰减至接近0值,由此地板上将看不到相应的光照效果。因此我们让衰减因子在光束方向上再延长extendLen的距离,让其完全穿透地板,以在地板上产生清晰的光照效果。

现在,地板的光照效果是出来了,但要渲染这条经延长后的方向向量,我们需取出延长后的射线终点位置。难点在于该射线的斜率是任意的。函数extendLenAlongVector的作用正在于此。尽管从函数名称上看不出来,vec3scaleAndAdd可以在任意斜率的线段上特别方便地算出特定偏移值。它就像一把尽管名字只写了金属折叠工具的刀,但在我们手里,它就是一把可对任意斜率的空间向量进行伸缩的瑞士军刀。

最后,加入聚光

同样,基于上面的分析,这里的extendLen很重要,它可以调节聚光的衰减范围。其值越大,地板位置的衰减值就越小,聚光的效果就越明显。

再次运行应用。在3种不同类型的光照下,整个场景光彩斑斓,鲜艳夺目。

这就是3D编程的乐趣及威力所在,我们不再通过渐变色等手段来手动模拟光效,那很假。相反,我们通过基于物理原理的数学计算,真实地还原出大自然该有的绚丽。并且,修改相应参数,光照效果立即截然不同。

重构片断着色器代码

上面片断着色器calcDirectLightings函数中,主要存在两个问题。一是部分变量多次重复计算,如N, V, F0,造成资源浪费、效率降低,这些变量应移至循环之外。二是各种光源在计算diffusespecular时,存在大量的重复代码,这违背了《程序员修炼之道》一书所提到的DRY原则,即Don't Repeat Yourself,应将这些重复的代码提炼为函数。

由此,片断着色器的完整代码大幅重构如下:

为减轻GPU负担,normedDirectionnormedConeDirection在客户端须先行归一化再传入着色器,故将它们如此命名后,在客户端既能清晰地看到并遵循要求,而在着色器中则可以放心地直接使用而无需重复归一化。

重构后,代码逻辑清晰,维护方便。且应用性能也有较大幅度的提升,尤其在光源数量较多的情况下更是如此。

运行应用

运行应用后,在Safari中每刷新(重新加载页面)一次,都会出现一个极为短暂的闪白。但在Chrome中,每次刷新,均系零延迟且没有任何闪白,即可看到不同的光照颜色组合效果,用户体验非常好。

参考资源

Specifications

  1. WebGL 1.0 Specification
  2. WebGL 2.0 Specification
  3. OpenGL ES 3.0 Reference Pages

JS Libraries

  1. glMatrix.net

Scientific Papers

  1. Sampling the GGX Distribution of Visible Normals
  2. HAL: Open Science