物理エンジンMatter.jsをテキストでレンダリング

CRTっぽいテキスト画面をWebGLで作るLocatePrintを使って物理エンジンをレンダリングするというデモも作った。

consolephysics

物理エンジンMatter.jsを使っている。Matter.jsはもちろん自前のレンダラを持っているんだけどそれを他のレンダラで差し替えられる

差し替える独自レンダラはMatter.jsのRender.jsを参考に作れば良い。特にRender.bodyWireframes関数にボディの頂点を線でつなぐ一番単純な描画方法が実装されているので、これを真似るのが簡単だ。

デモのコードでは、頂点を線でつなぐ部分でテキストを書くようにした。画面をテキストの幅と高さ(40キャラ x 20キャラ)のグリッドに分割し、線とグリッドが交わるポイントをリストアップする。グリッド内の各キャラについて、それらポイントがどこにあるかに応じて書くテキストを変える。例えば上端右と下端左にポイントがあれば/、右端上と下端左にあればFという具合に。それらのパターンを全部配列で記述した。

後はボディごとに設定された色に従ってcolor, locate, printするだけ。昔ながらの画面で近代的な物理エンジンが動く妙な絵が作れる。楽しい。

昔のCRTっぽい画面をWebGLで作る

マイコンをつないだテレビみたいな画面を作りたいと思ってLocatePrintっていうコードを書いた。

skigame

こんな画面が作れる、し遊べる。locate, print, colorなどの命令でテキストを書けます。というかテキストしか書けない。

こういうブラウン管っぽい画面を作るにはWebGLをポストプロセスとして使うのが便利。

この記事に必要なことはほとんど書いてある。glfx.jsっていうWebGLのイメージエフェクトライブラリを使って、ブラウン管の丸みっぽく画面を歪ませ (bulgePinch)、ふちをちょっと暗くすれば (vignette)、昔懐かしのテレビっぽい絵になる。

ただこの記事だとスキャンラインはPNGイメージを重ねることで実現しているけど、これもできればWebGLのシェーダーでやりたい。あとパピコンAppleIIみたいな色のにじみもつけたい。

そういう時はglfx.jsにカスタムのエフェクトが追加できればいいんだけど、残念ながらglfx.js自体はそのような仕組みを用意してない、が、やればできないことはない。

glfx.jsのいくつかの関数を無理やりexportsして外部から使えるようにする。そうすればスキャンライン色のにじみを再現するシェーダーをglfx.js内で使うことができる。

WebGL使えばブラウザ上でも簡単にポストエフェクトで遊べて良いね。フラットなLCDを丸いCRTにするという後ろ向きな使い方でも楽しければ良いのだ。

BLCK4777の絵の作られ方も知っておきたい

JavaScriptデモBLCK4777音の作られ方について前に調べたので、ついでにその絵の方もどのように作られているか調べてみた。多分に憶測が入っているけど、多分以下の様な感じ、のはず。

前の記事で追ったAudio生成部分を除いてコードを整理してみると、以下になる。

ctx = b.getContext('2d');
p = 0;

window.onload = function() {
  b.style.background = "radial-gradient(circle,#345,#000)";
  b.style.position = "fixed";
  b.style.height = b.style.width = "100%";
  b.height = 720;
  h = b.style.left = b.style.top = A = f = C = 0;
  //       x, y, vx, vy,    r, color1, color2, idx,
  p = [    0, 0,  0,  0,  180,      2,      0,   1, // triangle 1
        -360, 0,  0,  0,   99,      1,      0,   2, // triangle 2
         360, 0,  0,  0,   99,      1,      0,   3, // triangle 3
       -2880, 0,  0,  0, 1280,      0,   1280,   0];// line
  u();
}

