Windows と C++

DirectComposition: 変換とアニメーション

Kenny Kerr

Kenny KerrDirectComposition のビジュアルは、これまでのコラムで説明してきたオフセットやコンテンツ プロパティ以上の機能を数多く提供します。このビジュアルは、変換やアニメーションを施すことで実際に使えるようになります。変換とアニメーションのどちらを使用する場合でも、Windows 合成エンジンが一種のプロセッサの役割を果たします。開発者が行うのは、変換行列の計算や構築、さらには三次関数や正弦波を使用したアニメーション曲線の計算や構築などです。さいわい、Windows API は、この非常に補完的な API のペアを使って必要なサポートを提供します。Direct2D は、変換行列の定義や、回転、拡大縮小、遠近法、移動などを記述する作業を簡略化する優れたサポートを提供します。同様に、Windows アニメーション マネージャーは、数学の専門知識の必要性をなくし、アニメーション遷移の機能豊富なライブラリ、キー フレームや負荷を含むストーリーボードなどを使用して、アニメーションを記述できるようにします。Windows アニメーション マネージャー、Direct2D、および DirectComposition を 1 つのアプリケーションに組み合わせて、簡単な操作でその能力を実際に体感することができます。

前回のコラム (msdn.microsoft.com/magazine/dn759437) では、保持モードとイミディエイト モードのグラフィックスを最大限に活かせるように、DirectComposition API と Direct2D を組み合わせて使用する方法について説明しました。前回付属のサンプル プロジェクトでは、円を作成して移動し、Z オーダーをごく簡単に制御するシンプルなアプリケーションを使用して、この考え方を例示しています。今回は、変換とアニメーションを使用して、強力な効果をいくつか簡単に追加する方法を取り上げます。まず分かるのは、DirectComposition には、スカラー プロパティの多くに対応するオーバーロードが用意されていることです。このことは、ここ数か月の API に関するコラムをお読みいただければ分かります。たとえば、ビジュアルのオフセットは、次のように SetOffsetX メソッドと SetOffsetY メソッドを使用して設定できます。

ComPtr<IDCompositionVisual2> visual = ...
HR(visual->SetOffsetX(10.0f));
HR(visual->SetOffsetY(20.0f));

しかし、IDCompositionVisual2 の派生元の IDCompositionVisual インターフェイスも、これらのメソッドのオーバーロードを提供します。このオーバーロードは、浮動小数点値ではなく、アニメーション オブジェクトを受け取ります。このアニメーション オブジェクトは IDCompositionAnimation インターフェイスとして実体化されます。たとえば、アニメーションにする必要があるのが一方の軸か両方の軸かに応じて、1 つまたは 2 つのアニメーション オブジェクトを使用してビジュアルのオフセットを設定します。

ComPtr<IDCompositionAnimation> animateX = ...
ComPtr<IDCompositionAnimation> animateY = ...
HR(visual->SetOffsetX(animateX.Get()));
HR(visual->SetOffsetY(animateY.Get()));

ただし、合成エンジンを使用すれば、ビジュアルのオフセットよりもはるかに高度なアニメーションを設定できます。ビジュアルは 2D 変換も 3D 変換もサポートします。ビジュアルの SetTransform メソッドを使用すると、スカラー値を指定して 2D 変換を適用できます。

D2D1_MATRIX_3X2_F matrix = ...
HR(visual->SetTransform(matrix));

この例で DirectComposition は実際には Direct2D API によって定義される 3x2 の行列を使用します。ビジュアルには、回転、移動、拡大縮小、傾斜などの操作を行うことができます。その結果、ビジュアルのコンテンツが投影される座標空間が影響を受けますが、その影響は X 軸と Y 軸で構成される 2 次元グラフィックスに限定されます。

当然、IDCompositionVisual インターフェイスは SetTransform メソッドのオーバーロードを提供しますが、このオーバーロードがアニメーション オブジェクトを直接受け取ることはありません。アニメーション オブジェクトが果たす役割は、時間をかけて 1 つの値をアニメーションで変化させることのみです。行列は本質的に複数の値から構成されます。実現する効果に応じて、その行列のさまざまなメンバーをアニメーションで変化させる場合があります。そのため、SetTransform オーバーロードはアニメーション オブジェクトの代わりに変換オブジェクトを受け取ります。

ComPtr<IDCompositionTransform> transform = ...
HR(visual->SetTransform(transform.Get()));

