PlayCanvasでH2MDを使ってみた

##InterBee2019でセッションしてました
2019/11/13~15にInterBee2019というイベントに出展しまして。

その11/14でセッションの登壇をしていました。
使用した資料は以下から閲覧できます。
https://support.playcanvas.jp/hc/ja/articles/360038755154

その中で、私キドが担当したH2MDの解説をしていたのですが、セッションでコードを解説しても
文字は小さいし、スライドの中に収めるのは難しかったので後日技術記事として公開するようにしました。

そして、今回の記事がその解説になります。
ちなみに、SlideShareでいう87ページ目でコードの解説をしようとしていますのでご参照くだしあ。


##H2MDとは
この記事から閲覧したかにはH2MDがなんなのかわからないかもしれませんので、こちらも説明をば。

H2MDは、株式会社アクセルが手がけるムービーミドルウェアなんですが、
ただのムービーミドルウェアではなく、「アルファ」ムービーミドルウェアなんです。
透過情報を持ったムービーをWebで使うことは出来ないことがありましたが、これを使うことで実現できちゃいます。

もともと、Unityなどのプラグインは用意されていたのですが、
なんとPlayCanvasのプラグインもあると！

このPlayCanvasのプラグインを使ってよりリッチなものにしちゃうっていうお話をセッションではしていました。

PlayCanvasでH2MDを使う際には、ムービーテクスチャとして使用するマテリアルを選択して、それにH2MDムービーを貼り付ける感じです。
これらはPlayCanvasプラグインのスクリプトを使うことで実現できます。

H2MD → https://h2md.jp/

