なんだか雲行きの怪しい雑記帖

ぐだぐだ日記とメモと,あと不定期更新

【HSP】【DXLib】検証デモ「がん☆ぷろ!」を作ってみて分かったことまとめ

気づいたら前回の記事から半年近く経ってる恐怖。

この記事は?

検証デモ「がん☆ぷろ!」(下記参照)を作ったときに得られた知見と工夫と苦悩とその他諸々を書き残す記事です。

「がん☆ぷろ!」について

プロ生ちゃん(本家様)と一緒に飛行艇に乗って敵を撃ったりバルーンを撃ったりする、HSP+DXライブラリ+Effekseerのデモ(アプリ)です。

このブログでは諸々事情があって公開してませんので、DLはHSPコンテストのページからどうぞ。
gunpro_intro1.pnggunpro_intro2.pnggunpro_intro3.png

なぜ「デモ(アプリ)」というややこしい表記をしているかについてですが、「がん☆ぷろ!」を作ったきっかけというか目標を述べておきますと…

  • HSP+DXライブラリを用いた3D描画の可能性(発展性)の検証・試作
  • (自作ライブラリ)Effekseerプラグイン for HSPの簡易検証・テスト
  • (自作ライブラリ)mistのβ版リリースのためのテスト

といった感じです、順不同で思いついた順で書きましたので順番は特に意味がありません。

3D描画の「試作」や「主張」といった意味合いで「デモ」、「検証」や「テスト」という目的があるため「(アプリ(アプリケーション:ソフトウェア))」みたいな表記が混在するに至った次第です、ごちゃごちゃしててすみませぬ。
完全に私自身のエゴに依るところが大きいのですが、特段面白さを突き詰めて作ったわけでもないので少なくとも「ゲーム」ではないし、「アプリ」と称するにもやるというより見てもらうものだし、と散々迷った挙句上述の表記に落ち着きました。

記事中で触れていく内容について

本記事(と前後編と分けるかもしれないので一連の記事)では上述のそれぞれの目的別に、

・「実際どれくらい使えるものだったのか」
・「こういう使い方が出来るようなのでやってみました」

といったことを中心に書いていこうと思います。

目的の後半2つは動作テストがメインなので記事中ではあまり掘り下げません。
つまりDXライブラリで3D描画周りの話がメインになるかと思います。

ただDXライブラリはHSP用にカスタムされたAPIを持っている訳ではありません。
トコロにより読む際にはWin32APIなどライブラリ呼び出しの基本的な知識だったり、
3Dも扱うため立体解析幾何(具体的にはベクトルとか行列とか)についての知識が必要になります(予定)、ご了承ください。

でもまぁ、説明を書く私の知識がそもそも豊富ではないため一般的に難しい内容に触れる気はそんなにありません、
ちゃっちゃと流すところは流していきます。
興味があるところだけかいつまんで読んで頂ければそれで私としても有難い限りですので、軽い気持ちでどうぞよろしく。

HSP+DXライブラリによる3D描画

実はここ、1カテゴリとして書いてしまったけど内容については基本的なプリミティブの描画からライティング、3Dモデル+アニメーション、シェーダなど多岐に渡ります。

3Dプリミティブの描画

とりあえず最初はDXライブラリで簡単なところから検証を始めました。
つまり、「ちゃんと3Dのカメラ・射影の設定ができて、簡単なプリミティブを描画できるか」ですね。
本節ではプリミティブ描画をするところまで触れます。

といっても難しいことは全くなく、DXライブラリに

・3D空間のカメラの設定をする関数、射影の設定をする関数
・プリミティブの描画をする関数

がそれぞれあるので、それを呼ぶだけなんですけどね。

HSPからDXライブラリを使う際の設定は私の前の記事「HSPとDxLibで2D描画の基礎ら辺とか」やinovia氏のcodetterに投稿しているコードなどをご参照ください。

と、DXライブラリの初期化周りを省いたところで、実際に3Dのプリミティブのいくつかを描画するサンプルコード。
#include "DxLib.as"

// ウィンドウモードをON(OFFの場合フルスクリーンになる)
ChangeWindowMode 1

// DxLibで描画するウィンドウをHSPのウィンドウに変更
SetUserWindow hwnd
SetUserWindowMessageProcessDXLibFlag 0

// DxLibの初期化
DxLib_Init
if ( stat == -1 ) : dialog "初期化エラー"

repeat
// 裏画面を描画ターゲットに指定
SetDrawScreen DX_SCREEN_BACK
ClearDrawScreen 0
SetUseZBuffer3D 1
SetWriteZBuffer3D 1

// 3D描画用のカメラ設定
SetCameraPositionAndTargetAndUpVec cos(deg2rad(cnt))*40.0, 10.0, sin(deg2rad(cnt))*40.0/* 位置 */, 0.0, 0.0, 0.0/* 注視点 */, 0.0, 1.0, 0.0/* 鉛直上 */
SetCameraNearFar 1.0, 100.0// ニア・ファークリップ
SetupCamera_Perspective deg2rad(60.0)// 画角

// 3Dプリミティブを描画
DrawCapsule3D -10.0, 10.0, 0.0/* 先端位置 */, -10.0, -5.0, 0.0/* 底位置 */, 4.0/* 半径 */, 3, 0xff00ffff, 0xffffffff, 1
DrawCone3D 10.0, 10.0, 0.0/* 先端位置 */, 10.0, -5.0, 0.0/* 底位置 */, 4.0/* 半径 */, 3, 0xffff00ff, 0xffffffff, 1
DrawSphere3D 0.0, 0.0, 0.0/* 位置 */, 4.0/* 半径 */, 3/* 分割:プリミティブの滑らかさ */, 0xffffff00/* ディフューズ:ARGB */, 0xffffffff/* スペキュラ:ARGB */, 1

// 裏画面を表画面へ転送
ScreenFlip

// ウィンドウメッセージ等の処理(DxLib側)
ProcessMessage

// ウィンドウメッセージ等の処理(HSP側)
await 0
loop
stop

gunpro_3dprimitive_sample.png

カメラは「カメラがいる位置」、「注視先の位置」そして「カメラの上方向となるベクトル」を指定することで設定します。
カメラの設定は「3D空間のどの部分が見るのか」を指定するもので、「(画面上で)どのように見えるのか」については射影(プロジェクション)の設定になります。
これらカメラのパラメータ設定は専用のAPIを呼び出します、hgimg3やhgimg4と違いカメラはオブジェクトとして存在している訳ではないのでそこはちょっと注意ですね。

射影の方では「カメラからどれくらいの距離のものが見えるか」としてNear・Far、「どれくらいの角度の範囲が見えるのか」として画角(Field Of View)を指定します。
あとは使うのは若干レアですが射影後の消失点の位置も指定できます。
ただ用途が少し特殊なのでこれは忘れても大丈夫だと思います。

あとやらなきゃいけないこととして注意しておくべきなのは、3Dの描画ではZバッファのテストと書き込みを有効にすることぐらいですかね。
これを設定することで、各ピクセル単位で一番前にあるポリゴンが最終的なカラーとして残ることになります。

この辺の理解は重要ですので、もし3DのZバッファを使わないとどうなるか、一応サンプルで示しておきましょう。
#include "DxLib.as"

// ウィンドウモードをON(OFFの場合フルスクリーンになる)
ChangeWindowMode 1

// DxLibで描画するウィンドウをHSPのウィンドウに変更
SetUserWindow hwnd
SetUserWindowMessageProcessDXLibFlag 0

// DxLibの初期化
DxLib_Init
if ( stat == -1 ) : dialog "初期化エラー"

repeat
// 裏画面を描画ターゲットに指定
SetDrawScreen DX_SCREEN_BACK
ClearDrawScreen 0

// デプスは使用しない
SetUseZBuffer3D 0
SetWriteZBuffer3D 0

// 裏面カリングはする
SetUseBackCulling 1

// 3D描画用のカメラ設定
SetCameraPositionAndTargetAndUpVec cos(deg2rad(cnt))*40.0, 10.0, sin(deg2rad(cnt))*40.0/* 位置 */, 0.0, 0.0, 0.0/* 注視点 */, 0.0, 1.0, 0.0/* 鉛直上 */
SetCameraNearFar 1.0, 100.0// ニア・ファークリップ
SetupCamera_Perspective deg2rad(60.0)// 画角

// 3Dプリミティブを描画
DrawCapsule3D -10.0, 10.0, 0.0/* 先端位置 */, -10.0, -5.0, 0.0/* 底位置 */, 4.0/* 半径 */, 3, 0xff00ffff, 0xffffffff, 1
DrawCone3D 10.0, 10.0, 0.0/* 先端位置 */, 10.0, -5.0, 0.0/* 底位置 */, 4.0/* 半径 */, 3, 0xffff00ff, 0xffffffff, 1
DrawSphere3D 0.0, 0.0, 0.0/* 位置 */, 4.0/* 半径 */, 3/* 分割:プリミティブの滑らかさ */, 0xffffff00/* ディフューズ:ARGB */, 0xffffffff/* スペキュラ:ARGB */, 1

// 裏画面を表画面へ転送
ScreenFlip

// ウィンドウメッセージ等の処理(DxLib側)
ProcessMessage

// ウィンドウメッセージ等の処理(HSP側)
await 0
loop
stop

gunpro_polygon3d_withoutDepth_sample.png

こんな感じで、各ピクセル単位で描画した順番によって最終的に残るカラーが決まります。
Zを使わないと必然同じポリゴンの中でも表面・裏面で後から描画された方が残ってしまうので、それはさすがにまずかったので裏面カリング(カメラから見えない面は描画しない)を有効にしています(というか、デフォルト無効なのか)。

さて、この辺は流石にDXライブラリだろうとすぐ出来るとは予想ついていたのでサクサク次へ。

シェーディング

シェーディングというべきかライティングというべきか迷いますが…。
DXライブラリが内蔵している機構で想定していたのはシェーディングだと思うのでシェーディングとしておきました。

DXライブラリが標準で採用しているシェーディングのシステムは、DirectX9以前の時代にあった固定機能パイプラインと呼ばれるものです。
現在家庭用ゲーム・PCゲーム・3Dツールなど垣根を分けず広く普及しているPBR世代のシェーディング(というかライティング)とはそもそも仮定しているモデルが異なるので注意が必要。
 尚一応、後述しますがDXライブラリはシェーダをフルスクラッチで書けるのでPBR出来ないというわけではありません、あくまで「標準で採用しているシェーディングが」というだけです。

あまり複雑なものではないのですが、その処理内容は知っておかないとどういう結果になるか分からないので、非常に簡単にですが説明しておきます。

参照先としてMSDNの固定機能パイプラインライティングにおける数学的計算アンビエントライトディフューズライトスペキュラライトエミッションライトあたりが分かれば全然大丈夫だと思います。

まず、最終的に画面にでてくるポリゴンの色は、MSDNによると次のように決定されるそうです。
グローバル イルミネーション =
 アンビエント ライト + ディフューズ ライト + スペキュラ ライト + エミッション ライト

それぞれの項について簡単に説明すると、次のような感じになります。

