Blenderで作ったglTFモデルをSiv3D(旧)で動かそう！


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915805/0499eaa3-a109-ede3-62bb-f0ca00c3c3eb.png)

#はじめに

Siv3Dでなにか作るときに、手ごろな編集ツールがあって欲しい...と、思う事があったので、無料で3Dモデルを作成できるBlenderとSiv3Dを連携しようと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915805/239d4953-86f5-edea-3cdf-396f77d01b9d.png)

使用するファイル形式には、最近は割と一般的になってきた(?)glTFを使っています。
glTFは比較的新しい3Dモデルを保存できるファイル形式で、頂点やテクスチャデータの他にボーンやモーフィングを使ったアニメーションや光源やカメラの情報を記録できます。

3Dモデルを使用することが前提なので、現在のOpenなSiv3Dではなくて**旧Siv3D**を対象とした内容になります。

#Maple-glTF

今は昔、OSもなくファミコンとかMSXの8bitCPUの時代には、大した編集ツールもないけど、簡単に絵が作れるBG面とかPCGとかいう仕組みがあって、画面は割と気楽にデザインできました。
普通に使っているフォントを好きなように変更して表示できるので、printfでグラフィックスを表示できたんですね。

Siv3Dでも、テキスト記述の**かんたん気軽さ**を引き継ぎつつ**現在のGPUの力を存分に引き出して**やろうじゃないの!

というのがMaple-glTFのコンセプトになります。
※名前のMapleは、**MAP**と**L**ite、 **E**ditingの語呂合わせです。

編集ツールにBlenderを使って、glTF Exporterで3Dモデルを用意し、CSVファイルで3Dの空間を定義して、Siv3Dでいろいろ使うことができれば、気軽に遊べそうです。

取得は以下のリポジトリから。

```  c++
git clone https://github.com/itakawa/Maple-glTF.git
```

ビルドは、構成を**Release構成**に、プラットフォームは**x86**をそれぞれ選択する必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915805/498990eb-59a4-3b60-2bb7-7e83f0bb8665.png)


#動作環境
- Maple-glTF　　　　　　　　https://github.com/itakawa/Maple-glTF/
- 旧 Siv3D (August 2016 v2)　　https://github.com/Siv3D/Reference-JP/wiki/
- Blender　　　　　　　　　　https://www.blender.org/

#依存しているライブラリ
- TinyGLTF　　　　　　　　　https://github.com/syoyo/tinygltf
- json.hpp　　　　　　　　　https://github.com/nlohmann/json

※glTFのパーサーには@syoyoさんのTinyGLTFを使わさせていただいてます。
Tinyとありますが、glTFの各要素をC++の構造体で直で表現してくれるので、開発者的にはめっちゃ使いやすいです。TinyGLTF自体は、MITの下でライセンスされているNiels Lohmannさんのjson.hppを使用しています。

#Mapleのマップ定義のサンプル
例えば上の画像のMaple記述(csvファイル)は以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915805/9b23280e-d0dc-1026-cbc1-e8c4efbf8d30.png)

基本的にCSVファイルですが、//でコメント書けると、何となくスクリプト風にみえますね。

大雑把には、
1. /chunk句で、ひとかたまり(チャンク)の空間を用意して、ワールド座標に配置
2. /entity句で、glTFファイルを対応する文字に割り当てつつ、ローカル座標設定とアニメーション方法などを選択
3. /def句で、チャンク内に2次元マップの空間を用意
4. /map句で、2次元マップの物体(エンティティ)の並び順を文字で配置
という感じです。

最小単位の物体(エンティティ)は、非アニメモデルとアニメ付きモデルがあって、背景などで動かない物と、キャラクターなど動く物とで使い分けています。
動く物についてはとりあえずCSVファイルで表示用に登録しておいて、メインループに入ったらエンティティの構造体を操作して動作させていきます。

#glTFファイルの作り方
１つの物体の大きさは縦横高＝１ｍが基本サイズになっていてブロックを積み重ねていくと、マインクラフトのような造形ができます。
ですけど、実際はなんでもよくて形はブロック(立方体)でなくても良く、角度や大きさも特に制限なく自由です。

※注意点としては、基本的な色の付け方はBlenderでのMaterialカラーがSiv3Dにおいての頂点カラーになっている事でしょうか。Blenderにも頂点カラーはありますが、自分があまり使わないのでそうしています。

あと、モーフィング(シェイプキー)については1メッシュに限定しています。
glTFの描画にCPUスキニングを使っていて、性能を出すために全て前計算でメッシュデータを生成していますが、モーフィングだけは動的に処理する必要があるので、再計算しています。
再計算はかなり遅くなるので、メッシュを限定して作る必要があります。

