GLSLを使って影を表現してみた

気がつけば10ヶ月以上、更新してなかったんですね。今回は、GLSL(OpenGLShadingLanguage)を使って影の表現を試してみました。

GLSLとは?

GLSL(OpenGLShadingLanguage)は、WebGLにも採用されている、グラフィック描画に特化したプログラミング言語になります。GLSLはGPUによって解釈されるので、JavaScriptだけでは難しい高負荷な計算処理も、難なくこなす事が出来ます。

GLSL Sandbox

こちらのサイトでは、多くのGLSLのコードと、その結果を見ることができます。

GLSLのフラグメントシェーダーというもので、1つのドットに対して実行されるコードで、この計算式に自身の位置と、時間軸やマウス座標等を与える事で、そのドットが何色を表示するかという計算式になります。

実際に書いてみた

GLSLをWebサイトに導入する方法はWebGLを使うことですが、実際どんな感じになるのか、コードを書いてみました。

マウスを光源としたイメージで、後ろに影が映り込む感じのシェーダーを書いています。

ソースコードは以下より参照ください。

▼ HTML・JSコード

<canvas id="canvas" width="512" height="512"></canvas>
<canvas id="texture" width="512" height="512" style="display: none;"></canvas>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script>
  $(function () {

      var WIDTH = 512;
      var HEIGHT = 512;

      // マウス座標
      var mouseX = 0.5;
      var mouseY = 0.5;
      var moveX = 0.5;
      var moveY = 0.5;

      // テクスチャ用CANVAS
      var textureCanvas = document.getElementById('texture');
      textureCanvas.width = WIDTH;
      textureCanvas.height = HEIGHT;
      var textureContext = textureCanvas.getContext('2d');
      textureContext.fillStyle = '#ffffff';
      textureContext.fillRect(0, 0, WIDTH, HEIGHT);

      // GLSL描画用CANVAS
      var webglCanvas = document.getElementById('canvas');
      webglCanvas.width = WIDTH;
      webglCanvas.height = HEIGHT;
      var webglContext = webglCanvas.getContext('webgl', {preserveDrawingBuffer: true});
      webglContext.viewport(0, 0, WIDTH, HEIGHT);

      // プログラムの取得
      var program = webglContext.createProgram();

      // テクスチャ読み込み
      loadTexture('./texture.png', function(texture){
          textureContext.drawImage(texture, 0, 0, WIDTH, HEIGHT);

          // フラグメントシェーダ読み込み
          loadFragmentShader('./fragment.glsl', function(shader){

              // フラグメントシェーダ設定
              var fragmentShader = webglContext.createShader(webglContext.FRAGMENT_SHADER);
              webglContext.shaderSource(fragmentShader, shader);
              webglContext.compileShader(fragmentShader);
              webglContext.attachShader(program, fragmentShader);

              // バーテックスシェーダ読み込み
              loadVertexShader('./vertex.glsl', function(shader){

                  // バーテックスシェーダ設定
                  var vertexShader = webglContext.createShader(webglContext.VERTEX_SHADER);
                  webglContext.shaderSource(vertexShader, shader);
                  webglContext.compileShader(vertexShader);
                  webglContext.attachShader(program, vertexShader);

                  // シェーダをリンク
                  webglContext.linkProgram(program);

                  // プログラムオブジェクトの有効化
                  webglContext.useProgram(program);

                  // 頂点データ
                  var vertices = new Float32Array([-1, -1, 0, -1, 1, 0, 1, -1, 0, 1, 1, 0]);
                  var verticesBuff = webglContext.createBuffer();
                  webglContext.bindBuffer(webglContext.ARRAY_BUFFER, verticesBuff);
                  webglContext.bufferData(webglContext.ARRAY_BUFFER, vertices, webglContext.STATIC_DRAW);
                  var vertexAttr = webglContext.getAttribLocation(program, 'vertex');
                  webglContext.enableVertexAttribArray(vertexAttr);
                  webglContext.vertexAttribPointer(vertexAttr, 3, webglContext.FLOAT, false, 0, 0);

                  // テクスチャデータ
                  var texture = webglContext.createTexture();
                  webglContext.bindTexture(webglContext.TEXTURE_2D, texture);
                  webglContext.texImage2D(webglContext.TEXTURE_2D, 0, webglContext.RGBA, webglContext.RGBA, webglContext.UNSIGNED_BYTE, textureCanvas);
                  webglContext.generateMipmap(webglContext.TEXTURE_2D);

                  // テクスチャ座標
                  var coord = new Float32Array([0,1, 0,0, 1,1, 1,0]);
                  var coordBuff = webglContext.createBuffer();
                  webglContext.bindBuffer(webglContext.ARRAY_BUFFER, coordBuff);
                  webglContext.bufferData(webglContext.ARRAY_BUFFER, coord, webglContext.STATIC_DRAW);
                  var coordAttr = webglContext.getAttribLocation(program, 'coord');
                  webglContext.enableVertexAttribArray(coordAttr);
                  webglContext.vertexAttribPointer(coordAttr, 2, webglContext.FLOAT, false, 0, 0);

                  // マウス座標
                  $('#canvas').on('mouseenter', function(e) {
                    $('#canvas').on('mousemove', onMouseMove);
                  });
                  $('#canvas').on('mouseleave', function(e) {
                    $('#canvas').off('mousemove', onMouseMove);
                      mouseX = 0.5;
                      mouseY = 0.5;
                  });
                  function onMouseMove(e) {
                      mouseX = e.clientX / WIDTH;
                      mouseY = e.clientY / HEIGHT;
                  }

                  // レンダリング
                  render();

                  // レンダリング
                  function render() {
                      uniform = {};

                      // マウス座標のイージング
                      moveX += (mouseX - moveX) * 0.1;
                      moveY += (mouseY - moveY) * 0.1;

                      // uniform変数mouseのロケーション取得
                      uniform.mouse = webglContext.getUniformLocation(program, 'mouse');

                      // uniform変数をプッシュ
                      webglContext.uniform2fv(uniform.mouse, [moveX, moveY]);

                      // 描画
                      webglContext.drawArrays(webglContext.TRIANGLE_STRIP, 0, 4);
                      webglContext.flush();
                      webglContext.finish();

                      // 再起
                      requestAnimationFrame(render);
                  }

              });
          });

      });

      // テクスチャ読み込み
      function loadTexture(src, cb) {
          var image = new Image();
          image.onload = function () {
              cb(image);
          };
          image.src = src;
      }

      // フラグメントシェーダ読み込み
      function loadFragmentShader(src, cb) {
          $.ajax(src, {
              type: 'get',
              dataType: 'text'
          }).done(function(data){
              cb(data);
          });
      }

      // バーテックスシェーダ読み込み
      function loadVertexShader(src, cb) {
          $.ajax(src, {
              type: 'get',
              dataType: 'text'
          }).done(function(data){
              cb(data);
          });
      }

    });
