[WPF] 使用 Effect 玩玩陰影、內陰影、 長陰影

最近在學習怎麼用 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>

4. 源碼

https://github.com/DinoChan/wpf_design_and_animation_lab

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章