Rendering 11
Transparency
- Cut holes with a shader.
- Use a different render queue.
- Support semitransparent materials.
- Combine reflections and transparency.
This is the eleventh part of a tutorial series about rendering. Previously, we made our shader capable of rendering complex materials. But these materials have always been fully opaque. Now we'll add support for transparency.
这是渲染教程系列的第十一部分。之前,我们让着色器能够渲染复杂的材质。但这些材质始终是完全不透明的。现在我们将添加对透明度的支持。
This tutorial was made with Unity 5.5.0f3.
本教程基于 Unity 5.5.0f3 制作。

有些四边形不是完全的。
Cutout Rendering 剔除渲染
To create a transparent material, we have to know the transparency of each fragment. This information is most often stored in the alpha channel of colors. In our case, that's the alpha channel of the main albedo texture, and the alpha channel of the color tint.
要创建透明材质,我们必须知道每个片段的透明度。此信息最常存储在颜色的 alpha 通道中。在我们的例子中,这就是主反照率纹理的 alpha 通道和颜色色调的 alpha 通道。
Here is an example transparency map. It's a solid white texture with fading smooth noise in the alpha channel. It's white so we can fully focus on the transparency, without being distracted by an albedo pattern.
以下是一个透明度贴图的示例。它是一个纯白色的纹理,在 alpha 通道中带有逐渐消失的平滑噪声。它是白色的,这样我们就可以完全专注于透明度,而不受反照率图案的干扰。

黑色背景上的透明贴图。
Assigning this texture to our material just makes it white. The alpha channel is ignored, unless you chose to use it as the smoothness source. But when you select a quad with this material, you'll see a mostly-circular selection outline.
将此纹理赋给我们的材质只会使其变为白色。alpha 通道被忽略,除非你选择将其用作平滑度源。但当你选择一个带有此材质的四边形时,你会看到一个大致呈圆形的选中轮廓。

在实心四边形上的选中轮廓。
Determing the Alpha Value
确定 Alpha 值
To retrieve the alpha value, we can use add a GetAlpha
function to the My Lighting include file. Like albedo, we find it by multiplying the tint and main texture alpha values.
若要获取 Alpha 值,我们可以在 `include` 文件中添加一个 ` GetAlpha
` 函数。与反照率类似,我们通过将着色和主纹理的 Alpha 值相乘来获得它。
float GetAlpha (Interpolators i) { return _Tint.a * tex2D(_MainTex, i.uv.xy).a; }
However, we should only use the texture when we're not using its alpha channel to determine the smoothness. If we didn't check for that, we could be misinterpreting the data.
但是,只有当我们不使用纹理的 alpha 通道来确定平滑度时,才应该使用该纹理。如果对此不加检查,可能会误解数据。
float GetAlpha (Interpolators i) { float alpha = _Tint.a; #if !defined(_SMOOTHNESS_ALBEDO) alpha *= tex2D(_MainTex, i.uv.xy).a; #endif return alpha; }
Cutting Holes 打孔
In the case of opaque materials, every fragment that passes its depth test is rendered. All fragments are fully opaque and write to the depth buffer. Transparency complicates this.
不透明材质的每个通过深度测试的片段都会被渲染。所有片段都是完全不透明的,并写入深度缓冲区。透明度使问题复杂化。
The simplest way to do transparency is to keep it binary. Either a fragment is fully opaque, or it's fully transparent. If it is transparent, then it's simply not rendered at all. This makes it possible to cut holes in surfaces.
实现透明度最简单的方法是将其保持为二元。一个片段要么完全不透明,要么完全透明。如果它是透明的,那么它根本就不会被渲染。这使得在表面上切割孔成为可能。
To abort rendering a fragment, we can use the clip
function. If the argument of this function is negative, then the fragment will be discarded. The GPU won't blend its color, and it won't write to the depth buffer. If that happens, we don't need to worry about all the other material properties. So it's most efficient to clip as early as possible. In our case, that's at the beginning of the MyFragmentProgram
function.
要中止片段渲染,我们可以使用 clip
函数。如果此函数的参数为负数,则该片段将被丢弃。GPU 不会混合其颜色,也不会写入深度缓冲区。如果发生这种情况,我们无需担心所有其他材质属性。因此,尽早裁剪效率最高。在我们的案例中,那是在 MyFragmentProgram
函数的开头。
We'll use the alpha value to determine whether we should clip or not. As alpha lies somewhere in between zero and one, we'll have to subtract something to make it negative. By subtracting ½, we'll make the bottom half of the alpha range negative. This means that fragments with an alpha value of at least ½ will be rendered, while all others will be clipped.
我们将使用 alpha 值来决定是否裁剪。由于 alpha 值介于 0 和 1 之间,我们必须减去一个值才能使其变为负数。通过减去 ½,我们将使 alpha 值下半部分变为负数。这意味着 alpha 值至少为 ½ 的片段将被渲染,而所有其他片段将被裁剪。
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
float alpha = GetAlpha(i);
clip(alpha - 0.5);
…
}

