読者です 読者をやめる 読者になる 読者になる

Processing.js(3Dモード)ではぅ君をふわふわさせる、の巻(メイキング篇)#p5advent

Processing

この記事は 本篇 のつづきです(ページが重くなりすぎたので分割しました)。

【もくじ】

はじめに

今回の出展作品は、一見、立方体をくるくる回すだけの比較的簡素なスケッチに見えます。

しかしながら、制作は Processing.js のバグとの闘いの連続であり、コードにはあらゆる苦悩の蹟が凝縮されています。

この問題に直面したのがおよそ1年前。2014年のAdvent Calendarでは技術不足のため、3Dの質感表現のほぼすべてを犠牲にした失敗作を投稿してしまいました。

今回はいろいろ改善できたので、ひとまず記録としてここに残しておこうと思います。

でこぼこ感のつくり方

概要と実装手順

今回のスケッチでは、見る角度によってテクスチャを動的に変化させることで「でこぼこ感」を表現しています。このテクニックはバンプマッピングと呼ばれており、手法としては比較的古典の部類に入ります。

以下に、効果がわかりやすいサンプルを置いておきます。激しく重いと思いますが。

バンプマッピングの実装手順は、一般的に以下の通りです。

  1. 高さマップを作る
  2. 高さマップを法線マップに変換する
  3. 法線マップからピクセルごとの陰影情報を計算する
  4. ピクセルごとの陰影情報を、面のテクスチャと合成する

以下に処理の詳細を示します*1

高さマップを作る

高さマップとは、表面の擬似的な高さ(でこぼこ)情報が記録された特殊なテクスチャのことです。下図のように、高さが高いほど白く、低いほど黒くなります。
f:id:tercel_s:20151204205252p:plain

今回用意した高さマップはこんな感じです(わかりやすいようにグリッドを追加)。
f:id:tercel_s:20151204210624p:plain:w300

高さマップを法線マップに変換する

法線マップとは、テクスチャ画像の全ピクセルの法線ベクタを格納した2次元配列のことで、先ほどの高さマップから導出できます。

はじめに、テクスチャ座標 {(x_i,\,y_i)} における高さマップの値を、{H(x_i,\,y_i)} と書くこととしましょう。ちなみに実装上の話ですが、{H(x_i,\,y_i)}の値域は00xffにしています(真っ黒なときは0、真っ白なときは0xffを取ります)。

{(x_i,\,y_i)} における{x}軸方向の変化は、左右の高さの差の平均をとって以下のように求めます。

{\dfrac{\partial H}{\partial x} = \dfrac{1}{2} \left( H(x_i+1,\,y_i) - H(x_i-1,\,y_i) \right)}

また{y}軸方向の変化も同様に計算できます。

{\dfrac{\partial H}{\partial y} = \dfrac{1}{2} \left( H(x_i,\,y_i+1) - H(x_i,\,y_i-1) \right)}

上式より、あるテクスチャ座標 {(x_i,\,y_i)} における {x}{y}各軸の傾きベクタを求めます。

{x}軸方向の傾きベクタ{d \boldsymbol{x}}{y}軸方向の傾きベクタ{d \boldsymbol{y}}とすると、それぞれ以下のように計算できます。

{
d \boldsymbol{x} = \left( 1,\, \dfrac{\partial H}{\partial x}  \right) = \left(  1,  \dfrac{1}{2} \bigl( H(x_i+1,\,y_i) - H(x_i-1,\,y_i) \bigr) \right)
}
{
d \boldsymbol{y} = \left( 1,\, \dfrac{\partial H}{\partial y}  \right) = \left(  1,  \dfrac{1}{2} \bigl( H(x_i,\,y_i+1) - H(x_i,\,y_i-1) \bigr) \right)
}

法線{\boldsymbol{n}}は、上記の傾きベクタに直交します。
具体的には、以下のように外積を計算することで{\boldsymbol{n}}を得ます。

