STUDIO TAMA


thumbnail

投稿日:2022-07-21

【Three.js】 Popping Sphere Animation

  • #Three.js

  • #Basic

最近 THREE.js を勉強しており、アウトプットがてらブログにしていこうと思います。跳ねながら球体が成長していくようなアニメーションを作成していこうと思います。ただ、あくまで Three.js 初学者ですので、コードは参考程度にしていただければと思います。きっとより効率のよい実装方法があるかと思います。

Code Pen

でソースコードと完成品を公開しておりますので、是非のぞいてみてください。Youtube にも完成版をアップロードしております。以下のリンクから完成形を確認できます。

準備

  • index.html, style.css, main.js ファイルを作成し、index.html と style.css を以下の様なコードになります。

index.html

1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta http-equiv="X-UA-Compatible" content="IE=edge">
6    <meta name="viewport" content="width=device-width, initial-scale=1.0">
7    <link rel="stylesheet" href="style.css">
8    <title>Document</title>
9</head>
10<body>
11    <script
12    src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"
13    integrity="sha512-dLxUelApnYxpLt6K2iomGngnHO83iUvZytA3YjDUCjT0HDOHKXnVYdf3hU4JjM8uEhxf9nD1/ey98U3t2vZ0qQ=="
14    crossorigin="anonymous"
15    referrerpolicy="no-referrer"
16    ></script>
17    <script src="main.js"></script>
18</body>
19</html>

style.css

1* {
2  margin: 0;
3  padding: 0;
4}
  • Three.js の実行方法は色々ありますが、今回はこちらのリンクから Threej.s の CDN をコピーして、script タグで張り付けていきます。その後、main.js の script を読み込んでおります。

ベースとなる球の生成

thumbnail

main.js

1window.addEventListener("load", init);
2
3function init() {
4  let width = window.innerWidth;
5  let height = window.innerHeight;
6
7  const scene = new THREE.Scene();
8  scene.background = new THREE.Color(0x050505);
9
10  const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);
11  cameraSetBackDist = 7;
12  camera.position.z = cameraSetBackDist;
13
14  const light = new THREE.PointLight(0xffffff, 2);
15  light.position.set(10, 10, 10);
16  scene.add(light);
17
18  const renderer = new THREE.WebGLRenderer();
19  renderer.setSize(width, height);
20  document.body.appendChild(renderer.domElement);
21
22  const geometry = new THREE.IcosahedronGeometry(1, 10);
23
24  //↓この範囲は後で消す
25  const material = new THREE.MeshBasicMaterial({
26      wireframe: true,
27      color: 0xc100eb
28  });
29  const sphere = new THREE.Mesh(geometry, material);
30  scene.add(sphere)
31  scene.add(camera)
32  renderer.render(scene, camera);
33  //↑この範囲は後で消す
34}
  • ページが読み込まれたら、init 関数を実行していきます。
  • まず初めに、scene / camera / light /renderer を生成しています。
  • その後、icoSphere のジオメトリを生成します。可視化するために、マテリアルを設定しメッシュ化します。上画像の様なメッシュが生成されるかと思います。
  • 上コードのこの範囲は消すで囲まれたところは、一時的に可視化するためのものなので消します。

球を生成する三角形メッシュの法線ベクトルの取得と移動させる球の生成

main.js

