追記's:

  • 2017/12/20:半透明モデルの取り扱いについて追記しました。
  • 2018/01/08:カメラのアスペクト比について追記しました。(というかユーザカメラのサンプル、アス比間違ってました、ごめんなさい…。。。)

この記事は

hgimg4自体は2013/07/11にリリースされたHSP3.4beta2同梱のものが初リリースだそうです。
それから暫くの間あまり精力的な更新が行われていませんでしたが、今年2017年はHSP3.5正式版のリリースもあったからか、hgimg4もそれなりに機能拡張がなされています。

一方で、HSP3.5はベータから正式版まで期間が長く、その間に入ったhgimg4の機能拡張は元より、そもそもHSP3.5時点ではどこまで出来るのか、あまり知られていないのでは、という見解を個人的には持っています。

そこでこの記事では、「hgimg4(再)入門」と題打って、HSP3.5版をターゲットとしてhgimg4の基礎についてじっくり解説していきたいと思います。

前準備編

外部リソースの準備

早速hgimg4でのスクリプトを…その前に、hgimg4の説明では必ずと言っていいほど付き纏う、必要なリソースについてです。

hgimg4では#include "hgimg4.as"したスクリプト単体では動作しません
スクリプト以外にhgimg4がシステム的に必要とするリソース(ファイルなどの外部データのこと)を、スクリプトや実行ファイルと同じディレクトリに配置する必要があります。

リソースフォルダについて
HGIMG4では、起動時に実行するスクリプトと同じフォルダにある
「res」フォルダから必要なリソースを読み込みます。
リソースファイルは、「sample/hgimg4/res」のフォルダに含まれています。
以下のファイルは、起動のために必要ですので、実行ファイル作成時なども必ず入れておいてください。
res/font.gpb
res/shaders フォルダ(中のファイルも含む)

公式サイトhgimg4.txtより

これから紹介していくスクリプトを実行する前に、かならずリソースを同じディレクトリにコピーで置いてから実行してください。
なお、一部サンプルスクリプトではhgimg4のサンプルとして含まれているモデルファイルも使っていますので、sample/hgimg4/resごとコピーするのが無難です。

ウィンドウをだすまで

HSPなので、hgimg4を使っていようとウィンドウをだすまではとても簡単です。

// hgimg4ランタイムを使う
#include "hgimg4.as"

// hgimg4ランタイムのリセット
gpreset

// メインとなるウィンドウを準備
screen 0, 640, 480

repeat
    redraw 0//< 描画始め
    redraw 1//< 描画終わり
    await 16//< 描画後のウェイト
loop

これだけです、gpresetというhgimg4の初期化命令が入っていますが、それ以外は至っていつも通りなコードですね。

…ところが、これではうまくいってないところがあります

ウィンドウサイズの指定

上記ではscreen 0, 640, 480でウィンドウサイズを 640 x 480 にしたつもりのコードですが、hgimg4ではこれは意味を成しません。

これは後々にも問題になってくるところなのですが、hgimg4はHSP3Dishのシステム上に構築されており、HSP3Dishと同じ制約を受けます

HSP3Dishはスマートフォンなどでも実行できるランタイムとなっていますが、このランタイムになった時に動的にウィンドウの解像度を変える、という機能が消えました。
そのため、HSP3Dishに乗っているhgimg4でも同様に、実行時にウィンドウの解像度を決めることが出来なくなっています。

ではどうやってウィンドウサイズを指定するかというと、スクリプトから実行時はスクリプトと同じディレクトリにおいてあるhsp3dish.iniファイルを、実行ファイルを作った際はコンパイルオプションで指定をすることが出来ます。
(詳細はHSP3Dish プログラミングマニュアル・基本仕様ガイドの起動設定ファイルについてを参照してください)

hsp3dish.iniファイルは次のように書きます。

hsp3dish.ini
wx=640
wy=480

コンパイルオプションには#packoptを使って次のように書きます。

#packopt xsize 640      ; 横サイズ
#packopt ysize 480      ; 縦サイズ

hgimg4はこのように微妙にどこを参照すればいいか分からない情報があったりするので注意が必要です。

ここまでで、hgimg4で指定したスクリーンを表示したいだけなのに、リソースのコピーやhsp3dish.iniファイルの用意など、従うべきルールがいくつか存在したことに面食らった人も居るかと思います

分からない場合は詳しい人が多いのでHSP3掲示板に素直に聞きに行きましょう。
きっと詳しくて親切な人が教えてくれると思います…たぶん。

OpenGLによる描画

hgimg4は描画にOpenGLを使用しています。
OpenGLはグラフィクスハードウェア向けのAPIで、難しい話を省略するとGPUを使って描画を行うことが出来るAPIです。

GPUを使って直接描画をコントロールできるため一般的に描画が高速ですが、描画のあらゆる操作をOpenGLを通して行うため、描画の仕方や作法はOpenGLが定めたものに従うことになります。

通常これらはHSP側(ひいては内部で使っているライブラリであるGamePlay3D)で隠蔽してくれているため意識することはない…と良かったのですが、残念ながらhgimg4でもOpenGLの作法を多少なり知っておく必要があります

描画できるウィンドウは1つだけ

HSP3Dishというスマートフォンもターゲットにしたランタイムと、OpenGLというGPUを使った描画のため、HSP側から画面に表示できるウィンドウはスクリーンIDが 0 の1つだけです。1

// メインとなるウィンドウを準備:OK!
screen 0, 640, 480

// サブとなるウィンドウを準備:NG! ID0以外にウィンドウは作れない!
screen 1, 640, 480

描画結果の反映タイミング

元々HSPではウィンドウに対してmesboxf等で描画が発生した場合、それが即座にウィンドウに反映されていました。
ただしそれでは画面がチラついてしまうため、通常はredraw 0で一旦ウィンドウへの描画反映を無効化し、redraw 1で描画結果の反映を行うというフローが慣例的によく使われています。

一方、OpenGLではそもそも描画したものが即座に画面に反映される訳ではありません

GPUでのやりとりの都合になるのですが、画面に表示している描画バッファに書き込むのが難しいからです。
なので、OpenGLでは次に表示する描画バッファを持っておき、その描画バッファに対して描画が済んだ後、この描画バッファを画面に表示してください、という流れで最終的な描画が行われます。

