EffectComposerで揺らぎエフェクト(2)
「EffectComposerで揺らぎエフェクト(1)」に続き「Creating a Water-like Distortion Effect with Three.js」を参考に、EffectComposerのオリジナルエフェクトで揺らぎエフェクトを制作しました。※Three.jsはr129を使用しています。
EffectComposerで揺らぎエフェクト
「EffectComposerで揺らぎエフェクト(1)」で、マウスの動きに合わせてcanvasに波紋を描きましたが、canvasをテクスチャに設定して、EffectComposerのオリジナルエフェクトで、水の揺らぎのようなエフェクトを制作します。
● 平面を生成
PlaneGeometryでエフェクトをかける平面を生成して、「EffectComposerでポストプロセッシング」でやったようにEffectComposerを設定します。
//テクスチャ画像の読み込み const texture = new THREE.TextureLoader().load( './img/pict.jpg' ); //平面の生成 const geometry = new THREE.PlaneGeometry(5,5,1,1); const material = new THREE.MeshPhysicalMaterial({ map:texture, roughness:0.5, side:THREE.DoubleSide, }); const plane = new THREE.Mesh(geometry,material); scene.add(plane); //EffectComposerを設定 const composer = new EffectComposer(renderer); const renderPass = new RenderPass(scene,camera); composer.addPass(renderPass); composer.addPass(waterPass); //ポイントライト const pointLight = new THREE.PointLight(0XFFFFFF,2.5,4,1); pointLight.position.set(0,0,2.5); scene.add(pointLight); //ヘルパー const pointLightHelper = new THREE.PointLightHelper(pointLight,0.5); scene.add(pointLightHelper); //アニメーション function rendering(){ requestAnimationFrame(rendering); //コンポーザーでレンダリング composer.render(); }
● TouchTextureの修正
TouchTextureを修正してcanvasをテクスチャに設定します。
import * as THREE from './three_jsm/three.module.js'; export class TouchTexture{ constructor(){ this.points = []; //サイズの調整 this.size = 64; this.width = this.height = this.size; this.radius = this.size * 0.1; 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(); //canvasをテクスチャに設定 this.texture = new THREE.Texture(this.canvas); } ~ 略 ~ update(){ ~ 略 ~ //テクスチャの更新 this.texture.needsUpdate = true; } }
● EffectComposerのオリジナルエフェクトを作成
EffectComposerでオリジナルエフェクトを制作するさいは、ShaderPassを使用します。ShaderPassには、CopyShaderのようなシェーダのオブジェクトやShaderMaterialを渡すことができ、今回はShaderMaterialを使用します。ShaderPass.jsは「examples > jsm > postprocessing」の中に、CopyShader.jsは「examples > jsm > shaders」の中にあります。
ShaderMaterialに関しては、「Three.jsでシェーダ(GLSL)入門」を参考にしてください。
//バーテックスシェーダ const vertexShader =` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `; //フラグメントシェーダ const fragmentShader = ` uniform float opacity; //前のPassの結果をテクスチャとして取得 uniform sampler2D tDiffuse; //canvasテクスチャを取得 uniform sampler2D wTexture; varying vec2 vUv; const float PI = 3.14159265359; void main(){ //TouchTextureのカラーチャネルから波紋の情報を取得 vec4 wTexel = texture2D(wTexture,vUv); float vx = -(wTexel.r * 2.0 - 1.0); float vy = -(wTexel.g * 2.0 - 1.0); float intensity = wTexel.b; //波紋をUV座標に変換 vec2 wUv = vUv; wUv.x += vx * 0.1 * intensity; wUv.y += vy * 0.1 * intensity; vec4 texel = texture2D(tDiffuse,wUv); gl_FragColor = opacity * texel; //動作確認用にTouchTextureのカラーチャネルをそのまま描画 //gl_FragColor = vec4(wTexel.r,wTexel.g,wTexel.b,1.0); } `; const uniforms = { tDiffuse: { value: null }, opacity: { value: 1.0 }, wTexture:{ value: null } } //シェーダマテリアル const shaderMaterial = new THREE.ShaderMaterial({ vertexShader:vertexShader, fragmentShader:fragmentShader, uniforms:uniforms }); shaderMaterial.uniforms.wTexture.value = touchTexture.texture;
ShaderPassを使用して、揺らぎエフェクトをEffectComposerのコンポーザーに追加します。
composer = new EffectComposer(renderer); const renderPass = new RenderPass(scene,camera); //揺らぎエフェクト const waterPass = new ShaderPass(shaderMaterial); composer.addPass(renderPass); //コンポーザーに追加 composer.addPass(waterPass);
● 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 * as THREE from './lib/three_jsm/three.module.js'; import { OrbitControls } from './lib/three_jsm/OrbitControls.js'; import { scene, camera, container, renderer } from './lib/basescene.js'; import { TouchTexture } from './lib/touchtexture.js'; import { EffectComposer } from './lib/three_jsm/postprocessing/EffectComposer.js'; import { RenderPass } from './lib/three_jsm/postprocessing/RenderPass.js'; import { ShaderPass } from './lib/three_jsm/postprocessing/ShaderPass.js'; //=============================================================== // Init //=============================================================== window.addEventListener('load',function(){ init(); }); let orbitControls; let texture; let touchTexture; let composer; function init(){ setLoading(); } function setLoading(){ TweenMax.to('.loader',0.1,{opacity:1}); const manifest = [ {id:'pict',src:'./img/pict.jpg'} ]; const loadQueue = new createjs.LoadQueue(); loadQueue.on('progress',function(e){ const progress = e.progress; }); loadQueue.on('complete',function(){ const image = loadQueue.getResult('pict'); texture = new THREE.Texture(image); texture.needsUpdate = true; TweenMax.to('#loader_wrapper',1,{ opacity:0, onComplete:function(){ document.getElementById('loader_wrapper').style.display ='none'; TweenMax.to('.loader',0,{opacity:0}); } }); threeWorld(); setLight(); setControll(); rendering(); }); loadQueue.loadManifest(manifest); } //=============================================================== // WaterShader //=============================================================== const vertexShader =` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `; const fragmentShader = ` uniform float opacity; uniform sampler2D tDiffuse; uniform sampler2D wTexture; varying vec2 vUv; const float PI = 3.14159265359; void main(){ vec4 wTexel = texture2D(wTexture,vUv); float vx = -(wTexel.r * 2.0 - 1.0); float vy = -(wTexel.g * 2.0 - 1.0); float intensity = wTexel.b; vec2 wUv = vUv; wUv.x += vx * 0.1 * intensity; wUv.y += vy * 0.1 * intensity; vec4 texel = texture2D(tDiffuse,wUv); gl_FragColor = opacity * texel; //gl_FragColor = vec4(wTexel.r,wTexel.g,wTexel.b,1.0); } `; const uniforms = { tDiffuse: { value: null }, opacity: { value: 1.0 }, wTexture:{ value: null } } //=============================================================== // Create World //=============================================================== function threeWorld(){ renderer.outputEncoding = THREE.sRGBEncoding; const geometry = new THREE.PlaneGeometry(5,5,1,1); const material = new THREE.MeshPhysicalMaterial({ map:texture, roughness:0.5, side:THREE.DoubleSide, }); const plane = new THREE.Mesh(geometry,material); scene.add(plane); touchTexture = new TouchTexture(); const shaderMaterial = new THREE.ShaderMaterial({ vertexShader:vertexShader, fragmentShader:fragmentShader, uniforms:uniforms }); shaderMaterial.uniforms.wTexture.value = touchTexture.texture; composer = new EffectComposer(renderer); const renderPass = new RenderPass(scene,camera); const waterPass = new ShaderPass(shaderMaterial); composer.addPass(renderPass); composer.addPass(waterPass); } function setLight(){ const ambientlight = new THREE.AmbientLight(0xFFFFFF,0.1); scene.add(ambientlight); const pointLight = new THREE.PointLight(0XFFFFFF,2.5,4,1); pointLight.position.set(0,0,2.5); scene.add(pointLight); const pointLightHelper = new THREE.PointLightHelper(pointLight,0.5); scene.add(pointLightHelper); } function setControll(){ document.addEventListener('touchmove',function(e){e.preventDefault();},{passive:false}); orbitControls = new OrbitControls(camera,renderer.domElement); orbitControls.enableDamping = true; orbitControls.dampingFactor = 0.5; 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(); } if(orbitControls){ orbitControls.update(); } composer.render(); }
● touchtexture.js
//=============================================================== // Import Library //=============================================================== import * as THREE from './three_jsm/three.module.js'; //=============================================================== // WaterTexture //=============================================================== export class TouchTexture{ constructor(){ this.points = []; this.size = 64; this.width = this.height = this.size; this.radius = this.size * 0.1; 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(); this.texture = new THREE.Texture(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,i){ _this.drawPoint(point); }); this.texture.needsUpdate = true; } }
● basescene.js
sceneやcameraなど基本的な設定を管理するbasescene.jsです。
//=============================================================== // Import Library //=============================================================== import * as THREE from './three_jsm/three.module.js'; //=============================================================== // Base scene //=============================================================== let scene,camera,container,renderer; init(); function init(){ scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera(50,window.innerWidth/window.innerHeight,0.1,100); camera.position.set(0,0,10); scene.add(camera); renderer = new THREE.WebGLRenderer({antialias:true}); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth,window.innerHeight); renderer.setClearColor(new THREE.Color(0x000000)); container = document.querySelector('#canvas_vr'); container.appendChild(renderer.domElement); window.addEventListener('resize',function(){ camera.aspect = window.innerWidth/window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth,window.innerHeight); },false); } export { scene, camera, container, renderer }
完成したデモになります。EffectComposerのオリジナルエフェクトで、水の揺らぎのようなエフェクトを制作しました。