基础渲染系列(五)——多灯光

2020-07-10 17:18:18 浏览数 (1)

本文重点:

1、逐物体渲染多灯光 2、支持不同类型的灯光 3、使用灯光cookies 4、计算顶点光 5、包含球谐函数

(温馨提示:本系列知识是循序渐进的,推荐第一次阅读的同学从第一章看起,链接在文章底部)

这是关于渲染的系列教程的第五部分。上一部分介绍了灯光,只带有一个定向光。现在,我们将添加对多个灯光的支持。

本教程使用Unity 5.4.0b21制作。

(一个无聊的白色球体,混合了有意思的灯光)

1 包含文件

要向着色器添加对多光源的支持,必须向其添加更多的pass。这些pass最终会包含几乎相同的代码。为了防止代码重复,我们将把着色器代码移动到包含文件中。

Unity没有菜单选项来创建着色器包含文件。因此,你必须通过操作系统的文件浏览器手动转到项目的资产文件夹。在与光照着色器相同的文件夹中创建My Lighting.cginc纯文本文件。你可以通过复制任意着色器文件,重命名然后清除其内容来实现。

(你的第一个包含文件)

从#pragma语句的右下方,直到ENDCG,将照明着色器的所有代码复制到此文件。由于此代码不再直接位于着色器pass内,因此不用再缩进它了。

现在,我们可以将此文件包含在着色器中,替换以前的代码。因为它在同一个文件夹中,所以我们可以直接引用它。

1.1 防止重定义

如你所知的那样,包含文件本身可以包含其他包含文件。如果包含的文件又包含相同的其他文件,则最终将导致代码重复。这会导致有关代码重新定义的编译器错误。

为防止此类重新定义的错误,通常使用定义检查来保护包含文件。这是预处理程序,用来检查是否已定义。该定义只是与包含文件的名称相对应的唯一标识符。你可以将其定义为任何东西,或者什么也不写。在本例中,我们将使用标识符MY_LIGHTING_INCLUDED。

现在,我们可以将包含文件的所有内容放入预处理程序if块中。条件是尚未定义MY_LIGHTING_INCLUDED。

通常,此包含文件防护中的代码不使用缩进。

2 第二个灯光

我们的第二盏灯将再次是个定向灯。复制主光源并更改其颜色和旋转度,以便你可以区分它们。另外,将其强度滑块减小到例如0.8。Unity将使用强度自动确定主光源。

(两个方向光)

即使我们有两个定向光,现在也没有视觉差异。通过一次只激活一个的时候,可以独立地看到它们的光。但是当两者都激活时,只有主光源才起作用。

(要么是1号光,要么是2号光)

2.1 第二个通道

我们仅看到一个光源,因为我们的着色器仅计算一个光源。forward base pass用于主方向灯。要渲染额外的灯光,我们需要额外的pass。

复制我们的着色器通道代码,并将新的灯光模式设置为ForwardAdd。Unity将使用此通道来渲染其他光源。

但现在,我们看到的是辅助光,而不是主光。Unity会同时渲染这两者,但是附加通道最终会覆盖基本通道的结果。这显然是错的, 附加通道必须将其结果添加到基本通道中,而不是替换它。我们可以通过更改附加通道的混合模式来指示GPU执行此操作。

新和旧像素数据的组合方式由两个因素决定。新数据和旧数据乘以这些因素,然后相加就成为最终结果。默认模式是不混合,等效于One Zero。这样通过的结果将替换帧缓冲区中以前的任何内容。要添加到帧缓冲区,我们必须指示它使用“ One One”混合模式。这称为additive blending。

(两个灯光现在都添加进来了)

第一次渲染对象时,GPU会检查片段是否出现在已经渲染到该像素的其他物体之前。该距离信息存储在GPU的深度缓冲区(也称为Z缓冲区)中。因此,每个像素都具有颜色和深度。该深度表示每个像素到相机最近表面的距离。就像声纳。