さて、ではこれをhgimg4ではどうカバーしているかというと、スクリーンID 0(つまりメインの描画バッファ)に対するredraw 1のタイミングで画面へ描画バッファを転送するようにしています。

このことから、hgimg4では例えどんな小さな単位の描画でも、redraw 0redraw 1の間に挟む必要があります。

どんなに短い描画命令でもredrawで挟む
gsel 0
redraw 0
    pos 10, 10 : mes "string"
redraw 1

そしてもう一つ、hgimg4では3D描画の都合があり、redraw 0のタイミングで画面の内容がクリアされます

redraw0のタイミングで画面がクリアされる
gsel 0
redraw 0
    pos 10, 10 : mes "string"//< 何か描く
redraw 1

// 追加で何か描くつもりが…
redraw 0//< ここで前に描いた内容は全てクリアされる!
    pos 10, 20 : mes "string string!!!!"
redraw 1

つまり、hgimg4では例えどんな小さな規模のプログラムでも、描画するものは全て単一のredraw 0redraw 1の間に挟む必要があります。

これとスクリーンID 0 に対するredraw 1は画面へ描画バッファを転送する、という縛りを合わせると、次の作法が導かれます。

hgimg4では例えどんな小さな規模のプログラムでも、描画するものは全て単一のredraw 0redraw 1の間に挟む必要があり、1 フレームできっかり 1回だけスクリーンID 0へのredraw 0redraw 1を行う必要がある

補足:画面のクリアについて

一応、hgimg4ではデフォルトでredraw 0でクリアする設定になっているだけなので、この設定を切ることが出来るにはできます。

redraw0でクリアする設定を切って描画
gsel 0

setcls CLSMODE_NONE//< クリアしない設定にする

redraw 0
    pos 10, 10 : mes "string"//< 何か描く
redraw 1
redraw 0
    pos 10, 20 : mes "string string!!!!"
redraw 1

実行してみれば分かりますが、画面がチラつきます。
これはスクリーンID 0のredraw 1で画面に描画バッファを転送してしまうからです。

画面クリアはしないでプログラムを書いていってもいいですが、実は色クリアは無効にできてもデプスのクリアは行われるなど細かい挙動で破綻しやすいので、個人的には単一のredraw 0redraw 1制御が一番安全だと思っています。

基本編

hgimg4の前準備が終わりました、ここまでの知識があればとりあえず

  • 実行できない
  • 画面が無駄にチラつく
  • 意図した絵をだすつもりが画面が真っ白

みたいなことは少なくなると思います。

ここからは、(ようやく本筋の)hgimg4を使った描画機能についてになります。

ボックスをだす

折角のhgimg4、3D描画ランタイムなので、やはり3D描画から入りたいですよね。
ということで、3D空間にボックスを出してみましょう。

// hgimg4ランタイムを使う
#include "hgimg4.as"

// hgimg4ランタイムのリセット
gpreset

// 白いボックスを一つ用意
gpbox id_box, 20, 0xffffffff

repeat
    redraw 0//< 描画始め
        gpdraw//< hgimg4に登録されたオブジェクトを描画
    redraw 1//< 描画終わり
    await 16//< 描画後のウェイト
loop

実行してみると、画面が真っ白。

abox_white.png

…よく見ると中央には灰色っぽい四角があるようにも見えます。

hgimg4ではredraw 0で画面がクリアされることは先に述べましたが、デフォルトではクリア色は白に設定されています。
白いキャンバスに白いボックスを描いても分かりづらいだけなので、ここではクリア色を黒に設定しましょう。

// hgimg4ランタイムを使う
#include "hgimg4.as"

// hgimg4ランタイムのリセット
gpreset

// ボックスを一つ用意
gpbox id_box, 20, 0xffffffff

// 画面クリアの設定:毎回黒でクリア
setcls CLSMODE_SOLID, 0x00000000

repeat
    redraw 0//< 描画始め
        gpdraw//< hgimg4に登録されたオブジェクトを描画
    redraw 1//< 描画終わり
    await 16//< 描画後のウェイト
loop

無事黒背景で白いボックスが出ていることがちゃんと確認できました。

abox_white_onblack.png

ボックスを回す、移動させる

折角ボックスを出したはいいものの、正面から見ているだけでは全く味気ないですね。
ここでは、作ったボックスを回転させたり、移動させたりしてみましょう。

移動させるには、setposが使えます。

setpos <オブジェクトのID>, <X>, <Y>, <Z>

回転させるには、setangsetquatが使えます。

setang <オブジェクトのID>, <X>, <Y>, <Z>
setquat <オブジェクトのID>, <X>, <Y>, <Z>, <W>

今回のサンプルでは使いませんが、スケールを変えるにはsetscaleが使えます。

setscale <オブジェクトのID>, <X>, <Y>, <Z>

また、今の値に対して加算したりする場合に便利な、addposaddangなどの命令も存在しています。

オブジェクトのIDというのは、gpboxなど何か3D形状を作ったりモデルデータ読み込む命令を使った時、貰えるIDです。
hgimg4ではこのIDを使うことで対象のパラメータを設定することが出来ます。

それでは、少し適当ですがスクリプトを。

repeat
    // ボックスに対する操作
    setpos id_box, cos(deg2rad(cnt)*1.7)*10.0, sin(deg2rad(cnt)*1.7)*10.0, 0.0
    setang id_box, deg2rad(cnt)*0.97, deg2rad(cnt)*0.87, deg2rad(cnt)*0.1

    redraw 0//< 描画始め
        gpdraw//< hgimg4に登録されたオブジェクトを描画
    redraw 1//< 描画終わり
    await 16//< 描画後のウェイト
loop

record_20171217_162707.gif

ボックス以外の形状

どうせなので、ボックス以外の形状のものも試してみましょう。
と言いつつ、hgimg4ではボックス以外の形状はプレートしか用意されていません。
(正確にはgpplategpfloorがありますが、いずれもプレート(板)形状です)

…物悲しいので、サンプルに入っているアヒルの3Dモデルも読み込んで表示してみましょう。

プレートモデルはgpplate命令を、3Dモデルの読み込みはgpload命令を使います。