1window.addEventListener("load", init);
2
3function init() {
4  let width = window.innerWidth;
5  let height = window.innerHeight;
6
7  const scene = new THREE.Scene();
8  scene.background = new THREE.Color(0x050505);
9
10  const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);
11  cameraSetBackDist = 7;
12  camera.position.z = cameraSetBackDist;
13
14  const light = new THREE.PointLight(0xffffff, 2);
15  light.position.set(10, 10, 10);
16  scene.add(light);
17
18  const renderer = new THREE.WebGLRenderer();
19  renderer.setSize(width, height);
20  document.body.appendChild(renderer.domElement);
21
22  const geometry = new THREE.IcosahedronGeometry(1, 10);
23  const geometryPos = geometry.getAttribute("position").array;
24
25  const mesh = [];
26  const normalDirection = [];
27
28  for (let i = 0; i < geometryPos.length; i += 9) {
29    const geometry2 = new THREE.BufferGeometry();
30
31    const vertices = new Float32Array([
32      geometryPos[i],
33      geometryPos[i + 1],
34      geometryPos[i + 2],
35      geometryPos[i + 3],
36      geometryPos[i + 4],
37      geometryPos[i + 5],
38      geometryPos[i + 6],
39      geometryPos[i + 7],
40      geometryPos[i + 8]
41    ]);
42
43    geometry2.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
44    geometry2.setAttribute("normal", new THREE.BufferAttribute(vertices, 3));
45
46    const normal = new THREE.Vector3(
47      (geometryPos[i] + geometryPos[i + 3] + geometryPos[i + 6]) / 3,
48      (geometryPos[i + 1] + geometryPos[i + 4] + geometryPos[i + 7]) / 3,
49      (geometryPos[i + 2] + geometryPos[i + 5] + geometryPos[i + 8]) / 3
50    );
51
52    normal.normalize();
53    const icoSphereGeometry = new THREE.IcosahedronGeometry(0.1, 1);
54    const material = new THREE.MeshBasicMaterial({
55      wireframe: false,
56      color: 0xc100eb
57    });
58
59    const sphere = new THREE.Mesh(icoSphereGeometry, material);
60    mesh.push(sphere);
61
62    normalDirection.push(normal);
63
64  }
65
  • 23 行目で、先ほど生成した icoSphere のジオメトリの頂点の座標を取得します。
  • 25/ 26 行目で mesh / normalDirection という変数でからの配列を生成しておきます。
  • 28 ~ 44 行目では取得した頂点の座標から、icoSphere を生成する、三角形メッシュ単体のジオメトリを生成し、position / normal の attribute をセットしています。
  • 46 ~ 52 行目までで、各三角形の法線ベクトルを取得し、単位ベクトルに直しています。
  • icoSphere のジオメトリとそれに割り当てるマテリアルを生成し、メッシュ化したものと、三角形の法線ベクトルを先ほど生成した空の配列に格納していきます。これを for 文でまわしているので、最初に生成した icoSphere の三角形メッシュの回数だけ icoSphere が生成されることになります。

main.js

