MMD on WebGL デモページを用意した

試せるようにしたほうがいいと言われたので、マウス操作などの機能をつけてちゃんと形にした。

もしかしたら将来は別の所に移動するかも。とりあえず暫定。

Chrome でしか使えません。Firefox は早く DataView を実装してください。David Flanagan さんの DataView.js を使えば Firefox でも動かせると教えてもらったので Firefox でも動くようにした。

Shift+Drag で移動なんだけど、文字選択されてるときは Chrome だとおかしなことになる。説明文を書いてないときは問題なかったのに!ちょっと不満。

マウス操作とキーボード操作以外の変更点は、軸と中心点を付けたこと。

中心点は Point Sprite という方法でやった。(それ以外にどうやればいいのか知らないけど)

↓このへんを見たらだいたいわかる。

床井研究室の解説にある glEnable(GL_VERTEX_PROGRAM_POINT_SIZE);glEnable(GL_POINT_SPRITE); は、WebGL ではしなくてもいいらしい。Learning WebGL の方にはVERTEX_PROGRAM_POINT_SIZE が無かったので gl.enable(0x8642) とか書いたら動いたぜ…ってあるけど、このハックは既に最近の WebGL では必要ないらしい。

やることは、JS 側ではこんだけで、

  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.center), gl.STATIC_DRAW);
  gl.drawArrays(gl.POINTS, 0, 1);

Vertex Shader では(いつもの座標変換以外では)こういうことをする。

      gl_Position.z = 0.0; // always on top
      gl_PointSize = 16.0;

z 座標を0にすることによってモデルに隠れてしまわないようにしてる。