あとあと、Siv3Dは32bit環境用なので、2GBメモリ上限があります。
作るものにもよりますが、MMDの様に長めのアニメを再生しようとすると、制限に引っかかるかもしれません。ゲーム用のキャラクタ表示ぐらいであれば問題ないです。

人物モデルのサンプルとしては、以下のリポジトリにSIV3D-KUNのBlendファイルを置いています。

```  c++
git clone https://github.com/itakawa/siv3dkun.git
``` 

※モデルデータの作成が面倒な場合は、下記のサイト(Sketchfab)を利用しても良さそうです。
https://sketchfab.com/search?q=City&sort_by=-relevance&type=models


#Siv3Dの記述
この記事のサンプルコードは、下の方に記載していますが

基本的には、Main.cppの最初の方で、Maple-glTFのヘッダーファイルを追加（mapleGLTF.hpp）します。

``` C++
#include "3rd/mapleGltf.hpp"                   // MapleGLTF追加
```

次に、Main()関数でMapleマップ定義ファイルをCSVReaderにかけて、
ChunkMapクラスとMapleMapクラスの実体を生成して、
LoadMaple()でCSVから、制御データを構築します。

``` C++
void Main()
{
    String maplepath = FileSystem::CurrentPath()+L"Example/MapleGLTF/";     // リソースのパス

    // マップ定義ファイルから座標リストとマップを生成
    CSVReader csv1(maplepath + L"map.csv");
    MapleMap maple1;                                // 物体の座標リスト
    ChunkMap map1, atr1;                            // 物体と属性のマップ
    LoadMaple(maple1, map1, atr1, csv1, maplepath);
```

後は、メインループでは、RenderMaple()でこれまで定義してきた内容を描画します。
その他、CtrlKeboard()、CtrlAction()などの処理は、このサンプルではSIV3DKUNの操作を制御します。

``` C++
    while (System::Update())
    {
        Graphics3D::FreeCamera();
        camera = Graphics3D::GetCamera();

        auto ms1 = stopwatch.ms();

        CtrlKeyboard(ent_s, map1, FontT);           // キー入力制御
        CtrlAction(maple1, ent_s, map1, atr1);      // 行動制御
        RenderMaple(maple1, map1, atr1);            // 描画
```

当たり判定なども、表示に使っているマップが文字列定義なので、例えば足の下が空間(=空気ブロック)
かどうか調べるには、substr()を使います。
MaplePos2Idx()に調べたい座標(ent->Trans)の、1メートル下(Float3(0, -1, 0))を指定すると、
chunkmap文字列のカラム番号に変換されるので、substr()で取ってきて判定できます。
3Dの空間ですが、実体は1次元の文字列っていうところがおもしろいかなと。

``` C++
String GetMap(Float3 flt3,ChunkMap &chunkmap)
{
    auto idx = MaplePos2Idx(flt3, chunkmap);
    return (idx >= 0) ? chunkmap.Text.substr(idx, 1) : L"　";
}

String under = GetMap(ent->Trans + Float3(0, -1, 0), chunkmap);
if ( under != L"　" )
{
//落ちる処理
}
```

アニメーションについてはボーンとモーフィングの2つが使えます。
Siv3Dkunのモデルには4種類のボーンアニメと10種類のモーフを収録していて下記のサンプルは1235のキーで移動(Run)mでジャンプ、メインキーの0～9で表情を操作できます。

カメラ移動はSiv3D標準のwasdexとカーソルキーを使います。

ボーンアニメは前計算なので、動的操作はできないのですが、立ち(Idle)状態では、再生と逆再生と停止を乱数で制御してそれっぽく動かしてます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915805/1a179eb9-f847-bd64-c601-26ec6d2e48fe.png)![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915805/3a2b5148-8d55-ff99-4a7e-9ddf87998c6a.png)![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915805/383c2595-4ab1-b3c2-fb70-8e81dcf6764a.png)


モーフィングについては、表情と目と口の3つを独立して動かせるように3CH合成としています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915805/397c696a-c22a-ca09-f82c-63c6c4c55539.png)![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915805/1a72c46b-4f4a-3749-2056-bb204743df02.png)![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915805/43cfea09-be8c-84c3-246d-a55cd2a520a6.png)

あと、アニメーションについては、連番のglTFが再生できます。
ファイル名の最後にカッコ書きでフレーム番号を書いてパラパラアニメする奴ですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915805/3397b7d2-7a4b-f76e-17d0-6daa0ccf645d.png)

