MoNo.Wpf.GLViewport

GLViewportMoNo.Graphics.GLViewControl をラップしたWPFコントロールです。

  • XAML にこのコントロールを埋め込むことができます。
  • XAML に表示対象の SceneGraph を埋め込むことができます。
  • 表示対象のエンティティや表示色などは Data Binding によってビューモデルと連携することができます。
  • エンティティを描画するためのシーンオブジェクトをプラグイン機構によって組み込むことができます。

クラス図

GLViewportSceneGraph およびその周辺のクラス図を下記に示します。

../../_images/wpf_classdiagram.png

要点を箇条書きでまとめます。

  • GLViewport は描画対象として SceneGraph を1つ参照します。
  • シーングラフのノードはツリー構造を作ります。 SceneGraph がツリー構造のルートとなり、その下に Entry が作るツリー構造がぶら下がります。
  • EntryObject プロパティに描画対象のエンティティ(例えば直線とか円弧とかメッシュとか)が保持されます。
  • EntryScene プロパティには、Object を描画するためのシーンオブジェクトが設定されます。ObjectIScene インターフェイスを実装している場合には Object 自身がシーンオブジェクトとなり、そうでない場合はプラグインによって関連付けられているシーンオブジェクトが生成され設定されます。(プラグインについては後述)
  • NodeSceneGraphEntry の共通部分を抽出した親クラスです。
  • NodeFreezable の派生クラスとして定義されていることにより、シーングラフを XAML に埋め込んで Data Binding 機能を利用することが可能となっています。
  • ほとんどのプロパティは Dependency Property として定義されており、Data Binding によってビューモデルと連携することができます。

シーンのプラグイン

WPF の話題に入る前に、シーンのプラグインについて説明しておきます。 (プラグイン機構については プラグイン機構 を御覧ください。)

次のようなエンティティ型があるものとします。

type SampleEntity() =
  member val P1 = Point3d (1.0, 0.0, 0.0)
  member val P2 = Point3d (0.0, 1.0, 0.0)
  member val P3 = Point3d (0.0, 0.0, 1.0)

これを描画するためのシーンをプラグインによって定義するには、次のように記述します。

open System.ComponentModel.Composition

[<Export(typeof<MoNo.Graphics.ISceneFactory>)>]
type SampleEntitySceneFactory () =
  inherit MoNo.Graphics.AbstractSceneFactory<SampleEntity> ()
  override __.Create (target, _) =
    let nrm = ((target.P2 - target.P1) * (target.P3 - target.P1)).Normalize()
    GraphicsUT.CreateScene (
      MoNo.OpenGL.GLPrimType.Triangles,
      [| PointNormal3d (nrm, target.P1)
         PointNormal3d (nrm, target.P2)
         PointNormal3d (nrm, target.P3) |])

なお、 Export 属性を利用するには参照設定に System.ComponentModel.Composition.dll を追加する必要があります。 プラグイン機構を利用するプロジェクトでは忘れないように参照設定に追加して下さい。

SampleEntity オブジェクトから対応するシーンオブジェクトを得るには、次のように記述します。

let scene = MoNo.Graphics.SceneFactory.NewInstance (SampleEntity ())

以上のプラグイン機構を踏まえて、次のサンプルプロジェクトの説明に入っていきます。

GLViewportSample

MoNo.RAIL.Samples に GLSamples/GLViewportSample というプロジェクトを用意しました。 このサンプルプロジェクトに沿って説明していきます。

参照設定

サンプルプロジェクトに追加されている参照設定は以下の通りです。

  • MoNo.dll
  • MoNo.Basics.dll
  • MoNo.Framework.dll
  • MoNo.OpenGL.dll
  • MoNo.Wpf.dll
  • System.ComponentModel.Composition.dll

特にMoNo.RAILで3D描画を扱うため、OpenGL APIをラップした MoNo.OpenGL.dll と、表示用のモジュール MoNo.Wpf.dll が必要です。 また、MoNo.RAILのモジュールではありませんが、前述したシーンのプラグインを実現するために System.ComponentModel.Composition.dll を追加する必要があります。

なお、本サンプルではF#のコードは使用していないため MoNo.FSharp.dll や MoNo.Framework.FSharp.dll は追加していません。

ビュー

まずはビューであるMainViewModel.xamlを見てみましょう。

<Window x:Class="GLViewportSample.MainWindow"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                xmlns:local="clr-namespace:GLViewportSample"
                xmlns:m="http://rail.monocommunity.com"
                mc:Ignorable="d"
                Title="MainWindow" Height="350" Width="525">
        <Window.DataContext>
                <local:MainViewModel/>
        </Window.DataContext>
        <m:GLViewport Background="Black">
                <m:GLViewport.Lights>
                        <m:Light IsEnabled="True" Diffuse="Gray" Ambient="Black"
                                        Position="1, 1, 1, 0"/>
                </m:GLViewport.Lights>
                <m:SceneGraph>
                        <m:Entry Object="{Binding Path=Scene1}"/>
                        <m:Entry Object="{Binding Path=Entity1}"/>
                </m:SceneGraph>
        </m:GLViewport>
</Window>

それではこのXAMLを順番に解説していきます。

...
xmlns:m="http://rail.monocommunity.com"
...

XML名前空間を設定し、 m を接頭辞とすることでMoNo.RAILに定義されたクラスにアクセスできるようにしています。

...
<Window.DataContext>
        <local:MainViewModel/>
</Window.DataContext>
...

ここでWindowのDataContextにサンプルプロジェクト内のMainViewModelを設定しています。 このMainViewModelについては後で見ていきます。

