FREENANCE Ad

チュートリアルをなぞるだけ!Three.jsでインタラクティブエフェクトを作ろう

three-10
FREENANCE Ad

demo

Webブラウザ上で3DCGを扱うための仕組みであるWebGL。そのWebGLを扱うにあたって活用されているのが、無料で利用できるJavaScriptライブラリ『three.js』です。

今回はこのthree.jsとTweenMax(GSAP)を使用して、インタラクティブエフェクトを作成する方法をご紹介します。

なお、エフェクトはBestServedBoldのHolographic-Interactionsを参考にしています。

完成予定図

three-1

ひとつめのデモでは完成された実用的な例をご紹介しましたが、これから作成過程をご紹介するデモは調整およびアレンジが可能です。

また前述のとおり、今回ご紹介するエフェクトはBestServedBoldによるHolographic-Interactionsを参考にしています。

コアコンセプト

エフェクトを作成するにあたってコアとなっているのが、「マウスの動きに連動してランダムなエレメントグリッドを生成する」という発想です。

グリッドの各エレメントは、マウスの位置からエレメントの中心までの距離に基づいて、Yの位置、回転値、尺度値を更新します。

three-2

マウスがエレメントに近づけば近づくほど、エレメントが大きく表示されます。

three-3

また半径を定義することによって、半径内のエレメントへの影響をコントロールします。半径が大きければ大きいほど、マウスを動かしたときに反応するエレメント数が増えます。

three-4

実際に作ってみよう

1. HTMLページ

まずデモ用にHTMLページを設定しましょう。

コードはすべてキャンバスエレメント内で実行されるため、HTMLページ自体はシンプルです。

<html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <meta name="target" content="all">
      <meta http-equiv="cleartype" content="on">
      <meta name="apple-mobile-web-app-capable" content="yes">
      <meta name="mobile-web-app-capable" content="yes">
      <title>Repulsive Force Interavtion</title>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/96/three.min.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/TweenMax.min.js"></script>
    </head>
  <body>
  </body>
</html>

コードからもわかるとおり、CDNからthree.jsとTweenMaxにリンクしています。

2. ヘルパー関数

次に2点間の距離を計算し、値をマッピングして、角度をラジアンに変換するためのヘルパー関数を定義しましょう。

const radians = (degrees) => {
  return degrees * Math.PI / 180;
}

const distance = (x1, y1, x2, y2) => {
  return Math.sqrt(Math.pow((x1 - x2), 2) + Math.pow((y1 - y2), 2));
}

const map = (value, start1, stop1, start2, stop2) => {
  return (value - start1) / (stop1 - start1) * (stop2 - start2) + start2
}

3. グリッドエレメント

グリッドを構築する前に、使用するオブジェクトを定義しましょう。

箱型のオブジェクト(Box)

 // credits for the RoundedBox mesh - Dusan Bosnjak

  import RoundedBoxGeometry from 'roundedBox';

  class Box {
    constructor() {
      this.geom = new RoundedBoxGeometry(.5, .5, .5, .02, .2);
      this.rotationX = 0;
      this.rotationY = 0;
      this.rotationZ = 0;
    }
  }

円錐型のオブジェクト(Cone)

class Cone {
    constructor() {
      this.geom = new THREE.ConeBufferGeometry(.3, .5, 32);
      this.rotationX = 0;
      this.rotationY = 0;
      this.rotationZ = radians(-180);
    }
  }

ドーナツ型のオブジェクト(Torus)

class Torus {
    constructor() {
      this.geom = new THREE.TorusBufferGeometry(.3, .12, 30, 200);
      this.rotationX = radians(90);
      this.rotationY = 0;
      this.rotationZ = 0;
    }
  }

4. 三次元の設定

メインクラスの中に、セットアップ用の関数を作成しましょう。

setup() {
    // handles mouse coordinates mapping from 2D canvas to 3D world
    this.raycaster = new THREE.Raycaster();

    this.gutter = { size: 1 };
    this.meshes = [];
    this.grid = { cols: 14, rows: 6 };
    this.width = window.innerWidth;
    this.height = window.innerHeight;
    this.mouse3D = new THREE.Vector2();
    this.geometries = [
      new Box(),
      new Tourus(),
      new Cone()
    ];

    window.addEventListener('mousemove', this.onMouseMove.bind(this), { passive: true });

    // we call this to simulate the initial position of the mouse cursor
    this.onMouseMove({ clientX: 0, clientY: 0 });
  }