Mapleだと例えばこんな風に登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915805/d23aa1b5-82f2-e8c7-2c04-da0f4cb50929.png)

glTFはBlenderの全機能を表現できませんが、最終的にメッシュになっていれば表示はできるので、1コマ毎のglTFを連番で用意する事で実現します。これを使えばおそらくシミュレーションとかボリュームレンダリング等も~~我手中に~~Siv3Dで動かす事ができます。

Sea(x).glbは、Blenderの海洋シミュレーションで作ったglTFモデルです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915805/fcf67926-69ea-6001-d903-8edcb738f9ea.png)

連番アニメの類似品で、乱番ブロックというのもあります。
形式は連番アニメのように()書きのファイル名でglTFを用意しておくのですが、実際には乱数で選択されたブロックが表示されます。
どういう時に使うのかというと、森とか街などを手っ取り早く作りたい時ですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915805/54b25941-9849-6329-6e90-5aa947e19298.png)


こんな感じになります。
ちょっと街モデルが手抜きでマテリアル貼ってないのですが、スパイダーマン風な奴がつくれそうですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915805/9934c763-6aab-71e4-361d-e7611c4fc7e4.png)

今度は空を追加してみましょうか。スカイドームは何を作るにしても必要ですね。
spin(x,y,z)というパラメータを設定するとブロックの自転速度を変えられます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915805/01ac7431-dca8-b458-0309-d6042ef32ea0.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915805/874a36be-7471-30af-c4ae-aedb280f45f8.png)

3D文字列なんかも表示できます。
まずToD4UIフォント(TOD4UI-Regular.otf)という名前で、ゲーム用のフォントとしてOpenTypeフォントで作ったものがありまして。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915805/9c98811d-4623-b562-986f-a1d36f1142d6.png)
小文字は大文字の小さくしただけの書体ですが、これをわざわざglTFにしています。
何をやりたかったかというと、メッシュのポリゴンが筆順に合わせてソートしてあるので筆順表現ができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915805/2c23807b-da49-3fb9-cdda-64d61a80b6f1.png)

現時点の文字列ブロックは直線表示と円形表示の２つがあります。
円形表示で筆順表現と言ったら魔法陣ぐらいしか使い道なさそうですけど、恐らくフォントの部分に、文字ではなくてある種の3Dモデルを埋め込むことで、変わった表現(VFX表現とかデジタルメーターやゲージ)ができるかもしれません。GPUは頂点の移動は得意ですが増減は苦手ですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915805/5bba86d3-2334-62a2-83a7-06e923f75831.png)

肝になっているのは、表示文字数の部分で、整数部が表示文字数を表し、小数部は筆順表現の進度を表します。
更に、正の値の場合は1文字ずつのタイプライターアニメになりますが、負の数を与えた場合には、全文字同時の筆順表現になっています。

と、これまではglTFを並べる内容を説明してきましたけど、Mapleで定義するものはメッシュだけではないです。
属性マップという他の概念を同じ座標に重ねて定義することで、プログラミングによって例えば重力を曲げるとか、イベントの埋め込みができるようになります。
ちょっとあまり出来は良くないですが、重力変更は下記のような感じ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915805/4ebcea73-b48e-0eca-d8b5-5c9e3d6be054.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915805/23e3491b-9a6b-81a0-2599-8a3142a2d13a.png)


#さいごに
色々途中なのですが、やっぱりglTFは再利用がやりやすいところが良いです。
また、メッシュだけでなく、光源やカメラ軌道の情報も載せられるので、まだまだ遊べそうですね。
プログラムで動かしたり、自分のアプリや他のプラットフォームで使えると、もっと楽しくなってくるのかなと思います。

※、、、と、ここまで書いておきながらなんですが。
肝心のMapleの方はというと、構造体だけ用意して好き勝手にいじってるだけ、、の域を越えてないので、「できます。」連呼しててもこれで一体どーしろと言うんだ的な(  -_・)?感想しか残らない代物なので、まー来年こそは何かの形にしていきたいですね。

長くなりましたが、乱文にて。
Siv3Dのソースコードは以下になります。

