とShadertoy

Shadertoyは凄いシェーダーの実験場で有名なサイトです。よく聞かれるのがThree.jsでShadertoyのシェーダーを使う方法です。

シェーダーのおもちゃと呼ばれている理由を認識する事が大切です。 一般的にshadertoyのシェーダーはベストプラクティスではありません。 むしろ、dwitter (140文字でコードを書く)やjs13kGames (13k以下でゲームを作る)のような楽しいチャレンジです。

Shadertoyの場合のパズルは、ピクセルに何か面白いものを描画する関数を書く事です。 それは楽しいチャレンジで、多くの投稿は凄いシェーダーです。 しかし、それはベストプラクティスではありません。

街全体を描く凄いshadertoyシェーダーで比較してみましょう。

上記のシェーダーは私のPCではフルスクリーンだと1秒間に約5フレームで動作します。 Citiesのようなゲーム: スカイラインとは対照的です。

このゲームはテクスチャと三角形の建物を描画する伝統的な技術を使用しており、同じマシンで1秒間に30~60フレーム動作します。

それでもthree.jsでShadertoyのシェーダーを使ってみましょう。

以下は2019年1月現在でshadertoy.comで"New"を選ぶした時のデフォルトのshadertoyシェーダーです。

// By iq: https://www.shadertoy.com/user/iq  
// license: Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates (from 0 to 1)
    vec2 uv = fragCoord/iResolution.xy;

    // Time varying pixel color
    vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));

    // Output to screen
    fragColor = vec4(col,1.0);
}

シェーダーを理解する上で重要な事の1つは、特殊な型を含む3D数学用に設計されたGLSL (Graphics Library Shading Language)と呼ばれる言語で書かれている事です。 上記コードでは vec4vec2vec3 のような特殊な型があります。 vec2 は2つの値を持ち vec3 は3つ、vec4 は4つの値を持ちます。 たくさんの方法で引数を指定できます。 例えば以下のように xyzw を使うのが最も一般的です。

vec4 v1 = vec4(1.0, 2.0, 3.0, 4.0);
float v2 = v1.x + v1.y;  // adds 1.0 + 2.0

JavaScriptとは異なり、GLSLはC/C++のように変数の型を宣言しなければなりません。 var v = 1.2; ではなく float v = 1.2; となり v は浮動小数点数です。

GLSLを詳しく解説するとこの記事で紹介している以上の事ができます。 GLSLの簡単な概要はこの記事を参照して下さい。 The Book of Shadersの連載記事の後に続くかもしれません。

2019年1月現在ではshadertoy.comフラグメントシェーダー のみ使えます。 フラグメントシェーダーの役割は、ピクセルの位置が与えられた時にそのピクセルの色を出力します。

上記の関数ではシェーダーに fragColor という out パラメーターがあります。 outoutput の略です。 outは関数が値を提供する事を意味するパラメーターです。 これを何か他の色に設定します。

また、in (入力用) パラメーターとして fragCoord があります。 これは描画しようとしているピクセル座標です。座標によって色を決めます。 描画先のキャンバスが400 x 300ピクセルの場合、この関数は400 x 300回つまり12万回呼ばれます。 毎回 fragCoord は別のピクセル座標になります。

コードに未定義の変数が2つ使用されています。 1つは iResolution です。これはキャンバスの解像度です。 キャンバスが400 x 300の場合、iResolution は400, 300になります。 ピクセル座標が変化すると uv はテクスチャ全体で0.0 〜 1.0の間で変化します。 正規化された値を使い動作させると物事が単純になる事が多く、shadertoyのシェーダーの大部分は正規化したものから始まります。

コードに定義されていないもう1つの変数は iTime です。 これはページが読み込まれてからの時間を秒単位で表します。

シェーダの専門用語では、これらのグローバル変数は ユニフォーム(uniform) 変数と呼ばれています。 この変数は変更されないため ユニフォーム と呼ばれ、シェーダーの1回のイテレーションから次のイテレーションまで同じ状態を保ちます。 ここで注意したいのはそれらは全てshadertoy特有のものです。 これらは オフィシャルな GLSL変数ではありません。 それらはshadertoy側で作った変数です。

Shadertoyのドキュメントを見ると、さらにいくつか特有の定義があります。 とりあえず、上記のシェーダーで使われている2つを処理するコードを書いてみましょう。

まずはキャンバスを塗りつぶす1枚の平面を作ってみましょう。 まだ読んでいない方は背景とスカイボックスの記事でこのようにしましたので、その例を参考に立方体を削除してみましょう。 かなり短いコードなので全体を紹介します。

