Special Windows 10 issue 2015

Volume 30 Number 11

グラフィックスとアニメーション - Windows 合成 が 10 歳になる

Kenny Kerr | Windows 2015

デスクトップ ウィンドウ マネージャー (DWM) とも呼ばれる Windows 合成エンジンに、Windows 10 用の新しい API が登場しました。DirectComposition は合成に使用する主要インターフェイスでしたが、昔ながらの COM API だったため、一般的なアプリ開発者はほとんど利用できませんでした。新しい Windows 合成 API は Windows ランタイム (WinRT) を基盤とし、高パフォーマンスなレンダリングの土台となります。これは、Direct2D と Direct3D によるイミディエイト モードのグラフィックスの世界が、アニメーションや効果の機能が大幅に向上した保持モードのビジュアル ツリーと融合しているためです。

私が DWM について初めて取り上げたのは、Windows Vista がベータ版だった 2006 年の記事 (goo.gl/19jCyR、英語) でした。当時は DWM を使用すると、特定のウィンドウに対するぼかし効果の程度を制御して、デスクトップとうまく調和したカスタム クロムを作成することができました。図 1 は、この成果が Windows 7 で頂点に達したところを示しています。Direct3D と Direct2Dでハードウェア アクセラレータによるレンダリングを実現して、アプリケーションに魅力的な外観を作り出すことが可能でした (goo.gl/IufcN1、英語)。さらに、GDI やユーザー コントロールによる従来の処理を DWM と融合することもできました (goo.gl/9ITISE、英語)。それでも、注意深く観察すればおわかりだったとおり、DWM には大きな改善の余地がありました。Windows 7 の Windows フリップ 3D 機能は、その良い証拠でした。

Windows Aero
図 1 Windows Aero

Windows 8 では、DirectComposition という DWM 用の新しい API が導入されました。この API は、設計のヒントとなった従来型 COM API の DirectX ファミリにちなんで命名されています。DirectComposition の登場によって、DWM の機能を開発者が把握しやすくなりました。また、名前もわかりやすくなりました。DWM は本当に Windows 合成エンジンとなり、デスクトップ ウィンドウのレンダリング方法が根本的に変わったために Windows Vista や Windows 7 の華々しい効果を生むことが可能でした。既定では、合成エンジンはトップレベル ウィンドウあたり 1 つのリダイレクト サーフェスを作成していました。この動作については、2014 年 6 月号のコラムで詳しく説明しています (goo.gl/oMlVa4)。このようなリダイレクト サーフェスがビジュアル ツリーの一部を構成していたため、DirectComposition では、まさに同じテクノロジをアプリで使用して、高パフォーマンスのグラフィックを実現する軽量な保持モード API を提供できました。DirectComposition に用意されていたビジュアル ツリーとサーフェスの管理機能を使用すると、効果やアニメーションの作成をアプリの代わりに合成エンジンで実行できました。こうした機能については、2014 年 8 月号のコラム (goo.gl/CNwnWR) と 9 月号のコラム (goo.gl/y7ZMLL) で解説しています。また、Pluralsight (goo.gl/fgg0XN) では、DirectComposition を使用した高パフォーマンスのレンダリングに関するコースも開講しています。

Windows 8 の登場により、DirectComposition や、DirectX ファミリのその他の API に対するすばらしい機能強化が提供されただけではなく、開発者の OS に対する考え方が一変するような Windows API の新時代も到来しました。Windows ランタイムの導入には、他のすべての影がかすむほどのインパクトがありました。マイクロソフトがアプリのビルドや OS サービスへのアクセスに関するまったく新しい方法を約束した結果、いわゆる Win32 API は使用されなくなっていきました。Win32 API は、アプリケーションのビルドや OS との対話に関してそれまで長年にわたって主流を占めてきた方法です。Windows 8 の滑り出しは厳しかったものの、Windows 8.1 では多数の問題を修正しました。さらに Windows 10 では、大幅に包括的になった API を提供するようになったため、Windows 向けの一流アプリケーションやそれどころか重大なアプリケーションのビルドに関心がある、はるかに多くの開発者が満足できるようになっています。