gl_PointSize を指定すると Fragment Shader で gl_PointCoord が使える。描かれる点は、drawArrays(gl.POINTS で与えた点を中心とし、縦横の長さが gl_PiointSize ピクセルの正方形で、常に正面を向いている。gl_PointCoord はその正方形内の座標で、原点が正方形の左上で、右下が (1, 1) になる。

      vec2 uv = gl_PointCoord * 2.0 - 1.0; // transform [0, 1] -> [-1, 1] coord systems
      float w = dot(uv, uv);
      if (w < 0.3 || (w > 0.5 && w < 1.0)) {
        gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
      } else {
        discard;
      }

discard を呼んだところは色を付けないのでそのまま後ろが見える。


中心点を置いてみたはいいけど、どうせ画面の中心だし、別に要らないかも。MMD っぽい雰囲気を出すためということで…

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 の使い方とかの良い勉強になった。詳しくはコードを見てください。

MMD on WebGL シェーダーが大体完成

輪郭がちゃんと付くようになった。

一昨日は悲惨だったこのアングルも…

ちゃんとできてる。

どうしても輪郭線がうまく行かなくて MMDAgent のソースを見てみると、

  1. 普通に頂点をレンダリングし、
  2. 次に Cull Face でオモテ面をバッサリ切って、
  3. 輪郭線のある頂点を法線のほうに少し引き伸ばして
  4. 再度裏面だけ黒でレンダリングする

というようなことをしてた。

一度この方法でやったこともあったんだけど、その時は線が太くなりすぎて諦めてた。太くなったり汚くなったりするのはシェーダーの微妙な違いによるものだったので、今回はたまたまうまく行った。(vec4 の w 要素を normalize の前に消すか消さないかとかそういう程度)

最終的にはだいたいこんな感じ。

    // vertex shader
    if (uEdge) {
      vec4 pos = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
      vec4 pos2 = uPMatrix * uMVMatrix * vec4(aVertexPosition + aVertexNormal, 1.0);
      vec4 norm = normalize(pos2 - pos);
      gl_Position = pos + norm * uEdgeThickness * pos.w; // scale by pos.w to prevent becoming thicker when zoomed
    }

この pos.w というのは MMDAgent にはついてないんだけど、これを付けることによってズームしても輪郭が太くならない効果があり、より MMD に近くなる。


MMD でも同じような方法で輪郭を付けてるというのが、この角度で輪郭線がちょっと浮いてしまってることからわかる。

ただ、これだけだとスカートや服の裏地が黒くならないので、gl_FrontFace で判定して裏面のときも輪郭線の色で塗ってある。

同じ事を MMDAgent でもやってると思うんだけど、その部分のコードがどこに相当するのか分からなかった。

とりあえず今日はここまで。ソースコードも公開した。

次は、Lat 式を表示するのはちょっと置いといて、地面の影とセルフシャドウに挑戦しようかな。

WebGL で MMD を表示してみるテスト

もう何番煎じという感じだけど、それなりに見えるようになった。3D プログラミングの知識ゼロで始めたので、検索しながら色々勉強になるのが楽しい。

MMD はソース公開されてないので、シェーダーの調節が手探り状態。

有志によって解析された PMD データ形式の「マテリアル」の部分↓を見て、各値をどのように足したり掛けたりするかを想像して、GLSL に落とし込む。

 float diffuse_color[3]; // dr, dg, db // 減衰色
 float alpha;
 float specularity;
 float specular_color[3]; // sr, sg, sb // 光沢色
 float mirror_color[3]; // mr, mg, mb // 環境色(ambient)
 BYTE toon_index; // toon??.bmp // 0.bmp:0xFF, 1(01).bmp:0x00 ・・・ 10.bmp:0x09
 BYTE edge_flag; // 輪郭、影
 DWORD face_vert_count; // 面頂点数 // インデックスに変換する場合は、材質0から順に加算
 char texture_file_name[20]; // テクスチャファイル名またはスフィアファイル名 // 20バイトぎりぎりまで使える(終端の0x00は無くても動く)
http://blog.goo.ne.jp/torisu_tetosuki/e/ea0bb1b1d4c6ad98a93edbfe359dac32

WebGL じゃなくて OpenGL を使った MMDAgent というオープンソースソフトもあって、大変参考になるのだけど、OpenGL の glMaterialfv という便利機能が WebGL には無いので、そのあたりの処理は自作しないといけない。

輪郭線の部分はまだかなり違ってる(一番上のスクリーンショット参照)ので置いといて、色使いの部分のフラグメントシェーダーはこんな感じになった。

color = vec3(1.0, 1.0, 1.0);
if (uUseTexture) {
  color *= texture2D(uTexture, vTextureCoord).rgb;
}
if (uUseSphereMap) {
  vec2 sphereCoord = 0.5 * (1.0 + vec2(1.0, -1.0) * norm.xy);
  if (uIsSphereMapAdditive) {
    color += texture2D(uSphereMap, sphereCoord).rgb;
  } else {
    color *= texture2D(uSphereMap, sphereCoord).rgb;
  }
}

vec3 halfAngle = normalize(uLightDirection + cameraDirection);
float specularWeight = pow( max(0.0, dot(halfAngle, norm)) , uShininess );
vec3 specular = specularWeight * uSpecularColor;

color *= uAmbientColor + uLightColor * (uDiffuseColor + specular);
color = clamp(color, 0.0, 1.0);

vec2 toonCoord = 0.5 * (1.0 - (vec2(0.0, dot( uLightDirection, norm ))));
color *= texture2D(uToon, toonCoord).rgb;

まったく自分の想像でしかないので、違ってたら教えて下さい。

アルゴリズム的には大きく分けて3段階。

まずテクスチャーのあるマテリアルについてはテクスチャーを貼る。これは普通。普通のテクスチャーの他に、加算スフィアマップと乗算スフィアマップというのがあることもある。

次に、ambient、specular、diffuse の調整。

color *= uAmbientColor + uLightColor * (uDiffuseColor + specular);

uAmbientColor というのは上のデータ形式でいうと mirror_color というやつ。

uLightColor は MMD のこれ↓の RGB のところ。

uDiffuseColor はそのまま diffuse_color

specular は specular_color と specularity (shininess) から教科書通りに計算(表面の法線がカメラの方向と光源の方向のちょうど真ん中だと一番光沢が出る)。ただし、MMD とは微妙な光沢の違いが出てしまうので、もうちょっと改良できるならしたいところ。shininess を何かで割る感じかな?

ここで一旦色の上限を1に制限して、一番最後にトゥーンを掛ける。この部分は MMDAgent を参考にした。

vec2 toonCoord = 0.5 * (1.0 - (vec2(0.0, dot( uLightDirection, norm ))));

表面の法線が光源の方向を向いてると「明るい部分」の色、そうじゃなければ「暗い部分」の色になるという意味。「明るい部分」と「暗い部分」を連続的にではなくて段階的に(discrete)切り替えるのがトゥーンシェーディング。(↓を参考に)


輪郭線だけちゃんと出来るようになったらソースを公開しようと思う。最終的には物理演算まで行きたいところだけど、どうなるか…

何枚かスクリーンショットを貼っときます。

↓まずは標準モデル。輪郭以外の色はほぼ同じ。左腕の光沢が微妙に違う。

↓メタルモデル。乗算スフィアマップの確認にいい。髪の色だけちょっと薄い理由は不明。

↓ホメ春香。輪郭線の確認にいい。輪郭線の付け方はいくつかあるみたいだけど、その中でも「裏面を黒くする」という方針で合ってることが、スカートの裏地が黒いことでわかる。自分の方法だと裏面でないのにベタっと黒くなってしまう箇所(スカートのひだ)があるので何かがおかしい。

↓このアングルの髪の輪郭も太くなりすぎる。

↓顔の法線が逆になってる等特殊な作り方をしてあることで有名な Lat 式。まだ全然だめ。少なくともこれが表示できるようになりたい。

http://f.hatena.ne.jp/edvakf/20111003024903 (グロ注意)

Nyan Cat のビデオが YouTube から削除された件

復活しました→http://www.youtube.com/watch?v=QH2-TGUlwu4


このブログの趣旨とは違いますが、他に書くところもないのでここに。

YouTube で4月の月間最多再生数を取ったという Nyan Cat というビデオがあり、それに使われてた曲がニコニコ動画から出た曲だったので日本でも一部で話題になりました。

この動画のローディングバー(シークバー)が Nyan Cat になるという YouTube 運営による遊びもあり、先週の金曜日には2千万再生に到達しました。

ところが、つい先ほど Nyan Cat が削除されました。

一応解説しておきますと、Nyan Cat の動画は Saraj00n という方が YouTube にアップロードしたものですが、映像は prguitarman さんが配布していた gif アニメーションを使い、曲は daniwellP さんの Nyanyanyanyanyanyanya! をももももP さんという方がカバーしたものでした。ちなみにオリジナルの Nyanyanyanyanyanyanya! は非営利に限って複製・頒布が許されています。(ももももP さんのカバー版についてはよくわかりません)

自分は Nyan Cat のファンで、毎日5回ぐらい YouTube で聞いてたんですが、削除される数時間前に再生数は2070万回ぐらいだったと記憶しています。再生数が増えていくのを見るのが楽しみでしたが、著作権侵害だったら消されてもしょうがないか…と思って prguitarman さんのブログを見ると、

http://www.prguitarman.com/index.php?id=369

ということで、誰かが prguitarman さんの名を騙って YouTube にクレームを出したようです。

このブログ記事によると、prguitarman さんは YouTube に動画を復活させてもらうためのフォームを出したそうですが、YouTube からは「動画投稿者による申請でないと処理できない」と言われ、Saraj00n さんに連絡をとったそうです。

また、音楽の権利をはっきりさせるために daniwellP さんにも連絡をとっているそうです。

prguitarman さんが「誰でも簡単に(偽りでも)クレームを出せるのに、ビデオが自分のものであることを証明するためにはこんな面倒な手続きが必要なんて笑えるね」と皮肉で言っていますが、ほんとにそうですね。(いや、他人の著作物を勝手に使っているんだから証明が面倒なのは当たり前なんですが、偽クレームの簡単さに比べてどうなのって話です)

GNU screen を使うときにやるべきたった一行の設定と、Mac の Terminal.app でマウスを認識させる方法

タイトルは↓の記事へのオマージュです。

ここで紹介されてる

termcapinfo xterm* ti@:te@

について調べました。ここの FAQ に詳しく書いてありますが、ちょっと分かりにくいので順を追って説明します。


普通のターミナルで、less などのフルスクリーンプログラムを開いて、

ここで終了すると、

こうなりますよね?

ところが、GNU screen の中だと、デフォルトではこうなってしまいます。


これは、普通のターミナルの alternate screen という機能を使って「別画面で」フルスクリーンプログラムを実行しているからです。screen の中では alternate screen が有効ではないので、普段の感覚と違っていつもムキーってなります。このせいで僕は screen を敬遠してました。今日までは。

しかし、"alternate screen" "GNU screen" でググってみると、こんなオプションが!

altscreen on

これで screen の中でも、less 開いて閉じた後もいつもと同じように前のコマンドが見られて LifeChanging です!!!

話を戻して、

GNU screen が起動するときもこの alternate screen を使うので、GNU screen が終了した後は、起動する前の画面に戻ります。

ところが、termcapinfo xterm* ti@:te@ を使うと、screen が起動するときに alternate screen を使わないで起動するため、普通のターミナルのスクロールが進みます。screen を終了すると、↓みたいに「screen の中」の次の行から始まります。

だから termcapinfo xterm* ti@:te@ を使うとマウスでスクロールできるようになるのです。

でも、これだと、screen のコピーモードに入る → space でコピー開始 → スクロールでずーーっと上まで行く → space でコピー領域確定、という風にはできません。カーソルがついてきてくれないので。

よろしい、ならば MouseTerm だ

MouseTerm というのは Mac の Terminal.app がマウスを認識できるようにするための SIMBL プラグインです。

これさえあれば、termcapinfo xterm* ti@:te@ しなくても普通にマウスでスクロールできます。

それだけではなくて、VimEmacs でもマウスが使えるようになります。Vim なら .vimrc に

if has("mouse")
    set mouse=a
endif

と、Emacs なら .emacs にこのように書けばいいだけです。(だそうです)

(unless window-system
  (xterm-mouse-mode 1)
  (global-set-key [mouse-4] '(lambda ()
                               (interactive)
                               (scroll-down 1)))
  (global-set-key [mouse-5] '(lambda ()
                               (interactive)
                               (scroll-up 1))))

