月 の 上

ShadertoyのSound ShaderをThree.jsで実装してみた

ヤッターGLSLで音が鳴るぞ!!

codepen.io

この記事はWebGL Advent Calendar 2017 の15日目の記事です。

こんにちは、 id:amagitakayosi です! AtomでGLSLをライブコーディングできるパッケージを作ったりしています。

今日はShadertoyのコードを読んで、Three.jsで真似してみる、ということをやってみました。

経緯

ShadertoyにはGLSLで音声を出力できる機能があります。(以下 Sound Shader と呼びます) デモ用の効果音を生成したり、以下のようにガッチリ曲を作ったりできます。

qiita.com

しかし、その仕組みがどのようになってるか、どこにも解説されていない! というわけで、Shadertoyのコードを読みつつ、ちゃんと理解するためにThree.jsで実装してみました。

解説

ShadertoyのSound Shaderは、以下の2つのメソッドを読むと仕組みが分かります。

  • EffectPass.prototype.Create_Sound
  • EffectPass.prototype.Paint_Sound

Create_Sound

(https://www.shadertoy.com/js/effect.js の243行目〜)

ここでは、Sound Shaderを実行するための準備を行っています。

Sound Shaderではメイン関数を vec2 mainSound として書きます。 xが左、yが右チャンネルです。

Create_Soundでは、ユーザーが書いたSound Shaderに、main関数を含む文字列を結合します。 main関数を読むと、ピクセルごとに異なる引数でmainSoundを呼び出していることがわかります。

レンダリングするテクスチャのサイズは 512x512 であることがわかります。 音声のサンプルレートは 44100 なので、SoundShaderは1回のレンダリングで約6秒の音声データを作成できるようですね。 (512 * 512 / 44100 ≒ 5.94)

また、 this.mPlayTime = 60*3; とある通り、Sound Shaderが生成できる音声には180秒までの制限があるようです。

Paint_Sound

(https://www.shadertoy.com/js/effect.js の1900行目〜)

Paint_Soundは、シェーダーの変更後はじめてPaintが実行されたとき呼ばれるようになっています。 つまり、Paint_Soundは1回の実行で180秒分の音声データをすべて作成します。

uniform変数をガチャガチャ設定してる部分はどうでもいいので読み飛ばすと、forループがネストした箇所が見つかります(2053行目〜)。 ここが音声生成のコア部分です。

やってることは単純です。 外側のループは、180秒分の音声データが生成し終わるまでレンダリングを繰り返す、というループです。 内側のループでは、レンダリング結果の各ピクセルの値を音声データに変換し、AudioBufferに詰める、ということをやっています。 この時、 bufL[off+i] = -1.0 + 2.0*(this.mData[4*i+0]+256.0*this.mData[4*i+1])/65535.0; とある通り、一つのサンプルを2バイトで表現しています。 mainSound が返すのはvec2でしたが、Create_Soundで追加されたmain関数の中身をよく見てみると、 以下のように上位ビットと下位ビットに分けて出力していることがわかります。

  vec2 vl = mod(v,256.0)/255.0;
  vec2 vh = floor(v/256.0)/255.0;
  gl_FragColor = vec4(vl.x,vh.x,vl.y,vh.y);

これはおそらく、GLSLの世界では音声はfloatとして表現されるけれど、出力すると1バイト (0 ~ 255) に丸められてしまい、音声として使い物にならないという問題があったのだと思います。 そのため、1サンプルをr+g or b+aの2バイトを使って表現することで、より精度の高い音声出力を実現したのでしょう。

ループが終わったら、AudioBufferSourceNodeをstartして、音声を再生しています。

感想

Three.jsでもシュッと実装できて楽しかった。 あとShadertoyのコード意外と読みやすい。

やり方はわかったので、VEDAにも機能追加しようと思います。 音声をループ再生したり、音声の長さを指定できたりすると、ライブコーディングでの音楽パフォーマンスが楽しくなりそうですね。

インターネット溶かすボタンできた

twitter.com

f:id:amagitakayosi:20171213170638p:plain

インストールはこちらから。

chrome.google.com

Post Internetは、任意のWebサイトによるポストエフェクトをかけるChrome拡張です。
ボタンをクリックすると、現在のページのスクリーンショットを取得し、エフェクトをかけて表示します。

エフェクトはGLSLで書かれています。
ボタンを右クリックして設定画面を開くと、GLSLを編集してエフェクトを変更できます。

f:id:amagitakayosi:20171213175441p:plain

実装にはVEDA.jsを使っています。

f:id:amagitakayosi:20171213184753g:plain

f:id:amagitakayosi:20171213183647g:plain

f:id:amagitakayosi:20171213183757g:plain


はじめにFlashがあった。
中学では、PCの得意な同級生はPalaFlaやSuzukaでFlashを作っていて、それはしょうもない内容だったけど、彼らの操作するマウスとキーボードがインターネットを光らせていた、そんな実感があった。


大学では情報系の学部に進んだ。
インターネットをもっと知りたかった。

初めて感動したのは、JavaScriptで画面を光らせたときだった。
僕はやっとインターネットを光らせる、そのスイッチに触れたような、そんな気がした。


大人になり、Web開発を学ぶにつれて、世界は全く変わっていった。
インターネットには高クオリティなコンテンツが溢れ、無秩序な光は淘汰されてしまった。
ユーザーは正しくフィルタリングされたタイムラインをただ眺める。
僕は有益なソフトウェアばかり身につけるようになった。
FlashbackYOU ARE AN IDIOTも忘れてしまった。


そんな中、GLSLと出会った。
衝撃だった。
4kbのコード片が世界を作る。
キーを叩く指が、スクリーンに複雑な模様を描く。

久しぶりに魔法を見た気がした。


気がついたら今年はGLSLばかりやっていた。
ライブコーディングの世界を知った。
もっと便利にGLSLを書くためのAtom拡張を作った。
みんなに魔法を見せたかった。
目に映るインターネットのすべてを光らせたかった。

そのためにまず、ブラウザを光らせるChrome拡張を作ることにした。


インターネットが退屈だったら、あなたが光らせれば良い。


この記事は はてなエンジニア Advent Calendar 2017 の13日目の記事です。 前日は id:WindymeltCommon Lisp開発序ノ口 〜プロジェクトの作成と実行〜 でした。