Windows 10 は 2015 年 7 月にリリースされましたが、搭載されていた新しい合成 API はプレビュー版で、運用環境ではまだ使用できませんでした。合成 API には変更される可能性が残っていたため、Windows ストアに提出するユニバーサル Windows アプリでは使用できませんでした。これは適切な措置だったと言えます。なぜなら、運用環境で使用できるようになった合成 API は、大幅に変更され、改良されているためです。今回の Windows 10 の更新プログラムでは、初めてすべてのフォーム ファクターで同じ合成 API を使用できるようにもなり、ユニバーサル Windows プラットフォームのユニバーサルな面に対する信頼が高まっています。マルチディスプレイの高パフォーマンスなデスクトップ コンピューターとポケットに収まる小さなスマートフォンのどちらを対象にする場合も、合成は同じように動作します。

当然ながら、だれもが認める Windows ランタイムの特長は、Windows 向け共通言語ランタイムの公約がついに果たされていることです。C# でコーディングする開発者は、Microsoft .NET Framework に組み込まれたサポートを利用して直接 Windows ランタイムを使用できます。また、私のように C++ を使用する開発者は、中間言語や煩わしい抽象化を使用しなくても Windows ランタイムを使用できます。Windows ランタイムは .NET ではなく COM を基盤としているため、C++ を使用するには最適です。この記事では標準的な C++ の言語プロジェクションである Windows ランタイム用の Modern C++ (moderncpp.com、英語) を使用しますが、API はどの言語でも同じなので、お好みの言語で学習できます。さらに、今回は C# での例もいくつか紹介して、さまざまな言語が Windows ランタイムでシームレスにサポートされていることを示します。

Windows 合成 API は、起源となった DirectX とは距離を置いています。DirectComposition では、Direct3D デバイスや Direct2D デバイスをモデルとしたデバイス オブジェクトを提供していましたが、新しい Windows 合成 API の基本はコンポジターです。ただし、Windows 合成 API も、合成リソースのファクトリとして機能するという同じ目的を果たします。しかも Windows 合成は DirectComposition によく似ています。どちらにも、ウィンドウとビジュアル ツリーの間における一対一の関係を表す合成ターゲットがあります。ビジュアルについて詳しく調べると、違いがはっきりしてきます。DirectComposition のビジュアルには、ある種のビットマップを提供するコンテンツ プロパティがありました。提供するビットマップは、合成サーフェス、DXGI スワップ チェーン、別のウィンドウのリダイレクト サーフェスの 3 つのうちいずれかでした。一般的な DirectComposition アプリケーションはビジュアルとサーフェスで構成され、サーフェスがさまざまなビジュアルのコンテンツやビットマップとして機能していました。図 2 に示すように、合成ビジュアル ツリーはやや別物です。新しいビジュアル オブジェクトにはコンテンツ プロパティがなく、代わりに合成ブラシでビジュアル オブジェクトをレンダリングします。そのため、抽象化の柔軟性が高まっています。ブラシによるビットマップのレンダリング方法は従来どおりですが、シンプルな単色ブラシを非常に効率的に作成でき、複雑なブラシも定義できます。この手法は、少なくとも概念の上では、画像として取り扱い可能な効果を生む Direct2D の手法とよく似ています。適切に抽象化すれば、大きな違いが生まれます。

Windows 合成のビジュアル ツリー
図 2 Windows 合成のビジュアル ツリー

実践的な例をいくつか示しながら、こうしたしくみ全体について説明し、実行できる処理を簡単に紹介していきましょう。既に述べたとおり、お好みの WinRT 言語プロジェクションを選択できます。Modern C++ でコンポジターを作成する方法は、次のとおりです。

using namespace Windows::UI::Composition;
Compositor compositor;

同様に、C# でも同じ処理を実行できます。

using Windows.UI.Composition;
Compositor compositor = new Compositor();

C++/CX の壮麗な構文を使用することさえできます。

using namespace Windows::UI::Composition;
Compositor ^ compositor = ref new Compositor();

どの構文も API を中心に考えれば同等であり、言語プロジェクションの違いはほとんど反映されていません。現在、ユニバーサル Windows アプリを作成する方法は主に 2 つあります。おそらく最も一般的な方法は、OS の Windows.UI.Xaml 名前空間を使用することです。XAML がアプリでそれほど重要ではない場合は、XAML に依存せず、基盤となるアプリ モデルを直接使用することもできます。WinRT アプリ モデルについては、2013 年 8 月号のコラム (msdn.microsoft.com/ja-jp/magazine/dn342867.aspx) で説明しています。この方法を使用すると、IFrameworkView インターフェイスと IFrameworkViewSource インターフェイスの最小限の実装だけで十分になります。図 3 は、開発の出発点に使用できる C# の基本構造を示しています。Windows 合成を使用すると XAML との密接な統合も利用できますが、まずは XAML を使用しないシンプルなアプリを見ていきましょう。こちらのアプリの方が、合成を学ぶための実験台としてわかりやすいためです。XAML については後で説明します。

