GUIと2D/3Dグラフィックス

前節で、VCSSLの文法・仕様に関する説明はほぼ終わりました。即席ガイドとしてはここで終わるべきかもしれません。 しかしここで切るとまさに「Cでいいではないか」という内容だけになってしまうので、 今回は標準ライブラリから、GUIと2D/3Dグラフィックスを選んで簡単にまとめてみます。

なお、ここで扱う機能については、Vnanoでは全て削られており、VCSSLでのみ使用できます。
- 目次 -

GUI

まず、VCSSLでGUIを扱うサンプルコードは以下のようになります:


import GUI;

// ウィンドウ、テキストフィールド、ボタンを生成
int window = newWindow(0, 0, 350, 180, "WINDOW");
int textField = newTextField(10, 10, 300, 20, "TEXTFIELD");
int button = newButton(10, 50, 300, 50, "BUTTON");

// テキストフィールドとボタンを、ウィンドウ上に配置
mountComponent(textField, window);
mountComponent(button, window);

// ボタンが押された際に呼ばれるイベントハンドラ関数
void onButtonClick(int id, string label) {
    string text = getComponentText(textField);  // テキストフィールドのテキストを取得
    popup(text);                                // ポップアップメッセージで表示
}
GUISample.vcssl

実行すると、テキストフィールドとボタンが並ぶウィンドウが表示され、ボタンをクリックすると、テキストフィールドの入力内容を表示します。

実行結果の図
実行結果

上のコードで、newWindow 関数と newButton 関数はGUI部品を生成する関数です。戻り値はintですが、これは各GUI部品を区別するためのID番号が返されます。それをint型変数に格納しています:


int window = newWindow(0, 0, 350, 180, "WINDOW");
GUISampleNewWindow.vcssl

ID番号は、例えばウィンドウAは「0」、ウィンドウBは「1」、ウィンドウCは「2」... といったように、異なるGUI部品に対しては異なる番号が割り振られます。 VCSSLでは参照型やポインタの仕組みが無いので、GUI部品や3Dモデルやその他色々なリソースは、処理系の中でこのように番号付けして一元管理されていて、その番号によってアクセスや操作を行います。

※ この「なんでもかんでもID番号で管理する」というVCSSLのよくある仕様は、「オブジェクト指向の概念不要で扱える」という初学者向けの利点があるものの、静的型付け言語の利点を放棄してしまっているわけで、さすがに少しまずかったかもと思っています。将来的には、互換を保ちつつ、必要に応じて型検査を効かせられるような、何らかの仕組みを追加で提供するかもしれません。

初期化とNULLについて

ところで実装面の話ですが、実はこういったnew〜関数の実装には、現状であまり好ましくない挙動が残っています。 それはID番号が「 0から順に割り振られる 」という事です。 そしてint型のデフォルト値も「 0 」です。つまり初期化を忘れたID格納変数でも、0番のGUI部品を参照している状態と見なせるため、初期化を忘れるとデバッグの難しいバグを招きます。 これを防ぐため、かなり後付けで強引な仕様ですが、intをNULLで初期化できます:


int window = NULL;  // NULLで初期化すれば、ID番号の格納を忘れても大丈夫

...

window = newWindow(0, 0, 350, 180, "WINDOW");
GUISampleNULL.vcssl

intにNULLを代入すると、「 リソースのID番号として絶対に割り振られない値 」が代入されます。 IDを格納するのを忘れて使っても実行時エラーで落ちます。 普通にnew〜関数が1以上を返すように実装を変えれば済むのですが、現状は互換問題から上のような解決策になっています。

イベントのハンドリング

続いてイベントハンドラを見てみます:


// ボタンが押された際に呼ばれるイベントハンドラ関数
void onButtonClick(int id, string label) {
    string text = getComponentText(textField);  // テキストフィールドのテキストを取得
    popup(text);                                // ポップアップメッセージで表示
}
GUISampleEvent.vcssl

イベントハンドラとは、ボタンのクリックなど、外部からの操作があった際に、それに対応した処理を行わせるためのもので、VCSSLでは上のような普通の関数です。

しかし上の関数をボタンに紐づけるような記述は、コード中のどこにもありません。 実はVCSSLでは、「onButtonClick」という名称(と適切なシグネチャ)の関数は、自動でボタンのイベントハンドラと見なされ、全てのボタンに紐づけられます。 つまり、どのボタンを押しても上の関数が呼ばれます。

