読者です 読者をやめる 読者になる 読者になる

S.F. Page

Programming,Music,etc...

electronでcanvasの画像データ化スピードを上げる&YouTubeアップロード後の画質の劣化について

electron three.js

RYDEENの曲に合う動画をelectron+three.jsで作っている。

https://o2jkug-bn1305.files.1drv.com/y3mDmyj3LIMse4F1uf3pQls9-JPFmtLSFuU4mD5eGxkUTdGf0YhG5J524V-TjUSrL1XcPQ7FljW94xWQbBgsfsRMUMCGeyYCCxSiih0EOyPky5ZrMXjiI-42k4coAGQiNrU5ks1poc5zjjqApJLN_W_YUN_nczY5U0MBY-R9cuhDd4?width=984&height=556&cropmode=none

動画化はどのように行っているかというと、canvasでの描画結果を複数のビットマップファイルに落として、ffmpegでオーディオ・ファイルとマージしてmp4形式にしている。 肝の部分は描画結果をビットマップファイルに落とすところで、今までは以下の方法で行っていた。

  1. canvasにthree.jsで1フレーム描画
  2. canvasのtoDataURL()でpng画像データ化(data URL形式)
  3. 先頭から「,」までを取り除き、BASE64文字列化
  4. new Buffer((文字列),'base64')オブジェクトでバイナリデータ化
  5. Bufferオブジェクトの中身をファイル名.pngとして保存
  6. 必要なフレーム分 1-5を繰り返し

コードにすると以下のような感じ。

var data = d3.select('#console').node().toDataURL('image/png');
data = data.substr(data.indexOf(',') + 1);
var buffer = new Buffer(data, 'base64');
writeFile('./temp/out' + ('000000' + frameNo.toString(10)).slice(-6) + '.png',buffer,'binary'));

しばらくの間これで問題はなかったが、1080pの解像度で複雑な描画を行うようになると処理時間がすごくかかるようになってしまった。エンコードとファイル保存に時間がかかってしまっているらしい。 なのでコードを改良することにした。調べた結果jpeg形式にするとファイルサイズが小さくなるので保存が速くなるらしい。あと気になるのはバイナリデータであるcanvasビットマップをわざわざData URL化し、さらにそれをバイナリデータ化するという冗長な処理である。なんかうまい方法ないだろうかと調べたら、three.jsのreadRenderTargetPixels()を使えば何とかなりそうだ。

私はthree.jsのexampleで使われているEffectComposerという、ポストプロセスをするためのヘルパークラスを使っている。 このEffectComposerはWebGLRenderTargetを介して、ポストプロセスを行うためのものである。さらに複数のポストプロセスをチェインする機能も持っている。 なのでポストプロセスで受け渡されるWebGLRenderTargetreadRenderTargetPixels()で読みだせば、ピクセルデータがバイナリで得られる。なのでポストプロセスの1つとしてキャプチャーパスを作ることにした。


class SFCapturePass extends THREE.Pass {
    constructor(width = 0, height = 0) {
        super();

        this.buffers = [];
        for (let i = 0; i < 4; ++i) {
          this.buffers.push(new Uint8Array(width * height * 4));
        }
        this.bufferIndex = 0;
        this.currentIndex = 0;
        this.width = width;
        this.height = height;


        this.uniforms = THREE.UniformsUtils.clone(THREE.CopyShader.uniforms);

        this.material = new THREE.ShaderMaterial({
            uniforms: this.uniforms,
            vertexShader: THREE.CopyShader.vertexShader,
            fragmentShader: THREE.CopyShader.fragmentShader
        });

        this.camera = new THREE.OrthographicCamera(- 1, 1, 1, - 1, 0, 1);
        this.scene = new THREE.Scene();

        this.quad = new THREE.Mesh(new THREE.PlaneBufferGeometry(2, 2), null);
        this.scene.add(this.quad);
    }

    render(renderer, writeBuffer, readBuffer, delta, maskActive) {
        this.currentIndex = this.bufferIndex;
        renderer.readRenderTargetPixels(readBuffer, 0, 0, this.width, this.height, this.buffers[this.bufferIndex]);
        this.bufferIndex = (this.bufferIndex + 1) & 3;

        this.uniforms["tDiffuse"].value = readBuffer.texture;
        this.quad.material = this.material;
        if (this.renderToScreen) {
            renderer.render(this.scene, this.camera);
        } else {
            renderer.render(this.scene, this.camera, writeBuffer, this.clear);
        }
    }
}

THREE.SFCapturePass = SFCapturePass;

これをEffectComposerに追加し、render()するとbuffersプロパティに描画ビットマップデータの過去4画面分が格納される。 なぜ過去4画面なのかというと1画面分だけだとファイル保存中に書き込まれる可能性があるような気がして、念のためバッファを4画面用意することにした。 意味はないかもしれない。

さらにピクセルデータをjpeg方式で保存するためにsharpというネイティブ・モジュールを使用することにした。が、これをelectronで使用するには下の方法でインストール後ビルドしなおす必要があった。

electron/using-native-node-modules.md at master · electron/electron · GitHub

保存するコードは以下のような感じ。

var sharp = require('sharp');

function saveImage(buffer,path,width,height)
{
  return new Promise((resolve,reject)=>{
    sharp(buffer,{raw:{width:width,height:height,channels:4}})
    .rotate(180)
    .jpeg()
    .toFile(path,(err)=>{
      if(err) reject(err);
      resolve();      
    });
  });

}

.
.

saveImage(new Buffer(sfCapturePass.buffers[sfCapturePass.currentIndex].buffer),'./temp/out' + ('000000' + frameNo.toString(10)).slice(-6) + '.jpeg',WIDTH,HEIGHT);

レポジトリ:

github.com

結果だが処理速度はかなり速くなり、我慢できる程度にはなった。

が動画をYouTubeにアップすると、画質がものすごく荒れる。再エンコードされるのはしょうがないんだけど、ローカルで観ている画質とはかなり違ってしまっている。推奨されるパラメータでエンコードして、ビットレートも8M bps以下に落としてはいるんだけどね。実写とかだと、背景はそこそこ固定されているし、動く部分が少ないのでそんなに荒れないんだけどね。やっぱり画面全体が激しく変化するようなものは難しいのだろうか。ちょっとフレームレートを落としてみようかなとも思っている。でも落としすぎるとカクカクしちゃうしなあ。。