3
3

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期 4回目 メモ

Posted at

免責事項

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

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

これまでのあらすじ

概要

  • アルファブレンディング
  • カリング
  • フォグシェーダ
  • トゥーンレンダリング

アルファブレンディング

まずは、アルファブレンディングから。

アルファブレンディングは、簡単に言えば透過をシミュレートする処理です。

ポイントは実際に透過しているのではなく、複数の色をブレンド(混ぜ合わせている)ことにあります。

実際に混ぜ合わせる要素は下記の2つです。

  • SRC
  • DST

SRCがこれから出力しようとしている色DSTが既に出力されている色になります。

実際に書いてみましょう。

011/public/js/app.js
/* global quat */
/* global mat4 */
( function() {
  function main() {
    var c  = document.getElementById( 'canvas' );
    var gl = c.getContext( 'webgl' ) || c.getContext( 'experimental-webgl' );

    var canvasSize = Math.min( this.innerWidth, this.innerHeight );

    c.width  = canvasSize;
    c.height = canvasSize;

    var qt = quat.identity( quat.create() );
    c.addEventListener( 'mousemove', calculateQuat );

    var vs = createShader( 'vs' );
    var fs = createShader( 'fs' );

    if ( !vs || !fs ) {
      return;
    }

    var program = createProgram( [ vs, fs ] );

    var locations = new Array( 3 );
    locations[0]  = gl.getAttribLocation( program, 'positions' );
    locations[1]  = gl.getAttribLocation( program, 'colors' );
    locations[2]  = gl.getAttribLocation( program, 'normals' );

    var strides = [ 3, 4, 3 ];

    // vboの作成
    var positionVbo  = createVbo( positions );
    gl.bindBuffer( gl.ARRAY_BUFFER, positionVbo );
    gl.enableVertexAttribArray( locations[0] );
    gl.vertexAttribPointer( locations[0], strides[0], gl.FLOAT, false, 0, 0 );

    var colorVbo = createVbo( colors );
    gl.bindBuffer( gl.ARRAY_BUFFER, colorVbo );
    gl.enableVertexAttribArray( locations[1] );
    gl.vertexAttribPointer( locations[1], strides[1], gl.FLOAT, false, 0, 0 );

    var normalVbo = createVbo( normals );
    gl.bindBuffer( gl.ARRAY_BUFFER, normalVbo );
    gl.enableVertexAttribArray( locations[2] );
    gl.vertexAttribPointer( locations[2], strides[2], gl.FLOAT, false, 0, 0 );

    // iboの作成
    var ibo = gl.createBuffer();
    gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, ibo );
    gl.bufferData( gl.ELEMENT_ARRAY_BUFFER, new Int16Array( indexes ), gl.STATIC_DRAW );
    gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, null );
    gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, ibo );

    gl.enable( gl.DEPTH_TEST );
    gl.depthFunc( gl.LEQUAL );
    
    gl.enable( gl.BLEND );
  	gl.blendFuncSeparate(
  		gl.SRC_ALPHA,
  		gl.ONE_MINUS_SRC_ALPHA,
  		gl.ONE,
  		gl.ONE
  	);

    var count = 0;
    render();

    function createShader( id ) {
      var shaderSrouce = document.getElementById( id );
      var shader;

      if ( !shaderSrouce ) {
        console.error( '指定された要素が存在しません' );
        return;
      }

      switch( shaderSrouce.type ){
      case 'x-shader/x-vertex':
        shader = gl.createShader( gl.VERTEX_SHADER );
        break;
      case 'x-shader/x-fragment':
        shader = gl.createShader( gl.FRAGMENT_SHADER );
        break;
      default :
        return;
      }

      gl.shaderSource( shader, shaderSrouce.text );
      gl.compileShader( shader );
      if ( gl.getShaderParameter( shader, gl.COMPILE_STATUS ) ){
        return shader;
      } else {
        console.error( gl.getShaderInfoLog( shader ) );
      }
    }

    function createProgram( shaders ) {
      var program = gl.createProgram();

      shaders.forEach( function( shader ){ gl.attachShader( program, shader ); });
      gl.linkProgram( program );
      if( gl.getProgramParameter( program, gl.LINK_STATUS ) ){
        gl.useProgram( program );
        return program;
      }else{
        console.error( gl.getProgramInfoLog( program ) );
      }
    }

    function createVbo( data ) {
      var vbo = gl.createBuffer();
      gl.bindBuffer( gl.ARRAY_BUFFER, vbo );
      gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( data ), gl.STATIC_DRAW );
      gl.bindBuffer( gl.ARRAY_BUFFER, null );
      return vbo;
    }

    function render() {
      count++;

      var deg = count % 360;
      var rad = deg * Math.PI / 180;

      var mMatrix   = mat4.identity( mat4.create() );
      var vMatrix   = mat4.identity( mat4.create() );
      var pMatrix   = mat4.identity( mat4.create() );
      var vpMatrix  = mat4.identity( mat4.create() );
      var mvpMatrix = mat4.identity( mat4.create() );

      var fovy = 45;
      var cx   = 1 * Math.sin( 0 );
      var cz   = 2 * Math.cos( 0 );

      var lightDirection = [ 0.0, 0.25, 0.75 ];
      var eyePosition    = [ cx, 0.0, cz ];
      var centerPosition = [ 0.0, 0.0, 0.0 ];
      var cameraUp       = [ 0.0, 1.0, 0.0 ];
      var ambientColor   = [ 0.5, 0.1, 0.1, 0.0 ];
      

      var rotatedEyePosition = new Array( 3 );
      convertToVec3( rotatedEyePosition, qt, eyePosition );
      
      var rotatedCameraUp = new Array( 3 );
      convertToVec3( rotatedCameraUp, qt, cameraUp );

      // ビュー座標変換
      mat4.lookAt( vMatrix, rotatedEyePosition, centerPosition, rotatedCameraUp );
      // 投影変換・クリッピング
      mat4.perspective( pMatrix, fovy, 1, 0.1, 100.0 );

      mat4.rotateY( mMatrix, mMatrix, rad );

      // かける順番に注意
      mat4.multiply( vpMatrix, pMatrix, vMatrix );
      mat4.multiply( mvpMatrix, vpMatrix, mMatrix );

      var uLocations = new Array( 6 );
      uLocations[0]  = gl.getUniformLocation( program, 'mvpMatrix' );
      uLocations[1]  = gl.getUniformLocation( program, 'invMatrix' );
      uLocations[2]  = gl.getUniformLocation( program, 'lightDirection' );
      uLocations[3]  = gl.getUniformLocation( program, 'eyePosition' );
      uLocations[4]  = gl.getUniformLocation( program, 'centerPoint' );
      uLocations[5]  = gl.getUniformLocation( program, 'ambientColor' );

      gl.uniformMatrix4fv( uLocations[0], false, mvpMatrix );

      var invMatrix = mat4.identity( mat4.create() );
      mat4.invert( invMatrix, mMatrix );

      gl.uniformMatrix4fv( uLocations[1], false, invMatrix );

      gl.uniform3fv( uLocations[2], lightDirection );
      gl.uniform3fv( uLocations[3], rotatedEyePosition );
      gl.uniform3fv( uLocations[4], centerPosition );
      gl.uniform4fv( uLocations[5], ambientColor );

      gl.clearColor( 0.7, 0.7, 0.7, 1.0 );
      gl.viewport( 0, 0, c.width, c.height );
      gl.clearDepth( 1.0 );
      gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT );

      gl.drawElements( gl.TRIANGLES, indexes.length, gl.UNSIGNED_SHORT, 0 );
      
      mMatrix = mat4.identity( mat4.create() );
      mat4.translate( mMatrix, mMatrix, [ 0.5, 0.0, 1.0 ] );
      mat4.rotateY( mMatrix, mMatrix, rad );
      mat4.multiply( mvpMatrix, vpMatrix, mMatrix );
      gl.uniformMatrix4fv( uLocations[0], false, mvpMatrix );
      
      invMatrix = mat4.identity( mat4.create() );
      mat4.invert( invMatrix, mMatrix );
      gl.uniformMatrix4fv( uLocations[1], false, invMatrix );
      
      gl.uniform3fv( uLocations[2], lightDirection );
      gl.uniform3fv( uLocations[3], rotatedEyePosition );
      gl.uniform3fv( uLocations[4], centerPosition );
      
      ambientColor   = [ 0.0, 0.0, 0.0, -0.4 ];
      gl.uniform4fv( uLocations[5], ambientColor );
      
      gl.drawElements( gl.TRIANGLES, indexes.length, gl.UNSIGNED_SHORT, 0 );
      
      gl.flush();

      requestAnimationFrame( render );
    }

    function calculateQuat( e ) {
      var cw = c.width;
      var ch = c.height;
      var wh = 1 / Math.sqrt( cw * cw + ch * ch );
      
      var x      = e.clientX - c.offsetLeft - cw * 0.5;
      var y      = e.clientY - c.offsetTop - ch * 0.5;
      var vector = Math.sqrt( x * x + y * y );

      var theta = vector * 2.0 * Math.PI * wh;// 回転量

      if ( vector !== 1 ) {
        vector = 1 / vector;
        x     *= vector;
        y     *= vector;
      }
      
      var axis = [ y, x, 0 ];// 任意の回転軸

      quat.setAxisAngle( qt, axis, theta );// クォータニオン, 任意の回転軸, 回転量
    }
  }
  
  function convertToVec3( dest, qt, vector ) {
      var rQt = quat.create();
      quat.invert( rQt, qt );
      
      var qQt = quat.create();
      var pQt = quat.create();
      
      pQt[0] = vector[0];
      pQt[1] = vector[1];
      pQt[2] = vector[2];
      
      quat.multiply( qQt, rQt, pQt );
      
      var destQt = quat.create();
      quat.multiply( destQt, qQt, qt );
      
      dest[0] = destQt[0];
      dest[1] = destQt[1];
      dest[2] = destQt[2];
  }
  
  var positions = [
    -0.5,  0.0,  0.0,// 0
     0.0,  0.5,  0.0,// 1
     0.0,  0.0,  0.5,// 2
     0.0, -0.5,  0.0,// 3
     0.5,  0.0,  0.0,// 4
  
     0.5,  0.0,  0.0,// 5
     0.0,  0.5,  0.0,// 6
     0.0,  0.0, -0.5,// 7
     0.0, -0.5,  0.0,// 8
    -0.5,  0.0,  0.0 // 9
  ];
  
  // 色情報、左から順にRGBA
  var colors = [
    0.0, 0.0, 1.0, 1.0,// 0
    0.0, 0.0, 1.0, 1.0,// 1
    0.0, 0.0, 1.0, 1.0,// 2
    0.0, 0.0, 1.0, 1.0,// 3
    0.0, 0.0, 1.0, 1.0,// 4
  
    0.0, 0.0, 1.0, 1.0,// 5
    0.0, 0.0, 1.0, 1.0,// 6
    0.0, 0.0, 1.0, 1.0,// 7
    0.0, 0.0, 1.0, 1.0,// 8
    0.0, 0.0, 1.0, 1.0 // 9
  ];
  
  var normals = [
   -1.0, 0.0, 0.0,// 0
    0.0, 1.0, 0.0,// 1
    0.0, 0.0, 1.0,// 2
    0.0,-1.0, 0.0,// 3
    1.0, 0.0, 0.0,// 4
  
    1.0, 0.0, 0.0,// 5
    0.0, 1.0, 0.0,// 6
    0.0, 0.0,-1.0,// 7
    0.0,-1.0, 0.0,// 8
   -1.0, 0.0, 0.0,// 9
  ];
  
  var indexes = [
    0, 1, 2,
    0, 2, 3,
    2, 1, 4,
    2, 4, 3,

    5, 6, 7,
    5, 7, 8,
    7, 6, 9,
    7, 9, 8
  ];

  window.addEventListener( 'load', main );

} )();

