查看原文
其他

程序丨Unity 渲染教程(十二):半透明材质的阴影

2017-11-15 崔嘉艺 Gad-腾讯游戏开发者平台

译者:崔嘉艺(milan21)

审校:王磊(未来的未来)


  • 支持剪切造成的阴影。

  • 使用抖动。

  • 对半透明材质造成的阴影进行近似。

  • 在半透明材质的阴影和剪切生成的阴影之间切换。


这是关于渲染基础的系列教程的第十二部分。在前面的部分里我们已经可以渲染半透明表面了,但我们还没有实现半透明表面的阴影。现在我们来解决这个问题。


系列回顾:

Unity 渲染教程(一):矩阵

Unity 渲染教程(二):着色器基础

Unity 渲染教程(三):使用多张纹理贴图

Unity 渲染教程(四):第一个光源

Unity 渲染教程(五):多个光源

Unity 渲染教程(六):凹凸度

Unity 渲染教程(七):阴影

Unity 渲染教程(八):反射

Unity 渲染教程(九):复杂材质

Unity 渲染教程(十):更多复杂的应用场景

Unity 渲染教程(十一):透明度


这个教程是使用Unity 5.5.0f3开发的。


当物体淡入的时候,它们的阴影也要相应的淡入。


剪切造成的阴影


目前,我们透明材质的阴影总是像是由不透明材质投射的一样,因为这是我们的着色器所假设的一个条件。因此,阴影可能会显得很奇怪,直到你意识到你看到的是一个不透明对象所投射的阴影。在方向光生成的阴影情况下,这也可以导致不可见的几何体遮挡阴影。



在不透明渲染模式和剪切渲染模式下,所得到的方向阴影是相同的。


在聚光灯或点光源生成光影的情况下,你会得到一个实心的阴影。


实心的聚光灯阴影。


重构我的阴影


为了把透明度考虑进来,我们必须访问阴影投射着色器渲染通道中的透明度值。这意味着我们需要对反射率纹理进行采样。但是,当使用不透明渲染模式的时候,我们不需要对反射率纹理进行采样。所以我们需要为我们的阴影使用多个着色器变体。


现在我们有两个版本的阴影程序。 一个版本用于立方体阴影贴图,这是点光源所需的,一个用于其他光源类型。现在我们需要混合更多的变体。为了使混合更多的变体更容易,我们将重写My Shadow这个导入文件。我们将为所有变体使用插值器,并创建单个顶点和片段程序。


首先,将Interpolator的定义移出条件块。然后使光向量成为条件。



接下来,写一个新的顶点程序,其中包含两个不同版本的副本。非立方体的代码必须稍微调整以使用新的Interpolator输出。



对片段程序执行相同操作。然后摆脱旧的条件程序



裁剪阴影片段


我们首先处理剪切造成的阴影。 我们通过丢弃片段在阴影中切孔,就像我们在剪切渲染模式中的其他渲染过程中所做的那样。为达到这个目的,我们需要材料的色调、反射率纹理和透明度截止阈值这些信息。在My Shadows的顶部为它们添加相应的变量。



因此,当我们使用剪切渲染模式的时候,我们必须对反射率纹理进行采样。实际上,当我们不使用反射率纹理的透明度值来确定平滑度的时候我们必须这样做。当满足这些条件的时候,我们必须将UV坐标传递给片段程序。 同时如果满足这些条件的话,我们将SHADOWS_NEED_UV定义为1。这样,我们可以方便地使用#if SHADOWS_NEED_UV



将UV坐标添加到顶点输入数据。我们不需要让顶点输入数据成为有条件的。然后有条件地将UV坐标添加到内插值器里面去。



需要的情况下,我们将UV坐标传递到顶点程序中的插值器中去。



将GetAlpha方法从My Lighting复制到My Shadows里面。在这里,纹理是否被采样取决于SHADOWS_NEED_UV。 所以要检查的是SHADOWS_NEED_UV,而不是是否定义了_SMOOTHNESS_ALBEDO。 我对区别进行了标记。



现在我们可以获取片段程序中的透明度值,并在剪切渲染模式下使用它进行裁剪。



要使其实际工作,请将_RENDERING_CUTOUT和_SMOOTHNESS_ALBEDO这两个着色器功能添加到My FirstLighting着色器的阴影投射渲染通道里面。




剪切所形成的阴影,分别在方向光和聚光灯下的效果。


重构MyLighting


在我们继续往下走之前,让我们稍微调整下MyLighting。注意我们如何使用UnityObjectToClipPos来转换My Shadows中的顶点位置。我们可以在My Lighting中使用这个函数,而不是自己执行矩阵乘法。UnityObjectToClipPos函数也执行此乘法,但使用常量值1作为第四个位置坐标,而不是依赖网格数据。