5. マウス移動ハンドラー

onMouseMove({ clientX, clientY }) {
    this.mouse3D.x = (clientX / this.width) * 2 - 1;
    this.mouse3D.y = -(clientY / this.height) * 2 + 1;
  }

6. 3Dシーンの作成

createScene() {
    this.scene = new THREE.Scene();

    this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    this.renderer.setSize(window.innerWidth, window.innerHeight);
    this.renderer.setPixelRatio(window.devicePixelRatio);


    this.renderer.shadowMap.enabled = true;
    this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;

    document.body.appendChild(this.renderer.domElement);
  }

7. カメラ

カメラをシーンに追加しましょう。

createCamera() {
    this.camera = new THREE.PerspectiveCamera(20, window.innerWidth / window.innerHeight, 1);

    // set the distance our camera will have from the grid
    this.camera.position.set(0, 65, 0);

    // we rotate our camera so we can get a view from the top
    this.camera.rotation.x = -1.57;

    this.scene.add(this.camera);
  }

8. ランダムオブジェクトヘルパー

箱型、円錐型、ドーナツ型のオブジェクトをランダムに配置するためのヘルパーを作成しましょう。

 getRandomGeometry() {
    return this.geometries[Math.floor(Math.random() * Math.floor(this.geometries.length))];
  }

9. メッシュヘルパー

ジオメトリとマテリアルに基づいてメッシュを作成するためのヘルパーを作成しましょう。

getMesh(geometry, material) {
    const mesh = new THREE.Mesh(geometry, material);

    mesh.castShadow = true;
    mesh.receiveShadow = true;

    return mesh;
  }

10. エレメントをグリッドに配置

ここでやっとランダムなエレメントをグリッドにレイアウトできました。

three-5

 createGrid() {
    // create a basic 3D object to be used as a container for our grid elements so we can move all of them together
    this.groupMesh = new THREE.Object3D();

    const meshParams = {
      color: '#ff00ff',
      metalness: .58,
      emissive: '#000000',
      roughness: .18,
    };

    // we create our material outside the loop to keep it more performant
    const material = new THREE.MeshPhysicalMaterial(meshParams);

    for (let row = 0; row < this.grid.rows; row++) {
      this.meshes[row] = [];

      for (let col = 0; col < this.grid.cols; col++) {
        const geometry = this.getRandomGeometry();
        const mesh = this.getMesh(geometry.geom, material);

        mesh.position.set(col + (col * this.gutter.size), 0, row + (row * this.gutter.size));
        mesh.rotation.x = geometry.rotationX;
        mesh.rotation.y = geometry.rotationY;
        mesh.rotation.z = geometry.rotationZ;

        // store the initial rotation values of each element so we can animate back
        mesh.initialRotation = {
          x: mesh.rotation.x,
          y: mesh.rotation.y,
          z: mesh.rotation.z,
        };

        this.groupMesh.add(mesh);

        // store the element inside our array so we can get back when need to animate
        this.meshes[row][col] = mesh;
      }
    }

    //center on the X and Z our group mesh containing all the grid elements
    const centerX = ((this.grid.cols - 1) + ((this.grid.cols - 1) * this.gutter.size)) * .5;
    const centerZ = ((this.grid.rows - 1) + ((this.grid.rows - 1) * this.gutter.size)) * .5;
    this.groupMesh.position.set(-centerX, 0, -centerZ);

    this.scene.add(this.groupMesh);
  }

11. ライトの追加

アンビエントライト

次に、色の効果を出すためにAmbientLightを追加しましょう。

three-6

 addAmbientLight() {
    const light = new THREE.AmbientLight('#2900af', 1);

    this.scene.add(light);
  }

スポットライト

リアルなタッチにするために、スポットライトのような効果をつけられるSpotLightも追加しましょう。

three-7

addSpotLight() {
    const ligh = new THREE.SpotLight('#e000ff', 1, 1000);

    ligh.position.set(0, 27, 0);
    ligh.castShadow = true;

    this.scene.add(ligh);
  }

