※上記の広告は60日以上更新のないWIKIに表示されています。更新することで広告が下部へ移動します。
技術指南/基本技術
ここでは、MMD系のゲームを作るための基本的な技術について紹介します。
ここで紹介している関数はDXライブラリのごく一部の関数です。
また、かなり大雑把に説明しているので、
必ず、
DXライブラリ置き場のDXライブラリの関数リファレンスマニュアル&サンプルプログラムもあわせて確認してください。
モデルの表示
MMDゲーム化計画の第一歩である、モデルの表示方法を紹介します。
今回は、アールビットさんの十六夜咲夜モデルを使用させて頂きました。
ダウンロード>モデルから配布ページのリンクがありますので、興味のある方は是非ダウンロードしてください。
DXライブラリでは、3Dモデルを表示する際、まずモデルデータをメモリ上に読み込まなければなりません。
モデルのロードにはMV1LoadModel関数または、MV1DuplicateModel関数を使います。
int MV1LoadModel( char *FileName ) ;
int MV1DuplicateModel( int SrcMHandle ) ;
MV1LoadModel関数は、ファイル名を指定して3Dモデルをメモリ上にロードします。
読み込むことのできるモデルファイル形式は x, mqo, mv1, pmd( + vmd ) の3種類です。
pmxファイルは使用できないので注意してください。
返り値がメモリ上に読み込んだ3Dモデルファイルの識別番号で int 型の数値となっていて、
この値を使って、モデルの表示や位置の調整、向きの調整などを行います。
MV1DuplicateModel関数は、1度メモリ上に読み込んだ3Dモデルデータと同じデータを複製します。
弾やモブのように同じモデルを幾つも使うときにはこちらを使ったほうが、読み込み時間が少なくなります。
モデルの表示にはMV1LoadModel関数を使います。
int MV1DrawModel( int MHandle ) ;
では、実際に表示してみましょう。
+ | | ... |
#include "DxLib.h" int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow ) { int ModelHandle ;//モデルハンドル // ウインドウモードで起動 ChangeWindowMode( TRUE ) ; // DXライブラリの初期化 if( DxLib_Init() < 0 ) { // エラーが発生したら直ちに終了 return -1 ; } // 描画先を裏画面にする SetDrawScreen( DX_SCREEN_BACK ) ; // 3Dモデルの読み込み ModelHandle = MV1LoadModel( "Model/Sakuya/model.pmd" ) ; // メインループ(何かキーが押されたらループを抜ける) while( ProcessMessage() == 0 && CheckHitKeyAll() == 0 ) { // 画面のクリア ClearDrawScreen() ; // 3Dモデルの描画 MV1DrawModel( ModelHandle ) ; // 裏画面の内容を表画面に反映 ScreenFlip(); } // DXライブラリの後始末 DxLib_End() ; // ソフトの終了 return 0 ; }
|
左下にほんの僅かですが、モデルが表示されていますね。
2Dゲームとは違い、3Dゲームでは3Dモデルの位置とカメラの位置、向きの関係で見え方が異なります。
そのため、画面の中心にモデルを表示したいときは、カメラを表示したいモデルの近くに移動させて、表示したいモデルの方向に向けるか、
カメラが映している範囲に表示したい3Dモデルの位置を移動させる必要があります。
カメラの操作は次の項目で紹介するので、今回は3Dモデルをカメラの映している範囲に3Dモデルを移動させます。
3Dモデルの位置座標を設定するにはMV1SetPosition関数を使います。
int MV1SetPosition( int MHandle, VECTOR Position ) ;
モデルをロードした後に、この関数でモデルの位置座標を設定します。
+ | | ... |
#include "DxLib.h" int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow ) { int ModelHandle ;//モデルハンドル // ウインドウモードで起動 ChangeWindowMode( TRUE ) ; // DXライブラリの初期化 if( DxLib_Init() < 0 ) { // エラーが発生したら直ちに終了 return -1 ; } // 描画先を裏画面にする SetDrawScreen( DX_SCREEN_BACK ) ; // 3Dモデルの読み込み ModelHandle = MV1LoadModel( "Model/Sakuya/model.pmd" ) ; // 画面に映る位置に3Dモデルを移動 MV1SetPosition( ModelHandle, VGet( 320.0f, 225.0f, -370.0f ) ) ; // メインループ(何かキーが押されたらループを抜ける) while( ProcessMessage() == 0 && CheckHitKeyAll() == 0 ) { // 画面のクリア ClearDrawScreen() ; // 3Dモデルの描画 MV1DrawModel( ModelHandle ) ; // 裏画面の内容を表画面に反映 ScreenFlip(); } // DXライブラリの後始末 DxLib_End() ; // ソフトの終了 return 0 ; }
|
モデルのスケーリング、向きの変更、不透明度の変更
次にモデルを複製してスケールの変更、向きの変更、不透明度の変更を変更してみましょう。
それぞれ、MV1SetScale,MVSetRotationXYZ,MV1SetOpacityRate関数を使います。
int MV1SetScale( int MHandle, VECTOR Scale ) ;
int MV1SetRotationXYZ( int MHandle, VECTOR Rotate ) ;
int MV1SetOpacityRate( int MHandle, float Rate ) ;
回転に関しては、他に回転地ではなくモデルの方向ベクトルで指定するMV1SetRotationZYAxis関数や
int MV1SetRotationZYAxis( int MHandle, VECTOR ZAxis, VECTOR YAxis, float ZTwist ) ;
位置、回転、スケーリングなどを行列で指定する
int MV1SetMatrix( int MHandle, MATRIX Matrix ) ;
などもあります。
詳しくはDXライブラリ関数のリファレンスをみてください。
+ | | ... |
#include "DxLib.h" int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow ) { int i; int ModelHandle[4] ;//モデルハンドル配列 // ウインドウモードで起動 ChangeWindowMode( TRUE ) ; // DXライブラリの初期化 if( DxLib_Init() < 0 ) { // エラーが発生したら直ちに終了 return -1 ; } // 描画先を裏画面にする SetDrawScreen( DX_SCREEN_BACK ) ; // 3Dモデルの読み込み ModelHandle[0] = MV1LoadModel( "Model/Sakuya/model.pmd" ) ; //既存のモデルを複製 for(i = 1;i < 4;i ++){ ModelHandle[i] = MV1DuplicateModel(ModelHandle[0]) ; } // 各モデルを画面に映る位置に3Dモデルを移動 for(i = 0;i < 4;i ++){ MV1SetPosition( ModelHandle[i], VGet( 305.0f+i*10, 225.0f, -370.0f ) ) ; } //モデルのスケールの変更 MV1SetScale(ModelHandle[1], VGet(0.4f,0.2f,0.4f) ); //モデルの向きを変更 MV1SetRotationXYZ(ModelHandle[2], VGet(0.0f,PHI_F/2,0.0f) ); //モデルの不透明度を変更 MV1SetOpacityRate(ModelHandle[3], 0.1f); // メインループ(何かキーが押されたらループを抜ける) while( ProcessMessage() == 0 && CheckHitKeyAll() == 0 ) { // 画面のクリア ClearDrawScreen() ; // 3Dモデルの描画 for(i = 0;i < 4;i ++){ MV1DrawModel( ModelHandle[i] ) ; } // 裏画面の内容を表画面に反映 ScreenFlip(); } // DXライブラリの後始末 DxLib_End() ; // ソフトの終了 return 0 ; }
|
左から順にそのまま、スケール変更、向き変更、不透明度変更したモデルです。
半透明にしたモデルは、このように服が透けてしまうので、
透明度を操作してキャラをフェードイン、フェードアウトさせたい場合などは、ある程度工夫が必要です。
詳しくはミニテクニックの3Dモデルの半透明化を参考にしてください。
また、これら以外にもモデルの表示設定、ディフューズカラー、エミッシブカラー、アンビエントカラー、
Zバッファ使用の設定、物理演算の設定などを行うことができます。
これらについては自分で必要なときに確認してください。
カメラの操作
今度は、キャラを動かさずに、カメラの位置、方向を操作して映す範囲を設定します。
基本的には初期設定でモデル座標を決定し、カメラをキャラの後ろに付けるなどがいいでしょう。
カメラの位置、方向を設定するためにはこれらの関数を使います。
int SetCameraPositionAndTarget_UpVecY( VECTOR Position, VECTOR Target ) ;
int SetCameraPositionAndTargetAndUpVec( VECTOR Position, VECTOR Target, VECTOR Up ) ;
int SetCameraPositionAndAngle( VECTOR Position, float VRotate, float HRotate, float TRotate ) ;
SetCameraPositionAndTarget_UpVecY関数は、カメラ座標、注視点座標を引数とします。
カメラの上方向はY軸から算出するため、これが一番便利です。
カメラの上方向を変更したいとき(たとえば上下が反転する演出など)は
SetCameraPositionAndTargetAndUpVec関数を使用するとよいでしょう。
こちらは先程のカメラ座標、注視点座標のほかに、上方向ベクトルを引数に取ることができます。
SetCameraPositionAndAngle関数は、カメラ座標の他に垂直回転角度、水平回転角度、捻り回転角度を引数に取ります。
どれを使っても良いですが、上方向をいじらない場合は、
素直にSetCameraPositionAndTarget_UpVecY関数を使うとよいでしょう。
それでは、カメラ座標がくるくる回るものを作ってみましょう。
+ | | ... |
#include "DxLib.h" #include <math.h> int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow ) { int ModelHandle ;//モデルハンドル int t = 0;//時間 // ウインドウモードで起動 ChangeWindowMode( TRUE ) ; // DXライブラリの初期化 if( DxLib_Init() < 0 ) { // エラーが発生したら直ちに終了 return -1 ; } // 描画先を裏画面にする SetDrawScreen( DX_SCREEN_BACK ) ; // 3Dモデルの読み込み ModelHandle = MV1LoadModel( "Model/Sakuya/model.pmd" ) ; // モデルを原点に配置 MV1SetPosition(ModelHandle,VGet(0.0f,0.0f,0.0f)); // メインループ(エスケープキーが押されたらループを抜ける) while( ProcessMessage() == 0 && CheckHitKey( KEY_INPUT_ESCAPE ) == 0 ) { //時間経過 t++; // ModelHandleを注視し、カメラ座標はくるくる回る(3秒=180フレームで1回転) SetCameraPositionAndTarget_UpVecY( VGet(40.0f*cos(t*PHI_F/90),0.0f,40.0f*sin(t*PHI_F/90)), MV1GetPosition(ModelHandle) ) ; // 画面のクリア ClearDrawScreen() ; // 3Dモデルの描画 MV1DrawModel( ModelHandle ) ; // 裏画面の内容を表画面に反映 ScreenFlip(); } // DXライブラリの後始末 DxLib_End() ; // ソフトの終了 return 0 ; }
|
モデルの一部が表示されていませんね。
一瞬焦りますが、これはクリップ範囲の設定を忘れているからです。
クリップ範囲の設定
カメラのクリップ範囲の設定にはSetCameraNearFar関数を使います。
int SetCameraNearFar( float Near, float Far ) ;
カメラの手前(Near)、奥(Far)のクリップ距離を設定します。
クリップ範囲とは、いわゆるカメラに映る範囲のことで、
Near~Farの範囲がカメラに映ります。
前の項目でモデルの一部が表示されなかったのは、クリップ範囲を外れているためです。
極端に言えば、Nearを0.00001f,Farを100000.0fなどと設定すれば全てを表示することができますが、
Zバッファの仕様の関係で、特にNearについてはあまり小さい範囲を設定してしまうと、
環境によっては設定した範囲のものも表示されなくなってしまうことがあります。
Nearの値は不都合が無い範囲でなるべく大きな値を、
Farの値は描画したい最奥のモノのより少し大きな値を設定するようにしてください。
クリップ範囲を設定すると、このように先ほど表示されていなかった部分が表示されるようになります。
他に、視野角やフォグの設定などがありますが、こちらは各自リファレンスを読んでおいてください。
また、キャラが画面情報に表示されているのは、モデルの原点が足の裏になっているからです。
キャラの中心を注視したい場合は、キャラのセンターフレームの座標を取得して注視するか、
モーションでキャラの位置をずらすなどの方法があります。
ライトの操作
DXライブラリでは、ライトの追加、変更等を行うことができます。
基本的には、ライトは標準ライトのみ(デフォルト:ディレクショナルライト)で、
必要であればカメラ照明(カメラ方向にライトを向ける)にしておけばOKです。
(DXLibModelViewerのデフォルトと同じ環境)
※誰か追記をお願いします。
アニメーションアタッチ
モデルを表示するだけでなくアニメーションを再生したい時は、
アニメーションアタッチと呼ばれる方法を使います。
MMDモデルのアニメーション(vmdファイル)を再生したい場合、
まずモーション再生したいモデルファイルと同じフォルダに、
モデルファイル名000.vmd,モデルファイル名001.vmd,...と連番でモーションファイルを配置しなければなりません。
例えば、モデルファイルがmodel.pmdの時は同じフォルダに
model.pmd
model000.vmd
model001.vmd
…
と言った具合に配置していきます。
後でファイル名を指定してモーションを読み込む、別フォルダで管理などはできません。
モデルを読み込む際に物理演算についての設定が可能です。
int MV1SetLoadModelUsePhysicsMode( int PhysicsMode ) ;
int MV1SetLoadModelPhysicsWorldGravity( float Gravity ) ;
これらの関数で物理演算の方法、重力の大きさを変更することができます。
リアルタイム物理演算は動作が遅い、MV1SetScale関数を使えないなどの欠点があり、
不具合も多いので、基本的には、MV1SetLoadModelUsePhysicsMode関数ではデフォルトの
DX_LOADMODEL_PHYSICS_LOADCALC(ファイル読み込み時物理演算)を使用しましょう。
また、ループのあるモーションについてはファイル名の最後にLをつける(例えばmodel001L.vmd)ことで、
ファイル読み込み時物理演算をある程度自然にすることができます。
int MV1AttachAnim( int MHandle, int AnimIndex, int AnimSrcMHandle, int NameCheck ) ;
アニメーションを再生するには、MV1AttachAnim関数で、アニメーションをアタッチする必要があります。
この関数では、アニメーションを再生するモデルハンドル(int MHandle),アニメーション番号(int AnimIndex),
アニメーション元のモデルハンドル(int AnimSrcMHandle)、,フレーム名不一致のチェックするかどうか(int NameCheck)
を指定します。
アニメーション元のモデルハンドルと、アタッチ先のモデルハンドルは別で設定でき、
あるモーションを複数のモデル指定するとき、メモリ使用量軽減をはかることができます。
アニメーション元とアタッチ先のモデルが同じ時は、AnimSrcMHandleは-1を指定します。
また、戻り値として、アタッチインデックスが帰ってきます。
これはアニメーションの再生などに使用するのでどこかに保存しておきましょう。
アニメーション番号は、model000.vmdなら0、model001.vmdなら1を指定します。
int MV1SetAttachAnimTime( int MHandle, int AttachIndex, float Time ) ;
モデルにアニメーションをアタッチした後に、このMV1SetAttachAnimTime関数で、
再生時間を設定すればモーションを再生することができます。
再生時間は、手動で動かさなければいけません。
float MV1GetAttachAnimTotalTime( int MHandle, int AttachIndex ) ;
この関数で総再生時間を確認して、適切にカウントしましょう。
int MV1SetAttachAnimBlendRate( int MHandle, int AttachIndex, float Rate ) ;
また、複数のモーションをブレンドするときは、このMV1SetAttachAnimBlendRate関数を使います。
これを使用して複数のモーションを自然に遷移させることができます。
それでは、走る(model000L.vmd)→歩く(model001L.vmd)のモーションを繰り返すプログラムを作ってみましょう。
+ | | ... |
#include "DxLib.h" #include <math.h> int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow ) { int i; int ModelHandle ;//モデルハンドル int cam_t = 0;//カメラ時間 float play_t[2] = {0.0f,0.0f};//アニメーション時間 float TotalTime[2];//モデルのアニメーションの総再生時間 float AnimRate = 0.0f; int AttachIndex[2];//アタッチ番号 // ウインドウモードで起動 ChangeWindowMode( TRUE ) ; // DXライブラリの初期化 if( DxLib_Init() < 0 ) { // エラーが発生したら直ちに終了 return -1 ; } // 描画先を裏画面にする SetDrawScreen( DX_SCREEN_BACK ) ; // 3Dモデルの読み込み // model000L.vmd,model001L.vmdを同時に読み込む ModelHandle = MV1LoadModel( "Model/Sakuya/model.pmd" ) ; //アニメーションのアタッチインデックスと総再生時間を得る for(i = 0;i < 2;i++){ AttachIndex[i] = MV1AttachAnim(ModelHandle, i, -1, FALSE ); TotalTime[i] = MV1GetAttachAnimTotalTime( ModelHandle, AttachIndex[i]) ; } // モデルを原点に配置 MV1SetPosition(ModelHandle,VGet(0.0f,0.0f,0.0f)); //カメラのクリップ距離を設定する SetCameraNearFar( 5.0f,500.0f) ; // メインループ(エスケープキーが押されたらループを抜ける) while( ProcessMessage() == 0 && CheckHitKey( KEY_INPUT_ESCAPE ) == 0 ) { //時間経過 cam_t++; play_t[0] += 0.50f;//走り play_t[1] += 0.50f;//歩き(今回は再生スピードは同じ) for(i = 0;i < 2;i++){ // 各アニメーションの再生時間がアニメーションの総再生時間に達したら再生時間を0に戻す if( play_t[i] >= TotalTime[i] ){ play_t[i] = 0.0f ; } // アニメーションをセット MV1SetAttachAnimTime( ModelHandle, AttachIndex[i], play_t[i] ) ; } //アニメーションのブレンド量を調整する。 //時間でAnimRateを変化させることで、走る→歩くを繰り返す。 if(cam_t%2000 <= 50 && AnimRate > 0.0f){ AnimRate -= 0.02f; if(AnimRate < 0.0f){ AnimRate = 0.0f; } } if(cam_t%2000 > 1000 && cam_t%2000 <= 1050 && AnimRate < 1.0f){ AnimRate += 0.02f; if(AnimRate > 1.0f){ AnimRate = 1.0f; } } MV1SetAttachAnimBlendRate( ModelHandle, AttachIndex[0], 1.0f-AnimRate ) ;//走っているモーション MV1SetAttachAnimBlendRate( ModelHandle, AttachIndex[1], AnimRate ) ;//歩くモーション // (0.0f,10.0f,0.0f)を注視し、カメラ座標はくるくる回る(5秒=300フレームで1回転) SetCameraPositionAndTarget_UpVecY( VGet(40.0f*cos(cam_t*PHI_F/150),20.0f,40.0f*sin(cam_t*PHI_F/150)), VGet(0.0f,10.0f,0.0f) ) ; // 画面のクリア ClearDrawScreen() ; // 3Dモデルの描画 MV1DrawModel( ModelHandle ) ; DrawFormatString(0,0,GetColor(255,255,255),"AnimRate:%.2f",AnimRate); // 裏画面の内容を表画面に反映 ScreenFlip(); } // DXライブラリの後始末 DxLib_End() ; // ソフトの終了 return 0 ; }
|
ビルボード
ビルボードとは、3D空間に画像を表示する方法です。
3Dモデルではなく画像を表示するので、かなりの処理軽減になります。
特に丸いもの(水晶玉、シューティングの丸玉など)は、どこから見てもほとんど変わらない上に、
3Dモデルで表現すると頂点や面が多くなってしまうため、ビルボードで表現することをお勧めします。
他にも、ステージ部分だけを3Dモデルを使い、キャラをすべてビリボードで表現したゲームや、
逆にステージを画像にして、キャラクターのみを3Dモデルを表現する方法もあります。
有名なので言えば、前者はマリオカート64なんかがそうですね。
後者は、FF7などのPSのRPGでは多用されていたように思います。
ビルボードはかなり処理は軽くなりますが、結構面倒な計算や手続き(後述参考)を必要とするので、
うまく3Dモデルと使い分けてください。
int DrawBillboard3D( VECTOR Pos, float cx, float cy, float Size, float Angle, int GrHandle, int TransFlag ) ;
int DrawModiBillboard3D( VECTOR Pos, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, int GrHandle, int TransFlag ) ;
ビルボードを使用するには、DrawBillboard3D関数、DrawModiBillboard3D関数のどちらかを使います。
DrawBillboard3D関数は、ビルボードを貼る中心座標のみ、
DrawModiBillboard3D関数は、中心座標と、画像の4つ角の相対座標(2D)を指定できます。
3D座標の4点を指定できるわけではないので、例えば、楕円状の絵を使って、
楕円球を表現しようとした場合、3D→2Dの射影座標計算が必要となります。
それでは、たくさんのビルボードを表示してみましょう。
今回、前後関係を表現するため、Zバッファを使用します。
詳しくは
お勉強系のZバッファの項目を見てください。ここでは詳しい説明は省略します。
int SetUseZBuffer3D( int Flag ) ;
int SetWriteZBuffer3D( int Flag ) ;
この2つの関数を使って、Zバッファの使用を、書き込みをオンにして、描画します。
+ | | ... |
#include "DxLib.h" #include <math.h> #define IMG_MAX (100) //ランダムな方向ベクトルを生成する関数 VECTOR VRand(float Vsize); int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow ) { int i; int cam_t = 0;//カメラ VECTOR VECTORs[IMG_MAX]; int image[4]; // ウインドウモードで起動 ChangeWindowMode( TRUE ) ; // DXライブラリの初期化 if( DxLib_Init() < 0 ) { // エラーが発生したら直ちに終了 return -1 ; } // 描画先を裏画面にする SetDrawScreen( DX_SCREEN_BACK ) ; // 画像の読み込み image[0] = LoadGraph("Image/shot/R.png"); image[1] = LoadGraph("Image/shot/B.png"); image[2] = LoadGraph("Image/shot/Y.png"); image[3] = LoadGraph("Image/shot/G.png"); // ランダムなベクトルを生成 for(i = 0;i < IMG_MAX;i++){ VECTORs[i] = VRand(20.0f+GetRand(30)); } //Zバッファを有効にする SetUseZBuffer3D( TRUE ) ; //Zバッファの書き込みを有効にする SetWriteZBuffer3D( TRUE ) ; //カメラのクリップ距離を設定する SetCameraNearFar( 5.0f,500.0f) ; // メインループ(エスケープキーが押されたらループを抜ける) while( ProcessMessage() == 0 && CheckHitKey( KEY_INPUT_ESCAPE ) == 0 ) { //時間経過 cam_t++; // (0.0f,10.0f,0.0f)を注視し、カメラ座標はくるくる回る(5秒=300フレームで1回転) SetCameraPositionAndTarget_UpVecY( VGet(40.0f*cos(cam_t*PHI_F/150),20.0f,40.0f*sin(cam_t*PHI_F/150)), VGet(0.0f,10.0f,0.0f) ) ; // 画面のクリア ClearDrawScreen() ; //画像を描画 for(i = 0;i < IMG_MAX;i++){ DrawBillboard3D(VECTORs[i], 0.5f,0.5f,5.0f,0.0f, image[i%4],true) ; } // 裏画面の内容を表画面に反映 ScreenFlip(); } // DXライブラリの後始末 DxLib_End() ; // ソフトの終了 return 0 ; } VECTOR VRand(float Vsize){ float u1 = 2*PHI_F*GetRand(1000)/1000; float u2 = 2*PHI_F*GetRand(1000)/1000; VECTOR R = VGet(cos(u2)*cos(u1),cos(u2)*sin(u1),sin(u2)); return VScale(R,Vsize); }
|
このように、3D空間に色とりどりの弾が表示されました。
しかし、半透明部分の前後関係がおかしくなっていますね?
これは、Zバッファが透明色に対応していないからです。
(モデルの透明テクスチャでも同様の現象が起こります)
詳しい原因は
お勉強系を参考にしてください。
しかし、Zバッファをきると、
このように前後関係を無視して、描画順に表示されてしまいます。
見た目は綺麗ですが、壁のむこうの弾が透けてしまったりします。
これはこれでまずいですね。
これを解決するためには、複数のビルボードが重なる場合、奥のものから順に描画しなければなりません。
カメラやビルボードを自由に動かせる場合、カメラ方向で奥行きソートする必要があります。
当然、前後関係が途中で入れ替わるものはビルボードで表現できません。
このように、ビルボードを使用する場合はいくつか注意が必要で、正直使いにくい。
しかし、処理を軽くするためには必須技術と言えるでしょう。