##今回作ったプロジェクト
[こちらから](https://playcanvas.jp/example/vr-planet/)

スマホで見ればVRモードで体験することもできます。


##H2MDを適用したスクリプトの解説
H2MDを使うために書き換えたスクリプトを解説します。

まずは全貌を見せます。

```js
var H2MDTexture = pc.createScript('H2MDtexture');

H2MDTexture.attributes.add('materials', {
    type: 'asset',
    assetType: 'material',
    array: true
});

H2MDTexture.attributes.add('h2mdFile', {
    type: 'asset',
    assetType: 'binary',
    array: true
});

H2MDTexture.attributes.add('diffuseMap_array', {
    type: 'number',
    enum: [
        { 'true': 1 },
        { 'false': 2 }
    ],
    array: true
});

var h2mdfiles;

g_h2md = {};

var t_H2MDTexture = [];
var f_H2MDTexture = [];

// initialize code called once per entity
H2MDTexture.prototype.initialize = function() {
    h2mdfiles = this.h2mdFile;
    if(!h2mdfiles) {
        console.log("h2md loading error");
        return;
    }
    for (var i = 0; i < h2mdfiles.length; i++) {
        h2mdFunc(this,h2mdfiles[i],i);
    }
};

// update code called every frame
H2MDTexture.prototype.update = function(dt) {
    for (var i = 0; i < h2mdfiles.length; i++) {
        if(!f_H2MDTexture[i]){
            return;
        }
        t_H2MDTexture[i].upload();
    }
};

var h2mdFunc = function($this,$file,$count){
    var file = $this.app.assets.find($file.name);
    var filePath = file.getFileUrl();
    var app = $this.app;

    // Create a texture to hold the video frame data
    var _H2MDTexture = new pc.Texture(app.graphicsDevice, {
        format: pc.PIXELFORMAT_R8_G8_B8_A8,
        autoMipmap: false
    });
    _H2MDTexture.minFilter = pc.FILTER_LINEAR;
    _H2MDTexture.magFilter = pc.FILTER_LINEAR;
    _H2MDTexture.addressU = pc.ADDRESS_CLAMP_TO_EDGE;
    _H2MDTexture.addressV = pc.ADDRESS_CLAMP_TO_EDGE;    
    
    f_H2MDTexture[$count]=false;
    
    var h2md=new H2MD();
    h2md.error(function(error){ console.log(error); });

    var self=$this;
    
    h2md.open_packed(filePath,function(){ 
        console.log("h2md",$count);
        h2mdRender($count);
    });
    h2md.done(function(){ 
        console.log("done : end");
    });
    h2md.buffered(function(){
        console.log("buffered : end");
    });

    var h2mdRender = function($f_count){
        var videoCanvas = document.createElement("canvas");
        videoCanvas.width=h2md.info().width;
        videoCanvas.height=h2md.info().height;

        h2md.canvas(videoCanvas); //require h2md 1.3.1
        _H2MDTexture.setSource(videoCanvas);
        _H2MDTexture.upload();

        var material = self.materials[$f_count].resource;
        console.log([$f_count],self.diffuseMap_array[$f_count]);
        if(self.diffuseMap_array[$f_count] === 1){
            material.diffuseMap = _H2MDTexture;
        }
        material.emissiveMap = _H2MDTexture;
        material.update();

        h2md.loop(0); 
        h2md.play();

        t_H2MDTexture[$f_count] = _H2MDTexture;

        f_H2MDTexture[$f_count] = true;
    };
};
```

H2MDのPlayCanvasプラグインにある`h2mdtexture.js`を改変して
使っています。
これとh2md.min.jsというH2MDのムービーそのものを動かすためのスクリプトがあるのでこれも読み込ませる必要があります。

それでは上記から少しずつ解説していきます。

###属性

```js
H2MDTexture.attributes.add('materials', {
    type: 'asset',
    assetType: 'material',
    array: true
});

H2MDTexture.attributes.add('h2mdFile', {
    type: 'asset',
    assetType: 'binary',
    array: true
});

H2MDTexture.attributes.add('diffuseMap_array', {
    type: 'number',
    enum: [
        { 'true': 1 },
        { 'false': 2 }
    ],
    array: true
});
```
属性は配列で登録できるようにしました。
H2MDとマテリアルを選択しますが、配列の1番目、2番目、と言った感じで一つのスクリプトファイルで管理しちゃいます。
`diffuseMap_array`についてですが、後に説明しますが、選択したマテリアルのどのテクスチャマップにH2MDを差し替えるか処理を書きます。
そのテクスチャマップを差し替えるか差し替えないかを選択できるようにしています。
ここではDiffuseMapをbooleanで管理するようにしました。

![スクリーンショット 2019-11-20 12.59.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109168/882784bc-a1cf-9a92-d422-009af88f3bff.png)
Editorではこんな感じで設定できます。
Array Sizeで配列の数を決めて一つずつ設定していく感じです。


###init & update
```js
var h2mdfiles;

g_h2md = {};

var t_H2MDTexture = [];
var f_H2MDTexture = [];

// initialize code called once per entity
H2MDTexture.prototype.initialize = function() {
    h2mdfiles = this.h2mdFile;
    if(!h2mdfiles) {
        console.log("h2md loading error");
        return;
    }
    for (var i = 0; i < h2mdfiles.length; i++) {
        h2mdFunc(this,h2mdfiles[i],i);
    }
};

// update code called every frame
H2MDTexture.prototype.update = function(dt) {
    for (var i = 0; i < h2mdfiles.length; i++) {
        if(!f_H2MDTexture[i]){
            return;
        }
        t_H2MDTexture[i].upload();
    }
};
```

initとupdateではぞくせいで　読み込んだデータを使いやすくするために処理しています。
基本的には配列で管理するようにしています。なんとなく、配列がみやすいかなと思いまして…


###H2MD Function
```js
var h2mdFunc = function($this,$file,$count){
    var file = $this.app.assets.find($file.name);
    var filePath = file.getFileUrl();
    var app = $this.app;

    // Create a texture to hold the video frame data
    var _H2MDTexture = new pc.Texture(app.graphicsDevice, {
        format: pc.PIXELFORMAT_R8_G8_B8_A8,
        autoMipmap: false
    });
    _H2MDTexture.minFilter = pc.FILTER_LINEAR;
    _H2MDTexture.magFilter = pc.FILTER_LINEAR;
    _H2MDTexture.addressU = pc.ADDRESS_CLAMP_TO_EDGE;
    _H2MDTexture.addressV = pc.ADDRESS_CLAMP_TO_EDGE;    

    f_H2MDTexture[$count]=false;

    var h2md=new H2MD();
    h2md.error(function(error){ console.log(error); });

    var self=$this;

    h2md.open_packed(filePath,function(){ 
        console.log("h2md",$count);
        h2mdRender($count);
    });
    h2md.done(function(){ 
        console.log("done : end");
    });
    h2md.buffered(function(){
        console.log("buffered : end");
    });

    var h2mdRender = function($f_count){
        var videoCanvas = document.createElement("canvas");
        videoCanvas.width=h2md.info().width;
        videoCanvas.height=h2md.info().height;

        h2md.canvas(videoCanvas); //require h2md 1.3.1
        _H2MDTexture.setSource(videoCanvas);
        _H2MDTexture.upload();

        var material = self.materials[$f_count].resource;
        console.log([$f_count],self.diffuseMap_array[$f_count]);
        if(self.diffuseMap_array[$f_count] === 1){
            material.diffuseMap = _H2MDTexture;
        }
        material.emissiveMap = _H2MDTexture;
        material.update();

        h2md.loop(0); 
        h2md.play();

        t_H2MDTexture[$f_count] = _H2MDTexture;
        f_H2MDTexture[$f_count] = true;
    };
};
```

この関数がH2MDの処理を行うための大きな処理になります。
ほとんどがH2MDのPlayCanvasプラグインであるスクリプトのコードです。
書き換えたのは引数の箇所ぐらいですね。
これをinitializeで書いていますが、for文で回して読み込むようにしています。

以下のコードではどのテクスチャマップにH2MDを適用させるかを処理しています。
ここでDiffuseMapとかEmissiveMap、OpacityMapなどを選択することで反映することができます。
任意のテクスチャマップを選択することもできるのでいい感じに書き換えてあげるといいかんじになるかも。

```js
    var material = self.materials[$f_count].resource;
        console.log([$f_count],self.diffuseMap_array[$f_count]);
        if(self.diffuseMap_array[$f_count] === 1){
            material.diffuseMap = _H2MDTexture;
        }
        material.emissiveMap = _H2MDTexture;
        material.update();
```

こうしてみると結構単純ですが、これができるのもH2MDのスクリプトのおかげですね。





##まとめ
短めでしたが、大体はH2MDのプラグインのおかげで出来てしまう感じですね。
株式会社アクセルが用意しているサンプルプロジェクトもありますのでご参照ください。
書き方とか設定の仕方をみることもできると思います。
https://h2md.jp/playcanvas

単純に板ポリに貼り付けるのもいいですが、下図のような装飾のテクスチャマップなどにも使えるのでいろんな魅せ方が出来ます。
![h2md_sandy.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109168/794b5444-5625-8ead-c9ea-a948a715fa21.gif)

と言った感じで、InterBee2019のセッションでは説明できなかった技術の解説でした。