// ボックス
gpbox id_box, 10, 0xffffffff
setpos id_box, -20.0, 0.0, 0.0
setang id_box, deg2rad(-30), deg2rad(-30), 0

// プレート
gpplate id_plate, 10.0, 20.0, 0xffff0000
setang id_plate, deg2rad(-30), deg2rad(-30), 0

// 3Dモデル
gpload id_model, "res/duck"
setpos id_model, 20.0, 0.0, 0.0
setang id_model, deg2rad(-30), deg2rad(-30), 0
setscale id_model, 10.0, 10.0, 10.0

ashape.png

オブジェクトを削除する

オブジェクトを追加する場合、オブジェクトのパラメータを設定したりそもそもモデルオブジェクトなのかなどを指定するので大変ですが、削除するのは大変簡単です。
hgimg4内ではオブジェクトIDで管理されているので、特定のオブジェクトを削除するのはdelobjでオブジェクトIDを指定するだけです。

delobj <オブジェクトのID>

簡単なのでサンプルはなし。

ライティングする

hgimg4で読み込まれたモデルへはデフォルトでライティングがされます。
使われているのは太陽光の近似であるディレクショナルライト一灯のみですが、それでもないよりは全然マシです。

ディレクショナルライトの方向を変える

デフォルトで作られるディレクショナルライトは特定の予約されたIDGPOBJ_LIGHTを持ちます。
このIDを使って角度を指定すると、ディレクショナルライトの方向を変えることが出来ます。

// ライトの角度
light_rotx = deg2rad(-45.0)
light_roty = deg2rad(0.0)

repeat
    // ライトの調整
    stick key, 0x0f
    if (key & 1) : light_roty -= deg2rad(5)
    if (key & 2) : light_rotx += deg2rad(5)
    if (key & 4) : light_roty += deg2rad(5)
    if (key & 8) : light_rotx -= deg2rad(5)
    setang GPOBJ_LIGHT, light_rotx, light_roty, 0.0

    redraw 0//< 描画始め
        gpdraw//< hgimg4に登録されたオブジェクトを描画
    redraw 1//< 描画終わり
    await 16//< 描画後のウェイト
loop

tmp_20171217_164951.gif

追加機能:ディレクショナルライトの色

GPOBJ_LIGHTcolorパラメータを変更すると、ディレクショナルライトの色を変えることが出来ます。2
…のですが、gpboxgpplateなどで作ったモデルの場合、GPOBJ_LIGHTdirパラメータがライト色として解釈されるようです。3

// ライトの色
light_r = 0.0
light_g = 0.0
light_b = 0.0

repeat
    // ライトの調整
    key = 0
    getkey key, 'Z' : if (key) : light_r -= 0.05
    getkey key, 'X' : if (key) : light_r += 0.05
    getkey key, 'C' : if (key) : light_g -= 0.05
    getkey key, 'V' : if (key) : light_g += 0.05
    getkey key, 'B' : if (key) : light_b -= 0.05
    getkey key, 'N' : if (key) : light_b += 0.05
    setdir GPOBJ_LIGHT, light_r, light_g, light_b
    setcolor GPOBJ_LIGHT, light_r, light_g, light_b

    redraw 0//< 描画始め
        color 255, 255, 255 : pos 10, 30 : mes strf("R=%f, G=%f, B=%f", light_r, light_g, light_b)
        gpdraw//< hgimg4に登録されたオブジェクトを描画
    redraw 1//< 描画終わり
    await 16//< 描画後のウェイト
loop

tmp_20171217_171057.gif

setcolorなどヘルプに載ってない(けど実装されている)命令なので、これはおまけ程度の内容になります。

その他のライト

一応、gplightgpuselight命令を使うことでデフォルトで使われるライトを変えたりすることは出来る…予定のようです。

OpenHSPのソースコード追ってみましたが現状はディレクショナルライトしか対応してなかったので、ライトを切り替えても意味はないですが。
ポイントライトはあると3D表現ってグッと幅が広がるんですけどね…それはhgimg4の進化待ちとなります、とても残念。

カメラを凝る

ここまでモデルを動かすことでいい感じの画面にしていましたが、普通に考えてもっと複雑な事をし始めるとすぐ大変なコードになることでしょう。
ここではhgimg4でのカメラの扱いについて説明していきます。

カメラの位置、回転

ライティングの時と同じように、実はカメラも予約された特定のIDGPOBJ_CAMERAを持っています。
そのため、GPOBJ_CAMERAの位置や回転を変えることで、そのまま描画に使われるカメラの位置と回転も変えることが出来ます。

// カメラの位置と向き
camera_posx = 0.0 : camera_posy = 0.0 : camera_posz = 100.0
camera_rotx = deg2rad(0) : camera_roty = deg2rad(0.0)

repeat
    // カメラの調整
    stick key, 0x0f
    if (key & 1) : camera_roty += deg2rad(1)
    if (key & 2) : camera_posx -= sin(camera_roty)*1.0 : camera_posz -= cos(camera_roty)*1.0
    if (key & 4) : camera_roty -= deg2rad(1)
    if (key & 8) : camera_posx += sin(camera_roty)*1.0 : camera_posz += cos(camera_roty)*1.0
    setpos GPOBJ_CAMERA, camera_posx, camera_posy, camera_posz
    setang GPOBJ_CAMERA, camera_rotx, camera_roty, 0.0

    redraw 0//< 描画始め
        gpdraw//< hgimg4に登録されたオブジェクトを描画
    redraw 1//< 描画終わり
    await 16//< 描画後のウェイト
loop

tmp_20171217_173103.gif

特定の点を注視する

回転はsetangsetquatを使って設定できることは説明しましたが、カメラなどでよく使う特定の点を注視するについては別にgplookatという命令が用意されています。

// カメラの位置と向き
camera_posx = 0.0 : camera_posy = 0.0 : camera_posz = 100.0

repeat
    // カメラの調整
    t_len = sqrt(camera_posx*camera_posx + camera_posz*camera_posz + 0.00001)
    tx = -camera_posx / t_len : tz = -camera_posz / t_len
    rx = tz : rz = -tx
    stick key, 0x0f
    if (key & 1) : camera_posx += rx : camera_posz += rz
    if (key & 2) : camera_posx += tx : camera_posz += tz
    if (key & 4) : camera_posx -= rx : camera_posz -= rz
    if (key & 8) : camera_posx -= tx : camera_posz -= tz
    setpos GPOBJ_CAMERA, camera_posx, camera_posy, camera_posz
    gplookat GPOBJ_CAMERA, 0, 0, 0//< 常に中心を見る

    redraw 0//< 描画始め
        gpdraw//< hgimg4に登録されたオブジェクトを描画
    redraw 1//< 描画終わり
    await 16//< 描画後のウェイト
