JavaScript
WebGL
TypeScript

Generatorを用いたレンダリングループが簡単な実験に便利なんじゃないかって話

グラフィクスAPIを用いるプログラムを多数書くとちょっとした実験を回して試してみたいことが度々ある。僕が少し実験するときによく使っているジェネレーターをもちいる方法をここでは紹介しよう。
レンダリングループという概念を一種のジェネレータとして見てみると様々な便利な点が見えてくる。
(Unityを知っている人ならコルーチンの話をしているだけじゃんっていうしょうもない記事である)

前提(ジェネレータ関数の話)

あたかも途中で止められるように振る舞う関数のことをジェネレータ関数という。例えば、Javascriptでは以下のようなfunctionの末尾に*のついた関数を定義すると途中で止められるように振る舞う。

main.js
function* test(){
  yield 1;
  yield 2;
  yield 3;
}

この関数に対して以下のような呼び出しを行えば、1,2,3と順番に表示されて終わる。

main.js
for(let i of test()){
   console.log(i);
}

これは以下と同じ。

main.js
let itr = test();
while(!itr.done){
   console.log(itr.next())
}

レンダリングループとして使う

これが強いのは無限ループをジェネレータに普通に書いた場合。

main.js
function* test(){
   while(true){
      yield 1;
   }
}

この関数を普通に呼び出したらフリーズしてしまうがこういうふうに書くと無限に続くがフリーズしないジェネレータとして用いれる。

main.js
let iterator = test();
function onloop(){
   iterator.next();
   requestAnimationFrame(onloop);
}

さて、この関数の便利さは何だろうか。ループの外が最初のタイミングしか呼ばれないことを考えるとこれは初期化処理として使える。

main.js
function* test(){
   let buffer = gl.createBuffer();
   gl.bindBuffer(gl.ARRAY_BUFFER,buffer);
   // ....
   // 様々な初期化処理

   while(true){
      // 各レンダリングループ
   }
}

こうして書くと各レンダリングループ内で初期化時に使った変数を参照できる。updateやstartなどの関数を定義して毎フレーム呼び出すような仕組みを考えてメンバ変数に代入するような機構を書かなくても、ジェネレータを用いるとこういう構造がスマートにかける。

シーン遷移として使う

遷移の必要な複数個のiteratorがあって、順番に再生したいならJavascriptのイテレータは便利だ。yield*と書けばそのイテレータが終わるまでyieldし続けてくれる。これを用いるとシーン遷移が楽にできる。

main.js
function* main(){
   var scenes = [scene1(),scene2(),scene3()];
   scenes.forEach(s=>s.next()); // 最初だけ初期化のために呼んでおく
   for(let scene of scenes){
      yield* scene;
   }
}

一番メインのイテレータをこう書けば、scene1,scene2,scene3のイテレータを順番に再生してくれる。

main.js
function* scene1(){
   while(true){
      // scene1の更新処理
      if(/*scene1の終了判定*/){
         return;
      }
   }
}

アニメーションの合成に使う

複数個の物体がシーンの中を動いているとき、しっかりと書くならそれぞれのアニメーションを管理するクラスなどを設計して、ちゃんと毎回アップデートされるような構造をつくるべきだ。しかし、ちょっとした試作でせいぜい数個の物体を動かしたいだけならそれは不便だ。シーン中のアニメーション全体のイテレータを含む配列が一つあればこれは解決ができる。

main.js
function* main(){
   let animations = [anim1(),anim2(),anim3()];
   while(true){
      animations.forEach(a=>a.next());
      yield;
   }
}

最後に

レンダリングループは無限に続くループに見えるが、実際にはwhileでループを回していたらフリーズしてしまう。だからこそrequestAnimationFrameを用いるが、これだと全体のループとしての構造が見えにくい。
必要なのは初期化と更新だけでほとんどの場合十分なのだ。このプリミティブなレンダリングループにおけるそれぞれの構成要素は初期化とループでのアップデートだけ取れれば良いように思える。
であるならば、これらはジェネレーターとよくフィットするような構造であるはずだ。無限回の取り出せる操作という無限ループのイテレータはまさにこのような用途としてよくふっとしたものなのではないだろうか。