図 3 C# での Windows ランタイム アプリ モデル

using Windows.ApplicationModel.Core;
using Windows.UI.Core;
class View : IFrameworkView, IFrameworkViewSource
{
  static void Main()
  {
    CoreApplication.Run(new View());
  }
  public IFrameworkView CreateView()
  {
     return this;
  }
  public void SetWindow(CoreWindow window)
  {
    // Prepare composition resources here...
  }
  public void Run()
  {
    CoreWindow window = CoreWindow.GetForCurrentThread();
    window.Activate();
    window.Dispatcher.ProcessEvents(CoreProcessEventsOption.ProcessUntilQuit);
  }
  public void Initialize(CoreApplicationView applicationView) { }
  public void Load(string entryPoint) { }
  public void Uninitialize() { }
}

コンポジターを作成する場所は、アプリの SetWindow メソッド内です (図 3 参照)。実際、このタイミングがアプリのライフサイクルにおいてコンポジターを作成できる最も早い時点です。これは、コンポジターがウィンドウのディスパッチャーに依存しており、この時点でようやくウィンドウとディスパッチャーの両方が存在するようになるためです。ここで合成ターゲットを作成して、コンポジターとアプリ ビューの関係を確立します。

CompositionTarget m_target = nullptr;
// ...
m_target = compositor.CreateTargetForCurrentView();

合成ターゲットをアプリで保持することは非常に重要なので、必ず合成ターゲットを IFrameworkView 実装のメンバー変数にしてください。既に述べたように、合成ターゲットは、ウィンドウやビューとそのビジュアル ツリーの間における一対一の関係を表しています。合成ターゲットに対して実行できる操作は、ルート ビジュアルを設定することだけです。通常、ルート ビジュアルはコンテナー ビジュアルです。

ContainerVisual root = compositor.CreateContainerVisual();
m_target.Root(root);

前の例では、C++ を使用しています。C++ にはプロパティの言語サポートがないため、Root プロパティにアクセサー メソッドとしてプロジェクションを実行しています。C# のコードもそっくりですが、プロパティ構文が追加されます。

ContainerVisual root = compositor.CreateContainerVisual();
m_target.Root = root;

DirectComposition に用意されていたビジュアルは 1 種類だけでした。この 1 種類のビジュアルで、ビットマップ コンテンツを表すさまざまな種類のサーフェスをサポートしていました。Windows 合成には、さまざまな種類のビジュアル、ブラシ、およびアニメーションを表す小規模なクラス階層が用意されています。ただし、サーフェスは 1 種類だけで、サーフェスの作成に使用できるのは C++ だけです。これは、サーフェスが含まれている Windows 合成相互運用 API が、XAML などのフレームワークや、より洗練されたアプリ開発者による使用を目的としているためです。

図 4 にビジュアルのクラス階層を示します。CompositionObject は、コンポジターに基づいたリソースです。どの合成オブジェクトでも、プロパティをアニメーションにできます。ビジュアルには、ビジュアルの相対位置、外観、クリッピング、レンダリングに関するオプションのさまざまな性質を管理するための、多数のプロパティがあります。たとえば、変換行列プロパティ、拡大縮小や回転のショートカットなどがあります。。これは強力な基底クラスです。一方、ContainerVisual は、Children プロパティを追加するだけの比較的シンプルなクラスです。コンテナー ビジュアルは開発者が直接作成できますが、SpriteVisual では、ブラシを関連付ける機能を追加してビジュアルで独自のピクセルをレンダリングできるようにします。

合成ビジュアル
図 4 合成ビジュアル

ルート コンテナー ビジュアルを指定すると、任意の数の子ビジュアルを作成できます。

VisualCollection children = root.Children();

子ビジュアルもコンテナー ビジュアルにできますが、多くの場合はスプライト ビジュアルにします。たとえば、C++ の for ループを使用して、3 つのビジュアルをルート ビジュアルの子として追加できます。