ブレンドを有効にするには、下記のメソッドを利用します。

gl.enable( gl.BLEND );

さて、この設定だけだと画面が白飛びするだけになってしまいます。

※どうやら、gl.BLENDを有効にするだけでブレンド処理が動いてしまうようです。

というわけで設定しましょう。

こんな感じで設定ができます。

gl.blendFuncSeparate(
  gl.SRC_ALPHA,           // SRC_RGB
  gl.ONE_MINUS_SRC_ALPHA, // DST_RGB
  gl.ONE,                 // SRC_A
  gl.ONE                  // DST_A
);

すこしずつ、ひも解いて行きましょう。

まず、gl.SRC_ALPHAですが、これはSRCのα値をそのまま使用するという意味になります。

次にgl.ONE_MINUS_SRC_ALPHAですが、これは1からSRCのα値を引いたものを使用するという意味です。

今回の例では、SRCのα値が0.6なので1.0 - 0.6 = 0.40.4になりますね。

※分かりづらいかもしれませんが、ambientColorでα値を-0.4しているので、実質0.6になっています。

ラスト、gl.ONEですが、これはそのまま1.0が設定されます。

色々小難しいことを説明しましたが、アルファブレンディングを行いたい場合は上記設定の決めうちでOKです。

alpha_blend.png

