前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基础渲染系列(十五)——延迟光照

基础渲染系列(十五)——延迟光照

作者头像
放牛的星星
发布2020-07-10 17:28:28
3.3K0
发布2020-07-10 17:28:28
举报
文章被收录于专栏:壹种念头壹种念头

本文重点:

1、使用自定义的灯光着色器 2、解码LDR颜色 3、把灯光添加到独立的pass 4、支持方向光源、聚光灯、点光源 5、手动采样阴影贴图

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

这是关于渲染的系列教程的第15部分。在上一部分中,我们添加了雾。现在,我们将创建自己的延迟光照。

从现在开始,渲染教程使用Unity 5.6.0制作了。这个Unity版本在编辑器和着色器的一些设置都进行了某些更改,但是你仍然应该能够用自己的方式找到它们。

(我们自己的延迟光照玩法)

1 灯光着色器

我们在“第13章,延迟着色”中添加了对延迟渲染路径的支持。我们要做的只是填充G缓冲区,让灯光稍后渲染。而本教程简要说明了Unity如何添加这些灯光。这次,我们将自己渲染这些灯光。

为了测试灯光,我会使用一个简单的场景,将其环境强度设置为零。使用延迟的HDR摄像机渲染。

(测试场景,有和没有方向光)

场景中的所有对象都使用我们自己的着色器渲染到G缓冲区。但是灯光是使用Unity的默认延迟着色器渲染的,该着色器名为Hidden Internal-DefferedShader。你可以通过“EditProject Settings / Graphics”进入图形设置,然后将“Deferredshader”模式切换到“Custom shader”,以验证这一点。

(默认的延迟光照着色器)

1.1 使用自定义Shader

每个延迟的灯光都在单独的通道中渲染,从而影响图像的颜色。实际上,它们就是图像效果(Image Effect),例如上一教程中的延迟雾着色器。我们从一个简单的着色器开始,先用黑色覆盖所有内容。

指示Unity在渲染延迟光源时使用此着色器。

(使用自己的着色器)

1.2 第二个通道

切换到我们的着色器后,Unity报错说它没有足够的通道数量。显然,它需要第二个pass。我们只复制已经拥有的pass,看看会发生什么。

现在,Unity接受我们的着色器,并使用它来渲染定向光。结果,一切都变黑了。唯一的例外是天空。把模板缓冲区用作遮罩以避免在此处进行渲染,因为定向光不会影响背景。

(自定义着色器 受光和不受光)

但是为什么要使用第二个pass呢?请记住,禁用HDR后,灯光数据将会进行对数编码。最后的pass需要转换此编码。那就是第二个pass的目的。因此,如果你为相机禁用了HDR,那么我们着色器的第二个pass也要被用一次。

1.3 避开天空

在LDR模式下渲染时,你可能还会看到天空也变黑了。这可以在场景视图或游戏视图中发生。如果天空变黑,则转换过程将无法正确使用模板缓冲区作为遮罩。要解决此问题,请显式配置第二个Pass的模板设置。仅在处理不属于背景的片段时才应该渲染。通过_StencilNonBackground提供适当的模板值。

我们可以调试模板缓冲区吗?

不行,帧调试器没有显示有关模板缓冲区的任何信息,也没有显示其内容以及通过的方式。也许它将在将来的版本中添加。

1.4 转换颜色

为了使第二个pass工作正常,必须转换灯光缓冲区中的数据。像我们的雾着色器一样,使用UV坐标绘制全屏四边形,可用于对缓冲区进行采样。

可以通过_LightBuffer变量将灯光缓冲区本身提供给着色器。

(不受光的原始LDR数据)

使用公式

对LDR颜色进行对数编码。要对此进行解码,我们必须使用公式

(解码不受光的LDR图像)

现在我们知道它可以工作了,那就再次启用HDR吧。

2 方向光

第一个pass负责渲染灯光,因此它会相当复杂。让我们为其创建一个包含文件,名为MyDeferredShading.cginc。将所有代码从pass中复制到此文件。

然后在第一个pass中包括MyDeferredShading。

