2020年11月07日 - WebVR・Three.js
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を使用して球体アニメーションを制作するには、ベクトルが重要になってきます。ベクトルについて調べながら、ライトとマテリアルを調整しました。