アンビエントライト:大域(環境)照明、どのようなモノにでも一定で加算される値のこと、光の底上げと捉えればOK
ディフューズライト:拡散光、ライトの放射角度と物体表面の向きから計算される値のこと、視点位置に非依存で決まる
スペキュラライト:(鏡面)反射光、ライトの放射角度と物体表面の向き、更に視点の位置から計算される値のこと、水面のキラッと光る部分とかがそれ
エミッションライト:自己発光、物体自身が光を放っていると仮定して計算される値のこと、炎とかですね

上記の説明が簡潔すぎて分からない~、という場合はゲームつくろー!さんの「何だか取っ付きにくい「ライト」をまとめてみました」が非常によくまとまった資料なのでそちらを見た方がいいかもしれません…!

さて、これらのライトの計算をを各物体毎に行うのですが、物体毎にどういった色であるか、またライト自体にがどういった色を持っているかで計算結果が変わります。
固定機能パイプラインでは、各ライトと物体の色はRGB的に乗算された結果が最終的な色として加算されていきます。

どういう事かというと、例えば紫色のライト(RGB:1.0, 0.0, 1.0)と黄色の物体(RGB:1.0, 1.0, 0.0)から計算される結果は赤色(RGB:1.0, 0.0, 0.0)になります。

ライト、物体それぞれでアンビエントカラー、ディフューズカラー、スペキュラカラー、エミッションカラーは別々に設定できるので、つまり計算式で言うと

グローバル イルミネーション =
  (光源のアンビエントカラー×物体のアンビエントカラー)
  +(光源のディフューズカラー×物体のディフューズカラー×ディフューズ減衰)
  +(光源のスペキュラカラー×物体のスペキュラカラー×スペキュラー減衰)
  +(物体のエミッションカラー)

となります。

ちなみに、固定機能パイプラインではライトの数は複数個にできるので、上記の計算は各ライト毎に更に追加で行われます。
当然ですが、ライトをたくさん配置するとシェーダが重くなります。

DXライブラリの初期状態ではメインライトでディフューズとスペキュラが有効になっているので、ライトが当たっている方向は明るくシェーディングされます。
ただし、アンビエントが設定されていないため、ライトが当たっていない面は全て真っ黒になってしまっています。
(この処理はシェーディング処理によるので、ハーフランバートなどのシェーディングを行えば真っ黒にはならない(なる面積が少ない)とかはあります)


さて、大方説明も終わったのでどうせだから固定機能パイプラインに入っているライトをいくつか有効にして描画結果を見てみましょう。
こういうのは得てしてやってみないと見えてこないところが多かったりします。

メインライトはデフォルトではディレクショナルライトと言って一定方向から降り注ぐライトなんですが、今回メインライトは無効にしてポイントライトとスポットライトを有効にしてみました。
それに合わせ、3Dプリミティブのディフューズカラーも白に設定してあります。
#include "DxLib.as"

// ウィンドウモードをON(OFFの場合フルスクリーンになる)
ChangeWindowMode 1

// DxLibで描画するウィンドウをHSPのウィンドウに変更
SetUserWindow hwnd
SetUserWindowMessageProcessDXLibFlag 0

// DxLibの初期化
DxLib_Init
if ( stat == -1 ) : dialog "初期化エラー"

// 標準ライトは無効化
SetLightEnable 0

// ポイントライトの作成
CreatePointLightHandle 0.0, 0.0, 10.0/* 位置 */, 50.0/* 半径 */, 0.0, 0.2, 0.0/* 減衰 */
pointLight = stat
SetLightDifColorHandle poingLight, 1.0, 1.0, 1.0, 0.0
SetLightSpcColorHandle pointLight, 0.0, 0.0, 0.0, 0.0

// スポットライトの作成
CreateSpotLightHandle 0.0, 0.0, -10.0/* 位置 */, 0.0, 0.0, 1.0/* 方向 */, deg2rad(120.0), deg2rad(10.0), 50.0/* 半径 */, 0.0, 0.1, 0.0/* 減衰 */
spotLight = stat
SetLightDifColorHandle spotLight, 1.0, 0.0, 0.0, 0.0
SetLightSpcColorHandle spotLight, 0.0, 0.0, 0.0, 0.0

repeat
// 裏画面を描画ターゲットに指定
SetDrawScreen DX_SCREEN_BACK
ClearDrawScreen 0
SetUseZBuffer3D 1
SetWriteZBuffer3D 1

// 3D描画用のカメラ設定
SetCameraPositionAndTargetAndUpVec cos(deg2rad(cnt))*40.0, 10.0, sin(deg2rad(cnt))*40.0/* 位置 */, 0.0, 0.0, 0.0/* 注視点 */, 0.0, 1.0, 0.0/* 鉛直上 */
SetCameraNearFar 1.0, 100.0// ニア・ファークリップ
SetupCamera_Perspective deg2rad(60.0)// 画角

// 3Dプリミティブを描画
DrawSphere3D 0.0, 0.0, 0.0/* 位置 */, 4.0/* 半径 */, 3/* 分割:プリミティブの滑らかさ */, 0xffffffff/* ディフューズ:ARGB */, 0xffffffff/* スペキュラ:ARGB */, 1
DrawCone3D 10.0, 10.0, 0.0/* 先端位置 */, 10.0, -5.0, 0.0/* 底位置 */, 4.0/* 半径 */, 3, 0xffffffff, 0xffffffff, 1
DrawCapsule3D -10.0, 10.0, 0.0/* 先端位置 */, -10.0, -5.0, 0.0/* 底位置 */, 4.0/* 半径 */, 3, 0xffffffff, 0xffffffff, 1

// 裏画面を表画面へ転送
ScreenFlip

// ウィンドウメッセージ等の処理(DxLib側)
ProcessMessage

// ウィンドウメッセージ等の処理(HSP側)
await 0
loop
stop

gunpro_primitive3dLight_sample.png

3Dプリミティブの描画もシェーディングされるので、ライトがある方向が明るくなり、ライトとは反対の方向が暗くなっていますね
また、ライトの片方は赤色に設定してあるので、物体の色は白ですが結果は赤になっています。

3Dモデル描画

実際3D描画エンジンとして使う場合の必須項目、3Dモデルの取り扱いです。

DXライブラリが対応している3Dモデルの形式は
.x(DirectX形式)
.mqo(メタセコイア形式)
.pmx及び.pmd(+.vmd:MMD形式)
.mv1(DXライブラリ独自形式)

です。

正直3Dモデルの形式はモデリングソフトも色々なこともあってか様々なので、.mqoや.pmxなどが標準で対応しているのは素直に強みだなと思います。
.mv1はDXライブラリ付属ツールDxLibModelViewerで出力できる形式です。
DxLibModelViewerは.xファイルなどの読み込みに対応しているので、読み込ませてから出力させるようなフローを採用している場合にだせるファイルですね。
.mv1はDXライブラリに特化したファイル形式のため、ファイルサイズが小さくDXライブラリでの読み込みが高速になるメリットがあります。
また、アニメーションデータが内包されるため、複数のファイルで構成されるファイルを.mv1でちょこっと纏められるというメリットもあります。

ただ、ある程度ファイルをまとめる目的で.mv1を使ったとしても、テクスチャはファイルとして存在しないとDXライブラリが読み込み時に見つけられないという問題は残ったままだったりします。
(正確に言うとテクスチャ解決が出来ればファイルとして存在する必要がないので、テクスチャ解決する関数を自分で書けば、複数モデルでのテクスチャ共有化などが可能になったりします。
 実は「がん☆ぷろ!」ではモデルデータのアーカイブ化に伴って上記テクスチャ解決に相当する処理をしているのですが、複雑なので深くは触れません、ご容赦下さい)


さてさて、話が長くなりましたが、何かしら3Dモデルのファイルを用意すれば、モデルの読み込みから描画まではDXライブラリが面倒を見てくれます。

次は「がん☆ぷろ!」でも使っているプロ生ちゃんのMMDモデルを読み込んで表示するサンプルコードです。
プロ生ちゃんのMMDモデルはこちらからDLできます。(宣伝)
#include "DxLib.as"

// ウィンドウモードをON(OFFの場合フルスクリーンになる)
ChangeWindowMode 1

// DxLibで描画するウィンドウをHSPのウィンドウに変更
SetUserWindow hwnd
SetUserWindowMessageProcessDXLibFlag 0

// DxLibの初期化
DxLib_Init
if ( stat == -1 ) : dialog "初期化エラー"

// モデルの読み込み
MV1LoadModel "MMD/プロ生ちゃん.pmx"
modelHandle = stat
if ( modelHandle == -1 ) : dialog "モデル読み込み失敗" : end

MV1SetPosition modelHandle, 0.0, -10.0, 0.0
MV1SetScale modelHandle, 1.0, 1.0, 1.0

repeat
// 裏画面を描画ターゲットに指定
SetDrawScreen DX_SCREEN_BACK
ClearDrawScreen 0
SetUseZBuffer3D 1
SetWriteZBuffer3D 1

// 3D描画用のカメラ設定
SetCameraPositionAndTargetAndUpVec cos(deg2rad(cnt))*40.0, 10.0, sin(deg2rad(cnt))*40.0/* 位置 */, 0.0, 0.0, 0.0/* 注視点 */, 0.0, 1.0, 0.0/* 鉛直上 */
SetCameraNearFar 1.0, 100.0// ニア・ファークリップ
SetupCamera_Perspective deg2rad(60.0)// 画角

// 3Dモデルを描画
MV1DrawModel modelHandle

// 裏画面を表画面へ転送
ScreenFlip

// ウィンドウメッセージ等の処理(DxLib側)
ProcessMessage

// ウィンドウメッセージ等の処理(HSP側)
await 0
loop
stop

特に注意するべきことはありません。
モデルを生成したらハンドルが貰えて、ハンドル経由で位置や回転、スケールの設定ができ、描画も3Dプリミティブと同様一命令で行える、というぐらいですかね。

上記を実行してみると…
gunpro_mmdmodel_draw_sample.png

無事表示されました、ヤッター!

モデルの回転

モデルの回転設定は3D用の行列計算になります。
(3Dなので回転のみだと3x3ですが普通移動も扱うので3x4または4x4の行列が一般的)

行列に関する演算は結構辛いのでライブラリでユーティリティが用意されているものです。
例に漏れずDXライブラリも用意されています。
…が、正直HSPから構造体として扱うのは高度な知識が必要となります。

真面目に行列計算するのを避ける場合、DXライブラリでモデルの回転を設定する一つの方法としてオイラー角が使えます。
MV1SetRotationXYZ モデルハンドル, ピッチ(X軸回転), ヨー(Y軸回転), ロール(Z軸回転)

3Dでも大抵のフィールド動き回る系はこれで事足りることが多い(基本ヨー回転しか使わないので)のではないでしょうか。

また、ちょっと変則的な姿勢を設定したい場合は次の関数が汎用的で便利です。
MV1SetRotationZYAxis モデルハンドル, モデルのZベクトル, モデルのYベクトル, ロール回転(モデルのZ軸回転)

ZベクトルとYベクトルだけ計算すればいいので煩雑なのが要らなくて楽です。
ちなみに「がん☆ぷろ!」の機体はそれなりの自由度で回転するので姿勢指定にはこれを使ってます。

「いや、行列とか自分で計算してるから」って猛者の方用にこういうのもあります。
私個人は取り回し難しい気がするのであまりお勧めしませんが。
MV1SetRotationMatrix モデルハンドル, 回転行列