裁剪所有 alpha 值低于 0.5 的部分。
Variable Cutoff 可变截止
Subtracting ½ from alpha is arbitrary. We could've subtracted another number instead. If we subtract a higher value from alpha, then a large range will be clipped. So this value acts as a cutoff threshold. Let's make it variable. First, add an Alpha Cutoff property to our shader.
从 alpha 中减去 ½ 是任意的。我们也可以减去另一个数字。如果我们从 alpha 中减去一个更高的值,那么一个大的范围将被裁剪。因此,这个值充当了一个截止阈值。让我们让它可变。首先,将一个 Alpha Cutoff 属性添加到我们的着色器中。
Properties { … _AlphaCutoff ("Alpha Cutoff", Range(0, 1)) = 0.5 }
Then add the corresponding variable to My Lighting and subtract it from the alpha value before clipping, instead of ½.
然后将相应的变量添加到 My Lighting 中,并在裁剪之前从 alpha 值中减去它,而不是 ½。
float _AlphaCutoff; … float4 MyFragmentProgram (Interpolators i) : SV_TARGET { float alpha = GetAlpha(i); clip(alpha - _AlphaCutoff); … }
Finally, we also have to add the cutoff to our custom shader UI. The standard shader shows the cutoff below the albedo line, so we'll do that as well. We'll show an indented slider, just like we do for Smoothness.
最后,我们还需要将截止值添加到自定义着色器 UI 中。标准着色器在反照率线下方显示截止值,所以我们也会这样做。我们将显示一个缩进的滑块,就像我们为 Smoothness 所做的那样。
void DoMain () { GUILayout.Label("Main Maps", EditorStyles.boldLabel); MaterialProperty mainTex = FindProperty("_MainTex"); editor.TexturePropertySingleLine( MakeLabel(mainTex, "Albedo (RGB)"), mainTex, FindProperty("_Tint") ); DoAlphaCutoff(); … } void DoAlphaCutoff () { MaterialProperty slider = FindProperty("_AlphaCutoff"); EditorGUI.indentLevel += 2; editor.ShaderProperty(slider, MakeLabel(slider)); EditorGUI.indentLevel -= 2; }

Now you can adjust the cutoff as you like. You could also animate it, for example to create a materializing or de-materializing effect.
现在你可以根据自己的喜好调整截止值。你也可以将其动画化,例如创建一种实化或虚化效果。
The shader compiler converts a clip to a discard instruction. Here's the relevant OpenGL Core code fragment.
着色器编译器将 clip 转换为 discard 指令。以下是相关的 OpenGL 核心代码片段。
u_xlat10_0 = texture(_MainTex, vs_TEXCOORD0.xy); u_xlat1.xyz = u_xlat10_0.xyz * _Tint.xyz; u_xlat30 = _Tint.w * u_xlat10_0.w + (-_AlphaCutoff); u_xlatb30 = u_xlat30<0.0; if((int(u_xlatb30) * int(0xffffffffu))!=0){discard;}
And here it is for Direct3D 11.
Direct3D 11 中的情况也是如此。
0: sample r0.xyzw, v1.xyxx, t0.xyzw, s1 1: mul r1.xyz, r0.xyzx, cb0[4].xyzx 2: mad r0.w, cb0[4].w, r0.w, -cb0[9].x 3: lt r0.w, r0.w, l(0.000000) 4: discard_nz r0.w
Rendering Mode 渲染模式
Clipping doesn't come for free. It isn't that bad for desktop GPUs, but mobile GPUs that use tiled rendering don't like to discard fragments at all. So we should only include the clip statement if we're really rendering a cutout material. Fully opaque materials don't need it. To do this, let's make it dependent on a new keyword, _RENDERING_CUTOUT.
剪裁不是免费的。对于桌面 GPU 来说还不太糟糕,但使用平铺渲染的移动 GPU 根本不喜欢丢弃片段。所以我们只在真正渲染镂空材质时才应该包含剪裁语句。完全不透明的材质不需要它。为此,我们将其依赖于一个新的关键字 _RENDERING_CUTOUT 。
float alpha = GetAlpha(i); #if defined(_RENDERING_CUTOUT) clip(alpha - _AlphaCutoff); #endif
Add a shader feature for this keyword, both to the base pass and the additive pass.
为该关键字添加着色器功能,包括基础通道和叠加通道。
#pragma shader_feature _RENDERING_CUTOUT #pragma shader_feature _METALLIC_MAP
In our custom UI script, add a RenderingMode
enumeration, offering a choice between opaque and cutout rendering.
在我们的自定义 UI 脚本中,添加一个 RenderingMode
枚举,提供不透明和镂空渲染两种选择。
enum RenderingMode { Opaque, Cutout }
Add a separate method to display a line for the rendering mode. We'll use an enumeration popup based on the keyword, like we do for the smoothness source. Set the mode based on the existence of the _RENDERING_CUTOUT keyword. Show the popup, and if the user changes it, set the keyword again.
添加一个单独的方法来显示渲染模式的一行。我们将使用基于关键字的枚举弹窗,就像我们处理平滑度来源那样。根据 _RENDERING_CUTOUT 关键字的存在来设置模式。显示弹窗,如果用户更改了它,则再次设置关键字。
void DoRenderingMode () { RenderingMode mode = RenderingMode.Opaque; if (IsKeywordEnabled("_RENDERING_CUTOUT")) { mode = RenderingMode.Cutout; } EditorGUI.BeginChangeCheck(); mode = (RenderingMode)EditorGUILayout.EnumPopup( MakeLabel("Rendering Mode"), mode ); if (EditorGUI.EndChangeCheck()) { RecordAction("Rendering Mode"); SetKeyword("_RENDERING_CUTOUT", mode == RenderingMode.Cutout); } }
Like the standard shader, we'll show the rendering mode at the top of our UI.
和标准着色器一样,我们会在 UI 顶部显示渲染模式。
public override void OnGUI ( MaterialEditor editor, MaterialProperty[] properties ) { this.target = editor.target as Material; this.editor = editor; this.properties = properties; DoRenderingMode(); DoMain(); DoSecondary(); }

