在WebGL和ThreeJS中改善区域照明

59

我一直在使用WebGL实现一个区域照明,类似于这个演示:

http://threejs.org/examples/webgldeferred_arealights.html

上述的 three.js 实现是从 gamedev.net 上的 ArKano22 的工作进行移植的:

http://www.gamedev.net/topic/552315-glsl-area-light-implementation/

尽管这些解决方案非常令人印象深刻,但它们都有一些限制。ArKano22 原始实现的主要问题是漫反射项的计算没有考虑表面法线。

我已经花费几周时间来改进这个解决方案,并与 redPlant 合作解决了这个问题。目前,我已经将法线计算合并到该解决方案中,但结果也有缺陷。

这是我的当前实现的预览:

area lighting teaser

介绍

计算每个片段的漫反射项的步骤如下:

  1. 将顶点投影到区域光所在的平面上,使得投影向量与光的法线/方向重合。
  2. 通过比较投影向量和光的法线来检查顶点是否在区域光平面的正确侧面。
  3. 计算此投影点在平面上相对于光中心/位置的 2D 偏移量。
  4. 将这个 2D 偏移向量夹紧在光的区域内(由其宽度和高度定义)。
  5. 推导出被投影且夹紧的 2D 点的三维世界位置。这是离顶点最近的区域光点。
  6. 通过取顶点到最近点向量(归一化)和顶点法线的点积来执行通常针对点光源的漫反射计算。

问题

这种解决方案的问题在于,光照计算是从最近点开始进行的,并没有考虑到灯光表面上可能会更加照亮片段的其他点。让我来尝试解释一下为什么...

考虑下面的图示:

存在问题的区域照明情况

区域光源既垂直于表面又与其相交。表面上的每个片段始终返回光源上的最近点,即表面和光线相交的点。由于表面法向量和顶点到光线向量始终垂直,它们之间的点积为零。因此,尽管有大片的光线悬挂在表面上,漫反射贡献的计算却为零。

潜在解决方案

我建议我们不要从区域光源的最近点计算光线,而要从使顶点到光线向量(标准化)与顶点法向量点积最大的区域光源上的一个点进行计算。在上面的图中,这将是紫色点,而不是蓝色点。

帮帮我!

因此,这就是我需要您的帮助的地方。在我的脑海中,我对如何推导出这个点有一个很好的想法,但没有数学能力得到解决方案。

目前,在我的片段着色器中可用以下信息:

  • 顶点位置
  • 顶点法向量(单位向量)
  • 光源位置、宽度和高度
  • 光源法向量(单位向量)
  • 光源右侧向量(单位向量)
  • 光源上方向量(单位向量)
  • 从顶点投影到光线平面上的点(3D)
  • 从光源中心偏移的投影点(2D)
  • 夹紧偏移量(2D)
  • 这个夹紧偏移量的世界位置 - 最近点(3D)
为了将所有这些信息可视化,我创建了这个图表(希望它有所帮助):

available lighting information

为了测试我的建议,我需要区域光源上的投射点 —— 由红点表示,以便我可以在顶点法线与顶点到投射点(已归一化)之间执行点积。同样,这应该产生最大可能的贡献值。

更新!!!

我在CodePen上创建了一个交互式草图,用于可视化我目前实现的数学方法:

http://codepen.io/wagerfield/pen/ywqCp



codepen

你应该聚焦于第318行的相关代码。castingPoint.locationTHREE.Vector3的实例,也是谜题中缺少的部分。你还应该注意到草图左下角有2个值——这些值是动态更新的,以显示相关向量之间的点积。

我想解决方案可能需要另一个伪平面,与顶点法线方向对齐并且垂直于光源平面,但我可能错了!

抱歉,我想说ArKano22的原始实现没有考虑表面法线。我已经更新了问题以反映这一点。与three.js实现将2个余弦项相乘的方式类似,我也是这样做的,但引入了一个衰减因子,它会偏置最近点到顶点向量和光线法线之间的点积。这给出了我的预览中显示的光照区域,但牺牲了法线计算的包含。 - wagerfield
@WestLangley Paul Lewis早些时候在Twitter上建议了同样的迭代方法,这绝对是我明天想尝试的事情。我的大脑因为尝试解决这个问题而有点疲惫,但我仍然相信,在已经可用的信息量下,存在一个确切的解决方案。 - wagerfield
寻找投射点的伪代码是否可接受?我需要了解一些信息以便解决它。在链接的代码中,请提及1)表面法向量2)光平面边界(四条线段)和3)光线法向量。 - user568109
伪代码可以,只要它能够让人理解。最好能够提供一些实际数值的详细示例,这样我就能够知道如何将它们插入其中了。我不是数学家,也不太明白如何阅读公式,因此如果可能的话,我希望得到一个通俗易懂的答案。我会在CodePen草图中添加一些注释来标记上述组件在更新方法中的位置——希望有所帮助。如果你需要任何其他东西,请告诉我。再次感谢! - wagerfield
@user568109 我已经在CodePen示例中更新了一些注释,位于第321行下方。您将看到在vertex对象和light对象上可用的各种属性。需要设置的向量是castingPoint.location - 这在第332行。 - wagerfield
显示剩余2条评论
5个回答

