マルチビュー

ここではサンプルプロジェクト MultiViewSample を通して、1つのシーンを複数のビューで表示する方法について解説します。 つまり同じ3Dモデルを複数の視点から見ることが可能で、3面図などへの応用も可能です。

リソース

WPFにおいて、複数のUI要素から利用する共通のオブジェクトを使用する仕組みとしてリソースという仕組みがあります。 本サンプルではリソースとしてMoNo.RAILのシーングラフを設定することで、複数の3Dビューから同一のシーングラフを参照しています。

サンプルプログラムのMainWindow.xamlを見ると、Windowのリソースとしてシーングラフを登録し、2つの GLViewport から参照している構造が見て取れます。

<!-- 2つのビューで共有する SceneGraph をリソースとして定義 -->
<Window.Resources>
    <m:SceneGraph x:Key="sceneGraph"> ... </m:SceneGraph>
</Window.Resources>

<!-- 左のビュー -->
<m:GLViewport SceneGraph="{StaticResource ResourceKey=sceneGraph}">
...
</m:GLViewport>

<!-- 右のビュー -->
<m:GLViewport SceneGraph="{StaticResource ResourceKey=sceneGraph}">
...
</m:GLViewport>

ではこれを踏まえてサンプルプロジェクトを見ていきましょう。

MainWindow.xaml

MainWindow.xamlがサンプルプロジェクトのビューになります。 XAML全体は他のサンプルに比べ少々大きめなので要点毎にXAMLの一部を切り出して解説します。

XML名前空間

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

m を接頭辞とすることでMoNo.RAILに定義されたクラスにアクセスできるようにします。

ビューモデルの設定

...
<!-- DataContext にビューモデルを設定 -->
<Window.DataContext>
    <local:MainViewModel/>
</Window.DataContext>
...

WindowのDataContextにビューモデル MainViewModel を設定しています。

MainViewModel はMainViewModel.csで定義されており、これは1つの三角形を表示するシーン Triangle だけをプロパティとして持ちます。

class MainViewModel
{
    public MoNo.Graphics.IScene Triangle { get; } =
        GraphicsUT.CreateScene(GLPrimType.Triangles, new[]
        {
            new Point3f(1, 0, 0),
            new Point3f(0, 1, 0),
            new Point3f(0, 0, 1),
        });
}

リソースの設定

...
<!-- 2つのビューで共有する SceneGraph をリソースとして定義 -->
<Window.Resources>
    <m:SceneGraph x:Key="sceneGraph">
        <!-- ビューモデルの Triangle を Pink 色で描画 -->
        <m:Entry Object="{Binding Path=Triangle}" Color="Pink"/>
    </m:SceneGraph>
</Window.Resources>
...

複数(このサンプルでは2つ)のUI要素共通のデータとしてMoNo.RAILの SceneGraph クラスを sceneGraph をキーにしてリソースに登録します。 シーングラフには1つの Entry を設定し、そのオブジェクトにはビューモデルの Triangle をBindします。 また色をピンクに設定しています。

左のビュー

...
<!-- 左のビュー -->
<m:GLViewport
    Grid.Column="0" Margin="4" Background="DarkBlue"
    SceneGraph="{StaticResource ResourceKey=sceneGraph}">
    <!-- ワールド座標系に座標軸を描画 -->
    <m:GLViewport.WorldScenes>
        <m:AxisScene/>
    </m:GLViewport.WorldScenes>
</m:GLViewport>
...

まず1つ目のビューです。 暗い青を背景色として GLViewport を設置しています。 そしてシーングラフにはリソースに登録したシーングラフをStaticResourceマークアップ拡張で設定しています。

さらに左のビューにはMoNo.RAILで用意されている AxisScene という座標系を描画するシーンを WorldScenes に埋め込んでいます。 WorldScenes はワールド座標系のシーンのコレクションで、シーンを複数配置することができます。

../../_images/multiview_sample_left.png

