免責事項
3Dや数学の専門家ではないので、所々おかなしな記述があるかもしれません。
予め、ご承知おきください。
これまでのあらすじ
- WebGLスクール第2期 2回目 メモ
- WebGLスクール第2期 2回目 メモ - その2
- WebGLスクール第2期 3回目 メモ
- WebGLスクール第2期 4回目 メモ
- WebGLスクール第2期 5回目 メモ
もくじ
今回学習したのは、下記となります。
- グレイスケール
- セピア調変換
- モザイクフィルタ
- ガウシアンフィルタ
- ノイズ生成
- ズームブラーフィルタ
- ゴッドレイフィルタ
- ソーベルフィルタ
グレイスケール
まずは、グレイスケールから。
先に完成形を見てみましょう。
こんな感じになります。
処理の流れを先に説明すると、こんな感じです。
- Frame Bufferモデルを書き込む
- Frame Bufferに書き込んだ情報をテクスチャとしてキャンバスに書き込み
- 最後に、シェーダーでグレイスケール処理を実行
グレイスケールの実装はこんなふうになります。
<script id="fs" type="x-shader/x-fragment">
precision mediump float;
uniform sampler2D texture;
const float redScale = 0.298912;
const float greenScale = 0.586611;
const float blueScale = 0.114478;
const vec3 monochromeScale = vec3( redScale, greenScale, blueScale );
varying vec2 vTextureCoord;
void main(){
vec3 smpColor = texture2D( texture, vTextureCoord ).rgb;
float gray = dot( monochromeScale, smpColor );
gl_FragColor = vec4( vec3( gray ), 1.0 );
}
</script>
ポイントは、この部分。
NTSC系加重平均法
に則って、赤・緑・青それぞれに重みづけをしています。
NTSC系加重平均法は大ざっぱに言うと、赤・緑・青が人の目から見てどれぐらい明るく見えるかを数値化したものです。
const float redScale = 0.298912;
const float greenScale = 0.586611;
const float blueScale = 0.114478;
const vec3 monochromeScale = vec3( redScale, greenScale, blueScale );
テクスチャの色味と、上の係数の内積を取るときれいなグレイスケールが完成します。
float gray = dot( monochromeScale, smpColor );
セピア調変換
次はセピア調変換です。
こちらは、グレイスケール処理ができれば、すぐに作ることができます。
というのも、処理の流れが
- グレイスケール処理を実施
- セピア調変換処理を実施
となるためです。
実際の実装は、こんな感じになります。
<script id="fs" type="x-shader/x-fragment">
precision mediump float;
uniform sampler2D texture;
const float redScale = 0.298912;
const float greenScale = 0.586611;
const float blueScale = 0.114478;
const vec3 monochromeScale = vec3( redScale, greenScale, blueScale );
const float sRedScale = 1.07;
const float sGreenScale = 0.74;
const float sBlueScale = 0.43;
const vec3 sepiaScale = vec3(sRedScale, sGreenScale, sBlueScale);
varying vec2 vTextureCoord;
void main(){
vec2 v = vTextureCoord * 2.0 - 1.0;
float l = 1.25 - length( v );
vec3 smpColor = texture2D( texture, vTextureCoord ).rgb;
float gray = dot( monochromeScale, smpColor );
vec3 sepiaColor = vec3(gray) * sepiaScale;
gl_FragColor = vec4( sepiaColor * l, 1.0 );
}
</script>
新しく増えたのは、下記だけです。
const float sRedScale = 1.07;
const float sGreenScale = 0.74;
const float sBlueScale = 0.43;
const vec3 sepiaScale = vec3(sRedScale, sGreenScale, sBlueScale);
// 中略
vec3 sepiaColor = vec3(gray) * sepiaScale;
グレイスケール同様、セピア調を表現するための係数を用意してそれをグレイスケールに掛けてあげるだけでセピア調の表現を実装することができます。
モザイクフィルタ
次は、モザイクフィルタです。
こちらも先のグレイスケールやセピア調変換と同様、シェーダーに特殊な実装をいれることで実装することができます。
実装は、こんな感じになります。
<script id="fs" type="x-shader/x-fragment">
precision mediump float;
uniform sampler2D texture;
uniform vec2 resolution;// 解像度(512px x 512px)
varying vec2 vTextureCoord;
const float rectScale = 100.0;
void main(){
vec2 p = gl_FragCoord.xy / resolution;// 0 ~ 1の範囲
vec2 q = floor( p * rectScale ) / rectScale;
vec4 smpColor = texture2D( texture, q );
gl_FragColor = smpColor;
}
</script>
ポイントとなるのはfloor( p * rectScale )
の部分です。
ここではfloorという組み込み関数を利用して、小数点以下を切り捨てています。
これによって、ある一定の範囲が同じ色に指定されるため、モザイクのような表現が可能になります。
ガウシアンフィルタ
次に挑戦するのは、ガウシアンフィルタです。
ガウシアンフィルタというのは、いわゆるぼかし
の一種です。
実装は下記になります。
var tmpWeight = [];
var t = 0;
var d = 100;
var offset = -9;
for ( var i = 0; i < 19; i++) {
var absOffset = Math.abs( offset );
var r = 1.0 + 2.0 * absOffset;
var w = Math.exp( -0.5 * ( r * r ) / d );
tmpWeight.push( w );
if (i > 0) {
w *= 2.0;
}
t += w;
offset++;
}
var weight = tmpWeight.map( function( value ) { return value / t; } );
// 中略
// 横のぼかし
initOrthoRender();
var program = ctx.createProgram( [ 'vs', 'fs' ] );
ctx.useProgram( program );
setupOrthoVbos( program );
setupuOrthoUniforms( program, true );
ctx.drawElements( ctx.gl.TRIANGLES, 6 );
// 縦のぼかし
initOrthoRender();
setupuOrthoUniforms( program, false );
ctx.drawElements( ctx.gl.TRIANGLES, 6 );
<script id="fs" type="x-shader/x-fragment">
precision mediump float;
uniform sampler2D texture;
uniform vec2 resolution;
uniform float weight[ 19 ];
uniform bool isHorizontal;
varying vec2 vTexCoord;
void main() {
vec3 destColor = vec3(0.0);
vec2 p = gl_FragCoord.st;
float offset = -9.0;
// horizontal
if( isHorizontal ) {
float bufferScale = 1.0 / resolution.s;
for ( int i = 0; i < 19; i++) {
vec2 coord = ( p + vec2( offset++, 0.0 ) ) * bufferScale;
destColor += texture2D( texture, coord ).rgb * weight[i];
}
// vertical
} else {
float bufferScale = 1.0 / resolution.t;
for ( int i = 0; i < 19; i++) {
vec2 coord = ( p + vec2( 0.0, offset++ ) ) * bufferScale;
destColor += texture2D( texture, coord ).rgb * weight[i];
}
}
gl_FragColor = vec4( destColor, 1.0 );
}
</script>
ぼかしを表現するためには、隣り合う色を良い感じで混ぜ合わせる必要があります。
その計算を単純に行ってしまうと、物すごく重たい処理となってしまうのですが、ガウシアンブラーを利用することである程度高速に行うことが可能です。
ガウシアンブラーを実装する場合、縦・横のぼかしを2回にわけて行う必要があります。
JSでいうと、下記の部分ですね。
// 横のぼかし
initOrthoRender();
var program = ctx.createProgram( [ 'vs', 'fs' ] );
ctx.useProgram( program );
setupOrthoVbos( program );
setupuOrthoUniforms( program, true );
ctx.drawElements( ctx.gl.TRIANGLES, 6 );
// 縦のぼかし
initOrthoRender();
setupuOrthoUniforms( program, false );
ctx.drawElements( ctx.gl.TRIANGLES, 6 );
そして、もう一つガウス関数を利用してどの程度の割合で隣り合う色と混ぜ合わせるのかを計算する必要があります。
var tmpWeight = [];
var t = 0;
var d = 100;
var offset = -9;
for ( var i = 0; i < 19; i++) {
var absOffset = Math.abs( offset );
var r = 1.0 + 2.0 * absOffset;
var w = Math.exp( -0.5 * ( r * r ) / d );
tmpWeight.push( w );
if (i > 0) {
w *= 2.0;
}
t += w;
offset++;
}
var weight = tmpWeight.map( function( value ) { return value / t; } );
ここまで、処理をおこなったらシェーダーで中心に行くほど、ぼかしを強くするよう実装すればガウシアンブラーの完成です。
// horizontal
if( isHorizontal ) {
float bufferScale = 1.0 / resolution.s;
for ( int i = 0; i < 19; i++) {
vec2 coord = ( p + vec2( offset++, 0.0 ) ) * bufferScale;
// 中心に近くなるほど、係数が高くなるよう設定
destColor += texture2D( texture, coord ).rgb * weight[i];
}
// vertical
} else {
float bufferScale = 1.0 / resolution.t;
for ( int i = 0; i < 19; i++) {
vec2 coord = ( p + vec2( 0.0, offset++ ) ) * bufferScale;
// 中心に近くなるほど、係数が高くなるよう設定
destColor += texture2D( texture, coord ).rgb * weight[i];
}
}
gl_FragColor = vec4( destColor, 1.0 );
}
ノイズ生成
次に挑む、効果はノイズです。
ノイズは乱数を利用して生成します。
乱数を利用しますと言いましたが、GLSLには乱数を生成する組み込み関数はありません。
そのため、自前で乱数生成機を作成しなければなりません。
実際の実装を見てみましょう。
<script id="fs" type="x-shader/x-fragment">
precision mediump float;
uniform sampler2D texture;
uniform vec2 resolution;
varying vec2 vTextureCoord;
#define PI 3.14159265
#define OCT 7
#define PER 0.75
float rnd( vec3 p ) {
vec3 i = floor( p );
vec4 a = dot( i, vec3( 1.0, 57.0, 21.0 ) ) + vec4( 0.0, 57.0, 21.0, 78.0 );
vec3 f = cos( ( p - i ) * PI ) * ( -0.5 ) + 0.5;
a = mix( sin( cos( a ) * a ), sin( cos( 1.0 + a ) * ( 1.0 + a ) ), f.x );
a.xy = mix( a.xz, a.yw, f.y);
return mix( a.x, a.y, f.z );
}
float noise( vec3 p ) {
float t = 0.0;
for( int i = 0; i < OCT; i++ ) {
float freq = pow( 2.0, float( i ) );
float amp = pow( PER, float( OCT - i ) );
t += max( rnd( p / freq ), 0.1 ) * amp;
}
return t;
}
float snoise2d( vec2 p, vec2 w ){
vec2 q = p / w;
return noise( vec3( p.x, p.y, 0.0 ) ) * q.x * q.y +
noise( vec3( p.x, p.y + w.y, 0.0 ) ) * q.x * ( 1.0 - q.y ) +
noise( vec3( p.x + w.x, p.y, 0.0 ) ) * ( 1.0 - q.x ) * q.y +
noise( vec3( p.x + w.x, p.y + w.y, 0.0 ) ) * ( 1.0 - q.x ) * ( 1.0 - q.y );
}
void main() {
float snoise = snoise2d( gl_FragCoord.xy, resolution );
vec3 smpColor = texture2D( texture, vTextureCoord ).rgb * snoise;
gl_FragColor = vec4( smpColor, 1.0 );
}
</script>
実際にノイズを作成する処理は、下記の部分です。
float rnd( vec3 p ) {
vec3 i = floor( p );
vec4 a = dot( i, vec3( 1.0, 57.0, 21.0 ) ) + vec4( 0.0, 57.0, 21.0, 78.0 );
vec3 f = cos( ( p - i ) * PI ) * ( -0.5 ) + 0.5;
a = mix( sin( cos( a ) * a ), sin( cos( 1.0 + a ) * ( 1.0 + a ) ), f.x );
a.xy = mix( a.xz, a.yw, f.y);
return mix( a.x, a.y, f.z );
}
乱数生成以外は今回あまり特殊なことはやっていません。
処理の概要は、下記のようになります。
- 乱数を利用して、低解像度のノイズを作成
- 作成したノイズをぼかす
- 同じような処理を施した複数のノイズを掛け合わせて、自然なノイズを作成
- 作成したノイズを、テクスチャに掛け合わせる
乱数の生成方法を変えると、当然ながら結果も変わるので色々試すと面白いかもしれないです。
ズームブラーフィルタ
なかなか終わりませんが、次にやるのはズームブラーです。
ズームブラーは、取得する色の座標をちょっとずつずらしてやって、集中線のような効果を出すものを言います。
<script id="fs" type="x-shader/x-fragment">
precision mediump float;
uniform sampler2D texture;
uniform vec2 resolution;
varying vec2 vTextureCoord;
const float count = 20.0;
const float weight = 1.0 / count;
const float strength = 5.0;
float rnd( vec2 p ) {
return fract( sin( dot( p, vec2( 12.9898, 4.1414 ) ) ) * 43758.5453 );
}
void main() {
vec3 smpColor = vec3( 0.0 );
// 0 ~ 1に正規化
vec2 p = gl_FragCoord.xy / resolution * 2.0 - 1.0;
// 正規化した値を元に乱数を生成
float r = rnd( normalize( p ) );
for( float f = 0.0; f <= count; f++ ) {
float g = ( f + r ) * weight;
vec2 q = vTextureCoord - p * g * strength * weight;
smpColor += texture2D( texture, q ).rgb * weight;
}
gl_FragColor = vec4( smpColor, 1.0 );
}
</script>
今回のサンプルでは、下記の部分で原点にむかってまっすぐ伸びるベクトルを作成しています。
// 0 ~ 1に正規化
vec2 p = gl_FragCoord.xy / resolution * 2.0 - 1.0;
そして、乱数を利用して参照する色の位置を少しずつ変化させてカメラズームのようなぼかしを実現しています。
// 乱数利用してちょっとずつぼかしている...
float g = ( f + r ) * weight;
vec2 q = vTextureCoord - p * g * strength * weight;
ゴッドレイフィルタ
ズームブラーを利用して、もう少し高度なことが行えます。
それが、ゴッドレイと呼ばれる効果です。
やっていることは、ほぼほぼズームブラーと同じですが、ちょっとだけ工夫があります。
スクリーンショットだとちょっと分かりづらいですが、手前の球体が奥からくる光を遮断しています。
先ほどのズームブラーの例のままだと、実は光は遮断されず遮蔽物を透過してしまいます。
では、どうするのかと言うと下記のような手順で描画することで可能となります。
- 光らせるモデルをFrame Bufferに描画
- 遮蔽物の影をFrame Bufferに描画
- 遮蔽物をキャンバスに描画
- キャンバスにズームブラーを適用
- 加算合成を利用して、光を表現
実際の実装は下記となります。
function render() {
var lightProgram = ctx.createProgram( [ 'light_vs', 'light_fs' ] );
ctx.useProgram( lightProgram );
ctx.toggleDepthFunc( true );
ctx.depthFunc();
ctx.toggleBlend( false );
// render the texture ( off screen )
var board = createBoard( 2, 2 );
ctx.bindTexture( canvasTexture, 1 );
setupVbos( lightProgram, board );
initRender();
setupUniforms( lightProgram, true, [ 0, 0, 0, 0 ] );
ctx.drawElements( ctx.gl.TRIANGLES, board.index.length );
// render the outline ( off screen )
var sphere = createSphere( 64, 64, 0.1, [ 0, 1, 0, 1 ] );
setupVbos( lightProgram, sphere );
setupUniforms( lightProgram, false, [ 0, 0, 0, 1 ] );
ctx.drawElements( ctx.gl.TRIANGLES, sphere.index.length );
// render the object ( canvas )
ctx.bindFramebuffer( null );
ctx.clear( { r: 0.3, g: 0.3, b: 0.3, a: 1 } );
ctx.viewport({
x: 0,
y: 0,
width: canvas.width,
height: canvas.height});
ctx.bindTexture( frameBufferAttr.texture, 0 );
setupUniforms( lightProgram, false, [ 0, 1, 0, 1 ] );
ctx.drawElements( ctx.gl.TRIANGLES, sphere.index.length );
// disable the depthFunc
ctx.toggleDepthFunc( false );
// enable the blending
ctx.toggleBlend( true );
ctx.setBlending( Context.AdditiveBlending );
// render the effect ( canvas )
var program = ctx.createProgram( [ 'vs', 'fs' ] );
ctx.useProgram( program );
setupOrthoVbos( program, board );
setupuOrthoUniforms( program );
ctx.drawElements( ctx.gl.TRIANGLES, board.index.length );
requestAnimationFrame( render );
}
手順がやや複雑ではありますが、今までの応用で全て実装することが可能です。
ソーベルフィルタ
ラストはソーベルフィルタです。
ソーベルフィルタは色味の差分を検出する技術になります。
これを利用すると、輪郭をとったりすることが可能です。
ソーベルフィルタはカーネルと呼ばれる配列を利用して呼び出した色をどのように扱うかを決定します。
カーネルの実装は下記のようになります。
// kernel
var hWeight = [
1.0, 0.0, -1.0,
2.0, 0.0, -2.0,
1.0, 0.0, -1.0
];
var vWeight = [
1.0, 2.0, 1.0,
0.0, 0.0, 0.0,
-1.0, -2.0, -1.0
];
シェーダーでは、JSで定義したカーネルを利用して本来表示する色の周囲9pxをチェックして中心と同じ色であれば黒色にしています。
つまり、周囲が同じ色であれば一律黒になるということです。
今回のサンプルでは、球体が緑一色、背景が灰色一色だったため球体と背景の境界線のみ色が出ています。
<script id="fs" type="x-shader/x-fragment">
precision mediump float;
uniform sampler2D texture;
uniform vec2 resolution;
uniform float hWeight[ 9 ];
uniform float vWeight[ 9 ];
varying vec2 vTexCoord;
void main() {
vec2 offset[ 9 ];
offset[0] = vec2( -1.0, -1.0 );
offset[1] = vec2( 0.0, -1.0 );
offset[2] = vec2( 1.0, -1.0 );
offset[3] = vec2( -1.0, 0.0 );
offset[4] = vec2( 0.0, 0.0 );
offset[5] = vec2( 1.0, 0.0 );
offset[6] = vec2( -1.0, 1.0 );
offset[7] = vec2( 0.0, 1.0 );
offset[8] = vec2( 1.0, 1.0 );
vec2 p = 1.0 / resolution;
vec3 horizonColor = vec3(0.0);
vec3 verticalColor = vec3(0.0);
for( int i = 0; i < 9; i++ ) {
vec2 coord = ( gl_FragCoord.st + offset[i] ) * p;
horizonColor += texture2D( texture, coord ).rgb * hWeight[ i ];
verticalColor += texture2D( texture, coord ).rgb * vWeight[ i ];
}
vec3 rgb = sqrt( horizonColor * horizonColor + verticalColor * verticalColor );
gl_FragColor = vec4( vec3( rgb ), 1.0 );
}
</script>
最後に
いつになく長くなってしまいました。(記事を書く時間も含めて。。。)
WebGLSchoolは今回の講義が最後でしたので、この記事も今回が最後になります。
(もしかすると、そのうち第1回のメモも投稿するかもしれないですが。。。)
今回の実装サンプルは下記にありますので良かったら見てください。