loop

tmp_20171217_173815.gif

ところで、hgimg4ではIDを使うことでそのカメラだろうと普通のモデルだろうとオブジェクトの位置や回転を設定することができることは述べました。
そして、カメラはカメラオブジェクトの設定が反映されることも述べました。

このことから、実はgplookatは通常のモデルに対しても使うことが出来ます

ただし、カメラの都合になるのですが、gplookatは特定の位置に対してZ-が向くように回転を設定する命令です。4
使うこと自体に問題はありませんが、モデルのローカル座標系など使い方は少し工夫する必要があります。

画角、ニア、ファーを設定する

※ファーはFurじゃなくてFarの方です。

デフォルトで生成されているGPOBJ_CAMERAは画角、ニア、ファーが固定のカメラです。
自前で画角、ニア、ファーを設定したい場合、まず自前でカメラオブジェクトを作り、それを描画のカメラとして設定必要があります。
(ちなみに、hgimg4ではカメラと言いつつ実際は射影の設定を行うので、設定の際は描画対象のアスペクト比も必要になります)

カメラオブジェクトは実は実体がなく、特定のオブジェクトに対してカメラの属性を付与する形で行われます。
なので通常、何もないオブジェクトをgpnullで生成し、これに対しgpcameraでカメラ属性を付与します。
このカメラ属性付与時に使うgpcamera命令で、画角やニア、ファーの設定が可能になっています。

そして、作ったカメラオブジェクトを描画に使ってもらうため、カメラオブジェクトのIDをgpusecameraで指定します。

ちょっと話が難しそうに思えるかもしれませんが、スクリプト自体はシンプルです。

下記のサンプルでは、キーボードの矢印 ↑↓ で画角が変化するような処理内容です。

// カメラオブジェクトの生成
gpnull id_camera
setpos id_camera, 0, 0, 100

// カメラの画角とニア、ファー
camera_fov = 45.0//< 単位はDegree
camera_near = 0.5 : camera_far = 768.0

// 画面のアス比、ここではhsp3dish.iniで(640, 480)に設定しているので、その値で計算する
aspect_ratio = double(640) / double(480)

/**
 * 画面のアス比は、汎用的には、
 * gsel <描画対象のバッファID>
 * aspect_ratio = double(ginfo_winx) / double(ginfo_winy)
 * で求められる
 */ 

repeat
    // カメラの調整
    stick key, 0x0f
    if (key & 2) : camera_fov = limitf(camera_fov + 1, 1, 179)
    if (key & 8) : camera_fov = limitf(camera_fov - 1, 1, 179)

    gpcamera id_camera, camera_fov, aspect_ratio, camera_near, camera_far
    gpusecamera id_camera

    redraw 0//< 描画始め
        gpdraw//< hgimg4に登録されたオブジェクトを描画
    redraw 1//< 描画終わり
    await 16//< 描画後のウェイト
loop

tmp_20171217_175407.gif

うーん、画角を変えただけだと結果を見てもよく分からないし、あまり良いサンプルではなかったかも…。

モデルについてもう少し

ボックスがでた、ライティングも変えられた、カメラも設定できた、さぁ次は!?
という感じですが、一通りのことが出来ると改めて基本的なところに思考回路が戻ってきたりするものです。

ここではモデルについてもう少し掘り下げた話題について扱います。

マテリアル

3Dグラフィクス門外漢の場合聞きなれない単語だと思いますが、3Dグラフィクスに一度触れ始めると最初から最後まで付き纏う単語になります。

マテリアルとは日本語訳すると材料・材質となりますが、3Dにおいては質感を指す言葉になり、転じてそのモデルを描画するのに必要なパラメータを意味します。

より具体的には「金属である」とか「半透明である」といったややマクロな情報から、「ライトの影響をどの程度受けるのか」とか「どのテクスチャを使う」といった見た目の情報などを持ちます。
勿論、プログラムで扱うものなのでこれらは全て数値化されたものですが。

そして通常、3Dモデルには複数個のマテリアルが存在します。
簡単なモデルであればマテリアルは1つかもしれませんが、単位が大きくなればなるほどマテリアルも増えるのが普通です。

今までgpboxなどでボックスモデルを生成して表示していましたが、実はgpboxにはパラメータとしてマテリアルを指定することができます。
ここでは例として、テクスチャを貼るマテリアルを作って設定してみましょう。
テクスチャは例の如くサンプルにあるものを使いました。

// テクスチャを貼るマテリアルを生成する
gptexmat id_tex_mat, "res/btex.png"

// ボックス:先ほど作ったマテリアルを指定
gpbox id_box, 20, 0xffffffff, id_tex_mat

tmp_20171217_182146.gif

マテリアルを生成する命令を使うとマテリアルIDが貰えるので、モデルを生成する際にそのマテリアルIDを指定すれば、作ったマテリアルが設定されたモデルが作れる、という流れです。

gptexmatのヘルプは次の通りです。

gptexmat <マテリアルIDを保存する変数>, <テクスチャのファイルパス>, <その他のオプション>

設定可能なその他のオプション:
GPOBJ_MATOPT_NOLIGHT    ライティングを行なわない
GPOBJ_MATOPT_NOMIPMAP   MIPMAPを生成しない
GPOBJ_MATOPT_NOCULL     カリングを無効にする
GPOBJ_MATOPT_NOZTEST    Zテストを無効にする
GPOBJ_MATOPT_NOZWRITE   Zバッファ書き込みを無効にする
GPOBJ_MATOPT_BLENDADD   プレンドモードを加算に設定する

先ほど説明したようにライティングをしない設定などもマテリアルに含まれます。
そして、マテリアルはカリング無効やZテスト無効など、プログラム的な設定も含まれることに注意してください。
この辺の設定は3Dの難しい話が絡んでくるので、この基本編では取り扱いません。

