Help us understand the problem. What is going on with this article?

WPF で ShaderEffect

More than 1 year has passed since last update.

はじめに

この記事は 城東.NET #29 でお話した内容をもう少し詳しく書いたものです。

WPF で私がすごいと思う機能の一つである "ShaderEffect" を紹介したいと思います。

サンプルとして簡単な Shader Editor (ShaderPad と呼んだ方がそれっぽい) を作ってみました。

ShaderEffect とは

読んで字のごとく、 "GPU の Shader で実装する Effect" です。

WPF の Effect

WPF では映像フィルターとして "BitmapEffect" という機能を最初期から実装していました。

しかし BitmapEffect は処理が CPU 上で実行するコードで書かれており、パフォーマンスに大きな問題を抱えていました。従って使いどころも非常に限定的になり、アニメーションがかかっているところに使うなどもってのほかでした。

その後、 .NET 3.5 SP1 で "Effect" が新たに実装されます。

Effect は BitmapEffect と異なり GPU 実装となり処理が大幅に高速化され、普通に使えるようになりました。 Effect ではプリセットとして下記のものが用意されています。

プリセットはこの 2 つしかありませんが、これに加えて ShaderEffect が提供されました (これら 3 つは全て Effect を継承している) 。 ShaderEffect を使うことで自分の好きなような Effect を書くことができるようになりました。もちろん Shader なので GPU 実行となり非常に高速です。

ShaderEffect の特徴

  • Pixel Shader (SM 3.0 まで) で任意のフィルター処理が書ける
  • (基本的には) Direct3D をさわらなくても使える (知識は多少必要)
  • Effect を適用する Visual のレンダリング結果が Shader 上で texture として参照できる
  • 任意のパラメーターを DependencyProperty として定義すると Shader 上の定数として参照できる

WPF に実装する、と考えればある意味当たり前ではあるのですが、 DependencyProperty で Shader のパラメーターと接続できるというのは非常に強力で Animation と Binding することでごく自然に Shader アニメーションが実現できます。

ShaderEffect を使う

ShaderEffect クラスの継承クラスを定義する

ShaderEffect は ShaderEffect を継承したカスタムクラスの実装が必要です (Shader の設定が protected なため) 。

PixelShader の準備をする

ShaderEffect で使う Shader は "PixelShader" クラスで扱います (ややこしい) 。

PixelShader にセットする Shader コードはコンパイル済である必要があります (Direct3D の Shader はコンパイルされている必要があるため) 。このコンパイルだけは WPF だけではどうにもならないので、別途コンパイラーが必要になります。

  • Windows SDK に含まれるコマンドラインコンパイラー (fxc.exe) を使う。
  • SharpDX を利用する。

通常は頒布するアニメーションに決まった効果を適用すると思うので、 fxc による事前コンパイルになると思います。バイナリをリソースに保持し、実行時にロードするのがよいと思います。

今回のサンプルは Editor なので動的にコンパイルする必要があります。よって SharpDX を利用しました。 SharpDX は DirectX の .NET ラッパーでこの中に Shader コンパイラーのラッパーも含まれているので簡単にコンパイラーを利用できます。

string hlsl = @"
sampler2D input : register(s0);
float4 main(float2 uv : TEXCOORD) : COLOR
{
    return tex2D(input, uv));
}";

var pixelShader = new System.Windows.Media.Effects.PixelShader();
var compileResult = SharpDX.D3DCompiler.ShaderBytecode.Compile(hlsl, "main", "ps_3_0");
using (var ms = new System.IO.MemoryStream(compileResult.Bytecode))
{
    pixelShader.SetStreamSource(ms);
}

//  ShaderEffect.PixelShader プロパティにセット
this.PixelShader = pixelShader;

SharpDX を使って Shader をコンパイルし、設定するところまで実装例です。
コンパイルしたらバイナリーを PixelShader クラスのインスタンスにセットし、それを ShaderEffect に設定します。

ちなみに下記のプロパティを設定すると ShaderBytecode.Compile で例外を起きないようにもできます (戻り値のチェックが必要) 。

SharpDX.Configuration.ThrowOnShaderCompileError = false;

DependencyProperty を定義する

Shader は定数レジスタを通して実行時パラメーターを定義できます。所定のルールに従って定義することにより DependencyProperty を Shader の定数と接続することができます。

まず入力ソースの定義をします。これはお約束のようです。

public static readonly DependencyProperty InputProperty =
    ShaderEffect.RegisterPixelShaderSamplerProperty("Input", typeof(CustomShaderEffect), 0);

RegisterPixelShaderSamplerProperty は Effect として入力されるパスを定義するもので Brush 型のインスタンスが割り当てられますが、それをコードから使うことはないと思います。ここでは SamplingMode の指定もできますが明示的に NearestNeighbor を使いたい時以外は特に指定する必要もないと思います。