因为我们需要为图像添加光照信息,所以必须确保不擦除已经渲染的图像。可以通过更改混合模式以将全部源色和目标色组合在一起来实现。

我们需要所有可能的灯光配置的着色器变体。multi_compile_lightpass编译器指令创建我们需要的所有关键字组合。唯一的例外是HDR模式。为此,我们必须添加一个单独的多编译指令。

尽管此着色器应该用于所有三种光源类型,但首先将它限定于定向光源。

2.1 G-Buffer UV 坐标

我们需要UV坐标才能从G缓冲区采样。不幸的是,Unity不提供具有方便的纹理坐标的灯光pass。相反,必须从剪辑空间位置间接获取它们。可以使用在UnityCG中定义的ComputeScreenPos,该函数产生齐次坐标,就像剪辑空间坐标一样,因此需要使用float4来存储它们。

在片段程序中,我们可以计算最终的2D坐标。如渲染第七章,阴影中所述,这必须在插值之后发生。

2.2 世界坐标

创建延迟的雾效果时,我们必须找出片段与相机的距离。这个实现过程是通过从相机发射穿过每个片段到远平面的射线,然后按片段的深度值缩放这些光线。我们可以在此处使用相同的方法来重建片段的世界位置。

在定向光的情况下,将四边形的四个顶点的光线作为法线矢量提供。因此,我们可以将它们传递给顶点程序并进行插值。

可以通过采样_CameraDepthTexture纹理并将其线性化来在片段程序中找到深度值,就像我们对雾化效果所做的那样。

但是,最大的不同是我们将到达远平面的光线提供给了雾的着色器。这时,我们会获得到达近平面的射线。需要按比例缩放它们,以便获得到达远平面的射线。通过缩放射线使其Z坐标变为1并将其乘以远平面距离来完成。

按深度值缩放此射线可得到一个位置。因为所提供的光线在视图空间中定义的,所以得到的空间也是相机的局部空间。因此,我们现在也以片段在视图空间中的位置作为终点。

从相机空间到世界空间的转换是通过在ShaderVariables中定义的unity_CameraToWorld矩阵完成的。

2.3 读取 G-Buffer数据

接下来,我们需要访问G缓冲区以检索表面属性。通过三个_CameraGBufferTexture变量可以使用这些缓冲区。

我们在“渲染13,延迟着色器”教程中填充了相同的缓冲区。现在我们开始向他们读取。需要反照率,镜面反射色,平滑度和法线。

2.4 计算BRDF

BRDF函数在UnityPBSLighting中定义,因此我们必须包含该文件。

现在只需要三位数据就可以在片段程序中调用BRDF函数。首先是视图方向,与往常一样找到。

其次是表面反射率。我们从镜面色彩中得出。它只是最强的颜色成分。我们可以使用SpecularStrength函数提取它。

第三,我们需要灯光数据。让我们从虚拟灯开始。

最后,我们可以使用BRDF函数计算该片段的光贡献。

2.5 配置灯光

间接光不适用于该功能,因此保持黑色。另外需要配置直接光,使其与当前正在渲染的光匹配。对于定向光,我们需要一种颜色和一个方向。这些可以通过_LightColor和_LightDir变量使用。

创建一个单独的功能来设置灯光。只需将变量复制到一个轻型结构中并返回它。

在片段程序中使用此功能。

(光来自错误的方向)

终于有光照了,但它似乎来自错误的方向。这是因为_LightDir设置的是灯光传播的方向。为了进行计算,我们需要从表面到光线的方向,取反它。

(方向光 没有阴影)

2.6 阴影

在“My Lighting”中,我们依靠AutoLight中的宏来确定由阴影引起的光衰减。遗憾的是,该文件在编写时并没有考虑到延迟光照的情况。因此,我们需要自己进行阴影采样。通过_ShadowMapTexture变量可以访问阴影贴图。

但是,不能随意声明此变量。我们已经间接地在UnityShadowLibrary中为点和聚光灯阴影定义了它。因此,不应该再自己定义它,除非是使用定向光的阴影。