ここではサンプルとしてオイラー角を使ってモデル回転をしてみましょう。
#include "DxLib.as"

// ウィンドウモードをON(OFFの場合フルスクリーンになる)
ChangeWindowMode 1

// DxLibで描画するウィンドウをHSPのウィンドウに変更
SetUserWindow hwnd
SetUserWindowMessageProcessDXLibFlag 0

// DxLibの初期化
DxLib_Init
if ( stat == -1 ) : dialog "初期化エラー"

// モデルの読み込み
MV1LoadModel "MMD/プロ生ちゃん.pmx"
modelHandle = stat
if ( modelHandle == -1 ) : dialog "モデル読み込み失敗" : end

MV1SetPosition modelHandle, 0.0, -10.0, 0.0
MV1SetScale modelHandle, 1.0, 1.0, 1.0

repeat
// 裏画面を描画ターゲットに指定
SetDrawScreen DX_SCREEN_BACK
ClearDrawScreen 0
SetUseZBuffer3D 1
SetWriteZBuffer3D 1

// 3D描画用のカメラ設定
SetCameraPositionAndTargetAndUpVec cos(deg2rad(cnt))*40.0, 10.0, sin(deg2rad(cnt))*40.0/* 位置 */, 0.0, 0.0, 0.0/* 注視点 */, 0.0, 1.0, 0.0/* 鉛直上 */
SetCameraNearFar 1.0, 100.0// ニア・ファークリップ
SetupCamera_Perspective deg2rad(60.0)// 画角

// 回転計算
cntRate = double(cnt) * 0.1
MV1SetRotationXYZ modelHandle, deg2rad(cntRate*1.0), deg2rad(cntRate*2.0), deg2rad(cntRate*3.0)

// 3Dモデルを描画
MV1DrawModel modelHandle

// 裏画面を表画面へ転送
ScreenFlip

// ウィンドウメッセージ等の処理(DxLib側)
ProcessMessage

// ウィンドウメッセージ等の処理(HSP側)
await 0
loop
stop

gunpro_modeldraw_rotate_sample.png

モデル回転の中心はモデル原点と一致するので、その辺は色々やるなら色々処理が必要になります。

3Dモデルアニメーション

3Dモデル表示まではかなり素直だったかと思いますが、アニメーションは結構厄介です。
そもそもどこからアニメーションデータを持ってくるか、という問題もありますが、そこは一旦置いておいて…。

DXライブラリではアニメーションデータは基本的にモデルファイルに含まれていると仮定しており、API的には

1. モデルデータに入っているアニメーションをモデルにアタッチする(再生可能なデータとして適用して再生準備する)
2. アタッチしたアニメーションのフレーム数とブレンド率を設定する
3. あとは描画するだけでにアニメーションが適用された状態で描画される

という流れになります。

論よりサンプルコード。
アニメーションデータはMMDの場合VMDということでほぼ統一されているので、本当にどんなデータでもいいんですが、私はVPVP Wiki モーションデータから「MMDでぽっぴっぽー(一番なんとか)」をお借りしてきました(特に深い意図はないです)。

MMDとそのアニメーションをDXライブラリで扱う際は命名規則が決まっており、
「<MMDのファイル名>000.vmd」というファイルがあったら自動でモデルに引っ付いたアニメーションデータとして読み込まれるという仕様になっています。
(また、ループアニメーションの場合はL、重力設定がある場合はGという接尾辞を付けるなどの命名規則もあります)

ということで、ダウンロードしてきた.vmdファイル名を「.pmxのファイル名+000.vmd」にして、.pmxと同じディレクトリに置き、次のサンプルコードを実行してみましょう。
#include "DxLib.as"

// ウィンドウモードをON(OFFの場合フルスクリーンになる)
ChangeWindowMode 1

// DxLibで描画するウィンドウをHSPのウィンドウに変更
SetUserWindow hwnd
SetUserWindowMessageProcessDXLibFlag 0

// DxLibの初期化
DxLib_Init
if ( stat == -1 ) : dialog "初期化エラー"

// モデルの読み込み
MV1LoadModel "MMD/プロ生ちゃん.pmx"
modelHandle = stat
if ( modelHandle == -1 ) : dialog "モデル読み込み失敗" : end

MV1SetPosition modelHandle, 0.0, -10.0, 0.0
MV1SetScale modelHandle, 1.0, 1.0, 1.0

// アニメーションのアタッチ
MV1AttachAnim modelHandle, 0, modelHandle, 1
animHandle = stat
if ( animHandle == -1 ) : dialog "アニメーション読み込み失敗" : end

animFrame = 0.0

repeat
// 裏画面を描画ターゲットに指定
SetDrawScreen DX_SCREEN_BACK
ClearDrawScreen 0
SetUseZBuffer3D 1
SetWriteZBuffer3D 1

// 3D描画用のカメラ設定
SetCameraPositionAndTargetAndUpVec cos(deg2rad(cnt))*40.0, 10.0, sin(deg2rad(cnt))*40.0/* 位置 */, 0.0, 0.0, 0.0/* 注視点 */, 0.0, 1.0, 0.0/* 鉛直上 */
SetCameraNearFar 1.0, 100.0// ニア・ファークリップ
SetupCamera_Perspective deg2rad(60.0)// 画角

// アニメーション更新&適用
MV1SetAttachAnimTime modelHandle, animHandle, animFrame
MV1SetAttachAnimBlendRate modelHandle, animHandle, 1.0
animFrame += 0.2// アニメーションのフレームレートに対して適切に変えること

// 3Dモデルを描画
MV1DrawModel modelHandle

// 裏画面を表画面へ転送
ScreenFlip

// ウィンドウメッセージ等の処理(DxLib側)
ProcessMessage

// ウィンドウメッセージ等の処理(HSP側)
await 0
loop
stop

.vmdのサイズにもよりますが読み込み時IK計算のためアニメーションデータの読み込みとかにかなり時間がかかります、じっと1分くらい耐えた後…。
gunpro_mmdanim_sample.png

アニメーションできました、ヤッター!

一点注意点としては、DXライブラリとしては「このアニメーションのこのフレームの姿勢をモデルに適用する」というAPIしかないので、「アニメーションを再生する(フレームを進める)」処理は自前で行う必要がある、というところでしょうか。
ちなみに、ループ再生させる場合は自前でフレームをループさせる必要があります。

ブレンド率も設定するコードを書いておいたので、複数のアニメーションの切り替えをスムーズにする処理はこのブレンド率をクロスフェードさせることで実現できます。
なお、ブレンドしながらクロスフェードさせる場合も、それぞれのアニメーションのフレームを進める処理は自前で書く必要があります。

HSPでfloatの戻り値を受け取る

しれっと書きましたが、前節で触れたアニメーションのフレーム処理を自前で行う際、ループさせる場合はアニメーションの総フレームで剰余をとるという処理が必要になります。
さてさて、この「アニメーションの総フレーム」ですが、DXライブラリ的には
float MV1GetAnimTotalTime( int MHandle, int AnimIndex ) ;

という関数で取得できます。

…戻り値がfloatなんですよね。
戻り値がfloatの場合、関数の呼び出し規約云々もあるんですが、通常のintなどとは違いちょっと特殊な返され方をします。
ということで、その辺を適切に処理しなければならないのですが、すでに先人様方がやってくださっているものがあるので、それを使うと実現できます。

私個人はHSP掲示板のこれをお借りしました。

というわけでこれらを使ったアニメーションループのサンプルコードは次。
// floatの戻り値を取得するモジュール from http://hsp.tv/play/pforum.php?mode=pastwch&num=9727 YOYOさん作
#module
#uselib "kernel32.dll"
#func VirtualProtect "VirtualProtect" int, int, int, int
#deffunc getdouble
if code == 0 {
code=$0424448b,$04c218dd,$00000000
VirtualProtect varptr(code), length(code)*4, $40, varptr(res)
fret=0.0
}
prm = varptr(fret)
res = callfunc(prm, varptr(code), 1)
return fret
#global


#include "DxLib.as"

// ウィンドウモードをON(OFFの場合フルスクリーンになる)
ChangeWindowMode 1

// DxLibで描画するウィンドウをHSPのウィンドウに変更
SetUserWindow hwnd
SetUserWindowMessageProcessDXLibFlag 0

// DxLibの初期化
DxLib_Init
if ( stat == -1 ) : dialog "初期化エラー"

// モデルの読み込み
MV1LoadModel "MMD/プロ生ちゃん.pmx"
modelHandle = stat
if ( modelHandle == -1 ) : dialog "モデル読み込み失敗" : end

MV1SetPosition modelHandle, 0.0, -10.0, 0.0
MV1SetScale modelHandle, 1.0, 1.0, 1.0

// アニメーションのアタッチ
MV1AttachAnim modelHandle, 0, modelHandle, 1
animHandle = stat
if ( animHandle == -1 ) : dialog "アニメーション読み込み失敗" : end

animFrame = 0.0
MV1GetAnimTotalTime modelHandle, animHandle
getdouble : animTotalFrame = refdval

repeat
// 裏画面を描画ターゲットに指定
SetDrawScreen DX_SCREEN_BACK
ClearDrawScreen 0
SetUseZBuffer3D 1
SetWriteZBuffer3D 1

// 3D描画用のカメラ設定
SetCameraPositionAndTargetAndUpVec cos(deg2rad(cnt))*40.0, 10.0, sin(deg2rad(cnt))*40.0/* 位置 */, 0.0, 0.0, 0.0/* 注視点 */, 0.0, 1.0, 0.0/* 鉛直上 */
SetCameraNearFar 1.0, 100.0// ニア・ファークリップ
SetupCamera_Perspective deg2rad(60.0)// 画角

// アニメーション更新&適用
MV1SetAttachAnimTime modelHandle, animHandle, animFrame
MV1SetAttachAnimBlendRate modelHandle, animHandle, 1.0
animFrame += 0.2// アニメーションのフレームレートに対して適切に変えること

// ループ処理
if ( animFrame >= animTotalFrame ) {
animFrame -= animTotalFrame
}

// 3Dモデルを描画
MV1DrawModel modelHandle

// フレーム情報を描画
DrawString 0, 0, strf("animFrame:%.2f, totalFrame:%.2f", animFrame, animTotalFrame), 0xffffffff

// 裏画面を表画面へ転送
ScreenFlip

// ウィンドウメッセージ等の処理(DxLib側)
ProcessMessage

// ウィンドウメッセージ等の処理(HSP側)
await 0
loop
stop


3Dモデルのマテリアル設定とmv1ファイルのススメ

ここまででモデルを表示~アニメーションの適用まで駆け足で説明してきました。
いくつかの形式に対応していることも説明しましたが、DXライブラリでモデルを扱う場合は独自形式の.mv1がオススメです。

というのも、.mv1は

・サイズが小さい
・読み込みが早い
・DXライブラリ付属ツールの「DxLibModelViewer」でプレビュー・変換・保存できる

という3点セットがついてるからです。

特に最後のライブラリ付属ツール「DxLibModelViewer」では、読み込んだ.xモデル、.mqoモデル、.pmxモデルそれぞれでアニメーションの確認や、マテリアルの設定が変更して.mv1として保存できます。
ツール自体がDXライブラリの描画と互換性があるので、描画結果を確認しながら調整できるという利点は大きいでしょう。

