MMD on WebGL セルフシャドウを付けた

セルフシャドウなし セルフシャドウあり
MMDGL
MMD

MMD のほうがぼんやりしてる影の部分がある。まあこれはそんなに気にするものじゃないので適当なところで放置。

WebGL でのセルフシャドウには↓のサンプルコードが参考になった。

具体的には、まず光源を視点としてモデルをレンダリングする。このとき、フラグメントの色ではなく、z 座標(奥行き・光源への近さ)を色に置き換えて描画する。

↑テクスチャーの座標では y 軸が下向きのため、上下が逆になってる。

このレンダリング結果は canvas として画面に表示せずに、framebuffer というやつの中でやる。

MMDGL.ShadowMap.prototype.initFramebuffer = function initFramebuffer() {
  var gl = this.gl;

  this.framebuffer = gl.createFramebuffer();
  gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);

  this.texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, this.texture);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
  gl.generateMipmap(gl.TEXTURE_2D);

  // ↓ここで最後の引数を null にして、このテクスチャーにレンダリングさせる
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.width, this.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);

  // depth (z座標) を使うためのバッファーを作る
  var renderbuffer = gl.createRenderbuffer();
  gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer);
  gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, this.width, this.height);

  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture, 0);
  gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderbuffer);

  gl.bindTexture(gl.TEXTURE_2D, null);
  gl.bindRenderbuffer(gl.RENDERBUFFER, null);
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
};

頂点シェーダーでは普通に

  void main() {
    gl_Position = mvpMatrix * position;
  }

みたいにして、フラグメントシェーダーで

  void main() {
    gl_FragColor = vec4(gl_FragCoord.z, 0.0, 0.0, 0.0);
  }

こんな感じにすることで深さを色に変換する。(実際にはもうちょっと処理をしてるのでソース見てください)

ここで使った mvpMatrix は、モデル座標からライトを基準とした座標に変換するためのもので、これを lightMatrix として覚えておいてまた後で使う。

シャドウマップを記録したら、それをテクスチャーとしてつけて、framebuffer をデフォルトのもの(画面に表示されるもの)に戻して、普通にレンダリングする。

    gl.activeTexture(gl.TEXTURE3); // 3 -> shadow map
    gl.bindTexture(gl.TEXTURE_2D, this.shadowMap.getTexture());
    gl.uniform1i(program.uShadowMap, 3);

    gl.bindFramebuffer(gl.FRAMEBUFFER, null); // null = default framebuffer

このとき、頂点シェーダーではビュー座標と一緒にライト座標も作る。こんな感じで。

  void main() {
    gl_Position = mvpMatrix * position;
    lightCoord = lightMatrix * position;
  }

↑さっきの頂点シェーダーでは1行目しか無かったけど、今回の mvpMatrix はカメラを基準にした座標に変換するものでさっきとは別の処理。2行目の lightMatrix はさっき mvpMatrix だったものなので、今回もまったく同じ変換をしてることになる。

フラグメントシェーダーでこの lightCoord.xy を使ってシャドウマップから深さを読み、それを lightCoord.z と比べてる。

  float depth = texture2D(shadowMap, lightCoord.xy * 0.5 + 0.5).x;
  // ↑ 0.5 掛けて足してるのは光源を原点とした座標からテクスチャー画像上の座標に変換するため
  if (depth < lightCoord.z) {
    // シャドー!
  }

つまり、この同じ lightCoord.xy に相当するフラグメント(いくつかあるうち)で光があたってるのはライトからの距離が depth と一致してるフラグメントのみで、それ以上の場合は別の(影ではない)フラグメントがさらにライトの近くにある、と考える。

framebuffer の使い方とかの良い勉強になった。詳しくはコードを見てください。