見出し画像

Three.jsで3Dの宇宙空間をWeb上で実現する(WebGL)

はじめに

こんにちは!SHIFT DAAE部のTanakaです。

ここ10年の間にHTML5が普及したこともあり、ユニークなWebサイトがとても多くなってきました。

その理由の1つに、今まで難しかった動画や音声、グラフィックの描画が簡単になったことが挙げられます。

その中でもWebサイトをインパクトあるものにする技術として、WebGLというものがあります。今回はこのWebGLという技術を、3Dの宇宙空間の開発を通して具体的に紹介していきます。

はじめに、この記事を通して太陽の光を受けた地球が宇宙空間に浮かぶ様を表現します。


WebGL, Three.jsとは

それでは、WebGLThree.jsとは一体どんなものなのか、それぞれ紹介します。

WebGL(ウェブジーエル、Web Graphics Libraryの略称)は、互換性のある任意のウェブブラウザ上で、プラグインを使用せずにインタラクティブな2次元および3次元のコンピュータグラフィックをレンダリングするためのJavaScript APIである[2]。WebGLはウェブ標準に完全に統合されているため、ウェブページのcanvas要素上でGPUアクセラレータを使用した物理シミュレーション、画像処理、画像効果などを表現できる。
参考:https://ja.wikipedia.org/wiki/WebGL

つまり、WebGLという技術を用いれば、web上に3Dモデルを描画することができます。

そして、Three.jsはそのWebGLをラッパしたJavascriptのライブラリであり、数あるWebGLライブラリの中で最も人気のあるライブラリとなっております。

例えば、このWebGL, Three.js が使われているWebサイトにはこんなものがあります。

参考:https://www.updata.one/

スクロールやカーソル移動をすると、web上の物体がニョロニョロ動いてとても面白いです。

参考: https://www.nissin.com/jp/

こちらは日清食品グループのホームページで、商品が螺旋状にグルグル回っていてかなりユニークで表現力のあるWebサイトとなっています。

Three.jsの概念を理解しよう

今回の開発で扱うWebGLライブラリであるThree.jsについて、Web上にその3Dの空間を生み出す概念に、カメラ、シーン、レンダラーというものがあります。

カメラ
我々がよく想像するあのカメラですね。物体を写すものになります。

シーン
撮られるところ。劇場とかでいうところのステージ(舞台)にあたります。

レンダラー
カメラで撮ったものをWeb上に映し出すための変換器。

上記をまとめると、シーン(舞台)をカメラで撮影し、その撮影したものをレンダラーという変換器を通してWeb上に写し出します。

オブジェクトを作成するためには

3D空間ができたらその空間に 物体(オブジェクト) を作りましょう。オブジェクトを作成するためには以下の3つのステップを踏みます。

  1. ジオメトリの作成

  2. マテリアルの作成

  3. メッシュの作成

ジオメトリは、オブジェクトの形状、骨格を表すものです。ジオメトリを作成することでどんなオブジェクトを作るのかを決定します。

次のマテリアルは、オブジェクトの色やマッピングする画像などを設定できます。マテリアルを作成することで、オブジェクトのデザインを決定します。

最後にメッシュ。ジオメトリとマテリアルを組み合わせたものです。

ここで初めて作りたいオブジェクトが生成されるイメージです。

Web上に3Dの地球を映してみよう

いよいよ実践編です。

上記の概念をもとにWeb上に3Dの地球を実際に描画していきます

1. 環境構築、Viteの導入

Three.jsはjavascriptですので、node.jsの環境構築が事前に必要です。

プロゲートの提供する記事を参考にnodeの環境を構築します。

また、今回WebGLを動かすためのビルドツールとしてViteを使います。

Viteを用いることでローカルでブラウザ表示するまでのセットアップを簡単に行うことができます。

以下の手順でローカルにViteを導入しましょう。

terminal setup % npm create vite@latest // viteの導入コマンド

Need to install the following packages:
  create-vite@3.1.0
Ok to proceed? (y) y
✔ Project name: … daae-3d-earth // プロジェクト名の設定
✔ Select a framework: › Vanilla // vanillaを選択
✔ Select a variant: › JavaScript // JavaScriptを選択

Scaffolding project in /Users/username/Desktop/threeJsWeb/setup/daae-3d-earth...

続いて以下のコマンドを実行し、Three.jsのインストールとローカルサーバーを起動してみます。

 cd daae-3d-earth
 npm install
 npm install --save three
 npm run dev