gunpro_dxlibmodelviewer_materialSample.png
上服のマテリアルを変更して拡散光を赤っぽくしたり。

gunpro_dxlibmodelviewer_sample.png
モデルに入っているモーフ(シェイプアニメ)の確認も出来たり。

DxLibModelViewerはDXライブラリの付属ツールなので、C++版のDXライブラリが必要になります。
HSP+DXライブラリでツールも使いたい場合、C#版のDXライブラリに加えてC++版のDXライブラリもダウンロードしてくる必要があったりします。

プログラムから生成したポリゴンモデルの描画

さてさて、3Dモデルの話も変わって今度はプログラムから生成したポリゴンをどうやって描画するかについて。
予めデータとして作ったモデルの描画基本については前節まででほぼ説明し尽くしたと思っているので、これは発展的な話題です。

「がん☆ぷろ!」では後述しますが地形生成がプロシージャルなので、プログラムから生成して描画するってことをやっています。
(自分で言うのもなんですが、「がん☆ぷろ!」は都合よくこういった実装が必要になる絶妙なサンプルだったんだなって思いました…)

その時にどうやって切り抜けたか(実装したか)という話になりますが、構造体とHSPでの変数メモリレイアウトに関する知識が必要になりますので少し難しめかも。

任意のポリゴンをDXライブラリで描画する場合

3Dモデルに相当する部分を自分で用意して専用の関数を呼べばOKです。
3Dモデルに相当する部分というのは具体的には頂点データ(と場合によっては頂点インデックス)です、マテリアルほど細かい設定は出来ません。

使用するDXライブラリの関数は次。
int DrawPolygon3D( VERTEX3D *Vertex, int PolygonNum, int GrHandle, int TransFlag );

VERTEX3D *Vertex : 三角形ポリゴンを形成する頂点配列のアドレス
int PolygonNum : 描画する三角形ポリゴンの数
int GrHandle : 描画するポリゴンに貼り付ける画像のハンドル( 画像を張らない場合は DX_NONE_GRAPH )
int TransFlag : 画像の透明度を有効にするかどうか( TRUE:有効 FALSE:無効 )

公式関数リファレンスより。

さて、構造体VERTEX3Dは次のように定義されます。
// 3D描画に使用する頂点データ型
struct VERTEX3D {
// 座標
VECTOR pos ;

// 法線
VECTOR norm ;

// ディフューズカラー
COLOR_U8 dif ;

// スペキュラカラー
COLOR_U8 spc ;

// テクスチャ座標
float u, v ;

// サブテクスチャ座標
float su, sv ;
} ;

公式関数リファレンスより。

ということで、この構造体を何とかHSPで用意してそのポインタを渡せば実現できそうです。

HSPでfloatを扱う

ところで、構造体のメンバにはfloatが含まれています。
(VECTOR型はfloat3つ、COLOR_U8はDWORDと同じで、1バイトごとに0~255の輝度値が入った値です)

HSP標準ではAPIに渡す引数としてfloatが使えますが、値としてfloatを扱うことができません。
プラグインを使うのも一つの手ですが、単に値として詰められればいいので、floatの値の32ビット表現が得られればそれで十分だったりします。

そして、doubleからfloatへの変換は実は結構簡単だったりします。
難しい話はすっ飛ばして、今回はsprocketさんの小さなねた3で紹介されているコードを使いました、スゲー!

HSPの変数メモリレイアウト

さて、いざ構造体を作る! 前に、HSPの変数メモリレイアウトも理解しておかないと躓きます。
といっても、「直観的に想像するものとは逆」とだけ覚えておけばいいんですけどね。

例えば、2次元配列を用意したとき、各要素の先頭からのアドレスオフセットを確認してみましょう。

    dim a, 4, 4
text = ""
repeat length(a) : gcnt=cnt : repeat length2(a)
text += strf("a(%d, %d) : offset %+02d\n", gcnt, cnt, varptr(a(gcnt, cnt))-varptr(a(0, 0)))
loop : loop
mesbox text, 640, 480


結果はこんな感じ。
a(0, 0) : offset +0
a(0, 1) : offset +16
a(0, 2) : offset +32
a(0, 3) : offset +48
a(1, 0) : offset +4
a(1, 1) : offset +20
a(1, 2) : offset +36
a(1, 3) : offset +52
a(2, 0) : offset +8
a(2, 1) : offset +24
a(2, 2) : offset +40
a(2, 3) : offset +56
a(3, 0) : offset +12
a(3, 1) : offset +28
a(3, 2) : offset +44
a(3, 3) : offset +60

ということで、a(0, 0)の次にくるのはa(0, 1)ではなくa(1, 0)です。
つまり、VERTEX3D構造体(32ビットメンバが12個)を3頂点分用意する場合、「dim vertex, 3, 12」ではなく「dim vertex, 12, 3」とし、メンバの値の設定もその通りに行う必要があります。

HSPから生成したポリゴンをDXライブラリに描画してもらう

さて、ここまでで道具は揃いました、あとはコードを組むだけ。
都合のいいことにfloatは32ビット、COLOR_U8も32ビットなので、VERTEX3D構造体はdimの配列で素直にアクセスできます。

それを利用して、下記のようなコードで実装が可能です。
サンプルとして与える頂点データは、正三角形となるデータにしています。
#module
/*
sprocketさん作 floatへの変換関数
http://spn.php.xdomain.jp/hsp_koneta3.htm#tofloat
*/
#defcfunc tofloat double p1
temp = p1
return lpeek(temp)>>29&7|(p1<0)<<31|lpeek(temp,4)-(p1!0)*0x38000000<<3
#global


#include "DxLib.as"

// ウィンドウモードをON(OFFの場合フルスクリーンになる)
ChangeWindowMode 1

// DxLibで描画するウィンドウをHSPのウィンドウに変更
SetUserWindow hwnd
SetUserWindowMessageProcessDXLibFlag 0

// DxLibの初期化
DxLib_Init
if ( stat == -1 ) : dialog "初期化エラー"

// HSPからVERTEX3Dを用意
dim polygonVertex, 12, 3

repeat 3// 正三角形
rad = deg2rad(360*cnt/3)
polygonVertex(0, cnt) = tofloat(20.0*cos(rad))// 位置X
polygonVertex(1, cnt) = tofloat(0.0)// 位置Y
polygonVertex(2, cnt) = tofloat(20.0*sin(rad))// 位置Z
polygonVertex(3, cnt) = tofloat(0.0)// 法線X
polygonVertex(4, cnt) = tofloat(1.0)// 法線Y
polygonVertex(5, cnt) = tofloat(0.0)// 法線Z
hsvcolor 180*cnt/3, 192, 255
polygonVertex(6, cnt) = (0xff00000000 | (ginfo_r<<16) | (ginfo_g<<8) | (ginfo_b))// ディフューズ
polygonVertex(7, cnt) = 0xffffffff// スペキュラ
loop

repeat
// 裏画面を描画ターゲットに指定
SetDrawScreen DX_SCREEN_BACK
ClearDrawScreen 0
SetUseZBuffer3D 1
SetWriteZBuffer3D 1

// 3D描画用のカメラ設定
SetCameraPositionAndTargetAndUpVec cos(deg2rad(cnt))*40.0, 10.0, sin(deg2rad(cnt))*40.0/* 位置 */, 0.0, 0.0, 0.0/* 注視点 */, 0.0, 1.0, 0.0/* 鉛直上 */
SetCameraNearFar 1.0, 100.0// ニア・ファークリップ
SetupCamera_Perspective deg2rad(60.0)// 画角

// 3Dプリミティブを描画
DrawPolygon3D varptr(polygonVertex), 1, DX_NONE_GRAPH, 0

// 裏画面を表画面へ転送
ScreenFlip

// ウィンドウメッセージ等の処理(DxLib側)
ProcessMessage

// ウィンドウメッセージ等の処理(HSP側)
await 0
loop
stop

これを実行してみると…
gunpro_polygon3d_sample.png

無事正三角形のポリゴンがでました、ヤッター!

シャドウ

シェーディングと何が違うの、という人はシェード(Shade:陰)とシャドウ(Shadow:影)の違いなので調べてみるといいかもしれません。

DXライブラリでは標準でデプスシャドウマップ方式のものが採用されています。
デプスシャドウマップ方式は原理がそんなに難しくないです。
詳しい説明はゲームつくろー!さんの「その46 深度バッファシャドウの根っこ:影を描画してみる」が素晴らしいのでそちらをどうぞ。

簡単に概要だけ書くと、各描画ピクセルについて「シャドウ(ライト)の方向から見た時に一番手前にあるかどうかを判定する」ような手法です。
なので半透明オブジェクトを含んでいる場合は素直に適用できない手法ですが、派生手法も合わせると非常によく使われています。

さて、3Dモデルの描画の話をいったん挟んでのシャドウですが、それにはちょっとした理由がありまして…実は最初に使っていた3Dプリミティブではシャドウマップを生成できないのです…。
正確にはMV1で描画するモデル系しかシャドウマップに含めることができないみたいです。
(モデル系のシェーダにしかシャドウマップを描画する設定が存在しないようで)

現在のDXライブラリの仕様なのでこれは仕方ないですね。

なお、デプスシャドウマップの適用は流れ自体は殆ど難しくありません。

1. シャドウマップを生成してハンドルをもらう
2. シャドウマップに深度情報を書き込む(3Dモデルの描画)
3. シャドウマップを有効にして3Dモデルを描画する

2.と3.で2回3Dモデルを描画します。
シャドウマップの内容が変化しない場合は2.の工程は最初に一回やるだけで十分です。

論よりサンプル、ということでモデルにセルフシャドウを落とすサンプル。
…と思ってプロ生ちゃんのモデルでやってみたんですが、うまくシャドウが落ちなかったのでDXライブラリ(C++版)付属のDxChara.xを使っています。
(トゥーンを使っているか、スフィアマップあたりが怪しい気がしてますが、とりあえず原因はよく分からんです。
 よくよく考えたらこれ「がん☆ぷろ!」でも起こっているっぽいんですが、作っている時に私が気づかなかったので調べてません、ガハハ!w)

#define USE_DXCHARA_MODEL    (1)

#include "DxLib.as"

// ウィンドウモードをON(OFFの場合フルスクリーンになる)
ChangeWindowMode 1

// DxLibで描画するウィンドウをHSPのウィンドウに変更
SetUserWindow hwnd
SetUserWindowMessageProcessDXLibFlag 0

// DxLibの初期化
DxLib_Init
if ( stat == -1 ) : dialog "初期化エラー"

// モデルの読み込み
#if USE_DXCHARA_MODEL
MV1LoadModel "dxchara/DxChara.x"
#else
MV1LoadModel "MMD/プロ生ちゃん.pmx"
#endif
modelHandle = stat
if ( modelHandle == -1 ) : dialog "モデル読み込み失敗" : end

MV1SetPosition modelHandle, 0.0, -10.0, 0.0
#if USE_DXCHARA_MODEL
MV1SetScale modelHandle, 0.03, 0.03, 0.03
#else
MV1SetScale modelHandle, 1.0, 1.0, 1.0
#endif