ちょっとした補足

ブレンドの注意点の一つに、モデルを書き出す順番があります。

アルファブレンディングを行う際は、カメラから見て奥から順にモデルを書き出す必要があります。

これを怠ると、きれいにアルファブレンディングが行われません...

なぜか

手前からモデルを書くと、深度テストと競合してしまうためです。

※手前にモデルがある部分だけ出力の計算を止めてしまいます

アルファブレンディングを行う際は、モデルの書き出し順番に注意しましょう。

カリング

次は、カリングについて。

カリングは別名隠面消去とも言います。

カリングを有効にすると、隠面を描画しないようになります。

ポリゴンには表面と裏面があり、通常裏面が隠面となっています。

カリングを利用することで、隠面を描画する処理が省略できるので負荷を軽減することが可能です。

では、実際にやってみましょう。

012/public/js/app.js
/* global quat */
/* global mat4 */
( function() {
  function main() {
    var c  = document.getElementById( 'canvas' );
    var gl = c.getContext( 'webgl' ) || c.getContext( 'experimental-webgl' );

    var canvasSize = Math.min( window.innerWidth, window.innerHeight );

    c.width  = canvasSize;
    c.height = canvasSize;

    var qt = quat.identity( quat.create() );
    c.addEventListener( 'mousemove', calculateQuat );

    var vs = createShader( 'vs' );
    var fs = createShader( 'fs' );

    if ( !vs || !fs ) {
      return;
    }

    var program = createProgram( [ vs, fs ] );

    var locations = new Array( 3 );
    locations[0]  = gl.getAttribLocation( program, 'positions' );
    locations[1]  = gl.getAttribLocation( program, 'colors' );
    locations[2]  = gl.getAttribLocation( program, 'normals' );

    var strides = [ 3, 4, 3 ];

    // vboの作成
    var positionVbo  = createVbo( positions );
    gl.bindBuffer( gl.ARRAY_BUFFER, positionVbo );
    gl.enableVertexAttribArray( locations[0] );
    gl.vertexAttribPointer( locations[0], strides[0], gl.FLOAT, false, 0, 0 );

    var colorVbo = createVbo( colors );
    gl.bindBuffer( gl.ARRAY_BUFFER, colorVbo );
    gl.enableVertexAttribArray( locations[1] );
    gl.vertexAttribPointer( locations[1], strides[1], gl.FLOAT, false, 0, 0 );

    var normalVbo = createVbo( normals );
    gl.bindBuffer( gl.ARRAY_BUFFER, normalVbo );
    gl.enableVertexAttribArray( locations[2] );
    gl.vertexAttribPointer( locations[2], strides[2], gl.FLOAT, false, 0, 0 );

    // iboの作成
    var ibo = gl.createBuffer();
    gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, ibo );
    gl.bufferData( gl.ELEMENT_ARRAY_BUFFER, new Int16Array( indexes ), gl.STATIC_DRAW );
    gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, null );
    gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, ibo );

    gl.enable( gl.DEPTH_TEST );
    gl.depthFunc( gl.LEQUAL );
    
    gl.enable( gl.CULL_FACE );

    var count = 0;
    render();

    function createShader( id ) {
      var shaderSrouce = document.getElementById( id );
      var shader;

      if ( !shaderSrouce ) {
        console.error( '指定された要素が存在しません' );
        return;
      }

      switch( shaderSrouce.type ){
      case 'x-shader/x-vertex':
        shader = gl.createShader( gl.VERTEX_SHADER );
        break;
      case 'x-shader/x-fragment':
        shader = gl.createShader( gl.FRAGMENT_SHADER );
        break;
      default :
        return;
      }

      gl.shaderSource( shader, shaderSrouce.text );
      gl.compileShader( shader );
      if ( gl.getShaderParameter( shader, gl.COMPILE_STATUS ) ){
        return shader;
      } else {
        console.error( gl.getShaderInfoLog( shader ) );
      }
    }

    function createProgram( shaders ) {
      var program = gl.createProgram();

      shaders.forEach( function( shader ){ gl.attachShader( program, shader ); });
      gl.linkProgram( program );
      if( gl.getProgramParameter( program, gl.LINK_STATUS ) ){
        gl.useProgram( program );
        return program;
      }else{
        console.error( gl.getProgramInfoLog( program ) );
      }
    }

    function createVbo( data ) {
      var vbo = gl.createBuffer();
      gl.bindBuffer( gl.ARRAY_BUFFER, vbo );
      gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( data ), gl.STATIC_DRAW );
      gl.bindBuffer( gl.ARRAY_BUFFER, null );
      return vbo;
    }

    function render() {
      count++;

      var deg = count % 360;
      var rad = deg * Math.PI / 180;

      var mMatrix   = mat4.identity( mat4.create() );
      var vMatrix   = mat4.identity( mat4.create() );
      var pMatrix   = mat4.identity( mat4.create() );
      var vpMatrix  = mat4.identity( mat4.create() );
      var mvpMatrix = mat4.identity( mat4.create() );

      var fovy = 45;
      var cx   = 1 * Math.sin( 0 );
      var cz   = 1 * Math.cos( 0 );

      var lightDirection = [ 0.0, 0.25, 0.75 ];
      var eyePosition    = [ cx, 0.0, cz ];
      var centerPosition = [ 0.0, 0.0, 0.0 ];
      var cameraUp       = [ 0.0, 1.0, 0.0 ];
      var ambientColor   = [ 0.0, 0.0, 0.0, 0.0 ];
      

      var rotatedEyePosition = new Array( 3 );
      convertToVec3( rotatedEyePosition, qt, eyePosition );
      
      var rotatedCameraUp = new Array( 3 );
      convertToVec3( rotatedCameraUp, qt, cameraUp );

      // ビュー座標変換
      mat4.lookAt( vMatrix, rotatedEyePosition, centerPosition, rotatedCameraUp );
      // 投影変換・クリッピング
      mat4.perspective( pMatrix, fovy, 1, 0.1, 100.0 );

      mat4.rotateY( mMatrix, mMatrix, rad );

      // かける順番に注意
      mat4.multiply( vpMatrix, pMatrix, vMatrix );
      mat4.multiply( mvpMatrix, vpMatrix, mMatrix );

      var uLocations = new Array( 6 );
      uLocations[0]  = gl.getUniformLocation( program, 'mvpMatrix' );
      uLocations[1]  = gl.getUniformLocation( program, 'invMatrix' );
      uLocations[2]  = gl.getUniformLocation( program, 'lightDirection' );
      uLocations[3]  = gl.getUniformLocation( program, 'eyePosition' );
      uLocations[4]  = gl.getUniformLocation( program, 'centerPoint' );
      uLocations[5]  = gl.getUniformLocation( program, 'ambientColor' );

      gl.uniformMatrix4fv( uLocations[0], false, mvpMatrix );

      var invMatrix = mat4.identity( mat4.create() );
      mat4.invert( invMatrix, mMatrix );

      gl.uniformMatrix4fv( uLocations[1], false, invMatrix );

      gl.uniform3fv( uLocations[2], lightDirection );
      gl.uniform3fv( uLocations[3], rotatedEyePosition );
      gl.uniform3fv( uLocations[4], centerPosition );
      gl.uniform4fv( uLocations[5], ambientColor );

      gl.clearColor( 0.7, 0.7, 0.7, 1.0 );
      gl.viewport( 0, 0, c.width, c.height );
      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.flush();
    }

    function calculateQuat( e ) {
      var cw = c.width;
      var ch = c.height;
      var wh = 1 / Math.sqrt( cw * cw + ch * ch );
      
      var x      = e.clientX - c.offsetLeft - cw * 0.5;
      var y      = e.clientY - c.offsetTop - ch * 0.5;
      var vector = Math.sqrt( x * x + y * y );

      var theta = vector * 2.0 * Math.PI * wh;// 回転量

      if ( vector !== 1 ) {
        vector = 1 / vector;
        x     *= vector;
        y     *= vector;
      }
      
      var axis = [ y, x, 0 ];// 任意の回転軸

      quat.setAxisAngle( qt, axis, theta );// クォータニオン, 任意の回転軸, 回転量
    }
  }
  
  function convertToVec3( dest, qt, vector ) {
      var rQt = quat.create();
      quat.invert( rQt, qt );
      
      var qQt = quat.create();
      var pQt = quat.create();
      
      pQt[0] = vector[0];
      pQt[1] = vector[1];
      pQt[2] = vector[2];
      
      quat.multiply( qQt, rQt, pQt );
      
      var destQt = quat.create();
      quat.multiply( destQt, qQt, qt );
      
      dest[0] = destQt[0];
      dest[1] = destQt[1];
      dest[2] = destQt[2];
  }
  
  var positions = [
    -0.5,  0.5,  0.0,// 0
    -0.5, -0.5,  0.0,// 1
     0.5,  0.5,  0.0,// 2
     0.5, -0.5,  0.0,// 3
  ];

  var colors = [
    1.0, 0.0, 0.0, 1.0,// 0
    0.0, 1.0, 0.0, 1.0,// 1
    0.0, 0.0, 1.0, 1.0,// 2
    1.0, 1.0, 1.0, 1.0,// 3
  ];

  var normals = [
   -0.25, 0.0, 0.75,// 0
   -0.25, 0.0, 0.75,// 1
    0.25, 0.0, 0.75,// 2
    0.25, 0.0, 0.75,// 3
  ];
  
  var indexes = [
    0, 1, 2,
    2, 3, 1
  ];

  window.addEventListener( 'load', main );

} )();

