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

