用于多个光源的 WebGL 片段着色器?
WebGL fragment-shader for multiple light sources?
我希望能够将多个光源附加到我的场景图的每个节点,但我不知道该怎么做!
从 learningwebgl.com 上的教程中,我学会了使用定向照明或位置照明,但我找不到关于如何实现多个光源的很好的解释。
因此,objective 应该是,可以选择向每个节点附加任意数量的光源,其类型可以是定向照明或位置照明,如果可能和可取,这应该通过仅使用一个着色器程序来实现(如果这不是唯一的可能性的话),因为我会根据每个节点的特定需求自动为每个节点创建程序(除非堆栈上已经有一个程序具有相等的设置)。
根据 learningwebgl.com 上的教程,我的节点对象的片段着色器源代码使用没有预设绑定到其中一种照明类型的照明可能如下所示...
precision highp float;
uniform bool uUsePositionLighting;
uniform bool uUseDirectionalLighting;
uniform vec3 uLightPosition;
uniform vec3 uLightDirection;
uniform vec3 uAmbientColor;
uniform vec3 uDirectionalColor;
uniform float uAlpha;
varying vec4 vPosition;
varying vec3 vTransformedNormal;
varying vec3 vColor;
void main (void) {
float directionalLightWeighting;
if (uUseDirectionalLighting) {
directionalLightWeighting = max(dot(vTransformedNormal, uLightDirection), 0.0);
else if (uUsePositionLighting) {
vec3 lightDirection = normalize(uLightPosition, vPosition.xyz);
directionalLightWeighting = max(dot(normalize(vTransformedNormal, lightDirection), 0.0);
}
vec3 lightWeighting = uAmbientColor + uDirectionalColor * directionalLightWeighting;
gl_FragColor = vec4(vColor * lightWeighting, uAlpha);
}
...所以,这基本上是我对这个主题的了解不足。
我也问自己,增加更多的光源会如何影响灯光颜色:
我的意思是,uAmbientColor
和 uDirectionalColor
有 总和 1.0
吗?在这种情况下(特别是当使用多个光源时),在将这些值传递给着色器之前预先计算它们肯定会很好,不是吗?
将你的灯光放入一个数组中,并为每个片段循环遍历它们。从固定光源阵列开始,直到 OpenGL 4.3 才支持无限阵列,并且使用起来更复杂。
大致如下:
uniform vec3 uLightPosition[16];
uniform vec3 uLightColor[16];
uniform vec3 uLightDirection[16];
uniform bool uLightIsDirectional[16];
....
void main(void) {
vec3 reflectedLightColor;
// Calculate incoming light for all light sources
for(int i = 0; i < 16; i++) {
vec3 lightDirection = normalize(uLightPosition[i], vPosition.xyz);
if (lightIsDirectional[i]) {
reflectedLightColor += max(dot(vTransformedNormal, uLightDirection[i]), 0.0) * uLightColor[i];
}
else {
reflectedLightColor += max(dot(normalize(vTransformedNormal, lightDirection), 0.0) * uLightColor[i];
}
}
glFragColor = vec4(uAmbientColor + reflectedLightColor * vColor, uAlpha);
}
然后您可以通过将 uLightColor 设置为 (0,0,0) 为您不使用的条目 enable/disable 光源。
Ambient 和 directional 不必总和为 1,实际上光源的强度可以比 1.0 强得多,但随后您需要进行色调映射以回到可以调整的值范围显示在屏幕上,我建议四处游玩以感受正在发生的事情(例如,当光源具有负色或高于 1.0 的颜色时会发生什么?)。
uAmbientColor
只是模拟在场景中多次反弹的光的一种(糟糕的)方式。不然影子里的东西就全黑了,不真实
反射率通常应介于 0 和 1 之间(在此示例中,它是 'max' 计算返回的部分),否则通过 material 观察时光源会变得更强。
@ErikMan 的回答很好,但可能涉及 GPU 方面的大量额外工作,因为您要检查每个片段的每个光,这并不是绝对必要的。
我建议构建 clip-space 四叉树而不是数组。 (如果您的目标平台/GL 版本支持,您可以在计算着色器中执行此操作。)
节点可能具有如下结构(伪代码,因为我的 JS 生锈了):
typedef struct
{
uint LightMask; /// bitmask - each light has a bit indicating whether it is active for this node. uint will allow for 32 lights.
bool IsLeaf;
} Node;
const uint maxLevels = 4;
const uint maxLeafCount = pow(4,maxLevels);
const uint maxNodeCount = (4 * maLeafCount - 1) / 3;
/// linear quadtree - node offset = 4 * parentIndex + childIndex;
Node tree[maxNodeCount];
构建树时,只需根据隐式节点边界检查每个灯的 clip-space 边界框。 (根从 (-1,-1) 到 (+1,+1)。每个 child 在每个维度上都是该大小的一半。因此,您实际上不需要存储节点边界。)
如果光线接触到节点,则在Node.LightMask中设置一个对应于光线的位。如果光完全包含该节点,则停止递归。如果它与节点相交,则细分并继续。
在您的片段着色器中,找到哪个叶节点包含您的片段,并应用其位在叶节点的遮罩中设置的所有灯光。
如果您希望树密集,也可以将树存储在 mipmap 金字塔中。
将瓷砖的尺寸保持为 32 的倍数,最好是正方形。
vec2 minNodeSize = vec2(2.f / 32);
现在,如果您的灯数量很少,这可能有点过分了。您可能必须有很多灯才能看到任何真正的性能优势。此外,正常循环可能有助于减少着色器中的数据分歧,并更容易消除分支。
这是实现简单平铺渲染器的一种方法,打开了拥有数百盏灯的大门。
我希望能够将多个光源附加到我的场景图的每个节点,但我不知道该怎么做!
从 learningwebgl.com 上的教程中,我学会了使用定向照明或位置照明,但我找不到关于如何实现多个光源的很好的解释。
因此,objective 应该是,可以选择向每个节点附加任意数量的光源,其类型可以是定向照明或位置照明,如果可能和可取,这应该通过仅使用一个着色器程序来实现(如果这不是唯一的可能性的话),因为我会根据每个节点的特定需求自动为每个节点创建程序(除非堆栈上已经有一个程序具有相等的设置)。
根据 learningwebgl.com 上的教程,我的节点对象的片段着色器源代码使用没有预设绑定到其中一种照明类型的照明可能如下所示...
precision highp float;
uniform bool uUsePositionLighting;
uniform bool uUseDirectionalLighting;
uniform vec3 uLightPosition;
uniform vec3 uLightDirection;
uniform vec3 uAmbientColor;
uniform vec3 uDirectionalColor;
uniform float uAlpha;
varying vec4 vPosition;
varying vec3 vTransformedNormal;
varying vec3 vColor;
void main (void) {
float directionalLightWeighting;
if (uUseDirectionalLighting) {
directionalLightWeighting = max(dot(vTransformedNormal, uLightDirection), 0.0);
else if (uUsePositionLighting) {
vec3 lightDirection = normalize(uLightPosition, vPosition.xyz);
directionalLightWeighting = max(dot(normalize(vTransformedNormal, lightDirection), 0.0);
}
vec3 lightWeighting = uAmbientColor + uDirectionalColor * directionalLightWeighting;
gl_FragColor = vec4(vColor * lightWeighting, uAlpha);
}
...所以,这基本上是我对这个主题的了解不足。
我也问自己,增加更多的光源会如何影响灯光颜色:
我的意思是,uAmbientColor
和 uDirectionalColor
有 总和 1.0
吗?在这种情况下(特别是当使用多个光源时),在将这些值传递给着色器之前预先计算它们肯定会很好,不是吗?
将你的灯光放入一个数组中,并为每个片段循环遍历它们。从固定光源阵列开始,直到 OpenGL 4.3 才支持无限阵列,并且使用起来更复杂。
大致如下:
uniform vec3 uLightPosition[16];
uniform vec3 uLightColor[16];
uniform vec3 uLightDirection[16];
uniform bool uLightIsDirectional[16];
....
void main(void) {
vec3 reflectedLightColor;
// Calculate incoming light for all light sources
for(int i = 0; i < 16; i++) {
vec3 lightDirection = normalize(uLightPosition[i], vPosition.xyz);
if (lightIsDirectional[i]) {
reflectedLightColor += max(dot(vTransformedNormal, uLightDirection[i]), 0.0) * uLightColor[i];
}
else {
reflectedLightColor += max(dot(normalize(vTransformedNormal, lightDirection), 0.0) * uLightColor[i];
}
}
glFragColor = vec4(uAmbientColor + reflectedLightColor * vColor, uAlpha);
}
然后您可以通过将 uLightColor 设置为 (0,0,0) 为您不使用的条目 enable/disable 光源。
Ambient 和 directional 不必总和为 1,实际上光源的强度可以比 1.0 强得多,但随后您需要进行色调映射以回到可以调整的值范围显示在屏幕上,我建议四处游玩以感受正在发生的事情(例如,当光源具有负色或高于 1.0 的颜色时会发生什么?)。
uAmbientColor
只是模拟在场景中多次反弹的光的一种(糟糕的)方式。不然影子里的东西就全黑了,不真实
反射率通常应介于 0 和 1 之间(在此示例中,它是 'max' 计算返回的部分),否则通过 material 观察时光源会变得更强。
@ErikMan 的回答很好,但可能涉及 GPU 方面的大量额外工作,因为您要检查每个片段的每个光,这并不是绝对必要的。
我建议构建 clip-space 四叉树而不是数组。 (如果您的目标平台/GL 版本支持,您可以在计算着色器中执行此操作。)
节点可能具有如下结构(伪代码,因为我的 JS 生锈了):
typedef struct
{
uint LightMask; /// bitmask - each light has a bit indicating whether it is active for this node. uint will allow for 32 lights.
bool IsLeaf;
} Node;
const uint maxLevels = 4;
const uint maxLeafCount = pow(4,maxLevels);
const uint maxNodeCount = (4 * maLeafCount - 1) / 3;
/// linear quadtree - node offset = 4 * parentIndex + childIndex;
Node tree[maxNodeCount];
构建树时,只需根据隐式节点边界检查每个灯的 clip-space 边界框。 (根从 (-1,-1) 到 (+1,+1)。每个 child 在每个维度上都是该大小的一半。因此,您实际上不需要存储节点边界。)
如果光线接触到节点,则在Node.LightMask中设置一个对应于光线的位。如果光完全包含该节点,则停止递归。如果它与节点相交,则细分并继续。
在您的片段着色器中,找到哪个叶节点包含您的片段,并应用其位在叶节点的遮罩中设置的所有灯光。
如果您希望树密集,也可以将树存储在 mipmap 金字塔中。
将瓷砖的尺寸保持为 32 的倍数,最好是正方形。
vec2 minNodeSize = vec2(2.f / 32);
现在,如果您的灯数量很少,这可能有点过分了。您可能必须有很多灯才能看到任何真正的性能优势。此外,正常循环可能有助于减少着色器中的数据分歧,并更容易消除分支。
这是实现简单平铺渲染器的一种方法,打开了拥有数百盏灯的大门。