function main() {
  const canvas = document.querySelector('#c');
  const renderer = new THREE.WebGLRenderer({canvas});
  renderer.autoClearColor = false;

  const camera = new THREE.OrthographicCamera(
    -1, // left
     1, // right
     1, // top
    -1, // bottom
    -1, // near,
     1, // far
  );
  const scene = new THREE.Scene();
  const plane = new THREE.PlaneGeometry(2, 2);
  const material = new THREE.MeshBasicMaterial({
      color: 'red',
  });
  scene.add(new THREE.Mesh(plane, material));

  function resizeRendererToDisplaySize(renderer) {
    const canvas = renderer.domElement;
    const width = canvas.clientWidth;
    const height = canvas.clientHeight;
    const needResize = canvas.width !== width || canvas.height !== height;
    if (needResize) {
      renderer.setSize(width, height, false);
    }
    return needResize;
  }

  function render() {
    resizeRendererToDisplaySize(renderer);

    renderer.render(scene, camera);

    requestAnimationFrame(render);
  }

  requestAnimationFrame(render);
}

main();

背景とスカイボックスの記事で説明したように、これらのパラメーターを持つ OrthographicCamera と2の長さの平面がキャンバスを塗り潰します。 平面は赤の MeshBasicMaterial を使用しているため、赤いキャンバスが表示されます。

これで動作するようになったので、shadertoyシェーダーを追加してみましょう。

const fragmentShader = `
#include <common>

uniform vec3 iResolution;
uniform float iTime;

// By iq: https://www.shadertoy.com/user/iq  
// license: Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates (from 0 to 1)
    vec2 uv = fragCoord/iResolution.xy;

    // Time varying pixel color
    vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));

    // Output to screen
    fragColor = vec4(col,1.0);
}

void main() {
  mainImage(gl_FragColor, gl_FragCoord.xy);
}
`;

上記では2つのユニフォーム変数を宣言しました。 そして、shadertoyからGLSLのシェーダーコードを追加しました。 最後に mainImage を呼び出し、gl_FragColorgl_FragCoord.xy を渡します。 gl_FragColor はオフィシャルのグローバル変数でシェーダーが現在のピクセルに設定する色を指定します。 gl_FragCoord は現在の色を選択しているピクセルの座標を教えてくれる、もう1つのオフィシャルなグローバル変数です。

次にシェーダーに値を設定できるようにThree.jsのユニフォームを設定します。

const uniforms = {
  iTime: { value: 0 },
  iResolution:  { value: new THREE.Vector3() },
};

Three.jsの各ユニフォームには value パラメータがあります。 この値はユニフォームの型と一致してなければなりません。

フラグメントシェーダーとユニフォームの両方を ShaderMaterial に渡します。

-const material = new THREE.MeshBasicMaterial({
-    color: 'red',
-});
+const material = new THREE.ShaderMaterial({
+  fragmentShader,
+  uniforms,
+});

レンダリングする前にユニフォームの値を設定します。

-function render() {
+function render(time) {
+  time *= 0.001;  // convert to seconds

  resizeRendererToDisplaySize(renderer);

+  const canvas = renderer.domElement;
+  uniforms.iResolution.value.set(canvas.width, canvas.height, 1);
+  uniforms.iTime.value = time;

  renderer.render(scene, camera);

  requestAnimationFrame(render);
}

注意点: なぜ iResolutionvec3 なのか、 3番目の値に何が入っているのか shadertoy.comには文書化されてない のでさっぱりわかりません。 上記では使わないのでとりあえず1にしておきます。 ¯\_(ツ)_/¯

これは新しいシェーダーでShadertoyで見たものと一致します。 少なくとも2019年1月の時点では😉。上記のシェーダーは何をしているのでしょうか?

  • uv は0 〜 1です。
  • cos(uv.xyx) は3つのコサイン値を vec3 としてます。
  • 3つとは uv.xuv.yuv.z です。
  • 時間を加えて、cos(iTime+uv.xyx) でアニメーションさせます。
  • cos(iTime+uv.xyx+vec3(0,2,4)) のように vec3(0,2,4) を加えると、コサイン波の cos が-1 〜 1にオフセットされます。
  • そのため 0.5 * 0.5 + cos(....) は -1 <-> 1 〜 0.0 <-> 1.0 に変換され、その結果が現在のピクセルのRGB色として利用されます。

わずかな変更でコサイン波が見やすくなります。 uv は0 〜 1までの間だけです。 コサインは2πで繰り返すので、0 〜 40.0を掛けて40にしてみましょう。 これで6.3回くらいリピートするはずです。

-vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));
+vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx*40.0+vec3(0,2,4));

以下を数えてみると6.3回くらいリピートしています。 +vec3(0,2,4) で4だけオフセットされているので赤の間に青が見えます。 それがないと青と赤が完全に重なり、紫になってしまいます。

シンプルな入力でshadertoyにある a city canala foresta snaila mushroom をより印象的なものにして見る事ができます。 上手くいけば三角形からシーンを作る伝統的な方法と比較して、一般的に正しいアプローチではない理由も明確になるでしょう。 全てのピクセルの色を計算するために多くの計算をしなければならないので、これらの例は非常に遅く実行されます。

