同时渲染多种光源
撰写时间:2026-07-26
修订时间:2026-07-27
概述
多光源的光效,环境光在场景中单独设置,不需累加;而漫反射光与高光则需要将各个光源的光效逐一相加。代码基本结构为:
各种光源,不管其具体类型,均需要下面的属性:
| 类型 | 属性名称 | 含义 |
|---|---|---|
| 光源自身属性 | 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来分别替换原来的属性名。
客户端代码:
运行应用。
