Twitter
2021年06月12日 - WebVR・Three.js

EffectComposerで揺らぎエフェクト(1)

EffectComposerでポストプロセッシング」に続き「Creating a Water-like Distortion Effect with Three.js」を参考に、EffectComposerで揺らぎエフェクトを制作しました。※Three.jsはr129を使用しています。

EffectComposerで揺らぎエフェクト

EffectComposerのオリジナルエフェクトで、水の揺らぎのようなエフェクトを制作します。まずは管理用クラスを作成し、マウスの動きに合わせてcanvasに波紋を描きます。

● 管理用クラスの作成

管理用クラスを作成してcanvasを生成します。

class TouchTexture{
	constructor(){

		//canvasのサイズ
		this.width = window.innerWidth;
		this.height = window.innerHeight;

		//波紋の半径
		this.radius = this.width * 0.05;

		this.init();
	}

	init(){

		//canvasの生成
		this.canvas = document.createElement('canvas');
		this.canvas.id = 'TouchTexture';
		this.canvas.width = this.width;
		this.canvas.height = this.height;
		this.ctx = this.canvas.getContext('2d');
		this.clear();

		//canvasを配置
		const container = document.querySelector('#canvas_vr');
		container.append(this.canvas);
	}

	clear(){

		//canvasをクリア
		this.ctx.fillStyle = 'black';
		this.ctx.fillRect(0,0,this.canvas.width,this.canvas.height);
	}

	update(){
	}
}

TouchTextureのインスタンスを生成します。

import { TouchTexture } from './lib/touchtexture.js';

//インスタンスを生成
const touchTexture = new TouchTexture();

//アニメーション
function rendering(){
	requestAnimationFrame(rendering);

	if(touchTexture){
		touchTexture.update();
	}
}

● 波紋の生成

マウスの動きに合わせて波紋を生成します。波紋は少しぼかします。

class TouchTexture{
	constructor(options){

		//波紋用の配列
		this.points = [];
		this.width = window.innerWidth;
		this.height = window.innerHeight;
		this.radius = this.width * 0.05;

		//波紋のライフ
		this.maxAge = 64;

		this.init();
	}

	init(){
		this.canvas = document.createElement('canvas');
		this.canvas.id = 'TouchTexture';
		this.canvas.width = this.width;
		this.canvas.height = this.height;
		this.ctx = this.canvas.getContext('2d');
		this.clear();

		const container = document.querySelector('#canvas_vr');
		container.append(this.canvas);
	}

	clear(){
		this.ctx.fillStyle = 'black';
		this.ctx.fillRect(0,0,this.canvas.width,this.canvas.height);
	}

	addPoint(point){

		//配列に追加
		this.points.push({x:point.x,y:point.y,age:0});
	}

	drawPoint(point){
		let pos = {
			x:point.x * this.width,
			y:point.y * this.height
		}
		const radius = this.radius;
		const ctx = this.ctx;

		//波紋の透明度
		let intensity = 1.0;
		intensity = 1.0 - point.age / this.maxAge;
		
		let color = '255,255,255';
		let offset = this.width * 5.0;

		//波紋をぼかす
		ctx.shadowOffsetX = offset;
		ctx.shadowOffsetY = offset;
		ctx.shadowBlur = radius * 1;
		ctx.shadowColor = `rgba(${color},${0.2 * intensity})`;

		//波紋を描画
		this.ctx.beginPath();
		this.ctx.fillStyle = 'rgba(255,0,0,1)';
		this.ctx.arc(pos.x - offset,pos.y - offset,radius,0,Math.PI * 2);
		this.ctx.fill();
	}

	update(){
		this.clear();

		const _this = this;
		this.points.forEach(function(point,i){

			//ライフがなくなったら波紋を削除
			point.age += 1;
			if(point.age > _this.maxAge){
				_this.points.splice(i,1);
			}
		});
		this.points.forEach(function(point){

			//波紋を描画
			_this.drawPoint(point);
		});
	}
}

マウスの動きに合わせて波紋を生成します。

import { TouchTexture } from './lib/touchtexture.js';

const touchTexture = new TouchTexture();

//マウスの動きに合わせて波紋を生成
window.addEventListener('mousemove',onMouesMove);