We can now switch between fully opaque and cutout rendering. However, the alpha cutoff slider remains visible, even in opaque mode. Ideally, it should only be shown when needed. The standard shader does this as well. To communicate this between DoRenderingMode
and DoMain
, add a boolean field that indicated whether the alpha cutoff should be shown.
我们现在可以在完全不透明渲染和镂空渲染之间切换。然而,alpha 截止滑块在不透明模式下仍然可见。理想情况下,它应该只在需要时显示。标准着色器也实现了这一点。为了在 DoRenderingMode
和 DoMain
之间进行通信,添加一个布尔字段来指示是否应显示 alpha 截止。
bool shouldShowAlphaCutoff;
…
void DoRenderingMode () {
RenderingMode mode = RenderingMode.Opaque;
shouldShowAlphaCutoff = false;
if (IsKeywordEnabled("_RENDERING_CUTOUT")) {
mode = RenderingMode.Cutout;
shouldShowAlphaCutoff = true;
}
…
}
void DoMain () {
…
if (shouldShowAlphaCutoff) {
DoAlphaCutoff();
}
…
}
Rendering Queue 渲染队列
Although our rendering modes are now fully functional, there is another thing that Unity's shaders do. They put cutout materials in a different render queue that opaque materials. Opaque things are rendered first, followed by the cutout stuff. This is done because clipping is more expensive. Rendering opaque objects first means that we'll never render cutout objects that end up behind solid objects.
尽管我们的渲染模式现在已经完全可用,但 Unity 的着色器还有另一个作用。它们将剪切材质放入与不透明材质不同的渲染队列中。不透明物体会首先渲染,其次是剪切物体。这样做是因为剪裁的成本更高。首先渲染不透明物体意味着我们永远不会渲染最终位于实体物体后面的剪切物体。
Internally, each object has a number that corresponds with its queue. The default queue is 2000. The cutout queue is 2450. Lower queues are rendered first.
在内部,每个对象都有一个与其队列对应的数字。默认队列是 2000。剪切队列是 2450。较低的队列会首先渲染。
You can set the queue of a shader pass using the Queue tag. You can use the queue names, and also add an offset for more precise control over when objects get rendered. For example, "Queue" = "Geometry+1"
您可以使用 Queue 标签设置着色器通道的队列。您可以使用队列名称,也可以添加一个偏移量,以更精确地控制何时渲染对象。例如, "Queue" = "Geometry+1"
But we don't have a fixed queue. It depends on the rendering mode. So instead of using the tag, we'll have our UI set a custom render queue, which overrules the shader's queue. You can find out what the custom render queue of a material is by selecting it while the inspector is in debug mode. You'll be able to see its Custom Render Queue field. Its default value is −1, which indicates that there is no custom value set, so the shader's Queue tag should be used.
但我们没有固定的队列。它取决于渲染模式。因此,我们不会使用 RenderQueue 标签,而是让我们的 UI 设置一个自定义的渲染队列,该队列会覆盖着色器的队列。你可以通过在调试模式下选择材质来查看其自定义渲染队列。你将能够看到它的 Custom Render Queue 字段。它的默认值是 -1,表示没有设置自定义值,因此应该使用着色器的 RenderQueue 标签。

We don't really care what the exact number of a queue is. They might even change in future Unity versions. Fortunately, the UnityEngine.Rendering
namespace contains the RenderQueue
enum, which contains the correct values. So let's use that namespace in our UI script.
我们并不关心队列的具体编号。它们甚至可能在未来的 Unity 版本中发生变化。幸运的是,` UnityEngine.Rendering
` 命名空间包含 ` RenderQueue
` 枚举,其中包含了正确的值。因此,让我们在 UI 脚本中使用该命名空间。
using UnityEngine; using UnityEngine.Rendering; using UnityEditor; public class MyLightingShaderGUI : ShaderGUI { … }
When a change is detected inside DoRenderingMode
, determine the correct render queue. Then, iterate through the selected materials and update their queue overrides.
当在 DoRenderingMode
中检测到更改时,确定正确的渲染队列。然后,遍历选定的材质并更新它们的队列覆盖。
if (EditorGUI.EndChangeCheck()) { RecordAction("Rendering Mode"); SetKeyword("_RENDERING_CUTOUT", mode == RenderingMode.Cutout); RenderQueue queue = mode == RenderingMode.Opaque ? RenderQueue.Geometry : RenderQueue.AlphaTest; foreach (Material m in editor.targets) { m.renderQueue = (int)queue; } }
Render Type Tag 渲染类型标签
Another detail is the RenderType tag. This shader tags doesn't do anything by itself. It is a hint that tells Unity what kind of shader it is. This is used by replacement shaders to determine whether objects should be rendered or not.
另一个细节是 RenderType 标签。这个着色器标签本身不做任何事情。它是一个提示,告诉 Unity 这是哪种着色器。替换着色器就是利用这一点来决定对象是否应该被渲染。
To adjust the RenderType tag, we have to use the Material.SetOverrideTag
method. Its first parameter is the tag to override. The second parameter is the string containing the tag value. For opaque shaders, we can use the default, which is accomplished by providing an empty string. For cutout shaders, it's TransparentCutout.
为了调整 RenderType 标签,我们必须使用 Material.SetOverrideTag
方法。它的第一个参数是要覆盖的标签。第二个参数是包含标签值的字符串。对于不透明着色器,我们可以使用默认值,这通过提供一个空字符串来实现。对于裁剪着色器,它是 TransparentCutout 。
RenderQueue queue = mode == RenderingMode.Opaque ? RenderQueue.Geometry : RenderQueue.AlphaTest; string renderType = mode == RenderingMode.Opaque ? "" : "TransparentCutout"; foreach (Material m in editor.targets) { m.renderQueue = (int)queue; m.SetOverrideTag("RenderType", renderType); }
After switching your material to cutout mode, it will now get an entry in its String Tag Map list, which you can view via the debug inspector.
将材质切换到裁剪模式后,它将在其 String Tag Map 列表中获得一个条目,您可以通过调试检查器查看该条目。