如果要渲染的片段前面没有任何内容,则当前是最靠近相机的表面。GPU继续运行片段程序。它会覆盖像素的颜色,并记录其新的深度。

如果该片段结束于比已经存在的片段更远的地方,则它前面有东西。在这种情况下,我们看不到它,也根本不会渲染它。

那半透明的物体呢?

深度缓冲方法仅适用于完全不透明的对象。半透明对象需要不同的方法。我们将在以后的教程中处理这些内容。

对辅助光重复此过程,只是现在我们要添加到已有的灯光中。再说明一下,仅当我们要渲染的内容前无任何片段时,才会运行片段程序。如果是这样的话,我们最终会到达与上一个通道完全相同的深度,因为它是针对同一对象的。因此,我们最终记录了完全相同的深度值。

由于不需要两次写入深度缓冲区,因此可以禁用它。这是通过ZWrite Off着色器语句完成的。

2.2 Draw Call 合批

为了更好地了解发生了什么,你可以启用游戏视图右上角的“Stats”面板。查看批处理的数量以及通过批处理节省的数量。这些代表Draw Call。仅在主光源处于活动状态时执行此操作。

(5个批次,一共7个)

因为我们有六个对象,所以应该有六个批次才对。但是启用动态批处理后,所有的三个立方体将合并为一个批处理。因为节省了2个,所以一共有5个。

多余的批次是由动态阴影引起的。让我们通过“Edit/ Project Settings / Quality”完全禁用质量设置中的阴影来消除它。确保调整编辑器中当前使用的质量设置。

(没有阴影了,4个批次)

为什么我会有多余的一个批次?

你可能正在渲染环境立方体贴图。那是另一个Draw Call。我们在上一教程中说了怎样禁用它。

有时候可能需要触发一下统计信息更新(例如,通过单击游戏视图),之后Draw Call应为四个,其中两个通过批处理节省。

然后,激活辅助灯。

(2个灯光12个批次)

因为每个对象现在渲染两次,所以最终得到十二个批次,而不是六个批次。这是符合预期的。主要是因为动态批处理失效了。因为Unity的动态批处理仅适用于最多受单个方向光影响的对象。激活第二盏灯使得该优化变得不可能了。

2.3 Frame Debugger

为了更好地了解场景的渲染方式,可以使用帧调试器。通过 Window/ Frame Debugger 将其打开。

(Frame Debugger 窗口)

启用后,帧调试器允许你逐帧完成每个单独的Draw Call。窗口本身显示每个Draw Call的详细信息。游戏视图将显示选定的Draw Call所呈现的内容。

(按步显示Draw Call)

首先绘制靠近照相机的不透明物体。这种从前到后的绘制顺序非常有效,因为有了深度缓冲区,被隐藏的片段就会被跳过。如果要从后往前绘制,就渲染更多不必要的渲染。这被称为overdraw,应尽可能避免。

Unity从前到后对对象进行排序,但这并不是确定绘制顺序的唯一方法。因为更改GPU状态也很昂贵,也应将它的影响其最小化。这是通过将相似的对象放在一起渲染来完成的。例如,Unity倾向于按组渲染球体和立方体,因为那样就不必频繁地在网格之间切换。同样,Unity倾向于对使用相同材质的对象进行分组。

3 点光源

定向光并不是唯一的光源类型。通过GameObject/ Light / Point Light 可以添加一个点光源。

(点光源)

为了更好的看清它,先禁用两个定向灯。然后将点光源稍微移动一点。

(移动光源,从底部到顶部)

灯光表现得很奇怪。这是怎么回事?使用帧调试器时,你会注意到我们的对象首先呈现为纯黑色,然后再次渲染为怪异的光照。

第一遍是基础pass。即使没有活动的定向光源,也始终会渲染它。因此,我们最终得到了黑色的轮廓。