function onMouesMove(event){
	const point = {
		x:event.clientX / window.innerWidth,
		y:event.clientY / window.innerHeight
	}

	//波紋を追加
	touchTexture.addPoint(point);
}

function rendering(){
	requestAnimationFrame(rendering);

	if(touchTexture){
		touchTexture.update();
	}
}

● 波紋の動きを調整

最後に生成した波紋の位置座標を使用して単位ベクトルを計算し、加速度と合わせて波紋の動きを調整します。

class TouchTexture{
	constructor(options){

		~ 略 ~

		this.last = null;
	}

	~ 略 ~

	addPoint(point){
		let force = 0;
		let vx = 0;
		let vy = 0;
		const last = this.last;

		if(last){

			//単位ベクトルを計算
			const relativeX = point.x - last.x;
			const relativeY = point.y - last.y;
			const distanceSquared = relativeX * relativeX + relativeY * relativeY;
			const distance = Math.sqrt(distanceSquared);
			vx = relativeX / distance;
			vy = relativeY / distance;

			//加速度を計算
			force = Math.min(distanceSquared * 10000,1.0);
		}

		//最後に生成した波紋として代入
		this.last = {
			x:point.x,
			y:point.y
		}

		//配列に追加
		this.points.push({x:point.x,y:point.y,age:0,force,vx,vy});
	}

	~ 略 ~

	update(){
		this.clear();

		let agePart = 1.0 / this.maxAge;

		const _this = this;
		this.points.forEach(function(point,i){

			//波紋の速度を計算
			let slowAsOlder = (1.0 - point.age / _this.maxAge);
			let force = point.force * agePart * slowAsOlder;
			point.x += point.vx * force;
			point.y += point.vy * force;

			//ライフがなくなったら波紋を削除
			point.age += 1;
			if(point.age > _this.maxAge){
				_this.points.splice(i,1);
			}
		});
		this.points.forEach(function(point){
			_this.drawPoint(point);
		});
	}
}

● イージングの追加とカラーチャネルへのデータエンコード

波紋の動きにイージングを追加します。また、波紋の動きをThree.jsのテクスチャとして出力し、単位ベクトルや不透明度の値をシェーダで使えるようにしたいので、カラーチャネルへデータエンコードします。

drawPoint(point){

	~ 略 ~

	//波紋の透明度
	let intensity = 1.0;
	if(point.age < this.maxAge * 0.3){
		intensity = easeOutSine(point.age / (this.maxAge * 0.3),0,1,1);
	}else{
		intensity = easeOutQuad(1-(point.age - this.maxAge * 0.3) / (this.maxAge * 0.7),0,1,1);
	}
	intensity *= point.force;

	//カラーチャネルへデータエンコード
	let red = ((point.vx + 1) / 2) * 255;
	let green = ((point.vy + 1) / 2) * 255;
	let blue = intensity * 255;
	let color = `${red},${green},${blue}`;

	let offset = this.width * 5;

	~ 略 ~

	//イージング
	function easeOutSine(t,b,c,d){
		return c * Math.sin((t/d) * (Math.PI / 2)) + b;
	}
	function easeOutQuad(t,b,c,d){
		t /= d;
		return -c * t * (t - 2) + b;
	}
}

● script.js

必要なライブラリを読み込みます。

<script src="js/preloadjs.min.js"></script>
<script src="js/TweenMax.min.js"></script>
<script src="js/script.js" type="module"></script>

完成したscript.jsになります。

//===============================================================
// Import Library
//===============================================================
import { TouchTexture } from './lib/touchtexture.js';

//===============================================================
// Init
//===============================================================
window.addEventListener('load',function(){
   init();
});

let touchTexture;

function init(){
	setLoading();
}

function setLoading(){
   TweenMax.to('.loader',0.1,{opacity:1});
	TweenMax.to('#loader_wrapper',1,{
        opacity:0,
        delay:1,
        onComplete: function(){
            document.getElementById('loader_wrapper').style.display = 'none';
            TweenMax.to('.loader',0,{opacity:0});
        }
    });
	threeWorld();
	rendering();
}