要应用定向阴影,只需要采样阴影纹理并使用它来减弱光色即可。在CreateLight中执行此操作意味着必须将UV坐标添加为参数。

在片段程序中将UV坐标传递给它。

(方向光带阴影)

当然,这仅在定向光启用了阴影时才有效。如果不是,则阴影衰减始终为1。

2.7 淡入阴影

阴影贴图是有限的。它无法覆盖整个世界。它覆盖的面积越大,阴影的分辨率越低。Unity具有绘制阴影的最大距离。超出之后,就没有实时阴影了。可以通过“Edit/ Project Settings / Quality”来调整此距离。

(阴影距离设置)

当阴影接近此距离时,它们会淡出。至少,Unity的着色器是这么做的。因为我们是手动采样阴影贴图,所以到达贴图的边缘时,阴影会被截断。结果是阴影被锐利地截断,或者超出了淡入淡出的距离。

(阴影距离,大VS小)

要淡化阴影,必须先知道应完全消除阴影的距离。该距离取决于方向阴影的投影方式。在“Stable Fit”模式下,衰落是球形的,居中于地图中间。在“Close Fit”模式下,它基于视图深度。

UnityComputeShadowFadeDistance函数可以为我们找出正确的指标。它以世界位置和视图深度为参数。返回距阴影中心的距离或未修改的视图深度。

阴影在接近淡入距离时应开始淡入,一旦到达阴影就完全消失。UnityComputeShadowFade函数计算适当的淡入淡出因子。

这些函数是什么样的?

它们在UnityShadowLibrary中定义。unity_ShadowFadeCenterAndType变量包含阴影中心和阴影类型。_LightShadowData变量的Z和W分量包含用于淡入的比例和偏移。

阴影淡入因子是从0到1的值,它指示阴影应淡出多少。可以通过简单地将此值添加到阴影衰减并将其钳位为0–1来完成实际的衰落。

然后,请在片段程序中为CreateLight提供世界位置和视图深度。视图深度是片段在视图空间中位置的Z分量。

(淡出阴影)

2.8 光Cookies

我们必须支持的另一件事是轻型Cookie。可通过_LightTexture0使用cookie纹理。除此之外,还必须从世界空间转换为灯光空间,以便可以对纹理进行采样。可以通过unity_WorldToLight矩阵变量来进行此转换。

在CreateLight中,使用矩阵将世界位置转换为灯光空间坐标。然后使用它们来采样cookie纹理。我们使用一个单独的衰减变量来跟踪cookie的衰减。

(方向光 带cookie)

除非你特别的去关注几何图形边缘,不然结果看起来还不错。

(边缘失真)

