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