同时渲染多种光源
撰写时间: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中将被展平,因此在客户端必须使用结构体名称.属性名称
的方式来调用gl的getUniformLocation方法才能正确地取出结构体的各属性在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的作用正在于此。尽管从函数名称上看不出来,vec3的scaleAndAdd可以在任意斜率的线段上特别方便地算出特定偏移值。它就像一把尽管名字只写了金属折叠工具
的刀,但在我们手里,它就是一把可对任意斜率的空间向量进行伸缩的瑞士军刀。
最后,加入聚光。
同样,基于上面的分析,这里的extendLen很重要,它可以调节聚光的衰减范围。其值越大,地板位置的衰减值就越小,聚光的效果就越明显。
再次运行应用。在3种不同类型的光照下,整个场景光彩斑斓,鲜艳夺目。
这就是3D编程的乐趣及威力所在,我们不再通过渐变色等手段来手动模拟光效,那很假。相反,我们通过基于物理原理的数学计算,真实地还原出大自然该有的绚丽。并且,修改相应参数,光照效果立即截然不同。
重构片断着色器代码
上面片断着色器的calcDirectLightings函数中,主要存在两个问题。一是部分变量多次重复计算,如N, V, F0,造成资源浪费、效率降低,这些变量应移至循环之外。二是各种光源在计算diffuse及specular时,存在大量的重复代码,这违背了《程序员修炼之道》一书所提到的DRY原则,即Don't Repeat Yourself,应将这些重复的代码提炼为函数。
由此,片断着色器的完整代码大幅重构如下:
为减轻GPU负担,normedDirection及normedConeDirection在客户端须先行归一化再传入着色器,故将它们如此命名后,在客户端既能清晰地看到并遵循要求,而在着色器中则可以放心地直接使用而无需重复归一化。
重构后,代码逻辑清晰,维护方便。且应用性能也有较大幅度的提升,尤其在光源数量较多的情况下更是如此。
运行应用。
运行应用后,在Safari中每刷新(重新加载页面)一次,都会出现一个极为短暂的闪白。但在Chrome中,每次刷新,均系零延迟且没有任何闪白,即可看到不同的光照颜色组合效果,用户体验非常好。