using namespace Windows::Foundation::Numerics;
for (unsigned i = 0; i != 3; ++i)
{
  SpriteVisual visual = compositor.CreateSpriteVisual();
  visual.Size(Vector2{ 300.0f, 200.0f });
  visual.Offset(Vector3{ 50 + 20.0f * i, 50 + 20.0f * i });
  children.InsertAtTop(visual);
}

図 5 のようなアプリ ウィンドウをすぐに想像されたことでしょうが、このコードを実行しても何もレンダリングされません。ビジュアルに関連付けられたブラシがないためです。ブラシのクラス階層を図 6 に示します。CompositionBrush は、単にブラシの基底クラスであり、独自の機能はありません。CompositionColorBrush クラスはきわめてシンプルで、単色のビジュアルをレンダリングするための色プロパティだけを備えています。これはあまり興味深く感じられないでしょうが、この色プロパティにアニメーションを接続できることも忘れないでください。CompositionEffectBrush クラスと CompositionSurfaceBrush クラスも同様のブラシですが、他のリソースでサポートされているため、より複雑です。CompositionSurfaceBrush クラスは、アタッチされたビジュアルの合成サーフェスをレンダリングします。このクラスには、サーフェス自体はもちろん、補間、配置、伸縮など、ビットマップの描画を管理するさまざまなプロパティがあります。CompositionEffectBrush クラスは、多数のサーフェス ブラシを受け取ってさまざまな効果を生み出します。

ウィンドウに表示された複数の子ビジュアル
図 5 ウィンドウに表示された複数の子ビジュアル

合成ブラシ
図 6 合成ブラシ

色ブラシの作成と適用は簡単です。C# での例は次のとおりです。

using Windows.UI;
CompositionColorBrush brush = compositor.CreateColorBrush();
brush.Color = Color.FromArgb(0xDC, 0x5B, 0x9B, 0xD5);
visual.Brush = brush;

Color 構造体は Windows.UI 名前空間に所属し、アルファ、赤、緑、および青を 8 ビットの色値として表現しています。この点は、DirectComposition や Direct2D でよく使用されていた浮動小数点数の色値よりも進化しています。ビジュアルやブラシに関するこの手法の長所は、色プロパティをいつでも変更でき、同じブラシを参照しているすべてのビジュアルが自動的に更新されることです。しかも、先ほど簡単に述べたとおり、色プロパティもアニメーションにできます。では、どのようなしくみになっているのでしょう。これを明らかにするには、アニメーション クラスについて説明する必要があります。

アニメーションのクラス階層を図 7 に示します。CompositionAnimation 基底クラスには、式で使用する名前付きの値を保存する機能があります。式についてはすぐ後で説明します。KeyFrameAnimation には、実行時間、反復、停止動作など、一般的なキーフレーム ベースのアニメーション プロパティがあります。さまざまなキーフレーム アニメーション クラスに、キーフレームを挿入する型固有のメソッドや、型固有のアニメーション プロパティが用意されています。たとえば、ColorKeyFrameAnimation クラスを使用すると、複数の色値と、キーフレーム間の補間用に色空間を管理するプロパティを指定して、キーフレームを挿入できます。

合成アニメーション
図 7 合成アニメーション

アニメーション オブジェクトを作成してオブジェクトのアニメーションを特定の合成オブジェクトに適用することは非常に簡単です。たとえば、ビジュアルの不透明度をアニメーションにするとします。その場合は、次のように C++ でスカラー値をそのまま使用して、不透明度を 50% に設定できます。

visual.Opacity(0.5f);

別の方法として、キーフレームを指定したスカラー アニメーション オブジェクトを作成して、0 ~ 100% の不透明度を表す 0.0 ~ 1.0 のアニメーション変数を設定することもできます。

ScalarKeyFrameAnimation animation =
  compositor.CreateScalarKeyFrameAnimation();
animation.InsertKeyFrame(0.0f, 0.0f); // Optional
animation.InsertKeyFrame(1.0f, 1.0f);

InsertKeyFrame メソッドの 1 つ目のパラメーターは、アニメーションの開始時点 (0.0) からアニメーションの終了時点 (1.0) までの相対的なオフセットです。2 つ目のパラメーターは、アニメーション タイムラインの該当時点におけるアニメーション変数の値です。つまり、このアニメーションでは、アニメーションの実行時間中に値が 0.0 から 1.0 まで滑らかに移り変わります。続いて、次のようにこのアニメーション全体の実行時間を設定できます。