41
注意:three.js现在支持THREE.RectAreaLight。本回答涉及three.js的旧版本。

你使用最大化点积的方法是基本有缺陷的,而且在物理上不可行。

在你上面的第一个插图中,假设你的区域光只包含左半部分。

"紫色" 点--即左半部分最大化点积的点--与同时组合了两个半部分最大化点积的点相同。

因此,如果一个人使用你提出的方案,那么他将得出这样的结论:区域光的左半部分发射的辐射和整个光源一样。显然,这是不可能的。

计算区域光对给定点投射的总光量的解决方案相当复杂,但你可以在1994年的文章《部分遮挡多面体光源的辐照度雅各布》 这里 找到一个解释。

我建议你看一下图1和一些1.2节的段落——然后停下来。:-)

为了简单起见,我编写了一个非常简单的着色器,使用three.js WebGLRenderer-而不是延迟的。

编辑:已删除过时的 fiddle

enter image description here

片段着色器的核心非常简单

// direction vectors from point to area light corners

for( int i = 0; i < NVERTS; i ++ ) {

    lPosition[ i ] = viewMatrix * lightMatrixWorld * vec4( lightverts[ i ], 1.0 ); // in camera space

    lVector[ i ] = normalize( lPosition[ i ].xyz + vViewPosition.xyz ); // dir from vertex to areaLight

}

// vector irradiance at point

vec3 lightVec = vec3( 0.0 );

for( int i = 0; i < NVERTS; i ++ ) {

    vec3 v0 = lVector[ i ];
    vec3 v1 = lVector[ int( mod( float( i + 1 ), float( NVERTS ) ) ) ]; // ugh...

    lightVec += acos( dot( v0, v1 ) ) * normalize( cross( v0, v1 ) );

}

// irradiance factor at point

float factor = max( dot( lightVec, normal ), 0.0 ) / ( 2.0 * 3.14159265 );

更多好消息:

  1. 这种方法在物理上是正确的。
  2. 衰减会自动处理。(请注意,较小的光源将需要更大的强度值。)
  3. 理论上,这种方法应该适用于任意多边形,而不仅仅是矩形。

注意事项:

  1. 我只实现了漫反射部分,因为这是你问题所涉及的部分。
  2. 您将需要使用合理的启发式方法来实现镜面反射部分 - 类似于您已经编写的代码。
  3. 这个简单的例子没有处理区域光“部分在地平线以下”的情况 - 即不是所有4个顶点都在面的平面上方。
  4. 由于 WebGLRenderer 不支持区域光,您不能“将光源添加到场景中”并期望它工作。这就是为什么我将所有必要的数据传递到自定义着色器中的原因。(当然,WebGLDeferredRenderer 支持区域光。)
  5. 不支持阴影。

three.js r.73


这太棒了,非常感谢!为了你的努力、参考和示例,我将接受这个作为问题的答案,给你赚得的 500 分,但是希望你能帮助我进一步完善解决方案,以省略与表面法线产生负点积的光顶点。当光线与表面相交时会发生这种情况。当光线半沉没在表面下时,它们的贡献会互相抵消:http://jsfiddle.net/Us54P/1/ - wagerfield
你不能简单地“省略顶点”。你必须构建一个新的多边形,该多边形定义为包含在表面“地平线上方”的半空间中的区域光的一部分。(谷歌剪辑多边形平面)然后使用新的多边形执行上述计算。 - WestLangley
2
@WestLangley 如果可以的话,我会亲自给你500分。 :) - Ross
@WestLangley:请问是否有将这个实现为区域光源并集成到WebGLRenderer中的努力?我很想使用它,但如果在three.js中有更紧密的集成计划,我宁愿等待。 - Eskel
@Eskel -- 我目前所知道的是还没有,但我期望不久之后会有实现。 - WestLangley
显示剩余2条评论

