本文重点:
1、用着色器挖洞
2、使用不同的渲染队列
3、支持半透明材质
4、结合反射和透明度
这是关于渲染的系列教程的第十一部分。之前,我们使着色器能够渲染复杂的材质。但是这些材质一直都是完全不透明的。现在,我们将添加对透明度的支持。
本教程是使用Unity 5.5.0f3制作。
(一些不完整的四边形)
1 抠图渲染
要创建透明材质,我们必须知道每个片段的透明度。此信息通常存储在颜色的Alpha通道中。在我们的例子中,这是主反照率纹理的Alpha通道,以及颜色色调的Alpha通道。
下面是透明度贴图的示例。它是纯白色的纹理,因为它是白色的,所以我们可以完全专注于透明度,而不会受到反照率模式的干扰。
(在黑色背景上的透明度贴图)
将此纹理分配给我们的材质只会使其变为白色。除非你选择将其用作平滑度的源,否则它会忽略Alpha通道。但是,当你使用这种材质选择一个四边形时,你会看到一个大致为圆形的选择轮廓。
(在不透明四边形上展示选中的轮廓)
如何得到选中的轮廓?
Unity 5.5引入了新的选择轮廓的显示方法。以前,你只会看到选定网格的线框。现在,你还可以通过场景视图的Gizmos菜单选择使用轮廓效果。
Unity使用替换的着色器创建轮廓,我们将在后面提到。它采样主要纹理的Alpha通道。在alpha值变为零的位置绘制轮廓。
1.1 确定Alpha值
要检索alpha值,我们可以将GetAlpha函数添加到“My Lighting”包含文件中。像反照率一样,通过将色调和主纹理的alpha值相乘来找到它。
但是,仅在不使用纹理的Alpha通道确定平滑度时,才应使用该纹理。如果不检查,可能会误解数据。
1.2 挖洞
对于不透明的材质,将渲染通过深度测试的每个片段。所有片段都是完全不透明的,并写入深度缓冲区。透明度让这里变得更复杂。
实现透明性的最简单方法是使其保持二进制状态。片段是完全不透明的,或者是完全透明的。如果它是透明的,那么根本就不会渲染。这使得可以在某表面上切孔。
要中止渲染片段,可以使用clip函数。如果此函数的参数为负,则片段将被丢弃。
GPU不会混合其颜色,也不会写入深度缓冲区。如果发生这种情况,我们不必担心所有其他材质特性。因此,尽早clip是最有效的方法。在我们的例子中,那是MyFragmentProgram函数的开始。
我们将使用alpha值来确定是否应该裁剪。由于alpha介于零和一之间,因此我们必须减去一些值使其变为负数。通过减去½,我们将使alpha范围的下半部分为负。这意味着将渲染alpha值至少为½的片段,而所有其他片段将被剪切掉。
(裁切所有alpha 低于0.5的值)
1.3 Cutoff 变量
从alpha减去½不是固定的,我们可以减去任意数字。如果我们从alpha中减去一个较高的值,则会剪切掉较大的范围。因此,该值用作截止阈值。我们先使其可变。向着色器添加Alpha Cutoff属性。
然后将相应的变量添加到“My Lighting”,并在裁剪前从alpha值中减去它,而不再是减去固定的½。
最后,我们还必须将截止值添加到自定义着色器用户界面。标准着色器在反照率线下方显示了cutoff 值,我们也一样。就像我们对“Smoothness”所做的那样,我们将显示一个缩进的滑块。
(Alpha cutoff 滑块)
现在,你可以根据需要调整cutoff 值。也可以对其进行动画处理,例如创建实体化或去实体化效果。
(变化Alpha cutoff值)
着色器编译器将剪辑转换为丢弃指令。这是相关的OpenGL Core代码片段。
这是Direct3D 。
那阴影呢?
在下一个教程中,我们将处理cutout 和半透明材质的阴影。在此之前,你可以使用这些材质关闭对象的阴影。
1.4 渲染模式
clip不是免费的。对于台式机GPU来说还不错,但是使用切片渲染的移动GPU根本不喜欢丢弃片段。因此,仅当我们真正渲染抠图(CutOut)材质时,才应包括clip语句。完全不透明的材质不需要它。为此,让它依赖于新关键字_RENDERING_CUTOUT。
给此关键字添加一个着色器功能,包括基本pass和附加pass。
在我们的自定义UI脚本中,添加RenderingMode枚举,在不透明和抠图渲染之间进行选择。
添加单独的方法以显示渲染模式的一行。我们将使用基于关键字的枚举弹出窗口,就像我们对平滑度源所做的那样。根据_RENDERING_CUTOUT关键字的存在设置模式。显示弹出窗口,如果用户对其进行了更改,请再次设置关键字。
与标准着色器一样,我们将在UI顶部显示渲染模式。
(选择渲染模式)
现在,我们可以在完全不透明和抠图渲染之间切换。但是,即使在不透明模式下,alpha截止滑块也保持可见。理想情况下,仅应在需要时显示它。标准着色器也可以做到这一点。要在DoRenderingMode和DoMain之间进行通信,请添加一个布尔值字段,该字段指示是否应显示Alpha截止值。
1.5 渲染队列
尽管现在我们的渲染模式已完全可用,但是Unity的着色器还有另一件事。他们将cutout 材质放入了不透明材质的不同渲染队列中。不透明的东西首先被渲染,然后是cutout的东西。这样做是因为cutout更加昂贵。首先渲染不透明的对象意味着我们永远不会渲染最终在实体对象之后的剪切对象。
在内部,每个对象都有一个与其队列相对应的数字。默认队列为2000。cutout 队列为2450。首先渲染较低的队列。
你可以使用Queue标记设置着色器遍历的队列。可以使用队列名称,还可以添加偏移量,以更精确地控制对象何时呈现。例如,“ Queue” =“ Geometry + 1”
但是我们的材质没有固定的队列。这取决于渲染模式。因此,我们将使用UI设置自定义渲染队列,而不是使用标记,它会取代着色器的队列。通过在检查器处于调试模式下进行选择,可以找出材质的自定义渲染队列是什么。你能够看到其“Custom Render Queue”字段。它的默认值为-1,表示没有设置自定义值,因此应使用着色器的Queue标记。
(自定义渲染队列)
我们并不真正在乎队列的确切值。在将来的Unity版本中,它们甚至可能会更改。幸运的是,UnityEngine.Rendering命名空间包含RenderQueue枚举,该枚举包含正确的值。因此,我们在UI脚本中也使用该名称空间。
在DoRenderingMode内部检测到更改时,请确定正确的渲染队列。然后,遍历所选材质并更新其队列替代。
1.6 渲染模式tag
另一个细节是RenderType标签。此着色器标记本身不会执行任何操作。这是一个提示,告诉Unity它是哪种着色器。替换着色器使用它来确定是否应渲染对象。
什么是replacement着色器?
它可以否决使用哪种着色器渲染对象。然后,你可以使用这些着色器手动渲染场景。这可以用来创建许多不同的效果。在某些情况下,需要深度缓冲区但无法访问时,Unity可能会使用替换着色器创建深度纹理。再举一个例子,你可以使用着色器替换来查看是否有任何对象在视图中使用cutoff着色器,方法是将它们设置为亮红色或其他颜色。当然,这仅适用于具有适当RenderType标签的着色器。
要调整RenderType标签,我们需要使用Material.SetOverrideTag方法。它的第一个参数是要覆盖的标签。第二个参数是包含标签值的字符串。对于不透明的着色器,我们可以使用默认值,这是通过提供一个空字符串来实现的。对于抠图着色器,它是TransparentCutout。
将材质切换到剪切模式后,现在它将在它的“String Tag Map”列表中获得一个条目,你可以通过debug 检视器查看该条目。
(渲染类型tag)
2 半透明渲染
当想在某个物体上切一个洞时,cutout 渲染就足够了,但是当你需要半透明效果时就不行了。同样,cutout 渲染是针对每个片段的,这意味着边缘会出现锯齿。因为在表面的不透明部分和透明部分之间没有平滑过渡。为了解决这个问题,我们必须增加对另一种渲染模式的支持。此模式将支持半透明。Unity的标准着色器将此模式命名为Fade,因此我们将使用相同的名称。将其添加到我们的RenderingMode枚举中。
在此模式下,我们将使用_RENDERING_FADE关键字。调整DoRenderingMode也可以与此关键字一起使用。
2.1 渲染设置
Fade 模式带有其自己的渲染队列和渲染类型。队列值为3000,这是透明对象的默认值。渲染类型为“Transparent”。
让我们在UI类中定义一个结构来保存每种渲染类型的设置,而不是使DoRenderingMode变得更加复杂。
现在,我们可以为所有渲染类型创建一个静态设置数组。
在DoRenderingMode内部,使用该模式检索正确的设置,然后配置所有材质。
2.2 渲染透明几何体
现在,你可以将材质切换为“Fade”渲染模式。由于我们的着色器尚不支持该模式,因此它将恢复为不透明。但是,使用帧调试器时你会发现有所不同。
当使用不透明或抠图渲染模式时,材质对象将通过Render.OpaqueGeometry方法进行渲染。 但是使用“Fade”渲染模式时则有所不同,它通过Render.TransparentGeometry方法渲染。发生这种情况是因为我们使用了不同的渲染队列。
(不透明与半透明渲染)
如果同时具有不透明对象和透明对象,则将同时调用Render.OpaqueGeometry和Render.TransparentGeometry方法。首先渲染不透明和cut off的几何体,然后渲染透明的几何体。因此,半透明对象永远不会在实体对象之后绘制。
2.3 混合片段
为了使Fade模式起作用,首先需要调整渲染着色器功能。现在,我们支持带有两个关键字的三种模式,分别用于基本pass和附加pass。
在Fade模式下,必须将当前片段的颜色与已经绘制的内容混合在一起。这种混合是由GPU在片段程序之外完成的。它需要片段的alpha值来执行此操作,因此我们需要输出它,而不是输出我们到目前为止一直使用的常量值1.
要创建半透明效果,必须使用不同于用于不透明和cut off 材质的混合模式。与添加pass一样,我们需要将新颜色添加到已经存在的颜色中。但是,又不能简单地将它们加在一起。混合应该取决于我们的alpha值。
当alpha为1时,渲染完全不透明的东西。在那种情况下,应该像往常一样将Blend One Zero用作基础pass,将Blend One one用作附加pass。但是当alpha为零时,我们呈现的内容是完全透明的。如果是这样,我们不需要改变任何事情。然后,两次pass的混合模式必须为Blend Zero One 。如果alpha为¼,那么我们需要Blend 0.25 0.75和Blend 0.25 One之类的东西。
为了实现这个效果,可以使用SrcAlpha和OneMinusSrcAlpha混合关键字。
(半透明的四边形)
在此过程中,这些混合模式仅适用于Fade渲染模式。因此,必须使它们可变。幸运的是,这是可以的。首先为源和目标混合模式添加两个float属性。
由于这些属性取决于渲染模式,因此我们不会在UI中显示它们。如果不使用自定义UI,则可以使用HideInInspector属性将其隐藏。无论如何,我都会添加这些属性。
使用这些float属性代替必须可变的blend关键字。你需要将它们放在方括号内。这是旧的着色器语法,用于配置GPU。我们不需要在我们的顶点和片段程序中访问这些属性。
要控制这些参数,请将两个BlendMode字段添加到我们的RenderingSettings结构中,并适当地对其进行初始化。
在DoRenderingMode内部,需要直接设置材质的_SrcBlend和_DstBlend属性。可以通过Material.SetInt方法来实现。
2.4 深度问题
在Fade模式下使用单个对象时,一切似乎工作正常。但是,当多个半透明对象靠在一起时,会得到怪异的结果。例如,将两个四边形部分重叠,将一个四边形稍微重叠一点。从某些角度看,一个四边形似乎会切掉另一个。
(诡异的结果)
Unity尝试首先绘制最接近相机的不透明对象。这是渲染重叠几何图形的最有效方法。不幸的是,这不适用于半透明的几何体,因为它必须与它背后的任何东西进行混合。因此,必须以其他方式绘制透明的几何图形。首先绘制最远的对象,最后绘制最接近的对象。这就是为什么透明的东西比不透明的东西要贵得多的原因。
为了确定几何图形的绘制顺序,Unity使用其中心的位置。对于相距较远的小物体,此方法效果很好。但是,对于较大的几何图形或靠近放置的平面几何图形,效果并不理想。在这些情况下,更改视角时绘制顺序可能会突然翻转。这可能会导致重叠的半透明对象的外观突然改变。
暂时还没有办法克服此限制,尤其是在考虑相交几何时。但是,它通常并不明显。在我们的例子中,某些DrawCall显然会产生错误的结果。发生这种情况是因为我们的着色器仍会写入深度缓冲区。深度缓冲区是二进制的,并不关心透明度。如果片段没有被裁剪,其深度最终将写入缓冲区。由于半透明对象的绘制顺序并不完美,因此这是不理想的。不可见几何体的深度值最终可能会阻止渲染可见的东西。因此,在使用Fade渲染模式时,必须禁用对深度缓冲区的写入。
2.5 控制 ZWrite
像混合模式一样,我们可以使用属性来控制ZWrite模式。需要使用属性在基本pass中显式设置此模式。加法运算不会写入深度缓冲区,因此不需要更改。
添加一个布尔字段RenderingSettings以指示是否应启用对深度缓冲区的写入。这仅适用于“Opaque”和“Cutout”模式。
再次使用Material.SetInt方法在DoRenderingMode中包含_ZWrite属性。
将我们的材质切换到另一个渲染模式,然后再回到“Fade”模式。尽管半透明对象的绘制顺序仍然可以翻转,但我们在半透明几何体中不再出现意外的孔。
(不再有消失的几何形状了)
3 淡入淡出 VS 透明度
我们创建的半透明渲染模式会根据其alpha值淡出几何图形。请注意,几何图形颜色的全部贡献都会消失。它的漫反射和镜面反射都被淡化了。这就是为什么它被称为Fade模式。
(淡入红色以及白色的高光)
此模式适用于许多效果,但不能正确表示实体半透明表面。例如,玻璃实际上是完全透明的,但也具有清晰的高光和反射。反射光会添加到任何经过的光中。为此,Unity的标准着色器还具有透明渲染模式。因此,我们也还要添加该模式。
透明模式的设置与Fade的设置相同,只是我们必须要能够添加反射并与alpha值无关。因此,其源混合模式必须为1,而不是取决于alpha。
现在,不得不使用另一个关键字,我们用_RENDERING_TRANSPARENT。调整DoRenderingMode,使其可以检测并设置此关键字。
将关键字添加到我们的两个着色器功能指令中。
现在我们必须同时输出Fade和透明模式的alpha值。
将我们的材质切换为“透明”模式将再次使整个四边形可见。因为我们不再基于alpha来调制新颜色,所以四边形将比使用不透明模式时显得更亮。在片段后面添加多少颜色仍由alpha控制。因此,当alpha为1时,它看起来就像一个不透明的表面。
(Adding 代替 fading)
3.1 预乘Alpha
为了使透明度再次起作用,必须手动考虑alpha值。而且我们应该只调整漫反射,而不是镜面反射。可以通过将材质的最终反照率颜色乘以alpha值来实现。
(淡化反照率)
因为我们在GPU进行混合之前先乘以alpha,所以这种技术通常称为预乘alpha混合。许多图像处理应用程序在内部以这种方式存储颜色。纹理也可以包含预乘的alpha颜色。然后它们不需要Alpha通道,因为它们可以存储与与RGB通道关联的Alpha值不同的Alpha值。这样就可以使用相同的数据(例如,火和烟的组合)来变亮和变暗。但是,以这种方式在纹理中存储颜色的缺点是精度下降。
3.2 调整Alpha
如果某些东西既透明又可以反射,我们将看到它背后的一切并且也会反射。在对象的两面都是如此。但是,同一束光不能既被反射,又穿过对象。这再次是节能问题。因此,无论其固有的透明性如何,反射性越强,穿过它的光线越少。
为了表示这一点,我们必须在GPU执行混合之前但在更改反照率之后调整alpha值。如果表面没有反射,则其alpha不变。但是,当它反射所有光线时,其alpha有效地变为1。当我们在片段程序中确定反射率时,可以使用它来调整alpha值。给定原始 a和反射率r,修改后的a变为1-(1-a)(1-r)。
请记住,我们在着色器中使用的是负反射率,因此(1-r)可以用R 表示。然后,可以稍微简化一下公式。1-(1-a)R = 1-(R-a R)= 1-R + a R 。调整反照率颜色后,将此表达式用作新的Alpha值。
结果应该比以前暗一些,以模拟光线从对象背面反弹。
(调整 alpha)
请记住,这是对透明的极大简化,因为没有考虑对象的实际体积,而只考虑了可见表面。
那单向镜呢?
没有真正的单向镜。用于该目的的窗子实际上是双向镜像。这样的窗户非常反光。当一侧的房间非常明亮时,你不会注意到另一侧的暗室发出的光线。但是,当两个房间都被照亮时,你可以在两个方向上看到它。