1window.addEventListener("load", init);
2
3function init() {
4  let width = window.innerWidth;
5  let height = window.innerHeight;
6
7//////さきほどの続き、省略/////
8
9  let loopSpeed = 0;
10  let rot = 0;
11  const clock = new THREE.Clock();
12
13  const tick = () => {
14    rot += 0.3;
15    const cameraAngle = (rot * Math.PI) / 180;
16    let z = cameraSetBackDist * Math.cos(cameraAngle);
17    let x = cameraSetBackDist * Math.sin(cameraAngle);
18    camera.position.set(x, 0, z);
19    camera.lookAt(0, 0, 0);
20
21    const elapsedTime = clock.getElapsedTime();
22
23    mesh.map((spheremesh, index) => {
24      const coordinateAverageValue =
25        (normalDirection[index].x +
26         normalDirection[index].y +
27         normalDirection[index].z) / 3;
28      const addAngle = coordinateAverageValue * elapsedTime ;
29      const distance = 1;
30   loopSpeed += 0.002; 
31
32   const radians = (loopSpeed * Math.PI) / 180;
33      const angle = radians + addAngle;
34      const loop = (Math.sin(angle) + 1) * distance;
35      const scale = (Math.sin(angle) + 1.1) * 0.3;
36   
37      spheremesh.position.set(
38        normalDirection[index].x * loop,
39        normalDirection[index].y * loop,
40        normalDirection[index].z * loop
41      );
42      spheremesh.scale.set(scale, scale, scale);
43
44      const h = Math.abs(Math.sin(angle)) * 360;
45      const s = 100;
46      const l = 70;
47      const color = new THREE.Color(`hsl(${h},${s}%,${l}%)`);
48      spheremesh.material.color = color;
49
50      scene.add(spheremesh);
51    });
52    requestAnimationFrame(tick);
53  };
54  tick();
55
56}
  • tick()関数を作成して、アニメーションを生成していきます。
  • 9 行目~ 19 行目までで、まずは Camera を動かしていきます。カメラは、XZ 平面上の円を周回しながら、最初に可視化した IcoSphere の中心を常に見るように移動させます。
  • 実際に球を移動させていきます。23 行目~ 27 行目までで、まずは各三角形の法線ベクトルの xyz 成分の平均を値を取得します。xyz がすべて正で中心から離れれば離れるほど値は大きくなり、全て負で中心から離れれば離れるほど値は小さくなります。
  • 28 行目で、この平均した値に、clock インスタンスを生成してからの時間(elapsedTime)を掛けます。これを addAngle とします。
  • 32 行目で、アニメーションのスピードを調整するための値、loopSpeed をラジアン化し、そのラジアン化した角度に先ほどの addAngle を足して sin 関数で sin 波を取得します。
  • 34 行目。取得した sin 波は-1 ~ 1 の値なので、1 を足すことで 0 ~ 2 の正の値にします。これに、球体の移動させる距離を調整するための値 distance をかけ合わせます。これで、0 ~ 2 の値をループする値が取得できました。
  • 35 行目。移動距離に応じで球体を scale させたいので、scale するための値も取得しておきます。
  • 37 ~ 41 行目。mesh 配列に格納した icoSphere のメッシュをセットしていきます。法線ベクトルの xyz 座標に、先ほど生成した 0 ~ 2 までをループする値を掛けることで、0 ~ 2 までの範囲を行ったりきたりし、addAngle の値がそれぞれの icoSphere ごとに違う値をとるので、sine 波の位相がずれ、アニメーションの様な動きをします。
  • 42 行目。scale もセットすることで、移動距離に応じて球が大きなったり小さくなったりします。
  • 44 ~ 48 行目。マテリアルも虹色に変化するように設定します。-1 ~ 1 までの絶対値、すなわち 0 ~ 1 までの値に 360 度を掛け、hsl カラーの h に割り当てることで、虹色に変化するようになります。
  • 50 ~ 52 行目。scene に移動させた icoSphere を add し、その後再度 tick 関数自信を関数内で呼び出します。
  • 54 行目。tick 関数を忘れずに実行しましょう。

main.js

1window.addEventListener("load", init);
2
3function init() {
4  let width = window.innerWidth;
5  let height = window.innerHeight;
6
7 //////さきほどの続き、省略/////
8
9  window.addEventListener("resize", () => {
10    width = window.innerWidth;
11    height = window.innerHeight;
12    camera.aspect = width / height;
13    camera.updateProjectionMatrix();
14    renderer.setSize(width, height);
15  });
16
17  function animate() {
18    requestAnimationFrame(animate);
19    renderer.render(scene, camera);
20  }
21
22  animate();
23}
  • 9 ~ 15 行目で画面サイズが変わっても、レンダリングされたアニメーションもサイズに応じて変化するように設定しています。
  • 17 ~ 22 行目でレンダリングしています。

ソースコード全体

index.html

1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta http-equiv="X-UA-Compatible" content="IE=edge">
6    <meta name="viewport" content="width=device-width, initial-scale=1.0">
7    <link rel="stylesheet" href="style.css">
8    <title>Document</title>
9</head>
10<body>
11    <script
12    src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"
13    integrity="sha512-dLxUelApnYxpLt6K2iomGngnHO83iUvZytA3YjDUCjT0HDOHKXnVYdf3hU4JjM8uEhxf9nD1/ey98U3t2vZ0qQ=="
14    crossorigin="anonymous"
15    referrerpolicy="no-referrer"
16    ></script>
17    <script src="main.js"></script>
18</body>
19</html>
1* {
2  margin: 0;
3  padding: 0;
4}

main.js