通过网格提供的数据总是1,但着色器的编译器不知道这一点。因此,使用常数更加的有效。从版本5.6开始,当使用UNITY_MATRIX_MVP进行未优化的乘法操作的时候,Unity将给出性能警告。


工程文件下载地址可点击阅读原文获取。


部分阴影


为了能够支持渐变和透明渲染模式的阴影,我们必须将它们的关键字添加到阴影投射渲染通道的着色器功能里面。像其他渲染通道一样,渲染功能现在有四个可能的状态。



这两种模式是半透明的而不是剪切。所以他们的影子也应该是半透明的。让我们在My Shadows中定义一个方便的SHADOWS_SEMITRANSPARENT宏。



现在我们必须调整SHADOWS_NEED_UV的定义,所以它也在半透明阴影的情况下被定义。



抖动


阴影贴图包含着到阻挡光的表面的距离。或者光被阻挡在一定距离之外,或者没有发生阻挡。因此,没有办法指定光被半透明表面部分的阻挡。


我们可以做的,是裁剪阴影表面的一部分。这就是我们在剪切生成的阴影里面所做的事情。但是,这次的裁剪不是基于阈值进行裁剪,我们可以均匀地裁剪片段。举个简单的例子来说,如果一个表面让一半的光通过,我们可以使用棋盘模式剪切每个片段。总的来说,生成的阴影将显示为完整阴影的一半。


我们不总是使用相同的模式。根据透明度值,我们可以使用具有更多孔或是更少孔的模式。如果我们混合这些模式,我们可以创建阴影密度的平滑过渡。基本上,我们只使用两个状态来对梯度进行模拟。这种技术被称为抖动。


Unity包含了一个可以使用的抖动模式图集。它包含16个不同的4×4像素的模式。它从一个完全空的模式开始。接下去的每个模式会填充一个额外的像素,直到有七个被填充。然后,图案被反转和互换,直到所有像素被填充。


Unity使用的抖动模式。


VPOS


要将抖动模式应用到我们的阴影上去,我们必须对它进行采样。我们不能使用网格的UV坐标,因为那些坐标在阴影空间中不均匀。相反,我们需要使用片段的屏幕空间坐标。因为阴影贴图是从光在视野空间的位置开始渲染的,,这将使模式与阴影贴图对齐。


通过添加具有VPOS语义的参数,可以在片段程序中访问片段的屏幕空间位置。这些坐标不是由顶点程序显式输出的,但是图形处理器可以使它们对我们可用。


不幸的是,VPOS语义和SV_POSITION语义不起作用。在一些平台上,他们最终映射到相同的位置语义。所以我们不能同时在Interpolators结构中使用这两个语义。幸运的是,我们只需要在顶点程序中使用SV_POSITION,而VPOS只需要用在片段程序中。所以我们可以为每个程序使用一个单独的结构体。


首先,将Interpolators重命名为InterpolatorsVertex,然后调整MyShadowVertexProgram。不要调整MyShadowFragmentProgram。



然后创建一个新的Interpolators结构体在片段程序中使用。它是另一个结构的副本,当需要半透明阴影的时候,它应该包含UNITY_VPOS_TYPE vpos:VPOS而不是float4positions:SV_POSITION。 UNITY_VPOS_TYPE宏在HLSLSupport中进行定义。它通常是一个float4类型,除了Direct3D 9,Direct3D 9需要的是一个float2类型。



我们在片段程序中需要position 吗?


顶点程序需要输出它的变换位置,但我们不必在我们的片段程序中访问它。 所以技术上来讲我们可以把它从结构体移除去。然而,因为结构体的所有其他字段是有条件的,这可能导致一个空的结构。编译器不能总是处理这些空的情况,所以我们把这个信息保留在那里,以防止错误。


抖动


要访问Unity的抖动模式纹理,请将_DitherMaskLOD变量添加到My Shadows。不同的模式存储在3D纹理的层中,因此它的类型必须是sampler3D而不是sampler2D。



如果我们需要半透明阴影的话,需要在MyShadowFragmentProgram中对此纹理进行采样。这是通过tex3D函数完成的,这个函数需要的是3D坐标。 第三个坐标应该在0-1范围内,并且用于选择3D切片。由于有16个模式,第一个模式的Z坐标为0,第二个模式的坐标为0.0625,第三个模式的坐标为0.128,以此类推。让我们开始总是选择第二个模式。



当应该丢弃片段的时候,抖动纹理的透明度通道为零。所以从中减去一个小的值,并使用它来进行裁剪。



要实际看到一个模式,我们必须对这个模式进行放缩。为了好好看看这个模式,我们将这个模式放大100倍,这是通过将位置乘以0.01来做到的。聚光灯的阴影让我们可以很好地看看这个模式。



在淡入模式的均匀抖动效果。