変換オブジェクトとは、具体的には IDCompositionTransform から派生されたさまざまなインターフェイスです。変換オブジェクトは、スカラー値またはアニメーション オブジェクトを受け取るオーバーロード メソッドを提供します。このようにして、固定の中心点や軸ではなく、回転角をアニメーションで変化させて回転行列を定義することができます。もちろん、どのようなアニメーションを行うかは開発者次第です。次に簡単な例を示します。

ComPtr<IDCompositionRotateTransform> transform = ...
HR(transform->SetCenterX(width / 2.0f));
HR(transform->SetCenterY(height / 2.0f));
HR(transform->SetAngle(animation.Get()));
HR(visual->SetTransform(transform.Get()));

IDCompositionRotateTransform は IDCompositionTransform から派生するインターフェイスで、ビジュアルを Z 軸を中心に回転させる 2D 変換を表します。この例では、幅と高さから中心点を固定値に設定し、アニメーション オブジェクトを使用して角度を変化させています。

これが基本パターンです。ここでは 2D 変換のみを示しましたが、3D 変換もほぼ同じ方法で機能します。次は、前回のサンプル プロジェクトを利用して、Direct2D や Windows アニメーション マネージャーを使用したさまざまな方法で変換とアニメーションを適用する方法を紹介し、より実用的な例を示します。

文章だけで変換とアニメーションを説明するのは限界があります。変換とアニメーションの例を実際にオンライン コースで確認してください。そこでは実用的な例をお見せしています (bit.ly/WhKQZT、英語)。考え方を文章だけでもう少しわかりやすく説明するために、ここでは前回のサンプル コードを変更して、円ではなく四角形を使用します。これで、さまざまな変換の説明が少しわかりやすくなるはずです。まず、SampleWindow m_geometry メンバー変数を四角形のジオメトリに置き換えます。

ComPtr<ID2D1RectangleGeometry> m_geometry;

次に、SampleWindow CreateFactoryAndGeometry メソッドで、Direct2D ファクトリを取得して、楕円ジオメトリでなく四角形のジオメトリを作成します。

D2D1_RECT_F const rectangle =
  RectF(0.0f, 0.0f, 100.0f, 100.0f);
HR(m_factory->CreateRectangleGeometry(
  rectangle,
  m_geometry.GetAddressOf()));

必要なコードは以上です。アプリケーションの残りの部分は前回と同様に、レンダリングとヒット テストでジオメトリの抽象化を使用するだけです。図 1 にその結果を示します。

 円の代わりに四角形を使用する変換とアニメーションの例
図 1 円の代わりに四角形を使用する変換とアニメーションの例

次に、WM_KEYDOWN メッセージに応答するための簡単なメッセージ ハンドラーを追加します。そのためには、SampleWindow MessageHandler メソッドに、以下の if ステートメントを追加します。

else if (WM_KEYDOWN == message)
{
  KeyDownHandler(wparam);
}

通常、ハンドラーにはデバイスを利用できない状況から回復するためのエラー処理が必要です。図 2 は、WM_PAINT メッセージ ハンドラーがデバイス スタックを再構築できるように、デバイス リソースを解放し、ウィンドウを無効にする一般的なパターンを示しています。また、図形の追加時に使用する Ctrl キーとの混同を避けるため、ハンドラーの操作は Enter キーに限定しています。

図 2 デバイスが利用できない状況を回復するためのスキャフォールディング

void KeyDownHandler(WPARAM const wparam)
{
  try
  {
    if (wparam != VK_RETURN)
    {
      return;
    }
    // Do stuff!
  }
  catch (ComException const & e)
  {
    TRACE(L"KeyDownHandler failed 0x%X\n",
      e.result);
    ReleaseDeviceResources();
    VERIFY(InvalidateRect(m_window,
                          nullptr,
                          false));
  }
}

以上で、変換とアニメーションを試す準備が整いました。まずは、基本的な 2D 回転変換から試してみましょう。最初に、Z 軸を表す中心点 (回転の中心となる点) を決める必要があります。DirectComposition は物理ピクセル座標を想定するため、ここでは単純に GetClientRect 関数を使用します。

RECT bounds {};
VERIFY(GetClientRect(m_window, &bounds));

次に、以下のようにウィンドウのクライアント領域の中心点を導き出します。