//===============================================================
// Create World
//===============================================================
function threeWorld(){
	touchTexture = new TouchTexture();

	window.addEventListener('mousemove',onMouesMove);
	function onMouesMove(event){
		const point = {
			x:event.clientX / window.innerWidth,
			y:event.clientY / window.innerHeight
		}
		touchTexture.addPoint(point);
	}

}

function rendering(){
	requestAnimationFrame(rendering);

	if(touchTexture){
		touchTexture.update();
	}
}

● touchtexture.js

//===============================================================
// TouchTexture
//===============================================================
export class TouchTexture{
	constructor(options){
		this.points = [];
		this.width = window.innerWidth;
		this.height = window.innerHeight;
		this.radius = this.width * 0.05;
		this.maxAge = 64;
		this.last = null;

		this.init();
	}

	init(){
		this.canvas = document.createElement('canvas');
		this.canvas.id = 'TouchTexture';
		this.canvas.width = this.width;
		this.canvas.height = this.height;
		this.ctx = this.canvas.getContext('2d');
		this.clear();

		const container = document.querySelector('#canvas_vr');
		container.append(this.canvas);
	}

	clear(){
		this.ctx.fillStyle = 'black';
		this.ctx.fillRect(0,0,this.canvas.width,this.canvas.height);
	}

	addPoint(point){
		let force = 0;
		let vx = 0;
		let vy = 0;
		const last = this.last;
		if(last){
			const relativeX = point.x - last.x;
			const relativeY = point.y - last.y;
			const distanceSquared = relativeX * relativeX + relativeY * relativeY;
			const distance = Math.sqrt(distanceSquared);
			vx = relativeX / distance;
			vy = relativeY / distance;

			force = Math.min(distanceSquared * 10000,1.0);
		}

		this.last = {
			x:point.x,
			y:point.y
		}

		this.points.push({x:point.x,y:point.y,age:0,force,vx,vy});
	}

	drawPoint(point){
		let pos = {
			x:point.x * this.width,
			y:point.y * this.height
		}
		const radius = this.radius;
		const ctx = this.ctx;

		let intensity = 1.0;
		if(point.age < this.maxAge * 0.3){
			intensity = easeOutSine(point.age / (this.maxAge * 0.3),0,1,1);
		}else{
			intensity = easeOutQuad(1-(point.age - this.maxAge * 0.3) / (this.maxAge * 0.7),0,1,1);
		}
		intensity *= point.force;

		let red = ((point.vx + 1) / 2) * 255;
		let green = ((point.vy + 1) / 2) * 255;
		let blue = intensity * 255;
		let color = `${red},${green},${blue}`;

		let offset = this.width * 5;

		ctx.shadowOffsetX = offset;
		ctx.shadowOffsetY = offset;
		ctx.shadowBlur = radius * 1;
		ctx.shadowColor = `rgba(${color},${0.2 * intensity})`;

		this.ctx.beginPath();
		this.ctx.fillStyle = 'rgba(255,0,0,1)';
		this.ctx.arc(pos.x - offset,pos.y - offset,radius,0,Math.PI * 2);
		this.ctx.fill();

		function easeOutSine(t,b,c,d){
			return c * Math.sin((t/d) * (Math.PI / 2)) + b;
		}

		function easeOutQuad(t,b,c,d){
			t /= d;
			return -c * t * (t - 2) + b;
		}
	}

	update(){
		this.clear();

		let agePart = 1.0 / this.maxAge;

		const _this = this;
		this.points.forEach(function(point,i){
			let slowAsOlder = (1.0 - point.age / _this.maxAge);
			let force = point.force * agePart * slowAsOlder;
			point.x += point.vx * force;
			point.y += point.vy * force;
			point.age += 1;
			if(point.age > _this.maxAge){
				_this.points.splice(i,1);
			}
		});
		this.points.forEach(function(point){
			_this.drawPoint(point);
		});
	}
}

完成したデモになります。マウスの動きに合わせて波紋を描くことができます。動作確認のために、単位ベクトルや不透明度の値は色として描画しました。

  • このエントリーをはてなブックマークに追加

関連記事

前の記事へ

Three.jsでペーパーアニメーション

次の記事へ

EffectComposerで揺らぎエフェクト(2)