PixelShaderEffect
Win2Dには昨日紹介した組み込みエフェクトのほかに独自にカスタマイズしたPixelShaderを使用してエフェクトをかけることができます。
これをすることで組み込みエフェクトでは表現できないような描画処理が可能になります。
今回はこの機能を使って画像にアナログテレビのような映像の横揺れにチャレンジしてみましょう。
CanvasAnimatedControl
前回までUIで使用してきたのはCanvasControl
というコントロールでしたが、このコントロールは静的画像のみの取り扱いで、ゲームのように毎秒更新するようなコントロールではありません。
今回はPixelShaderを使用するので動きのあるものをと思い、CanvasAnimatedControl
という毎フレーム処理が走るコントロールが、つい3か月前くらいにWinUI3でリリースされたようなのでこれを使ってエフェクトをかけていこうと思います。
※UWPでは以前からCanvasAnimatedContorl
は存在していました。
CanvasControl
をCanvasAnimatedControl
に換装します。
+<canvas:CanvasAnimatedControl
x:Name="canvas"
CreateResources="Canvas_CreateResources"
+ Update="Canvas_Update"
Draw="Canvas_Draw"
PointerMoved="Canvas_PointerMoved"
PointerPressed="Canvas_PointerPressed"
PointerReleased="Canvas_PointerReleased"
/>
名前を変更するだけで換装できます。
また、このコントロールで肝となるのはUpdate
イベントです。
これを設定することで、毎フレームイベントを発行することができます。
プログラム処理では以下の部分が変更になります。
private void Canvas_CreateResources(CanvasAnimatedControl sender, CanvasCreateResourcesEventArgs args)
{
}
private void Canvas_Draw(ICanvasAnimatedControl sender, CanvasAnimatedDrawEventArgs args)
{
}
UIで登録しているイベントのCanvas_CreateResources
とCanvas_Draw
イベントの引数が変更になります。
それぞれsender
がCanvasAnimatedControl
,ICanvasAnimatedControl
クラスになり、Canvas_Draw
のargs
がCanvasAnimatedDrawEventArgs
クラスになります。
Animated
を追加したのでここも変わるよという簡単なお話です。
これでUI側の準備が整いました。
PixelShader
エフェクト処理するPixelShaderがないとエフェクトがかけられないので準備します。
以下は入力された画像に対して横揺れを加えるShaderになります。
//入力宣言 Win2D側から何枚の画像データが入力されるかを指定(今回は1枚)
#define D2D_INPUT_COUNT 1
//入力情報 Win2D側からの画像をどのように入力するかを指定。
//0は入力の番号2枚目に入力するものは1になる。
//SIMPLEとCOMPLEXがあるが高度なことをしない限りSIMPLEでよい。
#define D2D_INPUT0_SIMPLE
//Direct2Dのヘルパー関数をインクルード
#include "d2d1effecthelpers.hlsli"
//C#側から与えるプロパティを指定。今回はC#側からTimeの情報を与えて画像を動かす
float Time;
//説明割愛 ノイズを作る関数
float2 gradientNoise_dir(float2 p) {
p = p % 289;
float x = (34 * p.x + 1) * p.x % 289 + p.y;
x = (34 * x + 1) * x % 289;
x = frac(x / 41) * 2 - 1;
return normalize(float2(x - floor(x + 0.5), abs(x) - 0.5));
}
//説明割愛 ノイズを作る関数
float gradientNoise_func(float2 p) {
float2 ip = floor(p);
float2 fp = frac(p);
float d00 = dot(gradientNoise_dir(ip), fp);
float d01 = dot(gradientNoise_dir(ip + float2(0, 1)), fp - float2(0, 1));
float d10 = dot(gradientNoise_dir(ip + float2(1, 0)), fp - float2(1, 0));
float d11 = dot(gradientNoise_dir(ip + float2(1, 1)), fp - float2(1, 1));
fp = fp * fp * fp * (fp * (fp * 6 - 15) + 10);
return lerp(lerp(d00, d01, fp.y), lerp(d10, d11, fp.y), fp.x);
}
//説明割愛 ノイズを作る関数
float GradientNoise(float2 UV, float Scale) {
return gradientNoise_func(UV * Scale) + 0.5;
}
//説明割愛 入力に対して出力をリマップする
float Remap_Float(float In, float2 inMinMax, float2 outMinMax) {
return outMinMax.x + (In - inMinMax.x) * (outMinMax.y - outMinMax.x) / (inMinMax.y - inMinMax.x);
}
//説明割愛 画像を適度に揺らす関数
float StepLighting() {
float step = GradientNoise(Time * 10, 1.0f);
return step * step * step * step;
}
// メインの関数Win2D側から呼ばれるのはここ
D2D_PS_ENTRY(main)
{
//D2DGetInputCoordinate()でUV情報を取得(0)は入力順番(1枚目のデータ)
float2 uv0 = D2DGetInputCoordinate(0).xy;
//与えられた時間によってノイズを作る
float gradientNoise = Remap_Float(GradientNoise(uv0.y * Time * 10, 1.0f), float2(0,1), float2(-0.005f,0.005f)) * StepLighting();
//D2DSampleInput()で入力画像データをサンプリングする(texture.Sample()のようなもの)
//引数は入力画像データ(0は1枚目の画像データ),UV情報
//以下の内容は元々の画像のUVからノイズを引いたものをUVとして突っ込む
float4 color = D2DSampleInput(0, float2(uv0.x, uv0.y - gradientNoise));
//float4を返す(画像データを返す)
return color;
}
Win2Dを使うにあたって一般的なPixelShaderで使わない関数があるので注意です。
D2D_PS_ENTRY(main)はコンパイルするときに呼び出すメイン関数になります。
またWin2Dからの入力画像はD2DSampleInput
から取得する形になります。
UVの取得もD2DGetInputCoordinate()
から取得する形となります。
特別なヘルパー関数は以下のリンクに詳しくあります。
Direct2Dヘルパー関数
Win2Dではhlslの状態のままでは読み取ってくれないので、以下のcmdをVisualStudioの開発者用コマンドプロンプトで実行して、binファイルにコンパイルします。
@echo off
setlocal
pushd "%~dp0"
rem fxcコンパイラーがなければエラー
where /q fxc >nul
if %errorlevel% neq 0 (
echo fxc not found.
goto WRONG_COMMAND_PROMPT
)
rem WindowsSDKのディレクトリがないとエラー
if "%WindowsSdkDir%" == "" (
goto WRONG_COMMAND_PROMPT
)
set INCLUDEPATH="%WindowsSdkDir%\Include\%WindowsSDKVersion%\um"
rem Direct2D用のヘルパー関数ファイルを探し、なかったらエラー
if not exist %INCLUDEPATH%\d2d1effecthelpers.hlsli (
echo d2d1effecthelpers.hlsli not found.
goto WRONG_COMMAND_PROMPT
)
rem ここに作ったhlslのファイル名を記載する
call :COMPILE simpleshader.hlsl || goto END
goto END
:COMPILE
echo.
echo Compiling %1
rem hlslのメイン関数を呼び出して最終的にbinファイルを作成
fxc %1 /nologo /T lib_4_0_level_9_3_ps_only /D D2D_FUNCTION /D D2D_ENTRY=main /Fl %~n1.fxlib /I %INCLUDEPATH% || exit /b
fxc %1 /nologo /T ps_4_0_level_9_3 /D D2D_FULL_SHADER /D D2D_ENTRY=main /E main /setprivate %~n1.fxlib /Fo:%~n1.bin /I %INCLUDEPATH% || exit /b
del %~n1.fxlib
exit /b
:WRONG_COMMAND_PROMPT
echo Please run from a Developer Command Prompt for VS2017.
:END
popd
exit /b %errorlevel%
VisualStuidoにはfxcというシェーダーコンパイラーが付属しているのでそれを使うためにVisualStudioの開発用コマンドプロンプトではいけないのです。
上記のhlsl
とcmd
ファイルを同じフォルダに置き、VisualStudioの開発用コマンドプロンプトでそのフォルダまで移動し、cmd
を実行してください
エラーがなければ{シェーダー名}.bin
ファイルができるのでプロジェクトのAssetフォルダなどに入れてください。
これでPixelShaderの準備が完了しました。
描画処理
それではいよいよこのPixelShaderを使って画像にエフェクトをかけていきます。
前回から変更するところは以下のようになります。
// PixelShaderEffectのフィールドを作成
PixelShaderEffect simpleEffect;
// 前回マウスクリックを離してた時呼び出していた関数に以下を追加
private async void CreateEffect()
{
- //effectImage = CreateGaussianBlur(); //今回ガウシアンブラーは使わないのでコメントアウト
// 今回はエフェクト処理を一気にここに記述する形で行きます。
// マウスドラッグで取得した範囲でrectを作成
var rect = new Rect(pointerDrag.StartLocation, pointerDrag.CurrentLocation);
// PixelShaderEffectを作成ReadAllBytesでShaderをバイナリで読み込む
simpleEffect = new PixelShaderEffect(await ReadAllBytes(Windows.ApplicationModel.Package.Current.InstalledPath + "\\Assets\\simpleshader.bin"))
{
// Source1がhlslでのD2D_INPUT0_SIMPLEになる。
// 2枚目を指定する際はSource2となりhlslではD2D_INPUT1_SIMPLE
Source1 = new CropEffect
{
// 前回同様元画像をマウスドラッグで得た範囲にクリッピング
Source = bitmap,
SourceRectangle = rect,
},
// エフェクトの境界線をハードにするかソフトにするか
Source1BorderMode = EffectBorderMode.Hard,
// 画像の最大オフセット値基本的に1でいいと思うが(UVが0.0~1.0なので)自分が見たサンプルだと((int) Math.Ceiling(canvas.Dpi / 96))※canvasはコントロールのx:name
MaxSamplerOffset = 1,
// 元画像のUVをオフセットするときはこれがOffsetの必要がある
Source1Mapping = SamplerCoordinateMapping.Offset,
};
}
// ファイルを指定しバイナリを読み込む
public static async Task<byte[]> ReadAllBytes(string filename)
{
var file = await StorageFile.GetFileFromPathAsync(filename);
var buffer = await FileIO.ReadBufferAsync(file);
return buffer.ToArray();
}
private void Canvas_Draw(ICanvasAnimatedControl sender, CanvasAnimatedDrawEventArgs args)
{
args.DrawingSession.DrawImage(bitmap);
if (simpleEffect != null)
{
// PixelShaderEffectが作られたらそれを描画する
+ args.DrawingSession.DrawImage(simpleEffect);
}
}
// UI部分で追加した毎フレームごとの処理
private void Canvas_Update(ICanvasAnimatedControl sender, CanvasAnimatedUpdateEventArgs args)
{
// 時間を取得
var time = (float) args.Timing.TotalTime.TotalSeconds;
if (simpleEffect != null)
{
// hlslで記載したプロパティ"Time"に時間を与える
simpleEffect.Properties["Time"] = time;
}
}
以上でシェーダーを使用したエフェクトをかけることができるようになります。
シェーダーの詳細は後日紹介できたらと思います。