第二遍是我们的附加 pass。这次,它使用点光源而不是定向光。但是我们的代码仍然假设它是有方向性的。这里就是我们需要解决的问题。

3.1 灯光函数

因为我们的光照即将变得更加复杂,所以我们把创建它的代码移到一个单独的函数中。将此函数直接放在MyFragmentProgram函数上方。

现在我们可以简化MyFragmentProgram。

3.2 灯光位置

_WorldSpaceLightPos0变量包含当前灯光的位置。但是在定向光的情况下,它实际上只保持了定向光的方向。现在,我们使用了点光源,该变量只包含其名称所表示的位置数据。因此,我们必须自己计算光的方向。通过减去片段的世界位置并将结果归一化化来完成位置计算。

(从位置得出方向)

3.3 灯光衰减

在定向光的情况下,知道其方向就足够了。因为假定它是无限远的。但是点光源具有明确的位置。这意味着它到物体表面的距离也会产生影响。光线越远,它就会变得越暗。这称为光的衰减。

在方向光的情况下,假定的衰减变化非常缓慢,因此我们可以将其视为恒定。所以不用理会。但是,点光源的衰减是什么样的呢?

想象一下一个点,我们从该点发出单个光子的爆炸流。这些光子向各个方向移动。随着时间的流逝,光子会进一步远离该点。由于它们都以相同的速度传播,因此光子充当球体的表面,该球体的点位于其中心。随着光子的不断移动,该球体的半径增大。随着球体的增长,其表面也随之增长。但是此表面始终包含相同数量的光子。因此,光子的密度降低。这就确定了观察到的光的亮度。

(球形衰减)

半径为r 的球的表面积等于

。要确定光子密度,我们可以将其除以它。忽略常数4π,因为我们可以假设这是光线强度的因子。这导致衰减因子为

,其中d是光的距离。

(靠近的时候太亮了)

这会在靠近光线的地方产生非常明亮的效果。发生这种情况的原因是,当距离接近零时,衰减因子会达到无穷大。为确保光的强度在零距离处达到最大值,请将衰减方程式更改为

(不再那么明亮了)

3.4 灯光范围

在现实生活中,光子会不断移动直到撞到某物。这意味着即使光线太弱以至于我们无法再看到它,它的范围也可能是无限的。但是我们不想浪费时间渲染看不见的光。因此,会直接停止渲染它们。

点光源和聚光灯都有一定范围。只有位于此范围内的对象会通过此光线进行绘制。而所有其他对象都不会。默认范围是10。此范围越小,获得额外draw call的对象就越少,这会产生更高的帧率。将我们的灯光范围设置为1并四处移动试试。

(灯光半径为1)

你会清楚地看到物体何时进入和超出范围,因为它们会突然在点亮和熄灭之间切换。发生这种情况是因为在我们选择的范围之外,光仍然可见。要解决此问题,我们必须确保衰减和范围同步。

实际上,光没有最大范围。因此,我们设定的任何范围都是出于艺术自由。然后,我们的目标就是确保当物体移出范围时,不会出现突兀的光线过渡。这要求衰减系数在最大范围内达到零。

Unity通过将片段的世界位置转换为光线空间位置来确定点光源的衰减。这是光线物体局部空间中的一个点,按其衰减比例缩放。在这个空间中,点光源位于原点。距离一个以上的单元都会超出范围。因此,距原点的平方距离定义了比例衰减因子。

Unity又做了进一步设定,并使用平方距离对衰减纹理进行采样。这样做是为了确保衰减尽早降至零。如果不执行此步骤,当对象移入或移出范围时,仍然可能会弹出光。

在AutoLight包含文件中找到此技术的代码。让我们用它代替自己编写。

现在,我们可以访问UNITY_LIGHT_ATTENUATION宏。此宏插入代码以计算正确的衰减系数。它具有三个参数。第一个是包含衰减的变量的名称。第二个参数与阴影有关。由于我们尚不支持这些功能,因此请使用零。第三个参数是世界位置。