2
嗯,有点奇怪的问题!看起来你开始时有一个非常具体的近似值,现在正在朝着正确的解决方案努力。
如果我们只考虑漫反射和一个平坦表面(只有一个法线),那么入射的漫反射光是什么?即使我们只考虑每个入射光都有一个方向和强度,并且我们只取所有内部=积分(lightin)((lightin)。 (normal))* light,这很困难。所以整个问题就是解决这个积分。对于点光源,您可以通过将其变成总和并拉出光线来欺骗。这对没有阴影等的点光源效果很好。现在你真正想做的是解决那个积分。这就是您可以使用某种形式的光探针、球谐函数或许多其他技术来完成的,或者使用一些技巧来估计矩形中的光量。
对我来说,总是有帮助的是想到要照亮的点上方的半球。您需要所有进入的光。有些不太重要,有些更重要。这就是您的法线所用的地方。在生产射线跟踪器中,您可以只采样几千个点并猜测一个好的结果。在实时性方面,您必须更快地猜测很多。这就是您的库代码所做的:为一个好的(但有缺陷的)猜测做出快速选择。
这就是我认为您正在走回头路的地方:您意识到他们在猜测,而且有时候很糟糕(这就是猜测的本质)。现在,不要试图修复他们的猜测,而是想出一个更好的猜测!或者尝试理解为什么他们选择了那个猜测。一个好的近似值不是关于在角落情况下表现得好,而是关于良好退化。这对我来说看起来就是这样。(再次抱歉,我现在太懒了,不想读三.js代码)。
所以回答你的问题:
- 我认为你的做法是错误的。你从高度优化的想法开始,然后试图修复它。最好从问题开始。 - 一次解决一个问题。您的屏幕截图具有许多高光,这与您的问题无关,但非常直观,并且可能对设计模型的人员产生了很大影响。 - 您正在正确的轨道上,并且比大多数人都更了解渲染。这可以为您服务,也可以反过来。阅读一些现代游戏引擎及其照明模型。您将始终找到一种迷人的混合技巧和深刻理解。深刻的理解是推动选择正确技巧的驱动力:)
希望这有所帮助。我可能完全错了,而且在向寻求快速数学解决方案的人乱说话,如果是这样,我很抱歉。

感谢您的明智建议。我几乎完全同意您上面所说的一切。我已经采用了现有的近似方法(由于问题的固有性质,它将始终是一个近似方法),并突出了解决方案失败的“边缘”情况。自从年初以来,我一直在摸索3D编程,并阅读了尽可能多的材料,但是我很难理解一些更复杂的数学知识。在我能够测试我的提议之前,我不会知道它是好还是坏...但通过试错,我仍然保持希望。 - wagerfield

1

让我们先假定投射点总是在边缘上。

我们称“照亮部分”为空间中被光线四边形沿其法线拉伸所代表的部分。

如果表面点位于照亮部分,则需要计算包含该点、它的法向量和光线法向量的平面。该平面与光线的交点将给出两个选项(仅两个,因为投射点总是在边缘上)。因此,测试这两个交点以确定哪个贡献更大。

如果该点不在照亮部分中,则可以计算四个平面,每个平面都有表面点、它的法向量和光线四边形的一个顶点。对于每个光线四边形的顶点,您将有两个点(顶点+另一个交点)来测试哪个贡献最大。

这应该能解决问题。如果您遇到任何反例,请给我反馈。


感谢您抽出时间考虑这个问题。首先,亮部分不必位于从灯光四边形挤出的立方体体积内 - 它可以位于空间中的任何位置。然而,如果该点在此区域内,则可能会比外部点接收到更多的光线,但不一定如此。我在CodePen上创建了一个交互式草图,可视化了此计算的所有组件,并输出了最近点和投射点的点积计算。需要解决的是投射点。草图:http://codepen.io/wagerfield/pen/ywqCp - wagerfield
你误解了一些东西。如果点在挤出体内,那么铸造点就在边缘的某个地方,而对于铸造,你有两个选择:当相交光平面和保持表面点、其法线和光的法线的平面时,可以得到光的边缘上的点。 如果点不在挤出体内,则需要取四个平面,每个平面都包含光的四边形顶点之一、表面点及其法线。 - Dragan Okanovic

1

感谢您尝试解决这个问题...我很想在山顶上大喊哈利路亚,但不幸的是,我没有足够理解您的答案以便将其重新实现到我的代码中。我是一个自学成才的程序员,而不是数学家,所以我在解释公式方面有困难。我在CodePen上创建了一个交互式草图:http://codepen.io/wagerfield/pen/ywqCp - 如果您能够采用您的解决方案并实现它(从第306行开始),我将非常感激!如果您愿意,这个问题还有500分的奖励等着您 :-) - wagerfield

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接