ビットマップ効果

Last Updated 2011/12/09


.NET Framework 3.5 SP1 の UIElement クラスに Effect プロパティが追加されました。プロパティが追加されることは問題ないのですが、.NET Framework 4.0 では UIElement クラスの BitmapEffect プロパティがなくなっています。.NET Framework 4.0 以後では従来型のビットマップ効果クラスを使ってほしくないということなのでしょうか。ともあれ、このページでは新しいビットマップ効果について説明します。


.NET Framework 4.0 の UIElement クラスの Effect プロパティには System.Windows.Media.Effects.Effect クラスから派生するクラスを設定しなければなりますが、実態としては ShaderEffect クラスから派生するクラスを設定します。つまり、ShaderEffect クラスこそが問題の核心ということです。

まず、ShaderEffect の Shader とは何かから話を始めなくてはなりません。と書いたものの、Shader は DirectX や OpenGL ではなじみがあるらしいのですが、私はそれに対する知識を持ちません。したがって、あくまで .NET Framework の枠内における説明であることをお断りしておきます。

Shader は「影 "shade" を付けるもの」に由来するものと想像しますが、画像を加工する仕組みの総称と考えていいと思います。画像の加工とはたとえば、色調を明るくしたり、モザイクをかけるなどの操作です。

DirectX には以下にリストアップする 3 つの Shader があります。

これらのうち、.NET Framework では Pixel Shader だけをサポートします。これは名前から想像できるように、ピクセル単位で色を調整する機能と考えていいでしょう。たとえば、カラー画像をグレースケールに変換するような機能です。つまり、.NET Framework 的に表現すると、「ビットマップ効果」ですね。

Note 画像に対して Shader を適用することを .NET Framework 4.0 では「ビットマップ効果」と呼ぶことになったと理解してください。

さて、ここから ShaderEffect クラスを中心とするビットマップ効果に関する説明になりますが、まず関連するキーワードを確認しておきましょう。

  1. ShaderEffect クラス
  2. PixelShader クラス
  3. .fx ファイル
  4. HLSL
  5. .ps ファイル

.fx ファイルは「エフェクトファイル」と呼ぶようです。Shader の実際の動作方法を定義するものですが、HLSL "High Level Shading Language" を使って作成します。

Note HLSL のリファレンスは .NET Framework SDK の中にありますが英文のみです。しかも、.NET Framework ではなく、DirectX を対象とする内容なので、非常に分かりにくいものになっています。いずれにしろ、HLSL の説明は別の機会を予定しています。なにせ、まだ概要がよく分かっていませんので。

HLSL を使って作成する .fx ファイルはテキストファイルですが、文字コードは ASCII に限定されています。幸い、英数字と記号に関しては shift-jis と文字コードが同じですから shift-jis ファイルとして作成することができます。ただし、漢字などの日本語を含むことはできません。

Note .fx ファイルをプロジェクトに追加するテキストファイルとして作成すると、その文字コードは UTF-8 になってしまいます。したがって、プロジェクトに追加するファイルとしてではなく、別途ファイルとして作成してください。

先に説明したとおり、.fx ファイルは単なるテキストファイルですからコンパイルしてバイナリイメージに変換しなければなりません。バイナリファイルの拡張子は通常、.ps にします("ps" は "pixel shader" の略でしょうね)。コンパイラ(fxc.exe)は Visual Studio ではなく、DirectX SDK に付属しています。これは、Microsoft のサイトから無料でダウンロードできます。2011 年 11 月の時点における DirectX の最新バージョンは "June 2010" です。

DirectX SDK をインストールすると、コンパイラは私の環境では以下のディレクトリ内にあります。

  C:\Program Files\Microsoft DirectX SDK (June 2010)\Utilities\bin\x86\fxc.exe

コンパイル時の構文は以下のとおりです。入力ファイルは Test.fx で出力ファイルは Test.ps です。ps_2_0 は PixelShader のバージョンですが、.NET Framework 4.0 の場合は 2.0 にしておくほうが安全です。

  fxc Test.fx /T ps_2_0 /Fo Test.ps

PixelShader を作成する基本的な手順

文章でダラダラと説明するよりも具体的な例に基づくほうが分かりやすいと思います。実用性のない例ですが、.NET Framework SDK の ShaderEffect クラスの項の使用例にならってサンプルコードを作成してみました。

.fx ファイルの作成

まず、HLSL を使って .fx ファイルを作成します。テキストの編集はいつも使っているテキストエディタを利用してください。文字コードは shift-jis です。以下のコードを ThresholdEffect.fx として保存してください。

float threshold : register(c0);

sampler2D input : register(s0);
float4 blankColor : register(c1);

float4 main(float2 uv : TEXCOORD) : COLOR
{
  float4 color = tex2D(input, uv);
  float intensity = (color.r + color.g + color.b) / 3;

  float4 result;

  if (intensity > threshold)
  {
    result = color;
  }
  else
  {
    result = blankColor;
  }

  return result;
}