请注意,宏定义了当前作用域中的变量。因此,我们不应该再自己声明它。

UNITY_LIGHT_ATTENUATION是什么样的?

这是相关的代码。#ifdef POINT语句是#if defined(POINT)的简写。

阴影坐标类型在其他位置定义。它们是全精度或半精度浮点数。

点积产生单个值。rr只是复制了它,所以你最终得到了float2。然后将其用于采样衰减纹理。由于纹理数据为一维,因此其第二个坐标无关紧要。

UNITY_ATTEN_CHANNEL是r还是a,具体取决于目标平台。

因为我们不支持阴影,所以SHADOW_ATTENUATION宏变为1,可以忽略。

使用此宏后,衰减似乎不再起作用。那是因为它有多个版本,每种灯类型一个。默认

情况下,它用于定向光,完全没有衰减。

仅当已知我们正在处理点光源时才定义正确的宏。为了表达这一点,我们需要在包含AutoLight之前#define POINT。由于我们仅在添加过程中处理点光源,因此请在包含“My Lighting”之前在其中定义点光源。

(半径为10的衰减)

4 混合灯光

关闭点光源,然后再次激活我们的两个方向光。

(不正确的和正确的方向光)

这里有些不对了,因为我们将其光线方向解释为位置。并且,由附加通道生成的辅助定向光被完全视为点光源。为了解决这个问题,我们还需要为不同的光源类型创建着色器变体。

4.1 着色器变体

在检查器中检查我们的着色器。“Compile and show code”按钮下的下拉菜单包含一个区域,它会告诉我们当前有多少个着色器变体。单击“Show ”按钮以获取它们的概述。

(当前存在2个变体)

打开的文件告诉我们,我们有两个片段,每个片段都有一个着色器变体。其实就是我们的基本和附加通道。

我们要为附加通道创建两个着色器变体。一种用于定向光,另一种用于点光源。为此,我们在pass的代码中添加了多编译的编译指示。该语句定义关键字列表。Unity将为我们创建多个着色器变体,每个变体定义这些关键字之一。

每个变体都是单独的着色器。它们是单独编译的。它们之间的唯一区别是定义了哪些关键字。

现在,我们需要DIRECTIONAL和POINT,并且不再需要自己定义POINT。

再次调用着色器变体概览。这次,根据我们的要求,第二个代码段将包含两个变体。

4.2 使用关键字

我们可以检查其中存在哪些关键字,就像AutoLight用于POINT一样。在我们的例子中,如果定义了POINT,那么我们必须自己计算光的方向。否则,如果我们有一个定向光,而_WorldSpaceLightPos0就是表示该方向了。

它适用于两个附加pass变体。也适用于基本pass,因为它没有定义POINT。

Unity根据当前的灯光和着色器的variant关键字来决定使用哪个变量。渲染定向光时,它使用DIRECTIONAl变体。渲染点光源时,它使用POINT变体。当没有匹配项时,它只是从列表中选择第一个变体。

(渲染3个灯光)

5 聚光灯

除了定向和点光源外,Unity还支持聚光灯。聚光灯类似于点光源,不同之处在于它们被限制为圆锥形,而不是向各个方向发光。

(聚光灯)

那区域光呢?

仅静态光照贴图支持这些功能。我们将在以后的教程中介绍该主题。

为了也支持聚光灯,我们必须将SPOT添加到我们的多编译语句的关键字列表中。

我们的附加着色器现在具有三个变体。

聚光灯的位置与点光源一样。因此,当定义了POINT或SPOT时,我们必须计算光的方向。

(60度角的聚光灯)

这已经能够使聚光灯工作了。它们最后带有另一个UNITY_LIGHT_ATTENUATION宏,该宏负责处理圆锥形状。

