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