1window.addEventListener("load", init);
2
3function init() {
4  let width = window.innerWidth;
5  let height = window.innerHeight;
6
7  const scene = new THREE.Scene();
8  scene.background = new THREE.Color(0x050505);
9
10  const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);
11  cameraSetBackDist = 7;
12  camera.position.z = cameraSetBackDist;
13
14  const light = new THREE.PointLight(0xffffff, 2);
15  light.position.set(10, 10, 10);
16  scene.add(light);
17
18  const renderer = new THREE.WebGLRenderer();
19  renderer.setSize(width, height);
20  document.body.appendChild(renderer.domElement);
21
22  const geometry = new THREE.IcosahedronGeometry(1, 10);
23  const geometryPos = geometry.getAttribute("position").array;
24
25  const mesh = [];
26  const normalDirection = [];
27
28  for (let i = 0; i < geometryPos.length; i += 9) {
29    const geometry2 = new THREE.BufferGeometry();
30
31    const vertices = new Float32Array([
32      geometryPos[i],
33      geometryPos[i + 1],
34      geometryPos[i + 2],
35      geometryPos[i + 3],
36      geometryPos[i + 4],
37      geometryPos[i + 5],
38      geometryPos[i + 6],
39      geometryPos[i + 7],
40      geometryPos[i + 8]
41    ]);
42
43    geometry2.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
44    geometry2.setAttribute("normal", new THREE.BufferAttribute(vertices, 3));
45
46    const normal = new THREE.Vector3(
47      (geometryPos[i] + geometryPos[i + 3] + geometryPos[i + 6]) / 3,
48      (geometryPos[i + 1] + geometryPos[i + 4] + geometryPos[i + 7]) / 3,
49      (geometryPos[i + 2] + geometryPos[i + 5] + geometryPos[i + 8]) / 3
50    );
51
52    normal.normalize();
53    const icoSphereGeometry = new THREE.IcosahedronGeometry(0.1, 1);
54    const material = new THREE.MeshBasicMaterial({
55      wireframe: false,
56      color: 0xc100eb
57    });
58
59    const sphere = new THREE.Mesh(icoSphereGeometry, material);
60    mesh.push(sphere);
61
62    normalDirection.push(normal);
63
64  }
65
66  let loopSpeed = 0;
67  let rot = 0;
68  const clock = new THREE.Clock();
69
70  const tick = () => {
71    rot += 0.3;
72    const cameraAngle = (rot * Math.PI) / 180;
73    let z = cameraSetBackDist * Math.cos(cameraAngle);
74    let x = cameraSetBackDist * Math.sin(cameraAngle);
75    camera.position.set(x, 0, z);
76    camera.lookAt(0, 0, 0);
77
78    const elapsedTime = clock.getElapsedTime();
79
80    mesh.map((spheremesh, index) => {
81      const coordinateAverageValue =
82        (normalDirection[index].x +
83         normalDirection[index].y +
84         normalDirection[index].z) / 3;
85      const addAngle = coordinateAverageValue * elapsedTime * 1;
86      const distance = 1;
87      loopSpeed += 0.002;
88      const radians = (loopSpeed * Math.PI) / 180;
89      const angle = radians + addAngle;
90      const loop = (Math.sin(angle) + 1) * distance;
91      const scale = (Math.sin(angle) + 1.1) * 0.3;
92
93      spheremesh.position.set(
94        normalDirection[index].x * loop,
95        normalDirection[index].y * loop,
96        normalDirection[index].z * loop
97      );
98      spheremesh.scale.set(scale, scale, scale);
99
100      const h = Math.abs(Math.sin(angle)) * 360;
101
102      const s = 100;
103      const l = 70;
104      const color = new THREE.Color(`hsl(${h},${s}%,${l}%)`);
105      spheremesh.material.color = color;
106
107      scene.add(spheremesh);
108
109    });
110    requestAnimationFrame(tick);
111  };
112  tick();
113
114  window.addEventListener("resize", () => {
115    width = window.innerWidth;
116    height = window.innerHeight;
117    camera.aspect = width / height;
118    camera.updateProjectionMatrix();
119    renderer.setSize(width, height);
120});
121
122
123  function animate() {
124    requestAnimationFrame(animate);
125    renderer.render(scene, camera);
126  }
127
128  animate();
129}

以上になります。ソースコードは CodePen でも公開してますので、是非参考にしてみてください。

目 次