Everyday Pieces ::
  • Webサービス
  • ブログパーツ
  1. ホーム
  2. プログラミング

three.jsで遊んでみる(19)

2014年8月27日 2013年6月5日 プログラミング MMD, three.js

★2014年4月8日更新
画角に対する対応が不十分だった点と、
計算が間違っていた点について修正しました。

今回は、
輪郭線を見直してみました。

輪郭線を描画するには
Sobelフィルターによる手法や、
頂点を法線方向に少し引き伸ばして裏面描画による手法、
とかがあります。

MMDでは後者の手法を用います。
袖やスカートの内側などのポリゴンが貼られてない部分が
自動的に指定色で描画されることになるので都合が良いのです。

fragmentシェーダでは指定された輪郭色を出力するだけなので、
以降の説明ではvertexシェーダについてのみ、要点を記述します。
thickWeightは輪郭線の太さを調整するためのパラメータです。
GLSLなコードは以下のようになります。
あらかじめ gl.cullFace( gl.FRONT ) しておくことに注意してください。

gl_Position = projectionMatrix * modelViewMatrix * vec4( objectPosition + objectNormal * thickWeight, 1.0 );

上記の実装では、
モデルのobject座標系で頂点を引き伸ばすので、
視点から遠いと輪郭線は細く、近いと太くスケールされます。

一つのやり方としてこれはこれでアリかと思いますが、
視点からの距離に関係なく輪郭線の太さを固定してみたくなったので、
今回やってみた次第です。

ということで、
投影変換後のclip座標系で引き伸ばせば、
たぶんどうにかなるだろうとやってみたんですが、
なんかうまく行かない(^_^;)

ということでググってみたら
こちらを見つけました。
輪郭線描画は「outline shader」って言うようですね。
投影変換後の法線のxとyを使い、
頂点のZ値を乗算することで太さを固定させるようです。
ふむ、なるほど。
GLSLなコードは以下のようになります

なお、
offsetを求める計算は、
行列演算なのでprojectionMatrixの
要素[0][1]と[1][0]も計算に含めるべきなのですが、
要素の値が常にゼロになるので省略できます。
varying vec3 vNormal;
vNormal = normalize( normalMatrix * objectNormal );
gl_Position = projectionMatrix * modelViewMatrix * vec4( objectPosition, 1.0 );
vec2 offset;
offset.x = projectionMatrix[0][0] * vNormal.x;
offset.y = projectionMatrix[1][1] * vNormal.y;
gl_Position.xy += offset * gl_Position.z * thickWeight;

がしかし、
モデルを視点から遠近移動させてみると
何故か輪郭線の太さが固定されない。
う~む、何か間違っているのか・・・?

さらにググって調べていたら、こちらを見つけました。
eye座標系でのZ値で太さ補正しないとダメでした(^_^;)
clip座標系でのZ値だと変化がリニアにならないっぽい。

ということでGLSLコードは以下のようになります。

varying vec3 vNormal;
vNormal = normalize( normalMatrix * objectNormal );
gl_Position = modelViewMatrix * vec4( objectPosition, 1.0 );
float depth = -gl_Position.z; // right-handed
gl_Position = projectionMatrix * gl_Position;
vec2 offset;
offset.x = projectionMatrix[0][0] * vNormal.x;
offset.y = projectionMatrix[1][1] * vNormal.y;
gl_Position.xy += offset * depth * thickWeight;

よく調べたら、eye座標系での -z は
clip座標系での w と一致することに気が付きました。
ということで更に以下のように最適化できそうです。

varying vec3 vNormal;
vNormal = normalize( normalMatrix * objectNormal );
gl_Position = projectionMatrix * modelViewMatrix * vec4( objectPosition, 1.0 );
vec2 offset;
offset.x = projectionMatrix[0][0] * vNormal.x;
offset.y = projectionMatrix[1][1] * vNormal.y;
gl_Position.xy += offset * gl_Position.w * thickWeight;