これは LifeChangingAgain !!!

まとめ

  • .screenrc には altscreen on を!
  • Mac の Terminal.app には MouseTerm を!

Windows のタスクバーを完全に消す

「自動的に隠す」だと2ピクセルほど見えてしまうので、完全に見えなくしてみた。Win32 API を使えば簡単だった。

こんな感じで taskbar_switch.cpp を作る (C++ でもないが)。

#include <windows.h>
#include <tchar.h>

BOOL restore(LPCTSTR lpClassName, LPCTSTR lpWindowName) {
  HWND hWnd = FindWindow(lpClassName, lpWindowName);
  return ShowWindow(hWnd, SW_RESTORE);
}

BOOL hide(LPCTSTR lpClassName, LPCTSTR lpWindowName) {
  HWND hWnd = FindWindow(lpClassName, lpWindowName);
  return ShowWindow(hWnd, SW_HIDE);
}

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                                  LPTSTR lpCmdLine, int nCmdShow) {

  if (hide(_T("Shell_TrayWnd"), NULL)) { // hide taskbar
    hide(_T("Button"), _T("Start")); // hide start button
  } else {
    restore(_T("Shell_TrayWnd"), NULL); // restore taskbar
    restore(_T("Button"), _T("Start")); // restore start button
  }
  //hide(_T("DV2ControlHost"), NULL); // hide start menu

  return 0;
}
  • 最後のコメントアウトしてあるところは好みで。タスクバーを隠してもスタートメニューは出せるので。
  • タスクバーとスタートボタンは実は別のオブジェクトで (少なくとも Windows 7 では)、別々に隠している。
    • クラス名が Button でタイトルが Start って、けっこうかぶりそうな感じ。