また、その他に存在するマテリアルの種類として、色を持ったマテリアルを作るgpcolormatと、自前のシェーダを使うマテリアルを作るgpusermat命令がありますが、これについても割愛します。

アニメーション

ここで言うアニメーションはモデルのアニメーションで、正確に言うと現時点でhgimg4が対応しているスケルタルアニメーションについてです。

まずモデルを作ってとりあえず読み込むまで

ここは個々人の作業なので省略しようかと思いましたが、この辺サンプルでも作っている人が居なさすぎたので、参考になるか分かりませんがちょこちょこ書いておきます。

筆者はBlenderで適当にサンプルモデル作りました、モデルとスケルトンとスキニングは次の通り。

そして、アニメーションは次のような感じ、分かればいいので適当に捩じれてもらいました。

tmp_20171217_195946.gif

※慣れているとここまで10分もかからない程度だと思います。

あとはこれをFBXとして出力し、hgimg4専用形式のGPBにコンバートします。
ボックス3つのモデルなので、ファイル名はthreeとしています。

HSPに付属しているコンバータ(GUI)を使ってもいいと思いますが、ここでは普通に何かアプリ作る際に毎回GUI通してられないと思うので、CUIで変換できるようにバッチファイルで紹介しようと思います。

FBX→GPBコンバート
rem ↓のgamplay-econderのパスは適当なので、個人個人の環境に合わせて直してください
rem gameplay-encoder.exeはHSPをインストールしたディレクトリ直下に置いてあります
set CONV=%~dp0gameplay-encoder.exe
set OPT= -g:auto -m 

set FILENAME=three

"%CONV%" %OPT% "%FILENAME%.fbx" "%FILENAME%.gpb"

これでコンバートかけるとGPBファイルthree.gpbとマテリアルファイルthree.materialというファイルがでてきます。

それらをそのままスクリプトから見てdataというディレクトリにコピーして、スクリプトから読み込み、スケール調整とかして見えるようにすると、

// hgimg4ランタイムを使う
#include "hgimg4.as"

// hgimg4ランタイムのリセット
gpreset

// モデルを読み込む
gpload id_model, "data/three"
setpos id_model, 0, -15, 0
setang id_model, deg2rad(20), deg2rad(45), 0
setscale id_model, 0.1, 0.1, 0.1

// 画面クリアの設定:毎回黒でクリア
setcls CLSMODE_SOLID, 0x00000000

repeat

    redraw 0//< 描画始め
        gpdraw//< hgimg4に登録されたオブジェクトを描画
    redraw 1//< 描画終わり
    await 16//< 描画後のウェイト
loop

こういう感じで描画されます。

amodel_loaded_simple.png

GPBファイルコンバートあるある話なんですが、マテリアルのパラメータ設定変換がかなり適当なんで、手動で直してあげます

今回で言うとライティングがされてないところが問題です。
マテリアルファイルを見ると、ディレクショナルライトを使う設定になってないので、defines=xxxとなっているところにDIRECTIONAL_LIGHT_COUNT 1を付け足します。

マテリアルファイルの書き換え
material Base : colored
{
    // 色々設定が書いてある

    technique
    {
        pass 
        {
            defines = SKINNING;SKINNING_JOINT_COUNT 3;SPECULAR
            // ↑これに定義が足りてないので、↓のようにする
            defines = SKINNING;SKINNING_JOINT_COUNT 3;SPECULAR;DIRECTIONAL_LIGHT_COUNT 1
        }
    }
}

マテリアルファイルを書き換えて読み込み直すと、次のように正しくライティングされた結果になります。

amodel_loaded_light.png

補足:マテリアルファイル

マテリアルファイルを眺めているとdefinesSKINNINGが書かれています。
これによって、きちんとスケルタルアニメーション可能なモデルとして出力されている、というのがこれで分かります。
SKINNING_JOINT_COUNTというのがスケルタルアニメーションで使われるボーン(マトリクスパレット)の数です。(ちゃんとシェーダコード読んでないけど、たぶんそう)

あと、今回はモデルファイルにテクスチャを貼ってないので問題なかったですが、テクスチャは元のFBXファイルからの相対パスとして記述される一方、GPBに変換した後はディレクトリの部分が要らないので、ファイル名だけにしてあげたりする必要があります。

更にもう一つありがちな罠としては、マテリアル名に日本語を使うと正しく変換されない可能性があります
これは、GPBファイルはhgimg4内部で使っているGamePlay3Dというライブラリの独自ファイル形式で、コンバータも独自で作られているものなのですが、マルチバイト文字を意識して作られてないためです。
ASCIIで読めないバイトに関しては_に変換されるので、バイト数が一致する名前のマテリアルは、同じマテリアルとして認識されてしまいます。
この辺はコンバート処理のところなので、今後のhgimg4の改良に期待ですね。

余談ですが、ランタイム読み込み時はこの変換は行われないので、コンバータに細工して日本語マテリアル名をそのまま出力するようにしてみたところ、特に問題なくランタイムで扱うことが出来ました。
OpenHSPをとってくれば、コンバータをFBX SDKインストールする必要などありますが、ビルド自体はそこまで難しくないので、時間に余裕がある人にはそちらでもいいのかもしれない…。

スケルタルアニメーションさせる

さて、モデル読み込みまで出来たらアニメーションさせるまではかなり簡単です。

ただ単純にアニメーション全体をループ再生させる場合、gpact命令を実行するだけです。

// アニメーション全体をループ再生
gpact id_model

tmp_20171217_204004.gif

ウネウネしてますね、ちゃんと動いてます(苦笑)

アニメーションクリップ

実際のアプリでは複数のアニメーションを使い分けることになるのが普通だと思いますが、そういう時のためにhgimg4ではアニメーションクリップという機能があります。
アニメーションクリップは、アニメーションを区間に分割できる機能で、分割した区間毎に再生や一時停止といった操作を行うことができます。

各命令の説明の前に、先ずはスクリプトと結果を見てもらった方が早いので、一度サンプルを先にだします。

サンプルでは、

  • モデルに含まれるアニメーションのうち、前半(伸びるだけ)と後半(縮むだけ)を別のアニメーションクリップとして定義する
  • ZまたはXボタンを押すと対応するアニメーションクリップを再生する
  • アニメーションクリップの再生はループしない(1ループだけ再生する)