まず最初にCanvasのstyle設定や変数の初期化を行っている。変数'p'が重要で、画面に多数ある三角形はこの配列で制御されている。一つの三角形は8つのパラメタで管理されている。左からx, y座標, x, yの移動量, 大きさ, 色の設定用パラメタ2つ, インデックスだ。初期設定されているのは画面に最初からある横に並んでいる大三角形3つと、その大三角形を貫く線の4つだ。

function u() {
  requestAnimationFrame(u);
  g = f + 1;//audio.currentTime * 60;
  for (; g > f; h *= f % 1 ? 1 : 0.995) {
    s = Math.pow(Math.min(f / 5457, 1), 87) +
      Math.pow(1 - Math.min(f / 5457, 1), 8);
    if (f == [1280, 1599, 2175, 2469, 2777, 3183,
              3369, 3995, 4199, 4470, 4777, 5120][C]) {
      C++;
      h = 640;
    }
    b.width = 1280;
    ctx.translate(640, 360 + h / 45 * Math.random());
    ctx.rotate(A / 5457 - h / 5457);
    ctx.scale(1 + s * 8, 1 + s * 8);
    f++;

続いてrequestAnimationFrameで回る1フレーム毎の描画部分。gは音の経過時間、fは現在のフレーム数を表していて、これらの比較は絵と音の同期を取るために使われている。今回は音の部分を外してコードを整理してしまったため、gにはfより1フレーム先を設定し、同期処理を行わないようにした。

sは少しずつ引くカメラワークのために後でctx.scaleで使われている。

fと比較している配列は、大三角形が光るタイミングのフレーム数を表している。hに640を設定していることをトリガに、後で色の制御を行う。Cは何回光ったかに相当する値で、音の場面転換などにも使われる。

b.width = 1280は画面サイズを設定するとともに、画面を消去している。その後のctxに対する操作はカメラのブレや回転の制御用だ。

    i = p.length;
    for (; i; ) {
      y = p[i -= 7];
      x = p[i ^= 1];
      r = p[i + 4];
      l = p[i + 6];
      s = 2 * Math.random() + 1;
      t = s * 4;
      a = 122;
      if (640 > r) {
        // triangle
        // y += vy
        if (!(640 > Math.abs(p[i ^= 1] += p[i + 2]))) {
          p[i + 2] *= -1;
        }
        // x += vx
        if (!(640 > Math.abs(p[i ^= 1] += p[i + 2]))) {
          p[i + 2] *= -1;
        }

ここからpの中の各三角形の処理。まず三角形のパラメタを取り出す。rが640より小さいのは三角形で、それより大きいのは線だ。まず三角形の場合、x, y座標の移動、および画面端にぶつかった場合の反転処理をしている。i ^= 1がちょっと面妖だが、ビットのXORを取っていて、偶数を奇数、奇数を偶数にする処理をしている。これでy座標、次にx座標という処理が行える。

        t = Math.random() > p[i + 7] ||
          p[i + 7] == "22312131212313"[C] & h == 640;

三角形を光らせる処理。大三角形が光るフレーム数の時はhが640になるので、文字列と対応するインデックスを持つ三角形の色を設定する。それ以外の小さな三角形もたまに光るようになっている。

        w = x - A;
        if (!p[i + 2]) {
          if (r * r / 3 > w * w) {
            t = s * (r - Math.abs(w)) / 45 + 2;
            a = 2 * Math.random() + 5;
            p.push(A, 0, s * Math.sin(a += 599), s * Math.sin(a - 11),
                   s * t, C + s, 640, 0.995);
            s = 2 * Math.random() + 1;
            a = 2 * Math.random() + 5;
            p.push(A, 0, s * Math.sin(a += 599), s * Math.sin(a - 11),
                   s * t, C + s, 640, 0.995);
            s = 2 * Math.random() + 1;
            a = 2 * Math.random() + 2;
            p.push(A, 0, s * Math.sin(a += 599), s * Math.sin(a - 11),
                   s * t, C + s, 640, 0.995);
          }
        }
        a = p[i + 2] * y / 45;
        l = p[i + 6] = t ? 640 : 0.9 * l;
        t = r;

線が大三角形を貫く時に小さな三角形をばらまく処理。Aが線の先端のx座標を表しているので、それを使って衝突を判定、衝突している時はpにパラメタを追加することで、小さな三角形をばらまいている。

      } else {
        // line
        A = p[i]++;
      }

線の処理、x座標をインクリメントしているだけ。これで右に動く。

      if (!(g > f)) {
        s = r;
        ctx.beginPath();
        ctx.lineTo(x + s * Math.sin(a += 599), y - s * Math.sin(a - 11));
        s = t;
        ctx.lineTo(x + s * Math.sin(a += 599), y - s * Math.sin(a - 11));
        ctx.lineTo(x + s * Math.sin(a += 599), y - s * Math.sin(a - 11));
        ctx.shadowBlur = r;
        s = l;
        x = s * 2;
        a = p[i + 5];
        ctx.shadowColor = ctx.fillStyle = "rgb(" +
          [x + s * Math.sin(a += 599) | 1,
           x + s * Math.sin(a += 599) | 1,
           x + s * Math.sin(a += 599) | 1] + ")";
        ctx.fill();
      }
    }
  }
  ctx.fillText("BLCK4777", 90, 99);
};