npm run dev実行後、下記のようなページが表示できればセットアップ完了です。

2. まずは球体をWeb上に表示しよう

1のセットアップが完了したらいよいよWeb上に3Dの物体を映し出してみましょう。

main.jsに下記のコードを追加します。

// main.js
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// シーンの作成
const scene = new THREE.Scene();
// カメラの作成
const camera = new THREE.PerspectiveCamera(
  75,
  innerWidth / innerHeight,
  0.1,
  1000
);
camera.position.set(1, 1, 10);

// カメラ制御の設定
const controls = new OrbitControls(camera, document.querySelector("canvas"));
controls.enableDamping = true;

const sizes = {
  width: innerWidth,
  height: innerHeight,
};

// レンダラーの作成
const renderer = new THREE.WebGLRenderer({
  antialias: true, // 地球の画質をよくする
  canvas: document.querySelector("canvas"),
});
renderer.setSize(sizes.width, sizes.height);
// 地球の画質をよくする
renderer.setPixelRatio(window.devicePixelRatio);

// ジオメトリの作成
const sphereGeometry = new THREE.SphereGeometry(5, 50, 50);
// マテリアルの作成
const sphereMaterial = new THREE.MeshNormalMaterial({
  wireframe: true,
});
// メッシュの作成
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
// シーンに追加する。
scene.add(sphere);

// ワンフレーム毎に更新する関数をそれぞれ実行する。
function animate() {
  renderer.render(scene, camera);
  controls.update();
  requestAnimationFrame(animate);
}
animate();

// リサイズされる度に更新する関数を実行する。
addEventListener("resize", () => {
  // サイズを更新する
  sizes.width = innerWidth;
  sizes.height = innerHeight;

  // カメラを更新する
  camera.aspect = sizes.width / sizes.height;
  camera.updateProjectionMatrix();

  // レンダラーを更新する
  renderer.setSize(sizes.width, sizes.height);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});

Three.js では何か物体を表示するときに基本的に上のコードが雛形になります。

  1. シーンの作成

  2. カメラの作成

  3. レンダラーの作成

  4. オブジェクト(ジオメトリ、マテリアル、メッシュ)の作成

  5. アニメーション設定

  6. リサイズ設定

上記の手順以外に、今回はカメラの制御を追加しました。

// main.js
// カメラ制御の設定
const controls = new OrbitControls(camera, document.querySelector("canvas"));
controls.enableDamping = true;

OrbitControlsを設定することにより、 スクロールや、カーソル移動でカメラの向きを変えて立体を回転させたり、 拡大、縮小ができるようになります

camera.position.setのように、position.setを呼び出すことにより、カメラやオブジェクトのx座標、y座標、z座標を設定することができます。

手順の1 ~ 3は「Three.jsの概念を理解しよう」で説明した空間の作成になります。

手順4にて、簡単な球体を作成してます。 今回は、SphereGeometryという球体に、MeshNormalMaterialを適用しています。 SphereGeometryについては公式ページでもいじりながら確認することができます。

公式ドキュメント: SphereGeometry

手順5はanimate()を追加してアニメーションの設定をしています。

requestAnimationFrameで毎フレーム実行したい関数を設定します。

今回はレンダラーの更新と、Controlsの更新を加えています。

手順6の「リサイズの設定」では、画面のサイズを変更しても、オブジェクトのサイズ比を一定に保つようにしています。

これにより、画面を縮めてもオブジェクトの表示を一定にすることができます。

jsの作成が終わったら、html, cssをちょっとだけ修正します。

style.cssはbody, canvas要素のmargin, paddingを0にしてweb上の余白をなくしておきます。

// style.css
body {
  margin: 0;
  width: 100%;
  height: 100%;
}
canvas {
  margin: 0;
  width: 100%;
  height: 100%;
}

index.htmlにはcanvas要素、style.cssを追加しておきましょう。

// index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="style.css" /> ← 追加
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <canvas id="canvasContainer"></canvas> ← 追加
    <script type="module" src="/main.js"></script>
  </body>
</html>

ここまでできたら球体のオブジェクトがWeb上に反映されるはずです。

次にマテリアルをもう少しカスタマイズして地球を映し出します。

3.球体に地球の画像をマッピングしよう

球体に地球をマッピングさせて回転させるところまで実装します。

まずは、地球のuvマップを取得します。

今回は、Solar Texturesという衛星のフリーモデルを提供するサイトから地球の画像を取得しました。

