查看原文
其他

程序丨Unity 渲染基础系列教程(十一):透明度

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


译者:崔嘉艺(milan21)

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


  • 使用着色器剪孔。

  • 使用一个不同的渲染队列。

  • 支持半透明的材质。

  • 合并反射和透明度。


这是关于渲染基础的系列教程的第十一部分。在前面的内容里我们使我们的着色器能够渲染复杂的材质。但是这些材质一直是完全不透明的。现在我们将添加对透明度的支持。


系列回顾:

Unity 渲染教程(一):矩阵

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

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

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

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

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

Unity 渲染教程(七):阴影

Unity 渲染教程(八):反射

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

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


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



图中的一些四边形不完全在那里。


剪切渲染


要创建一个透明的材质,我们必须知道每个片段的透明度。透明度信息最常存储在颜色的透明度通道中。在我们的例子中,我们使用的是主反射率纹理的透明度通道和颜色色调的透明度通道。


这里有一个透明度贴图的例子。 它是一个白色纹理,在透明度通道带有平滑衰退噪声。它是白色的,所以我们可以完全专注于透明度,而不会被反射率的模式所 35 42491 35 14985 0 0 2905 0 0:00:14 0:00:05 0:00:09 2906心。

黑色背景下的透明度贴图。


将这个纹理分配给我们的材质只是使材质变成白色。透明度通道被忽略,除非你选择使用透明度通道作为平滑源。但是当你用这种材质选择一个四边形的时候,你会看到一个大致是圆形的选择轮廓。



在实四边形上的选择轮廓。


我如何能使用选择轮廓?


Unity 5.5引入了一种新的选择高亮方法。以前,你总是看到所选网格的线框。 现在,你还可以通过场景视图的Gizmos菜单选择使用轮廓效果。


Unity使用替换着色器来创建轮廓,稍后我们将提到这个着色器。这个着色器采样主纹理的透明度通道。轮廓会在那些透明度值变为零的地方进行绘制。


确定透明度值


要获取透明度的值,我们可以通过添加一个GetAlpha函数到My Lighting导入文件。像反射率一样,我们通过乘以色调和主纹理的透明度值来找到这个值。



然而,当我们不使用纹理的透明度通道来确定平滑度的时候,我们应该只使用纹理。如果我们没有检查这一点的话,那么我们可能会曲解数据。



剪孔


在不透明的材质情况下,每个通过深度测试的片段都会被渲染。所有片段是完全不透明的,并将结果写入深度缓冲区中。透明度的加入使这一点变得复杂。


实现透明度的最简单的方法是保持二进制。一个片段要么是完全不透明的,要么是完全透明的。如果它是透明的话,那么它根本不会被渲染。这使得可以在表面中剪出一个孔来。


要中止渲染片段,我们可以使用clip函数。 如果这个函数的参数为负,则片段将被丢弃。图形处理器不会混合这个片段的颜色,它的结果不会写入深度缓冲区中去。如果发生这种情况,我们不需要担心材质的所有其他属性。所以最有效的是尽早进行裁剪。在我们的例子中,这个事情放在MyFragmentProgram函数开始的地方。

我们将使用透明度值来确定我们是否应该裁剪。由于透明度值位于零和一之间,我们必须将其减去一些,使其为负。通过减去1/2,我们将使透明度值的范围的下半部分变为负。这意味着至少有1/2透明度值对应的片段将被渲染,而所有其他的片段将被裁剪。




剪切一切透明度值低于 0.5的片段。


可变的截止阈值


从透明度值中减去1/2是我们随便做的一个选择。我们可以减去另一个数字。如果我们从透明度值中减去一个更高的值,那么更大的范围将被剪切。因此,这个值用作截止阈值。让我们先把这个值变成一个变量。首先,在我们的着色器中添加Alpha Cutoff属性。



然后将相应的变量添加到My Lighting 之中,并在剪裁之前从透明度值中减去这个变量的值,而不是减去½。



