2Dレンダリング ― Qt Canvas Painter の紹介

このブログは「2D Rendering - Introducing Qt Canvas Painter」の抄訳です。

Qt Canvas Painter について紹介します。Qt Canvas Painter は、パフォーマンス・生産性・モダンな機能を兼ね備えることを目指した、新しい命令型 2D レンダリング APIです。本ブログ記事では、2D レンダリングのこれまでの歴史、現在の状況、そして将来の可能性について最新情報をお届けします。

まずは、Qt Canvas Painter を使った 2D レンダリングの概要を紹介する短い動画をご覧ください。

Qt における命令型 2D レンダリングの歴史

命令型 2D レンダリングの歴史は、Qt の愛されてきた描画 API である QPainter から振り返ることができます。QPainter は、20 年以上にわたって Qt の Widgets や命令型描画を支えてきた、汎用的で優れた描画 API です。QPainter の歴史について詳しく知りたい方には、Eirik による Qt World Summit 2025 の基調講演「30 Years of Graphics Rendering in Qt」を視聴することをおすすめします。そこから分かるのは、QPainter が CPU 上で実装されるペイントエンジンに最も適しているという点です。

一方で、現代における QPainter API とその 2D レンダリングアーキテクチャの弱点は、GPU や 3D API との相性があまり良くないことです。QPaintEngine という抽象化によって、さまざまなバックエンドで QPainter API をサポートすることは容易になりますが、並列処理や GPU メモリ上でのデータ保持を好む GPU の性能を最大限に引き出すことはできません。また、機能セットも制限されがちで、たとえば従来の OpenGL ベースのペイントエンジンでは、純粋なソフトウェア(いわゆるラスタ)ペイントエンジンが提供する機能の一部しか利用できません。

では、QPainter を汎用的な 2D レンダリング API として維持し、既存の QPainter や Widgets のユーザーに問題を起こさないようにしつつ、ハードウェアアクセラレーションされた命令型描画を実現するには、どのようなアプローチが最適でしょうか。約 1 年前、Qt オスロオフィスでグラフィックスチームのメンバーとともにワークショップを開催しました。その結果として、QRhi 上に構築された、よりコンパクトな代替ペイント API を作る実験的プロジェクトを開始することを決定しました。この API の主な目標は、パフォーマンスと生産性の両立です。

Qt Canvas Painter の起源は、Mikko Mononen によって開発された、コンパクトで優れた描画ライブラリ NanoVG にあると考えられます。約 10 年前、私は個人的に QNanoPainter というプロジェクトを立ち上げ、NanoVG の上に Qt の C++ API や Qt Quick・Widgets 向けのヘルパーを提供しました。このプロジェクトは長年にわたり、多くのユーザーから好意的なフィードバックを得てきました。

そこで私たちは、QNanoPainter をベースに Canvas Painter の開発を始めることにしました。まず、NanoVG の C 言語バックエンドを Qt C++ で書き直しました。OpenGL レンダリングバックエンドは QRhi を使う形に置き換えられ、OpenGL だけでなく Vulkan、Metal、Direct3D もサポートするようになりました。テキストレンダリングは、Qt Quick の強力なテキスト描画機能と符号付き距離場(Signed Distance Field)フォントを基盤として、ゼロから再実装されています。さらに、多数の機能を追加しました。その中には HTML Canvas 2D コンテキストに欠けていた機能を補うものもあれば、新しい可能性を切り開くものもあります。これらの新機能の一部はまだ実験的ですが、高性能かつモダンな要件に対して非常に有用になる可能性がすでに見えています。

ここで、恒例の注意書きをしておきます:

Qt Canvas Painter は Qt 6.11 におけるテクノロジープレビューです。これは、現時点では API や ABI の安定性を保証していないことを意味します.

優れた命令型 2D レンダリングへのステップ

新しいペインターの主な設計目標は、次の 3 点に要約できます:

パフォーマンス: まず第一に、そしておそらく最も重要な目標は、高いパフォーマンスを実現することです。特にモバイル/組み込みハードウェアにおいては重要です。既存の代替手段よりも性能が良くなければ、新しい API を使い始める魅力はおそらくありません。最大限のパフォーマンスを達成するために、機能や抽象化を削減し、描画は QRhi 上に直接実装しています。多くの 2D レンダリングライブラリは、当初は(CPU による)ソフトウェアレンダリングとして実装され、その後に(GPU による)高速化バックエンドを追加してきましたが、Qt Canvas Painter は最初から GPU 専用です。ソフトウェアバックエンドを実装する予定はありません。