では複数のボタンがある場合はどう区別するのかというと、引数idに、押されたボタンのIDが渡されるので、それで区別します:


void onButtonClick(int id, string label) {
    if(id == buttonA) {

        // ボタンAが押された場合の処理

    }else if(id == buttonB) {

        // ボタンBが押された場合の処理

    }
}
GUISampleEvent2.vcssl

この方式は、ボタンが多いと面倒ですが、少ない場合は手短に書けます。VCSSLは後者優先です。

さて、ここで扱ったのはテキストフィールドとボタンだけですが、概ねVCSSLでGUIをどう扱うかという雰囲気はまとめられたと思います。より詳しくは、以下の資料をご参照ください。

2D グラフィックス

続いて2Dグラフィックスです。まずはサンプルコードです:


import GUI;
import Graphics;
import Graphics2D;
import graphics2d.Graphics2DFramework;

// ここで初期化を行う
void onStart(int rendererID){
    setWindowSize(500, 300);    // ウィンドウサイズを設定
}

// ここで描画を行う
void onPaint(int rendererID){
    setDrawColor(rendererID, 0, 0, 255, 255);          // 描画色の設定
    drawRectangle(rendererID, 10, 10, 200, 200, true); // 四角形描画

    setDrawColor(rendererID, 0, 255, 0, 200);          // 描画色の設定
    drawEllipse(rendererID, 100, 100, 300, 100, true); // 楕円描画

    setDrawColor(rendererID, 0, 0, 0, 255);            //描画色の設定
    drawLine(rendererID, 20, 20, 400, 100, 10);        //線描画
}
Graphics2DSample.vcssl

実行すると、以下のような画面が表示されます。

実行結果の図
実行結果

上のコードは説明を手短に済ませるために2DCG用フレームワークを使っています。 「 import graphics2d.Graphics2DFramework; 」で読み込んでいるのがそれで、標準で使えます。 何をやってくれるかというと、ウィンドウや描画エンジンを生成したり、アニメーション用のループを回したり、フレームレートの調整を行ったりしてくれます。 なお、フレームワークを使わず、これらの処理をゼロから実装する事も可能です。

onStart 関数と onPaint 関数

上のコードでの基本構造として、onStart 関数と onPaint 関数の役割を簡単にまとめておきましょう:

描画処理の基本

さて、肝心の描画部分を説明します:


void onPaint(int rendererID){  // ここで描画を行う
    setDrawColor(rendererID, 0, 0, 255, 255);           //描画色の設定
    drawRectangle(rendererID, 10, 10, 200, 200, true);  //四角形描画

    ...

Graphics2DPaint.vcssl

まず onPaint 関数の引数rendererIDですが、これは描画エンジンのIDが渡されます。 GUI部品を生成するnew〜関数を思い出してください。あれと同じで、描画エンジンにもint型のIDが割り振られます (本来は描画エンジンもnewGraphics2DRenderer関数で、必要なら自分で何個でも生成できますが、今回はフレームワーク裏でやってくれていて、それが引数で渡されているわけです)。

続いて描画を行う関数です。 これらは第一引数に、描画エンジンのIDを指定します。 これは、例えば描画エンジンを2個生成して合成するような場合などに、どちらの描画エンジンへの命令かを区別するためです。 setDrawColor関数は描画色を設定する関数で、0〜255の範囲でRGBA(赤,緑,青,不透明度)値を指定します。 drawRectangle関数は四角形を描画する関数で、引数は「描画エンジンID, X, Y, 幅, 高さ, 塗りつぶしの有無」です。

特定キーを押すと画像保存するようにする

最後に、「S」キーを押すと画像を保存するようにしてみましょう。上のコードの末尾に追記します:


//「S」キーが押されたら画像を保存する
void onKeyDown(int id, string key){
    if(key == "S"){
        exportGraphics(getGraphics(), "test.jpg", "JPEG", 100.0);  // JPEG(100%)で保存
    }
}
Graphics2DSave.vcssl

onKeyDownはキー入力のイベントハンドラです。 画像の保存にはGraphicsライブラリexportGraphics関数を使用しています。 この関数は第一引数に「グラフィックスデータID」という、つまるところ描画用バッファ(オフスクリーンバッファ)の参照を渡す必要があるのですが、 フレームワークがgetGraphicsという関数の戻り値で返してくれるので、それをそのまま渡します。

2Dグラフィックスの使い方は大体こんな調子です。より詳しくは、以下の資料をご参照ください。

3D グラフィックス

最後に3Dグラフィックスです。サンプルコードは以下の通りです:


import GUI;
import Graphics;
import Graphics3D;
import graphics3d.Graphics3DFramework;

// ここで初期化を行う
void onStart(int rendererID){
    setWindowSize(800, 600);                // ウィンドウサイズを設定

    int axis = newAxisModel(3.0, 3.0, 3.0); // 座標軸モデルを生成
    mountModel(axis, rendererID);           // 描画エンジンに配置登録

    int box = newBoxModel(1.0, 1.0, 1.0);   // 箱型モデルを生成
    setModelColor(box, 0, 0, 255, 255);     // 色設定
    rotXModel(box, 1.0);                    // 回転
    moveModel(box, 0.5, 1.0, 1.5);          // 平行移動
    mountModel(box, rendererID);            // 描画エンジンに配置登録
}
Graphics3DSample.vcssl

実行すると、以下のような画面が表示されます。

実行結果の図
実行結果

2Dの場合同様、3Dでも簡潔に済ませるためにフレームワークを使用しました。 このフレームワークも標準で使えるもので、基本的に2D/3Dで共通設計になっています。なので、概ね同じような感覚で使用できます。

onStart 関数も2Dの場合と同様、起動時にフレームワーク側から1度だけ呼ばれる関数で、ここで初期化処理を行います。 引数には描画エンジンのIDが渡されます。このIDは立体の配置登録に使います。


// ここで初期化を行う
void onStart(int rendererID){
    setWindowSize(800, 600);                // ウィンドウサイズを設定

    int axis = newAxisModel(3.0, 3.0, 3.0); // 座標軸モデルを生成
    mountModel(axis, rendererID);           // 描画エンジンに配置登録

    int box = newBoxModel(1.0, 1.0, 1.0);   // 箱型モデルを生成
    setModelColor(box, 0, 0, 255, 255);     // 色設定
    rotXModel(box, 1.0);                    // 回転
    moveModel(box, 0.5, 1.0, 1.5);          // 平行移動
    mountModel(box, rendererID);            // 描画エンジンに配置登録
}
Graphics3DInitialize.vcssl

newAxisModel関数は、座標軸の形をしたモデルを生成する関数で、戻り値はモデルに固有のIDが返されます。 自分でポリゴンを定義してモデルを自作する事もできますが、単純なものは最初から用意されています。続いてmountModel関数で、それを描画エンジンに配置登録しています。

3Dでは2Dのように直接的に描画を行っていくのではなく、上の通りモデルやポリゴンなどの立体オブジェクトを生成し、それを描画エンジンに配置登録するという形が基本になります。 あとは描画エンジンが勝手に座標変換やシェーディングをした上でレンダリングしてくれます。 ウィンドウへの表示などやアニメーションループ、フレームレート制御などもフレームワークがやってくれます。

モデルの色設定や回転・移動などを行う関数は、第一引数にモデルのIDを渡します:


    setModelColor(box, 0, 0, 255, 255);    // 色設定
    rotXModel(box, 1.0);                   // 回転
Graphics3DSetModel.vcssl

ポリゴンをアニメーション変形させてみる

場のシミュレーションなどでは、箱型のような立体モデルよりも、四角ポリゴンを直接たくさん並べて描いて、動かしたい場合もあるでしょう。 実際に四角ポリゴンをアニメーションさせるコードを例示しておきます。


import GUI;
import Graphics;
import Graphics3D;
import graphics3d.Graphics3DFramework;
import Math;

int polygon = NULL; // ポリゴンのID格納変数
double t = 0.0;     // 時刻変数
double dt = 0.01;   // 時刻変数の加算単位

// ここで初期化を行う
void onStart(int rendererID){
    setWindowSize(800, 600);                           // ウィンドウサイズを設定
    mountModel(newAxisModel(3.0,3.0,3.0), rendererID); //座標軸モデルの生成と配置

    // 四角形ポリゴンの生成(引数は頂点XYZ座標×4頂点、しかしすぐ書き換えるので適当)
    polygon =newQuadranglePolygon(0.0,0.0,0.0, 0.0,0.0,0.0, 0.0,0.0,0.0, 0.0,0.0,0.0);
    setPolygonColor(polygon, 0, 0, 255, 255);  // 色設定
    mountPolygon(polygon, rendererID);         // 描画エンジンに配置登録
}

// ここで毎秒数十回のアニメーション更新処理を行う
void onUpdate(int rendererID){

    // 時刻を進める
    t += dt;

    // ポリゴンの座標値(正方形)
    float x0=0.0, y0=0.0, z0=0.0;
    float x1=1.0, y1=0.0, z1=0.0;
    float x2=1.0, y2=1.0, z2=0.0;
    float x3=0.0, y3=1.0, z3=0.0;

    // 時刻に応じて0番頂点の座標を変化させる
    x0=sin(t); y0=cos(t);

    // ポリゴンの座標値を更新(引数は頂点XYZ座標×4頂点)
    setPolygonVertex(polygon, x0,y0,z0, x1,y1,z1, x2,y2,z2, x3,y3,z3);
}
Graphics3DAnimation.vcssl

実行すると、四角ポリゴンの頂点の1つ (x0,y0,z0) が、X-Y平面内で円を描くようにアニメーションします。

実行結果の図
実行結果

上のコードでは、四角ポリゴンの頂点をsetPolygonVertex関数で書き換えた後、描画エンジンへの再登録などは行っていません。 この通り、描画エンジンに一度登録した立体オブジェクトは、移動や変形などが自動で反映されます。

描画性能水準について

なお、VCSSLの3D機能の実装は、現時点でZソート形式/フラットシェーディングのみです。 Zバッファ形式や、各種の補完シェーディング、テクスチャマッピングなどには対応していません。 早い話がベタ塗りのカクカクで、ポリゴン同士が重なるような場合には前後関係を正しく描画できません。

これはプラットフォームやハードウェアへの依存性を下げるため、GPUを使わないソフトウェアレンダリング式の3D描画エンジンをVCSSL処理系に内蔵しているためで、あまり大がかりなものを載せられないという点があります。

基本的にCPU処理が大きなウェイトを占めるため、GPUを強化してもあまり効果はありません。 パフォーマンスとしては概ね数十万ポリゴン/秒程度です。 シミュレーションの可視化などはある程度こなせると思いますが、ポリゴン数の多い美麗なグラフィックの3Dゲームを作ろうと思っても困難です。 このあたりの限界については、あらかじめご了承ください。

特定キーを押すと画像保存するようにする

さて最後ですが、画像の保存は2Dと同様、末尾に以下のようなコードを追記してください:


//「S」キーが押されたら画像を保存する
void onKeyDown(int id, string key){
    if(key == "S"){
        exportGraphics(getGraphics(), "test.jpg", "JPEG", 100.0);  // JPEG(100%)で保存
    }
}
Graphics3DSave.vcssl

onKeyDownはキー入力のイベントハンドラです。 画像の保存にはGraphicsライブラリexportGraphics関数を使用しています。 この関数は第一引数に「グラフィックスリソースID」という、つまるところレンダリングバッファの参照を渡す必要があるのですが、 フレームワークがgetGraphicsという関数の戻り値で返してくれるので、それをそのまま渡します。

3Dグラフィックスの使い方は大体こんな調子です。より詳しくは、以下の資料をご参照ください。

おまけ: 2D/3Dグラフの描画機能は標準で用意されています
(自力で実装しなくてもいい)

さて、今回はGUIや2D/3D描画処理についてざっと見てきましたが、最後に少しだけ触れておきたい事項があります。

ここで扱った描画処理は、点や線、面や箱型などの、基本的な図形を1つ1つ描いていくという内容でした。 これは、細かく組み合わせれば、最終的には複雑なものを描けるので、自由度は高いです。

しかし、例えば以下のような、非常によくあるデータ解析の場面で、いちいちそういう描画処理をコードに落とし込むのは面倒です:

こういう処理は、普通はグラフソフトやグラフ描画ライブラリに描いてもらいますよね。

実はVCSSLにも、同じ開発元の「リニアングラフ2D」と「リニアングラフ3D」というグラフソフトが同梱されていて、それを標準ライブラリとして使う事ができます。


上記のような場面では、ゼロから自力で2D/3D描画処理を書き下すよりも、こちらを使った方がよっぽど効率的です。 使い方については、リニアングラフの公式サイトの側に解説ページがあるので、そちらをご参照ください:

また、VCSSL公式サイト内の「コードアーカイブ」のコーナーでも、色々なグラフ制御のコード例を配信しています:

このガイドは次回で最終回です。次回は、VCSSLから他言語のコードを実行する方法についてまとめます。