.fx ファイルのコンパイル

.fx ファイルをコンパイルし、.ps ファイルを作成します。コンパイル時の構文は以下のとおりです。

  fxc ThresholdEffect.fx /T ps_2_0 /Fo ThresholdEffect.ps

.ps ファイルをプロジェクトに追加する

上記の手順で作成した .ps ファイルをプロジェクトに追加しますが、追加しただけでは十分ではありません。ソリューションエクスプローラにリストアップされた .ps ファイルのプロパティを開くと、[ビルドアクション] は「なし」になっているはずです。それを "Resource" に変更してください。これで EXE ファイルのリソースとして組み込むことができます。

PixelShader クラス

.ps ファイルができたところで、次に PixelShader クラスのインスタンスを作成し、その UriSource プロパティに .ps ファイルへの URI を設定します。URI の書式は以下のとおりです。assemblyName については後述する ThresholdEffect.cs のコードを参照してください。

  string uri = "pack://application:,,,/assemblyName;component/ThresholdEffect.ps";

  PixelShader pixelShader = new PixelShader();
  pixelShader.UriSource = new Uri(uri, UriKind.RelativeOrAbsolute);

ちなみに、component/ 以下の .ps ファイル名は相対パスが可能です。

ShaderEffect クラス

ShaderEffect クラスは Shader の .NET Frameworkとしての本体です。DirectX と .NET Framework とを結びつける機能を持つからです。このクラスから派生するクラスを作成し、そのインスタンスを UIElement クラスの Effect プロパティに設定することで Shader を有効にすることができます。

以下のコードを ThresholdEffect.cs として保存してください。

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Effects;

namespace ThresholdEffectTest
{
  public class ThresholdEffect : ShaderEffect
  {
    // Input プロパティをサンプラレジスタ S の 0 に登録する
    public static readonly DependencyProperty InputProperty = ShaderEffect.RegisterPixelShaderSamplerProperty(
        "Input", typeof(ThresholdEffect), 0);

    // Threshold プロパティを float 型定数レジスタ C の 0 に登録する(デフォルト値は 0.5)
    public static readonly DependencyProperty ThresholdProperty = DependencyProperty.Register(
        "Threshold", typeof(double), typeof(ThresholdEffect),
            new UIPropertyMetadata(0.5, PixelShaderConstantCallback(0)));

    // このプロパティの実態はよく分からないが、必須らしい
    // Color 型ではなく、Brush 型であることに留意
    public Brush Input
    {
      get { return (Brush)GetValue(InputProperty); }
      set { SetValue(InputProperty, value); }
    }

    // Threshold(しきい値)をプロパティとした(デフォルトは 0.5 だが、値を変えるとその効果のほどが分かる)
    public double Threshold
    {
      get { return (double)GetValue(ThresholdProperty); }
      set { SetValue(ThresholdProperty, value); }
    }

    //---------------------------------------------------------------------------------------------
    // コンストラクタ
    public ThresholdEffect()
    {
      // .NET Framework SDK の使用例の方法よりもスマートだと思うし、これが本来の手順だと思う
      System.Reflection.Assembly a = typeof(ThresholdEffect).Assembly;
      string assemblyName = a.GetName().Name;

      string uri = "pack://application:,,,/" + assemblyName + ";component/ThresholdEffect.ps";
      var pixelShader = new PixelShader();

      try
      {
        pixelShader.UriSource = new Uri(uri, UriKind.RelativeOrAbsolute);
      }
      catch (Exception ex)
      {
        MessageBox.Show(ex.Message);
      }

      pixelShader.Freeze(); // これはなくても正常に動作するようだ

      this.PixelShader = pixelShader;

      // 指定のプロパティに対応する Shader のプロパティ値を更新する
      // これはお約束の手順のようで、必須らしい
      UpdateShaderValue(InputProperty);
      UpdateShaderValue(ThresholdProperty);
    }
  } // end of ThresholdEffect class
} // end of namespace

このファイルの内容は一つのパターンしか持ちません。したがって、ThresholdEffect.cs をテンプレートとして使うことができます。

ThresholdEffect を適用する

下図は、テスト用アプリケーションを起動したところで、左は通常表示、右は Shader を設定したものです。分離コードはデフォルトのままでかまいません。

ThresholdEffect


<Window x:Class="ThresholdEffectTest.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:ThresholdEffectTest"
    Title="ThresholdEffect のテスト" Height="250" Width="480">

  <StackPanel Orientation = "Horizontal">
    <Image Source="images/Garden.jpg" Width="200" Stretch="Uniform" Margin="20" />
    <Image Source="images/Garden.jpg" Width="200" Stretch="Uniform" Margin="0,20,20,20">
      <Image.Effect>
        <local:ThresholdEffect Threshold="0.4" />
      </Image.Effect>
    </Image>
  </StackPanel>
