丸影
目次
はじめに
ゲームにおいても、足元の影の存在は結構重要です。 キャラクターや物体が空中に浮かんでいるのか、地面に接地しているのかを判断する材料として影は利用されます。 キャラクターの足と影がくっついていれば接地していて、離れていれば浮いているように見えます。
影が存在していなければ、空中に浮かんでいる物体の位置が把握しにくくなります。また逆に、地面にいるにもかかわらず、空中に浮かんでいるものとの区別がつかなかったりして、遊びにくいゲームになってしまいます。 位置把握のための影は、形がキャラクターのシルエットの形をしていなくてもいいですし、光源の位置を考慮していなくても問題ありません。とにかく存在していることが重要なのです。
ということで、影を作ってみます。 ここでの実装は、「丸影」と呼ばれるキャラクターの足元に黒い丸を置くだけの簡単な実装です。 幸いresフォルダーには、shadow.pngという丸影の画像が用意されていますので、これを使っていきます。
板モデル
丸影は、板モデルに黒丸の画像を張り付けて作成します。
hgimg4では、gpplate
命令で簡単に作成することができるので、まずは作ってみました。
#include "hgimg4.as"
; 環境構築
gpreset
setcls CLSMODE_SOLID, $4f7fff
setcolor GPOBJ_LIGHT, 1,1,1
setdir GPOBJ_LIGHT, 0.5,0.5,0.5
; 板モデル
gptexmat id_shadowtx, "res/shadow.png"
gpplate id_shadow, 1, 1, -1, id_shadowtx
; カメラ位置を設定
setpos GPOBJ_CAMERA, 0,1,3
gplookat GPOBJ_CAMERA, 0,0,0
cntFrame = 0
*main
stick key, $180F
if key&128 : end
; 回転
; 表:見える方の面。+Z側。
; 裏:見えない方の面。-Z側。
;r = double( cntFrame ) / 60 * 2.0 * M_PI
;setang id_shadow, 0, r, 0
redraw 0 ; 描画開始
gpdraw
redraw 1 ; 描画終了
cntFrame++
await 1000/60 ; 待ち時間
goto *main
gpplate
は、XY平面に正方形の板状の3Dモデルを作成してくれます。
テクスチャが透明色を使ってれば、透明に描画してくれます。
裏側がどうなっているか確認してみたいので、*main
ループに次のスクリプトを追加します。
r = double( cntFrame ) / 60 * 2.0 * M_PI
setang id_shadow, 0, r, 0
板モデルがY軸を中心として回転するようになりました。 どうやら裏側(-Z側面)からは、透明に見えるようです。
ついでに生成されるサイズをマイナスにした場合の挙動も確認してみます。
gpplate id_shadow, -1, 1, -1, id_shadowtx
起動直後が見えなくなりましたね。裏側(-Z側面)からだと見えるようになりました。面の裏表を反転できるようです。
ちなみに、gpfloor
も同じ動作をするようです。
半透明
丸影はただの黒丸ではなく、境界線は半透明のグラデーションを使用します。
hgimg4では、gpplate
のテクスチャに透明色を使っていれば、ちゃんと透明に表示してくれます。
先ほどのサンプルではわかりにくいので、適当な床を配置してみました。
#include "hgimg4.as"
; 環境構築
gpreset
setcls CLSMODE_SOLID, $4f7fff
setcolor GPOBJ_LIGHT, 1,1,1
setdir GPOBJ_LIGHT, 0.5,0.5,0.5
; 板モデル
gptexmat id_shadowtx, "res/shadow.png";, GPOBJ_MATOPT_NOZWRITE
;gptexmat id_shadowtx, "res/shadow.png", GPOBJ_MATOPT_NODISCARD ; ピクセル破棄
gpplate id_shadow, 1, 1, -1, id_shadowtx
;setobjmode id_shadow, OBJ_LATE
; クローン
;gpclone id, id_shadow
;setpos id, 0.5
;setang id, -M_PI/2
;setobjmode id, OBJ_LATE ; すでにid_shadowにOBJ_LATEが指定されている場合は不要。
; 床ノード
gptexmat id_texmat, "res/qbox.png"
gpfloor id_floor, 5, 5, -1, id_texmat
setpos id_floor, 0, -0.5, 0
; カメラ位置を設定
setpos GPOBJ_CAMERA, 0,1,3
gplookat GPOBJ_CAMERA, 0,0,0
*main
stick key, $180F
if key&128 : end
redraw 0 ; 描画開始
gpdraw
redraw 1 ; 描画終了
await 1000/60 ; 待ち時間
goto *main
半透明の部分だけが、透明になってくれませんね。 完全に透明な部分は、ピクセル破棄が行われているのか向こう側が見えています。
この問題は、gpplate
で板を作る前にgpfloor
で床を作っておけば簡単に修正されます。
しかしこれでは不便なので、作成順に影響されない解決策もあります。
gpplate
で丸影モデルを作成後に、OBJ_LATE
(常に後から描かれる(半透明オブジェクト用))モードを追加します。
setobjmode id_shadow, OBJ_LATE
半透明オブジェクトは、透けて見える向こう側のオブジェクトを描画した後に描画しないと透明にならない仕組みとなっています。 「常に後から描かれる」モードに設定することで、常に描画順を適切な状態にできます。
では、同じく半透明の物体がもう1つあったらどうなるのか試してみます。 2つ以上の「常に後から描かれる」物体があると、どちらが優先になるんでしょうね。
gpclone id, id_shadow
setpos id, 0.5
setang id, -M_PI/2
2つの半透明が重なった部分は、Zバッファ(深度)の比較と描画順の影響で一部が透けていません。 この問題は、テクスチャマテリアルに「Zバッファ書き込みを無効にする」オプションを追加することで回避できます。
gptexmat id_shadowtx, "res/shadow.png", GPOBJ_MATOPT_NOZWRITE
丸影の半透明描画は、この2つのポイントを守れば正しく描画できそうです。
- テクスチャマテリアルに
GPOBJ_MATOPT_NOZWRITE
オプションを追加。 setobjmode
でOBJ_LATE
モードを設定。
最後に、影は地面と平行なので90度回転しておけば完成です。
setang id_shadow, -M_PI/2, 0, 0
さて、影を描く準備ができました。
これをキャラクターの足元にsetpos
で移動すれば、影として使用できますね。
「回転するくらいなら最初からgpfloor
で作っておけば良かったのでは?」ですか。
この後の作業で必要になるので、まだ気にしないでください。
レイ
キャラクターの足元は、常に同じ高さの平面とは限りません。
高低差があったり、坂があるかもしれません。ここからはキャラクターの影を投影する地面の高さと傾きへの対応方法を調べてみます。
(同じ高さの平面しか使わないなら、ここから先を読む必要はありません。影もgpfloor
で作って大丈夫です。)
hgimg4には、任意のオブジェクトノードからレイ(Ray)を飛ばし、レイが物体に衝突した位置や法線(面に垂直なベクトルの事)、オブジェクトIDなどを取得する機能が備わっています。 この機能を使えば必要な情報を簡単に取得できます。
言葉が難しいですね。レイ(Ray)は、レーザー光線のようなもので、ある点から無限に伸びる直線です。 流石に無限の長さは扱えないので、hgimg4では長さを指定した線分で使用しています。 もう少しイメージしやすい言葉に置き換えて書いてみます。
hgimg4には、レーザー銃からレーザー光線を飛ばし、レーザー光線が物体に衝突した場所の位置や表面の傾き、物体のIDなどを取得する機能があります。
少しは、イメージしやすくなった気がします。 このイメージがあると、起点は何もない任意の座標ではなく、必ずノードオブジェクト(レーザー銃)が必要であるというのも理解しやすい気がします。 またレーザー光線は光なので、最初の物体に衝突すると貫通しないので、そこから先には進みません。 一番手前の物体のみを検出し、影に位置する物体を検出したり複数の物体を同時に検出することはありません。
このレイをキャラクターの足元から地面方向に飛ばして、地面の高さや傾きを調べようというのがここで解決方法です。
gppraytest
レイを飛ばす命令は、gppraytest
です。
ほとんどマニュアルに書いてある内容ですが、少し整理してみました。
gppraytest var, objid, distance var : 検出したオブジェクトID値が代入される変数名 objid(0) : オブジェクトID distance(100) : ベクトルの長さ
- objid
-
レイの起点(レーザー銃)となるオブジェクト。
ノードであれば、カメラやnullノード等何でもOK。 物理設定やコリジョングループも何でもOK。 レイを飛ばす方向(レーザー銃が向いている方向)は、「-Z方向」です。 - distance
-
レイの射程距離(レーザー光線の光が届く距離)。
レーザー光線は、物体を貫通しません。レイの起点(レーザー銃)からレイ(レーザー光線)を飛ばして、最初に当たった1つの物体だけを検出します。 - var
-
> 0 : 線分に衝突したオブジェクトのID。
== 0 : 何も衝突するオブジェクトがない。
< 0 : 何らかのエラーが発生。
衝突があった場合
objidで指定されたオブジェクトのノードワーク値(work, work2)に詳細な情報が格納されます。
ノードワーク値 | 設定される内容 |
---|---|
work | 座標 |
work2 | 法線ベクトル |
衝突が検出される物体
物理設定されたオブジェクトノードすべて。
コリジョングループの設定は無視される。
注意点(HSP3.7β3現在)
gppraytest
は、gpfloor
、gpplate
では衝突を検出しない事があります。
また厚さが薄いgpbox
でも同様に検出しないことがあります。
十分な厚さを持った物体であれば、衝突不検出を回避できるようです。
レイの標準サンプル
gppraytest
の実装例については、「\sample\hgimg4」フォルダーのサンプル「physics_2.hsp」を御覧ください。
カメラは必ず -Z方向を向いているので、「カメラの向き = レイを飛ばす方向」という関係になっています。 カメラを物体に向けるとレイが物体に衝突して、衝突した座標に白い煙のようなものが表示されます。
しかし、地面にカメラを向けても白い煙のようなものが出ません。
床もgppbind id_floor, 0
としてあり、物理設定が行われているので検出するはずです。
ということで、*main
ループ内の以下の行をコメントにしてみてください。
if hitres=id_floor : hitres=0
レイが床に衝突した場合は、衝突後の処理をスキップしていただけでした。 箱にだけ反応するようにしたかったんだと思います。
この他にも検出後の処理を分岐する方法があります。
たとえば、getobjcoli
命令を使ってオブジェクト グループ毎に挙動を変更させる実装が便利です。
事前にオブジェクト グループを設定しておく必要はありますが、gpclone
などで不特定多数のオブジェクトを作った場合にも対応できます。
丸影サンプル
大体わかってきたので、細かい話は後にしてサンプルを作ってみました。
#include "hgimg4.as"
; 環境構築
gpreset
setcls CLSMODE_SOLID, $4f7fff
setcolor GPOBJ_LIGHT, 1,1,1
setdir GPOBJ_LIGHT, 0.5,0.5,0.5
; 床ノード(物理)
gptexmat id_texmat, "res/qbox.png"
;gpfloor id_floor, 30, 30, -1, id_texmat ; 板モデル
gpbox id_floor, 1, -1, id_texmat ; 厚みがある床
setscale id_floor, 30, 0.03, 30
gppbind id_floor, 0
; 箱ノード(物理)
; 段差の例
gpbox id_box, 3, -1, id_texmat
setpos id_box, 3, -0.8
gppbind id_box, 0
; 箱ノード(物理)
; 斜めの床の例
gpbox id_box, 3, -1, id_texmat
setpos id_box, -3, -0.5, -0.5
setang id_box, deg2rad(-45), deg2rad(-45), 0
gppbind id_box, 10
gppset id_box, GPPSET_LINEAR_FACTOR, 0,0,0
; モデル読み込み
gpload id_model,"res/tamane2"
setscale id_model, 0.01, 0.01, 0.01
; 足元の影モデル
gptexmat id_shadowtx, "res/shadow.png", GPOBJ_MATOPT_NOZWRITE
gpplate id_shadow, -1, 1, -1, id_shadowtx
setang id_shadow, M_PI/2, 0, 0
setobjmode id_shadow, OBJ_LATE
; 作業用のヌルノード
; gppraytestを使って3Dモデル位置の地面高さを調べるために使用する。
; gppraytestは-Zベクトルを接触判定に使用する。
; 地面との接触を見るため、ベクトルはY軸方向を向いている必要がある。
; このため、-zベクトルが-Y軸方向を向くようにヌルノードを回転している。
gpnull id_null
setang id_null, -M_PI/2.0, 0, 0
; カメラ位置を設定
setpos GPOBJ_CAMERA, 0,3,10
*main
stick key, $180F
if key&128 : end
; カーソルキーでモデルを動かす
if key & 1 : addpos id_model, -0.1, 0 , 0
if key & 4 : addpos id_model, 0.1, 0 , 0
if key & 2 : addpos id_model, 0 , 0.1, 0
if key & 8 : addpos id_model, 0 , -0.1, 0
; 斜めの箱を回転
;gppapply id_box, GPPAPPLY_TORQUE, 0,10
; 床面高さを計測
; めり込み対策として、キャラクターの現在座標より少し(0.1)上から例を飛ばします。
getpos id_model, px, py , pz
setpos id_null , px, py+0.1, pz ; レイの飛ばし始め位置
posY_Ground = -10000.0 ; 真下の地面座標(初期値)
posH_Ground = 10000.0 ; 地面からの高さ(初期値)
gppraytest objid, id_null, posH_Ground
if objid > 0 {
; 地面座標
getwork id_null, x, posY_Ground, z
; 地面からの高さ
posH_Ground = py - posY_Ground
; 法線
getwork2 id_null, normalX_Ground, normalY_Ground, normalZ_Ground
}
; 丸影を地表面に配置
; 丸影を地表と同じ座標にすると、地表面と重なってちらつきが発生する。
; 重ならないようにするため、地表面よりも少し高い位置に配置する。
getpos id_model, x,y,z
setpos id_shadow, x, posY_Ground + 0.01, z
;地面が水平である場合
setang id_shadow, M_PI/2,0,0
; 丸影を地面と平行な向きに回転
;fvset fv, 0,0,0
;fvface fv, normalX_Ground, normalY_Ground, normalZ_Ground
;通常とは逆順
;setangy id_shadow, fv(0), -fv(1), fv(2)
; カメラ位置設定
getpos id_model,dx,dy,dz
gplookat GPOBJ_CAMERA, dx,dy,dz
redraw 0 ; 描画開始
gpdraw
; 2D描画
color 255,255,255
pos 8,8:mes "HGIMG4 sample"
mes "カーソルキー"
mes "↑↓ : 上昇下降"
mes "←→ : 左右移動"
pos 300, 10
mes "地面からキャラクターまでの高さ:" + posH_Ground
mes "地表面の高さ(標高) :" + posY_Ground
mes "法線方向 :" + strf("%7.2f, %7.2f, %7.2f", normalX_Ground, normalY_Ground, normalZ_Ground )
;mes "角度[deg]:" + strf("%7.2f, %7.2f, %7.2f", rad2deg(fv(0)), rad2deg(fv(1)), rad2deg(fv(2)) )
redraw 1 ; 描画終了
await 1000/60 ; 待ち時間
goto *main
上下と左右にしか動けませんが、丸影の動きを確認するには十分でしょう。 ちゃんとキャラクターの真下に丸影ができています。 高さが変わってもちゃんと表示されていますね。 「地面からの高さ」も取得できたので、空中にいるのか接地しているのかの判定もできそうです。
地面がすべて水平で作られているゲームであれば、この先は読まなくても大丈夫です。
地表に沿って丸影を傾ける
影は床面に沿ってできなければ不自然になってしまいます。サンプルのように水平な円盤が斜面に突き刺さるようでは困ります。 水平面には水平な影、斜面には斜面に沿った影、地表面に対して平行になるよう影には動いてほしいところです。
丸影を地面と平行方向に向けるためには、影を設置する場所の地面の法線ベクトル(面に垂直なベクトル)が必要です。
幸いなことに、gppraytest
は衝突検出点の法線ベクトルを取得できます。
丸影を任意のベクトル方向に回転させるための角度の計算には、fvface
を使用します。
fvface
命令は、任意座標にある -Zベクトルを任意座標方向に向けるための角度を取得できます。
法線は取得できたので、-Zベクトルを準備します。
丸影はgpplate
で作ったので、都合がいいことに面外方向(面に垂直な方向)はZ軸です。
-Z側が表(おもて)となるようにしたいので、丸影のメッシュサイズにマイナスを指定します。
gpplate id_shadow, -1, 1, -1, id_shadowtx
これで -Zベクトルも準備完了です。丸影をgpfloor
で作らなかったのはこれが理由です。
では、gppraytest
で検出した座標の -Zベクトルを、法線方向に回転する角度を計算してみます。
fvset fv, 0,0,0
fvface fv, normalX_Ground, normalY_Ground, normalZ_Ground
fv値に回転角度が代入されました。 丸影をこの方向に回転させます。
setangy id_shadow, fv(0), -fv(1), fv(2)
この数値では、setang
を使うと思った角度には回転できません。
-Y -> +X の順に回転させる必要があるようです。理由わかりませんが、そういう仕様のようです。
ちなみにZの回転は、必ず0になるはずなので、setangz
でも問題ありません。
もし丸以外の形の影を使うようであれば、setangy
を使った方がいいのかなと思います。
最後に動作確認のために、斜めの箱を回してみます。
gppapply id_box, GPPAPPLY_TORQUE, 0,10
傾斜が強めだと、ちらつきを起こすことがあるようです。 こういう場合は、地面からもう少し離れたところに丸影を接地するようにするとちらつき抑えることができます。
まとめ
丸影を付けるだけなのですが、半透明描画、地面位置検出、傾斜に合わせて回転と、なかなか幅広い対応が必要でしたね。 その分、応用すれば他にもいろいろ出来そうな気がする内容でした。まとめ作業している最中もとても勉強になりました。
実装は大変そうですが、ゲーム内容によっては全部対応する必要はないので、気軽に手を出してみてもいいと思います。 作業は大変ですが、実装できれば高さ方向の認識がしやすくなり操作性も向上が期待できます。 またジャンプも強調された印象になるので、キャラクター操作が楽しくなります。