// シャドウマップの生成
MakeShadowMap 1024, 1024
shadowMapHandle = stat
if ( shadowMapHandle == -1 ) : dialog "シャドウマップ生成に失敗" : end

repeat
// シャドウマップの更新
SetShadowMapLightDirection shadowMapHandle, cos(deg2rad(0.5*cnt)), -1.0, sin(deg2rad(0.5*cnt)) // シャドウ方向の設定
SetShadowMapDrawArea shadowMapHandle, -10.0, -10.0, -10.0,/* 最小位置 */ 10.0, 10.0, 10.0/* 最大位置 */ // シャドウマップを書き込む範囲
ShadowMap_DrawSetup shadowMapHandle
MV1DrawModel modelHandle// 遮蔽物となる3Dモデルの描画
ShadowMap_DrawEnd

// 裏画面を描画ターゲットに指定
SetDrawScreen DX_SCREEN_BACK
ClearDrawScreen 0
SetUseZBuffer3D 1
SetWriteZBuffer3D 1

// 3D描画用のカメラ設定
SetCameraPositionAndTargetAndUpVec 0.0, 10.0, -40.0/* 位置 */, 0.0, 0.0, 0.0/* 注視点 */, 0.0, 1.0, 0.0/* 鉛直上 */
SetCameraNearFar 1.0, 100.0// ニア・ファークリップ
SetupCamera_Perspective deg2rad(60.0)// 画角

// シャドウマップを使用する設定
SetUseShadowMap 0, shadowMapHandle

// 3Dモデルを描画
MV1DrawModel modelHandle

// シャドウマップを解除
SetUseShadowMap 0, -1

// シャドウマップの中身を表示
TestDrawShadowMap shadowMapHandle, 0, 0, 128, 128

// 裏画面を表画面へ転送
ScreenFlip

// ウィンドウメッセージ等の処理(DxLib側)
ProcessMessage

// ウィンドウメッセージ等の処理(HSP側)
await 0
loop
stop

gunpro_modelShadow_sample.png

以上で、3Dモデルに関する項目は終わり、お疲れさまでした。
補足できてないところ結構あると思いますが、あとはDXライブラリ本家の3D関係のリファレンスを参照してください。

ポストパスエフェクト

ポストパスエフェクトというのは、3D描画が終わった後の画面に対して画面全体に何らかのエフェクトを追加で適用する手法全般を指します。

代表的なものとしてはカラーグレーディングやブルーム、フィルムエフェクト、モーションブラー、SSAO(スクリーンスペースアンビエントオクルージョン)、被写界深度とかですかね。
HDRからLDRへのマッピングもポストエフェクトと捉えると、露出補正とかもここに含まれますね。

それぞれに対しての説明は割愛するとして、ポストパスエフェクトは適用すると画面全体に対して一貫した影響があり説得力が増すことが多いので効果が高いところです。

ポストパスエフェクト実装

ポストエフェクトの説明もほどほどに、HSP+DXライブラリでのポストパス実装について説明していきます。

ポストパスエフェクトって格好いい名前ついてますけど、やってることは実は画面全体を覆う2Dポリゴンを描画しているだけだったりします。
そのポリゴン描画時のシェーダをカスタムすることで上述のようなエフェクトを実現できるわけです。

DXライブラリで2Dポリゴンに対してシェーダを使った描画をする場合、
int SetUsePixelShader( int ShaderHandle ) ;
int DrawPolygon2DToShader( VERTEX2DSHADER *Vertex, int PolygonNum ) ;

あたりを使います、ToShaderがつくことで自分で設定したシェーダを使った描画に切り替わります。

自分で書いたシェーダをプログラムで使えるようにするには、予めシェーダをコンパイルしてバイナリを生成しておく必要があります。
シェーダの生成やコンパイルの仕方は、私が書くより先人様の分かり易い記事Qiita:DXライブラリでピクセルシェーダを使う その1を参考にした方がいいので省略。

簡単に要約だけ書いておくと、ピクセルシェーダのHLSLファイルを書いて、DXライブラリ付属ツールの「ShaderCompiler」に食わせてピクセルシェーダバイナリ(.pso)を生成し、それをLoadPixelShaderで読み込めば使えるようになります。

また、2Dのシェーダを使ったポリゴン描画時に使われる頂点の構造体は、実は3Dと違います。
使うのは次の構造体
struct VERTEX2DSHADER
{
VECTOR pos ; // スクリーン座標
float rhw ; // 同次 W の逆数、通常は 1.0f でOK
COLOR_U8 dif ; // ディフューズカラー
COLOR_U8 spc ; // スペキュラカラー
float u, v ; // テクスチャ座標0
float su, sv ; // テクスチャ座標1
} ;

公式関数リファレンスより。

以上をまとめて、HSPからは次のようなコードで実現できます。
まともなポストエフェクトの場合それなりにコードを書く必要があるので、今回はシンプルにするためにサンプル実装としてエッジ(っぽいもの)検出エフェクトです。
HSPから任意の3Dポリゴン描画の節でも書きましたが、HSPの変数メモリレイアウトに注意。

シェーダ:edgeDetect.psh
// ピクセルシェーダーの入力
struct PS_INPUT
{
float4 DiffuseColor : COLOR0 ;
float4 SpecularColor : COLOR1 ;
float2 TextureCoord0 : TEXCOORD0 ;
float2 TextureCoord1 : TEXCOORD1 ;
} ;

// ピクセルシェーダーの出力
struct PS_OUTPUT
{
float4 Output : COLOR0 ;
} ;

// 描画するテクスチャ
sampler colorTexture : register( s0 ) ;

// 描画パラメータ
float4 resolution: register( c0 );// x=Width, y=Height, z=1/Width, w=1/Height

PS_OUTPUT main( PS_INPUT PSInput )//入力値
{
PS_OUTPUT PSOutput ;

// 厳密なエッジ検出ではなく、各カラーのXY差分の平均
float4 color = tex2D( colorTexture , PSInput.TextureCoord0 );
float4 right = tex2D( colorTexture , PSInput.TextureCoord0 +float2(resolution.z, 0.0) );
float4 down = tex2D( colorTexture , PSInput.TextureCoord0 +float2(0.0, resolution.w) );
float4 rd = tex2D( colorTexture , PSInput.TextureCoord0 +float2(resolution.z, resolution.w) );

float4 edgeColor = saturate( ( abs(right -color) + abs(down -color) + abs(rd -color) ) *0.3333333 );// 平均
edgeColor.a = 1.0;

PSOutput.Output = edgeColor;
return PSOutput;
}


HSP側のコード
#module
/*
sprocketさん作 floatへの変換関数
http://sprocket.babyblue.jp/html/hsp_koneta3.htm
*/
#defcfunc tofloat double p1
temp = p1
return lpeek(temp)>>29&7|(p1<0)<<31|lpeek(temp,4)-(p1!0)*0x38000000<<3
#global

#include "DxLib.as"

screen 0, 640, 480

// D3D9を使うように設定:シェーダのバージョンとそろえる
SetUseDirect3DVersion DX_DIRECT3D_9EX

// ウィンドウモードをON(OFFの場合フルスクリーンになる)
ChangeWindowMode 1

// DxLibで描画するウィンドウをHSPのウィンドウに変更
SetUserWindow hwnd
SetUserWindowMessageProcessDXLibFlag 0

// DxLibの初期化
DxLib_Init
if ( stat == -1 ) : dialog "初期化エラー"
SetDrawScreen DX_SCREEN_BACK

// モデルの読み込み
MV1LoadModel "MMD/プロ生ちゃん.pmx"
modelHandle = stat
if ( modelHandle == -1 ) : dialog "モデル読み込み失敗" : end

MV1SetPosition modelHandle, 0.0, -10.0, 0.0
MV1SetScale modelHandle, 1.0, 1.0, 1.0

// シェーダを読み込み
LoadPixelShader "edgeDetect.pso"
edgeDetectShader = stat
if ( edgeDetectShader == -1 ) : dialog "シェーダ読み込みエラー" : end

// 3D描画用スクリーンを生成
MakeScreen 640, 480, 0
screen3dHandle = stat
if ( screen3dHandle == -1 ) : dialog "スクリーン生成失敗" : end

// HSPからVERTEX2DSHADERを用意
dim polygonVertex2D, 10, 6

repeat 6
vidx = cnt -(cnt>=3)*2
polygonVertex2D(0, cnt) = tofloat(vidx/2 *640)// 座標X
polygonVertex2D(1, cnt) = tofloat(vidx\2 *480)// 位置Y
polygonVertex2D(2, cnt) = tofloat(0.0)// 位置Z
polygonVertex2D(3, cnt) = tofloat(1.0)// 位置W
polygonVertex2D(4, cnt) = 0xffffffff// ディフューズ
polygonVertex2D(5, cnt) = 0xffffffff// スペキュラ
polygonVertex2D(6, cnt) = tofloat(vidx/2)// テクスチャU
polygonVertex2D(7, cnt) = tofloat(vidx\2)// テクスチャV
polygonVertex2D(8, cnt) = tofloat(vidx/2)// サブテクスチャU
polygonVertex2D(9, cnt) = tofloat(vidx\2)// サブテクスチャV
loop

repeat
// 3Dスクリーンを描画ターゲットに設定
SetDrawScreen screen3dHandle
ClearDrawScreen 0
SetUseZBuffer3D 1
SetWriteZBuffer3D 1

// 3D描画用のカメラ設定
SetCameraPositionAndTargetAndUpVec cos(deg2rad(cnt))*40.0, 10.0, sin(deg2rad(cnt))*40.0/* 位置 */, 0.0, 0.0, 0.0/* 注視点 */, 0.0, 1.0, 0.0/* 鉛直上 */
SetCameraNearFar 1.0, 100.0// ニア・ファークリップ
SetupCamera_Perspective deg2rad(60.0)// 画角

// 回転計算
cntRate = double(cnt) * 0.1
MV1SetRotationXYZ modelHandle, deg2rad(cntRate*1.0), deg2rad(cntRate*2.0), deg2rad(cntRate*3.0)

// 3Dモデルを描画
MV1DrawModel modelHandle

// 裏画面を描画ターゲットに指定
SetDrawScreen DX_SCREEN_BACK
ClearDrawScreen 0

// シェーダをセット
SetUseTextureToShader 0, screen3dHandle
SetUsePixelShader edgeDetectShader

// シェーダに渡すパラメータのセット
SetPSConstF 0, 640.0, 480.0, 1.0 /640.0, 1.0 /480.0

// 2Dプリミティブを描画
SetDrawBlendMode DX_BLENDMODE_NOBLEND, 255
DrawPolygon2DToShader varptr(polygonVertex2D), 2

// 左上に元の画面を描画
DrawExtendGraph 0, 0, 160, 120, screen3dHandle, 0

// 裏画面を表画面へ転送
ScreenFlip

// ウィンドウメッセージ等の処理(DxLib側)
ProcessMessage

// ウィンドウメッセージ等の処理(HSP側)
await 0
loop
stop

gunpro_2dshader_postpassSample.png

エッジというか隣接ピクセルとの色差分をだしているだけなので、厳密なエッジではないです。
左上が元になった画像です、3Dの描画結果ですね