描画処理。gfは音と絵の同期用。三角形のパラメタにしたがってctx.lineToで頂点座標を設定し、シャドウのブラー、色を設定、fillする。最後に固定のメッセージBLCK4777を表示。

うん、実は全体の流れは結構分かりやすかった。もちろん実際は細かなパラメタの値などに演出の妙がたくさんあって、その辺の加減が肝なんだけども、そこを深追いしなければ読み解けないこともなさそう。一見とっつきにくそうに見えるデモのコードも、ちゃんとフローに応じて読みやすい形に展開すれば、なんとか追うこともできるかも、という一条の光が得られました。

デモコーダーにはもちろんショートコーディングのテクニックも必要なんだけども、限られたコードでどのような演出をし、音との同期も考えた盛り上がりポイントをどう入れるか、っていうところの力量もかなり必要とされているんだなあという印象を受けた。BLCK4777だとh = 640のタイミングで、光り、音が変化し、パーティクルが飛び始める、これが短いコードの中で効率良く実現されているのが美しいですな。

追記:ここ2つの記事で分かった内容を受けて整理、コメントを足したBLCK4777のコードをGitHubに置いた

JS1KデモBLCK4777のせめて音を作ってる部分だけでも分かりたい

今年のAssembly 2015 1kb intro勝者のJavaScriptデモBLCK4777、わずか1023 bytesでできているとは思えない美しいデモで話題になった。

1023 bytes版のファイルはPNG bootstrapping techniqueで圧縮されているそうで見た目はバイナリである。見ても分からない。

Full archiveに入っているsafe版は非圧縮のものなので、ちゃんと見えるJavaScriptが書いてある。さらにそれを見やすく整形して置いてくれた人がいるので、それを見ると、

はい分からない。

いや、もうちょっと頑張って読もう。safe版には、上のgistに置いてあるコードの他にブートストラップ用の以下のコードが含まれている。

c = b.getContext('2d');
T = String.fromCharCode;
p = 0;
document.querySelector('button').onclick = function() {
  this.textContent = 'Starting...';
  this.disabled = true;
  setTimeout(u,1);
};

ここで変数pは0に初期化されている。function uの最初の方にあるg = p ?から始まる三項演算子は0がfalse相当なので後ろの式が採用される。後ろの式はまるで関数の引数のようなものが書いてあるが、これはコンマ演算子だ。順に実行され、最後の結果が採用される。三項演算子コンマ演算子を使えば、if文相当を短く書ける。

ちなみにJavaScriptのショートコーディングについて知りたければSuperpacking JS Demosなどの記事を読むと良い。WebGL用の長いメソッド正規表現で無理やり短くする方法とか、PNGを使ったコードの圧縮方法などが書いてある。

