Three.jsベースのVJ用シーンマネージャーを作った話

## 作ったもの
![vthreetest02.gif](https://qiita-image-store.s3.amazonaws.com/0/54225/a7ec940f-bf1b-4441-a4c5-557ce45388e3.gif "vthreetest02.gif")
はじめて作った、ブラウザとキーボードのみのVJアプリの一部抜粋です。


## 対象者
* Three.jsの初心者
* 入門用のサンプル見たけど、どうやってコンテンツ作っていいか未だピンときてない人。
* TypeScriptが何となく分かる人



## Three.jsでVJをやりたい
オーディオビジュアルパフォーマンスをやることになり、せっかく勉強してるThree.jsを使いたいということで、簡単なシーンマネージャーを作ることにしました。


## 構想
* 左右のキーで複数のシーンを切り替え
* 入力イベントなどを後から追加したりできるよう、拡張性は最低限ほしい

後は作りながら固める

## どのように作ったか
### そもそもThree.jsでレンダリングするまでのプロセスを把握する
だいたい以下のような仕組みになる。

1. まずはSceneを作る
2. Sceneの中にオブジェクトを入れる
3. Rendererを作る
4. Cameraを作る
5. RendererにCameraとSceneを受け渡す

これらを踏まえて以下のようなステップでシーンをマネージメントしたい

* Scene,Camera,オブジェクトを内包した独自のオブジェクトを作る
* 例えば3つのシーンを切り替えたいなら、上記を3つ作り、配列におさめる
* Rendererは一つだけ用意し、配列の中身をローテーションして渡す


以上の仕組みでなんとかなりそう。
オブジェクト思考だと実装が楽なので、TypeScriptを今回は採用した。
Three.jsはTypeScriptだと比較的ラクです。


## 子Sceneクラス
```JavaScript
/// <reference path="typings/index.d.ts" />


class SceneBoxA {

    public scene: THREE.Scene;
    public camera: THREE.Camera;
    private Box:THREE.Mesh;
    private timer:number = 0;


    constructor() {

        this.createScene();

    }

    // シーンを作る。ここでオブジェクトを格納していく。
    private createScene(){

        // シーンを作る
        this.scene = new THREE.Scene();

        // カメラを作成
        this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 10000 );
        this.camera.position.z = 1000;

        this.Box = new THREE.Mesh(
            new THREE.BoxGeometry(50,50,50),
            new THREE.MeshBasicMaterial(0xffffff)
        );

        this.scene.add(this.Box);

    }

    // ワンフレームごとの処理
    public update() {

        // ❑の横運動
        this.timer += 0.1;
        this.Box.position.x = 50 * Math.sin(this.timer);
    }


}


// *********** ふたつめのシーン *********** //

class SceneBoxB {

    public scene: THREE.Scene;
    public camera: THREE.Camera;
    private Box:THREE.Mesh;
    private timer:number = 0;


    constructor() {

        this.createScene();

    }

    private createScene(){

        this.scene = new THREE.Scene();

        this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 10000 );
        this.camera.position.z = 1000;


        this.Box = new THREE.Mesh(
            new THREE.BoxGeometry(50,50,50),
            new THREE.MeshBasicMaterial(0x888888)
        );

        this.scene.add(this.Box);

    }

    public update() {

        // ❑の縦運動
        this.timer += 0.1;
        this.Box.position.y = 50 * Math.sin(this.timer);
    }


}

```
<br>



今回は極力シンプルなものを用意しました。
それぞれのオブジェクトの中に入ってるのは

* シーン
* カメラ
* ボックスのオブジェクト

のみです。
Meshオブジェクトは自身が持っているシーンに格納し、
あとはアニメーションの処理をupdate()内に書きます。
シーンAはボックスが横にゆらゆら、シーンBは縦にゆらゆらします。

## 親シーンマネージャークラス

```JavaScript
class VThree
{
    // 現在のシーンの番号
    public NUM:number = 0;
    // シーンを格納する配列
    public scenes:any[] = [];
    // Renderer
    public renderer:THREE.Renderer;

    constructor()
    {
        // 初期化処理後、イベント登録
        this.init();

        document.addEventListener( 'resize', this.onWindowResize, false );
        document.addEventListener("keydown", this.onKeyDown, true);

    }


    public init()
    {

        // Rendererを作る

        this.renderer = new THREE.WebGLRenderer({antialias: true});
        this.renderer.setPixelRatio( window.devicePixelRatio );
        this.renderer.setSize( window.innerWidth, window.innerHeight );
        this.renderer.sortObjects = false;
        this.renderer.shadowMap.enabled = true;
        this.renderer.shadowMap.type = THREE.PCFShadowMap;
        this.renderer.domElement.id = "main";
        document.body.appendChild( this.renderer.domElement );

    }

    // 管理したいシーンを格納する関数

    public addScene(scene:Object)
    {

        this.scenes.push(scene);

    }




    // ウィンドウの幅が変わったときの処理
    public onWindowResize = () =>
    {
        this.renderer.setSize( window.innerWidth, window.innerHeight );
    }

    // 現在のシーン番号が、不適切な値にならないようにチェック
    public checkNum = () =>
    {
        if(this.NUM <0)
        {
            this.NUM = this.scenes.length-1;
        }

        if(this.NUM >= this.scenes.length)
        {
            this.NUM = 0;
        }

    }

    // ←→キーでシーン番号を足し引き

    public onKeyDown = (e:KeyboardEvent) => {

        console.log(e);
        // console.log(this.NUM);
        if(e.key == "ArrowRight")
        {
            this.NUM++;
            this.checkNum();
        }
        if( e.key == "ArrowLeft")
        {

            this.NUM--;
            this.checkNum();
        }

        console.log(this.NUM);

    }

    // 最終的な描写処理と、アニメーション関数をワンフレームごとに実行
    public draw() {

        this.scenes[this.NUM].update();
        this.renderer.render(this.scenes[this.NUM].scene, this.scenes[this.NUM].camera);
        requestAnimationFrame(this.draw.bind(this));

    }
}

```
<br>
  
Three.jsベースのVJ用のマネージャーということでVThreeというクラスを作ります。
基本的にはこの関数のインスタンスにシーンを突っ込んで、最終的にdraw()関数を呼ぶだけです。

## 最終的なHTML
```HTML
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<style>
    body {
        width: 100%;
        height:100%;
        margin: 0;
        background-color: #000
    }
    canvas {
        width: 100%;
        height: 100%;
        position: fixed;
        top: 0;
        left: 0;
    }

</style>
<body>
<script type="text/javascript" src="js/three.min.js"></script>
<script type="text/javascript" src="js/VThree.js"></script>
<script type="text/javascript" src="js/template.js"></script>

<script>
    (function () {

        // VThreeのインスタンスを作成
        var vthree = new VThree();
        // 用意したシーンのインスタンスを作成
        var sceneBoxA = new SceneBoxA();
        var sceneBoxB = new SceneBoxB();
        // vthreeオブジェクトに突っ込む
        vthree.addScene(sceneBoxA);
        vthree.addScene(sceneBoxB);
        
        // ループ実行
        vthree.draw();

    })();
</script>
</body>
</html>

```
<br>
スッキリしましたね。



## 実行結果
![vthreetest.gif](https://qiita-image-store.s3.amazonaws.com/0/54225/969faafe-c28f-ae89-414f-21be49fac8d4.gif "vthreetest.gif")
http://localhost:63342/vj_project/VThreeTest.html

わかりにくいですが、左右のキーボタンを押してます。


## おまけ：最初のサンプルのやつについて

![Screen Shot 2016-12-15 at 03.52.29.png](https://qiita-image-store.s3.amazonaws.com/0/54225/2a65845c-922e-a497-82de-b70a05cfc47c.png "Screen Shot 2016-12-15 at 03.52.29.png")
https://murasaki-uma.github.io/webgl_vj/

最後のサンプルは複数シーンに加えて、

* 2つの映像を重ねて描写する
* 数字キーを叩いて直接その番号のシーンを呼ぶ
* シーンが変わるときはふわっとフェードイン・アウトさせる

といった細かい演出が入ってます。
しかしこのサンプルを作ってるときは、まだ上記のように完成されたスクリプトで無いのに加え、VJしながら直接コードを書き換えたりしてたのでかなりコードが汚い…。
いずれ作り直します！

ざっくり説明なので、気になったことがあればコメントやツイッターで直接リプもらえると助かります！