おや?
はじめの z が w になっただけですね。
z を w と間違ったのだろうか?
ん~、これで正しいのかちょっと自信が無くなって来ました(^_^;)
合ってると信じましょう(^_^;)

ところで、
gl_Position は同次座標系での位置 (x,y,z,w) を示しており、
-w ≦ x ≦ w, -w ≦ y ≦ w, -w ≦ z ≦ w という関係になっています。
これを w で割ると、
-1 ≦ x’ ≦ 1, -1 ≦ y’ ≦ 1, -1 ≦ z’ ≦ 1 となり、
1辺の大きさが2の立方体な3次元空間に整理されます。
ちなみにこれを正規化デバイス座標系
(NDC: Normalized Device Coordinates)と呼びます。
clip座標系において w で掛け算しておけば、
NDCにおいて w で割り算されるので、変化ないことになります。
つまり太さ固定という観点ではこれで合っていそうな気がします。

transform flow

perspective projection

残念ながら、これでもまだ不十分でした
画角(fov)が変わると太さも変化してしまうことが分かりました。
Perspectiveカメラの画角を fov、aspect比を a とします。
さらに tan(fov/2) = V とおくと、
ProjectionMatrix[0][0] = 1/aV
ProjectionMatrix[1][1] = 1/V
となります。
細かい計算については割愛しますが、
こちらやこちらを参考にしています。
したがって画角によらず太さを固定するには、
以下のようにやれば良さそうです。

varying vec3 vNormal;
vNormal = normalize( normalMatrix * objectNormal );
gl_Position = projectionMatrix * modelViewMatrix * vec4( objectPosition, 1.0 );
vec2 offset;
offset.x = vNormal.x * projectionMatrix[0][0];
offset.y = vNormal.y * projectionMatrix[1][1];
gl_Position.xy += offset * gl_Position.w * thickWeight / projectionMatrix[1][1];

さらに以下のように整理できます。

varying vec3 vNormal;
vNormal = normalize( normalMatrix * objectNormal );
gl_Position = projectionMatrix * modelViewMatrix * vec4( objectPosition, 1.0 );
vec2 offset;
offset.x = vNormal.x * projectionMatrix[0][0] / projectionMatrix[1][1];
offset.y = vNormal.y;
gl_Position.xy += offset * gl_Position.w * thickWeight;

ようやく最終形に辿りつけたようです(^_^;)

さて、
最後に thickWeight の決め方ですが・・・

thickWeight は次のように決めます。
スクリーンあるいはビューポートの高さを VH ピクセルとした場合に、
thickWeight = thickness * 0.5/VH;
となります。thickness(太さ)の単位はピクセルです。
アスペクト比は変換行列で考慮されているので高さでOKなはず。

とか以前は書いていたりしましたが、
これは間違っていたようです(^_^;)
NDCにおける大きさ2の高さに対してVHが対応するので、
thickWeight = thickness * 2/VH;
が正しいことになります。
なんか思い違いして逆数の0.5にしてしまっていたようです。
以前と比べたら4倍も違うことになってしまいますね。
なんで気づかなかったんだろ(^_^;)

ということで、
輪郭線の太さを固定させた結果はこちらです。
太さは2ピクセルに設定してみました。
輪郭線の太さが変わらないので、
モデルを視点に対して遠近移動させると、
相対的に輪郭線の太さが変わったように感じますが、
それは錯覚です(^_^;)

ChromeなどのWebGL対応のブラウザで見てください。PCパワーもそれなりに必要となるかもしれません。あまりにも時間がかかり過ぎるようならば、リロードして再度試してみてください。

mytest23

ところで、
いまだに十分うまく行ってない影の件ですが、
bias値というのをいじってみたら少し改善したような気がします。
このパラメータの存在には気づいていたのですが、
影響はないんじゃないかと思い込んでしまっていました。

