最近在學習怎么用 Shazzam Shader Editor 編寫自定義的 Effect,並試着去實現陰影、內陰影和長陰影的效果。結果我第一步就放棄了,因為陰影用到的高斯模糊算法對我來說太太太太太太太太難了,最后只好用些投機取巧的方法來模仿這幾種效果。
1. 陰影
WPF 中的 DropShadowEffect 簡單來說就是將輸入源的圖像進行高斯模糊,然后根據 Color、Opacity、Direction、ShadowDepth 這幾個屬性來修改顏色、透明度和位移,形成一張新的圖像作為陰影,平鋪在原圖像的背后。要自己實現 DropShadowEffect 最大的難點就在高斯模糊這里,既然寫不出高斯模糊算法,就只好依賴 WPF 現有的東西。我的做法是用一個 VisualBrush 獲取需要做陰影的圖像,然后再用 WPF 的 BlurEffect 讓它變模糊:
<Grid ClipToBounds="True">
<Grid>
<Grid.Effect>
<BlurEffect Radius="38" />
</Grid.Effect>
<Grid.Background>
<VisualBrush Stretch="None" Visual="{Binding ElementName=ForegroundElement}" />
</Grid.Background>
</Grid>
</Grid>
<Grid x:Name="ForegroundElement">
<TextBlock VerticalAlignment="Center"
FontFamily="Lucida Handwriting"
FontSize="148"
FontWeight="ExtraBold"
Foreground="#f7e681"
TextAlignment="Center">
FAKE<LineBreak />
SHADOW</TextBlock>
</Grid>
現在的它看起來就是這個樣子。
然后寫一個 FakeDropShadowEffect。它獲取輸入源的 Alpha 通道,將 RGB 替換為指定的顏色(默認是黑色),組合成新的顏色。再通過 Angle 和 Depth 計算出偏移:
float4 main(float2 uv : TEXCOORD) : COLOR
{
float4 c = 0;
float rad = Angle * 0.0174533f;
float xOffset = cos(rad) * Depth;
float yOffset = sin(rad) * Depth;
uv.x += xOffset;
uv.y += yOffset;
c = tex2D(Texture1Sampler, uv);
c.rgb = Color.rgb * c.a * Opacity;
c.a = c.a * Opacity;
return c;
}
最后在應用了 BlurEffect 的元素外面再套一層 Grid,然后在這個 Grid 應用剛剛寫的 FakeDrpShadowEffect:
<Grid ClipToBounds="True">
<Grid.Effect>
<effects:FakeDropShadowEffect Angle="225"
Depth="0.03"
Opacity="0.5" />
</Grid.Effect>
成果如上圖所示,和 DropShadowEffect 幾乎一樣了。
2. 內陰影
關於內陰影的實現,我之前寫過另一篇文章介紹過:實現 WPF 的 Inner Shadow。現在用 Effect,我首先想到的做法是疊加兩個元素,上層的元素根據另一個元素的 VisualBrush 剪切出一個洞,然后在這個洞投下陰影:
<Grid x:Name="BackgroundElement">
<TextBlock x:Name="Text"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="150"
FontWeight="Bold"
Foreground="{StaticResource LightBackground}"
TextAlignment="Center">
INNER<LineBreak />
SHADOW</TextBlock>
</Grid>
<Grid ClipToBounds="True">
<Grid.Effect>
<DropShadowEffect BlurRadius="8"
Opacity="0.7"
ShadowDepth="5" />
</Grid.Effect>
<Grid Background="{StaticResource LightBackground}">
<Grid.Effect>
<effects:ClipEffect>
<effects:ClipEffect.Blend>
<VisualBrush Stretch="None" Visual="{Binding ElementName=BackgroundElement}" />
</effects:ClipEffect.Blend>
</effects:ClipEffect>
</Grid.Effect>
</Grid>
</Grid>
在上面的 XAML 中,ClipEffect 有另一個輸入 Blend
,這個輸入就是要剪切的形狀。ClipEffect 的代碼很簡單,就只是幾行,關鍵的功能是用 input 的Alpha 通道減去 blend 的 Alpha 通道作為結果輸出:
sampler2D blend : register(s1);
float4 main(float2 uv : TEXCOORD) : COLOR
{
float4 inputColor = tex2D(input, uv);
float4 blendColor = tex2D(blend, uv);
float4 resultColor = 0;
float opacity = inputColor.a - blendColor.a;
resultColor.rgb = inputColor.rgb * opacity;
resultColor.a = opacity;
return resultColor;
}
下圖是上面的 XAML 實現的效果:
3. 長陰影
我以前寫過一篇在 UWP 實現長陰影的博客:使用GetAlphaMask和ContainerVisual制作長陰影(Long Shadow) 。這次在 WPF 里重新用 Effect 實現一次。長陰影的原理是不斷向左上角(因為偷懶就只是做向右下的陰影)檢查,直到遇到 Alpha 通道為 1 的像素,然后計算這個像素與自身的距離得出陰影的 Alpha,所有代碼如下:
float4 main(float2 uv : TEXCOORD) : COLOR
{
float4 srcColor = tex2D(input, uv);
if (srcColor.a == 1)
{
return srcColor;
}
float4 tempColor = 0;
float2 offset = 0;
int maxDepth = 400;
float a = 0;
for (float i = 1; i < maxDepth; i++)
{
if (i < ShadowLength)
{
if (a == 0)
{
offset = uv.xy - float2(i / Width, i / Height);
if (offset.x > 0 && offset.y > 0)
{
tempColor = tex2D(input, offset);
if (tempColor.a == 1)
{
a = (1 - i / max(1,ShadowLength));
}
}
}
}
}
if (a == 0)
{
return srcColor;
}
a = min(1,a);
tempColor.rgb = Color.rgb * a * Opacity;
tempColor.a = a * Opacity;
float4 outColor = (1 - srcColor.a) * tempColor + srcColor;
return outColor;
}
使用起來的 XAML 和效果如下,需要輸入 ShadowLength 和 Color,因為 Effect 沒法知道輸入源的尺寸,所以還需要主動輸入 Width 和 Height:
<Grid x:Name="Root" Background="Transparent">
<Grid.Effect>
<effects:LongShadowEffect Width="{Binding ElementName=Root, Path=ActualWidth}"
Height="{Binding ElementName=Root, Path=ActualHeight}
ShadowLength="100"
Color="Red" />
</Grid.Effect>
<TextBlock x:Name="TextBlock"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="150"
FontWeight="Bold"
Text="NEXT" />
</Grid>