</script>

▼ fragment.glsl

precision highp float;

varying vec2 vCoord;
uniform sampler2D texture;
uniform vec2  mouse;

void main(void){
    vec2 position = vCoord;
    float distance = length(position - mouse);
    float textureX = ((position.x - mouse.x) / (1.0 + distance)) + mouse.x;
    float textureY = ((position.y - mouse.y) / (1.0 + distance)) + mouse.y;
    vec4 colorShadow = texture2D(texture, vec2(textureX, textureY));
    vec4 colorBase = texture2D(texture, vCoord);
    vec4 color = vec4(0, 0, 0, 0);
    if (0.0 < colorBase.a && (colorBase.r < 1.0 || colorBase.g < 1.0 || colorBase.b < 1.0)) {
        // Charactor Area
        color.r = colorBase.r + ((0.2 - distance) * 2.0);
        color.g = colorBase.g + ((0.2 - distance) * 2.0);
        color.b = colorBase.b + ((0.2 - distance) * 2.0);
        color.a = colorBase.a;
    } else if (0.0 < colorShadow.a && (colorShadow.r < 1.0 || colorShadow.g < 1.0 || colorShadow.b < 1.0)) {
        // Shadow Area
        color.r = colorShadow.r + 0.8 + (distance * 0.2);
        color.g = colorShadow.g + 0.8 + (distance * 0.2);
        color.b = colorShadow.b + 0.8 + (distance * 0.2);
        color.a = colorShadow.a;
    }
    gl_FragColor = color;
}

▼ vertex.glsl

attribute vec3 vertex;
attribute vec2 coord;
varying vec2 vCoord;

void main(void){
    gl_Position = vec4(vertex, 1.0);
    vCoord = coord;
}

まとめ

今回、初めてシェーダーを書いてみましたが、わからないことだらけで、ちゃんと勉強しなきゃいけない分野だなと実感しました。自由に操れれば、今までに無いような表現力をつけられるなと、とても可能性を感じます。GPUを使うことで、より複雑なアニメーションであっても、不可なく再現することができるのは、非常にありがたいことです。Webコンテンツでの活用方法を今後模索していきたいと思いました。

次回は、今回の表現をpixi.jsのカスタムシェーダーを使って、さらにリッチなものにしていきたいと思います。