Rendering 12
Semitransparent Shadows
渲染 12 半透明阴影
- Support cutout shadows. 支持镂空阴影。
- Use dithering. 使用抖动。
- Approximate semitransparent shadow.
近似半透明阴影。 - Toggle between semitransparent and cutout shadows.
在半透明和镂空阴影之间切换。
This is part 12 of a tutorial series about rendering. In the previous part, we made it possible to render semitransparent surfaces, but we didn't cover their shadows yet. Now we'll take care of that.
这是渲染系列教程的第 12 部分。在上一部分中,我们实现了半透明表面的渲染,但尚未涉及它们的阴影。现在我们将处理这个问题。
This tutorial was made with Unity 5.5.0f3.
本教程基于 Unity 5.5.0f3 制作。

当物体淡出时,它们的阴影也会随之淡出。
Cutout Shadows 镂空阴影
Currently, the shadows of our transparent materials are always cast as if they were solid, because that's what our shader assumes. As a result, the shadows might appear very strange, until you realize that you're seeing the shadows of a solid object. In the case of directional shadows, this can also lead to invisible geometry blocking shadows.
目前,我们透明材质的阴影总是以实体形式投射,因为我们的着色器就是这样假设的。结果是,阴影可能看起来非常奇怪,直到你意识到你看到的是一个实体物体的阴影。在定向阴影的情况下,这还可能导致不可见的几何体阻挡阴影。


不透明和镂空渲染模式,相同的定向阴影。
In the case of spotlight or point light shadows, you'll simply get a solid shadow.
对于聚光灯或点光源阴影,你只会得到一个实心阴影。