Google の画像検索で「earth uv map」と検索でも出てきます。

シェーダーについて

上記に示した地球には、太陽からの光が重なるような表現を組み込みました。

これはシェーダーという技術と組み合わせることで実現できます。

こちらの説明もまずはwikiを見てみましょう。

シェーダー(英: shader)とは、3次元コンピュータグラフィックスにおいて、シェーディング(陰影処理)を行うコンピュータプログラムのこと。

https://ja.wikipedia.org/wiki/シェーダー

つまりシェーダー技術を用いることでオブジェクトのグラデーションなどを表現できます

このシェーダーGPU上で動いています。 GPUの詳しい説明についてはこちらを参照するとわかりやすいです。

Three.jsはCPU上で動作するJavaScriptで書いていくのですが、

シェーダーGPU上で動かすためにGLSL言語で別途コーディングを行う必要があります。

シェーダーを理解するのはとても難しい(僕も正直まだまだ理解できていない)ので、コードだけ追記していきましょう。

シェーダーのコードをindex.htmlに追記します。

// index.html
<script id="vertex" type="x-shader/x-vertex">
      varying vec2 vertexUV;
      varying vec3 vertexNormal;

      void main() {
          vertexUV = uv;
          vertexNormal = normalize(normalMatrix * normal);
          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      }
    </script>

    <script id="fragment" type="x-shader/x-fragment">
      uniform sampler2D globeTexture;
      varying vec2 vertexUV;
      void main() {
          gl_FragColor = vec4(texture2D(globeTexture, vertexUV).xyz, 1.0);
      }
    </script>

    <script id="atmosphereVertex" type="x-shader/x-vertex">
      varying vec3 vertexNormal;
      void main() {
          vertexNormal = normalize(normalMatrix * normal);
          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 0.9);
      }
    </script>

    <script id="atmosphereFragment" type="x-shader/x-fragment">
      varying vec3 vertexNormal;
      void main() {
        float intensity = pow(0.5 - dot(vertexNormal, vec3(-0.4, -0.2, 1.0)), 9.5);
        gl_FragColor = vec4(0.5, 0.353, 0.3, 1.0) * intensity;
      }
    </script>

シェーダーは2つの役割に分かれます。

  • vertexShader: 頂点シェーダー。例えば三角形を表現したい場合、三つの頂点座標を定義する。

  • fragmentShader: vertexShaderで決めた頂点内部の色などをカスタマイズする。各頂点で異なる色を設定した場合それらの色が合わさるところを補完する機能などもある。

vertexShaderで頂点座標を決定し、fragmentShaderで内部に色を塗っていきます。

マテリアルにシェーダーを組み込む

ShaderMaterialを使うことでマテリアルにシェーダーを組み込むことができます。 ShaderMaterialを使うことでマテリアルにShaderを組み込むことができます。 各パラメーターの説明については公式ドキュメント:ShaderMaterialを参考にしてください。

sphereMaterialの更新、atmosphereGeometry、atmosphreMaterialを追加していきます。

// main.js
const sphereGeometry = new THREE.SphereGeometry(5, 50, 50);
const sphereMaterial = new THREE.ShaderMaterial({
  vertexShader: document.getElementById("vertex").textContent,
  fragmentShader: document.getElementById("fragment").textContent,
  uniforms: {
    globeTexture: {
      value: new THREE.TextureLoader().load("https://threejs-earth.s3.ap-northeast-1.amazonaws.com/earth.jpeg")
    },
  },
});
// 球体の作成
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
scene.add(sphere);
// 球体の枠をシェーダーで強調する
const atmosphere = new THREE.Mesh(
  new THREE.SphereGeometry(5, 50, 50),
  new THREE.ShaderMaterial({
    vertexShader: document.getElementById("atmosphereVertex").textContent,
    fragmentShader: document.getElementById("atmosphereFragment").textContent,
    blending: THREE.AdditiveBlending,
    side: THREE.BackSide,
  })
);
atmosphere.scale.set(1.0, 1.0, 1.0);

scene.add(atmosphere);

ここまでで、3Dでリアリティのある地球がWeb上に表示されているはずです。

次回の章で、地球、星、月、太陽からなる宇宙を完成させましょう。

4. 地球、星、月、太陽からなる宇宙を実現する。

今までの章で説明した実装を応用して地球、月、太陽からなる衛星を実現させます。

まずは、星を散りばめて、宇宙を表現します。大量の星を生成するコードを追記します。

