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

	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でペーパーアニメーションを試してみました。

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

関連記事

前の記事へ

平面から球体へモーフィングする頂点アニメーション

次の記事へ

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