[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. 源碼
//github.com/DinoChan/wpf_design_and_animation_lab