当相邻片段的cookie坐标之间存在较大差异时,会出现这些失真。应对这样的情况,GPU选择的mipmap级别对于最近的表面而言太低。ArasPranckevičius为Unity解决了这一点(http://aras-p.info/blog/2010/01/07/screenspace-vs-mip-mapping/)。

Unity使用的解决方案是在对Mip贴图进行采样时施加偏差,因此我们也将这样做。

(偏移后的Cookie采样)

2.9 支持LDR

到目前为止,我们只能在HDR模式下正确渲染定向光。对于LDR,这是错误的。

(不正确的LDR颜色)

首先,必须将编码的LDR颜色乘以光缓冲区,而不是相加。我们可以通过将着色器的混合模式更改为Blend DstColor Zero来实现。但是,如果这样做,HDR渲染将出错。相反,我们必须使混合模式变量。Unity为此使用_SrcBlend和_DstBlend。

(和前面的不一样,但是仍然是错误的)

当未定义UNITY_HDR_ON时,我们还必须在片段程序的末尾应用

转换。

3 聚光灯

由于定向光会影响所有内容,因此它们将被绘制为全屏四边形。相反,聚光灯仅影响场景中位于其圆锥体内的部分。通常无需为整个图像计算聚光灯照明,取而代之的是绘制一个与聚光灯的影响区域匹配的金字塔。

3.1 绘制金字塔

禁用定向光,改用聚光灯。因为我们的着色器仅对定向光源正常工作,所以结果将会是错误的。但是它允许你查看金字塔的哪些部分被渲染了。

(金字塔的一部分)

事实证明,金字塔被渲染为常规3D对象。它的背面被剔除,因此我们看到了金字塔的正面。而且只有当前面没有东西时才绘制它。除此之外,还添加了一个通道,该通道设置了模板缓冲区,以将图形限制为位于金字塔体内部的片段。你可以通过帧调试器验证这些设置。

(绘制流程)

这意味着我们的着色器的剔除和z测试设置被否决了。因此,将其从着色器中删除。

当聚光灯的体积距离相机足够远时,此方法适用。但是,当光线离摄像机太近时,它会失败。发生这种情况时,相机可能会进入该体积内。甚至有可能一部分近平面位于其内部,而其余部分位于其外部。在这些情况下,模板缓冲区就不能再用于限制渲染。

仍然渲染光线的技巧是绘制金字塔的内表面,而不是金字塔的外表面。这是通过渲染其背面而不是其正面来完成的。同样,仅当这些表面最终位于已经渲染的表面之后时才渲染它们。这种方法还涵盖了聚光灯体积内的所有片段。但这最终会渲染出太多的片段,因为通常金字塔的隐藏部分现在也被渲染了。所以,仅在必要时执行。

(靠近相机时绘制背面)

如果将摄像机或聚光灯移动到彼此附近,则会看到Unity根据需要在这两种渲染方法之间切换。一旦我们的着色器对聚光灯正常工作,两种方法之间就不会有视觉差异。

3.2 支持多灯光类型

当前,CreateLight仅适用于定向光源。让我们确保仅在适当的情况下使用特定于定向灯的代码。

尽管阴影衰减基于定向阴影贴图起作用,但其他光源类型的阴影也会衰减。这样可以确保所有阴影以相同的方式淡入淡出,而不仅仅是某些阴影。因此,只要有阴影,阴影淡入淡出代码便适用于所有灯光。所以,将该代码移到特定于光源的块之外。

不定向的灯光具有位置。通过_LightPos可以使用它。

现在我们可以确定聚光灯的光向量和光方向。

3.3 再次涉及世界位置

光线方向似乎不正确,结果为黑色。发生这种情况是因为聚光灯的世界位置计算不正确。当我们在场景中的某个地方渲染金字塔时,没有一个方便的全屏四边形,其光线存储在正常通道中。相反,MyVertexProgram必须从顶点位置获取射线。这是通过将点转换为视图空间来完成的,为此,我们可以使用UnityObjectToViewPos函数。

但是,这会产生方向错误的光线。我们必须取反它们的X和Y坐标。

(正确的世界坐标)

UnityObjectToViewPos如何工作?

该功能在UnityCG中定义。它首先将点转换为世界空间,然后使用视图矩阵将其转换为相机空间。

当在场景中渲染灯光几何时,此替代方法有效。当使用全屏四边形时,我们应该只使用顶点法线。Unity通过_LightAsQuad变量告诉我们正在处理哪种情况。

如果将其设置为1,将处理四边形,并且可以使用法线。否则,我们必须使用UnityObjectToViewPos。

3.4 Cookie衰减

聚光灯的圆锥衰减是通过cookie纹理创建的,无论它是默认圆还是自定义cookie。我们可以从复制定向灯的cookie代码开始。

但是,聚光灯Cookie越远离你的灯光位置,它就会变得越大。这是通过透视变换完成的。因此,矩阵乘法会产生4D齐次坐标。为了得到规则的2D坐标,我们必须将X和Y除以W。

(Cookie衰减)

这实际上导致了两个光锥,一个向前,一个向后。向后的圆锥体通常会终止于渲染区域的外部,但这不是必然的。因此,需要与一个负W坐标相对应的正向圆锥。

3.5 距离衰减

聚光灯发出的光也会根据距离而衰减。该衰减存储在查询纹理中,该纹理可通过_LightTextureB0使用。

设计纹理时,必须使用四边形的光线距离(根据光线的范围进行缩放)对它进行采样。该范围存储在_LightPos的第四个通道中。每个平台应使用哪个纹理通道由UNITY_ATTEN_CHANNEL宏定义。

(cookie和距离衰减)

3.6 阴影

当聚光灯具有阴影时,定义SHADOWS_DEPTH关键字。

聚光灯和定向光使用相同的变量来采样其阴影贴图。对于聚光灯,可以使用UnitySampleShadowmap来处理对硬阴影或软阴影进行采样的细节。需要为其提供阴影空间中的片段位置。unity_WorldToShadow数组中的第一个矩阵可用于将世界转换为阴影空间。

(聚光灯 带阴影)

4 点光源

点光源与聚光灯使用相同的光矢量,方向和距离衰减。这样他们就可以共享该代码。其余的Spotlight代码仅应在定义SPOT关键字时使用。

这已经足以使点光源工作。它们的渲染与聚光灯相同,不同之处在于,它们使用icosphere而不是金字塔。

(高强度的点光源)

4.1 阴影

点光源的阴影存储在立方体贴图中。UnitySampleShadowmap为我们处理采样。在这种情况下,我们必须为其提供从光到表面的向量,以对立方体贴图进行采样。这与光向量相反。

(点光源 带有阴影)

4.2 Cookies

还可以通过_LightTexture0提供点光源cookie。但是,在这种情况下,我们需要一个立方体贴图而不是常规纹理。

要对Cookie进行采样,请将片段的世界位置转换为浅色空间,然后使用该采样对立方体贴图进行采样。

(点光源带有cookie)

点光源cookie纹理不起作用?

如果你最初使用较旧的Unity版本导入了cookie的立方体贴图纹理,则可能具有错误的导入设置。这仅在立方体贴图中发生。确保其“Texture Type”为“ Cookie”,“Mapping”设置为“Auto”,“Light Type”为“Point”。

(Point cookie 纹理导入设置)

4.3 跳过阴影

现在,我们可以使用自己的着色器渲染所有动态光源。尽管我们目前并未对优化进行过多关注,但仍有一项潜在的大型优化值得考虑。

最终超出阴影淡入距离的片段不会被阴影化。但是,我们仍在采样它们的阴影,这可能会很耗时。可以通过基于阴影淡入因子进行分支来避免这种情况。它接近1,那么我们可以完全跳过阴影衰减。

但是,分支操作本身也可能很昂贵。这只是一个改进,因为这是一个连贯的分支。除了靠近阴影区域的边缘,所有片段都落在阴影区域的内部或外部。但这仅在GPU可以利用此优势的情况下才重要。在这种情况下,HLSLSupport定义UNITY_FAST_COHERENT_DYNAMIC_BRANCHING宏。

即使这样,仅当阴影需要多个纹理样本时才真正值得。对于柔和的聚光灯和点光源阴影,就是这种情况,用SHADOWS_SOFT关键字指示。定向阴影始终需要单个纹理样本,因此很便宜。

下一章,介绍静态光照。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-05-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 壹种念头 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 灯光着色器
    • 1.1 使用自定义Shader
      • 1.2 第二个通道
        • 1.3 避开天空
          • 1.4 转换颜色
          • 2 方向光
            • 2.1 G-Buffer UV 坐标
              • 2.2 世界坐标
                • 2.3 读取 G-Buffer数据
                  • 2.4 计算BRDF
                    • 2.5 配置灯光
                      • 2.6 阴影
                        • 2.7 淡入阴影
                          • 2.8 光Cookies
                            • 2.9 支持LDR
                            • 3 聚光灯
                              • 3.1 绘制金字塔
                                • 3.2 支持多灯光类型
                                  • 3.3 再次涉及世界位置
                                    • 3.4 Cookie衰减
                                      • 3.5 距离衰减
                                        • 3.6 阴影
                                        • 4 点光源
                                          • 4.1 阴影
                                            • 4.2 Cookies
                                              • 4.3 跳过阴影
                                              领券
                                              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档