生産性: もちろん、パフォーマンスだけが目標ではありません。そうであれば、開発者に QRhi や Vulkan を直接使うよう指示すれば済みます。これらの低レベル API は非常に強力で高速ですが、使いやすいとは言えず、生産性が高いとは言えません。Qt Canvas Painter の API の基盤として HTML Canvas 2D context を選んだのは、親しみやすい API、明確なドキュメント、コードサンプルを提供することで、開発生産性を高めることを狙っているためです。馴染みのある API と、インターネット上に豊富に存在するソースコードは、将来的に AI アシスタントの能力向上にも寄与します。

機能: 上記の 2 つの目標、すなわちパフォーマンスと生産性だけでも、この API は多くのユーザーにとって有用なものになり得ます。しかし、魅力的な機能セットを提供しなければ、依然としてニッチな存在に留まってしまう可能性があります。非常に高速に何千もの白い矩形を描画できるだけのライブラリでは、多くのユーザーのニーズを満たせません。HTML Canvas 2D コンテキストの機能を大きく取り込むことで、ライブラリとしての良い基盤は得られますが、本当にユーザーを惹きつけるためには、さらに多くの機能、特にモダンな UI やハードウェアアクセラレーションに特化した機能を提供する必要がありますし、提供したいと考えています。

これらのポイントについては、今後のブログ記事で、機能やベンチマーク結果とともに、より詳しく説明していく予定です。

Qt Quick で Canvas Painter を使う

Canvas Painter の主な 2D レンダリング用途として想定されているのは、カスタム Qt Quick アイテムの実装です。これは、QQuickPaintedItem を、より高性能で Qt Quick によりよく統合された形で置き換えるものと考えることができます。

カスタム QML アイテムを作成するには、QQuickCPainterItem を継承したクラスを実装し、createItemRenderer() メソッドをオーバーライドします。例えば次のようになります:


class HelloItem : public QQuickCPainterItem
{
    Q_OBJECT
    QML_ELEMENT
public:
    HelloItem(QQuickItem *parent = nullptr);
    QQuickCPainterRenderer *createItemRenderer() const override;

private:
    friend class HelloItemRenderer;
    QString m_label;
};

このクラスは、QQuickItem と同様に、QML と統合するためのプロパティやスロットなどを持つことができます。このシンプルな例では、ラベルのテキストを QML から取得するのではなく、クラス内で直接設定しています。そのため、このクラスの実装は次のようになります:


HelloItem::HelloItem(QQuickItem *parent)
    :  QQuickCPainterItem(parent)
{
    m_label = QString("Canvas Painter");
}

QQuickCPainterRenderer *HelloItem::createItemRenderer() const
{
    // Create the renderer for this item
    return new HelloItemRenderer();
}

レンダラークラスは QQuickCPainterRenderer を継承し、通常は synchronize()paint()  メソッドをオーバーライドします。多くのプラットフォームでは描画が別スレッドで行われるため、synchronize メソッドは、レンダラーとアイテムが互いの変数を安全に読み書きできる唯一の場所です。今回の Hello Canvas Painter の例では、レンダラーのコードは次のようになります:


void HelloItemRenderer::synchronize(QQuickCPainterItem *item)
{
    // Synchronize here data between the item and the renderer.
    HelloItem *helloItem = static_cast<helloitem*>(item);
    m_label = helloItem->m_label;
}

