2020年10月22日

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

mover operated by vector2D」を参考に、Three.jsで球体アニメーションを試してみました。※Three.jsはr120を使用しています。

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

● BoxHelper

立方体のフレームを表示します。球体のアニメーションが綺麗に見えるように、各面の対角線が表示されないBoxHelperを使用します。

//立方体の辺の長さを設定
const boxWidth = 1000;

const boxGeometry = new THREE.BoxGeometry(boxWidth,boxWidth,boxWidth);
const boxMaterial = new THREE.MeshBasicMaterial();
const box = new THREE.Mesh(boxGeometry,boxMaterial);

//BoxHelperを生成
const frameBox = new THREE.BoxHelper(box,0xFFFFFF);
scene.add(frameBox);

● 球体を生成

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

//球体の数を設定
const sphereNum = 40;

//球体用の配列を生成
const sphereArr = [];

for(let i = 0; i < sphereNum; i++){
	//球体の生成
	const sphere = new PhisicsSphere();

	//球体の座標を設定
	const x = Math.random() * boxWidth/2 - boxWidth/4;
	const y = Math.random() * boxWidth/2 - boxWidth/4;
	const z = Math.random() * boxWidth/2 - boxWidth/4;
	sphere.position.set(x,y,z);

	scene.add(sphere);
	sphereArr[i] = sphere;
}

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

		//半径
		this.radius = radius;

		//質量
		this.mass = radius / 10;

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

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

● 加速度の設定

球体の速度に加速度を加算し、速度を座標に反映することでアニメーションさせます。

Three.jsの速度、加速度はVector3(ベクトル)で設定し、ベクトルはaddで加算します。

for(let i = 0; i < sphereNum; i++){
	const sphere = new PhisicsSphere();

	//加速度の設定
	const radian = Math.random() * 360 * Math.PI / 180;
	const phi = Math.random() * 360 * Math.PI / 180;
	const scalar = Math.random() * 3;
	const fx = Math.cos(radian) * Math.cos(phi) * scalar;
	const fy = Math.cos(radian) * Math.sin(phi) * scalar;
	const fz = Math.sin(radian) * scalar;
	const fource = new THREE.Vector3(fx,fy,fz);
	sphere.applyFouce(fource);

	const x = Math.random() * boxWidth/2 - boxWidth/4;
	const y = Math.random() * boxWidth/2 - boxWidth/4;
	const z = Math.random() * boxWidth/2 - boxWidth/4;

	//velocityに変更
	sphere.velocity.set(x,y,z);

	scene.add(sphere);
	sphereArr[i] = sphere;
}

function rendering(){
	requestAnimationFrame(rendering);

	for(let i = 0; i < sphereArr.length; i++){
		//球体の取得
		const sphere = sphereArr[i];

		//アニメーション
		sphere.move();
	}

	renderer.render(scene,camera);
}

class PhisicsSphere extends THREE.Mesh{
	constructor(){
		const radius = Math.random() * 25 + 15;
		const geometry = new THREE.SphereGeometry(radius,radius,radius);
		const material = new THREE.MeshNormalMaterial();
		super(geometry,material);

		this.radius = radius;
		this.mass = radius / 10;
		this.velocity = new THREE.Vector3();
		this.acceleration = new THREE.Vector3();
	}

	move(){
		//速度に加速度を加算
		this.velocity.add(this.acceleration);

		//速度を座標に反映
		this.position.copy(this.velocity)
	}

	applyFouce(vector3){
		//加速度の設定
		this.acceleration.add(vector3);
	}
}

● 壁の跳ね返りの設定

壁の跳ね返りを設定します。球体が壁の外に出たら衝突判定をして、跳ね返りの角度を計算し、加速度に反映して跳ね返らせます。

//跳ね返り用のベクトル
const normal = new THREE.Vector3();

//衝突用のフラグ
let collision = false;