カリングを有効にするには下記のような記載でおこないます。

gl.enable( gl.CULL_FACE );

カリングを有効にすれば、あとはよしなにやってくれます。

ところで、ポリゴンの表面・裏面はどのように判断するのでしょうか?

答えは、頂点の回転順序です。

これは覚えるしかないので、覚えていきましょう。

  • 表面 -> 反時計回り
  • 裏面 -> 時計回り
var positions = [
  -0.5,  0.5,  0.0,// 0
  -0.5, -0.5,  0.0,// 1
   0.5,  0.5,  0.0,// 2
   0.5, -0.5,  0.0,// 3
];
  
var indexes = [
  0, 1, 2,// 反時計回り
  2, 3, 1 // 時計回り
];

実際、このサンプルでは、反時計回りの並びになっている正方形の左上のみが表示されています。

culling_sample.png

カリングは頂点の順序さえ、間違わなければ特別な手続きなく有効にすることができます。

最初は慣れないですが、覚えてしまえばこっちのものなので、覚えちゃいましょう。

補足

カリングの設定は、最初裏面を隠す設定になっています。

これを変更したい場合は、下記メソッドを利用します。

// 表面を隠す
gl.cullFace( gl.FRONT );
// 裏面を隠す( 初期設定 )
gl.cullFace( gl.BACK );