#ソースコード(Main.cpp)
``` C++

//
// Siv3D August 2016 v2 for Visual Studio 2019
// 
// Requirements
// - Visual Studio 2015 (v140) toolset
// - Windows 10 SDK (10.0.17763.0)
//

# include <Siv3D.hpp>

#include "3rd/mapleGltf.hpp"                   // MapleGLTF追加

using namespace s3d::Input;

enum ID_ANIME { DIG = 0, JUMP = 1, RUN = 2 ,IDLE = 3 };
Quaternion Q0001 = Quaternion(0, 0, 0, 1);      // よくある定数の略称
Float3 sca111 = Float3(1, 1, 1);
int32 WW, HH;

String GetMap(Float3 flt3,ChunkMap &chunkmap)
{
    auto idx = MaplePos2Idx(flt3, chunkmap);
    return (idx >= 0) ? chunkmap.Text.substr(idx, 1) : L"　";
}

void SetMorph(int32 ch, Entity* ent, int32 mtstate, float speed, Array<float> weight)
{
    if (ent == nullptr) return;

    auto& spd = ent->Morph[ch].Speed;
    auto& cnt = ent->Morph[ch].CntSpeed;
    auto& now = ent->Morph[ch].NowTarget;
    auto& dst = ent->Morph[ch].DstTarget;
    auto& idx = ent->Morph[ch].IndexTrans;
    auto&  wt = ent->Morph[ch].WeightTrans;

    if (now != mtstate && idx == 0)
    {
        if (ch) now = 0;
        spd = speed;
        cnt = 0;
        dst = mtstate;
        idx = 1;
        wt = (weight[0] == -1) ? WEIGHTTRANS : weight;
        wt.insert(wt.begin(), 0);
    }
}

void CtrlMorph(Entity* ent)
{
    if (ent == nullptr) return;

    for (auto ii = 0; ii < NUMMORPH; ii++)
    {
        auto& spd = ent->Morph[ii].Speed;
        auto& cnt = ent->Morph[ii].CntSpeed;
        auto& now = ent->Morph[ii].NowTarget;
        auto& dst = ent->Morph[ii].DstTarget;
        auto& idx = ent->Morph[ii].IndexTrans;
        auto&  wt = ent->Morph[ii].WeightTrans;

        if (now == -1) continue;

        cnt += spd;                     //モーフ変化量を加算
        if (cnt >= 1.0) cnt -= 1.0;
        else continue;

        if (now > -1 && idx)
        {
            idx++;
            if (wt[idx] < 0)
            {
                idx = 0;
                if (ii == 0) now = dst;  //プライマリ(表情)の場合は遷移する
                else         now = -1;   //セカンダリ(瞬き/口パク)の場合は遷移しない
            }
        }
    }
}

void CtrlKeyboard(Entity* ent, ChunkMap& map)
{
    if (ent == nullptr) return;

    const int16  DST[] = { -1, 0, 180, -1,270, 315, 225, -1,  90,  45, 135,  -1,-1,-1,-1,-1 };//押下キーに対応する方向テーブル
    static float rad = 0;
    auto& dir = ent->Rotate[0].y;       //現在の方向

    // キー入力
    uint32 key_m = 8 * KeyNum1.pressed + 4 * KeyNum3.pressed + 2 * KeyNum5.pressed + KeyNum2.pressed;
    uint32 key_a = 2 * KeyN.clicked + KeyM.clicked;

    auto& anime_s = ent->Maple->ArgI;
    // ArgI(アニメ選択)が掘るでも跳ぶでもない
    if (anime_s == ID_ANIME::IDLE || anime_s == ID_ANIME::RUN)
    {
        if (key_m && DST[key_m] != -1)  //走る
        {
            float vel = 15;              //回転の刻みは45度の公約数
            int16 dfp = 180 - abs(abs(int16(dir) + vel - DST[key_m]) - 180);
            int16 dfm = 180 - abs(abs(int16(dir) - vel - DST[key_m]) - 180);

            if (dfp < dfm) vel = +vel;
            else           vel = -vel;

            if (dir == DST[key_m]) vel = 0;
            else dir += vel;

            if (dir > 360)    dir -= 360;
            else if (dir < 0) dir += 360;

            ent->qRotate[0] *= Q0001.Yaw(Radians(vel));

//            Float3 tra = ent->qRotate[0] * Vec3::Backward * 0.1;              //旋回ターン
            Float3 tra = Q0001.Yaw(Radians(DST[key_m])) * Vec3::Backward * 0.1; //でも、直接ターンにしないと操作性悪すぎ

            //当たり判定
            auto wall_u = GetMap(ent->Trans + Float3(0, 1, 0) + tra * 10, map); // 上半身
            auto wall_l = GetMap(ent->Trans + Float3(0, 0, 0) + tra * 10, map); // 下半身 移動成分ｘ10でちょい先読み

            if (wall_u == L"　" && wall_l == L"　") ent->Trans = ent->Trans + tra;

            auto under = GetMap( ent->Trans + Float3(0, -1, 0), map) ;
            if (under == L"　") ent->Trans += Float3(0, -0.5, 0);
            else ent->Trans.y = Floor(ent->Trans.y);                            //着地時の高さを再計算

            anime_s = ID_ANIME::RUN;
        }

        if (DST[key_m] == -1 && key_a == 0) anime_s = ID_ANIME::IDLE;   //立ち
        if (KeyB.clicked) anime_s = ID_ANIME::DIG;                      //掘る
        if (KeyM.clicked) anime_s = ID_ANIME::JUMP;                     //ジャンプ
        
        if      (Key0.clicked) SetMorph(0, ent,  0, 1.0, WEIGHTTRANS);  //瞬き
        else if (Key1.clicked) SetMorph(1, ent,  1, 1.0, WEIGHTBLINK);
        else if (Key2.clicked) SetMorph(1, ent,  2, 1.0, WEIGHTBLINK);
        else if (Key3.clicked) SetMorph(2, ent,  3, 1.0, WEIGHTBLINK);
        else if (Key4.clicked) SetMorph(2, ent, 14, 1.0, WEIGHTBLINK);

        else if (Key5.clicked) SetMorph(0, ent, 10, 1.0, WEIGHTTRANS);  //表情
        else if (Key6.clicked) SetMorph(0, ent, 11, 1.0, WEIGHTTRANS);
        else if (Key7.clicked) SetMorph(0, ent, 12, 1.0, WEIGHTTRANS);
        else if (Key8.clicked) SetMorph(0, ent,  8, 1.0, WEIGHTTRANS);
        else if (Key9.clicked) SetMorph(0, ent, 14, 1.0, WEIGHTTRANS);

    }

    if (anime_s == ID_ANIME::JUMP)   //跳んでからも制御可能(マリオジャンプ)
    {
        if (key_m && DST[key_m] != -1)                     //ジャンプXZ移動
        {
            float vel = 5;         //回転の刻みは45度の公約数
            int16 dfp = 180 - abs(abs(int16(dir) + vel - DST[key_m]) - 180);
            int16 dfm = 180 - abs(abs(int16(dir) - vel - DST[key_m]) - 180);

            if (dfp < dfm) vel = +vel;
            else           vel = -vel;

            if (dir == DST[key_m]) vel = 0;
            else dir += vel;

            if (dir > 360)    dir -= 360;
            else if (dir < 0) dir += 360;

            ent->qRotate[0] *= Q0001.Yaw(Radians(vel));

            Float3 tra = Q0001.Yaw(Radians(DST[key_m])) * Vec3::Backward * 0.1; // 直接
            auto wall = GetMap(ent->Trans + tra * 10, map);                     // 移動成分ｘ10でちょい先読み

            if (wall == L"　") ent->Trans = ent->Trans + tra;
        }
    }
}


void CtrlAction(MapleMap& maple, Entity* ent, ChunkMap& chunkmap, ChunkMap& chunkatr)
{
    if (ent == nullptr) return;

    auto& anime_s = ent->Maple->ArgI;
    auto& currentframe = ent->Maple->AniModel.precAnimes[anime_s].currentframe;
    auto& animespeed = ent->Maple->ArgF;
    auto& jump = ent->CtrlF;
    auto& starty = ent->CtrlF[0];
    const Array<float> G = { 27.40f, 26.42f, 25.44f, 24.46f, 23.48f, 22.50f, 21.52f, 20.54f, 19.56f, 18.58f,
                             17.60f, 16.62f, 15.64f, 14.66f, 13.68f, 12.70f, 11.72f, 10.74f,  9.76f,  8.78f,
                              7.80f,  6.82f,  5.84f,  4.86f,  3.88f,  2.90f,  1.92f,  0.94f, -0.04f, -1.02f,
                             -2.00f, -2.98f, -3.96f, -4.94f, -5.92f, -6.90f, -7.88f, -8.86f, -9.84f,-10.82f,
                            -11.80f,-12.78f,-13.76f,-14.74f,-15.72f,-16.70f,-17.68f,-18.66f,-19.64f,-20.62f,
                            -21.60f,-22.58f,-23.56f,-24.54f,-25.52f,-26.50f,-25.20f,-0.126f,0,0
    };

//初期化
    if (ent->CtrlI[0] != anime_s)     // [0]:アニメステート履歴で切り替わった時
    {
        if      (anime_s == ID_ANIME::DIG) animespeed = 4; // 掘る 倍速設定
        else if (anime_s == ID_ANIME::RUN)  animespeed = 4;// 走る 倍速設定
        else if (anime_s == ID_ANIME::IDLE) animespeed = 2;// 立つ
        else if (anime_s == ID_ANIME::JUMP)                // 跳ぶ
        {
            starty = ent->Trans.y;
            jump.insert(jump.end(), G.begin(), G.end());
            ent->CtrlI.emplace_back(1);
            animespeed = 1;                             // 等速設定
        }

        currentframe = 1;           // フレームカウンタを1に設定してアニメスタート(ループして0で終了)
        ent->CtrlI[0] = anime_s;
    }
    else
    {
//各アニメ制御
        if (anime_s == ID_ANIME::IDLE)                 // 立ち行動の表現
        {
            static int32 cnt = 0;
            static float sign = +1;
            static float VEL[] = { 0.0, 0.1, 0.2, 0.4, 0.4, 0.4, 0.6, 0.6, 0.6, 0.6,
                                   0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.6, 0.6,
                                   0.6, 0.6, 0.4, 0.4, 0.4, 0.2, 0.1, -1 };
            if (VEL[cnt] == -1) cnt = 0;
            else
            {
                if (cnt == 0 && Random(0.0, 100.0) < 1.0)
                {
                    cnt = 1;
                    if (Random(0, 10) < 5) sign *= -1;
                }
            }
            animespeed = VEL[cnt] * sign;
            if (cnt) cnt++;

            auto under = GetMap(ent->Trans + Float3(0, -1, 0), chunkmap);

            if (under == L"　") ent->Trans += Float3(0, -0.5, 0);   //落下
            else                ent->Trans.y = Floor(ent->Trans.y); //着地時の高さを再計算
        }

        if (anime_s == ID_ANIME::JUMP)         // 跳び行動の表現
        {
            if (currentframe == 0 || ent->CtrlI[1] >= G.size())
            {
                anime_s = ID_ANIME::RUN;      // 終わったら2:走るに変更
                animespeed = 4; // 走る 倍速設定
                ent->Trans.y = starty;
                ent->CtrlF.resize(1);
                ent->CtrlI.resize(1);
                return;
            }

            auto tra = // ent->qRotate[0] * Vec3::Backward * 0.08 +  // xz平面の移動成分
                       Float3(0, jump[ent->CtrlI[1]++] / 190, 0);    // y軸の移動成分

            auto under = GetMap(ent->Trans + Float3(0, -1, 0) + tra, chunkmap);

            if (tra.y < 0 && under != L"　")
            {
                anime_s = ID_ANIME::RUN;  // 終わったら2:走るに変更
                animespeed = 4;           // 走る 倍速設定
                ent->CtrlF.resize(1);
                ent->CtrlI.resize(1);
                return;
            }
            else
            {
                auto wall = GetMap(ent->Trans + tra, chunkmap);
                if (wall == L"　") ent->Trans += tra;
                else
                {
                    anime_s = ID_ANIME::RUN;  // 終わったら2:走るに変更
                    animespeed = 4;         // 走る 倍速設定
                    ent->CtrlF.resize(1);
                    ent->CtrlI.resize(1);
                    return;
                }
            }
        }

        if (anime_s == ID_ANIME::DIG)         // 掘り行動の表現
        {
            if (currentframe == 0) anime_s = ID_ANIME::IDLE; // 終わったら3:立つに変更
        }

        //床の重力ベクトル対応(床が動いていたら一緒に動かす)
        auto map_under = GetMap(ent->Trans + Float3(0, -1, 0), chunkmap);
        if (map_under != L"　")  //足元に何かある
        {
            auto atr_under = GetMap(ent->Trans + Float3(0, -1, 0), chunkatr);
            for (auto& atr : maple.Attribs)
            {
                if (atr.Name == atr_under)                               //取ってきた属性を選択
                {
                    ent->Trans += atr.Trans;                             //属性の重力ベクトルを加算
                    break;
                }
            }
        }
    }
}

void Main()
{
    String maplepath = FileSystem::CurrentPath()+L"Example/MapleGLTF/";     // リソースのパス

    // マップ定義ファイルから座標リストとマップを生成
    CSVReader csv1(maplepath + L"map.csv");
    MapleMap maple1;                                // 物体の座標リスト
    ChunkMap map1, atr1;                            // 物体と属性のマップ
    LoadMaple(maple1, map1, atr1, csv1, maplepath);

    Window::Resize(1366, 768);
    WW = Window::Width();  HH = Window::Height();

    const Font FontT(15, L"TOD4UI");    // 英数フォント
    const Font FontM(7);                // 日本語フォント

    // カメラ設置
    Camera camera = Graphics3D::GetCamera();
    camera.pos = Float3(0.32, 4, 20);    camera.lookat = Float3(16, 1, -16);
    Graphics3D::SetCamera(camera);

    // 背景色
    Graphics::SetBackground(Palette::Midnightblue);
    Graphics3D::SetAmbientLight(ColorF(0.7, 0.7, 0.7));

    //専用制御(モーフ/可変アニメ)のEntityを取得
    Entity* ent_s = nullptr;        
    Entity* ent_d = nullptr;        
    Entity* ent_f = nullptr;        
    for (auto& ent : maple1.Entities)
    {
        if (ent.Name == L"Ｓ") //SIV3DKUNの初期化
        {
            ent_s = &ent;
            ent_s->Maple->ArgI = ID_ANIME::IDLE; //アニメ種類3：待ち
        }
        if (ent.Name == L"Ｄ") //情報ブロックの初期化
        {
            ent_d = &ent;
            ent_d->Count = 100;
        }
        if (ent.Name == L"文") //文字列の初期化
        {
            ent_f = &ent;
        }
    }

    //ストップウォッチ開始
    Stopwatch stopwatch;
    stopwatch.start();              

    while (System::Update())
    {
        Graphics3D::FreeCamera();
        camera = Graphics3D::GetCamera();

        auto ms1 = stopwatch.ms();

        CtrlKeyboard(ent_s, map1);                  // キー入力制御 [1]：←[2]：↓[3]：→[5]：↑[M]：ジャンプ[B]:掘る
        CtrlMorph(ent_s);                           // モーフィング制御
        CtrlAction(maple1, ent_s, map1, atr1);      // 行動制御
        RenderMaple(maple1, map1, atr1);            // 描画

        //情報ブロック表示
        if (ent_d)
        {
            ent_d->Trans = ent_s->Trans;
            ent_d->Trans.y = ent_s->Trans.y + 1;
            ent_d->Maple->Mode = String(L"円:MapPosition X=(X) Y=(Y) Z=(Z)")
                .replace(L"(X)", Format(ent_s->Trans.x))
                .replace(L"(Y)", Format(ent_s->Trans.y))
                .replace(L"(Z)", Format(ent_s->Trans.z));
        }

        //文字列表示
        if (ent_f)
        {
            static float vel = 0.1;
            ent_f->Count += vel;
            if ((vel > 0 && ent_f->Count > ent_f->Maple->Mode.length-2) || 
                (vel < 0 && ent_f->Count < 0) ) vel *= -1;
        }

        //物体マップ表示
        auto &dd = map1.MapSize.z;
        auto &ww = map1.MapSize.x;
        for (int32 yy = 0; yy < dd ; yy++)           
        {
            for (int32 xx = 0; xx < ww; xx++)
            {
                int32 idx = MaplePos2Idx(xx, 1, (dd-1) - yy, 32, 32, 32);
                auto cc = map1.Text.substr(idx, 1);
                FontM(cc).draw(xx * 8, yy * 8);
            }
        }
    }
}

```