Semitransparent Rendering
半透明渲染
Cutout rendering is sufficient when you want to cut a hole into something, but not when you desire semi-transparency. Also, cutout rendering is per fragment, which means that the edges will be aliased. There is no smooth transition between opaque and transparent parts of the surface. To solve this, we have to add support for another rendering mode. This mode will support semi-transparency. Unity's standard shaders name this mode Fade, so we'll use the same name. Add it to our RenderingMode
enumeration.
镂空渲染足以在物体上切出洞,但不能实现半透明。此外,镂空渲染是基于片段的,这意味着边缘会有锯齿。表面不透明和透明部分之间没有平滑过渡。为了解决这个问题,我们必须添加对另一种渲染模式的支持。此模式将支持半透明。Unity 的标准着色器将此模式命名为 Fade ,所以我们也使用相同的名称。将其添加到我们的 RenderingMode
枚举中。
enum RenderingMode { Opaque, Cutout, Fade }
We'll use the _RENDERING_FADE keyword for this mode. Adjust DoRenderingMode
to work with this keyword as well.
我们将为此模式使用 _RENDERING_FADE 关键字。调整 DoRenderingMode
以便与此关键字配合使用。
void DoRenderingMode () { RenderingMode mode = RenderingMode.Opaque; shouldShowAlphaCutoff = false; if (IsKeywordEnabled("_RENDERING_CUTOUT")) { mode = RenderingMode.Cutout; shouldShowAlphaCutoff = true; } else if (IsKeywordEnabled("_RENDERING_FADE")) { mode = RenderingMode.Fade; } … if (EditorGUI.EndChangeCheck()) { RecordAction("Rendering Mode"); SetKeyword("_RENDERING_CUTOUT", mode == RenderingMode.Cutout); SetKeyword("_RENDERING_FADE", mode == RenderingMode.Fade); … } }
Rendering Settings 渲染设置
Fade mode comes with its own render queue and render type. The queue number is 3000, which is the default for transparent objects. The render type is Transparent.
Fade 模式有其自己的渲染队列和渲染类型。队列号为 3000,这是透明对象的默认值。渲染类型为 Transparent 。
Instead of making DoRenderingMode
more complex, let's define a struct inside our UI class to hold the settings per rendering type.
与其让 DoRenderingMode
变得更复杂,不如在 UI 类中定义一个结构体,用于存储每种渲染类型的设置。
enum RenderingMode { Opaque, Cutout, Fade } struct RenderingSettings { public RenderQueue queue; public string renderType; }
Now we can create a static settings array for all of our rendering types.
现在我们可以为所有渲染类型创建静态设置数组。
struct RenderingSettings { public RenderQueue queue; public string renderType; public static RenderingSettings[] modes = { new RenderingSettings() { queue = RenderQueue.Geometry, renderType = "" }, new RenderingSettings() { queue = RenderQueue.AlphaTest, renderType = "TransparentCutout" }, new RenderingSettings() { queue = RenderQueue.Transparent, renderType = "Transparent" } }; }
Inside DoRenderingMode
, use the mode to retrieve the correct settings, then configure all materials.
在 DoRenderingMode
中,使用该模式检索正确的设置,然后配置所有材质。
if (EditorGUI.EndChangeCheck()) { RecordAction("Rendering Mode"); SetKeyword("_RENDERING_CUTOUT", mode == RenderingMode.Cutout); SetKeyword("_RENDERING_FADE", mode == RenderingMode.Fade);// RenderQueue queue = mode == RenderingMode.Opaque ?// RenderQueue.Geometry : RenderQueue.AlphaTest;// string renderType = mode == RenderingMode.Opaque ?// "" : "TransparentCutout";RenderingSettings settings = RenderingSettings.modes[(int)mode]; foreach (Material m in editor.targets) { m.renderQueue = (int)settings.queue; m.SetOverrideTag("RenderType", settings.renderType); } }
Rendering Transparent Geometry
渲染透明几何体
You're now able to switch your material to Fade rendering mode. Because our shader does not support that mode yet, it will revert to opaque. However, you'll notice a difference when using the frame debugger.
您现在可以将材质切换到 Fade 渲染模式。由于我们的着色器尚不支持该模式,因此它将恢复为不透明。但是,您会注意到在使用帧调试器时有所不同。
When using Opaque or Cutout rendering mode, objects using our material are rendered by the Render.OpaqueGeometry
method. This has always been the case. But it's different when using Fade
rendering mode. Then they're rendered by the Render.TransparentGeometry
method. This happens because we're using a different render queue.
当使用 Opaque 或 Cutout 渲染模式时,使用我们材质的对象通过 Render.OpaqueGeometry
方法进行渲染。这向来如此。但当使用 Fade
渲染模式时则不同。那时它们通过 Render.TransparentGeometry
方法进行渲染。出现这种情况是因为我们使用了不同的渲染队列。


不透明与半透明渲染。
If you have both opaque and transparent objects in view, both the Render.OpaqueGeometry
and the Render.TransparentGeometry
methods will be invoked. The opaque and cutout geometry is rendered first, followed by the transparent geometry. So semitransparent objects are never drawn behind solid objects.
如果视图中同时存在不透明对象和透明对象,则会同时调用 Render.OpaqueGeometry
和 Render.TransparentGeometry
方法。不透明和镂空几何体首先渲染,接着是透明几何体。因此,半透明对象永远不会绘制在实心对象后面。
Blending Fragments 混合片段
To make Fade mode work, we first have to adjust our rendering shader feature. We now support three modes with two keywords, both for the base and additive pass.
为使 Fade 模式正常工作,我们首先要调整渲染着色器功能。现在我们支持三种模式,通过两个关键字控制,基底通道和叠加通道均是如此。
#pragma shader_feature _ _RENDERING_CUTOUT _RENDERING_FADE
In the case of Fade mode, we have to blend the color of our current fragment with whatever's already been drawn. This blending is done by the GPU, outside of our fragment program. It needs the fragment's alpha value to do this, so we have to output it, instead of the constant value – one – that we've used until now.
在 Fade 模式下,我们必须将当前片段的颜色与已绘制的任何内容混合。这种混合由 GPU 在我们的片段程序之外完成。它需要片段的 alpha 值来执行此操作,因此我们必须输出它,而不是我们一直使用的常量值——一。
color.rgb += GetEmission(i); #if defined(_RENDERING_FADE) color.a = alpha; #endif return color;
To create a semitransparent effect, we have to use a different blend mode than the one we use for opaque and cutout materials. Like with the additive pass, we have to add the new color to the already existing color. However, we can't simply add them together. The blend should depend on our alpha value.
要创建半透明效果,我们必须使用与不透明和镂空材质不同的混合模式。与附加通道一样,我们必须将新颜色添加到已有的颜色中。然而,我们不能简单地将它们相加。混合应该取决于我们的 alpha 值。
When alpha is one, then we're rendering something that's fully opaque. In that case, we should use Blend One Zero
for the base pass, and Blend One One
for the additive pass, as usual. But when alpha is zero, what we're rendering is fully transparent. In that case, we shouldn't change a thing. Then blend mode has to be Blend Zero One
for both passes. And if alpha were ¼, then we'd need something like Blend 0.25 0.75
and Blend 0.25 One
.
当 alpha 为 1 时,我们渲染的是完全不透明的物体。在这种情况下,我们基础通道应使用 Blend One Zero
,叠加通道照常使用 Blend One One
。但当 alpha 为零时,我们渲染的物体是完全透明的。在这种情况下,我们不应该改变任何东西。那么两个通道的混合模式都必须是 Blend Zero One
。如果 alpha 是¼,那么我们将需要类似于 Blend 0.25 0.75
和 Blend 0.25 One
的东西。
To make this possible, we can use the SrcAlpha
and OneMinusSrcAlpha
blend keywords.
为了实现这一点,我们可以使用 SrcAlpha
和 OneMinusSrcAlpha
混合关键字。
Pass { Tags { "LightMode" = "ForwardBase" } Blend SrcAlpha OneMinusSrcAlpha … } Pass { Tags { "LightMode" = "ForwardAdd" } Blend SrcAlpha One ZWrite Off … }

While this works, these blend modes are only appropriate for the Fade rendering mode. So we have to make them variable. Fortunately, this is possible. Begin by adding two float properties for the source and destination blend modes.
虽然这可行,但这些混合模式只适用于 Fade 渲染模式。所以我们必须使它们可变。幸运的是,这是可行的。首先为源和目标混合模式添加两个浮点属性。
Properties { … _SrcBlend ("_SrcBlend", Float) = 1 _DstBlend ("_DstBlend", Float) = 0 }
As these properties depend on the rendering mode, we're not going to show them in our UI. If we weren't using a custom UI, we could've hidden them using the HideInInspector attribute. I'll add those attributes anyway.
由于这些属性取决于渲染模式,我们不会在 UI 中显示它们。如果我们不使用自定义 UI,我们可以使用 HideInInspector 属性隐藏它们。我还是会添加这些属性。
[HideInInspector] _SrcBlend ("_SrcBlend", Float) = 1 [HideInInspector] _DstBlend ("_DstBlend", Float) = 0
Use these float properties in place of the blend keywords that have to be variable. You'll have to put them inside square brackets. This is old shader syntax, to configure the GPU. We don't need to access these properties in our vertex and fragment programs.
使用这些浮点属性代替必须可变的混合关键字。你需要将它们放在方括号中。这是旧的着色器语法,用于配置 GPU。我们不需要在顶点和片段程序中访问这些属性。
Pass { Tags { "LightMode" = "ForwardBase" } Blend [_SrcBlend] [_DstBlend] … } Pass { Tags { "LightMode" = "ForwardAdd" } Blend [_SrcBlend] One ZWrite Off … }
To control these parameters, add two BlendMode
fields to our RenderingSettings
struct, and initialize them appropriately.
要控制这些参数,请向我们的 RenderingSettings
结构体添加两个 BlendMode
字段,并适当地初始化它们。
struct RenderingSettings { public RenderQueue queue; public string renderType; public BlendMode srcBlend, dstBlend; public static RenderingSettings[] modes = { new RenderingSettings() { queue = RenderQueue.Geometry, renderType = "", srcBlend = BlendMode.One, dstBlend = BlendMode.Zero }, new RenderingSettings() { queue = RenderQueue.AlphaTest, renderType = "TransparentCutout", srcBlend = BlendMode.One, dstBlend = BlendMode.Zero }, new RenderingSettings() { queue = RenderQueue.Transparent, renderType = "Transparent", srcBlend = BlendMode.SrcAlpha, dstBlend = BlendMode.OneMinusSrcAlpha } }; }
Inside DoRenderingMode
, we have to directly set the _SrcBlend and _DstBlend properties of the materials. We can do this via the Material.SetInt
method.
在 DoRenderingMode
中,我们必须直接设置材质的 _SrcBlend 和 _DstBlend 属性。我们可以通过 Material.SetInt
方法来实现。
foreach (Material m in editor.targets) { m.renderQueue = (int)settings.queue; m.SetOverrideTag("RenderType", settings.renderType); m.SetInt("_SrcBlend", (int)settings.srcBlend); m.SetInt("_DstBlend", (int)settings.dstBlend); }
Depth Trouble 深度难题
When working with a single object in Fade mode, everything seems to work fine. However, when you have multiple semitransparent objects close together, you might get weird results. For example, partially overlap two quads, placing one slightly above the other. From some view angles, one of the quads appears to cut away part of the other.
在 Fade 模式下处理单个物体时,一切似乎都运行正常。但是,当您有多个半透明物体靠得很近时,您可能会得到奇怪的结果。例如,部分重叠两个四边形,将一个稍微放置在另一个上方。从某些视角看,其中一个四边形似乎切掉了另一个四边形的一部分。

Unity tries to draw the opaque objects that are closest to the camera first. This is the most efficient way to render overlapping geometry. Unfortunately, this doesn't work for semitransparent geometry, because it has to be blended with whatever lies behind it. So transparent geometry has to be drawn the other way around. The furthest objects are drawn first, and the closest are drawn last. That's why transparent things are more expensive to draw than opaque things.
Unity 会尝试优先绘制距离摄像机最近的不透明物体。这是渲染重叠几何体最有效的方式。不幸的是,这不适用于半透明几何体,因为它必须与位于其后面的物体混合。所以透明几何体必须以相反的方式绘制。最远的物体首先绘制,最近的物体最后绘制。这就是为什么透明物体比不透明物体绘制成本更高的原因。
To determine the draw order of geometry, Unity uses the the position of their centers. This works fine for small objects that are far apart. But it doesn't work so well for large geometry, or for flat geometry that's positioned close together. In those cases, the draw order can suddenly flip while you change the view angle. This can cause a sudden change in the appearance of overlapping semitransparent objects.
为了确定几何体的绘制顺序,Unity 使用其中心的 FZ 坐标。这对于相距很远的小物体来说效果很好。但对于大型几何体,或放置在一起的扁平几何体来说,效果不佳。在这些情况下,当你改变视角时,绘制顺序可能会突然翻转。这可能会导致重叠的半透明对象的ظهر突然改变。
There's no way around this limitation, especially not when considering intersecting geometry. However, it often isn't noticeable. But in our case, certain draw orders produce obviously wrong results. This happens because our shaders still write to the depth buffer. The depth buffer is binary and doesn't care about transparency. If a fragment isn't clipped, its depth ends up written to the buffer. Because the draw order of semitransparent objects isn't perfect, this isn't desirable. The depth values of invisible geometry can end up preventing otherwise visible stuff from being rendered. So we have to disable writing to the depth buffer when using the Fade rendering mode.
这种限制是无法避免的,尤其是在考虑相交几何体时。然而,它通常并不显眼。但在我们的案例中,某些绘制顺序会产生明显错误的结果。发生这种情况是因为我们的着色器仍然写入深度缓冲区。深度缓冲区是二进制的,不关心透明度。如果片段未被裁剪,其深度最终会写入缓冲区。由于半透明对象的绘制顺序不完美,这是不理想的。不可见几何体的深度值最终可能会阻止原本可见的内容被渲染。因此,在使用 Fade 渲染模式时,我们必须禁用写入深度缓冲区。
Controlling ZWrite 控制 ZWrite
Like for the blend modes, we can use a property to control the ZWrite mode. We need to explicitly set this mode in the base pass, using the property. The additive pass never writes to the depth buffer, so it requires no change.
同混合模式类似,我们可以使用一个属性来控制 ZWrite 模式。我们需要使用该属性在基础通道中明确设置此模式。附加通道从不写入深度缓冲区,因此无需更改。
[HideInInspector] _SrcBlend ("_SrcBlend", Float) = 1 [HideInInspector] _DstBlend ("_DstBlend", Float) = 0 [HideInInspector] _ZWrite ("_ZWrite", Float) = 1 … Blend [_SrcBlend] [_DstBlend] ZWrite [_ZWrite]
Add a boolean field RenderingSettings
to indicate whether writing to the depth buffer should be enabled. This is only true for the Opaque and Cutout modes.
添加一个布尔字段 RenderingSettings
,指示是否应启用写入深度缓冲区。这仅在 Opaque 和 Cutout 模式下为真。
struct RenderingSettings { public RenderQueue queue; public string renderType; public BlendMode srcBlend, dstBlend; public bool zWrite; public static RenderingSettings[] modes = { new RenderingSettings() { queue = RenderQueue.Geometry, renderType = "", srcBlend = BlendMode.One, dstBlend = BlendMode.Zero, zWrite = true }, new RenderingSettings() { queue = RenderQueue.AlphaTest, renderType = "TransparentCutout", srcBlend = BlendMode.One, dstBlend = BlendMode.Zero, zWrite = true }, new RenderingSettings() { queue = RenderQueue.Transparent, renderType = "Transparent", srcBlend = BlendMode.SrcAlpha, dstBlend = BlendMode.OneMinusSrcAlpha, zWrite = false } }; }
Include the _ZWrite property inside DoRenderingMode
, again using the Material.SetInt
method.
将 _ZWrite 属性包含在 DoRenderingMode
中,再次使用 Material.SetInt
方法。
foreach (Material m in editor.targets) { m.renderQueue = (int)settings.queue; m.SetOverrideTag("RenderType", settings.renderType); m.SetInt("_SrcBlend", (int)settings.srcBlend); m.SetInt("_DstBlend", (int)settings.dstBlend); m.SetInt("_ZWrite", settings.zWrite ? 1 : 0); }
Switch our material to another rendering mode, and then back to Fade mode. While the draw order of semitransparent objects can still flip, we no longer get unexpected holes in our semitransparent geometry.
将我们的材质切换到另一种渲染模式,然后再切换回 Fade 模式。虽然半透明物体的绘制顺序仍然会翻转,但我们的半透明几何体中不再出现意外的孔洞。

几何体不再消失。
Fading vs. Transparency 褪色与透明度
The semitransparent rendering mode that we created fades out the geometry based on its alpha value. Note that the entire contribution of the geometry's color is faded. Both its diffuse reflections and its specular reflections are faded. That's why it's know as Fade mode.
我们创建的半透明渲染模式根据几何体的 alpha 值使其淡出。请注意,几何体颜色的全部贡献都将淡出。其漫反射和镜面反射都将淡出。这就是它被称为 Fade 模式的原因。

褪色的红,带有白色高光。
This mode is appropriate for many effects, but it does not correctly represent solid semitransparent surfaces. For example, glass is practically fully transparent, but it also has clear highlights and reflections. Reflected light is added to whatever light passes through. To support this, Unity's standard shaders also have a Transparent rendering mode. So let's add that mode as well.
这种模式适用于许多效果,但不能正确表示实心半透明表面。例如,玻璃实际上是完全透明的,但它也有清晰的高光和反射。反射光会添加到任何穿过的光线中。为了支持这一点,Unity 的标准着色器也有一个 Transparent 渲染模式。所以我们也添加这种模式。
enum RenderingMode { Opaque, Cutout, Fade, Transparent }
The settings for Transparent mode are the same as for Fade, except that we have to be able to add reflections regardless of the alpha value. Thus, its source blend mode has to be one instead of depending on alpha.
Transparent 模式的设置与 Fade 模式相同,只不过我们必须能够添加反射,无论其 alpha 值是多少。因此,其源混合模式必须为“一”,而不是取决于 alpha。
public static RenderingSettings[] modes = { new RenderingSettings() { queue = RenderQueue.Geometry, renderType = "", srcBlend = BlendMode.One, dstBlend = BlendMode.Zero, zWrite = true }, new RenderingSettings() { queue = RenderQueue.AlphaTest, renderType = "TransparentCutout", srcBlend = BlendMode.One, dstBlend = BlendMode.Zero, zWrite = true }, new RenderingSettings() { queue = RenderQueue.Transparent, renderType = "Transparent", srcBlend = BlendMode.SrcAlpha, dstBlend = BlendMode.OneMinusSrcAlpha, zWrite = false }, new RenderingSettings() { queue = RenderQueue.Transparent, renderType = "Transparent", srcBlend = BlendMode.One, dstBlend = BlendMode.OneMinusSrcAlpha, zWrite = false } };
We'll have to use yet another keyword, in this case _RENDERING_TRANSPARENT. Adjust DoRenderingMode
so it can detect and set this keyword.
在这种情况下,我们必须使用另一个关键字,即 _RENDERING_TRANSPARENT 。调整 DoRenderingMode
以便它能检测和设置该关键字。
void DoRenderingMode () { … else if (IsKeywordEnabled("_RENDERING_TRANSPARENT")) { mode = RenderingMode.Transparent; } EditorGUI.BeginChangeCheck(); mode = (RenderingMode)EditorGUILayout.EnumPopup( MakeLabel("Rendering Mode"), mode ); if (EditorGUI.EndChangeCheck()) { RecordAction("Rendering Mode"); SetKeyword("_RENDERING_CUTOUT", mode == RenderingMode.Cutout); SetKeyword("_RENDERING_FADE", mode == RenderingMode.Fade); SetKeyword( "_RENDERING_TRANSPARENT", mode == RenderingMode.Transparent ); … } }
Add the keyword to our two shader feature directives as well.
在两个着色器特性指令中也添加关键字。
#pragma shader_feature _ _RENDERING_CUTOUT _RENDERING_FADE _RENDERING_TRANSPARENT
Now we have to output the alpha value both for Fade and Transparent mode.
现在我们必须为 Fade 和 Transparent 模式输出 alpha 值。
#if defined(_RENDERING_FADE) || defined(_RENDERING_TRANSPARENT) color.a = alpha; #endif return color;
Switching our material to Transparent mode will once again make the entire quad visible. Because we're no longer modulating the new color based on alpha, the quad will appear brighter than when using Opaque mode. How much of the color behind the fragment gets added is still controlled by alpha. So when alpha is one, it looks just like an opaque surface.
将材质切换到 Transparent 模式将再次使整个四边形可见。由于我们不再基于 Alpha 调制新颜色,四边形将比使用 Opaque 模式时显得更亮。片段后面的颜色有多少被添加仍然由 Alpha 控制。所以当 Alpha 为 1 时,它看起来就像一个不透明的表面。

添加而不是淡入。
Premultiplied Alpha 预乘 Alpha
To make transparency work again, we have to manually factor in the alpha value. And we should only adjust the diffuse reflections, not the specular reflections. We can do this by multiplying the the material's final albedo color by the alpha value.
要使透明度再次起作用,我们必须手动考虑 Alpha 值。并且我们应该只调整漫反射,而不是镜面反射。我们可以通过将材质的最终反照率颜色乘以 Alpha 值来实现这一点。
float4 MyFragmentProgram (Interpolators i) : SV_TARGET { … float3 specularTint; float oneMinusReflectivity; float3 albedo = DiffuseAndSpecularFromMetallic( GetAlbedo(i), GetMetallic(i), specularTint, oneMinusReflectivity ); #if defined(_RENDERING_TRANSPARENT) albedo *= alpha; #endif … }

Because we're multiplying by alpha before the GPU does its blending, this technique is commonly known as premultiplied alpha blending. Many image-processing apps internally store colors this way. Textures can also contain premultiplied alpha colors. Then they either don't need an alpha channel, of they can store a different alpha value than the one that's associated with the RGB channels. That would make it possible to both brighten and darken using the same data, for example a combination of fire and smoke. However, a downside of storing colors that way in textures is a loss of precision.
因为我们在 GPU 进行混合之前就乘以了 Alpha 值,所以这种技术通常被称为预乘 Alpha 混合。许多图像处理应用程序内部都以这种方式存储颜色。纹理也可以包含预乘 Alpha 颜色。这样它们要么不需要 Alpha 通道,要么可以存储与 RGB 通道相关联的 Alpha 值不同的 Alpha 值。例如,这将使得使用相同的数据(如火与烟的组合)既能增亮也能变暗。然而,以这种方式在纹理中存储颜色的缺点是精度损失。
Adjusting Alpha 调整 Alpha 值
If something is both transparent and reflective, we'll see both what's behind it, and the reflections. This is true on both sides of the object. But the same light cannot both get reflected and also pass through the object. This is once again a matter of energy conservation. So the more reflective something is, the less light is able to travel through it, regardless of its inherent transparency.
如果一个物体既透明又反光,我们将能看到它后面的东西以及反射。这在物体的两侧都成立。但同样的光既不能被反射又穿透物体。这再次涉及能量守恒。因此,一个物体反射性越强,能够穿透它的光就越少,无论其固有的透明度如何。
To represent this, we have to adjust the alpha value before the GPU performs blending, but after we've changed the albedo. If a surface has no reflections, its alpha is unchanged. But when it reflects all light, its alpha effectively becomes one. As we determine the reflectivity in the fragment program, we can use that to adjust the alpha value. Given the original alpha and reflectivity , the modified alpha becomes .
要表示这一点,我们必须在 GPU 执行混合之前,但在我们更改反照率之后调整 Alpha 值。如果一个表面没有反射,其 Alpha 值保持不变。但当它反射所有光线时,其 Alpha 值实际上变为一。由于我们在片段程序中确定反射率,因此可以利用它来调整 Alpha 值。给定原始 Alpha 和反射率 ,修改后的 Alpha 变为 。
Keeping in mind that we're using one-minus-reflectivity in our shader, can be represented with . Then we can simplify the formula a bit. . Use this expression as the new alpha value, after adjusting the albedo color.
考虑到我们在着色器中使用了“一减反射率”, 可以用 表示。然后我们可以稍微简化一下公式。 。在调整反照率颜色后,将此表达式用作新的 Alpha 值。
#if defined(_RENDERING_TRANSPARENT) albedo *= alpha; alpha = 1 - oneMinusReflectivity + alpha * oneMinusReflectivity; #endif
The result should be slightly darker than before, to simulate light bouncing off the backside of our object.
最终效果应该比之前略暗一些,以模拟光线从我们物体背面反射的情形。

Keep in mind that this is a gross simplification of transparency, because we're not taking the actual volume of an object into account, only the visible surface.
请记住,这是对透明度的高度简化,因为我们没有考虑物体的实际体积,只考虑可见表面。
The next tutorial is Semitransparent Shadows.
下一个教程是半透明阴影。