void HelloItemRenderer::paint(QCPainter *p)
{
    float size = std::min(width(), height());
    QPointF center(width() * 0.5, height() * 0.5);
    
    // Paint the background circle
    QCRadialGradient gradient1(center, size * 0.6);
    gradient1.setStartColor(0x909090);
    gradient1.setEndColor(0x202020);
    p->beginPath();
    p->circle(center, size * 0.46);
    p->setFillStyle(gradient1);
    p->fill();
    p->setStrokeStyle(0x202020);
    p->setLineWidth(size * 0.02);
    p->stroke();
    
    // Paint texts
    p->setTextAlign(QCPainter::TextAlign::Center);
    p->setTextBaseline(QCPainter::TextBaseline::Middle);
    QFont font1("Titillium Web");
    font1.setWeight(QFont::Weight::Bold);
    font1.setItalic(true);
    font1.setPixelSize(size * 0.08);
    p->setFont(font1);
    p->setFillStyle(0x2CDE85);
    p->fillText("HELLO", center.x(), center.y() - size * 0.28);
    QFont font2("Titillium Web");
    font2.setWeight(QFont::Weight::Thin);
    font2.setPixelSize(size * 0.12);
    p->setFont(font2);
    p->fillText(m_label, center.x(), center.y() - size * 0.16);
    
    // Paint the heart
    QCImage logo = p->addImage(m_logoImage, QCPainter::ImageFlag::Repeat |
                                                QCPainter::ImageFlag::GenerateMipmaps);
    float pSize = size * 0.05;
    QCImagePattern pattern(logo, center.x(), center.y(), pSize, pSize);
    p->setFillStyle(pattern);
    p->setStrokeStyle(0x2CDE85);
    p->beginPath();
    float hs = size * 2;
    QPointF hc(width() * 0.5, height() * 0.25);
    p->moveTo(hc.x(), hc.y() + hs * 0.3);
    p->bezierCurveTo(hc.x() - hs * 0.25, hc.y() + hs * 0.1,
                     hc.x(), hc.y() + hs * 0.05,
                     hc.x(), hc.y() + hs * 0.18);
    p->bezierCurveTo(hc.x(), hc.y() + hs * 0.05,
                     hc.x() + hs * 0.25, hc.y() + hs * 0.1,
                     hc.x(), hc.y() + hs * 0.3);
    p->fill();
    p->stroke();
}

上記の例を実行すると、カスタム Canvas Painter アイテムは次のように表示されます:
qcpainter_helloitem_example-1
より複雑な例については、Qt 6.11 以降の Qt リリースに含まれているサンプルをご覧ください。たとえば、利用可能な多くの 2D レンダリング機能を紹介する Gallery example があります。また、Qt Quick や Qt Widgets に依存せず、QCPainter を QRhi および QWindow と直接組み合わせて使用する方法を示した Compact Health example も用意されています。あわせて、Canvas Painter の API ドキュメント、特に QCPainter クラスのドキュメントも確認してください。

Widgets については?

Qt Widgets をお使いの皆さんに朗報です。Canvas Painter は Widgets も完全にサポートしています。QRhiWidget の上に構築された QCPainterWidget というヘルパークラスが用意されており、QWidget と同じように自分のウィジェットクラスで継承し、描画メソッドを再実装して使用します。ただし、QPainter の代わりに QCPainter を使います。これにより、QWidget アプリケーション内で、QRhi 上でハードウェアアクセラレーションされた描画を行うカスタムウィジェットを作成できます。以下は、標準ウィジェットと Canvas Painter で描画されたアナログ時計を含む、シンプルなサンプルアプリケーションです。
qcpainter_clockwidget_example-1この仕組みでは、既存のアプリケーションやウィジェットは引き続き Raster バックエンドの QPainter を使用し、Canvas Painter を利用するよう明示的に実装されたウィジェットのみが変更されます。このアプローチの利点は、すべてのウィジェットがこれまで通りの見た目と挙動を維持しつつ、恩恵を受けられるカスタムウィジェットにだけアクセラレーションを適用できる点です。一方で、既存の QPainter コードが自動的に高速化されるわけではない、という点は欠点とも言えます。それでも、この方式により、描画コードをそのまま再利用しながら、必要に応じて Widgets アプリケーションを Qt Quick へ移行しやすくなります。

Canvas Painter を使ったカスタムウィジェットの実装例としては、Hello Widget example をご確認ください。

現状

現時点で私たちが提供しているのは、HTML Canvas 2D context に概ね準拠した API です(そのため「Canvas Painter」という名称になっています)。2D コンテキストをベース仕様として選んだ理由はいくつかあります。まず、Canvas 2D コンテキストは QPainter と比べて API としてよりコンパクトであり、コンパクトさを保ちながら、QPainter よりも 2D コンテキストとの互換性を広く確保できます。また、新しいペインタの用途の一つとして Quick Canvas の新しいバックエンドになることを想定していたため、Quick Canvas の QML 要素にとって、2D コンテキストの C++ 実装以上に適したバックエンドはないだろう、という理由もあります😀

