C++ ときどき ごはん、わりとてぃーぶれいく☆

Wonder Rabbit Projectのなかのひとのブログ。主にC++。

Qt5 / Qt Quick 2.0 第2回 続・はじめたよ : ShaderEffect

前回に続き、例の紹介ビデオを教材に、25分あたりからのシェーダーエフェクトをざっくり遊んでみる事にしましょう。今回は冗長な解説はせずに初出の事項についててきぱき解説するよ。

教材

対象

  • QMLについてなんとなく雰囲気くらい分かった程度のさむばでぃ
  • プログラミング一般の基礎知識はありそうなさむばでぃ
  • GLSLとか触った事あるか基礎的な概要くらい知ってるさむばでぃ

雛形

教材にしたビデオの25:50付近で最初の雛形が登場します。

f:id:USAGI-WRP:20130103165416p:plain

「しれっとロゴ画像ちゃうやんけー」などは気にしなくて良い事です。

解説

Row

内包する要素群をカラム要素として1つの行(=row)を成して表示する為のGUIコンテナー。今回はImageとShaderEffectの2つの要素を内包して使われている。

Image

画像をロードして表示するGUI部品。sourceはURLに対応。smoothはboolでデフォルトがtrueなので現在は明示的に指定している意味は無い。

ShaderEffect

他の何らかの要素(RectangleでもImageでも他適当な)にOpenGLにより頂点シェーダーとフラグメントシェーダーを適用して表示するエフェクター。シェーダープログラムの定義方法はGLSL。

頂点とフラグメントしか無いのが残念だけど、QMLで手軽にGLSLエフェクターも使えるのは嬉しいですね。今回のメイン。

property

これはC++による実装も含め、QMLを含むQtのプロパティシステムについて触れる必要が生じる。

とは言え、現段階で全貌を把握する必要はないので、今回登場するQMLでの property キーワードは要素にプロパティをQMLから追加またはオーバーライドする宣言であるという事だけ分かれば十分です。

variant

property宣言に続いて登場している variant は型名です。QMLではintやrealやstringなどの型システムがあり、QML要素のプロパティはC++バインディングされていますから当然内部的には型があります。property宣言でプロパティを追加またはオーバーライドする為にはそのプロパティの型も指示する必要があり、property宣言とプロパティ名の間に今回はvariantと型名が入って居ます。

variant型は一般的なプログラミングにおける動的型付けでお馴染みのバリアント型だと思って置けば良いのですが、仕様には

The variant type is a generic property type. It is obsolete and exists only to support old applications; new applications should use var type properties instead.

曰く、「variant型はジェネリックなプロパティの型。これは廃止されており、古いアプリケーションらの為だけにサポートされている。新しいアプリケーションでは代わりにvar型をプロパティらに使ってね。」

と、いうわけで既に時代は variant ではなく var にすべきなのでありました。

var

あらゆるデータの型に対する参照が可能なQt新時代のバリアント型にしてECMAScriptとも互換な var 型です。

これからはvariantじゃなくてvarを使いましょう。

雛形・改

GLSL を書きますよ

教材にしているビデオの26:30辺りでShaderEffectへしれっとGLSLがコピペされて登場します^^;

GLSLを知っているか、或いはHLSLでもCgでも良いのでシェーダー言語による基礎的なGPUプログラミングの知識があれば、なんという事の無い頂点シェーダーとフラグメントシェーダーです。

ここ数年の一般的なシェーダプログラムの実行方法ではC++レベルの通常のアプリケーションがOSで実行されるとOpenGLなどを介しGPUドライバーにシェーダープログラムを実行直前にコンパイルさせます。よってQML要素レベルでもGLSLプログラムソースをstringで定義する形式になっています。

f:id:USAGI-WRP:20130103211510p:plain

まだ、実行しても特に先ほどの雛形から見た目の変化はありません。

GPUプログラミング

GPUプログラミングに詳しくない方の為に少しだけ解説すると、プログラマブルな世代の現代的なGPUでは基本要素として2つのプログラム構成部分が必ず用意されています。1つは頂点シェーダー、もう1つはフラグメントシェーダー(≈Microsoft系の世界ではピクセルシェーダーとも呼ばれます)。

