有没有办法为片段着色器传递任意数量的光照位置(和颜色),并在着色器中循环遍历它们?
如果不是,那么应该如何模拟多个灯光?例如,对于漫射定向照明,您不能只传递明暗器的光权重之和。
有没有办法为片段着色器传递任意数量的光照位置(和颜色),并在着色器中循环遍历它们?
如果不是,那么应该如何模拟多个灯光?例如,对于漫射定向照明,您不能只传递明暗器的光权重之和。
Answers:
通常有两种方法可以解决此问题。如今,它们被称为正向渲染和延迟渲染。我将在下面讨论这两种情况的一种变化。
对影响对象的每个光源渲染一次。这包括环境光。您使用加法混合模式(glBlendFunc(GL_ONE, GL_ONE)
),因此每个光源的贡献会相互添加。由于不同光线的贡献是累加的,因此帧缓冲区最终获得了价值
您可以通过渲染到浮点帧缓冲区来获得HDR。然后,您在场景上进行最后遍历,以将HDR照明值缩小到可见范围。这也是实现布隆和其他后效应的地方。
此技术(如果场景中有很多对象)的一种常见性能增强方法是使用“预通过”,即在渲染所有对象时不向彩色帧缓冲区绘制任何内容(用于glColorMask
关闭彩色写入)。这只是填充深度缓冲区。这样,如果渲染的对象位于另一个对象之后,GPU可以快速跳过这些片段。它仍然必须运行顶点着色器,但是它可以跳过通常更昂贵的片段着色器计算。
这更易于编写代码,也更易于可视化。在某些硬件(主要是移动GPU和嵌入式GPU)上,它可能比其他方法更有效。但是在高端硬件上,通常在灯光较多的场景中胜出。
延迟渲染有点复杂。
用于计算曲面上某个点的光线的光照方程式使用以下曲面参数:
在正向渲染中,这些参数可以通过直接从顶点着色器传递,从纹理中拉出(通常通过从顶点着色器传递的纹理坐标)或从片段着色器中的整块布料生成而达到片段着色器的照明功能。其他参数。漫射颜色可以通过将每个顶点的颜色与纹理组合,组合多个纹理来计算。
在延迟渲染中,我们将这一切明确化。在第一遍中,我们渲染所有对象。但是我们不渲染颜色。相反,我们渲染表面参数。因此,屏幕上的每个像素都有一组表面参数。这是通过渲染到屏幕外的纹理来完成的。一种纹理将漫反射颜色存储为其RGB,可能将镜面反射光泽存储为alpha。另一种纹理将存储镜面反射颜色。三分之一将存储正常值。等等。
该职位通常不被存储。取而代之的是,它在第二遍通过数学方法进行了重构,因为它太复杂了,无法深入到这里。可以说,我们使用深度缓冲区和屏幕空间片段位置作为输入来计算出该点在曲面上的相机空间位置。
因此,既然这些纹理基本上包含了场景中每个可见像素的所有表面信息,我们就开始渲染全屏四边形。每个灯光都会获得全屏四边形渲染。我们从表面参数纹理采样(并重新构造位置),然后仅使用它们来计算该光线的贡献。再次glBlendFunc(GL_ONE, GL_ONE)
将其添加到图像中。我们一直这样做,直到用尽所有灯光。
HDR再次是后处理步骤。
延迟渲染的最大缺点是抗锯齿。需要进行更多的工作才能正确消除锯齿。
如果您的GPU有很多内存带宽,那么最大的好处就是性能。我们只渲染一次实际的几何图形(如果正在做阴影映射,则每个具有阴影的光为1 + 1)。在此之后,我们绝不会将时间花在看不见的隐藏像素或几何上。所有的照明时间都花费在实际可见的物体上。
如果您的GPU没有足够的内存带宽,那么光通过确实会开始受到伤害。每个屏幕像素从3-5个纹理中提取图像并不有趣。
这是延迟渲染的一种变体,具有一些折衷方案。
与延迟渲染一样,您可以将曲面参数渲染到一组缓冲区中。但是,您已经缩写了表面数据。这次您唯一关心的表面数据是深度缓冲区值(用于重构位置),法线和镜面光泽度。
然后,对于每个灯光,您仅计算照明结果。没有与表面颜色相乘,什么也没有。仅点(N,L)和镜面反射项,完全没有表面颜色。高光和扩散项应保留在单独的缓冲区中。每个光的镜面反射项和漫射项在两个缓冲区中求和。
然后,使用总的镜面反射光和漫反射光计算重新渲染几何图形,以与表面颜色进行最终组合,从而产生整体反射率。
这里的好处是您可以进行多重采样(至少比延迟采样更容易)。您进行的每对象渲染比前向渲染少。但是,这样做的主要延迟是为不同表面具有不同照明方程式的时间更容易。
使用延迟渲染时,通常每个光源都使用相同的着色器绘制整个场景。因此,每个对象都必须使用相同的材质参数。使用光照预传递,可以为每个对象赋予不同的着色器,以便它可以自己完成最后的光照步骤。
这没有提供正向渲染情况那样的自由。但是,如果您有可用的纹理带宽,它仍然会更快。
invariant
关键字可以保证在其他情况下可以保证不变)。
您需要使用延迟渲染或预通过照明。一些较旧的固定功能管线(阅读:无着色器)最多支持16或24个光源- 就是这样。延迟渲染消除了光照限制;但要以更复杂的渲染系统为代价。
显然,WebGL支持MRT,这对于任何形式的延迟渲染都是绝对必需的-因此它可能是可行的。我只是不确定它是否合理。
或者,您可以研究Unity 5-它已将开箱即用的渲染推迟了。
解决此问题的另一种简单方法是简单地确定灯光的优先级(也许基于与玩家的距离以及它们是否在相机视锥中),并仅启用前8位。许多AAA标题设法做到了这一点却没有太大影响有关输出质量的信息(例如,“孤岛惊魂1”)。
您还可以查看预先计算的光照贴图。诸如《雷神之锤1》这样的游戏从中获得了很多收益-它们可以很小(双线性过滤可以很好地柔化拉伸的光照贴图)。不幸的是,预先计算不包括100%动态灯光的概念,但它确实看起来很棒。您可以将其与您的8个灯光限制相结合,例如,只有火箭等具有真实的灯光-但是墙上或此类的灯光将是光照贴图。
旁注:您是否不想在着色器中循环播放它们?与您的表现说再见。GPU 不是 CPU,并且不能像JavaScript那样以相同的方式工作。请记住,渲染的每个像素(即使它被覆盖)也必须执行循环-因此,如果您以1920x1080的分辨率运行并且运行16次的简单循环,则该循环中的所有内容都有效地运行了33177600次。您的图形卡将并行运行很多这些片段,但是这些循环仍然会消耗旧的硬件。
您可以使用支持n盏灯的像素着色器(其中n是较小的数字,如4或8),并多次重绘场景,每次传递新的一批灯光,并使用加性混合将它们全部组合在一起。
这是基本思想。当然,要使速度足够快以适合合理大小的场景,还需要进行大量优化。不要只绘制可见光(视锥和遮挡剔除);不要绘制所有光。实际上,并不是每次绘制都重新绘制整个场景,而只是重新绘制该光照范围内的对象;着色器具有多个版本,它们支持不同数量的光源(1、2、3,...),因此您不会浪费时间评估过多的光源。
当您有许多小光源时,另一个答案中提到的延迟渲染是一个不错的选择,但这不是唯一的方法。