レクトエリアライト

さらに四角く平面を照らすRectAreaLightも追加します。

three-8

addRectLight() {
    const light = new THREE.RectAreaLight('#0077ff', 1, 2000, 2000);

    light.position.set(5, 50, 50);
    light.lookAt(0, 0, 0);

    this.scene.add(light);
  }

ポイントライト

最後にポイントを照らすPointLightを追加すれば、ライティングは完成です。

three-9

addPointLight(color, position) {
    const light = new THREE.PointLight(color, 1, 1000, 1);

    light.position.set(position.x, position.y, position.z);

    this.scene.add(light);
  }

12. シャドーフロア

次に、マウスカーソルがホバーできる場所のマッピングオブジェクトとして機能する図形を追加しましょう。

addFloor() {
    const geometry = new THREE.PlaneGeometry(100, 100);
    const material = new THREE.ShadowMaterial({ opacity: .3 });

    this.floor = new THREE.Mesh(geometry, material);
    this.floor.position.y = 0;
    this.floor.receiveShadow = true;
    this.floor.rotateX(- Math.PI / 2);

    this.scene.add(this.floor);
  }

13. ドロー/アニメートエレメント

最後に、アニメーションの処理をハンドルする関数を追加しましょう。これはrequestAnimationFrame内のすべてのフレームに呼び出されます。


  draw() {
    // maps our mouse coordinates from the camera perspective
    this.raycaster.setFromCamera(this.mouse3D, this.camera);

    // checks if our mouse coordinates intersect with our floor shape
    const intersects = this.raycaster.intersectObjects([this.floor]);

    if (intersects.length) {

      // get the x and z positions of the intersection
      const { x, z } = intersects[0].point;

      for (let row = 0; row < this.grid.rows; row++) {
        for (let col = 0; col < this.grid.cols; col++) {

          // extract out mesh base on the grid location
          const mesh = this.meshes[row][col];

          // calculate the distance from the intersection down to the grid element
          const mouseDistance = distance(x, z,
            mesh.position.x + this.groupMesh.position.x,
            mesh.position.z + this.groupMesh.position.z);

          // based on the distance we map the value to our min max Y position
          // it works similar to a radius range

          const maxPositionY = 10;
          const minPositionY = 0;
          const startDistance = 6;
          const endDistance = 0;
          const y = map(mouseDistance, startDistance, endDistance, minPositionY, maxPositionY);

          // based on the y position we animate the mesh.position.y
          // we don´t go below position y of 1
          TweenMax.to(mesh.position, .4, { y: y < 1 ? 1 : y });

          // create a scale factor based on the mesh.position.y
          const scaleFactor = mesh.position.y / 2.5;

          // to keep our scale to a minimum size of 1 we check if the scaleFactor is below 1
          const scale = scaleFactor < 1 ? 1 : scaleFactor;

          // animates the mesh scale properties
          TweenMax.to(mesh.scale, .4, {
            ease: Back.easeOut.config(1.7),
            x: scale,
            y: scale,
            z: scale,
          });

          // rotate our element
          TweenMax.to(mesh.rotation, .7, {
            ease: Back.easeOut.config(1.7),
            x: map(mesh.position.y, -1, 1, radians(45), mesh.initialRotation.x),
            z: map(mesh.position.y, -1, 1, radians(-90), mesh.initialRotation.z),
            y: map(mesh.position.y, -1, 1, radians(90), mesh.initialRotation.y),
          });
        }
      }
    }
  }

完成!

これでインタラクティブエフェクトは完成です。今回ご紹介したデザイン以外にも、グリッドのレイアウトを変えたり、オブジェクトを増やしたりするなど、さまざまな可能性を秘めています。

以下はグリッドのレイアウトを変更した例です。

three-10

また以下のように、カメラの角度を変えても面白いかもしれません。

three-11

今回のチュートリアルはお楽しみいただけたでしょうか。ぜひこのチュートリアルをベースにして、オリジナルの作品を作ってみてください!

(原文:Ion D. Filho 翻訳:Asuka Nakajima)

SHARE

  • 広告主募集
  • ライター・編集者募集
  • WorkshipSPACE
週1〜3 リモートワーク 土日のみでも案件が見つかる!
Workship