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

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);

	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のオリジナルエフェクトで、水の揺らぎのようなエフェクトを制作しました。

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

関連記事

前の記事へ

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

次の記事へ

シェーダで雲を制作