using namespace Windows::Foundation;
animation.Duration(TimeSpan::FromSeconds(1));

アニメーションの準備が整ったら、後はアニメーションを任意の合成オブジェクトとプロパティに接続するだけです。

visual.StartAnimation(L"Opacity", animation);

StartAnimation メソッドは、実際には CompositionObject 基底クラスから継承しています。このため、さまざまなクラスのプロパティをアニメーションにできます。これも、DirectComposition よりも進化している点です。DirectComposition では、各アニメーション プロパティから、スカラー値のオーバーロードとアニメーション オブジェクトを提供していました。Windows 合成では、プロパティ システムが大幅に充実し、非常に興味深い機能も使用できるようになっています。特に、テキスト式の作成機能をサポートしているため、高度なアニメーションや効果を実現するために記述が必要なコードの量が減りました。テキスト式は、Windows 合成エンジンによって実行時に解析され、コンパイルされて、効率的に実行されます。

ビジュアルを Y 軸中心に回転させて、奥行を演出する必要があるとしましょう。ビジュアルの RotationAngle プロパティ (単位はラジアン) は、遠近感のある変換を生み出さないので十分ではありません。ビジュアルの回転に応じて、ユーザーの視点に近い側の端を大きく表示し、反対側の端を小さく表示する必要があります。図 8 に、この動作の説明として、多数の回転しているビジュアルを示します。

回転しているビジュアル
図 8 回転しているビジュアル

このようなアニメーション効果を実現するにはどうすればよいでしょうか。まずは、回転角に関するスカラー キーフレーム アニメーションを作成しましょう。

ScalarKeyFrameAnimation animation = compositor.CreateScalarKeyFrameAnimation();
animation.InsertKeyFrame(1.0f, 2.0f * Math::Pi,
  compositor.CreateLinearEasingFunction());
animation.Duration(TimeSpan::FromSeconds(2));
animation.IterationBehavior(AnimationIterationBehavior::Forever);

線形イージング関数は、加速と減速に関する既定の関数をオーバーライドして、連続した回転動作を生み出します。次に、式内から参照可能なプロパティを備えた、カスタム オブジェクトを定義する必要があります。コンポジターには、ちょうどこの目的のためのプロパティ セットが用意されています。

CompositionPropertySet rotation = compositor.CreatePropertySet();
rotation.InsertScalar(L"Angle", 0.0f);

プロパティ セットは合成オブジェクトでもあるため、StartAnimation メソッドを使用して、組み込みプロパティと同じくらい簡単にカスタム オブジェクトをアニメーションにできます。

rotation.StartAnimation(L"Angle", animation);

これで、Angle プロパティが有効なオブジェクトが完成しました。今度は、目的の効果を実現する変換行列を定義しながら、この回転角自体のアニメーション化したプロパティをデリゲートする必要があります。次の式を記述します。

ExpressionAnimation expression =
  compositor.CreateExpressionAnimation(
    L"pre * Matrix4x4.CreateFromAxisAngle(axis, rotation.Angle) * post");

式アニメーションはキーフレーム アニメーション オブジェクトではないため、アニメーション変数の (補間関数に基づいた) 変化に使用される可能性がある相対的なキーフレーム オフセットはありません。代わりに式では、従来の意味でのアニメーションにもできるパラメーターを参照します。ただし、pre、axis、rotation、および post の各パラメーターを定義するのは開発者の役割です。まずは axis パラメーターから定義しましょう。

expression.SetVector3Parameter(L"axis", Vector3{ 0.0f, 1.0f, 0.0f });

式内の CreateFromAxisAngle メソッドは、回転の軸を受け取ると想定していて、Y 軸に沿った軸を定義します。また、CreateFromAxisAngle メソッドは回転角も受け取ると想定しています。このため、"Angle" プロパティがアニメーションになっている rotation プロパティ セットを回転角に利用できます。

expression.SetReferenceParameter(L"rotation", rotation);

回転の中心をビジュアルの左端ではなく中心にするには、CreateFromAxisAngle メソッドで作成した回転行列に、回転位置まで軸を論理上移動する変換を事前に乗算する必要があります。

expression.SetMatrix4x4Parameter(
  L"pre", Matrix4x4::Translation(-width / 2.0f, -height / 2.0f, 0.0f));