最后,我们还必须向我们的自定义着色器的UI里面添加截止阈值。标准着色器显示了反射率线下面的截止阈值,所以我们也这样做。 我们将显示一个缩进的滑块,就像我们对平滑度的滑块所做的事情一样。




Alphacutoff 属性的滑块。


现在,你可以根据需要来调整截止阈值。 你还可以对其进行动画处理,例如创建添加材质或去除材质的效果。

Alpha cutoff 属性变化的效果请见:https://zippy.gfycat.com/SelfishCloudyAyeaye.mp4


着色器编译器将剪辑转换为丢弃指令。这里是相关的OpenGL核心代码片段。



这里是相关的Direct3D11核心代码片段版本。



阴影怎么办?


我们将在下一个教程中处理剪切和半透明材质的阴影。在此之前,你可以使用对使用这些材质的对象关闭阴影。


渲染模式


剪裁不是免费的。它在桌面电脑的图形处理器上不那么糟糕,但使用分片渲染的移动图形处理器不喜欢丢弃片段。所以如果我们真的渲染一个剪切材质的话,我们应该只包括clip语句。而完全不透明的材料不需要这一点。为此,让我们依赖一个新的关键字_RENDERING_CUTOUT。



为这个关键字添加一个着色器特性,包括基本渲染通道和附加渲染通道。



在我们的自定义UI脚本中,添加一个RenderingMode枚举,提供不透明和剪切渲染两种选择。



添加单独的方法来给渲染模式显示一条线。我们将使用基于关键字的枚举弹出窗口,就像我们对平滑源所做的事情一样。根据_RENDERING_CUTOUT关键字是否存在来设置模式。显示弹出窗口,如果用户更改渲染模式的话,会再次设置关键字。



和标准着色器一样,我们将在UI的顶部显示渲染模式。




我们现在可以在完全不透明渲染和剪切渲染之间进行切换。但是,即使在不透明渲染模式下,透明度的截止阈值滑块仍然可见。在理想情况下,透明度的截止阈值滑块只应在需要的时候显示。标准着色器也做到了这一点。要在DoRenderingMode和DoMain之间传达此信息,请添加一个布尔字段,指示是否应该显示透明度的截止阈值滑块。



渲染队列


虽然我们的渲染模式现在完全正常工作,但Unity的着色器还做了另外一件事。他们把剪切材质放在与不透明材质不同的渲染队列之中。首先渲染不透明的物体,然后再是那些剪切的东西。这是因为剪切更昂贵。首先渲染不透明的物体意味着我们永远不会渲染那些被不透明物体遮挡住的剪切对象。


在内部,每个物体都有一个与其队列相对应的数字。默认队列为2000。剪切队列为2450。数字较低的队列首先渲染。


你可以使用 Queue tag来设置着色器渲染通道的队列。你可以使用队列名称,还可以添加偏移量,以更精确地控制渲染对象什么时候被渲染。让我们举个简单的例子来说, "Queue" = "Geometry+1"。


但是我们没有固定队列。它取决于渲染模式。 因此,不使用标记,我们将让UI设置一个自定义的渲染队列,这会覆盖着色器的队列。你可以在检视器处于调试模式的时候通过选择材质来找出材质的自定义渲染队列。你将能够看到它的自定义渲染队列字段。其默认值为-1,表示没有设置自定义值,因此应该使用着色器的Queue标记。



自定义渲染队列。


我们真的不在乎队列的确切数字是什么。他们甚至可能在未来的Unity版本中有所改变。 幸运的是,UnityEngine.Rendering命名空间包含了RenderQueueenum,RenderQueueenum会包含正确的值。所以,让我们在我们的UI脚本中使用这个命名空间。



当在DoRenderingMode内部检测到更改的时候,确定正确的渲染队列。然后,遍历所选的材质并更新它的渲染队列。



渲染类型标签


另一个细节是RenderType标签。这个着色器标签本身不做任何事情。这是一个提示,告诉Unity它是什么样的着色器。这用于替换后的着色器,用于确定是否该渲染对象。


什么是替换着色器?