コードに戻ると、pに値を設定するのは一番下のAudio.playの引数部分だ。Audio.playの引数は何の効用もないはずなので単にここで値を代入しているだけ。なので、ここでpの値が設定され、requestAnimationFrame(u)で再度uが呼び出されるまでにAudio、つまりこのデモの音が生成されている。

デモ全体を把握するのはキツイので、このAudioを作るまでをせめて見てみたい。pが0である条件で通るパスだけを切り出すと、以下のコードになる。

T = String.fromCharCode;
function u() {
  audio = "RIFFdataWAVEfmt " + atob("EAAAAAEAAQAAeAAAAHgAAAEACAA") + "data";
  g = 6177;
  h = f = C = 0;
  for (; g > f; h *= f % 1 ? 1 : 0.995) {
    s = Math.pow(Math.min(f / 5457, 1), 87) + 
        Math.pow(1 - Math.min(f / 5457, 1), 8);
    if (f == [1280, 1599, 2175, 2469, 2777, 3183,
              3369, 3995, 4199, 4470, 4777, 5120][C]) {
      C++;
      h = 640;
    }
    audio += T((1 + s * 8) * Math.random() + 
               (1 - s) * (h / 45 * (f * (2 + C / 3 % 1) & 1) + 
               (C > 3) * 8 * (f * (2 + (f / 8 & 3)) % 1)) | 1);
    f += 1 / 512;
  }
  new Audio("data:Audio/WAV;base64," + btoa(audio)).play();
}

はい分からない。

いや言いたいことは分かる。最初のaudioの初期化ではwavファイルのヘッダ相当のものを作っている。atobbase64をデコードしているのはフォーマットやチャネル数を指定するためのデータで、これはリニアPCM、モノラル、サンプリングレート30720Hz、bit/sampleは8bitを示している。

なのでaudioにTString.fromCharCodeを使って数値を足しているところで8bitのサンプルごとのデータを生成している。最後のnew Audioで作ったデータをbtoabase64エンコードし音を作っているのだ。

じゃあTの後ろの面妖な式は何だ。何だ。分からん。

ちくしょう!可視化だ!

データを適当に間引いた上でc3.js使ってグラフ化してみたけど……分からん。fと比較している配列内のフレーム数のところでなんらかの変化が起きているから、ここで曲の内容を変化させているのだとは思う。変化はCをインクリメントし、hを640にすることで起こしている。式にはC > 3という部分もあるので、ある程度フレームが進んだ後のみ使われる部分もあるのだろう。hは少しずつ減衰しているように見える。

これはいわゆるbytebeatと似たような手法なのだと思う。bytebeatは単純な式から様々な音を生成することができる手法で、実装方法もごく単純にフレーム数を引数とした式の出力をリニアPCMのサンプルごとの値にするだけというもの。本当に単純なのでショートコーディングにはとても向いた方法なのだが、問題はどんな式がどんな音をだすのかさっぱり分からないこと。

それっぽい式を適当に組わせるUIがあれば、簡単に音が作れるんじゃないかと思って、前にbytebeatbank (これも音が出ます)ってのを作ったんだけど、やはり型にはまった式の組み合わせだけだとあまりバリエーションが出ないという問題があった。かといって自由な式を生成してしまうとまともな音が鳴ることはほぼ無いという別の問題が起こる。

bytebeatに限らず、こういった数式でそれっぽい音を簡単に作る方式があれば、短いコードでBGMなり効果音なりが作れて楽しいんだろうけど、そういう手法を解説しているページとか、欲しいところだ。bytebeatについては以下のページがたぶん役に立つ、んだろうけどこれを読み解くのも大変そうである。

ちなみに今回まるっとスキップした、絵を作る部分はこれにさらに輪をかけて謎の数式が乱舞しているので誰か解説して欲しい。BLCK4777の例では無いけど、短い数式や関数で驚くべきグラフィックスを作る方法については、以下のページなどが役に立つ、はず。

