免責事項
3Dや数学の専門家ではないので、所々おかなしな記述があるかもしれません。
予め、ご承知おきください。
これまでのあらすじ
概要
- アルファブレンディング
- カリング
- フォグシェーダ
- トゥーンレンダリング
アルファブレンディング
まずは、アルファブレンディングから。
アルファブレンディングは、簡単に言えば透過をシミュレート
する処理です。
ポイントは実際に透過しているのではなく、複数の色をブレンド(混ぜ合わせている)ことにあります。
実際に混ぜ合わせる要素は下記の2つです。
- SRC
- DST
SRCがこれから出力しようとしている色
、DSTが既に出力されている色
になります。
実際に書いてみましょう。
/* 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.4
で0.4
になりますね。
※分かりづらいかもしれませんが、ambientColor
でα値を-0.4しているので、実質0.6
になっています。
ラスト、gl.ONE
ですが、これはそのまま1.0
が設定されます。
色々小難しいことを説明しましたが、アルファブレンディングを行いたい場合は上記設定の決めうちでOKです。
ちょっとした補足
ブレンドの注意点の一つに、モデルを書き出す順番
があります。
アルファブレンディングを行う際は、カメラから見て奥から順に
モデルを書き出す必要があります。
これを怠ると、きれいにアルファブレンディングが行われません...
なぜか
手前からモデルを書くと、深度テストと競合してしまうためです。
※手前にモデルがある部分だけ出力の計算を止めてしまいます
アルファブレンディングを行う際は、モデルの書き出し順番に注意しましょう。
カリング
次は、カリングについて。
カリングは別名隠面消去
とも言います。
カリングを有効にすると、隠面
を描画しないようになります。
ポリゴンには表面と裏面があり、通常裏面が隠面
となっています。
カリングを利用することで、隠面
を描画する処理が省略できるので負荷を軽減することが可能です。
では、実際にやってみましょう。
/* 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 // 時計回り
];
実際、このサンプルでは、反時計回りの並びになっている正方形の左上のみが表示されています。
カリングは頂点の順序さえ、間違わなければ特別な手続きなく有効にすることができます。
最初は慣れないですが、覚えてしまえばこっちのものなので、覚えちゃいましょう。
補足
カリングの設定は、最初裏面を隠す設定になっています。
これを変更したい場合は、下記メソッドを利用します。
// 表面を隠す
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 );
ハマりどころ
今回のサンプルで始めて、頂点シェーダとフラグメントシェーダで共通のuniform
を指定しました。
GLSLでは、uniformを頂点シェーダとフラグメントシェーダとで共用できます。
ただ1点注意があります。
precision mediump float;
を頂点シェーダとフラグメントシェーダに指定しないと、Uniforms with the same name but different type/precision
というエラーが発生します。
実際に自分もハマった所なので、気をつけましょう...
トゥーンレンダリング
最後に、トゥーンレンダリングについて。
トゥーンレンダリングは、アニメ調のレンダリングを行うテクニックを指します。
トゥーンレンダリングは、これまでに学習したことの組み合わせで実現することが可能です。
では、実際に作ってみましょう。
<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>
トゥーンレンダリングのミソは特殊な画像をテクスチャに利用することです。
下記のようにグラデーションが粗いモノクロ画像をテクスチャにすることで、陰影をクッキリさせるという仕組みになっています。
実際には、下記の処理で拡散光の計算をテクスチャの陰影に変換することでトゥーンレンダリングを実現しています。
vec4 shadeColor = texture2D( shadeTexture, vec2( diff, 0.0 ) );
実際の結果はこんな感じですね。
最後に
今回の実装サンプルは下記にありますので良かったら見てください。
WebGLしようぜ!!