GPUは元々CPUだけでは描画しきれない膨大な3Dグラフィックスを描画する為に進化した計算機の部品です。3Dグラフィックスの描画と云うのは、3Dのデータ(≈3Dモデルデータ)を特定のパラメーターで2Dの絵に変換して、それをディスプレイに接続されたフレームバッファーへ書き出す仕事が基本です。

通常一般のディスプレイは2Dですから、2Dの画像データを単に描画するのであれば、座標を指定して画素情報をコピーするだけです。しかし、3Dのデータと云うのは一般に画素ではなく形状を構成する点群(≈頂点群)とその点群をどの様に接続した面で物体が構成されるのかという約束事で構成されており、3Dの世界を2Dのディスプレイに描画する為にはこの様な3Dモデルデータを、どこから見ているのか(≈カメラ)を基にまるで撮影のシミュレーションそのものの様な処理を計算で行わなければなりません。綺麗な計算機画像を得る為には3Dモデルデータも数万個やそれ以上の頂点で構成されていたり、複雑な光線に関する計算が必要となり、とてもとても重たい処理で、CPUだけでやっていてはとてもとても計算時間が必要なものです。

そこで、CPUの様に汎用的に何でもプログラマブルに計算できる訳じゃないけれど、代わりに3Dグラフィックスに関する大量の計算には滅法強いGPUなる部品を作ったのです。

その流れは今でも勿論GPUプログラミングに影響、というかそのものとして受け継がれて居て、大量の3Dモデルデータの頂点群を特定のパラメーター(視点だとか)ではスクリーンの何処に映るのかを計算する頂点シェーダー、スクリーン上に転写された点群からそれら点群により構成される面に色塗りをするフラグメントシェーダーがGPUプログラミングの基本となっています。

現代的なGPUプログラミングでは、今回も用いられるGLSL(≈OpenGL)の他、HLSL(≈Direct3D)、Cg(NVIDIA言語)などC風の文法の専用言語を用います。ちなみにGPGPUGPUの変態的な応用技術であってGPUは本来GPGPUを行う為に設計された訳ではありませんから、GPUで工夫した計算をさせようと、特にDirectComputeやOpenCLやCUDAなどそれ用に更に特殊化されたド変態言語やAPIを用いない限りはいろいろとGPUの特徴(というか癖というか生の仕様が透けて見えるというか)なプログラミングのスキルや知識が少なからず必要です。

です、が、GPUプログラミングの仕組みや概要が分かってしまえばなかなか楽しいものではありますので、他の部分はさておいて突拍子もなくGLSLで割と純粋にGPUプログラミングを楽しめるプラットフォームとしてQt5/Qt Quick 2.0が目の前に現れてくれているのですから、之を期にでも是非少しは遊んで見る事をお勧めします。(実際生のOpenGLDirect3DではGLSLなどのシェーダー言語を書く他にもGPUを使う為のアレコレの初期化やシェーダープログラムのコンパイルなどの制御やそもそものGUIの制御などそれなりに煩わしいものです。)

余談

ちなみに、頂点シェーダーとフラグメントシェーダーに加えて現代のGPUプログラミングでは超ド変態な進化を遂げていて数年前から頂点データ群から別の頂点データ群を生成するジオメトリーシェーダーとかストリームアウトとか、サブディビジョンサーフェイス用にと実装が進んだテッセレーターがどうのとか、まあいろいろともっと進化した機能を使い熟す事も可能な、まあ兎に角ド変態デバイスとその取り扱いへと進化しています。著者もテッセレーターがどうした辺りからGPUをいじらなくなって久しいです。

解説

vertexShader

  1. qt_TexCoord0 に qt_MultiTexCood0 を代入しています。
    1. qt_MultiTexCood0 は attribute highp vec2 で頂点シェーダーに渡されて来る頂点属性の2つ目の値。
  2. gl_Position に qt_Matrix * qt_Vertex を代入しています。
    1. qt_Matrix は uniform highp mat4 でシェーダーに渡されている値。
    2. qt_Vertex は attribute highp vec4 で頂点シェーダーに渡されて来る頂点属性の1つ目の値。