いくつかのshadertoyシェーダーは、これのようにテクスチャを入力として受け取るものがあります。

// By Daedelus: https://www.shadertoy.com/user/Daedelus
// license: Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
#define TIMESCALE 0.25 
#define TILES 8
#define COLOR 0.7, 1.6, 2.8

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord.xy / iResolution.xy;
    uv.x *= iResolution.x / iResolution.y;

    vec4 noise = texture2D(iChannel0, floor(uv * float(TILES)) / float(TILES));
    float p = 1.0 - mod(noise.r + noise.g + noise.b + iTime * float(TIMESCALE), 1.0);
    p = min(max(p * 3.0 - 1.8, 0.1), 2.0);

    vec2 r = mod(uv * float(TILES), 1.0);
    r = vec2(pow(r.x - 0.5, 2.0), pow(r.y - 0.5, 2.0));
    p *= 1.0 - pow(min(1.0, 12.0 * dot(r, r)), 2.0);

    fragColor = vec4(COLOR, 1.0) * p;
}

シェーダーにテクスチャを渡すのは通常のマテリアルにテクスチャを渡すのと似ていますが、ユニフォームにテクスチャを設定する必要があります。

まず、シェーダーにテクスチャのユニフォームを追加します。 GLSLでは sampler2D と呼ばれています。

const fragmentShader = `
#include <common>

uniform vec3 iResolution;
uniform float iTime;
+uniform sampler2D iChannel0;

...

ここで取り上げたこれのようなテクスチャをロードし、ユニフォームの値を設定します。

+const loader = new THREE.TextureLoader();
+const texture = loader.load('resources/images/bayer.png');
+texture.minFilter = THREE.NearestFilter;
+texture.magFilter = THREE.NearestFilter;
+texture.wrapS = THREE.RepeatWrapping;
+texture.wrapT = THREE.RepeatWrapping;
const uniforms = {
  iTime: { value: 0 },
  iResolution:  { value: new THREE.Vector3() },
+  iChannel0: { value: texture },
};

これまではShadertoy.comで使われているShadertoyシェーダーをそのまま使っていましたが、キャンバスを塗りつぶすように描画しています。 しかし、そのユースケースだけに限定する必要はありません。 覚えておくべき重要な事は、shadertoy上で書かれている関数は fragCoord の入力と iResolution を受け取るだけです。 代わりにテクスチャ座標のような他のものを使えば、他のテクスチャと同じように使えます。 関数を使ってテクスチャを生成するこの手法は、プロシージャルテクスチャと呼ばれています。

上記のシェーダーを変更してみましょう。 最も簡単なのはthree.jsが通常提供しているテクスチャ座標を取得し、それに iResolution を掛けて fragCoords に渡す事かもしれません。

そのためには ヴァリイング(varying) を追加します。 ヴァリイングとは頂点シェーダーからフラグメントシェーダーに渡される値で、頂点間で補間される値(または変化する値)の事です。 フラグメントシェーダーで使用するために宣言します。 Three.jsのテクスチャ座標は uv で前にある vヴァリイング を意味します。

...

+varying vec2 vUv;

void main() {
-  mainImage(gl_FragColor, gl_FragCoord.xy);
+  mainImage(gl_FragColor, vUv * iResolution.xy);
}

次に独自の頂点シェーダーも用意する必要があります。 ここでは最小限のthree.jsの頂点シェーダーを紹介します。 Three.jsは uvprojectionMatrixmodelViewMatrixposition を宣言し、その値を提供します。

const vertexShader = `
  varying vec2 vUv;
  void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
  }
`;

頂点シェーダーを ShaderMaterial に渡します。

const material = new THREE.ShaderMaterial({
  vertexShader,
  fragmentShader,
  uniforms,
});

iResolution の値は初期化時には変化しないので、iResolution の値をユニフォームに設定できます。

const uniforms = {
  iTime: { value: 0 },
-  iResolution:  { value: new THREE.Vector3() },
+  iResolution:  { value: new THREE.Vector3(1, 1, 1) },
  iChannel0: { value: texture },
};

レンダリング時に設定する必要がなくなりました。

-const canvas = renderer.domElement;
-uniforms.iResolution.value.set(canvas.width, canvas.height, 1);
uniforms.iTime.value = time;

元のカメラとレスポンシブデザインの記事から3つの回転する立方体を設定するコードでコピーバックしました。 その結果です。

これで少しでもthree.jsを使ったshadertoyシェーダーの使い方を知ってもらえればと思います。 繰り返しになりますが、ほとんどのshadertoyシェーダーは実際にパフォーマンスの高い方法で描画する推奨された方法ではなく、 面白いチャレンジ(単一の機能で全てを描画する)と覚えておく事が重要です。 それでもshadertoyのシェーダー達は素晴らしく、印象的で、美しく、どのように機能するかコードを見て学ぶ事ができます。