コピペで慣れる生WebGL#01ディストーションエフェクト
#00 はじめにでコピペで慣れる生WebGLという5回のシリーズをなぜ始めるのかを書きました。今回から5週に渡り実践的なサンプルをアップしていきたいとおもいます。
1回目はよく目にするディストーションエフェクトです。
3年前ぐらいからよく目にするようになり、画像ギャラリーの切替からサイトのシーン全体の遷移アニメーションなどで使うことができます。アクセントとしてかっこよく見せることができますが、多用し過ぎると気持ち悪く、残念な感じになります。。
最近見かけた、ディストーションエフェクトを使ったサイトの一部となります。
ディストーションエフェクトは生WebGLで書くと100行前後、5KB以下です。
このエフェクトのためだけに500KB以上のthree.jsや400KB以上のpixi.jsを使うのは賢明とは言えないと思います。
今回作成したディストーションエフェクトを使用したサンプルです。
生WebGLバージョン+コード
three.jsバージョン+コード
サンプルで使用している画像はunsplash.comからダウンロードしました。
仕組み
ディストーション用テクスチャーの色情報をベースにでテクスチャー#00とテクスチャー#01を切り替える仕組みになっています。
ディストーション用テクスチャーの色が黒 -> 切り替わるタイミング早い
ディストーション用テクスチャーの色が白 -> 切り替わるタイミング遅い
という風になっています。
テクスチャの切り替えはfragmentシェーダーで行っているので、そちらを見てください。
1. レンダリングを開始する前に行う初期化
2. 毎フレーム実行するdraw関数などのレンダリング
3. マウスを画像にホーバし変化するインタラクション
の大きく3つに分けて簡単に説明します。
初期化
共通部分
シェーダー(vertex, fragment)の作成
// vertexシェーダ
const vertexSrc = `
precision mediump float;
attribute vec4 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
gl_Position = position;
vUv = vec2( (position.x + 1.)/2., (-position.y + 1.)/2.);
}
`
// fragmentシェーダ
const fragmentSrc = `
precision mediump float;
uniform float uTrans;
uniform sampler2D uTexture0; // 画像#00
uniform sampler2D uTexture1; // 画像#01
uniform sampler2D uDisp; // ディストーション用画像
varying vec2 vUv;
float quarticInOut(float t) {
return t < 0.5
? +8.0 * pow(t, 4.0)
: -8.0 * pow(t - 1.0, 4.0) + 1.0;
}
void main() {
// ディストーションのタイミングを決定する
vec4 disp = texture2D(uDisp, vec2(0., 0.5) + (vUv - vec2(0., 0.5)) * (0.2 + 0.8 * (1.0 - uTrans)) );
float trans = clamp(1.6 * uTrans - disp.r * 0.4 - vUv.x * 0.2, 0.0, 1.0);
trans = quarticInOut(trans);
// 画像#00 画像#01の情報を取得
vec4 color0 = texture2D(uTexture0, vec2(0.5 - 0.3 * trans, 0.5) + (vUv - vec2(0.5)) * (1.0 - 0.2 * trans));
vec4 color1 = texture2D(uTexture1, vec2(0.5 + sin( (1. - trans) * 0.1), 0.5 ) + (vUv - vec2(0.5)) * (0.9 + 0.1 * trans));
gl_FragColor = mix(color0, color1 , trans);
}
`;
vertexシェーダーとfragmentシェーダーは生WebGLとthree.js両バージョンとも共通にしています。
three.jsのPlaneGeometryはattributeとしてuvを自動的に生成するので、そちらを使用して、vUvの値に代入する方が簡単です。
シェーダーについて詳しく知りたい方は以下のサイトをどうぞ。とても詳しく説明していますし、サンプル1つ1つの質が高いです。
生WebGL
プログラムの生成とシェーダーのコンパイル
// プログラムの作成
let program = gl.createProgram();
// vertextシェーダをコンパイル
var vShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vShader, vertexSrc);
gl.compileShader(vShader);
// fragmentシェーダをコンパイル
var fShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fShader, fragmentSrc);
gl.compileShader(fShader);
// プログラムにシェーダ(vertex, fragment)をリンクさせる
gl.attachShader(program, vShader);
gl.deleteShader(vShader);
gl.attachShader(program, fShader);
gl.deleteShader(fShader);
gl.linkProgram(program);
vertexシェーダーでposition attributeとして使用するバッファーの初期化
// バッファーの作成
let vertices = new Float32Array([
-1, -1,
1, -1,
-1, 1,
1, -1,
-1, 1,
1, 1,
]);
let vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
let vertexLocation = gl.getAttribLocation(program, 'position');
gl.bindBuffer(gl.ARRAY_BUFFER, null);
uniformのロケーションをあらかじめ取得しておく
// uniformのロケーションを取得しておく
let uTransLoc = gl.getUniformLocation(program, 'uTrans');
let textureLocArr = [];
textureLocArr.push(gl.getUniformLocation(program, 'uTexture0'));
textureLocArr.push(gl.getUniformLocation(program, 'uTexture1'));
textureLocArr.push(gl.getUniformLocation(program, 'uDisp'));
テクスチャ生成・初期化、画像設定
assetUrls.forEach( (url, index)=>{
let img = new Image();
// テクスチャの生成
let texture = gl.createTexture();
textureArr.push(texture);
img.onload = function(_index, _img){
let texture = textureArr[_index];
// imageをテクスチャーとして更新する
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, _img);
gl.generateMipmap(gl.TEXTURE_2D);
}.bind(this, index, img)
img.crossOrigin = "Anonymous";
img.src = url;
})
three.js
レンダラーの初期化
let renderer = new THREE.WebGLRenderer();
カメラの初期化
let camera = new THREE.OrthographicCamera( -1, 1, 1, -1, 1, 1000 );
camera.position.z = 1;
シーンの初期化
let scene = new THREE.Scene();
テクスチャの初期化と画像設定
assetUrls.forEach( (url, index) =>{
let img = new Image();
let texture = new THREE.Texture();
texture.flipY= false;
textureArr.push(texture);
img.onload = function(_index, _img){
let texture = textureArr[_index];
texture.image = _img;
texture.needsUpdate = true;
}.bind(this, index, img);
img.crossOrigin = "Anonymous";
img.src = url;
})
ジオメトリーの初期化
let geo = new THREE.PlaneGeometry(2, 2);
マテリアルの初期化(RawShaderMaterialを使用)
let mat = new THREE.RawShaderMaterial( {
uniforms: {
uTrans: { value: obj.trans },
uTexture0: {value: textureArr[0]},
uTexture1: {value: textureArr[1]},
uDisp: {value: textureArr[2]},
},
vertexShader: vertexSrc,
fragmentShader: fragmentSrc
} );
メッシュの初期化と初期化したメッシュをシーンに配置する
let mesh = new THREE.Mesh(geo, mat)
scene.add(mesh);
レンダリング(毎フレーム実行するメソッドなど)
生WebGL
function loop(){
// WebGLを初期化する
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 使用するprogramを指定する
gl.useProgram(program);
// 描画に使用する頂点バッファーをattributeとして設定する。
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.vertexAttribPointer(
vertexLocation, 2, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(vertexLocation);
// uniformsの値を指定する
// 描画に使用するのtexture設定
textureArr.forEach( (texture, index)=>{
gl.activeTexture(gl.TEXTURE0 + index);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform1i(textureLocArr[index], index);
})
gl.uniform1f(uTransLoc, obj.trans);
gl.drawArrays(gl.TRIANGLES, 0, 6);
requestAnimationFrame(loop);
}
毎フレーム
- WebGLの初期化
- プログラムの指定
- レンダリングで使用するattributeの指定
- レンダリングで使用するuniformの指定
- draw関数でレンダリング開始
という処理を行い、WebGLのレンダリングを行っています。
viewportはリサイズのみ変更するので、毎フレームgl.viewportという関数を呼ぶ必要はありません。
使うプログラムが1つなのでプログラム、attribute、uniformの指定は毎フレーム呼ぶ必要もありません。
プログラム2個以上になったときも考え、コピペで使えるよう毎フレーム呼んでいます。
最適化を考えて、毎フレームどのようにしたら関数の呼び出しを最小化ができるか、プログラムを書き直すのもいいかと思います。
three.js
function loop(){
mat.uniforms.uTrans.value = obj.trans;
renderer.render(scene, camera);
requestAnimationFrame(loop);
}
非常にシンプルです。
renderer.renderで何をしているのかは以下のリンクから確認することができます。
色々なメソッドが呼ばれているので、把握するのは大変だと思います。
インタラクション
// ロールオーバー時に呼び出される
canvas.addEventListener('mouseenter', function(){
TweenMax.killTweensOf(obj);
TweenMax.to(obj, 1.5, {trans: 1});
});
// ロールアウト時に呼び出される
canvas.addEventListener('mouseleave', function(){
TweenMax.killTweensOf(obj);
TweenMax.to(obj, 1.5, {trans: 0});
});
共通して同じ関数を使っています。
ロールオーバー完了したときにobj.transの値が1になり、ロールアウトが完了したときに0になります。
obj.transの値をuniformの値として渡し、ディストーションさせています。
以上、サンプルの簡単な説明となります。
fragmentシェーダーの数値を変えたり、テクスチャを変えたりしてみて、どのように変化するのかをみてください。
コピペで慣れるWebGL
#00 はじめに
#01 ディストーションエフェクト
補足
WebGLのレンダリングの仕組み(Graphic Pipeline)は英語ですが以下のサイトの図と説明がわかりやすいです。
AttriubutesとBufferの関係については以下のサイトでの解説がとても参考になります。