衰减方法开始时与点光源相同。转换为光空间,然后计算衰减系数。然后,对原点后面的所有点强制将衰减设为零。这会限制聚光灯前面的所有物体的光。

然后,将光空间中的X和Y坐标用作UV坐标以对纹理进行采样。此纹理用于遮挡光线。纹理只是带有模糊边缘的圆形。这产生了一个轻质的圆柱体。为了将其变成圆锥体,到光空间的转换实际上是透视变换,并使用齐次坐标。

聚光灯的UNITY_LIGHT_ATTENUATION是什么样的?

如下。请注意,在对蒙版纹理进行采样时,会从齐次坐标转换为欧氏坐标。在那之后加½可使纹理居中。

5.1 聚光灯 Cookies

默认的聚光灯蒙版纹理是模糊的圆圈。但是,你可以使用任何正方形纹理,只要它的边缘降至零即可。这些纹理称为聚光Cookies。此名称源自cucoloris,cucoloris是指将阴影添加到灯光中的电影,剧院或摄影道具。

Cookie的Alpha通道用于遮挡光线。其他通道无关紧要。这是一个示例纹理,其中所有四个通道均设置为相同的值。

(聚光灯 Cookie)

导入纹理时,可以选择Cookie作为其类型。然后,你还需要设置其灯光类型,在本例中为Spotlight。然后,Unity将为你处理大多数其他设置。

(导入贴图)

现在可以将此纹理用作聚光灯的自定义Cookie了。

(使用了自定义的聚光灯Cookie)

6 更多的Cookies

点光源也可以有Cookies。在这种情况下,光线会全方位传播,因此cookie必须包裹在一个球体上。这是通过使用立方体贴图完成的。

你可以使用各种纹理格式来创建点光源cookie,Unity会将其转换为立方体贴图。你必须指定Mapping,以便Unity知道如何解释你的图像。最好的方法是自己提供一个立方体贴图,可以使用自动映射模式。

(点光源的cookie 立方体贴图)

点光源Cookie没有任何其他设置。

我们必须将POINT_COOKIE关键字添加到我们的多编译语句中。这时,它已成为一长串的清单。因为这是一个普通的列表,所以Unity为我们提供了一个速记实用的说明,我们可以代替它使用。

你可以验证这确实产生了我们需要的五个变体。

而且,不要忘了使用Cookie也要计算点光源的光方向。

(带有cookie的点光源)

UNITY_LIGHT_ATTENUATION是什么样的?

它等效于常规点光源的宏,除了它还会对Cookie进行采样。由于在这种情况下cookie是立方体贴图,因此它使用texCUBE来完成。

7 顶点光

每个可见对象始终使用其base pass进行渲染。该通道可以从主要的定向光中获取数据。每增加一个灯光,都会在此之上增加一个额外的附加通道。因此,多灯光将导致多DrawCall。范围内有多物体的多灯光将导致大量的DrawCall。

以一个包含四个点光源和六个对象的场景为例。所有对象都在所有四个灯光的范围内。这需要每个对象进行五次DrawCall。一个为base pass,另外四个为additive passes。总共有30个DrawCall。不过需要注意,可以向其中添加一个定向光,而不会增加DrawCall。

(4个点光源,6个物体,30个DC)

为了控制DrawCall的数量,你可以通过质量设置来限制“Pixel Light Count”。这定义了每个对象使用的最大像素光量。按片段计算时,它们称为像素光。

更高的质量级别允许更多像素的光。最高质量级别的默认值为四个像素光源。

(物体受到光数量的影响 0-4)

每个对象渲染的光都不同。Unity根据灯光的相对强度和距离从最高到最低对灯光进行排序。预期贡献最少的灯会首先被丢弃。

实际上,还有更多其他的事情发生,但我们稍后再讨论。

由于不同的对象会受到不同的光照的影响,因此你会获得不一致的光照。当物体运动时,情况会变得更糟,因为这可能导致光线的突然改变。

