はじめに
以前、以下の記事で、DirectWriteを用いて縁付き文字列を描画する方法について紹介しました。
deep-verdure.hatenablog.com
一度に画面に表示する文字の量が少なければ上記の方法で問題ないのですが、文字の量が多くなるとかなり処理が重くなります。そこで、今回は縁付き文字列の描画を高速化する方法を見つけましたので、その方法を紹介したいと思います。
GeometryRealizationとは
ID2D1DeviceContext#DrawGeometry()や、ID2D1DeviceContext#FillGeometry()を呼ぶ度にエイリアス化/アンチエイリアス化されたプリミティブ情報がテセレーションにより作成され、画面に描画されます。この描画は軽くはなく、文字列の量が多くなると性能劣化に繋がります。この問題を解決するために用意されたDirect2D の機能がGeometryRealizationです。
GeometryRealizationは、テセレーションにより作成されたエイリアス化/アンチエイリアス化されたプリミティブ情報をキャッシュする仕組みで、一度GeometryRealizationを作成しておけば、以降は作成したGeometryRealizationを用いて描画することでプリミティブ情報の作成をスキップでき、大幅に処理が軽くなります。なお、GeometryRealizationは通常のビットマップキャッシュと異なり、ジオメトリを特徴付ける点集合としてキャッシュされるため、ビットマップキャッシュよりもメモリ使用率が低く抑えられています。※ペイントソフトに対するドローソフトと同様の考え方です。
GeometryRealizationは描画を高速化しますが、その代わりにキャッシュの作成に時間がかかります。そのため、毎フレームごとに描画内容が変更され、キャッシュを作成する必要がある場合は、高速化の恩恵を受けられないどころか逆に低速になります。このような場合はID2D1DeviceContext#DrawGeometry()や、ID2D1DeviceContext#FillGeometry()で描画しましょう。
GeometryRealizationを用いた描画の流れは以下のようになります。
1. ID2D1PathGeometryインスタンス、ID2D1GeometrySinkインスタンスを作成する。
2. ID2D1GeometryRealizationインスタンスを作成する。
3. GeometryRealizationを用いて描画する。
手順1については、GeometryRealizationを使用しない場合と手順は同じです。本記事冒頭に掲載した、以前の記事を参考にしてください。手順2以降について、以下で解説していきます。
ID2D1GeometryRealizationインスタンスの作成
ID2D1DeviceContext1インスタンスのメソッドで、GeometryRealizationのキャッシュを示すID2D1GeometryRealizationインスタンスを作成します。ID2D1DeviceContext1インスタンスはID2D1DeviceContext#As()メソッドで変換して準備しましょう。
ID2D1DeviceContext1#CreateFilledGeometryRealization()メソッドで、塗り潰しのためのID2D1GeometryRealizationインスタンスを作成できます。第1引数にはGeometryRealizationの元となるID2D1PathGeometryインスタンスを渡し、第2引数には平坦化許容値を、第3引数にはID2D1GeometryRealizationインスタンスの格納先となるポインタのポインタを指定します。
ここで、平坦化許容値とは、ジオメトリの真の曲線と、GeometryRealization生成の過程でDirect2Dが実行する多角形近似の結果との間で最大でどの程度のずれまでを許容するかを示す値であり、DIP値で指定します。平坦化許容値が低いほど、GeometryRealizationは元のジオメトリに近くなりますが、トレードオフとして描画コストが高くなります。平坦化許容値はD2D1::ComputeFlatteningTolerance()関数で計算できます。第1引数には追加のスケーリング変換を行う行列を渡します。通常は、D2D1::Matrix3x2F::Identity()を渡しておけば問題ありません。第2,3引数には画面解像度を渡します。第4引数にはmaxZoomFactorを指定します。これはGeometryRealizationを拡大描画する場合に指定します。どの程度まで拡大する可能性があるかを指定してあげることで、それに合わせた最適な平坦化許容値を算出してくれます。拡大をしない場合、デフォルト値の1.0fで問題ありません。
以下に、ID2D1GeometryRealizationインスタンスの作成コードの一部を掲載します。
const auto flatteningTolerance = D2D1::ComputeFlatteningTolerance(D2D1::Matrix3x2F::Identity(), static_cast<FLOAT>(screenSize.first), static_cast<FLOAT>(screenSize.second), maxZoomFactor); ComPtr<ID2D1GeometryRealization> filledGeometryRealization = nullptr; if (FAILED(d2dDeviceContext->CreateFilledGeometryRealization(pathGeometry.Get(), flatteningTolerance, &filledGeometryRealization))) [[unlikely]] { return; }
なお、ID2D1GeometryRealizationインスタンスを作成できるメソッドはもう一つあります。それがID2D1DeviceContext1#CreateStrokedGeometryRealization()メソッドであり、その名の通りストローク部分のGeometryRealizationを作成できます。しかし、このメソッドは非常に重いため、使用できるケースは限られるでしょう。文字列の場合、数文字分作成するだけで数秒~数十秒ほどフリーズする代物であり、マルチスレッドで裏で頑張って作成しておいてもらうなどの工夫が必要です。私は文字列のストロークだけはID2D1DeviceContext#DrawGeometry()で普通に描画することを選びました。文字列の内部をGeometryRealizationで埋めるだけでも高速化の恩恵を十分受けることができますし、数百文字分のストロークのGeometryRealizationを作成すると5分程度かかるため、マルチスレッドを使ったとしても実用が厳しかったためです。
GeometryRealizationを用いた描画
ID2D1GeometryRealizationインスタンスを作成したら、ID2D1DeviceContext1#DrawGeometryRealization()メソッドで、GeometryRealizationを用いた描画を行えます。第1引数には描画元となるID2D1GeometryRealizationインスタンスを渡します。第2引数には色を塗るためのID2D1Brushインスタンスを渡します。この通り、描画は非常に簡単に行えます。
d2dDeviceContext->DrawGeometryRealization(textPathGeometryRealization.Get(), solidColorBrush.Get());
おわりに
以上でGeometryRealizationの解説を終わります。そこまで複雑な手順を踏まなくてもジオメトリ描画を高速化できるので、ジオメトリ描画が遅くて悩んでいる方は是非試してみて下さい!