你可以通过以0.0625的步长增大Z坐标来检查所有16种抖动模式。阴影在0的时候被完全裁剪,在0.9375的时候完全渲染。


https://v.qq.com/txp/iframe/player.html?vid=c1328jap5xh&width=500&height=375&auto=0


对半透明效果的近似


不是使用一个统一的模式,我们必须基于表面的透明度值来选择抖动模式。当达到0.9375的时候,是完全不透明的状态,将透明度值乘以该因子,然后将得到的值用作Z坐标。



基于透明度的抖动。


抖动现在基于表面不透明度的值会发生变化。为了使它看起来更像一个真正的阴影,我们将不得不对模式的大小进行放缩。Unity使用0.25的因子,因此我们也将使用这个因子。



缩放抖动。


这看起来好多了,但它不是完美的。抖动的明显程度取决于阴影贴图的分辨率。分辨率越高,模式越小且越不明显。


抖动在软方向阴影的情况工作的更好。屏幕空间的过滤将抖动片段模糊到模式不再明显的程度。得到的结果是接近实际的半透明阴影效果。



带有抖动的硬方向阴影和软方向阴影。


不幸的是,抖动在视觉上不稳定。当物体移动的时候,你可以得到非常明显的阴影游动的效果。不只是沿着边缘,而是整个阴影都有这个情况!


https://v.qq.com/txp/iframe/player.html?vid=h1328q6e0l0&width=500&height=375&auto=0

阴影游动的效果


在半透明表面上要接收阴影该怎么办?


不幸的是,Unity不支持在半透明表面上进行阴影投射。因此,使用淡入渲染模式或是透明渲染模式的材质不会接收阴影。但是剪切渲染模式可以正常工作。


工程文件下载地址可点击阅读原文获取。


可选的半透明阴影


考虑到半透明阴影的限制,你可能决定不使用它们。你可以通过其网格渲染器组件的“投射阴影”模式来完全禁用一个物体的阴影。但是,它可能是剪切生成的阴影工作正好对一个半透明的物体来说非常适合。举个简单的例子来说,当这个物体的表面占非常显着的部分是完全不透明的时候。因此,让我们可以在两种类型的阴影之间进行选择。


为了支持这个选择,在阴影投射渲染通道里面通过一个新的关键字_SEMITRANSPARENT_SHADOWS来添加一个着色器特性。



在My Shadows中,只有在设置了_SEMITRANSPARENT_SHADOWS着色器关键字的情况下才会定义SHADOWS_SEMITRANSPAREN。



如果没有启用新的着色器功能,那么我们应该回退到剪切生成的阴影处理那里。我们可以通过手动定义_RENDERING_CUTOUT来完成这个操作。



因为新的着色器功能尚未启用,我们现在使用淡入渲染模式或是透明渲染模式的时候,可以得到剪切生成的阴影。


使用淡入渲染模式,得到的剪切生成的阴影。


切换半透明度


为了再次启用半透明阴影,我们必须为我们的自定义着色器UI添加一个选项。 所以添加一个DoSemitransparentShadows方法到MyLightingShaderGUI里面。



我们只需要在使用淡入渲染模式或是透明渲染模式的时候显示这个选项。我们知道在DoRenderingMode里面使用了哪种模式。因此,如果需要的话,在此方法的结尾调用DoSemitransparentShadows。



由于这是一个二进制的选择,我们可以用切换按钮来表示这个选择。因为标签Semitransparent Shadows 比Unity的默认检视器窗口的宽度要宽,我在这里使用了缩写。为了清楚起见,我给了它一个没有缩写的工具提示。



半透明阴影复选框。


与其他关键字一样,会去检查用户是否进行了更改并相应地设置了关键字。



显示阴影的透明度截止阈值


当使用剪切生成的阴影的时候,我们可能想更改透明度截止阈值。目前,透明度截止阈值仅在使用剪切渲染模式的时候会显示在我们的UI中。但是,当不使用半透明阴影的时候,它现在也必须可以在淡入渲染模式和透明渲染模式下访问。我们可以通过在DoSemitransparentShadows中设置shouldShowAlphaCutoff为true来支持这一点。



在需要的时候显示透明度截止阈值。


这个系列的下一篇教程是:延迟渲染。

工程文件下载地址,可点击阅读原文获取。


【版权声明】

原文作者未做权利声明,视为共享知识产权进入公共领域,自动获得授权。


----------------------

今日推荐


使用UGUI绘制多边形雷达图

如何为你的Unity2D游戏增加一些透视效果?


添加小编微信,可享双重福利

1.加入GAD程序猿交流基地

获取行业干货资讯,观看大牛分享直播

2.领取60G独家程序资料,地址在小编朋友圈

包括腾讯内部分享、文章教程、视频教程等全套资料

↓长按添加小编GAD苏苏↓

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存