といった処理をさせています。

// モデルを読み込む
gpload id_model, "data/three"
setpos id_model, 0, -15, 0
setang id_model, deg2rad(20), deg2rad(45), 0
setscale id_model, 0.1, 0.1, 0.1

gpaddanim id_model, "grow", 0, 1250
gpaddanim id_model, "shrink", 1250, 2500

repeat
    // アニメーションが1ループしていたら止める
    repeat 2
        gpgetanim playing, id_model, cnt+1, GPANIM_OPT_PLAYING
        if (playing ==0) : continue

        gpgetanim duration, id_model, cnt+1, GPANIM_OPT_DURATION
        gpgetanim elapsed, id_model, cnt+1, GPANIM_OPT_ELAPSED
        if (elapsed >= duration) {
            anim_name = "grow"
            if (cnt) : anim_name = "shrink"
            gpact id_model, anim_name, GPACT_STOP//< これで0フレームに戻るだけ
        }
    loop

    // アニメ再生
    getkey key_z, 'Z' : if (key_z^pkey_z&pkey_z) : gpact id_model, "grow", GPACT_PLAY
    getkey key_x, 'X' : if (key_x^pkey_x&pkey_x) : gpact id_model, "shrink", GPACT_PLAY
    pkey_z = key_z : pkey_x = key_x

    redraw 0//< 描画始め
        gpdraw//< hgimg4に登録されたオブジェクトを描画
    redraw 1//< 描画終わり
    await 16//< 描画後のウェイト
loop

tmp_20171217_210159.gif

さて、命令についての解説になります。
アニメーションクリップを追加するにはgpaddanim命令を使います。

