「AWS無料相談会」をオンラインで開催中

Three.jsでシェーダお絵かき環境を作ってみた

入社して約2ヶ月になりました、フロントエンジニアの内山です。
リモートワークという働き方は自分に合っているようで、とても快適に過ごしております。

プライベートの時間もだいぶ確保できるようになったので、趣味のプログラミングも捗るようになりました。

今回は、最近取り組んでいる「シェーダーお絵かき」についてご紹介します。

シェーダーお絵かきとは

シェーダーお絵かきとは、
3DCGの表示などで使われるシェーダーで、お絵かきをしようというものです。

以下のスライドで紹介されています。
楽しい!Unityシェーダー お絵描き入門!

元はUnity上で行う方法が紹介されているのですが、
WebGL上でもできるんじゃないかと思ったので試しに構築してみました。

Three.jsの導入

最初は素のWebGLを使って構築したのですが、初期化手順が多くて面倒でした。
今回は紹介のために、それらを簡略化してくれるThree.jsを導入することにしました。

以下のサイトでダウンロードして、three.jsファイルを作業ディレクトリに移します。
https://threejs.org/

キャンバスの準備

HTMLのcanvasタグで、絵を描く準備をします。
以下の方針で設定しています。

  • 400x400固定で表示
  • 頂点シェーダー/フラグメントシェーダーはscriptタグ内に記述
<html>
<head>
    <title>Shader Test</title>
    <style>
        body {
            border: 0;
            background-color: white;
        }
        canvas {
            width: 400px;
            height: 400px;
            display: block;
        }
    </style>
</head>
<body>
<canvas id="c" width="400" height="400"></canvas>
<script src="three.js"></script>
<script id="vertexShader" type="x-shader/x-vertex">
        void main() {
            gl_Position = vec4( position, 1.0 );
        }
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
        uniform vec2 u_resolution;
        uniform float u_time;

        void main() {
            vec2 st = gl_FragCoord.xy/u_resolution.xy;
            gl_FragColor = vec4(st.x, st.y, 0.0, 1.0);
        }
</script>
<script>
  var container;
  var camera, scene, renderer;
  var uniforms;

  init();
  animate();

  function init() {
    container = document.getElementById( 'c' );

    camera = new THREE.Camera();
    camera.position.z = 1;

    scene = new THREE.Scene();

    var geometry = new THREE.PlaneBufferGeometry( 2, 2 );

    uniforms = {
      u_time: { type: "f", value: 1.0 },
      u_resolution: { type: "v2", value: new THREE.Vector2() },
      u_mouse: { type: "v2", value: new THREE.Vector2() }
    };

    var material = new THREE.ShaderMaterial( {
      uniforms: uniforms,
      vertexShader: document.getElementById( 'vertexShader' ).textContent,
      fragmentShader: document.getElementById( 'fragmentShader' ).textContent
    } );

    var mesh = new THREE.Mesh( geometry, material );
    scene.add( mesh );

    renderer = new THREE.WebGLRenderer({
      canvas: container,
    });
    renderer.setSize(400, 400);
    uniforms.u_resolution.value.x = renderer.domElement.width;
    uniforms.u_resolution.value.y = renderer.domElement.height;
  }

  function animate() {
    requestAnimationFrame( animate );
    render();
  }

  function render() {
    uniforms.u_time.value += 0.05;
    renderer.render( scene, camera );
  }
</script>
</body>
</html>

これを表示すると以下のような画像が表示されます。

フラグメントシェーダーについて

フラグメントシェーダーは、各ピクセルの色を決める処理をする機能です。
<script id="fragmentShader" type="x-shader/x-fragment">で囲われている部分で実装しています。
C言語に似たGLSLという言語で書きます。

uniform vec2 u_resolution;
uniform float u_time;

void main() {
    vec2 st = gl_FragCoord.xy/u_resolution.xy;
    gl_FragColor = vec4(st.x, st.y, 0.0, 1.0);
}

main関数が実行され、gl_FragColorという特別な変数に色(rgba)を設定します。
上記の例では、ピクセルの位置(st)を色に変換vec4(st.x, st.y, 0.0, 1.0)しています。

いくつかのサンプル

環境が構築できたので、スライドで紹介されている例を同じように実装できるか試してみました。
Unityのサポート機能?が使われている部分もあったので、若干書き直しが必要でしたが、同じ絵を描くことができました。

マルを描く

        uniform vec2 u_resolution;
        uniform float u_time;

        vec4 circle(vec2 st) {
            float d = distance(vec2(0.5, 0.5), st);
            float a = 0.4;

            float ret = step(a, d);

            return vec4(ret, ret, ret, 1);
        }

        void main() {
            vec2 st = gl_FragCoord.xy/u_resolution.xy;
            gl_FragColor = circle(st);
        }        

マルをアニメーションさせる

        uniform vec2 u_resolution;
        uniform float u_time;

        vec4 circle(vec2 st) {
            float d = distance(vec2(0.5, 0.5), st);
            float a = abs(sin(u_time*0.5)) * 0.4;

            float ret = step(a, d);

            return vec4(ret, ret, ret, 1);
        }

        void main() {
            vec2 st = gl_FragCoord.xy/u_resolution.xy;
            gl_FragColor = circle(st);
        }        

複数の四角をアニメーションさせる

        uniform vec2 u_resolution;
        uniform float u_time;

        vec2 stepVec2(vec2 a, float v) {
            return vec2(step(a.x, v), step(a.y, v));
        }

        vec2 fracVec2(vec2 v) {
            return vec2(fract(v.x), fract(v.y));
        }

        float wave(vec2 st, float n) {
            st = (floor(st * n) + 0.5) / n;
            float d = distance(vec2(0.5, 0.5), st);
            return (1.0 + sin(d * 3.0 - u_time)) * 0.5;
        }

        float box(vec2 st, float size) {
            size = 0.5 + size * 0.5;
            st = stepVec2(st, size) * stepVec2(1.0 - st, size);
            return st.x * st.y;
        }

        void main() {
            vec2 st = gl_FragCoord.xy/u_resolution.xy;

            float n = 10.0;
            vec2 xy = fracVec2(st.xy * n);
            float size = wave(st.xy, n);
            float ret = box(xy, size);
            gl_FragColor = vec4(ret, ret, ret, 1);
        }

まとめ

WebGL上でシェーダーお絵かきの環境を構築してみました。
UnityとWebGLは両者ともGLSLでシェーダーを実装するので、同じようにお絵かきすることができました。
WebGLで構築することで、Unityがやってくれていたことなども知れました。
これからも両者を見比べつつ、シェーダーや3Dまわりの知見を広めていきたいですね。