2021年06月05日 - WebVR・Three.js
Three.jsでペーパーアニメーション

「How to Unroll Images with Three.js」を参考に、Three.jsで紙を広げるようなペーパーアニメーションを試しました。※Three.jsはr129を使用しています。
Three.jsでペーパーアニメーション
● 平面を生成
PlaneGeometryで、ワイヤーフレーム表示にした平面を生成します。マテリアルは、「平面にシェーダを設定」でやったようにRawShaderMaterialを設定します。
また、uniformsにはアニメーションで使用するprogressとangleを設定します。
const uniforms = {
//進行状況
progress:{type:'f',value:0.0},
//紙を広げる角度
angle:{type:'f',value:0.0},
};
const geometry = new THREE.PlaneGeometry(1,1,80,80);
const material = new THREE.RawShaderMaterial({
vertexShader:vertexShader,
fragmentShader:fragmentShader,
uniforms:uniforms,
wireframe:true,
side:THREE.DoubleSide,
});
const plane = new THREE.Mesh(geometry,material);
scene.add(plane);
● 紙を広げるようなアニメーション
「How to Unroll Images with Three.js」を参考に、バーテックスシェーダで頂点の位置座標を計算します。
三角関数を使用して、アニメーション後の位置座標を計算し、進行状況の数値を反映することより紙を広げるようなアニメーションを実装します。
● glsl.js
//バーテックスシェーダ
const vertexShader =`
precision highp float;
attribute vec3 position;
attribute vec2 uv;
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
//紙を広げる角度
uniform float angle;
//進行状況
uniform float progress;
//行列による回転
mat4 rotationMatrix(vec3 axis, float angle){
axis = normalize(axis);
float s = sin(angle);
float c = cos(angle);
float oc = 1.0 - c;
return mat4(oc * axis.x * axis.x + c, oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s, 0.0,
oc * axis.x * axis.y + axis.z * s, oc * axis.y * axis.y + c, oc * axis.y * axis.z - axis.x * s, 0.0,
oc * axis.z * axis.x - axis.y * s, oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c, 0.0,
0.0,0.0,0.0,1.0);
}
//回転
vec3 rotate(vec3 v, vec3 axis, float angle){
mat4 m = rotationMatrix(axis, angle);
return (m * vec4(v,1.0)).xyz;
}
void main(void){
float pi = 3.14159265359;
//最終的な角度を計算
float finalAngle = angle - 0.0 * 0.3 * sin(progress * 6.0);
vec3 newPosition = position;
float rad = 0.1;
float rolls = 8.0;
//アニメーション後の位置座標を計算
newPosition = rotate(newPosition - vec3(-0.5,0.5,0.0),vec3(0.0,0.0,1.0),-finalAngle) + vec3(-0.5,0.5,0.0);
float offs = (newPosition.x + 0.5) / (sin(finalAngle) + cos(finalAngle));
float tProgress = clamp((progress - offs * 0.99) / 0.01,0.0,1.0);
newPosition.z = rad + rad * (1.0 - offs/2.0) * sin(-offs * rolls * pi - 0.5 * pi);
newPosition.x = -0.5 + rad * (1.0 - offs/2.0) * cos(-offs * rolls * pi + 0.5 * pi);
newPosition = rotate(newPosition - vec3(-0.5,0.5,0.0),vec3(0.0,0.0,1.0),finalAngle) + vec3(-0.5,0.5,0.0);
newPosition = rotate(newPosition - vec3(-0.5,0.5,rad),vec3(sin(finalAngle),cos(finalAngle),0.0),-pi*progress*rolls);
newPosition += vec3(
-0.5 + progress * cos(finalAngle) * (sin(finalAngle) + cos(finalAngle)),
0.5 - progress * sin(finalAngle) * (sin(finalAngle) + cos(finalAngle)),
rad * (1.0 - progress/2.0)
);
//進行状況を反映
vec3 finalPosition = mix(newPosition,position,tProgress);
gl_Position = projectionMatrix * modelViewMatrix * vec4(finalPosition,1.0);
}
`;
//フラグメントシェーダ
const fragmentShader =`
precision highp float;
void main(void){
gl_FragColor = vec4(1.0,1.0,1.0,1.0);
}
`;
export { vertexShader, fragmentShader };
進行状況の数値を増やすことで、アニメーションさせます。
function rendering(){
requestAnimationFrame(rendering);
time++;
plane.material.uniforms.progress.value = time * 0.008;
renderer.render(scene,camera);
}
● テクスチャ画像
白い紙だと面白くないので、テクスチャ用にPIXTAで地図画像を購入しました。
● テクスチャとライトを設定
RawShaderMaterialにテクスチャとライトを設定しました。詳細は、「PlaneGeometryをシェーダで波形アニメーション」と「RawShaderMaterialにライトを設定」を参考にしてください。
● 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 { vertexShader, fragmentShader } from './glsl.js';
//===============================================================
// Init
//===============================================================
window.addEventListener('load',function(){
init();
});
let orbitControls;
let texture;
let plane;
let time = 0;
function init(){
setLoading();
}
function setLoading(){
TweenMax.to('.loader',0.1,{opacity:1});
const manifest = [
{id:'map',src:'./img/map.jpg'}
];
const loadQueue = new createjs.LoadQueue();
loadQueue.on('progress',function(e){
const progress = e.progress;
});
loadQueue.on('complete',function(){
const image = loadQueue.getResult('map');
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);
}
//===============================================================
// Create World
//===============================================================
function threeWorld(){
renderer.outputEncoding = THREE.sRGBEncoding;
const uniforms = {
progress:{type:'f',value:0.0},
angle:{type:'f',value:0.35},
texture:{type:'t',value:null},
diffuse:{type:'c',value:new THREE.Color(0xFFFFFF)},
emissive:{type:'c',value:new THREE.Color(0x000000)}
};
const geometry = new THREE.PlaneGeometry(2,1,160,80);
const material = new THREE.RawShaderMaterial({
vertexShader:vertexShader,
fragmentShader:fragmentShader,
uniforms:THREE.UniformsUtils.merge([
THREE.UniformsLib.lights,
uniforms,
]),
lights:true,
side:THREE.DoubleSide,
});
material.uniforms.texture.value = texture;
plane = new THREE.Mesh(geometry,material);
plane.rotation.x = -Math.PI / 2;
scene.add(plane);
}
function setLight(){
const ambientlight = new THREE.AmbientLight(0xFFFFFF,0.1);
scene.add(ambientlight);
const pointLight = new THREE.PointLight(0XFFFFFF,10.0,2.15,1.0);
pointLight.position.set(0,2,0);
scene.add(pointLight);
//const pointLightHelper = new THREE.PointLightHelper(pointLight,0.1);
//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;
}
function rendering(){
requestAnimationFrame(rendering);
if(orbitControls){
orbitControls.update();
}
time++;
if(plane.material.uniforms.progress.value <= 1.5){
plane.material.uniforms.progress.value = time * 0.008;
}
renderer.render(scene,camera);
}
● glsl.js
const vertexShader =`
precision highp float;
attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
uniform mat3 normalMatrix;
uniform float angle;
uniform float progress;
varying vec2 vUv;
varying vec3 vViewPosition;
varying vec3 vNormal;
mat4 rotationMatrix(vec3 axis, float angle){
axis = normalize(axis);
float s = sin(angle);
float c = cos(angle);
float oc = 1.0 - c;
return mat4(oc * axis.x * axis.x + c, oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s, 0.0,
oc * axis.x * axis.y + axis.z * s, oc * axis.y * axis.y + c, oc * axis.y * axis.z - axis.x * s, 0.0,
oc * axis.z * axis.x - axis.y * s, oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c, 0.0,
0.0,0.0,0.0,1.0);
}
vec3 rotate(vec3 v, vec3 axis, float angle){
mat4 m = rotationMatrix(axis, angle);
return (m * vec4(v,1.0)).xyz;
}
void main(void){
vUv = uv;
vNormal = normalMatrix * normal;
float pi = 3.14159265359;
float finalAngle = angle - 0.0 * 0.3 * sin(progress * 6.0);
vec3 newPosition = position;
float rad = 0.1;
float rolls = 8.0;
newPosition = rotate(newPosition - vec3(-0.5,0.5,0.0),vec3(0.0,0.0,1.0),-finalAngle) + vec3(-0.5,0.5,0.0);
float offs = (newPosition.x + 0.5) / (sin(finalAngle) + cos(finalAngle));
float tProgress = clamp((progress - offs * 0.99) / 0.01,0.0,1.0);
newPosition.z = rad + rad * (1.0 - offs/2.0) * sin(-offs * rolls * pi - 0.5 * pi);
newPosition.x = -0.5 + rad * (1.0 - offs/2.0) * cos(-offs * rolls * pi + 0.5 * pi);
newPosition = rotate(newPosition - vec3(-0.5,0.5,0.0),vec3(0.0,0.0,1.0),finalAngle) + vec3(-0.5,0.5,0.0);
newPosition = rotate(newPosition - vec3(-0.5,0.5,rad),vec3(sin(finalAngle),cos(finalAngle),0.0),-pi*progress*rolls);
newPosition += vec3(
-0.5 + progress * cos(finalAngle) * (sin(finalAngle) + cos(finalAngle)),
0.5 - progress * sin(finalAngle) * (sin(finalAngle) + cos(finalAngle)),
rad * (1.0 - progress/2.0)
);
vec3 finalPosition = mix(newPosition,position,tProgress);
vec4 mvPosition = modelViewMatrix * vec4(finalPosition, 1.0);
vViewPosition = mvPosition.xyz;
gl_Position = projectionMatrix * mvPosition;
}
`;
const fragmentShader =`
precision highp float;
uniform vec3 diffuse;
uniform vec3 emissive;
uniform sampler2D texture;
uniform mat4 viewMatrix;
varying vec2 vUv;
varying vec3 vViewPosition;
varying vec3 vNormal;
#include <common>
#include <bsdfs>
#include <lights_pars_begin>
void main(void){
vec3 mvPosition = vViewPosition;
vec3 transformedNormal = vNormal;
GeometricContext geometry;
geometry.position = mvPosition.xyz;
geometry.normal = normalize(transformedNormal);
geometry.viewDir = (normalize(-mvPosition.xyz));
vec3 lightFront = vec3(0.0);
vec3 indirectFront = vec3(0.0);
IncidentLight directLight;
float dotNL;
vec3 directLightColor_Diffuse;
#if NUM_POINT_LIGHTS > 0
#pragma unroll_loop_start
for (int i = 0; i < NUM_POINT_LIGHTS; i++) {
getPointDirectLightIrradiance(pointLights[ i ], geometry, directLight);
dotNL = dot(geometry.normal, directLight.direction);
directLightColor_Diffuse = PI * directLight.color;
lightFront += saturate(dotNL) * directLightColor_Diffuse;
}
#pragma unroll_loop_end
#endif
vec4 diffuseColor = vec4(diffuse, 1.0);
ReflectedLight reflectedLight = ReflectedLight(vec3(0.0),vec3(0.0),vec3(0.0),vec3(0.0));
vec3 totalEmissiveRadiance = emissive;
reflectedLight.indirectDiffuse = getAmbientLightIrradiance(ambientLightColor);
reflectedLight.indirectDiffuse += indirectFront;
reflectedLight.indirectDiffuse *= BRDF_Diffuse_Lambert(diffuseColor.rgb);
reflectedLight.directDiffuse = lightFront;
reflectedLight.directDiffuse *= BRDF_Diffuse_Lambert(diffuseColor.rgb);
vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;
vec3 color = texture2D(texture,vUv).rgb;
gl_FragColor = vec4(color * outgoingLight,diffuseColor.a);
}
`;
export { vertexShader, fragmentShader };
● 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,2,1.25);
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 }
完成したデモになります。Three.jsでペーパーアニメーションを試しました。