问题看起来非常严重,因为灯光会完全关闭。不过也不是没有办法,还有另一种方法可以使灯光便宜得多,而且无需完全关闭它们。我们可以逐顶点而不是逐片段渲染它们。

每个顶点渲染一个光源意味着你可以在顶点程序中执行光照计算。然后对所得颜色进行插值,并将其传递到片段程序。这非常廉价,以至于Unity在base pass中都包含了这种灯光。发生这种情况时,Unity会使用VERTEXLIGHT_ON关键字寻找base pass着色器变体。

仅点光源支持顶点光照。因此,定向灯和聚光灯不能使用。

要使用顶点光,我们必须在base pass中添加一个多编译语句。它只需要一个关键字VERTEXLIGHT_ON。另一个选择是根本没有关键字。为了表明这一点,我们使用_。

7.1 一个顶点光

要将顶点光的颜色传递给片段程序,我们需要将其添加到Interpolators结构中。当然,只有在定义了VERTEXLIGHT_ON关键字时才需要这样做。

创建一个单独的函数来计算这种颜色。它从内插器中读取和写入,因此成为inout参数。

现在,我们将仅传递第一个顶点光的颜色。并且只有在灯光存在的情况下才做, 否则,什么都不做。UnityShaderVariables定义了一组顶点光颜色。这些是RGBA颜色,但是我们只需要RGB部分。

在片段程序中,我们必须将此颜色添加到此处计算的所有其他灯光中。可以通过将顶点光颜色视为间接光来实现。将间接照明数据的创建移至其自身的函数中。在其中,将顶点光颜色分配给间接漫反射分量(如果存在的话)。

将像素光计数设置为零。现在,应将每个对象渲染为具有单个灯光颜色的轮廓。

(逐物体的第一个顶点光颜色)

Unity通过这种方式最多支持四个顶点灯。这些灯光的位置存储在四个float4变量中,每个坐标一个。它们是unity_4LightPosX0,unity_4LightPosY0和unity_4LightPosZ0,它们在UnityShaderVariables中定义。这些变量的第一部分包含第一顶点光的位置。

接下来,我们计算光向量,光方向和ndotl因子。不能在这里使用UNITY_LIGHT_ATTENUATION宏,所以我们只使用

。这会产生最终的颜色。

请注意,虽然我们也可以计算镜面反射项,但是在大三角形上插值时看起来会非常糟糕。

实际上,UnityShaderVariables提供了另一个变量unity_4LightAtten0。它包含有助于近似估算像素光衰减的因素。使用这个,我们的衰减变成

(一个顶点光,逐物体渲染)

7.2 4个顶点光

要包括Unity支持的所有四个顶点光,我们必须执行4次相同的顶点光计算,并将结果相加。不用我们自己编写所有代码,我们可以使用在UnityCG中定义的Shade4PointLights函数。向它提供位置矢量,光色,衰减因子,以及顶点位置和法线。

Shade4PointLights是什么样的?

实际上,进行了四次相同的计算。操作顺序略有不同。使用rsqrt在点积之后执行归一化。该函数计算倒数平方根。

(4个顶点光)

现在,如果一个对象最终获得的光多于像素光的数量,那么最多将包括四个光作为顶点光。实际上,Unity试图通过同时包含一个像素和顶点光源来隐藏像素和顶点光源之间的过渡。包含两次该光,其顶点和像素版本的强度有所不同。

顶点灯少于四个时会发生什么?

仍然会计算四个顶点光。其中一些只是黑色。因此,始终要支付四盏灯的价格。

(顶点光和像素光的切换)

默认情况下,Unity决定哪些光源变为像素光源。你可以通过更改灯光的渲染模式来覆盖此设置。不管有什么限制,重要的光源总是渲染为像素光源。不重要的光永远不会渲染为像素光。

(灯光渲染模式)

