Win2Dについて その６ PixelShaderEffect

# PixelShaderEffect
Win2Dには昨日紹介した組み込みエフェクトのほかに独自にカスタマイズしたPixelShaderを使用してエフェクトをかけることができます。
これをすることで組み込みエフェクトでは表現できないような描画処理が可能になります。
今回はこの機能を使って画像にアナログテレビのような映像の横揺れにチャレンジしてみましょう。

# CanvasAnimatedControl
前回までUIで使用してきたのは`CanvasControl`というコントロールでしたが、このコントロールは静的画像のみの取り扱いで、ゲームのように毎秒更新するようなコントロールではありません。
今回はPixelShaderを使用するので動きのあるものをと思い、`CanvasAnimatedControl`という毎フレーム処理が走るコントロールが、つい3か月前くらいにWinUI3でリリースされたようなのでこれを使ってエフェクトをかけていこうと思います。
※UWPでは以前から`CanvasAnimatedContorl`は存在していました。

`CanvasControl`を`CanvasAnimatedControl`に換装します。
```diff_xml
+<canvas:CanvasAnimatedControl
    x:Name="canvas"
    CreateResources="Canvas_CreateResources"
+   Update="Canvas_Update"
    Draw="Canvas_Draw"
    PointerMoved="Canvas_PointerMoved"
    PointerPressed="Canvas_PointerPressed"
    PointerReleased="Canvas_PointerReleased"
    />
```
名前を変更するだけで換装できます。
また、このコントロールで肝となるのは`Update`イベントです。
これを設定することで、毎フレームイベントを発行することができます。

プログラム処理では以下の部分が変更になります。
```csharp
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になります。
```hlsl
//入力宣言　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ヘルパー関数](https://learn.microsoft.com/ja-jp/windows/win32/direct2d/hlsl-helpers?redirectedfrom=MSDN#helper-functions)

Win2Dではhlslの状態のままでは読み取ってくれないので、以下のcmdを<font color=red>VisualStudioの開発者用コマンドプロンプトで実行</font>して、binファイルにコンパイルします。
```bat
@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を使って画像にエフェクトをかけていきます。
前回から変更するところは以下のようになります。
```diff_c_sharp
// 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になる。
        // ２枚目を指定する際は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;
    }
}
```

以上でシェーダーを使用したエフェクトをかけることができるようになります。

シェーダーの詳細は後日紹介できたらと思います。