Twitter
2020年12月19日 - Three.js・WebVR

Three.jsで螺旋アニメーション

Three.jsでいろいろなアニメーションを制作できるようになりたいと思い、螺旋アニメーションを試してみました。※Three.jsはr123を使用しています。

三角関数やベクトルなど、数学や物理を使用したアニメーションは「[ゲーム&モダンJavaScript文法で2倍楽しい]グラフィックスプログラミング入門」が参考になります。

三角関数とベクトル

● 三角関数

三角関数を使用すると、半径(r)と角度(θ)をもとに円周上の座標を求めることができます。X座標を求めるにはコサインを、Y座標を求めるにはサインを使用します。

角度は日常生活で使用する度数法ではなく、ラジアンを使用します。ラジアンは、円周の長さを基準とした角度の単位で、360度は円周率(π)×2で6.28です。度数法からラジアンに変換する式はよく使うので覚えておくと便利です。

// 度数法からラジアンに変換
const radian = degree * Math.PI / 180;

X座標・Y座標は、ラジアンとサイン・コサインを使用して求めます。

// X座標
const x = r * Math.cos(radian); 
// Y座標
const y = r * Math.sin(radian);

三角関数に関しては下記ページが参考になります。

● ベクトル

ベクトルは「向き」と「量」の2つの情報を持ち、始点と終点の2つの座標を矢印で結んで表現します。原点(0,0)を始点とするベクトルは(X,Y)と表記し、ベクトルは足したり引いたりすることができます。

ベクトルに関しては下記ページが参考になります。

Three.jsで螺旋アニメーション

● 球体の円形アニメーション

ラジアンの値を増やすことで、球体を円形にアニメーションさせます。角度の値を増やしてからラジアンに変換するとわかりやすいです。角度は大きくなりすぎないように360以上になったら0に戻します。

マテリアルは、動作確認のためライティングを必要としないMeshNormalMaterialを使用します。

let sphere;
let angle = 0;

//球体の生成
const geometry = new THREE.SphereGeometry(6,24,24);
const material = new THREE.MeshNormalMaterial();
sphere = new THREE.Mesh(geometry,material);
scene.add(sphere);

function rendering(){
	requestAnimationFrame(rendering);

	//角度の値を増やす
	angle++;
	if(angle >= 360){
		angle = 0;
	}

	//角度をラジアンに変換
	const radian = angle * Math.PI / 180;

	//座標を設定
	const r = 50;
	const x = r * Math.cos(radian);
	const y = 0;
	const z = r * Math.sin(radian);

	sphere.position.set(x,y,z);

	renderer.render(scene,camera);
}

● 球体の螺旋アニメーション

球体を螺旋アニメーションさせます。Three.jsは右手座標系でY軸が上方向なので、円形アニメーションしている球体のY軸に速度を設定します。

let sphere;
let angle = 0;

//速度の設定
let velocity = 0;

const geometry = new THREE.SphereGeometry(6,24,24);
const material = new THREE.MeshNormalMaterial();
sphere = new THREE.Mesh(geometry,material);
scene.add(sphere);

function rendering(){
	requestAnimationFrame(rendering);

	angle++;
	if(angle >= 360){
		angle = 0;
	}
	const radian = angle * Math.PI / 180;

	//速度に値を追加
	velocity += 0.1;

	//座標を設定
	const r = 50;
	const x = r * Math.cos(radian);

	//Y軸に速度を設定
	const y = velocity;
	const z = r * Math.sin(radian);

	sphere.position.set(x,y,z);

	renderer.render(scene,camera);
}

● 球体クラス

球体を複数生成する前に、管理しやすいように球体クラスを作成します。また、速度はベクトルとして設定します。

let sphere;

//球体クラスからインスタンス生成
sphere = new PhisicsSphere();
scene.add(sphere);

function rendering(){
	requestAnimationFrame(rendering);

	//球体をアニメーション
	sphere.update();

	renderer.render(scene,camera);
}

//THREE.Meshを継承したアニメーション用の球体クラス
class PhisicsSphere extends THREE.Mesh{
	constructor(){
		const geometry = new THREE.SphereGeometry(6,24,24);
		const material = new THREE.MeshNormalMaterial();
		super(geometry,material);

		//角度
		this.angle = 0;

		//半径
		this.radius = 50;

		//速度
		this.velocity = new THREE.Vector3();
	}

	setVelocity(angle){
		const radian = angle * Math.PI / 180;
		this.velocity.x = Math.cos(radian) * this.radius;
		this.velocity.y += 0.1;
		this.velocity.z = Math.sin(radian) * this.radius;
	}

	update(){
		this.angle++;

		if(this.angle >= 360){
			this.angle = 0;
		}

		this.setVelocity(this.angle);

		//座標に速度を設定
		this.position.copy(this.velocity);
	}
}

● 一定の間隔で球体を生成

タイムスタンプを使用して、一定の間隔で球体を生成します。

Date.nowは、1970年1月1日の午前0時からの経過時間をミリ秒(1/1000秒)で返してくれます。timerPastに代入した開始時のタイムスタンプとtimerNowを比較することで、どのくらい時間が経過したか調べ、一定の間隔で球体を生成します。

let sphere;
let timerPast,timerNow;
const sphereArr = [];

//開始時のタイムスタンプを取得
timerPast = Date.now();

function rendering(){
	requestAnimationFrame(rendering);

	//現在のタイムスタンプを取得
	timerNow = Date.now();

	//一定の間隔で球体を生成
	if(timerNow - timerPast > 500){
		sphere = new PhisicsSphere();
		scene.add(sphere);

		//配列に格納
		sphereArr.push(sphere);
		//タイムスタンプを代入
		timerPast = Date.now();
	}

	//配列に格納された球体をアニメーション
	sphereArr.map(function(sphere){
		sphere.update();
	});

	renderer.render(scene,camera);
}