attribute 各種はQMLのバックエンドの仕様に基づいて渡されてくる(はず)。少なくともQMLのShaderEffectを使う分には頂点属性は固定だと思って良い(はず)で、QMLのShaderEffectの頂点属性は { vec4 頂点座標, vec2 テクスチャー座標 } と推論できる。

と、いうわけで、日本語で読み下すと、

  1. テクスチャー座標をフラグメントシェーダーへパイプ。
    1. テクスチャー座標は頂点属性で渡されて居る値を引っ張り出す。
  2. スクリーン座標系へ変換した頂点座標をフラグメントシェーダーへパイプ。
    1. スクリーン座標系への入力頂点座標の変換はシェーダーに共通で渡されて居るバッファーから引っ張り出した4x4行列(≈ここでは3DCG用語で云うならば(ワールド×ビュー×スクリーン)変換行列)。
    2. 頂点の入力座標は頂点属性で渡されて居る値を引っ張りだす。

と、いうわけで、推測ですがQMLの元要素を4頂点の四角の座標+元要素を画像としてテキスチャーで転送されたデータがシェーダーにやってきて居て、変換行列の処理については2D以外等への応用を視野にしていて少なくともこの推測の元では単位行列でも入ってて、名目上の頂点座標処理を記述しつつテクスチャー座標と共に頂点座標をフラグメントシェーダーに出力(パイプ)している、のだと思います。

※実際ビデオを見続けると、そんな様な事の一部始終を解説してくれている。

GLSL tips 1

attribute は頂点シェーダーに渡って来る1つ1つの生の頂点データを変数にバインドする旨のGLSLのキーワード。HLSLで言ったらセマンティクスとかに近い辺り。

uniform は頂点毎などではなくシェーダープログラム全体に共通して渡されるデータを変数にバインドする旨のキーワード。HLSLで言ったらSM5の定数バッファー(HLSLの、だよ。GLSLのconstとかじゃないよ)の取り扱い的な感じ。

varying は頂点シェーダー→フラグメントシェーダーとパイプされるGPU内部での変数を宣言するキーワード。HLSLで言ったらシェーダー間のoutとinネ。

highp は C++風に言ったら long double 的な感じ。GPUネイティブな小数点数型(≠浮動小数点数型、ハードウェアによっては浮動とは限らない)にも精度の異なる型がサポートされていて、C++の float-double-long double みたいに一定の精度を区切りにGPUのハードウェアに依存した精度が lowp-midp-highp に割り当てられる。詳しはこちらを見ると分り易いと思うよ。

mat4 は 4x4 の行列型で、その前に highp とか付けて highp mat4 とか宣言して highp で構成された 4x4 行列の型という事になる。vec4 や vec2 も同様でこちらは4次元と2次元のベクター(≈要素次元N個を表すデータ配列)。

void main() { ... } は関数の定義で、mainは各シェーダーのエントリーポイント的な関数と思って差し支え無い。

fragmentShader

  1. 描画対象領域について2Dテクスチャーサンプラーでシェーダーに渡されたテクスチャー(GPU組み込みのテクスチャーサンプラー機能で要素の値を補完して取得できる)、シェーダーに共通で渡された不透明度を基に、サンプリングされた色に不透明度を掛け算して描画点の色を決定、出力している。
GLSL tips 2

sampler2D はGPUの内部機能の1つテクスチャーサンプラーを扱う型の宣言。テクスチャーサンプラーはC++風に言えば { return texture.get_pixel_rgba(texture_coordinate); } 的なアルゴリズムの関数でテクスチャーから要素を得るためのGPU組み込みの仕掛け、機能、と思えばOK。実際には一般に補完機能付きのアルゴリズムを用いる。

float は vec4 や vec2 風に言えば vec1 、つまり単一要素、スカラー値の型名です。

QMLからGLSLのuniformへ値を渡す

ビデオの29:50頃のコードでQMLからGLSLのuniformへと値を渡す手法についてデモを行なっています。

f:id:USAGI-WRP:20130103225431p:plain

oh... わたしのアイコン使うんじゃなかったかしら…orz

解説