...
<m:GLViewport Background="Black">
        <m:GLViewport.Lights>
                <m:Light IsEnabled="True" Diffuse="Gray" Ambient="Black"
                                Position="1, 1, 1, 0"/>
        </m:GLViewport.Lights>
        <m:SceneGraph>
                <m:Entry Object="{Binding Path=Scene1}"/>
                <m:Entry Object="{Binding Path=Entity1}"/>
        </m:SceneGraph>
</m:GLViewport>
...

ここでMoNo.RAILで定義されているUIコントロール GLViewport を埋め込んでいます。 GLViewport は1つの SceneGraph を埋め込むことができ、埋め込んだシーングラフを描画するためのコントロールです。

Lights プロパティには1つのライトを設定しています。 ここで設定されているライトは拡散光の色として灰色を、環境光として黒が設定されたライトで、原点から見て(1, 1, 1)の方向からの平行光源として配置しています。

SceneGraph には Entry オブジェクトをツリー状に配置することができます。また Entry クラスは Object プロパティにビューモデルの任意のオブジェクトをBindすることができます。 ここでは2つの Entry が登録されており、それぞれの Object プロパティにはビューモデルの Scene1Entity1 がBindされていることがわかります。

ビューモデル

では次にビューモデルであるMainViewModel.csを見てみましょう。 ビューにBindされている2つのプロパティ Scene1Entity1 だけが実装されているシンプルなビューモデルです。

(通常MoNo.RAILではビューはC#で作成し、ビューモデルはF#で記述することを推奨していますが、本サンプルでは簡便のため単一のC#プロジェクトにビューモデルを作成しています。)

...
public MoNo.Graphics.IScene Scene1
{
        get
        {
                return MoNo.GraphicsUT.CreateScene(
                        MoNo.OpenGL.GLPrimType.Lines,
                        new[] { MoNo.Point3d.Zero, new MoNo.Point3d(1, 2, 3) });
        }
}
...

Scene1 プロパティはユーティリティクラスである MoNo.GraphcisUTCreateScene メソッドにより IScene 実装インスタンスを返します。 ここでは原点と(1, 2, 3)を結ぶ線分を描画するシーンが作成され返されます。

EntryObject プロパティにBindされたもうひとつのプロパティ Entity1 を見てみましょう。

...
public object Entity1
{
        get { return new SampleEntity(); }
}
...

サンプルプログラムで作成した型である SampleEntiry を返しています。 これはSampleEntity.csで実装されている、3点だけを格納するクラスで IScene インターフェースも実装していません。

class SampleEntity
{
        public Point3d P1 { get; } = new Point3d(1, 0, 0);
        public Point3d P2 { get; } = new Point3d(0, 1, 0);
        public Point3d P3 { get; } = new Point3d(0, 0, 1);
}

しかし、サンプルプログラムを実行すると Scene1 で作成された線分の他に、三角形が描画されていることがわかります。

../../_images/sample_app.png

つまりこの三角形は SampleEntityObject に設定した Entry がシーングラフに追加されていることにより描画されていることになります。 このシーンはどこで作成されているのでしょうか?

その秘密はこの項の最初で説明したMoNo.RAILのプラグイン機構にあります。

シーンファクトリのプラグイン

SampleEntity.csに定義されているもうひとつのクラスを見てみましょう。

[System.ComponentModel.Composition.Export(typeof(MoNo.Graphics.ISceneFactory))]
class SampleEntitySceneFactory : MoNo.Graphics.AbstractSceneFactory<SampleEntity>
{
        protected override IScene Create( SampleEntity target, object[] args )
        {
                Console.WriteLine("SampleEntitySceneFacotry.Create()");
                var nrm = ((target.P2 - target.P1) * (target.P3 - target.P1)).Normalize();
                return GraphicsUT.CreateScene(
                        MoNo.OpenGL.GLPrimType.Triangles,
                        new[] { new PointNormal3d(nrm, target.P1), new PointNormal3d(nrm, target.P2), new PointNormal3d(nrm, target.P3) });
        }
}

これが SampleEntity のためのシーンを作成するクラスです。 Create メソッドで SampleEntity を引数に取り、保持している3点から法線を持つ三角形のシーンを作成しています。

MEFの作法に従い [Export(typeof(MoNo.Graphics.ISceneFactory))] 属性をつけることにより、このクラスはシーンファクトリクラスとしてプラグインされます。 また MoNo.Graphics.AbstractSceneFactory を継承し、そのジェネリック引数で渡された型 SampleEntity に対するシーンファクトリクラスとして関連付けされます。

MoNo.RAILはシーングラフ中の Entry を辿り、そのオブジェクトが IScene でない場合、その型に関連付けられたシーンファクトリクラスからシーンを作成して描画します。 (関連付けられたシーンファクトリクラスがない場合、画面には何も描画されません。)

この仕組みにより SampleEntity に対応するシーンが自動的に作成されるのです。

サンプルプロジェクト内のApp.xaml.csにも SampleEntity に関連付けられたファクトリクラスがシーンを作成するコードが記述してあります。

var scene = MoNo.Graphics.SceneFactory.NewInstance(new SampleEntity());
Console.WriteLine(scene.GetType());

ここで MoNo.Graphics.SceneFactory.NewInstance メソッドに SampleEntity インスタンスを渡しています。 MoNo.RAILは SampleEntity に関連付けられた SampleEntitySceneFacotry を見つけ出し、その Create メソッドを呼び出します。

試しに上記コードの NewInstance メソッドを呼び出す箇所と SampleEntitySceneFacotryCreate メソッドにブレークポイントを置いて実行してみて下さい。 NewInstance の中からプラグインされた SampleEntitySceneFactoryCreate メソッドが呼ばれていることがわかると思います。