丸影

目次

はじめに

 ゲームにおいても、足元の影の存在は結構重要です。 キャラクターや物体が空中に浮かんでいるのか、地面に接地しているのかを判断する材料として影は利用されます。 キャラクターの足と影がくっついていれば接地していて、離れていれば浮いているように見えます。

 影が存在していなければ、空中に浮かんでいる物体の位置が把握しにくくなります。また逆に、地面にいるにもかかわらず、空中に浮かんでいるものとの区別がつかなかったりして、遊びにくいゲームになってしまいます。 位置把握のための影は、形がキャラクターのシルエットの形をしていなくてもいいですし、光源の位置を考慮していなくても問題ありません。とにかく存在していることが重要なのです。

 ということで、影を作ってみます。 ここでの実装は、「丸影」と呼ばれるキャラクターの足元に黒い丸を置くだけの簡単な実装です。 幸い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オプションを追加。
  • setobjmodeOBJ_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は、gpfloorgpplateでは衝突を検出しない事があります。 また厚さが薄い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

 傾斜が強めだと、ちらつきを起こすことがあるようです。 こういう場合は、地面からもう少し離れたところに丸影を接地するようにするとちらつき抑えることができます。

まとめ

 丸影を付けるだけなのですが、半透明描画、地面位置検出、傾斜に合わせて回転と、なかなか幅広い対応が必要でしたね。 その分、応用すれば他にもいろいろ出来そうな気がする内容でした。まとめ作業している最中もとても勉強になりました。

 実装は大変そうですが、ゲーム内容によっては全部対応する必要はないので、気軽に手を出してみてもいいと思います。 作業は大変ですが、実装できれば高さ方向の認識がしやすくなり操作性も向上が期待できます。 またジャンプも強調された印象になるので、キャラクター操作が楽しくなります。

関連記事

  1. 地表を歩く 目次 地表を歩かせよう サンプル 大まかな手順 gppray...
  2. UVマッピング 目次 はじめに サンプル 解説1 解説2 解説3 gpmes...
  3. ぜんぶ食べないと出られない部屋 前の画像 次の画像 ダウンロード ぜんぶ食べないと出られない...
  4. クォータニオン計算モジュール 前の画像 次の画像 ダウンロード クォータニオン計算モジュー...
  5. パラメータ 目次 はじめに PRMSET_FLAG PRMSET_MOD...
  6. マテリアルの設定変更 目次 materialファイルを書き換えてみる ZテストとZ...