ShaderEffect::vertexShader はすっぱり無くなって居ますが、先程の内容がデフォルトなので無くても同様だったという訳。

さて、肝心のこの状態の内容ですが、先ずShaderEffect要素にQMLでユーザー独自のプロパティを追加して値をセットしてあります。

次に、ShaderEffect::fragmentShader のフラグメントシェーダーの定義中で uniform highp で先ほど定義したプロパティ名と同じ名前の変数を宣言しています。

これだけで QML → フラグメントシェーダー の値の受け渡しが出来ています。簡単で良いですね!

GLSL tips 3

GLSL(HLSLでもそうですが)では、基礎的な数学関数が言語組み込みのAPIで利用できます。 sin は正弦を求める御馴染みの三角関数ですね。今回は引数が float ではなく vec2 ですが、ベクターの要素毎に sin を求めたベクターが出力されるオーバーロードがあると思って差し支えありません。

アニメーションの適用

ぎゅうううんぎゅうううん…私のアイコンがー…orz

解説

ShaderEffectにQMLでtimeプロパティを追加しています。フラグメントシェーダーではこのtimeに依存する様に先ほどのsin関数の中を書き換えてあげたのみ。

レンダリングはQt5がよしなに回してくれて居るので再描画毎にシェーダーに渡るプロパティtimeの値が取得され、その値は NumberAnimation によって動的に生成されており、アニメーション(=時刻に応じた描画内容の変化)が実現できて居ます。

NumberAnimation on time

NumberAnimation on time は NumberAnimation なる関数オブジェクト的なQML要素をtime変数にバインドして居ます。

私のソースの書き方だとちょっとややこしいかもしれませんが、直前の property real time からの続きとしての定義ではなくて、 property real time はそれだけで値の定義は無いが変数の宣言としては完結しています(C++で言えば int value = 123; と定義を伴わずに int value; と宣言のみと言う事)。

NumberAnimation は ShaderEffect の単に子要素としてQMLで定義し、 on time と要素宣言に追加してある事で親要素の time の内容としてバインドさせて居ます。

NumberAnimation

QML要素のプロパティをアニメーションさせる事に特化した要素で、値を from から to へと評価時刻により変えて出力します。厳密な範囲としてはfromもtoも共に生成される値に含まれます(C++のrangeのbegin-endとは異なる)。

プロパティは from と to だけの様に仕様をチラ見しただけでは思えますが、きちんと SmoothedAnimation と SpringAnimation からの継承についても触れられて居ます。

duration は to から from への変化をマッピングする時間間隔を指定します。単位は ms (ミリ秒)。

loops は アニメーションの繰り返し方の指定で、デフォルトは 1 、つまり1回だけアニメーションしてループはしない。Animation.Infiniteについては後述。

from と to が含まれるのか確認したい?

こんな具合で小細工すれば、from の 0 も to の 1 も含まれてコンソールに出力される事を確認できます。

Animation.Infinite

Animation.Infinite は NumberAnimation の基底型でもあるAnimationに定義された定数。

loops に Animation.Infinite がセットされると stop() が明示的に呼び出されるまでアニメーションを繰り返してくれる。

非表示のShaderEffectの元要素を定義できる ShaderEffectSource

ビデオを続けると31:30頃にShaderEffect専用にそれ自体は表示しないQML要素の定義方法が紹介されます。

解説

ShaderEffectSource

ShaderEffect の source とする要素を定義するQML要素。動作はソースを見たまま予想通りで分り易いですね。

今回の結び

ShaderEffectを使うとQMLだけでもリッチなエフェクトを簡単に実装できてしまいますね。GUI ToolkitとしてもUI定義としてもECMAScriptベースのアプリケーションエンジンとしても強力なQMLにGPUパワーも少なくともエフェクターとしては、驚くべき事にバックエンドのC++コードに必要無ければ全く触れずとも多彩な表現力を発揮できる、と。

ビデオをもう少しだけ進めると次は "Drawing with JavaScript, based on HTML5 Canvas API" だそうで。明日も楽しみですね。…とは言え、そろそろC++コード書かないとなかのひとがうぐぅ…って感じではありますけれど・x・;