function rendering(){
	requestAnimationFrame(rendering);

	for(let i = 0; i < sphereArr.length; i++){
		const sphere = sphereArr[i];
		sphere.move();

		//壁の跳ね返りの設定
		if(sphere.position.x >= boxWidth/2 - (sphere.radius/2)){

			//跳ね返り用のベクトルを設定
			normal.set(-1,0,0);

			//球体の座標を設定
			sphere.velocity.x = boxWidth/2 - (sphere.radius/2);

			//衝突判定を設定
			collision = true;

		}else if(sphere.position.x < -boxWidth/2 + (sphere.radius/2)){
			normal.set(1,0,0);
			sphere.velocity.x = -boxWidth/2 + (sphere.radius/2);
			collision = true;
		}
		if(sphere.position.y >= boxWidth/2 - (sphere.radius/2)){
			normal.set(0,-1,0);
			sphere.velocity.y = boxWidth/2 - (sphere.radius/2);
			collision = true;
		}else if(sphere.position.y < -boxWidth/2 + (sphere.radius/2)){
			normal.set(0,1,0);
			sphere.velocity.y = -boxWidth/2 + (sphere.radius/2);
			collision = true;
		}
		if(sphere.position.z >= boxWidth/2 - (sphere.radius/2)){
			normal.set(0,0,-1);
			sphere.velocity.z = boxWidth/2 - (sphere.radius/2);
			collision = true;
		}else if(sphere.position.z < -boxWidth/2 + (sphere.radius/2)){
			normal.set(0,0,1);
			sphere.velocity.z = -boxWidth/2 + (sphere.radius/2);
			collision = true;
		}

		if(collision){
			//衝突判定がTrueだったら、跳ね返り処理
			sphere.reflect(normal);
			collision = false;
		}
	}

	renderer.render(scene,camera);
}

class PhisicsSphere extends THREE.Mesh{

	constructor(){
		const radius = Math.random() * 25 + 15;
		const geometry = new THREE.SphereGeometry(radius,radius,radius);
		const material = new THREE.MeshNormalMaterial();
		super(geometry,material);

		this.radius = radius;
		this.mass = radius / 10;
		this.velocity = new THREE.Vector3();
		this.acceleration = new THREE.Vector3();
	}

	move(){
		this.velocity.add(this.acceleration);
		this.position.copy(this.velocity)
	}

	applyFouce(vector3){
		this.acceleration.add(vector3);
	}

	reflect(vector3){

		//跳ね返り処理
		this.acceleration.reflect(vector3);
	}
}

● 摩擦の設定

摩擦を設定します。摩擦はスカラーを調整した加速度の逆方向のベクトルを、加速度に加算して設定します。

for(let i = 0; i < sphereNum; i++){
	const sphere = new PhisicsSphere();

	//加速度の設定
	sphere.init();

	const x = Math.random() * boxWidth/2 - boxWidth/4;
	const y = Math.random() * boxWidth/2 - boxWidth/4;
	const z = Math.random() * boxWidth/2 - boxWidth/4;
	sphere.velocity.set(x,y,z);

	scene.add(sphere);
	sphereArr[i] = sphere;
}

function rendering(){
	requestAnimationFrame(rendering);

	for(let i = 0; i < sphereArr.length; i++){
		const sphere = sphereArr[i];
		sphere.move();

		if(sphere.acceleration.length() <= 1){
			//加速度の再設定
			sphere.init();
		}

		if(sphere.position.x >= boxWidth/2 - (sphere.radius/2)){
			normal.set(-1,0,0);
			sphere.velocity.x = boxWidth/2 - (sphere.radius/2);
			collision = true;
		}else if(sphere.position.x < -boxWidth/2 + (sphere.radius/2)){
			normal.set(1,0,0);
			sphere.velocity.x = -boxWidth/2 + (sphere.radius/2);
			collision = true;
		}
		if(sphere.position.y >= boxWidth/2 - (sphere.radius/2)){
			normal.set(0,-1,0);
			sphere.velocity.y = boxWidth/2 - (sphere.radius/2);
			collision = true;
		}else if(sphere.position.y < -boxWidth/2 + (sphere.radius/2)){
			normal.set(0,1,0);
			sphere.velocity.y = -boxWidth/2 + (sphere.radius/2);
			collision = true;
		}
		if(sphere.position.z >= boxWidth/2 - (sphere.radius/2)){
			normal.set(0,0,-1);
			sphere.velocity.z = boxWidth/2 - (sphere.radius/2);
			collision = true;
		}else if(sphere.position.z < -boxWidth/2 + (sphere.radius/2)){
			normal.set(0,0,1);
			sphere.velocity.z = -boxWidth/2 + (sphere.radius/2);
			collision = true;
		}

		if(collision){
			sphere.reflect(normal);
			collision = false;
		}
	}

	renderer.render(scene,camera);
}