なお、行列乗算には可換性がありません。したがって、pre (前) と post (後) の行列は名前のとおりに乗算します。最後に回転行列の後で、遠近感を付けてビジュアルを元の位置に復元します。

expression.SetMatrix4x4Parameter(
  L"post", Matrix4x4::PerspectiveProjection(width * 2.0f) *
    Matrix4x4::Translation(width / 2.0f, height / 2.0f, 0.0f));

以上で、式で参照しているすべてのパラメーターが完成しました。式アニメーションを使用するだけで、TransformMatrix プロパティを使用したビジュアルをアニメーションにできます。

visual.StartAnimation(L"TransformMatrix", expression);

ここまではビジュアルの作成、塗りつぶし、およびアニメーション化を実行するためのさまざまな方法を探ってきましたが、ビジュアルを直接レンダリングする必要がある場合はどうすればよいでしょうか。DirectComposition には、事前に割り当てられたサーフェスと、仮想サーフェスという一時的に割り当てられたビットマップの両方が用意されていました。仮想サーフェスは必要に応じて割り当てられ、サイズ変更も可能でした。Windows 合成には、サーフェスを作成する機能が用意されていないように思えます。CompositionDrawingSurface クラスは存在していますが、外部の力を借りずにこのクラスを作成する方法はありません。その解決策は、Windows 合成相互運用 API にあります。WinRT クラスでは、コンポーネントの Windows メタデータしかない場合には直接参照できない、追加の COM インターフェイスを実装している場合があります。このようなクロークされたインターフェイスを知っていれば、クロークされたインターフェイスを C++ で簡単に照会できます。当然ながら、Windows 合成 API で主流派の開発者が利用できる便利な抽象化から逸脱することになるので、この手法を利用すると手間が増えます。まずは、レンダリング デバイスを作成する必要があります。Direct3D 12 が Windows 合成でまだサポートされていないので、ここでは Direct3D 11 を使用します。

ComPtr<ID3D11Device> direct3dDevice;

次に、デバイス作成フラグを準備します。

unsigned flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT |
                 D3D11_CREATE_DEVICE_SINGLETHREADED;
#ifdef _DEBUG
flags |= D3D11_CREATE_DEVICE_DEBUG;
#endif

BGRA のサポートを利用すると、このデバイスでのレンダリングに、比較的わかりやすい Direct2D API を使用できます。続いて、D3D11CreateDevice 関数でハードウェア デバイス自体を作成します。

check(D3D11CreateDevice(nullptr, // Adapter
                        D3D_DRIVER_TYPE_HARDWARE,
                        nullptr, // Module
                        flags,
                        nullptr, 0, // Highest available feature level
                        D3D11_SDK_VERSION,
                        set(direct3dDevice),
                        nullptr, // Actual feature level
                        nullptr)); // Device context

次は、デバイスの DXGI インターフェイスを照会する必要があります。Direct2D デバイスの作成にはこのインターフェイスが必要だからです。

ComPtr<IDXGIDevice3> dxgiDevice = direct3dDevice.As<IDXGIDevice3>();

いよいよ、Direct2D デバイス自体を作成しましょう。

ComPtr<ID2D1Device> direct2dDevice;

ここでも、追加の診断用にデバッグ層を有効にします。

D2D1_CREATION_PROPERTIES properties = {};
#ifdef _DEBUG
properties.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION;
#endif

デバイスを作成するには、先に Direct2D ファクトリを作成することもできます。この方法は、デバイスに依存しないリソースを作成する必要がある場合に便利です。今回は、D2D1CreateDevice 関数に用意されているショートカットを使用します。

check(D2D1CreateDevice(get(dxgiDevice), properties, set(direct2dDevice)));

レンダリング デバイスの準備が整いました。想像した内容を自由にレンダリングするのに使用できる、Direct2D デバイスが完成しました。今度は、Windows 合成エンジンにこのレンダリング デバイスについて伝える必要があります。ここで、クロークされたインターフェイスが役に立ちます。この記事で使用してきたコンポジターを使用すると、次のように ICompositorInterop インターフェイスを照会できます。

namespace abi = ABI::Windows::UI::Composition;
ComPtr<abi::ICompositorInterop> compositorInterop;
check(compositor->QueryInterface(set(compositorInterop)));

