免責事項
3Dや数学の専門家ではないので、所々おかなしな記述があるかもしれません。
予め、ご承知おきください。
ひとりごと
光学迷彩の理解に手間取ってしまい、更新がおくれました。
この記事を書き終わるよりも前に、WebGLスクールも終了してしまいました。(苦笑)
@h_doxasさんお疲れさまでした。
これまでのあらすじ
もくじ
- バンプマッピング
- フレームバッファ
- 環境マッピング
- 光学迷彩シェーダ
バンプマッピング
「法線マッピング」と呼ばれることもあるようです。
頂点の個数は変えずに、あたかもそこに凸凹があるように模倣する技術です。
バンプマッピングを行うためには、法線マップという特殊なテクスチャを利用します。
特殊なテクスチャは下記で簡単に作成することが可能です。
テクスチャの例
さて、法線マップを活用するためには3つのベクトルを使用します。
- 法線ベクトル
- 接線ベクトル
- 従法線ベクトル
法線ベクトルについてはWebGLスクール第2期 3回目 メモをご参照ください。
接線ベクトルは法線ベクトルとY軸の外積で求めることができます。
一方、従法線ベクトルは法線ベクトルと接線ベクトルの外積で求めることができます。
なぜ、3つのベクトルが必要なのか???
テクスチャは言わずもがなですが、2次元情報です。
しかしながら、光源が扱われるのは3次元空間なので、このままでは情報量が足りません。
そこで、3つのベクトルの出番です。
それぞれのベクトルは、下記のような対応づけとなります。
- 接線 -> X軸
- 従法線 -> Y軸
- 法線 -> Z軸
というわけで作ってみました。
出力結果はこんな感じです。
全てのコードが見たいという方はこちらを参照してください。
ポイントだけ、抜粋します。
vec3 N = normalize( normal );
// 法線とY軸の外積で接線を求める
vec3 T = normalize( cross( N, vec3( 0.0, 1.0, 0.0 ) ) );
// 法線と接線から従法線を求める
vec3 B = normalize( cross( N, T ) );
// light vector transform
vLightDirection.x = dot( T, lightVector );
vLightDirection.y = dot( B, lightVector );
vLightDirection.z = dot( N, lightVector );
// eye vector transform
vEyeDirection.x = dot( T, eyeVector );
vEyeDirection.y = dot( B, eyeVector );
vEyeDirection.z = dot( N, eyeVector );
まず、先の説明のとおり法線・接線・従法線を求めます。
normalize
関数を利用して、ベクトルを正規化しているのもポイントです。
その後、光源・視点情報と各ベクトルの外積を計算します。
/**
RGBの範囲は0~1なので、2倍すると0~2の範囲に。
これらから1を引くと、-1~1の範囲となり3次元の座標系に合わせられる
*/
vec3 N = normalize( smpColor * 2.0 - 1.0 );
最後にテクスチャのRGBの情報から3次元の座標情報を取得して凸凹を表現します。
ここでは、テクスチャの色情報を0~1の範囲から-1~1に変換しています。
RGBから3次元座標???
冒頭の説明で、バンプマップには特殊なテクスチャを使用すると説明しました。
ここでいう特殊なテクスチャとは、3次元座標空間をRGBに落とし込んだ画像となります。
XYZもRGBも3要素からなる、情報なのでこういったことが可能になります。
中々分かりずらいですよね...
※ちなみに、RがX座標、GがY座標、BがZ座標に対応します。
フレームバッファ
フレームバッファは、メモリ上に描画結果を格納する仕組みとなります。
フレームバッファを利用すると、シーン全体を加工する場合などに非常に有用です。
フレームバッファの実例として、ポストエフェクトを実践してみます。
ポストエフェクトは、一度オフスクリーンで描画した結果に対し、事後的に
シェーダで加工する処理になります。
実際に書いてみましょう。
実行するとこんな感じですね。
ポイントだけ見ていきましょう。
function createFrameBuffer( width, height ) {
var frameBuffer = gl.createFramebuffer();
gl.bindFramebuffer( gl.FRAMEBUFFER, frameBuffer);
// レンダーバッファを作成 -> バインド
var renderBuffer = gl.createRenderbuffer();
gl.bindRenderbuffer( gl.RENDERBUFFER, renderBuffer );
//
gl.renderbufferStorage( gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height );
gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderBuffer );
var fTexture = gl.createTexture();
gl.bindTexture( gl.TEXTURE_2D, fTexture );
gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null );
gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR );
gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR );
gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT );
gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT );
gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, fTexture, 0 );
// 後片づけ
gl.bindTexture( gl.TEXTURE_2D, null );
gl.bindRenderbuffer( gl.RENDERBUFFER, null );
gl.bindFramebuffer( gl.FRAMEBUFFER, null );
frameTexture = fTexture;
return {
framebuffer: frameBuffer,
depthRenderbuffer: renderBuffer,
texture: fTexture
};
}
最初にgl.createRenderbuffer()
でフレームバッファ用のレンダーバッファを作成します。
その後、gl.bindRenderbuffer()
でバインドします。
ここまでの作業が終わったら、フォーマットを指定するのですが現時点では今回の例である、
gl.renderbufferStorage( gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height );
一択で問題ないです。(そもそも選択肢がないので...)
最後に、gl.framebufferRenderbuffer()
フレームバッファにレンダーバッファを紐づけます。
次に、フレームバッファを貼り付けるテクスチャの準備をします。
途中までは、通常のテクスチャと同じですが、今回はgl.framebufferTexture2D()
というメソッドを利用しています。
これで、フレームバッファをテクスチャと関連づけています。
gl.bindTexture( gl.TEXTURE_2D, texture );
gl.bindFramebuffer( gl.FRAMEBUFFER, frameBuffer.framebuffer );
gl.clearColor( 0.3, 0.3, 0.3, 1.0 );
gl.viewport( 0, 0, bufferSize, bufferSize );
gl.clearDepth( 1.0 );
gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT );
gl.drawElements( gl.TRIANGLES, indexes.length, gl.UNSIGNED_SHORT, 0 );
gl.bindTexture( gl.TEXTURE_2D, frameTexture );
gl.bindFramebuffer( gl.FRAMEBUFFER, null );
gl.clearColor( 0.3, 0.3, 0.3, 1.0 );
gl.viewport( 0, 0, c.width, c.height );
gl.useProgram( orthoProgram );
var orthoLocations = new Array( 1 );
orthoLocations[0] = gl.getUniformLocation( orthoProgram, 'orthoMatrix' );
var orthoPMatrix = mat4.identity( mat4.create() );
var orthoVpMatrix = mat4.identity( mat4.create() );
var orthoMvpMatrix = mat4.identity( mat4.create() );
mat4.multiply( orthoVpMatrix, orthoPMatrix, vMatrix );
mat4.multiply( orthoMvpMatrix, vpMatrix, mMatrix );
gl.uniformMatrix4fv( orthoLocations[0], false, orthoMvpMatrix );
gl.drawElements( gl.TRIANGLES, indexes.length, gl.UNSIGNED_SHORT, 0 );
実際に使う時は、gl.bindFramebuffer()
でフレームバッファにバインドを実行。
その後、フレームバッファと関連づけたテクスチャを描画することで利用できます。
ちなみに
今回の例では、ネガポジ反転しています。
ネガポジ反転は下記コードで実装できます。
gl_FragColor = vec4( vec3( 1.0 - smpColor.rgb), 1.0 );
やっていることは、1から元々の色を引いているだけです。
簡単ですね。
注意点
フレームバッファへの描画結果はテクスチャ座標を上下反転して参照してしまいます。
使用する場合は、上下を反転させて使うよう気をつけましょう。
環境マッピング
環境マッピングは、映りこみを再現するテクニックになります。
実際の中身は、テクスチャの応用になります。
何はともあれ実装してみましょう。
実行結果はこんな感じです。
今回はキューブマッピングという技術を利用します。
キューブマッピングは下記のようなコードで実装が可能です。
function createCubeTexture( targets, fn ) {
var tex = gl.createTexture();
gl.bindTexture( gl.TEXTURE_CUBE_MAP, tex );
for ( var i = 0; i < targets.length; i++ ) {
gl.pixelStorei( gl.UNPACK_FLIP_Y_WEBGL, false );
gl.texImage2D( targets[i], 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, imgs[i] );
}
gl.generateMipmap( gl.TEXTURE_CUBE_MAP );
gl.texParameteri( gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR );
gl.texParameteri( gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR );
gl.texParameteri( gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE );
gl.texParameteri( gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE );
gl.bindTexture(gl.TEXTURE_CUBE_MAP, null);
fn( tex );
}
普通のテクスチャと違うのは、主に2つ。
-
gl.bindTexture( gl.TEXTURE_CUBE_MAP, tex );
という指定をする -
gl.texImage2D();
の第1引数にテクスチャをはる面(※)を指定する
※はる面に指定できるのは、以下の6つ
- gl.TEXTURE_CUBE_MAP_NEGATIVE_X
- gl.TEXTURE_CUBE_MAP_NEGATIVE_Y
- gl.TEXTURE_CUBE_MAP_NEGATIVE_Z
- gl.TEXTURE_CUBE_MAP_POSITIVE_X
- gl.TEXTURE_CUBE_MAP_POSITIVE_Y
- gl.TEXTURE_CUBE_MAP_POSITIVE_Z
次に反射の実装を見てみます。
まず、準備としてJS側で反射処理を行うか、真偽値を設定します。
uniformLocations[2] = gl.getUniformLocation( program, 'reflection' );
// 中略
gl.uniform1i( uniformLocations[2], true );
シェーダ側で、次のように実装すると周りの風景を反射することができます。
vec3 ref = vNormal;
if( reflection ){
ref = reflect( vPosition - eyePosition, vNormal );
}
vec4 smpColor = textureCube( texture, ref );
ここでは真偽値reflection
がtrue
の時だけ、カメラの向きと法線から反射ベクトルを計算しています。
最後に計算した反射ベクトルと、テクスチャを掛け合わせてキューブテクスチャを作成しています。
光学迷彩シェーダ
フレームバッファを利用すると、光学迷彩のような効果を再現することができます。
実際の手順は下記です。
- テクスチャをフレームバッファに焼く
- 普通にモデルをキャンバスに描き出す
- 事前に焼いたテクスチャを、射影する
ここでいう、射影というのは↓の図のようにある画像に光をあててスクリーン映像を映し出すような処理を指します。
実際に作ってみましょう。
出力結果は下のようになります。
ポイントだけ見ましょう。
ttMatrix[0] = 0.5; ttMatrix[1] = 0.0; ttMatrix[2] = 0.0; ttMatrix[3] = 0.0;
ttMatrix[4] = 0.0; ttMatrix[5] = 0.5; ttMatrix[6] = 0.0; ttMatrix[7] = 0.0;
ttMatrix[8] = 0.0; ttMatrix[9] = 0.0; ttMatrix[10] = 1.0; ttMatrix[11] = 0.0;
ttMatrix[12] = 0.5; ttMatrix[13] = 0.5; ttMatrix[14] = 0.0; ttMatrix[15] = 1.0;
// View
mat4.lookAt( tvMatrix, rotatedEyePosition, centerPosition, rotatedCameraUp );
// Projection
mat4.perspective( tpMatrix, 45, 1, 0.1, 30.0 );
// PT
mat4.multiply( tptMatrix, ttMatrix, tpMatrix );
// VPT
mat4.multiply( tvptMatrix, tptMatrix, tvMatrix );
今回の処理で一番大事なのはここの記述です。
ここでは、テクスチャようの行列を計算しています。
ttMatrix
にベタ書きで、行列が書いてありますが、これは3D座標空間からテクスチャの座標空間に変換するための行列となります。
今回のように画面全体に、射影する場合はこの値固定でよいので、おまじないと思って覚えると良いかもです。
これは、いったい何をやっているんだ???
と夜も眠れない方は、下記を参考にすると良いかもしれません。
☆PROJECT ASURA☆ [OpenGL] 『射影テクスチャリング』
この部分だけ、キチンと理解できればフレームバッファの応用なので簡単に実装することができます。
最後に
今回の実装サンプルは下記にありますので良かったら見てください。
心を折りに来る、第三期を待ってます!!