フォグ

つづいて、フォグについて。

フォグは直訳すると「霧(きり)」という意味です。

カメラからみて、遠くなるほど薄く霧がかかるような表現をする場合に利用するテクニックです。

では実装してみましょう。

<script id="vs" type="x-shader/x-vertex">
  precision mediump float;

  attribute vec3 positions;
  attribute vec4 colors;
  attribute vec3 normals;
  
  uniform   vec3 eyePosition;
  uniform   mat4 mMatrix;
  uniform   mat4 mvpMatrix;

  varying   vec4  vColor;
  varying   vec3  vNormal;
  varying   float vFog;
  
  const float fogStart = 0.0;
  const float fogEnd   = 15.0;
  const float fogCoef  = 1.0 / ( fogEnd - fogStart );

  void main( void ){
    vColor        = colors;
    vNormal       = normals;
    vec3 pos      = ( mMatrix * vec4( positions, 1.0 ) ).xyz;
    vFog          = length( eyePosition - pos ) * fogCoef;
    gl_Position   = mvpMatrix * vec4( positions, 1.0 );
  }
</script>
<script id="fs" type="x-shader/x-fragment">
  precision mediump float;
  
  varying   vec3    vNormal;
  varying   vec4    vColor;
  varying   float   vFog;
  
  uniform   mat4    invMatrix;
  uniform   vec4    ambientColor;
  uniform   vec3    lightDirection;
  uniform   vec3    eyePosition;
  uniform   vec3    centerPoint;
  
  const vec4 fogColor = vec4( 0.3, 0.3, 0.3, 1.0 );

  void main( void ){
    vec3 invLight = normalize( invMatrix * vec4( lightDirection, 1.0 ) ).xyz;
    vec3 invEye   = normalize( invMatrix * vec4( eyePosition - centerPoint, 1.0 ) ).xyz;
    vec3 halfVec  = normalize( invLight + invEye );
    float diff    = clamp( dot( invLight, vNormal ), 0.0, 1.0 );
    float spec    = clamp( dot( halfVec, vNormal ), 0.0, 1.0 );
    spec          = pow( spec, 10.0 );
    gl_FragColor  = mix( vec4( vec3( diff ), 1.0 ) * vColor + ambientColor + vec4( vec3( spec ), 0.0 ), fogColor, vFog );
  }