何気に「他の画面に描いた内容を別の画面に貼りつけ」という、所謂マルチパスレンダリングをしています。

DXライブラリはマルチパスレンダリングという用語はでてきてませんが、描画ターゲットの切り替えのことです。
少し凝ったポストエフェクトでは「縮小⇒変換⇒元の画面に適用」みたいなことするのでマルチパスレンダリングは必須となってくるでしょうし、ちょうどよく解説できたような気がします。
なお、同時に複数のスクリーンへレンダリングする所謂マルチレンダーターゲットはSetRenderTargetToShaderを使えばできるようです。
 シェーダを書き換えないと複数ターゲットへの同時出力が行えないので、独自シェーダを使った場合の専用処理になっているようですね

ということで、HSP+DXライブラリでポストエフェクト実装でした。
今回は単純にピクセルの色差分を実装したんですが、他にもポストエフェクトに該当するものは実装できるはずです。
そして、ポストエフェクトは簡単に画面全体の質が向上する手段なので(大体素直に適用すると重いという欠点はありますが)、これが理屈上は使えるというメリットは大きそうです。
「がん☆ぷろ!」でもポストパスエフェクトをサンプル実装していますが、その話は後述。


以上で、HSP+DXライブラリでの3D描画+α検証は大体説明終わりました。
後は軽い内容なのでサクサクいきます。

(自作ライブラリ)Effekseerプラグイン for HSPの簡易検証・テスト

今回のデモ「がん☆ぷろ!」を作ったきっかけというか戦犯です、

結局ライブラリって作ってコンテストに応募したとしても実際にどのレベルまで使えるのか確認できないと手を出しづらいよね、っていうのは痛いほど分かったので、「じゃあそういうの自分で作ります(半ギレ)」みたいなとこから始まりました。
ついでなのでmistの検証とかDXライブラリで3D描画の検証とかしますか、という事になってこの記事に至ります、裏話ですが。
実際、このデモを作る中で「この機能足りない」とか「ここバグってる」みたいなのに気づいて追加・修正することができましたし、検証としての意味合いを果たしています。

さて、Effekseerプラグインの使い方自体は私の前の記事「【HSP】【DXLib】【Effekseer】HSPからエフェクト再生ランタイム「Effekseer」を使えるプラグインを作った話」で説明しているので、ここで敢えて述べることは実はもうこれ以上ないのですが…。

折角なので今回の検証で追加した機能で、後から考えたらかなり必要だった! っていう機能を一つ説明したいと思います。

エミッタのRootだけ削除するefkStopRootの追加

ミサイルの煙とかがそうなんですが、「いつ消えるか分からないエフェクト」の制御のために必要でした。

通常、エフェクトは「決まった位置にだして、エフェクトが再生終わったら消滅する」という流れでいいのですが、ミサイルの煙などは「ミサイルがでている間はずっと出ているが、ミサイルが消えたら新しくパーティクルを放出するのをやめる」という処理にならないといけません。

ということで、こういった処理のために、「エミッタ(ルート)だけ消す」efkStopRootという関数が追加されました。
エミッタだけ消えるのでそれ以上パーティクルが生成されることもなく、パーティクルが寿命によりすべて消滅するとハンドル自体が無効になる、という仕組みです。
従来通り今でているパーティクルも含めエフェクト自体をばっさり消したい場合はefkStopを使えばOKです。

gunpro_missile_effectlive_sample.png
画面中央上のミサイルはまだ消滅していないのでエフェクトは出続ける
画面右下のミサイルは地面に衝突したのでこの時点で新規パーティクルの生成をやめ、爆発エフェクトは単純に再生する

という処理になっています。

(自作ライブラリ)mistの動作テスト

そのまんまですが、「がん☆ぷろ!」では自作ライブラリ「mist」の(beta版リリースのための)動作テストということで組み込みしました。
「mistってなんやねん」って言う方はこちらをご参照ください。
一言で書くと「HSPからHSPっぽいスクリプトを解析して実行してくれる」+α機能のプラグインです、+αの中にマルチスレッド実行とかがあります。

…で、組み込みしたんですが、「がん☆ぷろ!」ではステージ記述が思いの外何もなかった(ステージ毎の特殊なギミックとかを一個も作らなかった)ので、「がん☆ぷろ!」内ではマルチスレッドによる高速化に主に使っています。

しかも当初の予定と違って敵の数とか自機の弾の数とか高々2桁程度の数しかでないので、高速化に使うといってもそれは何ともな感じになってしまいました。(しかも実装前にそのことに気づいてしまったので結局やってないんですが)
アプリレベルでの組み込み・動作テストはうまくいったけど、機能を100%引き出せたかというと全くそうでない、という作者なのに情けない状況><;;

一番マルチスレッドにして頑張ってるのタイトルの背景にでてるパーティクルです(笑)。

gunpro_title_fsppParticle.png
これ、パーティクルの数っていくつでてるか分かりますか?
実はざっと2000個くらいでてます。マルチスレッドではこれを3分割して1スレッド650個程度のパーティクルを処理させています。

1パーティクルで必要な処理は「加速度・速度・位置・スケール更新」と「アルファ値更新、パーティクル生存判定」らへんです。
HSP本体で1スレッドで処理してもらうと大体700個くらいで1Fが16ms超えはじめます(筆者の開発環境では)。
ということで、3スレッド使ってHSP本体の大体3倍っていう理想的な高速化をしてあの画面ができています。

なお、あのタイトル画面のパーティクルはEffekseerを使えばネイティブコードで計算が走ってくれるので、この高速化は必要があったのか微妙という…。
…ともあれ、mistとしてはそこそこの規模のコードを食わせて実行してくれているので、次のリリースからようやくbeta版になります(予定)。

最新ではmistのラベルをHSP内でもラベルとして扱える機能を追加とかしてたりして親和性をあげていっていますので、興味がある方は是非上記リンクからどうぞ、できることできないこと書いてあります。
コンパイル時のエラー表示がまたエラく不親切ですが、そこは綺麗にしていってるところなので気長にお待ちいただければと思います。

「がん☆ぷろ!」デモ固有で工夫したところ

ライブラリの検証とかその辺の話は上述までで大体説明が終わりました。

ここからは「がん☆ぷろ!」を作る上で「この辺工夫しました」の話になります。
DXライブラリの3D描画の話も重めでしたが、それらを踏まえた上で「更にどんなことができるのか」といった問題となってくるので、ここもちょっと重めな話も混じってます。

地形

「がん☆ぷろ!」的には画面上の専有面積も多い地形は1大トピックです。
シャドウの項でも少しだけ触れましたが地形だけ専用シェーダだし、最終ミッション(ミッション6)までたどり着いている方は気づいたかもしれませんが実はランダム生成だし、っていう豪華さです。

動的生成

@2017/08/08
パーリンノイズではなくバリューノイズでした、勘違いしていたので修正

若干ネタばれしましたが地形は動的生成です。
手法としてはFractionalBrownianMotion(非整数ブラウン運動)、所謂バリューノイズを高さマップとして見て地形化している感じです。
(実際問題バリューノイズである必要性はないんですし、「地形生成ではパーリンノイズ」というのが定番なのですが、実装の簡単さから何も考えずバリューノイズ使ってます。)
生成するバリューノイズ画像は64x64で、つまり地形は64x64個の頂点数からできています。
このサイズは大きくすればするほど複雑な地形が作れるのですが、HSPで処理して現実的にストレスが溜まらない時間で処理させるとなると64x64ぐらいが最大サイズでした。

点(x,y)での高さを求める関数fbmは次の通り。
#defcfunc floorf double x
if ( x < 0.0 ) : return double(int(x)-1)
return double(int(x))

#defcfunc fractf double x
if ( x >= 0.0 ) : return x - int(x)
return x - int(x) +1.0

#defcfunc blockNoise2d double x, double y, local res
res = sin(x*91.4231 + y*38.544) * 73251.88924163
res = fractf(res)
return res

#defcfunc linearNoise2d double x, double y, local res, local tfx, local tfy, local tlt, local trt, local tld, local trd
tfx = floorf(x) : tfy = floorf(y)
tlt = blockNoise2d(tfx, tfy)
trt = blockNoise2d(tfx+1.0, tfy)
tld = blockNoise2d(tfx, tfy+1.0)
trd = blockNoise2d(tfx+1.0, tfy+1.0)
tfx = fractf(x) : tfy = fractf(y)
tlt = tlt*(1.0-tfx) + trt*tfx
tld = tld*(1.0-tfx) + trd*tfx
res = ( tlt*(1.0-tfy) + tld*tfy )
return res

#defcfunc fbm double x, double y, int octave, double freqMul, double ampMul, double rotXX, double rotXY, double rotYX, double rotYY, local res, local nmul, local tx, local ty, local txt
res = 0.0
nmul = 0.5
tx = x : ty = y
repeat octave
res += linearNoise2d( tx, ty ) * nmul
// 回転項
txt = tx
tx = ( txt*rotXX + ty*rotXY ) *freqMul
ty = ( txt*rotYX + ty*rotYY ) *freqMul
// サイズ項
nmul *= ampMul *0.5
loop
return res

FractionalBrownianMotionという大層な名前がついていますが、要するに異なる周波数・大きさの波を、いい感じに加算して最終的な波にする、というだけですね。

fbm関数的にはオクターブ数とか、オクターブ毎の周波数係数、影響度係数、サンプル座標の回転が入れられるようにしてあります。
最後の処理はより長い周期でバリューノイズのバリエーションをだせるように入っているだけなので、経験的になくても十分な品質のバリューノイズは得られます。

バリューノイズでは元になるノイズが別に必要なんですが、今回は入力値から決定的に得られる高周波数ノイズとしてGLSLでよくでてくるノイズ関数を使いました、実装もとてもお手軽です。
「がん☆ぷろ!」ではそんなに高周波数ノイズはいらないので適当に希釈して使っています。
(一番低い周波数が5~6ピクセル毎に一周する程度の周波数です)

gunpro_perlineNoize_ground.png
左上にでているのが高さマップの元になったバリューノイズの画像です。

シェーディングに必要になる頂点法線は頂点全ての高さが計算し終わってから、隣接頂点の高さを使ってそれっぽいのを求めています。
下図はViewNormal(カメラから見た法線の3次元方向)を可視化したもの。

gunpro_field_viewNormal.png
実はちゃんと計算あってるか確認してないんですが…多分大体あってるんじゃない…かなぁ…?

あと、実はこのバリューノイズ、CPUで生成した後GPUが読めるようにテクスチャも生成して、地形ポリゴンに貼り付けています。
地形ポリゴンはそもそも頂点カラーでざっくり色付けしてはいるんですけど、それだけだと頂点間がリニアに色補完されてしまいのっぺりした印象になってしまうので、ちょっとしたざらつきを足すためにノイズとして入れています。
地形高さとしてのノイズと純粋な色むらとしてのノイズと2回お得なバリューノイズ仕様。

やる気がある人はバンプマッピングとかディスプレースメントマッピングを使えばよりギザギザした地形が作れると思います、私は(実装重かったのと技術力が足らなかったので)やらなかったけど。

専用シェーダ

「がん☆ぷろ!」の地形は主にシャドウ関連で色々弄っていて、専用シェーダになっています。
専用シェーダと言ってもフルスクラッチで書いているわけではなく、DXライブラリに内蔵されているモデル描画のためのシェーダを参考にしています。

