1. Top
  2. Blog
  3. Three.jsでなめらかに動く球体を作成してみる
Three.jsでなめらかに動く球体を作成してみる

Three.jsでなめらかに動く球体を作成してみる

  • Morishita
  • 2023.12.20

はじめに

Three.jsは、Webページ上で3Dコンテンツを簡単に描画することができるJavaScriptライブラリです。近年では3D表現を用いたWebページを見かける機会が増えているように思います。今回はThree.jsを活用して、球体がなめらかに動くアニメーションを作成してみようと思います。

今回作成したもの

まず始めに、今回作成したものはこちらです。

 

3Dの球体がなめらかに動いています。また、ページ上部にあるコントローラーを左右に動かすことで、球体の動きに変化を与えることができるようになっています。

今回記述したコードはこちら。

HTML

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title>なめらかに動く球体</title>
    <link rel="stylesheet" href="style.css">
</head>
  <body>
    <canvas id="webgl"></canvas>
    <main>
      <div class="controls">
        <div>
            <label>Speed</label>
            <input type="range" min="5" max="120" value="13" step="1" name="speed">
        </div>
        <div>
            <label>Spikes</label>
            <input type="range" min=".03" max="2" value=".36" step=".05" name="spikes">
        </div>
        <div>
            <label>Processing</label>
            <input type="range" min=".3" max="2.4" value=".8" step=".01" name="processing">
        </div>
    </div>
    </main>
    <script type="module" src="<https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js>"></script>
    <script type="module" src="script.js"></script>
  </body>
</html>


HTMLには球体を描画するためのcanvas要素と、動きに変化を与えるコントローラーを表示するための要素を記述しています。また、script.js内でjQueryも使用しているため、CDNを読み込んでいます。

CSS

canvas {
  position: fixed;
  top: 0;
  left: 0;
  background: #000;
}

/* コントローラーのCSS */
.controls {
  background: #3F4656;
  display: flex;
  padding: 20px;
  border-radius: 10px;
  position: fixed;
  z-index: 3;
  top: 0;
  right: 0;
  box-shadow: 0 4px 20px -1px rgba(18, 22, 33, .7);
}

.controls label {
  color: #CDD9ED;
  font-weight: 500;
  font-size: 14px;
  display: block;
  margin-bottom: 16px;
}

.controls [type="range"] {
  width: 160px;
}

@media(max-width: 600px) {
  .controls [type="range"] {
    width: 280px;
  }
}

.controls > div:not(:last-child) {
  margin-right: 20px;
}


CSSには、canvas要素とコントローラー用のCSSを記載しています。

JavaScript

import * as THREE from "three";
import {SimplexNoise} from "three/examples/jsm/math/SimplexNoise";

//input値取得
let speedSlider = $('input[name="speed"]'),
    spikesSlider = $('input[name="spikes"]'),
    processingSlider = $('input[name="processing"]');

//canvas
const canvas = document.querySelector("#webgl")

//シーン
const scene = new THREE.Scene();

//サイズ
const sizes = {
  width: innerWidth,
  height: innerHeight
}

//カメラ
const camera = new THREE.PerspectiveCamera(
  75,
  sizes.width / sizes.height,
  0.1,
  1000
);
camera.position.z = 3;

//レンダラー
const renderer = new THREE.WebGLRenderer({
  canvas: canvas,
  alpha: true//背景を透明にする
});
renderer.setSize(sizes.width,sizes.height);
renderer.setPixelRatio(window.devicePixelRatio);

//オブジェクトを作成
const geometry = new THREE.SphereGeometry(1.5, 128, 128);
const positionAttributeBase = geometry.getAttribute( 'position' ).clone();//頂点を操作する用のクローンを作成
const material = new THREE.MeshPhysicalMaterial({
    // wireframe: true,
    color: 0xffffff,
    transparent: true,
    side: THREE.DoubleSide,
    transmission: 1,//透過率
    metalness: 0,//金属製
    roughness: 0,//粗さ
    ior: 1.2,//屈折率
    specularIntensity: 1,//反射量
    specularColor: 0xffffff,//反射色
});

//影の描画を有効化
renderer.shadowMap.enabled = true;

//ライトを追加
let lightTop = new THREE.DirectionalLight(0xffffff, .8);
lightTop.position.set(5, 40, -50);
scene.add(lightTop);

let lightBottom = new THREE.DirectionalLight(0xFFFFFF, 2);
lightBottom.position.set(0, 0, 400);
scene.add(lightBottom);

//オブジェクトをシーンに追加
const sphere = new THREE.Mesh(geometry, material);
scene.add(sphere);

//SimplexNoise
const simplex = new SimplexNoise();
//3Dベクトルを表すコンテナを作成
const vector = new THREE.Vector3();