</script>

まず、フォグの開始点から終了点までを設定し、フォグの係数を求めます。

※1 Coefはcoefficientの略称で、係数という意味です。
※2 fogEndにはperspective行列のfar値より手前の数値を指定する必要があります。

const float fogStart = 0.0; // fogの開始点
const float fogEnd   = 15.0;// fogの終了点
const float fogCoef  = 1.0 / (fogEnd - fogStart); // fogの係数

次に、カメラからの距離に応じてフォグを適用していきます。

// モデル座標を取得
vec3 pos = ( mMatrix * vec4( positions, 1.0 ) ).xyz;
// カメラからの距離 × fogの係数
vFog     = length( eyePosition - pos ) * fogCoef;

最後に、mix関数で色を混ぜ合わせれば完成です。

この時、混ぜ合わせるfogColorは背景色と同じにすることがミソです。

そうしないと不自然な色になってしまうので注意が必要です。

const vec4 fogColor = vec4( 0.3, 0.3, 0.3, 1.0 );
// 中略

// 第一引数に、基準とする色
// 第二引数に、fogで使用する色
// 第三引数に、fogの係数を指定
gl_FragColor  = mix( vec4( vec3( diff ), 1.0 ) * vColor + ambientColor + vec4( vec3( spec ), 0.0 ), fogColor, vFog );