シャドウ

「がん☆ぷろ!」ではシャドウはちょっと面倒なことしてて、地形のセルフシャドウ、プレイヤーから地形へのシャドウの2つがあります。
プレイヤー自身のセルフシャドウ、敵とかバルーンの地形へのシャドウはありません。
gunpro_shadow_fieldAndPlayer.png

プレイヤー自身のセルフシャドウは単純にデプスシャドウマップ方式を素直に適用すると解像度の問題でジャギが汚くなるからで、シェーディングとAO(たぶんモデルの項で後述)で十分暗い面はそれっぽく見えるなということで外しました。
敵とかバルーンの地形へのシャドウもシャドウマップの解像度の確保の問題で外してあります、シャドウマップのカスケード処理とか実装すれば大きな問題はないんですけどね、実装が重かったのと今回はそこまでは必要ないと判断したので見送り。

ただ、地形のシャドウはかなりジャギが強くでてしまうという問題があったので、一般的によく使われるVarianceShadowMapではなく若干マイナーな、expを使って誤魔化す手法を適用しています。
VarianceShadowMapはよく使われていて統計的に綺麗なソフトエッジになるのが特徴ですが、シャドウマップ自体に仕込みが必要なので今回はやらなかった(できなかった)、というのが正しいですが。
(ただ、DXライブラリのソースを眺めていた感じ若干それっぽいコードがあったので、今実験中でいつか対応されるのかもしれませんね:未確認情報です、ご注意を)

今回適用した手法はの実装はかなり単純で、シャドウマップで引いてきた深度とポリゴン上の深度差を定数倍してexp関数に突っ込むだけです、コードはこんな感じ。
// テクスチャに記録されている深度( +補正値 )よりZ値が大きかったら奥にあるということで減衰率を最大にする
//ShadowRate.x = smoothstep( PSInput.ShadowCoord0.z - cfShadowMap1_DAdj_Grad_Enbl0_1.y, PSInput.ShadowCoord0.z, TextureDepth.r + cfShadowMap1_DAdj_Grad_Enbl0_1.x ) ;

// 通常のデプスシャドウが smoothstep でジャギつくので軽くソフトな exp で処理
ShadowRate.x = saturate( exp( 20.0 * ( TextureDepth.r - PSInput.ShadowCoord0.z ) ) );

「20.0」はシャドウを適用する際の定数で、大きいほどハードなシャドウに、小さいほどソフトなシャドウになります。
ただ、小さすぎるとセルフシャドウが落ちないなどのエラーがあるので(元々そういうトリックを使った手法らしいので)、あんまり小さすぎない方がいいかも。

このシャドウが実際どんな感じで違うかというと次。

gunpro_fieldShadow_normalShadow.pnggunpro_fieldShadow_expShadow.png
左が普通のシャドウ(DXライブラリの)、右が今回のexpでごまかすシャドウです。

エッジに少しぼかしが入っているのと、地形のすぐ後ろにかかるセルフシャドウがかなり弱くなっていることが確認できるかと思います。

あと、DXライブラリでシャドウマップを生成する際、実はMV1から始まるモデル描画じゃないと正しく値が書き込まれず、DrawPolygon3Dなどプリミティブは遮蔽物としてシャドウマップに書き込めないということは結構前に述べましたが(シェーダが違うので)、それも専用シェーダで何とかしています。

DXライブラリで使用しているシャドウマップテクスチャはRチャンネルのみの16ビット深度を持つフォーマットで、値としてはシャドウAABBのNear/Farで正規化済み線形デプスが入っています。
なので、専用シェーダではこの正規化済み線形デプスを書き出すピクセルシェーダに書き換えればいいことが分かります。
…まぁ、「何言ってるんだ?」って思う人もおられると思うので、ちょこちょこ補足。

Rチャンネルのみっていうのは分かると思いますが、16ビット深度は「1チャンネルに割り当てられているビット数が16」と同じ意味です。
(「深度」という単語はこの場合の「色深度」って意味合いと、距離すなわちZ値という意味での深度と2つあります、ややこしいですよね
 だから、ややこしく言うなら「16ビット深度の深度バッファ」も正しい、という…)

正規化済み線形デプスはそのまま、例えばデプスがNearと同じだったら0.0、Farと同じだったら1.0、NearとFarのちょうど中間だったら0.5になるようなデプスを指します。
実は通常の3D描画で使われるデプスは近距離のものほど高いレンジが割り当てられる非線形デプスが一般的なのですが(大体、近距離10%くらいでデプス値の90%くらいを消費するような投影が一般的です)、シャドウマップにおいては線形のデプスにしないと特に遠景の物体について均一な品質にならないので線形デプスが使われることもあります。
DXライブラリは線形デプスを使っているわけですね。

ちょっとサンプルコードはだせません、申し訳ないです…。

代わりと言ってはなんですが、DXライブラリ本家に3Dモデルのシェーダ解説ページがあります。
3Dモデルのシェーダを自作してみたい! という方は上記のリンクを参考にどうぞ。

シェーディングの色底上げ

DXライブラリのシェーディングの節でも簡単に説明しましたが、DXライブラリが採用しているシステムをそのまま使っているとライトがあたらない面は全て真っ黒になってしまいます。
単純にモデルだけだしてシェーディングさせると以下のような感じの見た目になります。

gunpro_noambientOcclusion.png
暗いですね。
単純に暗い面を明るくするためにはハーフランバートシェーダを使うというのも一手なんですが、それをしようとなると全モデルのシェーダを差し替える必要があります。
そんなことはしたくなかったので、「がん☆ぷろ!」では愚直にアンビエントライトを追加することにしました。
ただ、アンビエントライトは全カラーに対して一括で加算されるものなので、例えばアンビエントライトとして(0.2, 0.2, 0.2)を追加するとこんな感じの見た目になります。

gunpro_uniformAmbientOcclusion.png
明るいけれど、一様に灰色が乗ってしまう…ちょっと期待していたものと違いますよね…。(なお、MMDモデルの方はシェーダが違うのかこれらの設定が反映されていないっぽいです)
ということで、「がん☆ぷろ!」ではマテリアルのアンビエントライトの乗算成分にディフューズから計算した色を設定しています。

gunpro_diffuseAmbientOcclusiono.png
とりあえずそれっぽい見た目になりました。

つまり結論から言うと単純にマテリアルに設定されているアンビエント項が間違っていたということになるのですが、リソース変えないでプログラム側で一括で設定しているところがミソです(手抜きした、とも言いますが)。
ディフューズとアンビエントが同じなのかというとちょっと疑問が残りますが、大体どのモデルに適用しても見た目はそれっぽくなったので、アプリ固有な処理な気がしないでもないけど満足。


背景です、3Dやってるとどんな状況でも悩みますよねこれ、例に漏れず「がん☆ぷろ!」作ってる時も悩みました。
結局全天球のテクスチャをだしているんですが…。

手法的にはスカイボックスと呼ばれるものを適用しています。

gunpro_skybox_viewer.png
見た通りで、キューブマップの各面をそれぞれ貼った6つの平面ポリゴンを、超遠景として描画しているだけです。
このスカイボックスを描画する際と通常の3D描画でNear・Farを同じにしてしまうと、自機モデルに割かれるZレンジが小さくなってしまう問題があったので、スカイボックスを描く時は別で大きいNear・Farを使っています。
スカイボックスを描画した後でZバッファをクリアすることで、それより後に描く3Dモデルに影響がないようにしています。

やってみて分かったんですが、スカイボックスだとそれっぽい背景になるんですが、「がん☆ぷろ!」みたいに視点が縦横無尽に動ける場合は結構ぺったり絵だってバレますね(既に某所で指摘されてしまいましたが)。
せめて雲だけはボルメトリックな手法に切り替えるべきだったかなと思いましたが、シェーダ(と実装)が重いと思ったので止めました。
また、ポストパスエフェクトが実装できると気づいたのはこの実装の後だったので、思い切って大気散乱を実装してもよかったのかなと思いました、この辺無念ポイントの一つです。

AA

「がん☆ぷろ!」ではポストパスエフェクトのサンプル実装としてAAを実装しています。
AAはAntiAliasing(アンチエイリアシング)の略で、ざっくり言うとポリゴンの輪郭線などにでるジャギを低減する手法全般を指します。

「こんな小規模なアプリなのにAAやっているの?!」って思う人居るかもしれませんが、3Dが抱えるそもそも本質的な問題に絡むところなので対処してます。
DXライブラリは標準でAA機構を持っている(というより、正確にはD3Dが持つフルスクリーンアンチエイリアシングへのAPIを提供している、…んだとと思ってます)んですが、中身を推察する限りたぶんMSAAなので、品質は高いですがそれなりに重い処理です。

更に「がん☆ぷろ!」ではQuaterHD画質(960×540)の60FPSを、というHSPにしては微妙にチャレンジングなことをしているので、CPU+GPUのコストを60FPS内で収める軽量化・処理の最適化の一環としてFXAAを実装して積んでいます。
(実際、当初はNoAAでやっていたんですが途中でちょっと耐えられなって、FSAAやるにしてもちょっと重いし何とかするか、という感じで実装しました。
 FSAAの重さを体感したい人はスタートアップで「FSAA x16」とかにしてみるといいと思います、すごい綺麗になるけどめっちゃ重くなります、当たり前ですが。
 ちなみに筆者の開発環境はFSAAの中では一番品質が低い「FSAA x2」でもミッション中は60FPSが保てない程度の貧弱さです;;)

FXAAがどんな風に機能するかについては画像を見ていただいた方が早いので以下。
一応、今回実装したFXAAは正確にはFXAA3.11と言われるバージョンで、私の記憶が正しければ最新バージョンのハズ。

gunpro_noaa_sample0.pnggunpro_noaa_sample1.png
gunpro_fxaa_sample0.pnggunpro_fxaa_sample1.png
上段がAAなし、下段がFXAAありです。

NoAAとFXAAをみてどちらが綺麗かと言われると一概には何とも言えないところですね。
ジャギは減ってるけど、ぼかしが入ってほしくないところにも入るし、全体的にボケっとした印象になるから個人的には辛いなぁってところは結構あります。

処理内容に簡単に言及しておくと、FXAAは入力画像の各ピクセルで隣接ピクセルとの輝度差によって強さを変えながらぼかし(正確には、エッジ方向によって混ぜるピクセルを変えるぼかし)を適用する手法です。
入力として画像1枚のみをとるためどのような状況にも適用でき、輝度差によってのみぼかしを加えるためシェーダもシンプルになり高速に動作します。
実際コストパフォーマンスという観点ではAA手法の中でもかなり良いものでしょう、たぶん。
半面、テクスチャであってもエッジがあれば問答無用でぼかしがかかるので全体的にぼけた印象の絵がでることと、広域でエッジの方向・形状を検出するわけではないので微妙なぼかしになるポイントがある、というのがデメリットでしょうか。
(AA界は闇が深いので、深入りはせずこの程度の説明で勘弁してください…!)


まとめ

だいぶ長くなったと思いますが、これにて「がん☆ぷろ!」知見工夫苦労知ってもらう記事終了となります、読んでくださった方お疲れ様です。
もう少しだけこの記事は続きますが、主に筆者の感想とか心境的なところが大きいです。