Visual Studioコマンドプロンプトから

> cl taskbar_switch.cpp /Fotaskbar_switch.obj /c
> link taskbar_switch.obj user32.lib /out:taskbar_switch.exe

とやって taskbar_switch.exe を作る。起動する度にタスクバーの表示と非表示を切り替え。

AutoHotKey に登録

せっかくなのでショートカットを割り当てとく。

さっきの taskbar_switch.exe を AutoHotKeyスクリプトと同じフォルダに置いて、AutoHotKey スクリプト

#i::Run "taskbar_switch.exe"

と書けば、Win+i でタスクバーの切り替え。

繰り返しになるが、タスクバーは消えてても Win キーでスタートメニューは出てくる。AutoHotKeyLWin Up::Return と書けばそれも無効にすることはできる。

こんなのみつけた

AutoHotKey だけで実現できる。コンパイルしなくても大丈夫。ただし、Windows 7 で使う場合にはタスクバーと一緒にスタートボタンも消すようにしないとスタートボタンが残ってしまう。


#Persistent
  ; タスクバーの非表示化
  WinHide,ahk_class Shell_TrayWnd
  WinHide,Start ahk_class Button
  TaskBarHide = 1
return

#i::
  ; タスクバーの非表示化
  if TaskBarHide =
  {
    WinHide,ahk_class Shell_TrayWnd
    WinHide,Start ahk_class Button
    TaskBarHide = 1
  }
  else
  {
    WinShow,ahk_class Shell_TrayWnd
    WinShow,Start ahk_class Button
    TaskBarHide =
  }
return

これだと Visual Studio もいらないのでラクですね。