fog_sample.png

ハマりどころ

今回のサンプルで始めて、頂点シェーダとフラグメントシェーダで共通のuniformを指定しました。

GLSLでは、uniformを頂点シェーダとフラグメントシェーダとで共用できます。

ただ1点注意があります。

precision mediump float;を頂点シェーダとフラグメントシェーダに指定しないと、Uniforms with the same name but different type/precisionというエラーが発生します。

実際に自分もハマった所なので、気をつけましょう...

トゥーンレンダリング

最後に、トゥーンレンダリングについて。

トゥーンレンダリングは、アニメ調のレンダリングを行うテクニックを指します。

トゥーンレンダリングは、これまでに学習したことの組み合わせで実現することが可能です。

では、実際に作ってみましょう。

014/public/index.html
<script id="vs" type="x-shader/x-vertex">
  attribute vec3 positions;
  attribute vec4 colors;
  attribute vec3 normals;

  uniform   mat4 mvpMatrix;

  varying   vec4 vColors;
  varying   vec3 vNormals;

  void main( void ){
    vColors        = colors;
    vNormals       = normals;
    gl_Position    = mvpMatrix * vec4( positions, 1.0 );
  }
</script>
<script id="fs" type="x-shader/x-fragment">
  precision mediump float;

  varying vec4 vColors;
  varying vec3 vNormals;
  varying vec2 vTextureCoords;

  uniform mat4      invMatrix;
  uniform vec3      lightDirection;
  uniform sampler2D shadeTexture;

  void main( void ){
    vec3 invLight     = normalize( invMatrix * vec4( lightDirection, 1.0 ) ).xyz;
    float diff        = clamp( dot( invLight, vNormals ), 0.0, 1.0 );
    vec4 shadeColor   = texture2D( shadeTexture, vec2( diff, 0.0 ) );
    
    gl_FragColor =  vColors * vec4( vec3( shadeColor ), 1.0 );
  }
</script>

トゥーンレンダリングのミソは特殊な画像をテクスチャに利用することです。

下記のようにグラデーションが粗いモノクロ画像をテクスチャにすることで、陰影をクッキリさせるという仕組みになっています。

shade.png

実際には、下記の処理で拡散光の計算をテクスチャの陰影に変換することでトゥーンレンダリングを実現しています。

vec4 shadeColor = texture2D( shadeTexture, vec2( diff, 0.0 ) );

実際の結果はこんな感じですね。

toon_sample.png

最後に

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

practice-webgl/

WebGLしようぜ!!

WebGL スクール第2期の募集を開始します! 2015年5月開講!

3
3
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
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?