ICompositorInterop インターフェイスには、DXGI サーフェスから合成サーフェスを作成するための各種メソッドが用意されています。こうしたメソッドは、既存のスワップ チェーンを合成ビジュアル ツリーに含める際には確かに便利ですが、はるかに興味深い別の機能も備えています。インターフェイスの CreateGraphicsDevice メソッドは、レンダリング デバイスを受け取って CompositionGraphicsDevice オブジェクトを作成します。CompositionGraphicsDevice クラスは、クロークされたインターフェイスではなく Windows 合成 API の通常のクラスですが、コンストラクターが用意されていないため、作成するには C++ と ICompositorInterop インターフェイスを使用する必要があります。

CompositionGraphicsDevice device = nullptr;
check(compositorInterop->CreateGraphicsDevice(get(direct2dDevice), set(device)));

CompositionGraphicsDevice は WinRT 型なので、ここでもポインターと手動エラー処理の代わりに Modern C++ を使用できます。CompositionGraphicsDevice が準備できたら、ついに合成サーフェスを作成できます。

using namespace Windows::Graphics::DirectX;
CompositionDrawingSurface surface =
  compositionDevice.CreateDrawingSurface(Size{ 100, 100 },
    DirectXPixelFormat::B8G8R8A8UIntNormalized,
    CompositionAlphaMode::Premultiplied);

この例では、サイズが 100 x 100 ピクセルの合成サーフェスを作成します。このサイズが物理ピクセルを表していることに注意してください。Windows 合成の他の部分で想定および提供されている、論理上の DPI 対応座標を表してはいません。サーフェスには、Direct2D でサポートされている 32 ビットのアルファ ブレンドされたレンダリングも備わっています。もちろん、Direct3D と Direct2D はまだ Windows ランタイムで利用できないため、クロークされたインターフェイスに戻ってこのサーフェスを実際に描画します。

ComPtr<abi::ICompositionDrawingSurfaceInterop> surfaceInterop;
check(surface->QueryInterface(set(surfaceInterop)));

前身の DirectComposition と同様に、Windows 合成には ICompositionDrawingSurfaceInterop インターフェイスの BeginDraw メソッドと EndDraw メソッドが用意されています。これらのメソッドは、同じ名前の Direct2D メソッド呼び出しに対する通常の呼び出しを統合し、その代わりとなります。

ComPtr<ID2D1DeviceContext> dc;
POINT offset = {};
check(surfaceInterop->BeginDraw(nullptr, // Update rect
                                __uuidof(dc),
                                reinterpret_cast<void **>(set(dc)),
                                &offset));

Windows 合成は、合成デバイスの作成時に提供した元のレンダリング デバイスを受け取り、このデバイスを使用してデバイス コンテキストやレンダー ターゲットを作成します。必要に応じて物理ピクセル単位のクリッピング四角形を指定することもできますが、今回は描画サーフェスへの無制限のアクセスを選択しています。BeginDraw メソッドは同じく物理ピクセル単位のオフセットも返すことで、目的の描画サーフェスの原点を示します。原点がレンダー ターゲットの左上隅にあるとは限らないので、このオフセットが適切に適用されるよう慎重にコマンドを調整または変換する必要があります。繰り返しますが、Windows 合成で既に BeginDraw メソッドを呼び出しているので、レンダー ターゲットでは呼び出さないでください。このレンダー ターゲットは論理上は合成 API に所有されているので、EndDraw メソッドの呼び出し後は保持し続けないようにする必要があります。レンダー ターゲットの準備は整いましたが、ビューの論理 DPI または有効な DPI に対応していません。Windows::Graphics::Display 名前空間を使用すると、現在のビューの論理 DPI を取得して、Direct2D でのレンダリングに使用される DPI を設定することができます。

using namespace Windows::Graphics::Display;
DisplayInformation display = DisplayInformation::GetForCurrentView();
float const dpi = display.LogicalDpi();
dc->SetDpi(dpi, dpi);

レンダリングを始める前の最後の段階は、合成オフセットをなんらかの方法で制御することです。シンプルな解決策としては、オフセットを使用して変換行列を提供します。Direct2D では論理ピクセル単位で処理することを思い出してください。このため、オフセットだけでなく、新しく設定された DPI 値も使用する必要があります。

dc->SetTransform(D2D1::Matrix3x2F::Translation(offset.x * 96.0f / dpi,
                                               offset.y * 96.0f / dpi));