// main.js
const starGeometry = new THREE.BufferGeometry();
const starMaterial = new THREE.PointsMaterial({
  color: 0xffffff,
});
const starCount = 12000;
const starPositionArray = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
  starPositionArray[i] = (Math.random() - 0.5) * 1000;
}
starGeometry.setAttribute(
  "position",
  new THREE.Float32BufferAttribute(starPositionArray, 3)
);
const stars = new THREE.Points(starGeometry, starMaterial);
scene.add(stars);

大量のオブジェクトを作成するためにBufferGeometryを使います。

starVerticesにxyz座標のセットの情報が10000個以上入った配列を作成します。

Float32BufferAttributeで10000個以上の星の頂点情報を作り、BufferGeometryの位置情報に格納していきます。

今回メッシュはTHREE.Pointsを使用するため、マテリアルはPointsMaterialにします。

ここまでで、大量の星が散りばめられ宇宙空間を表現できました。

次に太陽を表現します。

ここでは、ライトを太陽に見立てて表現していきましょう。

以下のコードを追記します。

// main.js
// 太陽から降り注ぐ光を表現
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(5,3,5);// 光の向き
scene.add(dirLight);
// 太陽光(赤色)を表現
const pointLight = new THREE.PointLight(0xff4000,0.5, 100)
pointLight.position.set(15,15,40);// 光の向き
scene.add(pointLight);
const pointLightHelper = new THREE.PointLightHelper( pointLight );
scene.add( pointLightHelper );

ライトはその名の通り、光を表現するTHREEライブラリのメソッドです。

ライトには様々な種類が用意されており、今回はその内のDirectionalLight(平行光源:特定の方向に放射される光)とPointLight(豆電球のように放射される光)を用いてます。

また、ライト自体はweb上で確認することはできないので、PointLightHelperのようなヘルパー関数を使うことで、ライトの位置を確認することができます。

次に月を表現します。

ここでも、Solar Texturesにある月のmapを使わせてもらいました。

以下が月の実装ですが、地球の実装とほぼ同じのため、説明は省きます。

// main.js
// 月を作る
const moonGeometry = new THREE.SphereGeometry(2, 10, 10);
const moonTxLoader = new THREE.TextureLoader();
const moonMaterial = new THREE.MeshPhongMaterial({
		color:0xffffff,
		map: moonTxLoader.load("https://threejs-earth.s3.ap-northeast-1.amazonaws.com/2k_moon.jpeg")
	});
const moon = new THREE.Mesh(moonGeometry, moonMaterial)
scene.add(moon);

月は、地球の周りを公転してます。

そのため、以下のコードを追記して、表現します。

// main.js
let rot = 0;
function animate() {
  rot += 0.005; // 毎フレーム角度を0.2度ずつ足していく
  // ラジアンに変換する
  const radian = (rot * Math.PI) / 180;
  moon.rotation.y += 0.002;
  // 月の円運動を実現
  moon.position.x = 20 * Math.cos(rot);
  moon.position.z =20 * Math.sin(rot);
...
}

ここは若干数学の知識を必要とするので少々複雑になりますが、物体の位置(position)に対して、sin, cosを適用してあげることで円運動が実装できます。

今回は、x座標にcos, z座標にsin関数を設定することによって、月を水平に円運動で動かすことができます

rotは動きの幅を表すものなのでここの数字を大きくすればするほど円運動の速度は早くなります(ワンフレーム毎の移動角度が大きくなるので)。

これでWeb上に小さい宇宙空間を表現することができました🎉🎉

まとめ

元々Githubのトップページがとても面白い作りになってて「これ何の技術が使われてるんだろう??」と思いWebGLを学んだのがきっかけでした。

いざやってみるとカメラシーンレンダラーといった今まで知らなかった新しい概念を学ぶことができてとても面白かったです。

シェーダー(GLSL言語) はちょっとクセがあって難しかったのでまだまだ勉強が必要だなと思いました😅

最後にここまで読んでくださりありがとうございました!🙇‍♂️

参考URL


その他の記事

\もっと身近にもっとリアルに!DAAE公式Twitter/



執筆者プロフィール: Kensho Tanaka
21年新卒としてSHIFT DAAE部に配属。
1人でもwebサービスをリリースできるようなエンジニアになるために日々フルスタックな技術習得に励んでいます。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/

みんなにも読んでほしいですか?

オススメした記事はフォロワーのタイムラインに表示されます!