#ソースコード(map.csv)
``` C++
// 空間定義スクリプト(MapleGLTF)
// /chunk: size(w,h,d) : チャンクサイズ。モデルを詰め込む領域の大きさを設定
//         scale(x,y,z): チャンクスケール(ワールド座標系)

/chunk,size(32,32,32),trans(0,0,0)

// 物体定義(map用) /entity:file'<記号>:glTFファイル名'：物体の定義。記号とglTFファイル(*.glb)を接続
//        :type'種別'               : 物体の種別。[ 物:静物 | 敵:敵MOB | 主:プレーヤー]
//        :trans(x,y,z)             : 物体の座標変換(移動)
//        :scale(w,h,d)             : 物体の座標変換(拡縮)

//        :rotate(yaw,pitch,roll)   : 物体の座標変換(初期角度)
//        :spin(yaw,pitch,roll)     : 物体の座標変換(継続回転)
//        :mode('モード',num)       : 物体のモード指定 ※mode['連'番アニメ|'乱'数選択]、
//                                    フレーム数numが1以外の場合はglbファイル名に()連番指定。

//        :rotate(yaw1,pitch1,roll1,yaw2,pitch2,roll2)  : 文字列の初期角度(<1>文字,<2>文字列)
//        :spin(yaw1,pitch1,roll1,yaw2,pitch2,roll2)    : 文字列の継続回転(<1>文字,<2>文字列)
//        :effect(attr)                                 : 属性マップの影響(0:受けない:1～:受ける属性マップ番号)
//        :mode('モード:<文字列>',radius,kerning)       : 文字列のモード指定 ['線':直線パス | '円':円形パス]
//        :mode('モード:ア',animeNo,animeSpeed)         : アニメモード指定 ['ア':アニメNo(-1:全部/n=アニメ番号) | アニメ速度:0.1=10%]

// 物体定義
/entity,type'物',file'＃:Block1.glb',   trans(0,0,0), scale(1,1,1),rotate(0,0,0), spin(0,0,0),attr(0,0),mode('連',1)
/entity,type'物',file'Ｓ:Siv3DKun2.glb',trans(2,10,-2), scale(1,1,1),rotate(0,0,0), spin(0,0,0),attr(0,0),mode('ア', -1, 2)
/entity,type'物',file'海:Sea(0).glb' ,  trans(0,-10,0),scale(1,1,1),rotate(0,0,0),spin(0,0,0),attr(0,0),mode('連',16)
/entity,type'物',file'街:City(0).glb',  trans(0,0,0),scale(0.5,0.5,0.5), rotate(0,0,0),spin(0,0,0),attr(0,0),mode('乱',8)
/entity,type'物',file'夜:SkydomeN.glb', trans(0,0,0),scale(1000,1000,1000),rotate(0,0,0),spin(0.008,0.005,0),attr(0,0),mode('連',1)
/entity,type'字',file'文:ToD4UI4.glb',  trans(0,1,0)   ,scale(20,20,20), rotate(0,90,0,0,0,0),spin(0,0,0,  0,0, 0),attr(0,0),mode('線:https://github.com/itakawa/maple-GLTF/',1,0.9)
/entity,type'字',file'Ｄ:ToD4UI4.glb',  trans(16,1,-16),scale(10,10,10), rotate(0,90,90,0,0,0),spin(0,0,-1, 0,0,-1),attr(0,0),mode('円：(DEBUG)',2,6)

/entity,type'物',file'＠:TreasureBox.glb',trans(0,0,0), scale(1,1,1),    rotate(0,0,0),spin(0,0,1),attr(1,0),mode('連',1)

// 属性定義
/attr,type'流',name'↑', trans(    0,0, 0.05),scale(1,1,1), rotate(0,  0,0),mode('空',0,0),tag'<while:0:10><if:><true:><false:><wait:><endwhile:>'
/attr,type'流',name'→', trans( 0.05.0,    0),scale(1,1,1), rotate(0, 90,0),mode('空',0,0),tag'<if:><true:><false:><anime:>'
/attr,type'流',name'↓', trans(    0,0,-0.05),scale(1,1,1), rotate(0,180,0),mode('空',0,0),tag'<set:><get:>'
/attr,type'流',name'←', trans(-0.05,0,    0),scale(1,1,1), rotate(0,270,0),mode('空',0,0),tag'<var:><event:>'

// [/xz : XZ平面のマップ定義 ｜ /xy : XY平面のマップ定義 ｜ /yz : YZ平面のマップ定義]
//        name'm'     : マップ名
//        pos(x,y,z)  : 座標         ※全角マップ左上の原点座標を示す(座標x,座標y,座標z)
//        size(w,h,d) : マップサイズ ※全角マップのサイズ(幅w,高さh,奥行d) ※奥行は同じ全角マップを繰り返す

//※全角スペースをテキストエディタの設定で可視化します。

// 物体マップ
/def
/xz,name'F0',pos(0,0,0),size(8,8,1), /xz,name'F1',pos(0,0,1),size(8,8,1), /xz,name'City',pos(0,0,5),size(8,8,1) 

/map
Ｓ＃＃＃＃＃＃海,文　　　　　　＠,街街街街街街街街
＃＃＃＃＃＃＃＃,　　　　　　　　,街街街街街街街街
＃＃＃＃＃＃＃＃,　　　　　　　　,街街　街街街街街
＃＃＃＃＃＃＃＃,　　　　＃＃　　,街街街街街街街　
＃＃＃＃＃＃＃＃,　　　＃＃＃　　,街街街街街街　　
＃＃＃＃＃＃＃＃,　　　＃＃＃　　,街街街街街　　街
＃＃＃＃＃＃＃＃,　　　　　　　　,街街街街　　街街
夜＃＃＃＃＃＃Ｄ,　　　　　　　　,街街街　　街街街

// 属性マップ
/def
/xz,name'L1',layer(1),pos(0,0,1),size(8,8,1)
/atr
↓←←←←←←←
↓　　　　　　↑
↓　　　　　　↑
↓　　　　　　↑
↓　　　　　　↑
↓　　　　　　↑
↓　　　　　　↑
→→→→→→→↑
/end

```