というわけでデモコーダーの超絶技法を知るのはとても大変そうであるという話でした。

New 3DSブラウザのゲーム開発向けJavaScript API対応具合チェック

Newニンテンドー3DS インターネットブラウザーの主な仕様を見ると

HTML4.01/HTML5/XHTML1.1/Fullscreen/Gamepad/SVG/WebSocket/Video Subtitle/
WOFF/Web Messaging/Server-Sent/Web Storageの一部/XMLHttpRequest/
canvas/Video/DOM1-3/ECMAScript/CSS1/CSS2.1/CSS3 の一部

のウェブ標準に対応していると書いてある。旧3DSの仕様と比べるとだいぶリッチに見える。特にFullscreenとGamepadに対応しているならゲーム開発向けに使えるんじゃないのかね、と思って少し調べた。

Fullscreen

これ本当に対応しているの?

  • document.documentElement.webkitRequestFullScreen():何も起きない
  • document.documentElement.webkitRequestFullscreen():何も起きない
  • document.documentElement.requestFullscreen():多分関数が無い
  • document.documentElement.mozRequestFullScreen():多分関数が無い

もちろんPCのChromeだとちゃんとフルスクリーンになるんだけど、New 3DS上では何も起きない。何か別の呼び出し方が必要なのか。そもそもNetFront Browserのベンダープレフィックスは何だ。

Gamepad

navigator.getGamepads()を使う標準的な方法で確かにパッドの状態をとれる。けど、デジタルパッドとAボタン以外はブラウザ自体の操作に割り当てられているので使えない。アナログパッドや他のボタン全滅。

いろいろと勘違いがあった、下記追記参照。

その他

CanvasはfillRectとputImageDataしか試してないけどちゃんと動く。JavaScript自体はあんまり速い印象はない。128 x 128ドットのピクセル操作とかをやらせるとてきめんに遅くなる。

当たり前だがデベロッパツールとかコンソールとかは無い。

これらをふまえると

フルスクリーンもゲームパッドもパフォーマンスもイマイチなので、これでアクションゲーム開発はちょっとキツイ。フルスクリーンが効いて、その時は全てのパッド入力が取れる、という仕様が理想だったのだが。タッチペン使うのならもうちょっといけるのかもしれん。

Gamepad追記

@tkoharaさんに教えてもらって、htmlに

<meta name="viewport" content="width=device-width, height=device-height, 
user-scalable=no, initial-scale=1, maximum-scale=1">

を追記してアナログパッドなどでの画面の拡縮、移動操作を禁止すれば、パッド操作も問題なくできることがわかった。

あとテストプログラムでキー入力で取れていたものとゲームパッドで取れていたものを混同していて、いろいろ間違っていた。

  • ゲームパッドを取得するのはwebkitベンダープレフィックス付きのnavigator.webkitGetGamepads()
  • gamepad.buttonsは押し込みの値が直接取れる古いインタフェース(pressedやvalueは無い)
  • 左のアナログパッドがaxesの0, 1、右が2, 3
  • デジタルパッドはボタンとして取れる
  • L, RボタンやBボタンは進む、戻るのナビゲーションに割り当てられているので、ゲームに使えるかどうかは画面遷移に依存。基本使えない

なのでアナログパッドとデジタルパッド、A, X, Yボタンは使えそうなので、頑張ればなんとかなる、か?

GitHubで行にリンクを張る

ファイルへのリンクの後ろに#L21とか#L21-L23とかつければOk。

ただこれだとファイルのバージョンが変わった時に行がずれてしまうことがある。なのでGitHubのショートカットキーyを押して、そのバージョンのファイルへのパーマネントリンクへURLを変更した方が良い。

機能モジュールのパイプラインでキャラを制御する