この WorldScenesSceneGraph とは別に管理されるため、この座標系のように3Dビュー内の補助的な情報を描画するのに向いています。 また、ワールド座標系に配置されるので画面の回転やズームなどのカメラ操作の影響を受けます。

右のビュー

...
<!-- 右のビュー -->
<m:GLViewport
Grid.Column="1" Margin="4" Background="Brown"
SceneGraph="{StaticResource ResourceKey=sceneGraph}">
    <!-- 背景にユーザー定義のシーン(四角形)を描画 -->
    <m:GLViewport.BackgroundScenes>
        <local:ScreenRectScene/>
    </m:GLViewport.BackgroundScenes>
</m:GLViewport>
...

次に2つ目のビューです。 茶色を背景色として GLViewport を配置しています。 左のビューと同様にリソースに登録してあるシーングラフを設定していますので、左のビューに表示されている三角形と同じものが表示されます。

そして右のビューにはサンプルプロジェクト内で定義しているシーン ScreenRectSceneBackgroundScenes に埋め込んでいます。 (後で見ますが、 ScreenRectScene はカーキの四角形を描画するシーンです。)

BackgroundScenes は、背景として描画されるスクリーン座標系のシーンのコレクションです。 また背景として描画されるため、常にシーングラフの背後に描画されます。

../../_images/multiview_sample_right.png

同様に GLViewport には ForegroundScenes という、前景として描画されるスクリーン座標系のシーンコレクションも持っています。 試しに上記XAMLの BackgroundScenesForegroundScenes に変更して実行してみて下さい。 四角形が常に三角形シーンの前景として描画されるはずです。

../../_images/multiview_sample_right2.png

これらはスクリーン座標系に表示されるため、カメラ操作の影響を受けません。 常に表示しておきたい情報などを描画するのに向いているでしょう。

ScreenRectScene

最後にScreenRectSceneを見ておきましょう。

...
class ScreenRectScene : System.Windows.DependencyObject, MoNo.Graphics.IScene
...

まず System.Windows.DependencyObject を継承してXAMLに埋め込めるようにしています。 また、MoNo.Graphics.IScene を実装するMoNo.RAILのシーンでもあります。

readonly MoNo.Graphics.IScene scene =
    GraphicsUT.CreateScene(GLPrimType.Quads, new[]
    {
        new Point3f( 10, 10, 0 ),
        new Point3f( 10, 90, 0 ),
        new Point3f( 90, 90, 0 ),
        new Point3f( 90, 10, 0 ),
    });

単純な四角形のシーンを作成し保持しています。 なお、このシーンはXAMLでスクリーン座標系の背景として登録されますので、画面左上が原点となり、画面右向きがX軸の正の向き、画面下向きがY軸の正の向きとなることに留意しておきましょう。

public void Draw( MoNo.Graphics.ISceneContext sc )
{
    using (var scope = sc.Push())
    {
        scope.Color = System.Drawing.Color.Khaki;
        scene.Draw(sc);
    }
}

IScene を実装するのに必要な Draw メソッドを実装しています。 描画前に、 ISceneContextPush メソッドを呼び出すことで、IScope の実装インスタンス scope を取得しています。 これは描画コンテキストの一部を局所的に変更するため、スタックにPushされる特殊なコンテキストだと考えて下さい。 ここでは色をカーキに変更し、保持していた scene を描画します。 usingステートメントを抜けると Dispose() が呼ばれることで、自動的にスタックからPopされ、以後この scope への変更が影響を与えることはなくなります。

このサンプルプログラムでは他に色を指定せず描画する要素がないため

public void Draw( MoNo.Graphics.ISceneContext sc )
{
    sc.Color = System.Drawing.Color.Khaki;
    scene.Draw(sc);
}

と書いても動作は変わりません。

しかしこのようにしてしまうと描画コンテキストを使用する要素全てに影響が及んでしまうため、全く関係のないはずのシーンに影響を及ぼします。 これは意図せぬ副作用を生む厄介なバグの原因となりますので、個別の描画ではこのように直接コンテキストを変更することはせず、スコープを使用して局所的な変更に留めるようにして下さい。