8 球谐函数

当我们用完所有像素光源和所有顶点光源时,可以使用另一种渲染光源的方法,球谐函数。所有三种光源类型均支持此功能。

球谐函数背后的想法是,你可以使用单个函数描述某个点处的所有入射光。此功能定义在球体的表面上。

通常,使用球形坐标描述此功能。但是你也可以使用3D坐标。这使我们可以使用对象的法线向量对函数进行采样。

要创建这样的功能,你必须在各个方向上对光强度进行采样,然后找出如何将其变成单个连续的功能。或者说,你必须对每个对象表面上的每个点都执行此操作。这当然是不可能的。所以我们需要要有一个近似值。

首先,仅从对象的本地原点的角度定义函数。这对于沿对象表面变化不大的光照很友好。对于小物体以及光线较弱或远离物体的情况都是如此。幸运的是,对于不符合像素或顶点光源状态的光源,通常都是这种情况。

其次,我们还必须知道近似函数本身。你可以将任何连续函数分解为不同频率的多个函数。这些被称为频段。对于任意功能,可能需要无限数量的频段来执行此操作。

一个简单的例子是组成正弦曲线。从基本的正弦波开始。

(sin2πx)

这是第一支频段。对于第二个频段,请使用频率为两倍,幅度为一半的正弦波。

(全频率,半振幅sin4πx/2)

当加在一起时,这两个频段将描述更复杂的功能。

(两个频段相加)

继续添加这样的频段,将频率加倍并将幅度减半。

(第三和第四频段)

添加的每个频段都会使功能更加复杂。

