dglでD言語くんを物理ベースで表示してみた

# dglって何？
GitHubのページはこちら
https://github.com/gecko0307/dgl

D言語で実装された、OpenGLを使ったグラフィックスライブラリです。
今のゲーム業界では一般的になっている物理ベースと呼ばれる考え方で、素材や光源などを計算して描画します。

GithubのREADMEに書かれている特徴としては以下になります。

- 完全にGCフリー
- Windows, Linux, OSX and FreeBSDをサポート
- ユーザー定義のイベントシステムとUnicodeをサポートしたキーボード入力
- スレッドでロードするリソースマネージャー
- 独自シーンファイルフォーマット(DGL3) をBlenderからの出力に対応
- テクスチャをPNG形式でロード
- マテリアル情報は人の読みやすい形のテキストファイルでロード
- 動的ソフトシャドウ
- 無制限の動的光源
- GLSL shaders
- ノーマルマップと視差マップをサポート
- イメージベースライティング
- 物理ベースレンダリング (PBR)
- 2D,3Dの無制限のレンダーパス
- レンダーテクスチャーのサポート
- アンチエイリアス
- 追尾カメラを標準装備
- 3D geometric shapes
- アニメーションする2Dスプライト
- ユニコードサポートしたTTFフォントの2Dテキスト描画
- VFS
- 設定コンフィグ搭載

# サンプルのビルド
サンプルに

- minimal.d
- pbr.d
- texio.d