とはいえ、2D コンテキスト API と 100% の互換性 を目指しているわけではありません。ブラウザの場合、ユーザーは canvas 要素の描画結果が Chrome と一致することを期待します(必ずしも仕様そのものと一致することではありません)。一方、Canvas Painter の C++ API(および Quick Canvas)では、開発者は常にコードを移植して使う立場であり、コンテンツも自分で完全にコントロールできます。そのため、完全な互換性はそれほど重要ではありません。それよりも、仕様が一貫していて高性能であることの方が重要です。

現在、2D コンテキスト仕様と比べて不足している主な機能は次のとおりです:

  • フィルター: Filter effects は、おそらく今後もサポートしない API になる可能性が高いです。オフスクリーンキャンバスに対するポストプロセス効果は将来的にサポートしたいと考えていますが、C++ API を「エフェクト名と値を文字列で並べ、それをパースする」形にはしません。これは、命令型 API として使いにくく、性能面でも非常に非効率だからです。
  • 破線ストローク(Dashed strokes): 現在の Canvas Painter は実線のみをサポートしており、破線には対応していません。主な理由は、実装に割ける時間の不足と性能への影響です。特に(丸い結合部を含む)破線の三角形分割は遅く、多くの三角形を生成します。将来的にサポートする可能性はありますが、現時点では代替案として、グリッドパターン画像パターンを使う、あるいはカスタムシェーダーブラシを用いて、破線と同様にストロークを目立たせる方法が考えられます。
  • シャドウ: 2D コンテキストはシャドウ効果をサポートしていますが、シャドウを作るにはオフスクリーンキャンバスへの描画とガウシアンブラーが必要になります。これは性能面での影響が大きく、Mozilla ですら「可能な限り shadowBlur プロパティを避けるべき」としています。オフスクリーンキャンバス自体の初期サポートはありますが、2D コンテキストのシャドウ API が最適な形なのかはまだ判断がついていません。オフスクリーンキャンバスの生成・更新タイミングをより細かく制御できるアプローチの方が良いのか、それとも高速な角丸矩形シャドウや自由に調整可能なアンチエイリアスで十分なのか、性能への影響を踏まえて検討中です。
  • 形状クリッピング(Shape clipping): 2D コンテキストは clip() メソッドをサポートしており、任意形状でのクリッピングが可能です。Canvas Painter は現時点ではこれをサポートしておらず、変換あり/なしの矩形領域に対する高速なクリッピングのみを提供しています。

このように、2D Canvas の一部機能は未対応ですが、新しく追加している機能はあるのでしょうか?
答えは「はい、実はかなりあります」。新機能のリストは非常に長く、その詳細についてはしっかり掘り下げたいので、Qt Canvas Painter の新機能に完全にフォーカスした続編のブログ記事を近日中に公開する予定です。

今後の展望

Qt Canvas Painter は、Qt 6.11 でテックプレビューとして提供されます。私たちはこの 2D レンダリング API をさらに改善し、その強みを最大限に引き出せるユースケースを見つけていくための大きな計画を持っています。最も分かりやすい例は、新しい Quick Canvas バックエンドです。これにより、Canvas Painter の C++ API と QML JavaScript API の間で機能互換性を実現できる可能性があります。つまり、QML スクリプティングによって高速で動的な UI を構築し、より低レベルな制御やデータ操作が必要になった場合には、そのコードを容易に C++ に移植できるようになる、ということです。

私個人としては、「未来はもう始まっている」と感じています。ぜひ Qt 6.11 のプレリリースをインストールするか、Qt をソースからビルドして、Canvas Painter を試してみてください。そして、バグ報告や改善提案のチケットをぜひ作成してください。実際のユーザーからのフィードバックこそが、私たちが改善を進め、テックプレビューから Qt の正式サポート機能へと昇格させるための最良の方法です。

次回のブログ記事では、新しい Canvas Painter の機能についてさらに詳しくご紹介する予定です。ぜひまた読みに来てください。


Blog Topics:

Comments