class PhisicsSphere extends THREE.Mesh{
	constructor(){
		const radius = Math.random() * 25 + 15;
		const geometry = new THREE.SphereGeometry(radius,radius,radius);
		const material = new THREE.MeshNormalMaterial();
		super(geometry,material);

		this.radius = radius;
		this.mass = radius / 10;
		this.velocity = new THREE.Vector3();
		this.acceleration = new THREE.Vector3();
	}

	init(){
		//加速度の設定
		const radian = Math.random() * 360 * Math.PI / 180;
		const phi = Math.random() * 360 * Math.PI / 180;

		//摩擦を設定した分少し強めに調整
		const scalar = Math.random() * 5 + 15;

		const fx = Math.cos(radian) * Math.cos(phi) * scalar;
		const fy = Math.cos(radian) * Math.sin(phi) * scalar;
		const fz = Math.sin(radian) * scalar;
		const fource = new THREE.Vector3(fx,fy,fz);

		//質量に応じて加速度を調整
		fource.divideScalar(this.mass);

		this.applyFouce(fource);
	}

	move(){
		this.applyFriction();
		this.velocity.add(this.acceleration);
		this.position.copy(this.velocity)
	}

	applyFouce(vector3){
		this.acceleration.add(vector3);
	}

	applyFriction(){
		//摩擦の設定
		const friction = this.acceleration.clone();
		friction.multiplyScalar(-1);
		friction.normalize();
		friction.multiplyScalar(0.1);
		this.applyFouce(friction);
	}

	reflect(vector3){
		this.acceleration.reflect(vector3);
	}
}

● 球体同士の跳ね返りの設定

最後に球体同士の跳ね返りを設定します。球体同士の距離を計算し、球体同士が近くなったら跳ね返りの角度を計算し、加速度に反映して跳ね返らせます。

if(collision){
	sphere.reflect(normal);
	collision = false;
}

//球体同士の跳ね返りの設定
for(let index = i; index < sphereArr.length; index ++){
	const distance = sphere.velocity.distanceTo(sphereArr[index].velocity);
	const rebound_distance = sphere.radius + sphereArr[index].radius;
	if(distance <= rebound_distance){
		const overlap = Math.abs(distance - rebound_distance);
		const normal = sphere.velocity.clone().sub(sphereArr[index].velocity).normalize();
		sphere.velocity.sub(normal.clone().multiplyScalar(overlap * -1));
		sphereArr[index].velocity.sub(normal.clone().multiplyScalar(overlap));
		sphere.reflect(normal.clone().multiplyScalar(-1));
		sphereArr[index].reflect(normal.clone());
	}
}

● 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;
const sphereNum = 40;
const sphereArr = [];
const boxWidth = 1000;
const normal = new THREE.Vector3();
let collision = false;

function init(){
	setLoading();
}