がありますが、pbr.dがモデルを表示するサンプルのなので、こちらをビルドしてみましょう。
![dgl_1.jpg](https://qiita-image-store.s3.amazonaws.com/0/65434/7384b2bd-9eac-c00e-15c1-ac1bfc96c86a.jpeg)

イカス銃が表示されると思います。
このサンプルを元にD言語くんを表示してみましょう。
それにはまず、D言語くんをdglで読める形式で出力し直す必要があります。

# モデルデータの作成
dglはOBJやFBXのような一般的なモデルデータは読み込めません。
Blenderからのエクスポーターが用意されているので、まずはD言語くんのモデルデータをBlenderで読み込めるようにします。

データはｲ五さんの作られたモデル
https://onedrive.live.com/?cid=F7E340CFC6666A27&id=F7E340CFC6666A27%21574
をお借りします。

D言語くんのモデルデータは、pmd形式とFBX形式とmqo形式で配布されていますので、これをなんとかしてBlenderに読み込ませるという、D言語とはまったく関係ない所で頑張る必要があります。
## FBXで読み込む
FBXのデータのバージョンが6.1と古くて読み込めなかった。
FBX Converterで出力し直してみましたが、そのデータをBlenderで読み込むと、何故かBlenderがフリーズしてしまうので、今のところ解決方法が見つかってません。
## mqoで読み込む
メタセコイア形式をBlenderで読み込むプラグインが、こちら
http://nullorempry.jimdo.com/work-list/blender2-5-2-7/mqo/
で公開されていたのでを使ってみたのですが、正しく読み込まれませんでした。
ってか、君は誰？
![d_man.jpg](https://dl.dropboxusercontent.com/u/148889/d_man.jpg)

## pmdで読み込む
こちらのツール
https://github.com/sugiany/blender_mmd_tools
を使うと正しくインポート出来ました。
（業界標準フォーマットのfbxとは一体・・・）
ただ、テクスチャーが貼られていないようなのですが、マテリアル情報は別にテキストで書くので、今回は問題無さそうです。

# モデルデータの読み込み
pbr.dを改造して、D言語くんを読み込めるようにします。

dataディレクトリ以下に、d_manというディレクトリを作成し、出力されたd_man.dgl3とテクスチャのd_man.pngをコピーします。
マテリアル情報はテキストなので、Material0.matというテキストファイルを作り、とりあえず
` name: "Material0";
ambientColor: [0.3, 0.4, 0.5, 1.0];
diffuseColor: [1.0, 1.0, 1.0, 1.0];
specularColor: [1.0, 1.0, 1.0, 1.0];
emissionColor: [0.0, 0.0, 0.0, 1.0];
roughness: 0.5;
specularity: 0.7;
diffuseTexture: "d_man.png";` 
と記入してください。

dglは仮想ファイルシステムの仕組みがあり、アセットの読み込み時にディレクトリ指定を省略することが出来るので、先程作成したd_manのディレクトリも追加します。

```d:pbr.d
    mountDirectory("data");
    mountDirectory("data/cerberus");
    mountDirectory("data/d_man");//追加
```

そして、cerberus.dgl3を読み込んでいる所をd言語くんを読み込むように変更します。

```d:pbr.d
//  model = addModelResource("cerberus.dgl3");
    model = addModelResource("d_man.dgl3");
```

これでビルドすると、以下のような画面が表示されると思います。
ホイールでカメラを近づけられるので、好きなだけD言語くんを愛でてみましょう。

![dgl_2.jpg](https://dl.dropboxusercontent.com/u/148889/dgl_2.jpg)

# もっと物理ベースっぽく
せっかく物理ベースの描画システムが使われているので、手を加えてみようと思います。
お手軽な所で、テクスチャを追加してみましょう。
d_man.pngをコピーして、d_man_2.pngという名前にしてペイントで開き、中身を全部白にしてしまいましょう。
先程作成したMaterial0.matに、
`metallicTexture: "d_man_2.png";`
を追加します。
metallicTextureというのは、どれくらい金属的かを表すデータを入れたテクスチャで、どれくらい光を反射するかの情報になります。
先程真っ白にしたので、反射率が1.0の全反射になります。
この状態で、もう一度実行してみましょう。

![dgl_3.jpg](https://dl.dropboxusercontent.com/u/148889/dgl_3.jpg)

はい、周りの景色が写り込んで、つやつやメタリックなD言語くんになりました。

# 光源を置いてみよう
無制限に動的光源が置けるのがウリの一つなので、せっかくだから点光源を置いてみましょう。
## ライトの追加
pointLightを作成して、Application3Dが持っているLightManagerに追加すると、点光源がシーンに反映されます。
pointLightの第一引数は位置で、第二は拡散光、第三は環境光の色になります。
点光源なのに環境光の設定があるのがよく分からないところですが、平行光源と同じ引数を使ってるだけなのかも？

## イベント
dglは内部でSDLを使用しているので、SDLのイベントを使うことが出来るクラスが用意されています。
Application3Dが持っているEventManagerにEventListenerを派生したクラスを登録し、そのクラスでインベントを処理します。
具体的な例として、スペースキーが点光源の位置がY方向に１０度移動するクラスは以下になります。


```d:pbr.d
class LightListener: EventListener
{
	Light light;
	Vector3f pos;
	float degree = 0.0f;
	this(EventManager emngr, LightManager lightManager, Vector3f in_pos, Color4f in_col)
	{
        super(emngr);
		pos = in_pos;
		light = lightManager.addLight(pointLight(in_pos, in_col, Color4f(0.1f, 0.1f, 0.1f, 1.0f)));
	}

    ~this()
    {
        Delete(light);
    }

    override void onKeyDown(int key)
    {
        if (key == SDLK_SPACE)
		{
			degree += 10.0f;
			float rad = degree*3.14f/180.0f;
			light.position = Vector3f(pos.x, pos.y*std.math.cos(rad), pos.z);
		}
    }
}
```
これを登録するリストをSimple3DAppのメンバーに持たせます。
せっかくなんで、ライブラリ側で用意してるリストを使います。
`    DynamicArray!LightListener pointLights;`
これに対して、以下のような形で、ライトを追加します。

```d:pbr.d
		LightListener light1 = New!LightListener(eventManager, lightManager, Vector3f(0.0, -5.0, 0.0), Color4f(0.0f, 1.0f, 1.0f, 1.0f));
		LightListener light2 = New!LightListener(eventManager, lightManager, Vector3f(0.0, 5.0, 0.0), Color4f(0.0f, 1.0f, 1.0f, 1.0f));
		pointLights.append(light1);
		pointLights.append(light2);
```

あとは、onUpdate関数の中で、各ライトに対してイベント処理を実行するようにprocessEvents関数を呼び出します。

```d:pbr.d
		foreach(light;pointLights)
		{
			light.processEvents();
		}
```

これで、スペースキーを押したイベントが処理されて、光源が動くようになります。
![dgl_4.jpg](https://dl.dropboxusercontent.com/u/148889/dgl_4.jpg)

# まとめ
データの出力というD言語と全然関係ない所で時間が取られてしまい、あまり詳しい説明が書けなくて、ごめんなさい。
dglは、同じ作者が作った[dlib](https://github.com/gecko0307/dlib)というライブラリ群が使われています。
結構な量のライブラリで、GCフリーのコンテナや計算ライブラリや画像ローダー等揃っているので、読んでみると面白いと思います。

dglにはまだモーション再生など無いですが、将来はIQM(Inter-Quake Model ）というフォーマットをサポートして、モーション再生をする予定みたいです。
あとは、ドキュメントがほぼ無いので、何をやるにもコードを読まなきゃいけないのが、結構大変です。
同じ作者が[dmech](https://github.com/gecko0307/dmech)いう物理エンジンライブラリも作っていて、それとdglを組み合わせた、[atrium](https://github.com/gecko0307/atrium)というゲーム
を作っているので、そちらを参考にすると良いかもしれません。

機能はそれなりに揃っているので、モーション再生がサポートされたら、割りと使えるライブラリになりそうな気がします。
ただ、完全にGCフリーなのをウリにしてるので、開放は常に自分でやらなければならないので、慣れが必要になりそうです。

最後に、最終的なコードを貼っておきます。

```d:pbr.d
/*
Copyright (c) 2016 Timur Gafarov 

Boost Software License - Version 1.0 - August 17th, 2003

Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:

The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/

module pbr;

import std.stdio;
import std.math;

import dlib.core.memory;
import dlib.core.stream;
import dlib.math.vector;
import dlib.math.affine;
import dlib.math.quaternion;
import dlib.math.utils;
import dlib.image.color;
import dlib.filesystem.filesystem;

import dgl.core.api;
import dgl.core.event;
import dgl.core.application;
import dgl.core.interfaces;

import dgl.templates.app3d;
import dgl.templates.freeview;

import dgl.graphics.entity;
import dgl.graphics.camera;
import dgl.graphics.shapes;
import dgl.graphics.material;
import dgl.graphics.texture;
import dgl.graphics.shader;
import dgl.graphics.shadow;
import dgl.graphics.state;
import dgl.graphics.light;
import dgl.graphics.pbrshader;

import dgl.asset.dgl3;
import dgl.asset.envtexture;

import dlib.container.array;

class EnvironmentSphere: Drawable
{
    Camera camera;
    ShapeSphere sphere;
    float radius = 50.0f;
    
    this(Camera cam)
    {
        camera = cam;
        sphere = New!ShapeSphere(1);
    }
    
    void draw(double dt)
    {
        glPushMatrix();
        Vector3f pos = camera.getTransformation.translation;
        glTranslatef(pos.x, pos.y, pos.z);
        glRotatef(180.0f, 0, 1, 0);
        glRotatef(-90.0f, 1, 0, 0);
        glScalef(-radius, -radius, -radius);
        glDisable(GL_LIGHTING);
        glDepthMask(GL_FALSE);
        sphere.draw(dt);
        glDepthMask(GL_TRUE);
        glEnable(GL_LIGHTING);
        glPopMatrix();
    }
    
    ~this()
    {
        Delete(sphere);
    }
}

class LightListener: EventListener
{
	Light light;
	Vector3f pos;
	float degree = 0.0f;
	this(EventManager emngr, LightManager lightManager, Vector3f in_pos, Color4f in_col)
	{
        super(emngr);
		pos = in_pos;
		light = lightManager.addLight(pointLight(in_pos, in_col, Color4f(0.1f, 0.1f, 0.1f, 1.0f)));
	}

    ~this()
    {
        Delete(light);
    }

    override void onKeyDown(int key)
    {
        if (key == SDLK_SPACE)
		{
			degree += 10.0f;
			float rad = degree*3.14f/180.0f;
			light.position = Vector3f(pos.x, pos.y*std.math.cos(rad), pos.z);
		}
    }
}

enum SHADOW_GROUP = 100;

class Simple3DApp: Application3D
{
    Freeview freeview;
    DGL3Resource model;
    
    ShadowMapPass shadow; 
    
    PBRShader pbrShader;
    
    EnvironmentSphere envSphere;
    Material envMat;
    
    EnvTextureResource ambTexRes;
    
    DynamicArray!LightListener pointLights;

    this()
    {
        super();

        Quaternionf sunLightRot = 
            rotationQuaternion(1, degtorad(0.0f)) *
            rotationQuaternion(0, degtorad(-30.0f));

        if (useShadows)
        {
            PipelineState.shadowMapSize = 2048;
            shadow = New!ShadowMapPass(PipelineState.shadowMapSize, scene3d, SHADOW_GROUP, eventManager);
            addPass3D(shadow);
            shadow.lightRotation = sunLightRot;
            shadow.projectionSize = 20;
        }

        LightManager.sunEnabled = true;
        LightManager.sunPosition = Vector4f(sunLightRot.rotate(Vector3f(0, 0, 1)));
        LightManager.sunPosition.w = 0.0f;
        LightManager.sunColor = Color4f(0.9, 0.9, 0.8, 1);
        
        Material.setFogDistance(50, 200);
        Material.setFogColor(Color4f(0.5, 0.8, 1.0, 1.0));
        
        setDefaultLoadingImage("data/loading.png");
        
        mountDirectory("data");
        mountDirectory("data/cerberus");
        mountDirectory("data/d_man");

        ambTexRes = addEnvironmentTexureResource("zion-sunsetpeek.png");
        //model = addModelResource("cerberus.dgl3");
        model = addModelResource("d_man.dgl3");
        
        loadResources();
        
        freeview = New!Freeview(eventManager);
        
        envSphere = New!EnvironmentSphere(freeview.camera);
        auto envSphereEntity = createEntity3D(envSphere);
        envMat = New!Material();
        envMat.textures[0] = ambTexRes.texture;
        envSphereEntity.material = envMat;
        
        pbrShader = New!PBRShader;
                
        addEntitiesFromModel(model);

		LightListener light1 = New!LightListener(eventManager, lightManager, Vector3f(0.0, -5.0, 0.0), Color4f(0.0f, 1.0f, 1.0f, 1.0f));
		LightListener light2 = New!LightListener(eventManager, lightManager, Vector3f(0.0, 5.0, 0.0), Color4f(0.0f, 1.0f, 1.0f, 1.0f));
		pointLights.append(light1);
		pointLights.append(light2);
    }
    
    EnvTextureResource addEnvironmentTexureResource(string filename)
    {
        EnvTextureResource res = New!EnvTextureResource(resourceManager.imageFactory);
        resourceManager.addResource(filename, res);
        return res;
    }

    ~this()
    {
        foreach(light; pointLights.data)
		{
            Delete(light);
		}
        pointLights.free();

        Delete(pbrShader);
        Delete(freeview);
        Delete(envSphere);
        Delete(envMat);
    }
    
    override void onUpdate(double dt)
    {
        super.onUpdate(dt);

		foreach(light;pointLights)
		{
			light.processEvents();
		}

        freeview.update();
        setCameraMatrix(freeview.getCameraMatrix());
        
        if (Material.uberShader)
            Material.uberShader.setViewMatrix(freeview.camera);
        if (pbrShader)
            pbrShader.setViewMatrix(freeview.camera);
        
        if (shadow)
            shadow.update(dt);
    }
    
    override void onRedraw(double dt)
    {
        if (shadow)
            shadow.bind(freeview.camera.getTransformation());       
        super.onRedraw(dt);
        if (shadow)
            shadow.unbind();
    }

    void addEntitiesFromModel(DGL3Resource model)
    {
        foreach(e; model.entities)
        {
            if (useShadows)
                e.groupID = SHADOW_GROUP;

            addEntity3D(e);

            if (e.material)
            {
                if (useShaders)
                {
                    e.material.setShader(pbrShader);                    
                    ambTexRes.applyToMaterial(e.material);
                }
                else
                {
                    e.material.textures[1] = null;
                    e.material.textures[2] = null;
                    e.material.textures[3] = null;
                    e.material.textures[4] = null;
                }
            }
        }
    }

    override void onKeyDown(int key)
    {
        if (key == SDLK_ESCAPE)
        {
            exit();
        }
    }
}

void main()
{
    writefln("Allocated memory: %s byte(s)", allocatedMemory);
    initDGL();
    auto app = New!Simple3DApp();
    app.run();
    Delete(app);
    deinitDGL();
    writefln("Allocated memory: %s byte(s)", allocatedMemory);
}
```