11
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

WebGLスクール第2期 6回目 メモ

Posted at

免責事項

3Dや数学の専門家ではないので、所々おかなしな記述があるかもしれません。

予め、ご承知おきください。

これまでのあらすじ

もくじ

今回学習したのは、下記となります。

  • グレイスケール
  • セピア調変換
  • モザイクフィルタ
  • ガウシアンフィルタ
  • ノイズ生成
  • ズームブラーフィルタ
  • ゴッドレイフィルタ
  • ソーベルフィルタ

グレイスケール

まずは、グレイスケールから。

先に完成形を見てみましょう。

こんな感じになります。

gray_scale_sample.png

処理の流れを先に説明すると、こんな感じです。

  1. Frame Bufferモデルを書き込む
  2. Frame Bufferに書き込んだ情報をテクスチャとしてキャンバスに書き込み
  3. 最後に、シェーダーでグレイスケール処理を実行

グレイスケールの実装はこんなふうになります。

019/src/index.html
<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 );

セピア調変換

次はセピア調変換です。

sepia_tone_sample.png

こちらは、グレイスケール処理ができれば、すぐに作ることができます。

というのも、処理の流れが

  1. グレイスケール処理を実施
  2. セピア調変換処理を実施

となるためです。

実際の実装は、こんな感じになります。

020/src/index.html
<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;

グレイスケール同様、セピア調を表現するための係数を用意してそれをグレイスケールに掛けてあげるだけでセピア調の表現を実装することができます。

モザイクフィルタ

次は、モザイクフィルタです。

こちらも先のグレイスケールやセピア調変換と同様、シェーダーに特殊な実装をいれることで実装することができます。

mosaic_sample.png

実装は、こんな感じになります。

021/src/index.html
<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という組み込み関数を利用して、小数点以下を切り捨てています。

これによって、ある一定の範囲が同じ色に指定されるため、モザイクのような表現が可能になります。

ガウシアンフィルタ

次に挑戦するのは、ガウシアンフィルタです。

ガウシアンフィルタというのは、いわゆるぼかしの一種です。

gaussian_sample.png

実装は下記になります。

022/src/js/index.js
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 );
022/src/index.html
<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 );
	}

ノイズ生成

次に挑む、効果はノイズです。

ノイズは乱数を利用して生成します。

noise_sample.png

乱数を利用しますと言いましたが、GLSLには乱数を生成する組み込み関数はありません。

そのため、自前で乱数生成機を作成しなければなりません。

実際の実装を見てみましょう。

023/src/index.html
<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 );
	}

乱数生成以外は今回あまり特殊なことはやっていません。

処理の概要は、下記のようになります。

  1. 乱数を利用して、低解像度のノイズを作成
  2. 作成したノイズをぼかす
  3. 同じような処理を施した複数のノイズを掛け合わせて、自然なノイズを作成
  4. 作成したノイズを、テクスチャに掛け合わせる

乱数の生成方法を変えると、当然ながら結果も変わるので色々試すと面白いかもしれないです。

ズームブラーフィルタ

なかなか終わりませんが、次にやるのはズームブラーです。

ズームブラーは、取得する色の座標をちょっとずつずらしてやって、集中線のような効果を出すものを言います。

zoom_blur_sample.png

024/src/index.html
<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;

ゴッドレイフィルタ

ズームブラーを利用して、もう少し高度なことが行えます。

それが、ゴッドレイと呼ばれる効果です。

god_ray_sample.png

やっていることは、ほぼほぼズームブラーと同じですが、ちょっとだけ工夫があります。

スクリーンショットだとちょっと分かりづらいですが、手前の球体が奥からくる光を遮断しています。

先ほどのズームブラーの例のままだと、実は光は遮断されず遮蔽物を透過してしまいます。

では、どうするのかと言うと下記のような手順で描画することで可能となります。

  1. 光らせるモデルをFrame Bufferに描画
  2. 遮蔽物の影をFrame Bufferに描画
  3. 遮蔽物をキャンバスに描画
  4. キャンバスにズームブラーを適用
  5. 加算合成を利用して、光を表現

実際の実装は下記となります。

/025/src/js/index.js
  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 );
  }

手順がやや複雑ではありますが、今までの応用で全て実装することが可能です。

ソーベルフィルタ

ラストはソーベルフィルタです。

ソーベルフィルタは色味の差分を検出する技術になります。

これを利用すると、輪郭をとったりすることが可能です。

sobel_filter_sample.png

ソーベルフィルタはカーネルと呼ばれる配列を利用して呼び出した色をどのように扱うかを決定します。

カーネルの実装は下記のようになります。

026/src/js/index.js
    // 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をチェックして中心と同じ色であれば黒色にしています。

つまり、周囲が同じ色であれば一律黒になるということです。

今回のサンプルでは、球体が緑一色、背景が灰色一色だったため球体と背景の境界線のみ色が出ています。

026/src/index.html
<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回のメモも投稿するかもしれないですが。。。)

今回の実装サンプルは下記にありますので良かったら見てください。

practice-webgl/

11
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?