D2D1_POINT_2F center
{
  bounds.right / 2.0f,
  bounds.bottom / 2.0f
};

また、Direct2D 行列のヘルパー関数を使用して、30 度の 2D 回転変換を表す行列を構築することもできます。

D2D1_MATRIX_3X2_F const matrix =
  Matrix3x2F::Rotation(30.0f, center);

続いて、ビジュアルの変換プロパティを単純に設定し、変更をビジュアル ツリーにコミットします。わかりやすくするため、ここではこの変更をルート ビジュアルにのみ適用します。

HR(m_rootVisual->SetTransform(matrix));
HR(m_device->Commit());

もちろん、多くの変更を多くのビジュアルに適用できます。この場合、合成エンジンを使用してすべての調整を簡単に行うことができます。この単純な 2D 変換の結果については、図 3 を参照してください。ある程度のエイリアシングが発生しているのが分かります。Direct2D を既定でアンチエイリアシングに設定している場合でも、描画はレンダリングされた座標空間内に表示されます。合成エンジンは、合成サーフェイスのレンダリングに使用されたジオメトリについては把握しないため、このエイリアシングを修正する方法はありません。いずれにしても、アニメーションを追加すればエイリアシングは短期間しか表示されず、目立たなくなります。

単純な 2D 変換
図 3 単純な 2D 変換

この変換にアニメーションを追加するには、行列構造体を合成変換用に切り替える必要があります。ここでは、D2D1_POINT_2F 構造体と D2D1_MATRIX_3X2_F 構造体の両方を 1 つの回転変換に置き換えます。まず、合成デバイスを使用して、回転変換を作成する必要があります。

ComPtr<IDCompositionRotateTransform> transform;
HR(m_device->CreateRotateTransform(transform.GetAddressOf()));

このように一見単純なオブジェクトでも、デバイスを利用できなくなった場合や再作成する場合には破棄するのを忘れないでください。次に、Direct2D 行列構造ではなく、インターフェイス メソッドを使用して中心点と角度を設定します。

HR(transform->SetCenterX(bounds.right / 2.0f));
HR(transform->SetCenterY(bounds.bottom / 2.0f));
HR(transform->SetAngle(30.0f));

すると、コンパイラが合成変換の処理に適切なオーバーロードを選択します。

HR(m_rootVisual->SetTransform(transform.Get()));

まだアニメーションを追加していないため、これを実行すると図 3 と同じ効果のままです。アニメーション オブジェクトの作成は簡単です。

ComPtr<IDCompositionAnimation> animation;
HR(m_device->CreateAnimation(animation.GetAddressOf()));

角度の設定時に定数値ではなく、作成したアニメーション オブジェクトを使用します。

HR(transform->SetAngle(animation.Get()));

アニメーションの構成は少し興味深い処理です。シンプルなアニメーションは比較的分かりやすいものです。既に説明したとおり、アニメーションは三次関数や正弦波を使って記述します。この回転角は、線形遷移を使ってアニメーションで変化させることができ、三次関数を追加することで値は 1 秒間で 0 から 360 へと増加します。

float duration = 1.0f;
HR(animation->AddCubic(0.0,
                       0.0f,
                       360.0f / duration,
                       0.0f,
                       0.0f));

AddCubic メソッドの 3 つ目のパラメーターが線形係数を示しています。これがなければビジュアルは 1 秒ごとに 360 度の回転を永遠に続けることになります。ここでは次のようにして、回転が 360 度に達したらアニメーションを終了するようにします。

HR(animation->End(duration, 360.0f));

End メソッドの 1 つ目のパラメーターは、アニメーション関数の値に関係なく、アニメーションの開始時点からのオフセットを示します。2 つ目のパラメーターは、アニメーションの最終値です。アニメーションがアニメーション曲線と一致しない場合はこの値に "スナップ" され、その結果、不快な視覚効果が生み出されます。

このような線形アニメーションであれば原因を簡単に推測できますが、複雑なアニメーションになると原因の究明は非常に複雑になります。そこで、Windows アニメーション マネージャーの出番です。正弦関数や三次多項式のセグメント、繰り返しのセグメントなどを追加するときに IDCompositionAnimation のさまざまなメソッドを呼び出す必要はなく、Windows アニメーション マネージャーとその遷移のリッチなライブラリを使用して、アニメーションのストーリーボードを構築します。処理が完了したら、その結果生成されるアニメーション変数を使用して、合成アニメーションを追加します。この処理を行うには少量のコードを追加作成する必要がありますが、アプリケーションのアニメーションをさらに強力に制御できるメリットがあります。まずは、アニメーション マネージャー自体を作成します。