(4个频段

本示例使用具有固定模式的规则正弦波。为了用正弦波描述任意函数,你必须调整每个频段的频率,幅度和偏移,直到获得完美的匹配为止。

如果使用的频段少于完美匹配所需要的频段,那么最终结果将是原始功能的近似值。使用的频段越少,近似值的准确性就越低。该技术用于压缩很多东西,例如声音和图像数据。在我们的案例中,我们将使用它来近似3D照明。

频率最低的频段与该功能的较大功能相对应。我们绝对希望保留这些。因此,我们将丢弃频率更高的频段。这意味着我们会丢失照明功能的一些细节。如果照明变化不快,那就很好了,因此我们必须再次限制它只能用于漫反射光。

8.1 球谐函数频段

照明的最简单近似是均匀的颜色。各个方向的照明都是相同的。这是第一个频段,我们将其识别为

。它由单个子功能定义,并且只是一个常数。

第二波段引入线性定向光。对于每个轴,它描述了大部分光线来自何处。因此,它分为三个函数,分别由

标识。每个函数都包含我们的法线坐标之一,乘以一个常数。

第三频段变得更加复杂。它包含五个功能,

。这些函数是二次函数,这意味着它们包含两个法线坐标的乘积。

继续往后,但是Unity仅使用前三个频段。所有项需要乘以

这实际上是单个功能,可以拆分,以便可以标识其子功能。最终结果是将所有九个条目加在一起。通过调制这九项中的每一项,会产生不同的照明条件,并附加一个系数。

什么会决定此函数的形状?

在球体中,球谐是拉普拉斯方程的一种解决方案。数学相当复杂。功能部分的定义是

是Legendre多项式,

项是归一化常数。

这是非常复杂的定义,使用复数i和球坐标, φ 和 θ。你也可以使用3D坐标的真实版本。这就变成了我们使用的功能了。

幸运的是,我们不需要知道如何导出函数。甚至不需要知道所涉及的具体数字。

最终,可以用九个因子来表示任何照明条件的近似值。由于这些是RGB颜色,因此最终得到27个数字。我们也可以将函数的常量部分合并到这些因素中。这导致了我们的最终功能,

,其中a和i是因子。

该方法是否适用于

所以那是

加上一个因数,再加上一个额外的常数。由于整个表格代表一个公式,常量可以合并到

项。

你可以可视化法线坐标以了解项所代表的方向。例如,这是一种将正坐标着色为白色而将负坐标着色为红色的方法。

然后,你可以使用i.normal.x和i.normal.x * i.normal.y等来可视化每个项。

(1)

(y)

(z)

(x)

(xy)

(yz)

(zz)

(xz)

(xx)

(-yy)

8.2 使用球谐函数

每个被球谐函数近似的光都必须分解为27个数。幸运的是,Unity可以非常快速地做到这一点。base pass可以通过在UnityShaderVariables中定义的七个float4变量的集合来访问它们。

UnityCG包含ShadeSH9函数,该函数根据球谐数据和法线参数计算照明。它需要一个float4参数,其第四部分设置为1。

ShadeSH9是什么样的?

该功能使用两个子功能,一个用于第一二频段,另一个用于第三频段。这样做是因为Unity的着色器可以在顶点程序和片段程序之间拆分计算。这是我们将来会考虑的优化。

另外,在线性空间中执行球谐函数的计算。ShadeSH9函数在需要时将结果转换为伽玛空间。

为了更好地了解最终的近似值,请直接在片段程序中返回ShadeSH9的结果。

现在关闭所有灯。

(环境色)

惊喜!我们的物体不再是黑色的。他们拾取了周围的颜色。Unity使用球谐函数将场景的环境颜色添加到对象中。

现在激活这一堆灯。请确保硬件有足够的性能,以便所有像素和顶点光都能用完。其余灯的被添加到球谐函数中。同样,Unity将拆分灯光以混合过渡。

(使用球谐函数的灯)

就像顶点光一样,我们将球谐光数据添加到漫反射间接光中。另外,让我们确保它永远不会造成任何负面影响。毕竟,这只是一个近似值。

但我们只能在base pass中执行此操作。由于球谐函数与顶点光无关,因此我们不能依赖相同的关键字。相反,我们将检查是否定义了FORWARD_BASE_PASS。

因为在任何地方都没有定义FORWARD_BASE_PASS,所以这又消除了球谐函数。如果将像素光计数设置为零,则仅可见顶点光。

(只剩4个顶点光)

在包括“My Lighting”之前,在基本通道中定义FORWARD_BASE_PASS。现在,我们的代码知道何时进入base pass。

(顶点光和球谐函数)

我们的着色器最终同时包括顶点光和球谐函数。并且,如果确保像素光计数大于零,则将看到所有三种照明方法的组合。

(4个附加的像素光)

8.3 天空盒

如果球谐函数包括纯净的环境颜色,那么它也可以与环境天空盒一起使用吗?

是的! Unity还将使用球谐函数来近似天空盒。要尝试此操作,请关闭所有灯光,然后为环境照明选择默认的天空盒。默认情况下,新场景使用此天空盒,但我们在先前的教程中已经将其删除了。

(默认天空盒,没有方向光)

现在,Unity在后台渲染天空盒。它是基于主方向灯的程序生成的天空盒。由于我们没有活动的光,因此它的行为就像太阳在地平线上。你会看到对象已经拾取了天空盒的某些颜色,从而产生了一些细微的阴影。所有这些都是通过球谐函数完成的。

打开主方向光。这将大大的改变天空盒。你可能会注意到,球形谐波的变化要比天空盒晚一些。那是因为Unity需要一些时间来近似天空盒。这只有在突然改变时才会引起注意。

(带有主方向光的天空盒,有球谐函数和没有球谐函数)

物体突然变亮了!环境贡献非常强。程序天空盒代表一个完美的晴天。在这些条件下,完全白色的表面确实会显得非常明亮。在伽玛空间中渲染时,这种效果将最强。在现实生活中,没有很多完全白色的表面,它们通常要深得多。

(添加纹理之后,有球谐和没有球谐)

下一章介绍凹凸。

0 人点赞