let update = () => {
  //基準を指定
  let time = performance.now() * 0.00001 * speedSlider.val() * Math.pow(processingSlider.val(), 3),
      spikes = spikesSlider.val() * processingSlider.val();
  const positionAttribute = geometry.getAttribute('position');
  for(let i = 0; i < positionAttributeBase.count; i++) { 
      vector.fromBufferAttribute(positionAttributeBase, i);//頂点を取り出す
      const noise = simplex.noise3d(vector.x * spikes, vector.y * spikes, vector.z * spikes + time);
      const ratio = noise * 0.05 + 0.98;
      vector.multiplyScalar(ratio);//ベクトルの各要素をratio乗する
      positionAttribute.setXYZ(i, vector.x, vector.y, vector.z)//頂点座標を更新
  }
  sphere.geometry.attributes.position.needsUpdate = true;//頂点座標が変更されたことをThree.jsに通知
  sphere.geometry.computeVertexNormals();//法線ベクトルを計算
}

//アニメーション
const tick = () => {
  update();
  renderer.render(scene, camera);
  window.requestAnimationFrame(tick);
}
tick();

JavaScriptの解説

今回は、球体の動きに焦点を当てているため、カメラやレンダラーなどの基本的な説明は省略します。

POINT1.必要なファイルをインポート

Three.jsを読み込む方法はいくつかありますが、今回は npm install three でインストール※しました。

import * as THREE from "three";
import {SimplexNoise} from "three/examples/jsm/math/SimplexNoise";

この記述により、Three.jsとSimplexNoise.jsをインポートしています。SimplexNoise.jsは、ノイズを生成するためのライブラリで、球体の自然な動きを表現するために使用しています。

※Three.jsのインストール方法はThree.jsの公式サイトを参考にしています。

POINT2.オブジェクト(球体)を作成

//オブジェクトを作成
const geometry = new THREE.SphereGeometry(1.5, 128, 128);
const positionAttributeBase = geometry.getAttribute( 'position' ).clone();//頂点を操作する用のクローンを作成
const material = new THREE.MeshPhysicalMaterial({
    // wireframe: true,
    color: 0xffffff,
    transparent: true,
    side: THREE.DoubleSide,
    transmission: 1,//透過率
    metalness: 0,//金属製
    roughness: 0,//粗さ
    ior: 1.2,//屈折率
    specularIntensity: 1,//反射量
    specularColor: 0xffffff,//反射色
});

光に反射するなめらかな球体を作成したいので、さまざまな属性を設定しています。
また、この後の処理で基準となる球体の頂点が必要となるので、先にジオメトリ(頂点や辺などの情報)を取得して positionAttributeBase に格納しています。

POINT3.球体を動かす

//SimplexNoise
const simplex = new SimplexNoise();
//3Dベクトルを表すコンテナを作成
const vector = new THREE.Vector3();

let update = () => {
  //基準を指定
  let time = performance.now() * 0.00001 * speedSlider.val() * Math.pow(processingSlider.val(), 3),
      spikes = spikesSlider.val() * processingSlider.val();
  const positionAttribute = geometry.getAttribute('position');
  for(let i = 0; i < positionAttributeBase.count; i++) { 
      vector.fromBufferAttribute(positionAttributeBase, i);//基準となる頂点を取得
      const noise = simplex.noise3d(vector.x * spikes, vector.y * spikes, vector.z * spikes + time);
      const ratio = noise * 0.05 + 0.98;
      vector.multiplyScalar(ratio);//ベクトルの各要素をratio乗する
      positionAttribute.setXYZ(i, vector.x, vector.y, vector.z)//頂点座標を更新
  }
  sphere.geometry.attributes.position.needsUpdate = true;//頂点座標が変更されたことをThree.jsに通知
  sphere.geometry.computeVertexNormals();//法線ベクトルを計算

この記述の部分が実際に球体を動かしているコードになります。

vector.fromBufferAttribute(positionAttributeBase, i);//基準となる頂点を取得

この記述ではfromBufferAttribute メソッドを使用して、先ほど定義したpositionAttributeBaseの頂点座標をもとに、3D空間上の頂点位置を表すためのVector3オブジェクトとして取り出しています。

const noise = simplex.noise3d(vector.x * spikes, vector.y * spikes, vector.z * spikes + time);
const ratio = noise * 0.05 + 0.98;
vector.multiplyScalar(ratio);//ベクトルの各要素をratio乗する
positionAttribute.setXYZ(i, vector.x, vector.y, vector.z)//頂点座標を更新

この記述では、SimplexNoise.js内に記載されているノイズの計算方法を利用して、頂点の値を再計算しています。

sphere.geometry.attributes.position.needsUpdate = true;//頂点座標が変更されたことをThree.jsに通知
sphere.geometry.computeVertexNormals();//法線ベクトルを計算

更新された頂点に対して法線ベクトルを再計算させることで、しっかりと光を反射してくれるようになります。

主なポイントは以上です。

input値やratio値を変えてあげることで球体の表現を自由に変化させることもできるようなので、興味のある方はぜひ試してみてください。