它可以推翻哪些着色器用于渲染对象的规则。然后,你可以使用这些着色器手动渲染场景。这可以用于创建许多不同的效果。当需要深度缓冲区但深度缓冲区不可访问的时候,Unity可能在某些情况下使用替换着色器来创建深度纹理。再举另外一个例子,你可以使用着色器替换来查看是否有任何对象在视图中使用剪切着色器,着色器替换可以使得使用了剪切着色器的对象变成红色或者其它东西。当然,这只适用于具有合适的RenderType标签的着色器。


要调整RenderType标签,我们必须使用Material.SetOverrideTag方法。 Material.SetOverrideTag方法的第一个参数是要覆盖的标签。第二个参数是包含标记值的字符串。对于不透明渲染着色器,我们可以使用默认值,这是通过提供一个空字符串来实现的。对于剪切渲染着色器,这个标记值是TransparentCutout。



在将你的材质切换到剪切模式之后,它将在其字符串标记映射列表中获取一个条目,你可以通过调试检视器来查看这个条目。



渲染类型标签。


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


半透明材质的渲染


当你想在一个物体上剪切一个洞的时候,剪切渲染就足够了,但是当你需要半透明的时候,仅仅有剪切渲染就不行了。另外,剪切渲染是对每个片段进行处理,这意味着边缘将被混叠。在表面的不透明部分和透明部分之间没有平滑过渡。要解决这个问题,我们必须添加对另一种渲染模式的支持。这个模式将支持半透明。Unity的标准着色器命名此模式为淡入,所以我们将使用相同的名称。将这个模式添加到我们的RenderingMode枚举。



我们将对这个模式使用_RENDERING_FADE关键字。


调整DoRenderingMode以使用此关键字。



渲染设置


淡入模式有自己的渲染队列和渲染类型。队列号为3000,这是透明对象的默认值。渲染类型为Transparent。


不是让DoRenderingMode变得更复杂,让我们在UI类中定义一个结构体来保存每个渲染类型的设置。



现在我们可以为所有的渲染类型创建一个静态设置数组。



在DoRenderingMode的内部,使用模式来获取正确的设置,然后配置所有的材质。



渲染透明的几何体


你现在可以将你的材质切换到淡入渲染模式。因为我们的着色器不支持该模式,你的材质将恢复为不透明。但是,在使用帧调试器查看的时候你会注意到有所不同。


当使用不透明渲染模式或是剪切渲染模式的时候,使用我们的材质的对象是由Render.OpaqueGeometry方法进行渲染的。一直是这种情况。当使用淡入渲染模式的时候,情况会有所不同。使用我们的材质的对象是由Render.TransparentGeometry方法进行渲染的。这是因为我们使用了不同的渲染队列。



不透明渲染模式和半透明渲染模式的对比。


如果在视图中同时具有不透明和透明的对象,则将调用Render.OpaqueGeometry和Render.TransparentGeometry方法。首先渲染的是不透明和剪切几何体,然后渲染的是透明几何体。所以被不透明物体遮挡的半透明对象从来不会被渲染。


片段的混合


要使淡入模式可以工作,我们首先要调整我们的渲染着色器的功能。我们现在支持两个关键字、三种模式,对基本渲染通道和附加渲染通道都是如此。



在淡入模式的情况下,我们必须将当前片段的颜色与已经渲染的任何片段的颜色进行混合。这种混合是在我们的片段程序之外由图形处理器完成的。它需要片段的透明度值来做到这一点,所以我们必须输出片段的透明度值,而不一个是常量值-这个常量值我们一直使用到现在。



为了创建半透明的效果,我们必须使用与我们用于不透明材质和剪切材质的混合模式有所不同的混合模式。像是附加渲染通道,我们必须添加新的颜色到已存在的颜色中去。但是,我们不能简单地将它们加在一起。混合的效果应该取决于我们的透明度值。