カリング無しで深度マップを作るのは、
裏面描画にくらべて描画量が増えることもあり、
好ましくないのではと思っていましたが、
こちらを見るとそうでもないようです。
「両面シャドウ」とか言うっぽい。
実はbiasの方がより重要だったのかも・・・。
う~む、まだ理解不足ですね。

ちなみに、
biasを掛けないとこちらにあるようなモアレみたいのが出てしまうようです。
ただ適正なbias値を求める手法は調べても見つからなかったので、
(そもそも無いのかも?)
手動で調整するしかないようです。

関連記事
three.jsで遊んでみる(46) GlslCanvasを使ってみた three.jsで遊んでみる(45) three.jsで遊んでみる(44) three.jsで遊んでみる(43) three.jsで遊んでみる(42) three.jsで遊んでみる(41) three.jsで遊んでみる(40) three.jsで遊んでみる(39) three.jsで遊んでみる(38) three.jsで遊んでみる(37) three.jsで遊んでみる(36) three.jsで遊んでみる(35) three.jsで遊んでみる(34) three.jsで遊んでみる(33) three.jsで遊んでみる(32) three.jsで遊んでみる(31) three.jsで遊んでみる(30) three.jsで遊んでみる(29) three.jsで遊んでみる(28) three.jsで遊んでみる(27) three.jsで遊んでみる(26) three.jsで遊んでみる(25) three.jsで遊んでみる(24) three.jsで遊んでみる(23) three.jsで遊んでみる(22) three.jsで遊んでみる(21) three.jsで遊んでみる(20) three.jsで遊んでみる(18) three.jsで遊んでみる(17) three.jsで遊んでみる(16) three.jsで遊んでみる(15) three.jsで遊んでみる(14) three.jsで遊んでみる(13) three.jsで遊んでみる(12) three.jsで遊んでみる(11) three.jsで遊んでみる(10) three.jsで遊んでみる(9) three.jsで遊んでみる(8) three.jsで遊んでみる(7) three.jsで遊んでみる(6) three.jsで遊んでみる(5) three.jsで遊んでみる(4) three.jsで遊んでみる(3) three.jsで遊んでみる(2) three.jsで遊んでみる

3件のコメント

  • usk より:
    2013年6月12日 05:29

    はじめまして
    3dプログラミングに興味があり、いつも参考にさせてもらっています。

    さて、三体のモデルデータを見ていて弱音ハクの眼が上下逆になっているようでした。
    そこで、プログラムを自分の環境に持ってきて他のpmxデータを読み込んでみたところ、テクスチャがずれて表示されました。もし良かったら確認してみてください。

    これからも投稿楽しみにしています。

    返信
    • 管理人 より:
      2013年6月12日 18:41

      ご指摘ありがとうございます。
      テクスチャの貼り方を上下逆にしてみました。
      これでいかがでしょうか。

      データの座標系を左手から右手に変換して導入しているのですが、
      UV座標に対する考慮が足りなかったようです。
      失礼しました。

      それにしても今まで全く気が付きませんでした(^_^;)

      返信
  • usk より:
    2013年6月12日 23:49

    ご返事ありがとうございます
    テクスチャが正しく表示されました!

    なるほど、座標系の変換をしているんですね。
    勉強になりました。

    返信

コメントする キャンセル

アドレスが公開されることはありません。が付いている欄は必須項目です。

投稿ナビゲーション

three.jsで遊んでみる(18)
アザラシ界で今サーフボードが熱いらしい

カテゴリー

WordPress つぶやき トピック プログラミング

タグ

AS3 enchant.js FamilyTreeVis Flash Geolocation gif.js kinect Linux MMD MoneyTrackNote notifier.js OpenCV PDFカレンダー RISC-V three.js セキュリティ テーマ自作 ブログパーツ 動物 動画 麻雀

アーカイブ

© Everyday Pieces ::