ここまで来れば、任意のコンテンツを描画してからサーフェスの相互運用インターフェイスで EndDraw メソッドを呼び出すことで、バッチ化された Direct2D 描画コマンドが処理され、サーフェスに対する変更が合成ビジュアル ツリーに反映されるようにできます。

check(surfaceInterop->EndDraw());

もちろん、まだサーフェスをビジュアルに関連付けていません。また、既に述べたとおり、ビジュアルにはコンテンツ プロパティがなくなったため、ブラシを使用してレンダリングする必要があります。さいわい、既存のサーフェスを表すブラシをコンポジターで作成できます。

CompositionSurfaceBrush brush = compositor.CreateSurfaceBrush(surface);

続いて、通常のスプライト ブラシを作成し、このブラシを使用してビジュアルを表示できます。

SpriteVisual visual = compositor.CreateSpriteVisual();
visual.Brush(brush);
visual.Size(Vector2{ ... });

この方法では相互運用性を十分に確保できない場合は、XAML 要素を受け取って、基盤となる合成ビジュアルを取得できます。C# での例は次のとおりです。

using Windows.UI.Xaml.Hosting;
Visual visual = ElementCompositionPreview.GetElementVisual(button);

ElementCompositionPreview は暫定状態にあるように思えますが、実際には運用環境で使用する準備が整っていて、Windows ストアに提出したアプリで使用できます。任意の UI 要素を渡すと、静的な GetElementVisual メソッドは基盤となる合成ビジュアル ツリーのビジュアルを返します。ただし、ContainerVisual や SpriteVisual ではなく Visual を返すことに注意してください。したがって、ビジュアルの子を直接操作することもブラシを適用することもできませんが、Windows 合成に用意されている多数のビジュアル プロパティを調整することはできます。ElementCompositionPreview ヘルパー クラスには、制御された方法で子ビジュアルを追加するための追加の静的メソッドが用意されています。ビジュアルのオフセットを変更できるので、UI ヒット テストなどの処理は XAML レベルで引き続き機能します。さらに、ビジュアルを基盤とする XAML インフラストラクチャを破損させることなく、そのビジュアルに Windows 合成でアニメーションを直接適用することもできます。では、ボタンを回転させるシンプルなスカラー アニメーションを作成しましょう。ビジュアルからコンポジターを取得する必要があります。これで、アニメーション オブジェクトの作成が以前と同様に機能するようになります。

Compositor compositor = visual.Compositor;
ScalarKeyFrameAnimation animation = compositor.CreateScalarKeyFrameAnimation();

線形イージング関数を使用してボタンをいつまでもゆっくり回転させる、シンプルなアニメーションを作成しましょう。

animation.InsertKeyFrame(1.0f, (float) (2 * Math.PI),
  compositor.CreateLinearEasingFunction());

続いて、1 回の回転の所要時間が 3 秒であって、いつまでも続くことを指定します。

animation.Duration = TimeSpan.FromSeconds(3);
animation.IterationBehavior = AnimationIterationBehavior.Forever;

最後に、XAML に用意されているビジュアルにアニメーションを接続し、RotationAngle プロパティをアニメーションにするよう合成エンジンに指示します。

visual.StartAnimation("RotationAngle", animation);

XAML だけを使用してもこれと同じ処理を実行できるでしょうが、Windows 合成エンジンは、抽象化レベルが大幅に低くてパフォーマンスが間違いなく優れていることを考えると、はるかに強力で柔軟です。別の例を挙げると、Windows 合成には、現時点では XAML でサポートされていない四元数アニメーションも用意されています。

Windows 合成エンジンについて説明するべき内容はまだたくさんあります。個人的な意見では、Windows 合成はこれまでで最も斬新な WinRT API です。開発者が手に入れる力は莫大です。しかも、UI やグラフィックスに関する多くの他の大規模 API とは異なり、パフォーマンスのトレードオフが発生することも学習が非常に難しいこともありません。さまざまな意味で、Windows 合成は Windows プラットフォームのあらゆる優れていて興味深い点を表しています。

Windows 合成チームは Twitter (@WinComposition) でフォローできます。


Kenny Kerrは、カナダを拠点とするコンピューター プログラマであり、Pluralsight の執筆者です。また、Microsoft MVP でもあります。彼のブログは kennykerr.ca (英語) で、Twitter は @kennykerr (英語) でフォローできます。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Mark Aldham、James Clarke、John Serna、Jeffrey Stall、および Nick Waggoner に心より感謝いたします。