見出し画像

コピペで慣れる生WebGL#01ディストーションエフェクト

#00 はじめにでコピペで慣れる生WebGLという5回のシリーズをなぜ始めるのかを書きました。今回から5週に渡り実践的なサンプルをアップしていきたいとおもいます。

1回目はよく目にするディストーションエフェクトです。
3年前ぐらいからよく目にするようになり、画像ギャラリーの切替からサイトのシーン全体の遷移アニメーションなどで使うことができます。アクセントとしてかっこよく見せることができますが、多用し過ぎると気持ち悪く、残念な感じになります。。

最近見かけた、ディストーションエフェクトを使ったサイトの一部となります。

画像1

画像2

ディストーションエフェクトは生WebGLで書くと100行前後、5KB以下です。
このエフェクトのためだけに500KB以上のthree.jsや400KB以上のpixi.jsを使うのは賢明とは言えないと思います。

今回作成したディストーションエフェクトを使用したサンプルです。

画像3

生WebGLバージョン+コード

three.jsバージョン+コード

サンプルで使用している画像はunsplash.comからダウンロードしました。


仕組み

画像4

ディストーション用テクスチャーの色情報をベースにでテクスチャー#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)は英語ですが以下のサイトの図と説明がわかりやすいです。

画像5


AttriubutesとBufferの関係については以下のサイトでの解説がとても参考になります。



この記事が気に入ったらサポートをしてみませんか?