gpaddanim <オブジェクトのID>, <追加するアニメーションクリップの名前>, <区間始まりの位置(ミリ秒)>, <区間終わり(ミリ秒>

となっています。
追加するアニメーションクリップに名前が付けられる訳ですね。

次、追加したアニメーションクリップを再生する場合、gpactで区間名を渡します。

gpact <オブジェクトのID>, <アニメーションクリップ名>, GPACT_PLAY

最後の引数でGPACT_PLAYとしているため再生の意味になりますが、一時停止の場合はGPACT_PAUSEを、停止する場合はGPACT_STOPを渡すとそのように操作できます。

ただ、gpactで再生したものは無条件でループ再生になってしまうため、1ループしたものは自前でアニメーションを停止させる必要があります。

設定したアニメーションクリップの情報を自分で覚えておいてもいいですが、hgimg4側にアニメーションクリップの情報を取得する命令があるので、今回はサンプルとしてそれらを使っています。

gpgetanim <結果が代入される変数>, <オブジェクトのID>, <アニメーションクリップのインデクス>, <取得したい内容>

GPANIM_OPT_START_FRAME     0           開始フレーム(ミリ秒単位)
GPANIM_OPT_END_FRAME       1           終了フレーム(ミリ秒単位)
GPANIM_OPT_DURATION        2           再生の長さ(ミリ秒単位)
GPANIM_OPT_ELAPSED         3           経過時間(ミリ秒単位)
GPANIM_OPT_BLEND           4           ブレンド係数(%単位)
GPANIM_OPT_PLAYING         5           再生中フラグ(0=停止/1=再生)
GPANIM_OPT_SPEED           6           再生スピード(%単位)
GPANIM_OPT_NAME            16          アニメーションクリップ名

…あれ? 急にアニメーションクリップのインデックスというのがでてきた…。

ここ、地味にハマるポイントだと思うのですが、実はアニメーションクリップは作った順に 1からインデックスが振られていきます
1から始まるのがポイントです、0はアニメーション全体を表す区間として既に定義されているためです。

ということで、アニメーションクリップは名前で指定する所とインデックスで指定するところと2つあるので、使用には注意が必要です。5

なお、サンプルでは使っていませんが、アニメーションクリップ毎に設定をするgpsetanimという命令があり、これを使うと再生速度やブレンド率を設定することができます。

gpsetanim <オブジェクトのID>, <アニメーションクリップのインデクス>, <設定したい内容>, <設定する値>

GPANIM_OPT_DURATION        2           再生の長さ(ミリ秒単位)
GPANIM_OPT_BLEND           4           ブレンド係数(%単位)
GPANIM_OPT_SPEED           6           再生スピード(%単位)

こちらもアニメーションクリップのインデックスを使います、注意してください。
以上、hgimg4でのスケルタルアニメーションについてでした。

その他のアニメーション

前節まではスケルタルアニメーションについての説明でしたが、hgimg4ではこれ以外のアニメーション(テクスチャのUVアニメやブレンドシェイプ(モーフ)など)はサポートされていません
表情のアニメーションによく使われるので、ブレンドシェイプが使えないのは結構痛手ですね…。。。

また、テクスチャのUVアニメや、その他マテリアルのパラメータのアニメーションは、シェーダが自前で書けるため原理的には出来ないこともないのですが、スクリプト側から自由にシェーダパラメータをセットできる口が少ないので、かなり難しいです。

これらについてはシェーダが絡んでくるので、応用編で解説したいと思います。

半透明モデル

3Dグラフィクスなので、hgimg4では勿論モデルの半透明化も対応しています。
ただし、ドキュメントが追い付いていないことと、半透明は現代の3Dグラフィクスにおいても扱いが難しいため、使うには多少なり覚悟が必要です。

ここでは、マテリアルファイルの修正やZ値の話など若干応用的な話題が入ってきますが、半透明は是非とも使いたい人は多いでしょうし、基本編で半透明について解説したいと思います。

モデルファイルを使わないモデルの半透明化

モデルファイルを使わないモデルとは具体的にはgpboxgpplateで生成したモデルを指します。
gploadで作ったものはモデルファイルからの読み込みで生成したモデルなので、次の節で取り扱います。

モデルファイルを使っていないモデルの場合、半透明化は比較的容易で、setalphaでアルファ値を0255の間で指定すればOKです。

ただし、半透明を使った際の問題も把握するため、サンプルでは3次元的に5 * 5 * 5の数でボックスモデルを作り、それを全て半透明で配置させてみます。

// hgimg4ランタイムを使う
#include "hgimg4.as"

// hgimg4ランタイムのリセット
gpreset

// ボックス
w = 5 : h = 5 : d = 5
dim id_box, w, h, d
repeat w : x=cnt : repeat h : y=cnt : repeat d : z=cnt
    gpbox tid, 5, 0xffffffff
    setpos tid, -20.0+40.0*x/5.0, -20.0+40.0*y/5.0, -20.0+40.0*z/5.0
    setang tid, deg2rad(-30), deg2rad(-30), 0
    setalpha tid, 128
    id_box(x, y, z) = tid
loop : loop : loop

// 画面クリアの設定:毎回黒でクリア
setcls CLSMODE_SOLID, 0x00000000

// カメラの位置と向き
camera_posx = 0.0 : camera_posy = 0.0 : camera_posz = 100.0

repeat
    // カメラの調整
    t_len = sqrt(camera_posx*camera_posx + camera_posz*camera_posz + 0.00001)
    tx = -camera_posx / t_len : tz = -camera_posz / t_len
    rx = tz : rz = -tx
    stick key, 0x0f
    if (key & 1) : camera_posx += rx : camera_posz += rz
    if (key & 2) : camera_posx += tx : camera_posz += tz
    if (key & 4) : camera_posx -= rx : camera_posz -= rz
    if (key & 8) : camera_posx -= tx : camera_posz -= tz
    setpos GPOBJ_CAMERA, camera_posx, camera_posy, camera_posz
    gplookat GPOBJ_CAMERA, 0, 0, 0//< 常に中心を見る

    redraw 0//< 描画始め
        gpdraw//< hgimg4に登録されたオブジェクトを描画
    redraw 1//< 描画終わり
    await 16//< 描画後のウェイト
loop

tmp_20171220_214436.gif

最初の視点では特に問題なさそうに見えますが、視点を回転していくとアルファ値を設定しているにも関わらず、後ろのオブジェクトが透過しない場合があることが分かります。

実は、hgimg4は半透明描画時も、特に設定を変えなければマテリアルはそのままなので、半透明であるにも関わらずZ値が書き込まれる設定になっています。
Z値が書き込まれた場合、それより奥にあるオブジェクトは描画されなくなるため、ブレンドが正しくならないのです。
ただ、これ自体は視点によるZソートを実装している場合、ある程度のケースまでは対応可能になるため、そこまで大きく間違っている方法である訳でもありません。

しかし、hgimg4はZソートはせず、オブジェクトを追加した順に描画するため、結局この問題が起こってしまっています。

では、Z値を書き込まないようにすればよいのかというとそういう訳でもないです。
結局Zソートしなければ奥にあるオブジェクトが後に描画(ブレンド)されるため、やっぱりZソートが必要ということが分かります。

先ほどのコードで少し実験してみましょう。
ボックス生成の箇所を下記のように書き換え、Z値を書かないように設定してみます。

// ボックス
w = 5 : h = 5 : d = 5
dim id_box, w, h, d
repeat w : x=cnt : repeat h : y=cnt : repeat d : z=cnt
    gpcolormat tid_mat, 0xff000000|((127*x/5+128)<<16)|((127*y/5+128)<<8)|((127*x/5+128)<<0), GPOBJ_MATOPT_NOLIGHT
    gpbox tid, 5, 0xffffffff, tid_mat
    setpos tid, -20.0+40.0*x/5.0, -20.0+40.0*y/5.0, -20.0+40.0*z/5.0
    setang tid, deg2rad(-30), deg2rad(-30), 0
    setalpha tid, 128
    gpmatstate tid, "depthWrite", "false"//< Z書き込みを無効化
    id_box(x, y, z) = tid
loop : loop : loop

tmp_20171220_220021.gif

最初の視点で、左奥側のボックスは色がついており、右手前側は白色です。
この状態で視点を回転させていくと、奥側から見た際一見正しくブレンドされているように見えますが、実はその時の視点一番手前にある色付きのボックスが、視点奥の白色のボックスにブレンドで負けてしまっています。
(これは少し状況を正しく理解していないとなかなか納得できないと思うので、手元でサンプルコードを走らせてみてください)

結局、hgimg4で半透明モデルを扱おうとした場合、ある程度は割り切るか、または生成順で正しく描画できるように制御するかの2択しかありません。

モデルファイルから読み込んだモデルの半透明化

モデルファイルから読み込んだモデルの場合も、基本は同じでsetalphaをすれば半透明化が可能です。
ただし、モデルファイルの場合はマテリアルの設定で半透明化可能になってない場合があり、この場合はいくらsetalphaでアルファ値を設定しても意味がありません

ポイントは2つあります。
今回はサンプルに入っているアヒルのモデルがちょうどsetalphaできないモデルだったので、duckのマテリアルファイルを例にして説明します。

setalphaを有効にする

まずは、setalphaで設定した値を使ってアルファ値を変更できるようにすることです。
実はsetalphaで設定しているのはシェーダのパラメータなので、実際にシェーダでそのアルファ値を乗算して最終的なアルファ値を出力してくれないと意味がありません。
なので、シェーダの設定でsetalphaの値を乗算するオプションをつけます。

マテリアルのdefinesMODULATE_ALPHAを追加すればOKです。

duck.materialのdefinesの修正
// …
pass 0
{
    // shaders
    vertexShader = res/shaders/textured.vert
    fragmentShader = res/shaders/textured.frag
    defines = SPECULAR;DIRECTIONAL_LIGHT_COUNT 1
    // ↑を↓に変更
    defines = SPECULAR;DIRECTIONAL_LIGHT_COUNT 1;MODULATE_ALPHA
// …
blendを変更する

次に、そもそもアルファ値を使ってカラーをブレンドするかを設定する必要があります。
GPUを使った描画では速度最適化のため、必要ない場合はブレンドを行わない方が高速なため、ブレンドを無効にする描画オプションがあります。
GPBファイルに変換した際、マテリアルの設定がどうなっているかは分かりませんが、ブレンドが無効化されている場合、それを有効化するように書き直す必要があります。

マテリアルのrenderStateを次のように書き換えます。

duck.materialのrenderStateの修正
// …
// render state
renderState
{
    // ↓ここから
    blend = true
    blendSrc = SRC_ALPHA
    blendDst = ONE_MINUS_SRC_ALPHA
    // ↑ここまでを追加

    cullFace = true
    depthTest = true
// …

まずblendでブレンドを有効にし、blendSrcで元のカラーにアルファ値を掛けるように、blendDstで描画先の乗算パラメータも変更します。

あとは通常通り描画するだけ
// ボックス
gpbox id_box, 10, 0xffffffff
setpos id_box, -20.0, 0.0, 0.0
setang id_box, deg2rad(-30), deg2rad(-30), 0
setalpha id_box, 128

// プレート
gpplate id_plate, 10.0, 20.0, 0xffff0000
setang id_plate, deg2rad(-30), deg2rad(-30), 0
setalpha id_plate, 128

// 3Dモデル
gpload id_model, "res/duck"
setpos id_model, 20.0, 0.0, 0.0
setang id_model, deg2rad(-30), deg2rad(-30), 0
setscale id_model, 10.0, 10.0, 10.0
setalpha id_model, 128

正しくマテリアルファイルを書き換える前、後とでアヒルのモデルの濃さが違うのが分かるでしょうか。

スクリーン

ほぼ終わりに近づいていますが、まだこれを説明していませんでした。

通常のHSPではbuffer命令を使うことで仮想スクリーンを作れます。
hgimg4でもbuffer命令を使って仮想スクリーン、というか描画バッファを追加で作ることができますが、少し作法が違います。

bufferでオフスクリーンを作る

hgimg4ではbufferに指定する引数が増えています。

buffer <描画バッファID>, <Xサイズ>, <Yサイズ>, <オプション>

screen_offscreen    描画可能なバッファとする
screen_usergcopy    この描画バッファをgcopyやcelputでコピーする際、gpusershaderで指定したシェーダが使われる

なんと、引数が増えており、かつそこにscreen_offscreenを指定しないと、従来のように描画対象となるバッファになりません。
裏を返せばこれを指定するだけで描画対象にできるようになるので、既存のスクリプトとの違いは多くはありません。
ただし、これらの描画バッファに対する描画も、hgimg4の作法に則りredraw 0redraw 1で括る必要があり、デフォルトではredraw 0で自動的にクリアされます。

// hgimg4ランタイムを使う
#include "hgimg4.as"

// hgimg4ランタイムのリセット
gpreset

// 別の描画バッファを作る
buffer 2, 320, 240, screen_offscreen

// ボックス
gpbox id_box, 20, 0xffffffff

// 画面クリアの設定:毎回黒でクリア
setcls CLSMODE_SOLID, 0x00000000

repeat
    // ボックスを回転
    setang id_box, deg2rad(cnt)*0.67, deg2rad(cnt)*0.97, 0

    // サブの描画バッファへ
    gsel 2
    redraw 0
        color 192, 192, 192 : boxf
        gpdraw
    redraw 1

    // メインの描画バッファへ
    gsel 0
    redraw 0
        gmode 0 : pos 320.0+cos(deg2rad(cnt)*2.0)*160.0, 240 : celput 2, 0, 1, 1, deg2rad(cnt)
    redraw 1
    await 16//< 描画後のウェイト
loop

tmp_20171217_190941.gif

描画バッファの特定の場所に描く

今までのコードでは、3Dの描画は自動的に対象の描画バッファ全体に描かれていました。
しかし、描画バッファの一部をgpviewportで指定し、そこに描画することも可能です。

repeat
    redraw 0
        // 左下
        setpos GPOBJ_CAMERA, 0, 0, 100
        gplookat GPOBJ_CAMERA, 0, 0, 0
        gpviewport 0, 0, 320, 240 : gpdraw

        // 右下
        setpos GPOBJ_CAMERA, 100, 0, 0
        gplookat GPOBJ_CAMERA, 0, 0, 0
        gpviewport 320, 0, 320, 240 : gpdraw

        // 左上
        setpos GPOBJ_CAMERA, 0, 0, -100
        gplookat GPOBJ_CAMERA, 0, 0, 0
        gpviewport 0, 240, 320, 240 : gpdraw

        // 右上
        setpos GPOBJ_CAMERA, -100, 0, 0
        gplookat GPOBJ_CAMERA, 0, 0, 0
        gpviewport 320, 240, 320, 240 : gpdraw
    redraw 1
    await 16//< 描画後のウェイト

redraw区間中に変更できるので、使える用途は限られていますが、有効に使える場合もあるかと思います。
…ただし、指定する矩形の座標系は、OpenGLのスクリーン座標なので、左下が原点になります、注意してください。

ashape_viewport.png

テクスチャパターンを読み込んで描画する

hgimg4ではcelloadceldivcelputはほぼ完全互換で動作します。

なので、テクスチャを読みこみパターンで分割して、それらを描画する場合はこれらの命令を使ってください。

おわりに

以上、「(再)入門」と言っていますが、実は自分でhgimg4検証した時の記録と今後の為の備忘録みたいなのでした。

基本編と書いているのでお察しの方いるかもしれませんが、本当はシェーダをばりばり使ったポストエフェクトのコード例とか、モデルのシェーダの書き換え方とか、モデルのボーン位置・回転を使う時にどうするかとかも少し検証したので、その辺をトピックにした応用編を私としては書きたかった…応用編って書くの楽しいんですよね。
でも、もう書き疲れたのでこの辺で終わりとしたいと思います。

ちなみにここまで書いておいてなんですが、hgimt4を色々と検証してみて結局、かゆいトコロに手が届かないランタイム、という印象だったりします。
その辺の話もいつかの応用編で…デキルカナー。6


  1. 実は正確に言うとOpenGLだから描画できるウィンドウが 1 つになっている訳ではないです、OpenGL自体はそんなに酷いAPIじゃないので、ウィンドウがいくつだろうと関係なく使えます(コンテキストが違ったりしますけど)。どちらかというと、1 画面の制約根拠はHSP3Dishに依るところが強いです。 

  2. 探してみたけど、これドキュメントには書いてないのかな…。 

  3. 探してみたけど、これもドキュメントには書いてないのかな…。 

  4. OpenGLでよく使われるビュー行列、射影行列がZ-前提で作られているから。 

  5. 凄いインタフェースだなって改めて思う。 

  6. 応用編はやる気がでたら書こうと思います。 

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.