当透明度值是一的时候,那么我们渲染的东西是完全不透明的。在这种情况下,我们应该像往常一样对于基本渲染通道使用Blend One Zero,对于附加渲染通道使用Blend One。但是当透明度值是零的时候,我们渲染的东西是完全透明的。在这种情况下,我们不应该渲染任何东西。混合模式必须Blend Zero One,对两个渲染通道都是如此。如果透明度值是1/4的话,那么我们需要像Blend 0.25 0.75和Blend 0.25 One e这样的东西。


为了使这成为可能,我们可以使用SrcAlpha和OneMinusSrcAlpha这两个混合关键字。




一个半透明的四边形。


虽然这样可以工作,但是这些混合模式只适合淡入渲染模式。所以我们必须使这些混合模式成为可变化的变量。幸运的是,这是可能的。首先为源混合模式和目标混合模式添加两个float属性。



由于这些属性取决于渲染模式,我们不会在我们的UI中显示这些属性。如果我们不使用自定义UI,我们可以使用HideInInspector属性来隐藏它们。我将添加这些属性。



使用这些float属性来代替那些必须要能变化的混合关键字。你必须把这些float属性放在方括号内。这是旧的着色器语法来对图形处理器做相应的配置。 我们不需要在顶点程序和片段程序中访问这些属性。



要控制这些参数,请将两个BlendMode 字段添加到我们的RenderingSettings 结构体中,并适当地对它们进行初始化。



在DoRenderingMode之中,我们必须直接设置材质的_SrcBlend和_DstBlend属性。我们可以通过Material.SetInt方法来做到这一点。



深度引起的麻烦


当在淡入模式下对单个对象进行工作的时候,一切似乎都能工作正常。但是,当你有多个半透明的对象靠近在一起的时候,你可能会得到奇怪的结果。举个简单的例子来说,部分重叠的两个四边形,将一个四边形稍微放置在另一个四边形之上。从一些视角看,其中一个四边形似乎切掉了另一个四边形的一部分。



奇怪的结果。


Unity会首先绘制最靠近相机的不透明物体。这是渲染重叠几何体的最有效的方式。不幸的是,这种方法不适用于半透明的几何体,因为它必须与其后面的任何东西进行混合。因此,透明的几何体必须以相反的方式绘制。首先绘制离相机最远的对象,最后绘制最近的对象。这就是为什么渲染透明的东西比渲染不透明的东西要更昂贵的原因。


要确定几何体的绘制顺序,Unity使用几何体中心的位置。这对于相距很远的小物体来说工作得很好。但是对于大型几何体,或者靠近在一起的扁平几何体来说,这种方法不是那么好。在这些情况下,在更改视图角度的时候,绘制顺序可能会突然翻转。这可能会导致重叠的半透明对象的外观突然有改变。


没有办法解决这个限制,特别是在考虑相交的几何体的时候。然而,这种瑕疵往往不明显。但在我们的情况下,某些渲染命令会产生明显错误的结果。这是因为我们的着色器仍然会对深度缓冲区进行写入。深度缓冲区是二进制的,不关心透明度。如果一个片段未被裁剪的话,那么它的深度值最终会写入深度缓冲区中。因为半透明对象的绘制顺序不是完美的,这种方法是不可取的。不可见的几何体的深度值可能会阻止其他可见的物体被渲染。因此,当使用淡入渲染模式的时候,我们必须禁用对深度缓冲区的写入。


控制ZWrite


像混合模式一样,我们可以使用属性来控制ZWrite模式。我们需要使用属性在基本渲染通道中显式的设置这个模式。附加渲染通道不会写入深度缓冲区,因此不需要更改。



添加一个布尔字段RenderingSettings来指示是否应启用写入深度缓冲区。这只适用于不透明渲染模式和剪切渲染模式。



在DoRenderingMode之中中包含_ZWrite属性,同样使用的是Material.SetIntmethod方法。



将我们的材质切换到另外一种渲染模式,然后切回到淡入模式。 虽然半透明对象的绘制顺序仍然可以翻转,我们不再会在我们的半透明几何体中看到意外的孔出现。



不再有消失的几何体了。


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


淡入vs透明度