{
\boldsymbol{n} = \dfrac{d \boldsymbol{x} \times d \boldsymbol{y}}{\left| d \boldsymbol{x} \times d \boldsymbol{y} \right| }
}

全ピクセルに対して法線を計算し、その結果を2次元配列に格納すれば法線マップのできあがりです。

法線マップからピクセルごとの陰影情報を計算する

ピクセルごとの法線{\boldsymbol{n}}がわかれば、あとは平行光源の方向と法線ベクタ内積を計算することで、陰影の濃淡値を決定できます。

このへんは(より基礎的な)平行光源の計算の議論なので、詳しい解説は割愛します。

ピクセルごとの陰影情報を、面のテクスチャと合成する

あとは、テクスチャ画像に対して濃淡値を合成することで、でこぼこ感のある外観を作ることができます。


……と、いかにも簡単に説明しましたが、実は一筋縄ではいかないのが Processing.js のつらいところ。

バンプマッピングを実装する際に泣かされた Processing.js のバグとその回避策を次のセクションで紹介します。

Processing.jsのバグと回避策

今回は以下2点のバグが制作上の障壁となりました。

  1. ライティング(シェーディング)のバグ
  2. テクスチャの書き換えができないバグ

それぞれサンプルつきでくわしく見ていきましょう。

ライティング(シェーディング)がレンダリングに反映されないバグ

Processing.js の3Dモードには、平行光源を設置した環境で、テクスチャの陰影が正しくレンダリングされないバグがあります*2

再現コードは以下の通りです。



立方体にチェックパターンのテクスチャを貼り、正面から平行光源を当てています。が、実行すると全く陰影がついておらず、のっぺりした見た目になってしまっています。


上記バグを、自力で陰影計算することにより回避したのがコチラ。



仕掛けは簡単で、毎フレームtint()を使ってテクスチャの明暗を設定しているだけです。

今回は実装も簡単ですし、処理自体もそこまで重くはないと思います。バグフィックスされるまではこの回避策で乗り切れるのではないでしょうか。

テクスチャを書き換えてもレンダリング結果に反映されないバグ

Processing.js の3Dモードには、テクスチャを書き換えてもレンダリングに反映されないバグがあります。

以下のコードは、フレームごとにテクスチャを書き換えていますが、実際に実行してみるとまったく変化がありません(簡単のため、ライトは計算していません)。


上記のバグは、テクスチャを書き換える都度、テスクチャ用のPImageを再生成することで回避可能です。

その他がんばったこと

ゆるふわ感アップ大作戦

はぅ君のアイコンにブラー(ぼかし)をかけただけです。

それ以上の凝ったことはしていませんが、バンプマッピングを適用するとふしぎと締まりのある絵になりました。

描画の高速化(焼け石に水)

バンプマッピングの処理はフラグメントシェーダ*3に書くのが一般的ですが、Processing.jsはシェーダが使えない*4ので、やむなくテクスチャ側で対処しています。ただし、テクスチャの書き換えは非常に重い処理なので、裏面カリング*5を行ってパフォーマンスを向上させるなど、細かな工夫を施しています(それでも遅いです)。

あとがき

というわけで、3歩進んで2歩下がるような開発でしたが、とりあえずなんとか形になってよかったです。わはー。つかれた。

謝辞

今回のキーワード「あたたかみ」「やわらかさ」「でこぼこ感」は、かべみさん(@sn2562)の活動からのインスパイアです。いつもありがとうございます。

*1:途中から息切れして解説が乱れます。

*2:p5.jsも似たようなバグがあります。

*3:GPUで、画面に出力する「色」を決定づける処理。または、その処理を書くための専用言語。別名ピクセルシェーダ。

*4:正確には、Processing.jsはシェーダをカプセル化している。

*5:ポリゴンの裏面を描画しないようにすること。

Copyright (c) 2012 @tercel_s, @iTercel, @pi_cro_s.