これはWebGL Advent Calender(http://qiita.com/advent-calendar/2015/webgl )17日目の記事です。
お初にお目にかかります!
今回はタイトル通り、videoテクスチャの法線マップを動的につくって、バンプマッピングしてみるという記事です。みなさんゴリゴリシェーダー書いたりライブラリについて書いている中恐縮ですが、箸休め的な感じでさらっと読んで頂けるとありがたいです。
よく見かけるwebGL Waterのようなシミュレーションって、すごくきれいですが、やはりいろいろな知識がないと実装は難しいよなぁと思い、なんだかよう分からん人でも似たようなことをビデオでやってみよう!というところから始めました。※あくまでパッと見それっぽい感じなだけということに注意!!
いろいろな動画をテクスチャとして試した結果、一番面白かったものをあげましたw
####デモ ※読み込みちょっとかかります
くまちゃんのお尻がかわいいです。
流れとしては、
<video>を生成&読み込み -> canvas上でビデオを更新しながらオフスクリーンで法線マップを生成 -> それをもとにバンプマッピング
という感じです。特に変わったことはせず、素直に書いてます。
####参考にしたページたち
今回は@doxasさんが運営しているwgld.orgから下記の記事を参考に作りました。
また、サイト内で紹介されているライブラリも使わせていただいております!
ほんとwgld.org様様です。半年前くらいからコツコツ1記事ずつやっていって、だんだん理解できるようになってきた気がします。本当にありがとうございます。
上記を参考に進めていけば実装できそうなので、とりたててコード載せて説明する必要もなさそうですが、1点だけ苦戦したところがあったので紹介したいと思います。
###動的に法線マップを生成する
<video>要素をcanvasにはっつけて、動的に法線マップを生成…というのを繰り返しているのですが、最初はこれをCPU側(JavaScript)でやってしまい、メモリが大変なことになってしまいました。
※下記はさきほど紹介した法線マップの生成から引用させて頂いてます。
function normalMap(src){
var i, j, k, l, m, n, o;
var g, f;
var width = src.width;
var height = src.height;
var ctx = src.getContext('2d');
f = ctx.getImageData(0, 0, width, height);
g = ctx.createImageData(f);
for(i = 0; i < width; i++){
for(j = 0; j < height; j++){
k = (i - 1 < 0 ? 0 : i - 1) + j * width;
m = returnLuminance(f.data, k * 4);
k = (i + 1 > width - 1 ? i : i + 1) + j * width;
n = returnLuminance(f.data, k * 4);
l = (n - m) * 0.5;
k = i + ((j - 1) < 0 ? 0 : j - 1) * width;
m = returnLuminance(f.data, k * 4);
k = i + ((j + 1) > height - 1 ? j : j + 1) * width;
n = returnLuminance(f.data, k * 4);
o = (n - m) * 0.5;
var dyx = [0.0, l, 1.0];
var dyz = [1.0, -o, 0.0];
var dest = vec3Normalize(vec3Cross(dyx, dyz));
k = i + j * width;
g.data[k * 4] = Math.floor((dest[2] + 1.0) * 0.5 * 255);
g.data[k * 4 + 1] = Math.floor((dest[0] + 1.0) * 0.5 * 255);
g.data[k * 4 + 2] = Math.floor((dest[1] + 1.0) * 0.5 * 255);
g.data[k * 4 + 3] = 255;
}
}
return g;
}
canvasにビデオをオフスクリーンでレンダリングしたものをこの関数を使い法線マップに変換するのですが、1フレームごとにこの関数を通して変換すると
f = ctx.getImageData(0, 0, width, height);
のところでメモリを膨大に消費し続け、chromeでは最終的にタブが落ちます。
さすがにこの状態のまま公開するわけにはいかないので、これをGPU側(GLSL)で処理するよう書き直しました。(コード内にコメント書いてますが、使うときは消してください。)
precision mediump float;
uniform sampler2D texture;
uniform vec2 resolution;
varying vec2 vTexCoord;
const float RED = 0.298912;
const float GREEN = 0.586611;
const float BLUE = 0.114478;
vec3 monochromeScale = vec3(RED, GREEN, BLUE);
void main(void){
float x = gl_FragCoord.x / resolution.x;
float y = gl_FragCoord.y / resolution.y;
float minW = 1.0 / resolution.x;
float minH = 1.0 / resolution.y;
//今のフラグメント位置の上下左右の位置をもってきて
vec2 left = vec2(x - minW < 0.0 ? 1.0 : x - minW , y);
vec2 top = vec2(x, y + minH > 1.0 ? 0.0 : y + minH);
vec2 right = vec2(x + minW > 1.0 ? 0.0 : x + minW, y);
vec2 bottom = vec2(x, y - minH < 0.0 ? 1.0 : y - minH);
//その位置の色をとって
vec4 leftColor = texture2D(texture, left);
vec4 topColor = texture2D(texture, top);
vec4 rightColor = texture2D(texture, right);
vec4 bottomColor = texture2D(texture, bottom);
//色の輝度をとって
float nLeft = dot(leftColor.rgb, monochromeScale);
float nTop = dot(topColor.rgb, monochromeScale);
float nRight = dot(rightColor.rgb, monochromeScale);
float nBottom = dot(bottomColor.rgb, monochromeScale);
//上下・左右の差で
float m = (nRight - nLeft) * 0.5;
float o = (nBottom - nTop) * 0.5;
//3次元ベクトルを求めて
vec3 dyx = vec3(0.0, m, 0.1);
vec3 dyz = vec3(0.1, -o, 0.0);
//外積を正規化
vec3 dest = normalize(cross(dyx, dyz));
//各色に割り当てる
vec4 smpColor = vec4((dest.z + 1.0) * 0.5, (dest.x + 1.0) * 0.5, (dest.y + 1.0) * 0.5, 1.0);
gl_FragColor = smpColor;
}
もっといい書き方があるんだと思いますが、今の自分にはこれくらいが分かりやすくてよいです。が、アドバイスを頂けると大変ありがたいです。そしてGPUってやっぱはやい。すごい。あとはこれをもとにバンプマッピングするという感じです。
##きづいたこと
- 動画素材に関することですが、撮影する際のピント調節で、バンプマッピングした時の深さ?細かさ?を調節できます。デモでオリジナル動画と見比べるとよくわかります。
- 今回アップしたデモは回転する処理を加えていないんですが、映像を撮影する段階でカメラが右や左にパンすると球が回っているように見えるんですね。いい感じにイージングもつくし。
- <video>と連携すると、そんなにすごいことしてないのに、見た目はいい感じにできていいなぁと思いました。webカメラとかつかったり、skyboxのように箱の内側に適用してみるのも面白いと思います。DMMさんのあの動画素材がほしいです。
ちょー余談ですが、最初はアドカレあんまり埋まってなかったら気軽に真ん中くらいに「なんか書く!」って登録したら、いつの間にか全部埋まってて、しかもどれもレベル高くて、息がつまりそうでした笑 けど楽しかったです。誰かのためになれば幸いです。
明日は@pnlybubblesさんです!