[WPF] 使用 Shazzam Shader Editor 編寫一個 Lighten Effect

之前在一篇文章(實現兩個任天堂 Switch 的加載動畫)裏爲了實現不同亮度的 Grid,使用了一個 LightenConverter 類,但是它只能處理 SolidColorBrush。爲了可以應用在更多場合,這篇文章自己寫一個 Effect 來實現相同 Lighten 的效果。

1. WPF 中的 Effect

Wpf 自帶兩種 Effect:BlurEffect 和 DropShadowEffect,用法如下:

<Grid.Effect>
    <BlurEffect/>
</Grid.Effect>

除了 WPF 自帶的這兩個,還可以在 Microsoft Blend for Visual Studio 2015 裏找到由 Microsoft.Expression.Effects 這個 dll 提供的一些 Effect。

現在這個 dll 也可以在 Nuget 上找到。

2. 編寫 Shader

WPF 中的 Effect 使用 HLSL(高級着色器語言)編寫,如果需要自定義 Effect 可以使用 Shazzam Shader Editor, 關於這款編輯器 walterlv 有一篇如何使用的教程:

WPF 像素着色器入門:使用 Shazzam Shader Editor 編寫 HLSL 像素着色器代碼 - walterlv

其實我之前也沒寫過,語法什麼的完全不懂,可是從網上抄一抄,很快就搞明白了一些基礎,最後從 Lightness.fx 這個改一改完成了我需要的 LightenEffect:

// Copyright (c) 2014 Marcus Schweda
// This file is licensed under the MIT license (see LICENSE)

sampler2D input : register(s0);

float delta : register(c0);

// RGB -> HSL
float4 hsl(float4 c) {
    float4 c2 = c.a;
    float M = max(c.r, max(c.g, c.b)),
          m =  min(c.r, min(c.g, c.b));
    float chroma = M - m;
    // Lightness
    c2[2] = (M + m) / 2;
    // Hue
    if (chroma != 0) {
        if (M == c.r)
            c2[0] = ((c.g - c.b) / chroma) % 6;
        else if (M == c.g)
            c2[0] = (c.b - c.r) / chroma + 2;
        else
            c2[0] = (c.r - c.g) / chroma + 4;
        if (c2[0] < 0)
            c2[0] += 6;
        // Saturation
        c2[1] = chroma / (1 - abs(2 * c2[2] - 1));
    } else {
        c2[0] = c2[1] = 0;
    }
    return c2;
}

float4 rgb(float4 c) {
    float4 c2 = c[3];
    float chroma = c[1] * (1 - abs(2 * c[2] - 1));
    float X = chroma * (1 - abs(c[0] % 2 - 1));
    
    if (0 <= c[0] && c[0] < 1)
        c2.rgb = float3(chroma, X, 0);
    else if (1 <= c[0] && c[0] < 2)
        c2.rgb = float3(X, chroma, 0);
    else if (2 <= c[0] && c[0] < 3)
        c2.rgb = float3(0, chroma, X);
    else if (3 <= c[0] && c[0] < 4)
        c2.rgb = float3(0, X, chroma);
    else if (4 <= c[0] && c[0] < 5)
        c2.rgb = float3(X, 0, chroma);
    else if (5 <= c[0] && c[0] < 6)
        c2.rgb = float3(chroma, 0, X);
        
    c2.rgb += c[2] - chroma / 2;
    return c2;
}

float4 main(float2 uv : TEXCOORD) : COLOR {
    float4 hcyin = hsl(tex2D(input, uv));
    if( delta>0)
    { 
    	hcyin[2] = saturate(hcyin[2] + (1-hcyin[2])* delta);
    }else
    {
    	hcyin[2] = saturate(hcyin[2] + hcyin[2] * delta);
    }
    
    return rgb(hcyin);
}

這份代碼分三部分,首先是定義的兩個變量 input 和 delta,input 即輸入的圖像,是每個 Shader 的固定部分,不要修改它;delta 是定義來控制 LightenEffect 亮度變化率的變量。然後是兩個自定義的函數,用於 rgb 和 hsl 互相轉換。最後是 main 函數,這也是每個 Effect 必須包含的部分,這個函數的輸入 uv 看起來是座標,用 tex2D(input, uv) 獲取 input 在 uv 的顏色,函數的返回值是處理後的 uv 所在的顏色。

在這段代碼裏的 main 函數還算簡單,就是把當前位置的顏色轉換爲 hsl,然後根據 delta 調整亮度,再轉換爲 rgb 返回。

函數完成並運行 Apply Shader 後可以使用 Shazzam Shader Editor 的 Tryout 功能驗證效果。可以看到 Delta 爲 -1 即全黑,爲 1 就全白。

2. 應用 Effect

驗證完這個 Shader,把生成的 C# 代碼和 .ps 文件放進項目,改好命名空間,編譯後就能使用(關於這部分的詳細操作,請參考 walterlv 的 這篇文章)。現在來看看生成的 C# 代碼:

public class LightenEffect : ShaderEffect
{
    public static readonly DependencyProperty InputProperty = ShaderEffect.RegisterPixelShaderSamplerProperty("Input", typeof(LightenEffect), 0);
    public static readonly DependencyProperty DeltaProperty = DependencyProperty.Register("Delta", typeof(double), typeof(LightenEffect), new UIPropertyMetadata(((double)(0D)), PixelShaderConstantCallback(0)));
    public LightenEffect()
    {
        PixelShader pixelShader = new PixelShader();
        pixelShader.UriSource = new Uri("/WpfDesignAndAnimationLab.Effects;component/Shaders/LightenEffect.ps", UriKind.Relative);
        this.PixelShader = pixelShader;

        this.UpdateShaderValue(InputProperty);
        this.UpdateShaderValue(DeltaProperty);
    }
    private Brush Input
    {
        get
        {
            return ((Brush)(this.GetValue(InputProperty)));
        }
        set
        {
            this.SetValue(InputProperty, value);
        }
    }
    public double Delta
    {
        get
        {
            return ((double)(this.GetValue(DeltaProperty)));
        }
        set
        {
            this.SetValue(DeltaProperty, value);
        }
    }
}

首先是自定義的兩個變量 Input 和 Delta,它們被封裝成依賴屬性。然後看看這句話,這句話定位產生的 .ps 文件,一定要保證位置正確:

pixelShader.UriSource = new Uri("/WpfDesignAndAnimationLab.Effects;component/Shaders/LightenEffect.ps", UriKind.Relative);

最後使用時只需要在前面加上 Effect 的命名空間。

<Rectangle.Effect>
    <effects:LightenEffect Delta=".5"/>
</Rectangle.Effect>

3. 最後

感謝 walterlv 寫的文章,讓我終於學會了 Shazzam Shader Editor 的用法。

4. 源碼

https://github.com/DinoChan/wpf_design_and_animation_lab

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