ComPtr<IUIAnimationManager2> manager;
HR(CoCreateInstance(__uuidof(UIAnimationManager2),
  nullptr, CLSCTX_INPROC, __uuidof(manager),
  reinterpret_cast<void **>(manager.GetAddressOf())));

Windows アニメーション マネージャーは COM アクティブ化を使用します。そのため、CoInitializeEx または RoInitialize を呼び出して、必ずランタイムを初期化します。また、遷移ライブラリを作成することも必要です。

ComPtr<IUIAnimationTransitionLibrary2> library;
HR(CoCreateInstance(__uuidof(UIAnimationTransitionLibrary2),
  nullptr, CLSCTX_INPROC, __uuidof(library),
  reinterpret_cast<void **>(library.GetAddressOf())));

通常、アプリケーションはアプリケーションの有効期間中これらの 2 つのオブジェクトを保持し、連続アニメーション (具体的には速度の一致) に対応します。次に、アニメーションのストーリーボードを作成します。

ComPtr<IUIAnimationStoryboard2> storyboard;
HR(manager->CreateStoryboard(storyboard.GetAddressOf()));

ストーリーボードは、遷移とアニメーション変数を関連付け、時間の経過に合わせて相対スケジュールを定義します。ストーリーボードを使用して、異なるアニメーション変数に適用されるさまざまな遷移をまとめることができます。また、遷移の同期を確保します。ストーリーボードでは全体としてスケジュールを設定します。もちろん、複数のストーリーボードを作成して個別のアニメーションのスケジュールを設定することもできます。ここで、アニメーション変数を作成するようにアニメーション マネージャーに要求します。

ComPtr<IUIAnimationVariable2> variable;
  HR(manager->CreateAnimationVariable(
    0.0, // initial value
    variable.GetAddressOf()));

ストーリーボードのスケジュール設定後は、アプリケーションから実効値をいつでも要求できるように変数を最新の状態に保つのはアニメーション マネージャーの役割です。ここでは単純にアニメーション変数を使用して、合成アニメーションにセグメントを追加した後、変数を破棄します。次のコードでは強力な遷移ライブラリを使用して、アニメーション変数に興味深い遷移効果を生み出します。

ComPtr<IUIAnimationTransition2> transition;
HR(library->CreateAccelerateDecelerateTransition(
  1.0,   // duration
  360.0, // final value
  0.7,   // acceleration ratio
  0.3,   // deceleration ratio
  transition.GetAddressOf()));

この遷移はアニメーション変数を使用して、指定した期間中、最終値に達するまで加速と減速を繰り返します。また、比率を使用して、変数による加速と減速の相対速度を指定します。比率の合計が 1 を超えないように注意してください。アニメーション変数と遷移の準備が整ったら、これらをストーリーボードに追加します。

HR(storyboard->AddTransition(variable.Get(),
                             transition.Get()));

これでストーリーボードのスケジュールを設定する準備が整いました。

HR(storyboard->Schedule(0.0));

Schedule メソッドの単一のパラメーターは、スケジューラに現在のアニメーション時間を指示します。これは、アニメーションを調整し、合成エンジンのリフレッシュ レートに合わせるのに便利ですが、現時点では上記のとおりで問題ありません。これでアニメーション変数の準備が整ったため、合成アニメーションに曲線を追加するように要求します。

HR(variable->GetCurve(animation.Get()));

このようなコードを使用すると、次のような合成アニメーションを呼び出したことになります。

HR(animation->AddCubic(0.0, 0.0f, 0.0f, 514.2f, 0.0f));
HR(animation->AddCubic(0.7, 252.0f, 720.0f, -1200.0f, 0.0f));
HR(animation->End(1.0, 360.0f));

Windows アニメーション マネージャーを使用するよりも大幅なコードの短縮になりますが、計算内容を理解するのは簡単ではありません。また、Windows アニメーション マネージャーには、実行中のアニメーションを調整し、スムーズに遷移させる機能があります。これは手動で実行しようとするときわめて困難な処理です。


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

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Leonardo Blanco と James Clarke に心より感謝いたします。