Refactoring My Shadows 重构我的阴影
In order to take transparency into account, we have to access the alpha value in the shadow caster shader pass. This means that we'll need to sample the albedo texture. However, this is not needed when using the opaque rendering mode. So we're going to need multiple shader variants for our shadows.
为了考虑透明度,我们必须在阴影投射器着色器通道中访问 alpha 值。这意味着我们需要采样反照率纹理。然而,当使用不透明渲染模式时,这不是必需的。因此,我们的阴影需要多个着色器变体。
Right now we have two versions of our shadow programs. One version for cube shadow maps, which is required for point lights, and one for the other light types. Now we need to mix in even more variants. To make this easier, we're going to rewrite our My Shadow include file. We'll use interpolators for all variants, and create a single vertex and fragment program.
现在我们有两种版本的阴影程序。一种用于立方体阴影贴图(点光源需要),另一种用于其他光源类型。现在我们需要混合更多变体。为了方便起见,我们将重写我们的 My Shadow 包含文件。我们将对所有变体使用插值器,并创建一个单独的顶点和片段程序。
First, move the definition of Interpolators
out of the conditional block. Then make the light vector conditional instead.
首先,将 Interpolators
的定义移出条件块。然后让光矢量变成有条件的。
struct VertexData { float4 position : POSITION; float3 normal : NORMAL; }; struct Interpolators { float4 position : SV_POSITION; #if defined(SHADOWS_CUBE) float3 lightVec : TEXCOORD0; #endif };
Next, write a new vertex program, which contains copies of the two different versions. The non-cube code has to be slightly adjusted to work with the new Interpolators
output.
接下来,编写一个新的顶点程序,其中包含两个不同版本的副本。非立方体代码必须稍微调整才能与新的 Interpolators
输出一起工作。
Interpolators MyShadowVertexProgram (VertexData v) { Interpolators i; #if defined(SHADOWS_CUBE) i.position = UnityObjectToClipPos(v.position); i.lightVec = mul(unity_ObjectToWorld, v.position).xyz - _LightPositionRange.xyz; #else i.position = UnityClipSpaceShadowCasterPos(v.position.xyz, v.normal); i.position = UnityApplyLinearShadowBias(i.position); #endif return i; }
Do the same for the fragment program. Then get rid of the old conditional programs.
对片元程序做同样的处理。然后去掉旧的条件程序。
float4 MyShadowFragmentProgram (Interpolators i) : SV_TARGET { #if defined(SHADOWS_CUBE) float depth = length(i.lightVec) + unity_LightShadowBias.x; depth *= _LightPositionRange.w; return UnityEncodeCubeShadowDepth(depth); #else return 0; #endif }//#if defined(SHADOWS_CUBE)// …//#endif
Clipping Shadow Fragments
裁剪阴影片元
We'll take care of cutout shadows first. We cut holes in the shadows by discarding fragments, like we do for the Coutout rendering mode in the other rendering passes. For this we need the material's tint, albedo texture, and alpha cutoff settings. Add variables for them to the top of My Shadows.
我们将首先处理镂空阴影。我们通过丢弃片段在阴影中“剪孔”,就像我们在其他渲染通道中对 Coutout 渲染模式所做的那样。为此,我们需要材质的色调、反照率纹理和 alpha 裁剪设置。将它们的变量添加到 My Shadows 的顶部。
#include "UnityCG.cginc"
float4 _Tint;
sampler2D _MainTex;
float4 _MainTex_ST;
float _AlphaCutoff;
So we have to sample the albedo texture when we're using Cutout rendering mode. Actually, we must only do this when we're not using the albedo's alpha value to determine smoothness. When these conditions are met, we have to pass the UV coordinates to the fragment program. We'll define SHADOWS_NEED_UV
as 1 when these conditions are met. This way, we can conveniently use #if SHADOWS_NEED_UV
.
所以,当使用 Cutout 渲染模式时,我们必须采样反照率纹理。实际上,我们仅在不使用反照率的 alpha 值来确定平滑度时才必须这样做。满足这些条件时,我们必须将 UV 坐标传递给片段程序。满足这些条件时,我们将 SHADOWS_NEED_UV
定义为 1。这样,我们就可以方便地使用 #if SHADOWS_NEED_UV
。
#include "UnityCG.cginc" #if defined(_RENDERING_CUTOUT) && !defined(_SMOOTHNESS_ALBEDO) #define SHADOWS_NEED_UV 1 #endif
Add the UV coordinates to the vertex input data. We don't need to make that conditional. Then conditionally add the UV to the interpolators.
将 UV 坐标添加到顶点输入数据中。我们无需使其成为条件性的。然后有条件地将 UV 添加到插值器中。
struct VertexData { float4 position : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct Interpolators { float4 position : SV_POSITION; #if SHADOWS_NEED_UV float2 uv : TEXCOORD0; #endif #if defined(SHADOWS_CUBE) float3 lightVec : TEXCOORD1; #endif };
Pass the UV coordinates on to the the interpolators in the vertex program, when needed.
需要时,在顶点程序中将 UV 坐标传递给插值器。
Interpolators MyShadowVertexProgram (VertexData v) { … #if SHADOWS_NEED_UV i.uv = TRANSFORM_TEX(v.uv, _MainTex); #endif return i; }
Copy the GetAlpha
method from My Lighting to My Shadows. Here, whether the texture is sampled has to depend on SHADOWS_NEED_UV. So check for that instead of whether _SMOOTHNESS_ALBEDO is defined. I marked the difference.
将 My Lighting 中的 GetAlpha
方法复制到 My Shadows 。这里,纹理是否采样必须取决于 SHADOWS_NEED_UV 。因此,请检查这个而不是检查 _SMOOTHNESS_ALBEDO 是否已定义。我已标记出区别。
float GetAlpha (Interpolators i) { float alpha = _Tint.a; #if SHADOWS_NEED_UV alpha *= tex2D(_MainTex, i.uv.xy).a; #endif return alpha; }
Now we can retrieve the alpha value in the fragment program, and use it to clip when in Cutout rendering mode.
现在我们可以在片段程序中检索 Alpha 值,并在 Cutout 渲染模式下使用它进行裁剪。
float4 MyShadowFragmentProgram (Interpolators i) : SV_TARGET { float alpha = GetAlpha(i); #if defined(_RENDERING_CUTOUT) clip(alpha - _AlphaCutoff); #endif … }
To make this actually work, add shader features for _RENDERING_CUTOUT and _SMOOTHNESS_ALBEDO to the shadow caster pass of My First Lighting Shader.
为了使其真正生效,请将 _RENDERING_CUTOUT 和 _SMOOTHNESS_ALBEDO 的着色器功能添加到 My First Lighting Shader 的阴影投射器通道中。
Pass { Tags { "LightMode" = "ShadowCaster" } CGPROGRAM #pragma target 3.0 #pragma shader_feature _RENDERING_CUTOUT #pragma shader_feature _SMOOTHNESS_ALBEDO … }


镂空阴影,方向光和聚光灯。
Refactoring My Lighting 重构光照
Before we move on, let's tweak My Lighting a bit as well. Notice how we've used UnityObjectToClipPos
to transform the vertex position in My Shadows. We can use this function in My Lighting as well, instead of performing a matrix multiplication ourselves. The UnityObjectToClipPos
function also performs this multiplication, but uses the constant value 1 as the fourth position coordinate, instead of relying on the mesh data.
在继续之前,我们也将对 My Lighting 进行一些调整。请注意,我们已使用 UnityObjectToClipPos
来变换 My Shadows 中的顶点位置。我们也可以在 My Lighting 中使用此函数,而不是自己执行矩阵乘法。 UnityObjectToClipPos
函数也执行此乘法,但使用常量值 1 作为第四个位置坐标,而不是依赖于网格数据。
Interpolators MyVertexProgram (VertexData v) { Interpolators i;// i.pos = mul(UNITY_MATRIX_MVP, v.vertex);i.pos = UnityObjectToClipPos(v.vertex); … }
The data supplied via the mesh is always 1, but the shader compiler doesn't know this. As a result, using a constant is more efficient. Beginning with version 5.6, Unity will give a performance warning when using an unoptimized multiplication with UNITY_MATRIX_MVP
.
通过网格提供的数据始终为 1,但着色器编译器不知道这一点。因此,使用常量效率更高。从 5.6 版本开始,当使用未经优化的乘法与 UNITY_MATRIX_MVP
时,Unity 将给出性能警告。
Partial Shadows 部分阴影
To also support shadows for the Fade and Transprant rendering modes, we have to add their keywords to the shader feature of or shadow caster pass. Like the other passes, the rendering feature now has four possible states.
为了支持 Fade 和 Transprant 渲染模式下的阴影,我们必须将它们的关键字添加到阴影投射通道的着色器功能中。像其他通道一样,渲染功能现在有四种可能的状态。
#pragma shader_feature _ _RENDERING_CUTOUT _RENDERING_FADE _RENDERING_TRANSPARENT
These two modes are semitransparent instead of cutout. So their shadows should be semitransparent as well. Let's define a convenient SHADOWS_SEMITRANSPARENT macro in My Shadows when this is the case.
这两种模式是半透明而不是镂空。所以它们的阴影也应该是半透明的。在这种情况下,我们可以在 My Shadows 中定义一个方便的 SHADOWS_SEMITRANSPARENT 宏。
#if defined(_RENDERING_FADE) || defined(_RENDERING_TRANSPARENT) #define SHADOWS_SEMITRANSPARENT 1 #endif
Now we have to adjust the definition of SHADOWS_NEED_UV, so it also gets defined in the case of semitransparent shadows.
现在我们必须调整 D 的定义,使其在半透明阴影的情况下也能被定义。
#if SHADOWS_SEMITRANSPARENT || defined(_RENDERING_CUTOUT) #if !defined(_SMOOTHNESS_ALBEDO) #define SHADOWS_NEED_UV 1 #endif #endif
Dithering 抖动
Shadow maps contain the distance to surfaces that block light. Either the light is blocked at some distance, or it is not. Hence, there is no way to specify that light is partially blocked by semitransparent surfaces.
阴影贴图包含阻挡光线的表面的距离。光线要么在某个距离被阻挡,要么不被阻挡。因此,无法指定光线被半透明表面部分阻挡。
What we can do, is clip part of the shadow surface. That's what we do for cutout shadows. But instead of clipping based on a threshold, we could clip fragments uniformly. For example, if a surface lets half the light through, we could clip every other fragment, using a checkerboard pattern. Overall, the resulting shadow will appear half as strong as a full shadow.
我们能做的是裁剪部分阴影表面。这正是我们对镂空阴影所做的。但与其基于阈值裁剪,我们可以均匀地裁剪片段。例如,如果一个表面允许一半的光线通过,我们可以使用棋盘格图案裁剪每隔一个片段。总体而言,生成的阴影将显得只有完全阴影的一半强度。
We don't always have to use the same pattern. Depening on the alpha value, we can use a pattern with more or less holes. And if we mix these patterns, we can create smooth transitions of shadow density. Basically, we're using only two states to approximate a gradient. This technique is known as dithering.
Unity contains a dither pattern atlas that we can use. It contains 16 different patterns of 4 by 4 pixels. It starts with a completely empty pattern. Each successive pattern fills one additional pixel, until there are seven filled. Then the pattern is inverted and reverses, until all pixels are filled.

VPOS
To apply a dither patter to our shadow, we have to sample it. We cannot use the UV coordinates of the mesh, because those aren't uniform in shadow space. Instead, we'll need to use the screen-space coordinates of the fragment. As shadow maps are rendered from the point of view of the light, this aligns the patterns with the shadow map.
The screen-space position of a fragment can be accessed in the fragment program, by adding a parameter with the VPOS
semantic to it. These coordinates are not explicitly output by the vertex program, but the GPU can make them available to us.
Unfortunately, the VPOS
and SV_POSITION
semantics don't play nice. On some platforms, they end up mapped to the same position semantic. So we cannot use both at the same time in our Interpolators
struct. Fortunately, we only need to use SV_POSITION
in the vertex program, while VPOS
is only needed in the fragment program. So we can use a separate struct for each program.
First, rename Interpolators
to InterpolatorsVertex
and adjust MyShadowVertexProgram
accordingly. Do not adjust MyShadowFragmentProgram
.
struct InterpolatorsVertex { float4 position : SV_POSITION; #if SHADOWS_NEED_UV float2 uv : TEXCOORD0; #endif #if defined(SHADOWS_CUBE) float3 lightVec : TEXCOORD1; #endif }; … InterpolatorsVertex MyShadowVertexProgram (VertexData v) { InterpolatorsVertex i; #if defined(SHADOWS_CUBE) i.position = UnityObjectToClipPos(v.position); i.lightVec = mul(unity_ObjectToWorld, v.position).xyz - _LightPositionRange.xyz; #else i.position = UnityClipSpaceShadowCasterPos(v.position.xyz, v.normal); i.position = UnityApplyLinearShadowBias(i.position); #endif #if SHADOWS_NEED_UV i.uv = TRANSFORM_TEX(v.uv, _MainTex); #endif return i; }
Then create a new Interpolators
struct for use in the fragment program. It is a copy of the other struct, except that it should contain UNITY_VPOS_TYPE vpos : VPOS
instead of float4 positions : SV_POSITION
when semitransparent shadows are needed. The UNITY_VPOS_TYPE
macro is defined in HLSLSupport. It's usually a float4
, except for Direct3D 9, which needs it to be a float2
.
struct InterpolatorsVertex { … } struct Interpolators { #if SHADOWS_SEMITRANSPARENT UNITY_VPOS_TYPE vpos : VPOS; #else float4 positions : SV_POSITION; #endif #if SHADOWS_NEED_UV float2 uv : TEXCOORD0; #endif #if defined(SHADOWS_CUBE) float3 lightVec : TEXCOORD1; #endif };
Dithering
To access Unity's dither pattern texture, add a _DitherMaskLOD
variable to My Shadows. The different patterns are stored in layers of a 3D texture, so its type has to be sampler3D
instead of sampler2D
.
sampler3D _DitherMaskLOD;
Sample this texture in MyShadowFragmentProgram
, if we need semitransparent shadows. This is done via the tex3D
function, which requires 3D coordinates. The third coordinate should be in the 0–1 range and is used to select a 3D slice. As there are 16 patterns, the Z coordinate of the first pattern is 0, the coordinate for the second pattern is 0.0625, the third is 0.128, and so on. Let's begin by always choosing the second pattern.
float4 MyShadowFragmentProgram (Interpolators i) : SV_TARGET { float alpha = GetAlpha(i); #if defined(_RENDERING_CUTOUT) clip(alpha - _AlphaCutoff); #endif #if SHADOWS_SEMITRANSPARENT tex3D(_DitherMaskLOD, float3(i.vpos.xy, 0.0625)); #endif … }
The alpha channel of the dither texture is zero when a fragment should be discarded. So subtract a small value from it and use that to clip.
#if SHADOWS_SEMITRANSPARENT float dither = tex3D(_DitherMaskLOD, float3(i.vpos.xy, 0.0625)).a; clip(dither - 0.01); #endif
To actually see a pattern, we have to scale it. To get a good look at it, magnify it by a factor of 100, which is done by multiplying the position by 0.01. A spotlight shadow allows us to get a good look at it.
tex3D(_DitherMaskLOD, float3(i.vpos.xy * 0.01, 0.0625)).a;

You can inspect all 16 dither patterns by increasing the Z coordinate in steps of 0.0625. The shadows get fully clipped at 0, and are fully rendered at 0.9375.
Approximating Semitransparency
Instead of using a uniform pattern, we have to base the selection of the dither pattern on the surface's alpha value. As full opacity is reached at 0.9375, multiply the alpha value by this factor, then use it as the Z coordinate.
tex3D(_DitherMaskLOD, float3(i.vpos.xy * 0.01, alpha * 0.9375)).a;

The dithering now varies based on the surface opacity. To make it look more like a true shadow, we'll have to scale down the pattern size. Unity uses a factor of 0.25, so we'll use that as well.
tex3D(_DitherMaskLOD, float3(i.vpos.xy * 0.25, alpha * 0.9375)).a;

This looks a lot better, but it's not perfect. How obvious the dithering is depends on the resolution of the shadow map. The higher its resolution, the smaller and less obvious the patterns.
Dithering works better with soft directional shadows. The screen-space filtering smudges the dithered fragments to such a degree that they're no longer obvious. The result is something that approaches actual semitransparent shadows.


Unfortunately, dithering is not visually stable. When things move, you can get very obvious shadow swimming. Not just along the edge, but across the entire shadow!
Optional Semitransparent Shadows
Considering the limitations of semitransparent shadows, you might decide not to use them. You can entirely disable the shadows of an object via the Cast Shadows mode of its Mesh Renderer component. However, it could be that cutout shadows work just fine for a semitransparent object. For example, when a significant portion of its surface is fully opaque. So let's make it possible to choose between both types of shadows.
To support this choice, add a shader feature to the shadow caster pass for a new keyword, _SEMITRANSPARENT_SHADOWS.
#pragma shader_feature _SEMITRANSPARENT_SHADOWS
In My Shadows, only define SHADOWS_SEMITRANSPARENT if the _SEMITRANSPARENT_SHADOWS shader keyword is set.
#if defined(_RENDERING_FADE) || defined(_RENDERING_TRANSPARENT) #if defined(_SEMITRANSPARENT_SHADOWS) #define SHADOWS_SEMITRANSPARENT 1 #endif #endif
If the new shader feature is not enabled, then we should fall back to cutout shadows. We can do this by manually defining _RENDERING_CUTOUT.
#if defined(_RENDERING_FADE) || defined(_RENDERING_TRANSPARENT) #if defined(_SEMITRANSPARENT_SHADOWS) #define SHADOWS_SEMITRANSPARENT 1 #else #define _RENDERING_CUTOUT #endif #endif
Because the new shader feature isn't enabled yet, we now get cutout shadows when using the Fade or Transparent rendering mode.

Toggling Semitransparency
To enable semitransparent shadows again, we have to add an option for it to our custom shader UI. So add a DoSemitransparentShadows
method to MyLightingShaderGUI
.
void DoSemitransparentShadows () { }
We only need to show this option when using the Fade or Transparent rendering mode. We know which mode we're using inside DoRenderingMode
. So invoke DoSemitransparentShadows
at the end of this method, if needed.
void DoRenderingMode () { … if (mode == RenderingMode.Fade || mode == RenderingMode.Transparent) { DoSemitransparentShadows(); } }
As this is a binary choice, we can represent it with a toggle button. Because the label Semitransparent Shadows is wider than Unity's default inspector window width, I've abbreviated it. For clarity, I gave it a tooltip that isn't abbreviated.
void DoSemitransparentShadows () { bool semitransparentShadows = EditorGUILayout.Toggle( MakeLabel("Semitransp. Shadows", "Semitransparent Shadows"), IsKeywordEnabled("_SEMITRANSPARENT_SHADOWS") ); }

Like with the other keywords, check whether the user makes a change and set the keyword accordingly.
void DoSemitransparentShadows () { EditorGUI.BeginChangeCheck(); bool semitransparentShadows = EditorGUILayout.Toggle( MakeLabel("Semitransp. Shadows", "Semitransparent Shadows"), IsKeywordEnabled("_SEMITRANSPARENT_SHADOWS") ); if (EditorGUI.EndChangeCheck()) { SetKeyword("_SEMITRANSPARENT_SHADOWS", semitransparentShadows); } }
Showing Alpha Cutoff for Shadows
When using cutout shadows, we might like to change the Alpha Cutoff threshold. Currently, it only shows up in our UI when using the Cutout rendering mode. However, it must now also be accessible in Fade and Transparent mode, when not using semitransparent shadows. We can support this by setting shouldShowAlphaCutoff
to true
in DoSemitransparentShadows
, when appropriate.
void DoSemitransparentShadows () { … if (!semitransparentShadows) { shouldShowAlphaCutoff = true; } }

The next tutorial is Deferred Shading.