Phaserをthree.jsで描画ネタのサンプルコードでもう一つ実験したのは、ゲーム内のキャラを機能のモジュールを組み合わせて制御できないかということ。例えば、上から降ってくるバルーンは、以下のコードのようにした。

interface Baloon extends Phaser.Sprite, U.HasMesh, U.HasName { }
function setBaloon() {
    var x, y;
    var baloon = <Baloon> U.chain([
        U.Name.set('Baloon'),
        U.Position.set(
            x = (Math.random() * .8 + .1) * 512,
            y = -.1 * 512),
        U.Body.set,
        U.GeomertyMaterialBody.addSquares
            (15, ['0110', '1111', '1111', '0110'], [0xeeeeaa]),
        U.Mesh.set,
        U.Sprite.removeWhenOutOfWorldBounds(60, 60, true),
        U.Body.setCollisionGroup(shotCollidingCg),
        U.Body.collides([shotCg, shotCollidingCg]),
        U.Body.setNotCollidingWorldBounds,
        U.Sprite.setUpdate((o: Baloon) => {
            o.body.thrust(66);
            U.PositionMesh.update(o);
            U.BodyMesh.update(o);
        }),
    ], U.Sprite.get());
}

elmで関数型にかぶれてた後に作ったので関数合成っぽくかけるようにした、んだけど実態は全然関数合成ではなくて、U.chainっていう関数の中身は配列内の関数にobjを順に渡しているだけ。

export function chain(funcs: Function[], obj = {}) {
   _.forEach(funcs, (f) => f(obj));
   return obj;
}

objはゲーム内のキャラを表すインスタンスで、PhaserではだいたいPhaser.Spriteを使う。objを扱う関数は、例えばその位置を設定するU.Position.setは以下のコード。

export interface HasPosition {
    position: Phaser.Point;
}
export module Position {
    export function set(x: number, y: number) {
        return (obj: HasPosition) => {
            obj.position.x = x;
            obj.position.y = y;
        }
    }
}

positionを持つHasPositionなobjにxとyを設定する関数を返している。自力カリー化みたいなことをすることで、関数合成もどきみたいなことをできるようにしている。objを直接書き換えずに、位置を更新した新しいobjをreturnするようにすれば、lodashのflowで繋げられる行儀の良い関数になると思うけど、面倒だからこの方式で。

利点としてはパイプラインで書けるので流れるようなインタフェースっぽいのが実現できること。あと機能をモジュールで分離できるので、一つのクラスにすべての機能を詰め込むようなことをしなくて済む。物理エンジンのBodyと3DライブラリのMeshの両方を持つobjだけで使うユーティリティ関数とかも作れる。

export interface HasBodyMesh extends HasBody, HasMesh { }
export module BodyMesh {
    export function update(obj: HasBodyMesh) {
        obj.mesh.rotation.z = -obj.body.angle * Math.PI / 180;
    }
}

欠点としては各関数で手動のカリー化みたいなのをしなきゃいけないこと。Ramdaとかの自動カリー化をしてくれるライブラリが使えればいいんだけど、TypeScriptの型情報を保持したままカリー化するのは、インタフェースをうまいこと使ったりする必要があったりして難しそう。

あとデバッグが面倒。パイプラインでつないだ関数を実行する実態はすべてU.chainの中にいってしまうので、まともにブレークポイントが張れない。これが結構きつい。

TypeScriptにこだわらないのであれば、ES6とRamdaの組み合わせとかでもうちょっとマシに作れるかもしれないけど、型が無いのは個人的にはかなり魅了減なので難しいところだ。

この記事みたいに、さらにBacon.jsまで使えばより関数型でFRPっぽい書き方ができそうな気もするが、そこまでするんだったらもうelmにしとけという気もする。

ゲーム内のキャラ制御を機能別にモジュール化できればコードの再利用性は高まると思うけど、モジュールの作り方、それらの組み合わせ方、妥当な書き方はまだよく分からんというところでした。