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