function setLoading(){
	TweenMax.to('.loader',0.1,{opacity:1});
	TweenMax.to('#loader_wrapper',1,{
        opacity:0,
        delay:1,
        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 boxGeometry = new THREE.BoxGeometry(boxWidth,boxWidth,boxWidth);
	const boxMaterial = new THREE.MeshBasicMaterial();
	const box = new THREE.Mesh(boxGeometry,boxMaterial);
	const frameBox = new THREE.BoxHelper(box,0xFFFFFF);
	scene.add(frameBox);

	for(let i = 0; i < sphereNum; i++){
		const sphere = new PhisicsSphere();
		sphere.init();

		const x = Math.random() * boxWidth/2 - boxWidth/4;
		const y = Math.random() * boxWidth/2 - boxWidth/4;
		const z = Math.random() * boxWidth/2 - boxWidth/4;
		sphere.velocity.set(x,y,z);

		scene.add(sphere);
		sphereArr[i] = sphere;
	}
}

function setLight(){
	const ambientlight = new THREE.AmbientLight(0xFFFFFF,1);
	scene.add(ambientlight);
}

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

	for(let i = 0; i < sphereArr.length; i++){
		const sphere = sphereArr[i];
		sphere.move();

		if(sphere.acceleration.length() <= 1){
			sphere.init();
		}

		if(sphere.position.x >= boxWidth/2 - (sphere.radius/2)){
			normal.set(-1,0,0);
			sphere.velocity.x = boxWidth/2 - (sphere.radius/2);
			collision = true;
		}else if(sphere.position.x < -boxWidth/2 + (sphere.radius/2)){
			normal.set(1,0,0);
			sphere.velocity.x = -boxWidth/2 + (sphere.radius/2);
			collision = true;
		}
		if(sphere.position.y >= boxWidth/2 - (sphere.radius/2)){
			normal.set(0,-1,0);
			sphere.velocity.y = boxWidth/2 - (sphere.radius/2);
			collision = true;
		}else if(sphere.position.y < -boxWidth/2 + (sphere.radius/2)){
			normal.set(0,1,0);
			sphere.velocity.y = -boxWidth/2 + (sphere.radius/2);
			collision = true;
		}
		if(sphere.position.z >= boxWidth/2 - (sphere.radius/2)){
			normal.set(0,0,-1);
			sphere.velocity.z = boxWidth/2 - (sphere.radius/2);
			collision = true;
		}else if(sphere.position.z < -boxWidth/2 + (sphere.radius/2)){
			normal.set(0,0,1);
			sphere.velocity.z = -boxWidth/2 + (sphere.radius/2);
			collision = true;
		}

		if(collision){
			sphere.reflect(normal);
			collision = false;
		}

		for(let index = i; index < sphereArr.length; index ++){
			const distance = sphere.velocity.distanceTo(sphereArr[index].velocity);
			const rebound_distance = sphere.radius + sphereArr[index].radius;
			if(distance <= rebound_distance){
				const overlap = Math.abs(distance - rebound_distance);
				const normal = sphere.velocity.clone().sub(sphereArr[index].velocity).normalize();
				sphere.velocity.sub(normal.clone().multiplyScalar(overlap * -1));
				sphereArr[index].velocity.sub(normal.clone().multiplyScalar(overlap));
				sphere.reflect(normal.clone().multiplyScalar(-1));
				sphereArr[index].reflect(normal.clone());
			}
		}
	}

	renderer.render(scene,camera);
}

class PhisicsSphere extends THREE.Mesh{
	constructor(num,sphereNum){
		const radius = Math.random() * 25 + 15;
		const geometry = new THREE.SphereGeometry(radius,radius,radius);
		const material = new THREE.MeshNormalMaterial();
		super(geometry,material);

		this.radius = radius;
		this.mass = radius / 10;
		this.velocity = new THREE.Vector3();
		this.acceleration = new THREE.Vector3();
	}

	init(){
		const radian = Math.random() * 360 * Math.PI / 180;
		const phi = Math.random() * 360 * Math.PI / 180;
		const scalar = Math.random() * 5 + 15;
		const fx = Math.cos(radian) * Math.cos(phi) * scalar;
		const fy = Math.cos(radian) * Math.sin(phi) * scalar;
		const fz = Math.sin(radian) * scalar;
		const fource = new THREE.Vector3(fx,fy,fz);
		fource.divideScalar(this.mass);
		this.applyFouce(fource);
	}

	move(){
		this.applyFriction();
		this.velocity.add(this.acceleration);
		this.position.copy(this.velocity)
	}

	applyFouce(vector3){
		this.acceleration.add(vector3);
	}

	applyFriction(){
		const friction = this.acceleration.clone();
		friction.multiplyScalar(-1);
		friction.normalize();
		friction.multiplyScalar(0.1);
		this.applyFouce(friction);
	}

	reflect(vector3){
		this.acceleration.reflect(vector3);
	}
}

完成したデモになります。Three.jsを使用して球体アニメーションを制作するには、ベクトルが重要になってきます。ベクトルについて調べながら、ライトとマテリアルを調整しました。

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

関連記事

前の記事へ

Three.jsでラインアニメーション