我们创建的半透明渲染模式基于其透明度值来淡入几何。请注意,几何体颜色的整体贡献都是做了淡入处理的。它的漫反射和镜面高光反射都做了淡入处理。这就是为什么它被称为淡入模式的原因。



在白色高光情况下做了淡入处理的红色。


这个模式适用于许多效果,但它不能正确地表示实心半透明的表面。举个简单的例子来说,玻璃实际上是完全透明的,但它也有明显的亮点和反射。反射后的光被添加到任何通过的光里面。为了支持这一点,Unity的标准着色器也有一个透明渲染模式。所以,让我们添加这个模式。



透明模式的设置与淡入模式的设置相同,除了我们必须能够不考虑透明度的值来添加反射以外。因此,透明模式的源混合模式必须是一而不是取决于透明度的值。



我们必须使用另一个关键字,在这种情况下关键字为_RENDERING_TRANSPARENT。 调整DoRenderingMode,以便它可以检测和设置此关键字。



将这个关键字添加到我们的两个着色器功能指令中。



现在我们必须在淡入模式和透明模式中输出透明度值。



将我们的材质切换到透明模式以后将再次使得整个四边形可见。因为我们不再基于透明度来调制新颜色,所以四边形将比使用不透明模式的时候更亮。片段后面会添加多少颜色仍然由透明度值进行控制。所以当透明度值是一的时候,它看起来就像是一个不透明的表面。



添加而不是淡入的效果。


预乘透明度值


为了使透明度能够再次工作,我们必须手动调整透明度值。我们应该只调整漫反射,而不要影响镜面高光反射。我们可以通过将材质的最终反射率颜色乘以透明度值来做到这一点。




淡入的反射率。因为我们是在图形处理器进行混合之前乘以透明度值,所以这种技术通常称为预乘透明度混合。许多图像处理应用程序内部以这种方式存储颜色。纹理也可以包含预乘透明度值得颜色。这样的话,他们或是不需要一个透明度通道,或是可以存储一个与相关联的RGB通道不同的透明度值。这将使得可以使用相同的数据(比如说是火和烟的组合)分别进行变亮和变暗处理。但是,在纹理中存储颜色有一个缺点就是会造成精度的损失。


调整透明度值


如果一个物体既透明又反光,我们将看到这个物体背后的东西和它发生的反射。这在对象的两侧都是正确的。但同样的光不能同时被反射和通过物体。这再次是能量守恒的问题。所以一个物体反射的光越多,那么就越少的光能够通过它,无论其固有的透明度是多少。


为了表示这一点,我们必须在图形处理器执行混合之前,在我们改变反射率之后调整透明度值。如果表面没有反射的话,它的透明度值不变。但当它反射所有的光的时候,它的透明度值会变成一。当我们确定片段程序中的反射率的时候,我们可以使用它来调整透明度值。 给定原始透明度值a和反射率r,修正后的透明度值变为1-(1-a)(1-r)。


请记住,我们在我们的着色器中使用一个负反射率,(1-r)可以用R来表示。 然后我们可以稍微简化公式。 1-(1-a)R = 1-(R-aR)= 1-R + aR。在调整反射率颜色后,使用这个表达式作为新的透明度值。



得到的结果应该比以前的结果稍微更暗一些,用来模拟光反弹从我们的对象的背面反弹出去的效果。



调整后的透明度值。


请记住,这是一个大致简化的透明度,因为我们没有把对象的实际体积考虑进来,只把可见表面考虑了进来。


单向镜怎么处理?


没有真正的单向镜。用于该目的的窗户实际上是双向镜。这种窗户非常反光。 当一侧的房间非常明亮的时候,你不会注意到来自另一侧的黑暗房间的光。但是,当两个房间同样亮的时候,你将能够看到光在这个窗户的两个方向通过。


这个系列的下一篇教程是:半透明材质的阴影。


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


【版权声明】

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


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

今日推荐


10月资料包、资料库下载

使用UGUI绘制多边形雷达图

实时渲染的圣经!《RTR3》 全书知识点总览


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

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

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

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

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

 

↓长按添加小编GAD苏苏↓

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

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