● 球体の消滅

球体が生成され続けてしまうので、一定の高さを超えたら球体クラスに設定したライフを0にして、球体を消滅させます。

function rendering(){
	requestAnimationFrame(rendering);

	timerNow = Date.now();

	if(timerNow - timerPast > 500){
		sphere = new PhisicsSphere();
		scene.add(sphere);
		sphereArr.push(sphere);
		timerPast = Date.now();
	}

	sphereArr.map(function(sphere,index,array){
		if(sphere.life == 1){
			sphere.update();

			//一定の高さを超えたら
			if(sphere.position.y >= 110){

				//球体のライフを0に			
				sphere.life = 0;
				//球体をシーンから削除
				scene.remove(sphere);
			}
		}else{
			//球体のライフが0の場合、配列から削除
			array.splice(index,1);
			sphere = null;
		}
	});

	renderer.render(scene,camera);
}

class PhisicsSphere extends THREE.Mesh{
	constructor(){
		const geometry = new THREE.SphereGeometry(6,24,24);
		const material = new THREE.MeshNormalMaterial();
		super(geometry,material);

		//ライフを設定
		this.life = 1;
		this.angle = 0;
		this.radius = 50;
		this.velocity = new THREE.Vector3();
	}

	setVelocity(angle){
		const radian = angle * Math.PI / 180;
		this.velocity.x = Math.cos(radian) * this.radius;
		this.velocity.y += 0.1;
		this.velocity.z = Math.sin(radian) * this.radius;
	}

	update(){
		this.angle++;

		if(this.angle >= 360){
			this.angle = 0;
		}

		if(this.life == 1){
			this.setVelocity(this.angle);
			this.position.copy(this.velocity);
		}
	}
}

● 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';

//===============================================================
// Init
//===============================================================
window.addEventListener('load',function(){
   init();
});

let orbitControls;

let sphere;
let timerPast,timerNow;
const sphereArr = [];

function init(){
	setLoading();
}

function setLoading(){
	TweenMax.to('.loader',0.1,{opacity:1});
	TweenMax.to('#loader_wrapper',1,{
        opacity:0,
        delay:0,
        onComplete: function(){
            document.getElementById('loader_wrapper').style.display = 'none';
            TweenMax.to('.loader',0,{opacity:0});
        }
    });
	threeWorld();
	setLight();
	setControll();
	rendering();
}

//===============================================================
// Create World
//===============================================================
function threeWorld(){
	renderer.outputEncoding = THREE.sRGBEncoding;

	const axesHelper = new THREE.AxesHelper(500);
	const gridHelper = new THREE.GridHelper(1000,50);
	//scene.add(axesHelper);
	//scene.add(gridHelper);

	timerPast = Date.now();
}

function setLight(){
	const ambientlight = new THREE.AmbientLight(0x333333,0.2);
	scene.add(ambientlight);

	const pointLight = new THREE.PointLight(0xFFFFFF,15,110,1.0);
	scene.add(pointLight);

	//const pointLightHelper = new THREE.PointLightHelper(pointLight,10);
	//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);

	timerNow = Date.now();

	if(timerNow - timerPast > 2100){
		sphere = new PhisicsSphere();
		scene.add(sphere);
		sphereArr.push(sphere);
		timerPast = Date.now();
	}

	sphereArr.map(function(sphere,index,array){
		if(sphere.life == 1){
			sphere.update();

			if(sphere.position.y >= 110){
				TweenMax.to(sphere.scale,0.4,{
					x:0,y:0,z:0,
					onComplete:function(){
						sphere.life = 0;
						scene.remove(sphere);
				    }
				});
			}
		}else{
			array.splice(index,1);
			sphere = null;
		}
	});

	if(orbitControls){
		orbitControls.update();
	}

	renderer.render(scene,camera);
}

class PhisicsSphere extends THREE.Mesh{
	constructor(){
		const geometry = new THREE.SphereGeometry(6,24,24);
		const material = new THREE.MeshPhysicalMaterial({
			wireframe:true,
			color:0xCCCCCC,
			roughness:0.5,
			metalness:0.5
		});
		super(geometry,material);

		this.life = 1;
		this.angle = 0;
		this.radius = 70;
		this.velocity = new THREE.Vector3(0,-130,0);

		this.init();
	}

	init(){
		const scale = 1.6;
		TweenMax.to(this.scale,0,{
			x:0,y:0,z:0,
		});
		TweenMax.to(this.scale,0.7,{
			x:scale,
			y:scale,
			z:scale,
		});
	}

	setVelocity(angle){
		const radian = angle * (Math.PI / 180 * 2) * 0.1;
		this.velocity.x = Math.cos(radian) * this.radius;
		this.velocity.y += 0.03 + (Math.cos(radian) * 0.01);
		this.velocity.z = Math.sin(radian) * this.radius;
	}

	update(){
		this.angle++;

		if(this.angle >= 1800){
			this.angle = 0;
		}

		if(this.life == 1){
			this.setVelocity(this.angle);
			this.position.copy(this.velocity);
		}

		this.rotation.x += 0.005;
		this.rotation.y += 0.003;
	}
}

完成したデモになります。球体の生成と消滅時は、TweenMaxを使用してアニメーションさせています。また、ライトとマテリアルを調整しました。

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

関連記事

前の記事へ

Three.jsで球体アニメーション

次の記事へ

Three.jsのPointsで頂点アニメーション