で、HSP+DXライブラリの3D描画について

改めて今回の検証内容をまとめると

・3Dプリミティブの描画
・固定機能パイプライン:ライトの設定
・3Dモデル(MMDモデル)のアニメーション、描画
・プログラムから生成したモデル(ポリゴン)の描画
・ポストエフェクト(オリジナルシェーダ)の適用
・エフェクトプラグインEffekseerとの連携


という感じでした。
筆者個人としては一か月の検証内容としてはまぁ…充実した? …ような気がする。(この記事の長さからもそんな気がする)

なおこの記事には書いてないんですが、追加で私個人が勝手に検証してた内容がちょくちょくあり、簡単に紹介しておきます。

全リソースのメモリからのロード対応
実際のアプリだとよくやる、リソースをアーカイブ化してユーザからは見えないようにする実装についてです。
DXライブラリはファイルから読み込むAPIだけでなく、メモリから読み込むAPIも基本的には揃っているので、そういった実装が可能になっています。

「がん☆ぷろ!」では画像、SE・BGM、シェーダ、内部スクリプトなどリソースは全てアーカイブ化しています。
思ったよりリソースサイズが膨れあがってしまったのでHSP側からファイル単位での暗号化・複合化処理まではしてない(アーカイバから読んだ際はメモリ上に生データが乗っている)ですが。
その代わりに並列プリロードなどをやったのでそういう意味では満足してます。

DXライブラリもリソースの並列読み込み・初期化に対応しているので、やろうと思えば最初から最後まで動的ロードにしてシームレスにシーケンスを繋げることが出来るんじゃないでしょうか。
ver1.1から「がん☆ぷろ!」でも非同期ロードの実装が入っており、最速でXキー連打してキャラクター選択に入ろうとすれば大抵3Dモデルの読み込みが終わってないので、ロード待ちの画面が入るようになっています。

gunpro_async_load_sample.png
やっぱりロード待ち演出入っていると格好良く感じますよね、なんとなくスマートな感じに見える、実際はそうでもないのに(笑)。

あと諸々の事情で私は使わなかったんですが、DXライブラリ付属のツールで作れるアーカイブ(DXアーカイブ:.dxaファイル)という、HSPでいう.dpmにあたるものがあります。
素直にこれを使えばDXライブラリとの親和性が高いハズなので、リソースのアーカイブ化とか非同期ロードはもしかしたらかなり楽かもしれませんね。

3Dサウンド
今回サウンド関連の話一個もでてきませんでしたが、SEはどうやったってつけるだろうと思っていたので、個人的には知らないから触ってみようと思ってたところをちょっとだけ触ってました。
ver1.1から「がん☆ぷろ!」では通常のSEを鳴らす以外に、一部のSE(ミサイル爆発のSE)だけXAudio(※オプションで切り替えできますが)を使って簡単な3Dサウンドとして計算する実装が入っています。
ベロシティの設定もできるようなのでドップラー効果とかも付加できそうですし、リバーブも変えられそうな空気を感じたんですがそこまでは検証してないです。
単純に3Dリスナーの設定とSEの3D位置とかの設定をしているだけです、機能的に使えなかったのはかなり勿体ないなとは自分でも思ってます。
…が、諸事情により音ネタは作ってる環境と時間がなかったので、プログラムの方から凝る意味がほとんど無かったこともあり、ほぼ手つかずのままで放置しました。
この辺は個人的な無念タスクとしてまた暇がある時まで持ち越しする予定。

固定機能パイプラインの処理
偉そうにこの記事書いてる筆者ですが、実は3Dモデルとか使って3Dのプログラム書くのこれが初めてだったので、スタートからゴールまで四方八方よう分からんって感じでした。
その最たるものが固定機能パイプラインで、最初処理内容全く分からない地点にいた時シェーディング結果見せられて、「この部分をもう少し明るくしたい」ってなったときに「で、どこを設定すればいいの?」となっていました。
現代ではおよそあまり見ない固定機能パイプラインですが、中身が分からなかった最初に比べたらだいぶ理解が進んだと思います。
ここまできてようやくForwardRendering(Lighting)とDefferedRendering(Lighting)の明確な違いも理解できるようになりました、Light-Prepassは当時凄い技術だったんだな、ピーキーなハード性能っていうのもあったんだろうけど。

これからもしDXライブラリで3Dとか考えている人がいる場合は、私みたいな例もあるので始める前に悩まずやってみるといいかもしれませんよ、などと書いておきます。
(なお、この記事で言及している3Dとグラフィクス全般の知識とか、シェーダに関しては日々無意識に貪ってる分だけで何とか絞り出して書いているだけだったりします)

3Dモデリング
こちらも同じく記事中でそういう空気が一滴も出てきませんでしたが、今回3Dモデル初めて作りました。
Blenderも初めて触りましたし、UVも初めて展開しました、AOも初めてベイクしました、ボーンも初めて埋めたしアニメーションも初めて作りました、もうホントに初めて尽くし。
初めてらしく雑なモデルになったな、と振り返っても思います(笑)。

あと記事中言及しなさ過ぎて私はプログラムの検証ばっかしてそうと思われてる気がするんですが、実際問題「がん☆ぷろ!」作る上でたぶん半分くらいはリソース作るのに時間使ってます。
プロ生ちゃんのアニメーションも、たった70フレームのアニメーションに見えるかもしれないですが、あれだけで1時間くらいかかってます!
(その分消えた時間のせいでその他3Dモデルはすごい適当になってたりします!)

今まで「3Dってやたら時間かかるんだなー」というボンヤリした印象は持ってたんですが、自分でやってみて「これは時間かかる…、作ってらっしゃる方凄い…」って思い知りました、ジッサイスゴイ。


おわりに

諸々の事情でちゃんと書けなかったところ、実は小さいTipsで書きたいこと、「こうすれば良さそう、できなかったけど!」みたいなのはまだまだあるんですが、そういうの書き始めるともう記事内容が発散するなと思ったので一旦切り上げておきました。
できなかったこと書いてもしょうがないんですよね、無念ナリ。

続きとか

もし「HSP+DXライブラリで3Dでもっと色々出来るネタ知りたい!」みたいな需要があるなら、続きとかまとめとか記事書くかもしれません。
何気に今回触れている項目は基礎から微妙に発展に刺さっているところもあるので、新規分野は検証に時間かかるところしか残ってなさそうな空気を感じるので、こういうこと言っちゃうとコワイですが。
たぶんやるとしたら扱う内容としては、手とか足に別のモデル引っ付けて動かすとか、そういう実践的な話題メインになる気がします。

ツッコミとか

「やっぱりここの独自実装っぽいとこ気になるよ」みたいなところがある方はコメントででも結構ですので突っ込んで頂ければ幸いです、私程度の力量だと答えられないものもあるかもしれないのでご容赦頂くことはあるかもしれませんが…その時はごめんなさい。

この記事の存在意義とか

本記事、HSP+DXライブラリ+Effekseerを2Dから3Dにそのまま当てはめて推し進めていった結果相当変態的な記事になったと自覚していますが、これが果たしてHSPユーザの方々にとってどれほどインパクトがある話なのか(そもそもインパクトあってよくない気もするが)よく分かってないので、単なる3D描画の読み物とか、普通のアプリでやりそうな工夫の話程度で受け止めていただくのが一番妥当ではないかと思っています(作った「がん☆ぷろ!」自体はアプリですらないんですけど!)。
そういう読み物用途のために、ライブラリに依存しすぎてない話も混ざっているので(苦笑)。

個人的な感想

HSP+DXライブラリって組み合わせの3Dは殆どカスタム性なさそうだな(噛み合わせ悪そうだな)って当初思っていたんですが、シェーダ然り、モデルとアニメーション然り、無理やりだとしても相当色々なところに手が届くんだなって思いました。
単純にDXライブラリ側が開けているAPIが多いからだと思いますが、一方隠しAPIなどにあったりして使いこなすこと自体にそこそこ調べる力とか必要になるというのも同時に分かりましたが…。
ご自分でやられる方は注意した方がいいかもしれません(※私みたいに変なことしなければ大丈夫なはずですが)

ただ、これも少し上に書いてますが、HSPがそもそも持っている力もあり、またDXライブラリやEffekseerなどライブラリの力もあり、暇な時に私一人でちょこちょこ適当に作業しているだけにも関わらず1ヶ月足らずでこのラインのものが出来たことは素直に喜ぶべきことなのでは、と思っています。
だって、シェーダによるMMDモデル描画、IK物理アニメーション、3Dエフェクト描画などありますからね、これらを一から書くにはそれなりに知識と時間がかかるところです、私はこの辺全て実装してないので本当にライブラリ様々です。

感謝の意

ライブラリ以外にも、MMDモデルからフォント、BGMなど素敵な素材を公開してくださっている方々に感謝。
素材がなかったらこのデモも全く見栄えがしないものになっていたと思います。
特にプロ生ちゃんはMMDモデルとSD画像いずれも使わせていただきました、大感謝!!
プロ生ちゃんの素材は公式HP様からDLできます!!!(再度宣伝)

プロ生ちゃんかわいい!!!
pronamachan_kawaii5.png

そんな感じで、じゃっ!!
記事長くなって疲れました…


Appendix

ソースコード

記事書いてるときにすっかり忘れてたんですが、この記事と一緒に「がん☆ぷろ!」のソースコードを公開する予定だったんでした。
実は他のプロジェクト用に書いてたコードがそもそもあったところに、付け足で作っているため相当ごちゃごちゃ混じってるんですが、とりあえず動いてるしこのままスパゲッティとしてあげてしまおうと思います(見る人、ごめんなさい;;)。

一点念押しで注意ですが、この程度の規模のソフトだし、この記事程度にはまとまっているソースコードなんだろうなと期待している方には大変申し訳ないですが、全然全くそんなことありません
勢いよく繋がりすぎてて分からん箇所は無尽にあると思いますし、この記事に書いてることは私の中でそれなりに精錬したものだったりします。

…あんまり私が泣き言書いてもしょうがないですね、DLは次デス。
2016/11/21 GoogleDrive

私が書いたソースコード自体はMIT Licenseでライセンスします。
なぜNYSLじゃなくてMIT Licenseなのかというと、HSPコンテスト等にだしている手前著作権まで放棄したりすると、「コードを改変して公開して自分のものとして主張する」等が行われると面倒なことになりそうだったからです。
基本的にこういうソースコードの類、処理の流れを見て「あーそうするのか」と納得したり、手元で弄って結果動作見るのがメインだと思うので、その範囲でなら好きにできるようなライセンスにしたつもりです、ご容赦願いたい。
あと、明らかに私が書いたとこじゃなさそうなところ、ライブラリのコードなど別にライセンスされている箇所はそれらの規約に従ってください。
って言われても分からないと思うので、具体的にドコが大丈夫でドコが駄目かは同梱のりどみを読んでください。

それでは改めて、お疲れさまでした。
関連記事
スポンサーサイト



コメント

コメントの投稿


管理者にだけ表示を許可する

トラックバック

トラックバック URL
http://fe0km.blog.fc2.com/tb.php/124-60d29c05
この記事にトラックバックする(FC2ブログユーザー)