</Window>

PixelShader のサンプルコード

Shader に関係するサンプルコードが次のサイトで公開されています。

Windows Presentation Foundation Pixel Shader Effects Library

http://wpffx.codeplex.com/

ダウンロードするタイトルは、WPFSLFx です。

ただし、私の環境では正常にコンパイルできませんでした。何か手順を間違えたのかもしれません。ところで、Shader の使い方を説明するためのサンプルコードとしては適切な感じがしません。あまりにもアプリケーションの規模が大きすぎて、ポイントを絞り込むことができないからです。

また、WEB サイトで公開されているサンプルコードの多くが、WPFSLFx と同じ人物の手になるものと考えられます。公開するコードとしてはいわゆる、クセがありすぎて非常に読みにくいものになっています。コメントの量は多いのですが、ほとんど役に立ちません。ちなみに、.NET Framework SDK の使用例もこの人物の手になるものだと思います。

Note これは私の個人的な感想ですが、ほかに適任者はいなかったのでしょうか。

ともあれ、Shader のサンプルが少ないので、参考にはなると思います(実感としてはチョット ? ではありますが)。

HLSL のサンプル

Shader を使うビットマップ効果は、HLSL を使ってエフェクトファイル(.fx ファイル)をどのように作成するかがすべてです。そこで、HLSL のヒントになればと考え、サンプルコードを作ってみました。HLSL のサンプルのページを参照してください。


HlslPad

Shader を使う上でのポイントは、.fx ファイルの作成にあります。先に説明したとおり、HLSL の使い方がヤヤコシイので、試行錯誤が必要です。そこで、HLSL をテストするツールを作成しました。下図は HlslPad を起動したところです。左の画像はオリジナルで、右の画像にサンプルの Shader を適用しました。この例ではグレースケール化しています。グローバル変数の C0 と C1 をスライダコントロールを使って変化させる機能も付けておきました。

HlslPad
Note あとで気付きましたが、似たようなツールがいくつか公開されています。気に入ったものをお使いください。NETClass または WPFClass のユーザーさんにはソースコードを公開しますので、自由に変更して利用してください。

このツールは、FX ファイルの内容をテキストボックスに入力し、コンパイル(ツールが実行する)後、指定の画像に Shader を適用します。なお、HlslPad は Tools のページにあります。


Shader 適用後の画像をファイルに保存

Shader は画像データを内部的に操作するだけですので、Shader を適用した画像を直接ファイルとして保存することはできません。たとえば、Image コントロールの Source プロパティに画像ファイルを設定し、Image コントロールの Effect プロパティに Effect オブジェクトを適用するとします。この状態で Source プロパティの内容をファイルに保存しても、Shader を適用する前の元の画像でしかありません。これは Effect プロパティに null を設定すると Shader の効果が無効になることからも理解できます。

では、Shader は画像ファイルの加工のために使えないのでしょうか。実は、裏技的には可能です。

RenderTargetBitmap クラスを使うと、Visual オブジェクトを BitmapSource オブジェクトに変換することができます。ここまでくれば、JpegBitmapEncoder クラスなどの BitmapEncoder クラスから派生するクラスを利用すると、希望の形式の画像ファイルとして保存できます。

以下のコードは、FrameworkElement オブジェクトをその内容を含めて画像ファイルに保存するメソッドです。ここではファイルの形式として PNG ファイルに固定ですが、JPEG などのほかの形式のほうがよければコードを変更して使ってください。

// FrameworkElement オブジェクトのイメージをファイルに保存する
// element  : FrameworkElement オブジェクト
// fileName : 出力ファイル名(.png)
private void SaveVisualToFile(FrameworkElement element, string fileName)
{
  var bitmap = new RenderTargetBitmap((int)element.ActualWidth, (int)element.ActualHeight,
    96, 96, PixelFormats.Pbgra32);

  bitmap.Render(element);
  BitmapFrame frame = BitmapFrame.Create(bitmap);
  frame.Freeze();

  var stream = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.Write);
  var encoder = new PngBitmapEncoder(); // PNG ファイルとして保存する
  encoder.Frames.Add(frame);

  try
  {
    encoder.Save(stream);
  }
  catch (Exception ex)
  {
    MessageBox.Show(ex.Message);
  }
  finally
  {
    stream.Close();
  }
}

ところで、古い JPEG の中には 72 DPI の環境で作ったものがあります。それを 96 DPI の環境で表示するとサイズが大きくなりますので、元のサイズで保存するには調整が必要です。

Shader を適用した画像をファイルに保存するツールを作ってみました。Tools のページにある HlslPad3 がそうです。これは元の画像自体に PixelShader を適用した状態でファイルに保存します。


−以上−