Shader から参照できる定数を定義したい場合は対応する DependencyProperty を定義します。

public double Time
{
    get { return (double)GetValue(TimeProperty); }
    set { SetValue(TimeProperty, value); }
}

public static readonly DependencyProperty TimeProperty =
DependencyProperty.Register(
    "Time",
    typeof(double),
    typeof(CustomShaderEffect),
    new PropertyMetadata(0.0, PixelShaderConstantCallback(2)));

要点は PropertyMetadata に指定する PixelShaderConstantCallback で、ここに指定する引数が定数レジスターのインデックスになります。上記の場合ですと HLSL 上では次のように定義します。

float time : register(c2);

Shader 上では float 型にしなくてはなりませんが、 WPF は double にしないと扱いにくいので DependencyProperty は double にしています。ここはうまくキャストしてくれているようです。

同様に Brush の DependencyProperty の PropertyMetadata に PixelShaderSamplerCallback を適用すると他の映像を Shader の入力ソースに使えます。

Effect の適用

Effect は UIElement の Effect プロパティ に設定します。

<Grid>
  <Grid.Effect>
    <local:CustomShaderEffect x:Name="shader" />      
  </Grid.Effect>
  <Grid.Triggers>
    <EventTrigger RoutedEvent="Grid.Loaded">
      <BeginStoryboard>
        <Storyboard>
          <DoubleAnimation
            Storyboard.TargetName="shader" Storyboard.TargetProperty="Time"
            From="0" To="3600" RepeatBehavior="Forever" AutoReverse="false"
            Duration="1:0:0"
            />
        </Storyboard>
      </BeginStoryboard>
    </EventTrigger>
  </Grid.Triggers>
</Grid>

合わせて Storyboard によるアニメーションの記述もしてみました。実装した Time プロパティに秒単位の時間が更新されていきます。これにより Shader 側で時間に同期したアニメーションを記述できます (!!) 。

簡単な例

WPF ShaderEditor では Width, Height, Time というプロパティを定義しています。

  • Width (0) = ActualWidth と Binding (ソース実サイズの幅)
  • Height (1) = ActualHeight と Binding (ソース実サイズの高さ)
  • Time(2) = 秒単位の時間 (1 時間でループ) を Animation で更新
sampler2D input : register(s0);
float width : register(c0);
float height : register(c1);
float time : register(c2);

基本

lsl
float4 main(float2 uv : TEXCOORD) : COLOR
{
    return tex2D(input, uv);
}

190228_1.jpg

無加工でそのまま出力。

波打ち

float4 main(float2 uv : TEXCOORD) : COLOR
{
    return tex2D(input, float2(uv.x + sin((uv.y + time * 0.5) * 3.14159 * 10) * 0.1, uv.y));
}

190228_2.jpg

いわゆるラスタースクロール (っぽいもの) 。

レトロ調

float4 main(float2 uv : TEXCOORD) : COLOR
{
    float4 c = tex2D(input, float2(uv.x, uv.y));
    float y = (0.299 * c.x + 0.587 * c.y + 0.114 * c.z) * max(0.2, sin(uv.y * 3.14159 * height));

    float d = distance(uv, float2(0.5, 0.5));
    d = min(1, 1 - d * d * 2.5);

    return float4(float3(1.0, 0.7, 0.5) * y * d, 1.0);
}

190228_4.jpg

  • セピア調
  • スキャンラインっぽい縞
  • 周辺を若干暗くする

ランダムで横にゆらすとさらによい。

おはじき

float4 main(float2 uv : TEXCOORD) : COLOR
{
    float x = 30.0;
    float y = 30.0;
    float l = 0.0;
    if (width > height)
    {
        x = x * width / height;
        l = 0.5 / y;
    }
    else
    {
        y = y * height / width;
        l = 0.5 / x;
    }
    float2 uv2 = float2(floor(uv.x * x) / x, floor(uv.y * y) / y);
    float2 uv3 = float2(uv2.x + 1 / (x * 2), uv2.y + 1 / (y * 2));
    if (distance(float2(uv.x * width / height, uv.y), float2(uv3.x * width / height, uv3.y)) < l)
    {
        return tex2D(input, uv2);
    }
    return float4(0.0, 0.0, 0.0, 1.0);
}

190228_5.jpg

単純なモザイクではなく一つ一つを円にしています。

おわりに

WPF は (私の主観では) .NET 4.0 を最後に機能向上もせず 10 年経ってしまった、という印象です (今現在でも使えているのはユーザー側の尽力によるものが大きいかと) 。しかし、あの当時で考えると相当先進的な機能を実装しており、ある意味「早すぎた」ものだったような気もします。

現在、グラフィック面の理由から WPF を積極的に使おうという事はそれほどないように思いますが、 ShaderEffect はかなり応用性が高いと思いますので、機会があれば是非活用してみてください。

tan-y
最新記事ははてなブログで。
https://tan-y.hatenablog.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした