ブラウザ上で2Dや3Dのコンピュータグラフィックを扱う場合、WebGLを用いてレンダリングするらしいですが、今回はそれを手軽に扱えるJavaScriptライブラリ「Three.js」の本を読んだので備忘録としてまとめておきます。

上手く扱うことができればブラウザ上でGPU性能を引き出してゲームなどを作成、動作させたり、洒落たWebページをデザインすることができるようです。
WebGLはウェブ標準に完全に統合されているため、ウェブページのcanvas要素上でGPUアクセラレータを使用した物理シミュレーション、画像処理、画像効果などを表現できる。WebGLの要素は、外側のHTMLと組み合わせたり、ページやページの背景の他のパーツと合成して使用できる。
引用:Wikipedia

メタバース事業に注目が集まる中、ブラウザで手軽に導入できるバーチャル空間を構築するなどの取り組みにも応用範囲が広そうです。

▼Three.jsを用いて作成された作品例


モチベーションを少しだけ上げたところで、始めていきます。


前提知識:
  • HTML記述少しわかる
  • 何かしらの手続き型/命令型言語少しわかる(for,if..など)

使うもの:
  • HTML
  • JavaScript
  • ブラウザ(Google Chrome等)


最終目標確認


最初に、本記事で取り上げるThree.jsの手順のみで作成できる簡単な例を紹介しておきます。

See the Pen Untitled by sukima-log (@sukima-log) on CodePen.




表示の確認


最初は、灰色のプレートが浮かんでいるだけの空間が描画されますが、右上のプロパティ一覧から「addcube」をクリックすることで回転する立方体を生成、追加することができます。


フレームレート表示

ここには、フレームレート(1秒間の描画枚数)が表示されています。表示は切り替えが可能で、フレームレート(fps)もしくは、1回クリックで1フレームを描画するのにかかる時間(ms)になります。大体、60fpsの時は約17msくらいです。これは、表示させないことも可能です。


コントロールパネル

インタラクティブ(対話的)な操作によって描画に変化、効果を与えられるプロパティのインタラクティブコントロール部が表示されます。これは、必要なければ表示させないことも可能です。

それぞれの項目は以下の通り
rotationSpeed立方体の回転速度
addCube立方体を1つ追加
removeCube立方体を1つ削除
removeAllCubeすべての立方体を削除
outputObjectコンソール(F12)にオブジェクトを出力
numberOfObjectsオブジェクト数を表示(初期値4)




Three.jsを使う事前準備


まずJavaScriptライブラリ「Three.js」の中身を記述していくための基本の「型」を用意します。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <!-- CDNからThree.js読み込み -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.js"></script>
  </head>
  <body>
    <!-- (表示) 出力を保持する -->
    <div id="WebGL-output"></div>

    <!-- Three.jsコードの記述 -->
    <script type="text/javascript">
      // レンダラーを定義
      var renderer;

      function init() {
        renderer = new THREE.WebGLRenderer();
        // レンダラーの出力をhtmlに追加する
        document.getElementById("WebGL-output").appendChild(renderer.domElement);
      }

      // 表示が終わってからThree.js関連処理(関数init)を実行
      window.addEventListener("load", init());
    </script>
  </body>
</html>
中身を見ていくと、まず5行目<head>タグ内でthree.jsを使用するため、CDN(コンテンツ・デリバリ・ネットワーク)からthree.jsを読み込んでいます。npm(Node.jsパッケージ管理システム)からローカルにインストールしてそこからインポートするほうが一般的かもしれませんが、環境構築の手間を省くためこのようにしました。

今回はこちらを利用しました。

次に<body>タグ内は主に2つに分けられており、9行目の描画内容を保持するためのHTMLタグと、12~24行目のJavaScript記述部分です。後半、JavaScript記述部分ではレンダラーを定義しています。
レンダラーカメラとシーンを用意すると、それをもとにカメラからシーンがどう見えるのか計算してくれる
当然、今のままではカメラもシーンも定義していないので何も描画されません。カメラやシーンは次の章で説明します。ここでは、なにも描画されませんが19行目でレンダラーの出力を9行目の位置に割り当てています。

19行目では、HTMLタグで指定したID("webGL-output")位置の要素を取得(getElementByID)し、DOM(Document Object Model)ツリー構造内の子ノードリストの末尾にレンダラーの要素(ノード)を追加(appendChild)しています。このようにすることで、HTMLに描画を追加しているようです。
781~810_comp
▲おそらくこんな感じ



3Dオブジェクトを表示する


それでは、上記の基本構成を用いてブラウザ上に3Dオブジェクトを描画します。
まず3D空間を作るために以下をコードに追加します。
  • シーン(scene):
    • オブジェクトを配置する撮影スタジオ、ここに全てのオブジェクトを追加していく
  • カメラ(camera):
    • シーン上のオブジェクトを映すいわゆるカメラ、向きや視野の要素を持つ
  • ライト(Light):
    • スタジオを照らすライトがないと真っ暗でカメラに何も映らない
  • レンダラー(renderer):
    • 上記でも述べた通り、カメラとシーンなどから描画内容を計算して表示する
以上を前章の型に追加したコードが以下の通りです。<script>タグ内のみの表示になります
<script type="text/javascript">
      var scene; // シーン定義
      var camera; // カメラ定義
      var renderer; // レンダラー定義

      function init() {
        /* シーン作成 */
        scene = new THREE.Scene();

        /* カメラ作成 */
        camera = new THREE.PerspectiveCamera(
          45, // 視野
          window.innerWidth / window.innerHeight, // アスペクト
          0.1, // どの程度のカメラ距離から描画を始めるか
          1000 // どのくらい遠くまで見えるか
        );
        scene.add(camera);

        // カメラのポジションと向きを決定
        camera.position.x = -30;
        camera.position.y = 40;
        camera.position.z = 30;
        camera.lookAt(scene.position); // シーンの中心にカメラを向ける

        /* レンダラー作成 */
        renderer = new THREE.WebGLRenderer();
        renderer.setClearColor(new THREE.Color(0xeeeeee));
        renderer.setSize(window.innerWidth, window.innerHeight);

        /* ライト作成 */
        // 均等光源(影ができない)
        var ambientLight = new THREE.AmbientLight(0x0c0c0c);
        scene.add(ambientLight);
        // 点座標への光源(影ができる)
        var spotLight = new THREE.SpotLight(0xffffff);
        spotLight.position.set(-20, 30, -5);
        spotLight.castShadow = true; // 影を落とす
        scene.add(spotLight);

        // レンダラーの出力をhtmlに追加する
        document.getElementById("WebGL-output").appendChild(renderer.domElement);

        // シーンを描画
        render();
        // シーンを描画する関数
        function render() {
          // 表示
          renderer.render(scene, camera);
          // レンダリング(再帰)
          requestAnimationFrame(render);
        }
      }

      // 表示が終わってからThree.js関連処理(関数init)を実行
      window.addEventListener("load", init());
</script>

44行目49行目などの「scene.add()」で作成したシーンに対してカメラや各種光源を追加していることがわかります。また、カメラやライトはシーン内のどこに配置するか「.position」で設定しています。

55行目で呼び出している関数render内の動作については、次章で詳しく説明しますが、59行目でレンダラーにシーンとカメラを適用していることがここでは重要です。

このままでは、なにもない空間が映し出されるだけなのでこのシーン上にカメラで捉えるためのオブジェクトを追加していきます。

オブジェクトを描画するには以下を追加します。
  • ジオメトリ(Geometry):
    • 描画対象の形状情報のみ、線や面、頂点などの骨組み情報
  • マテリアル(Material):
    • 描画対象の質感とか色の情報
  • メッシュ(Mesh):
    • ジオメトリ+マテリアル。本記事でオブジェクトと呼んでるもの
これらはこのように追加します。
/* 球体作成 */
var sphereGeometry = new THREE.SphereGeometry(9, 20, 20);
var sphereMaterial = new THREE.MeshLambertMaterial({ color: 0x1327ff });
var sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);

// 球体ポジション等
sphere.position.x = 0;
sphere.position.y = 0;
sphere.position.z = 0;
sphere.castShadow = true;

// オブジェクトをシーンに追加
scene.add(sphere);
これらを上記コードに追加すると以下のように球体(sphere)が描画されます。

See the Pen Untitled by sukima-log (@sukima-log) on CodePen.






3Dオブジェクトに動きをつける


次に、上記で作成したオブジェクトに動きをつけていきます。

オブジェクトを動作させるには、これまで触れていなかった関数render()内に処理を記述していきます。ここでは、render()周辺を以下のように書き換えます。
// シーンを描画
render();
var step = 0;

// シーンを描画する関数
function render() {
  // 球体を動かす
  step += 0.02;
  sphere.position.x = 0 + 10 * Math.cos(step);
  sphere.position.y = -10 + 15 * Math.abs(Math.sin(step * Math.PI));
  // 表示
  renderer.render(scene, camera);
  // レンダリング(再帰)
  requestAnimationFrame(render);
}
処理自体は単純で、14行目の「requestAnimationFrame」で関数render()を再帰的に読み出し、8~10行目で球体のポジションを更新し続けています。

ここで、12行目の「renderer.render()」と、自作関数のrender()は同名なだけで関係ありません。

「requestAnimationFrame」を用いた描画は有能らしく、これまで「setInterval」や「setTimeout」を再帰的に読みだして描画を更新していたらしいですが、実行間隔などはブラウザ任せになった分、タブを離れると裏での実行が止まるためCPU等のリソースの無駄が少なくなったそうです。

上記の記述をさらに追加したものが以下になります。

See the Pen Untitled by sukima-log (@sukima-log) on CodePen.






インタラクティブ制御機構追加


最後に動的に3D空間内のオブジェクト等の設定を変更するための、インタラクティブな制御部を追加します。

まず、<head>タグ内に以下を追加して、CDNからdat.gui.jsを読み込みます。
<!-- プロパティのインタラクティブ調整バー描画 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.9/dat.gui.js"></script>
続いてここでは、球体の動作速度とマテリアル色を可変要素に指定するためにJavaScript記述欄に以下を追加します。
/* インタラクティブコントロール部 */
var controls = new (function () {
  this.bouncingSpeed = 0.03;
  this.color = sphereMaterial.color.getStyle();
})();
// 表示設定
var gui = new dat.GUI();
gui.add(controls, "bouncingSpeed", 0, 0.5);
gui.addColor(controls, "color").onChange(function (e) {
  sphereMaterial.color.setStyle(e);
});
2~5行目で、可変対象の要素を定義し、7~11行目で制御部の表示を決定します。色の要素はフレームごとに切り替わるわけではないので、ここで実際の値に割り当てます。

動作速度に関しては、8行目でまだ実際動作に関係する値に割り当てられていないため、render関数内の記述を以下のように書き換えて、球体のバウンド速度を割り当てます。
// シーンを描画する関数
function render() {
  // 球体を動かす
  step += controls.bouncingSpeed;
  sphere.position.x = 0 + 10 * Math.cos(step);
  sphere.position.y = -10 + 15 * Math.abs(Math.sin(step * Math.PI));
  // 表示
  renderer.render(scene, camera);
  // レンダリング(再帰)
  requestAnimationFrame(render);
}
上の章のコードと見比べると、4行目に定数ではなく、変数が代入されています。

最終的な出力はこのようになります。右上Controlsから球体の動作速度と色のプロパティを変更することが可能です。

See the Pen Untitled by sukima-log (@sukima-log) on CodePen.






Three.jsの基礎まとめ


ここまでの内容のまとめとして、1章で示した完成品のコードを置いておきます。

上記までで説明していない、表示を整えるリサイズイベント等ありますが、できる限りコメントを挿入したので、コードを上から見ていけばわかると思います。
<!DOCTYPE html>
<html lang="ja">
  <head>
    <title>Add Remove Cube</title>
    <!-- CDNからThree.js読み込み -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <!-- フレームレートの描画 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/stats.js/10/Stats.js"></script>
    <!-- プロパティのインタラクティブ調整バー描画 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.9/dat.gui.min.js"></script>
    <style>
      body {
        /*ページ全体を使用するためmarginを0に設定
            overflowをhiddenに設定*/
        margin: 0;
        overflow: hidden;
      }
    </style>
  </head>
  <body>
    <!-- フレームレート表示 -->
    <div id="Stats-output"></div>
    <!-- (表示) 出力を保持する -->
    <div id="WebGL-output"></div>

    <!-- Three.jsコードの記述 -->
    <script type="text/javascript">
      var camera; // カメラ定義
      var scene; // シーン定義
      var renderer; // レンダラー定義

      /* すべての読み込みが終わってからThree.js関連の処理を実行 */
      function init() {
        var stats = initStats(); // フレームレートなど表示

        // オブジェクト、カメラ、ライトなどすべての要素を格納するシーン作成
        scene = new THREE.Scene();
        scene.fog = new THREE.FogExp2(0xffffff, 0.005); // 遠くの物が霞んで見える設定
        // カメラ設定
        camera = new THREE.PerspectiveCamera(
          45, // 視野
          window.innerWidth / window.innerHeight, // アスペクト
          0.1, // どの程度のカメラの距離から描画を始めるか
          1000 // どのくらい遠くまで見えるか
        );
        scene.add(camera); // カメラをシーンに追加

        // Cameraに基づいてブラウザ内でどのように見えるか計算
        renderer = new THREE.WebGLRenderer();
        renderer.setClearColor(new THREE.Color(0xeeeeee)); // レンダラー初期値決定
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.shadowMap.enabled = true; // オブジェクトシャドウを有効にする

        // 座標軸をシーンに追加
        // var axes = new THREE.AxisHelper(20);
        // scene.add(axes);

        // 平面作成
        var planeGeometry = new THREE.PlaneGeometry(60, 40, 1, 1); // width:60, height:40
        var planeMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff });
        var plane = new THREE.Mesh(planeGeometry, planeMaterial);
        plane.receiveShadow = true; // 影を受ける

        // 回転と位置決め
        plane.rotation.x = -0.5 * Math.PI;
        plane.position.x = 0;
        plane.position.y = 0;
        plane.position.z = 0;

        // シーンに平面を追加
        scene.add(plane);

        // シーンの中心にカメラを向ける
        camera.position.x = -30;
        camera.position.y = 40;
        camera.position.z = 30;
        camera.lookAt(scene.position);

        // 均等光源
        var ambientLight = new THREE.AmbientLight(0x0c0c0c);
        scene.add(ambientLight);

        // 光源をシーンに追加
        var spotLight = new THREE.SpotLight(0xffffff);
        spotLight.position.set(-20, 30, -5);
        spotLight.castShadow = true; // 影を落とす
        scene.add(spotLight);
        // レンダラーの出力をhtmlの(WebGL-output)に追加
        document
          .getElementById("WebGL-output")
          .appendChild(renderer.domElement);

        // インタラクティブに調整するプロパティ設定
        var controls = new (function () {
          this.rotationSpeed = 0.02; // 回転速度
          this.numberOfObjects = scene.children.length; // 現在のオブジェクト数
          // キューブを1つ削除する関数
          this.removeCube = function () {
            var allChildren = scene.children; // 子要素をすべて取得
            var lastObject = allChildren[allChildren.length - 1]; // 最後に追加したオブジェクト取得
            // planeでなければキューブを削除
            if (lastObject instanceof THREE.Mesh && lastObject != plane) {
              scene.remove(lastObject);
              // 個数の更新
              this.numberOfObjects = scene.children.length;
            }
          };
          // すべての追加したキューブを一括削除する関数
          this.removeAllCube = function () {
            var allChildren = scene.children;
            var iter = allChildren.length;
            for (var i = iter; i >= 0; --i) {
              var lastObject = allChildren[i];
              if (lastObject instanceof THREE.Mesh && lastObject != plane) {
                scene.remove(lastObject);
              }
            }
            this.numberOfObjects = scene.children.length;
          };
          // キューブを追加する関数
          this.addCube = function () {
            var cubeSize = Math.ceil(Math.random() * 3); // キューブサイズ決定
            var cubeGeometry = new THREE.BoxGeometry( // 正方形のジオメトリ作成
              cubeSize,
              cubeSize,
              cubeSize
            );
            var cubeMaterial = new THREE.MeshLambertMaterial({
              color: Math.random() * 0xffffff, // キューブ色決定
            });
            // キューブメッシュ作成
            var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
            cube.castShadow = true; // 影を落とす
            cube.name = "cube-" + scene.children.length; // メッシュ名称
            // ポジション決定
            cube.position.x =
              -30 + Math.round(Math.random() * planeGeometry.parameters.width);
            cube.position.y = Math.round(Math.random() * 5);
            cube.position.z =
              -20 + Math.round(Math.random() * planeGeometry.parameters.height);
            // シーンにキューブを追加
            scene.add(cube);
            // シーン内オブジェクトの個数更新
            this.numberOfObjects = scene.children.length;
          };
          // シーン内のオブジェクトをコンソール出力
          this.outputObjects = function () {
            console.log(scene.children);
          };
        })();
        // コントロールパネル設定
        var gui = new dat.GUI();
        gui.add(controls, "rotationSpeed", 0, 0.5); // 値の範囲0~0.5
        gui.add(controls, "addCube");
        gui.add(controls, "removeCube");
        gui.add(controls, "removeAllCube");
        gui.add(controls, "outputObjects");
        gui.add(controls, "numberOfObjects").listen();

        // シーンを描画
        render();
        // シーンを描画する関数
        function render() {
          stats.update();

          // 立方体を軸周りに回転
          // traverse : sceneに配置されたすべての子要素や孫要素を
          // 引数として受け取る
          scene.traverse(function (obj) {
            if (obj instanceof THREE.Mesh && obj != plane) {
              obj.rotation.x += controls.rotationSpeed;
              obj.rotation.y += controls.rotationSpeed;
              obj.rotation.z += controls.rotationSpeed;
            }
          });
          // レンダリング(再帰)
          requestAnimationFrame(render);
          // 表示
          renderer.render(scene, camera);
        }
        // フレームレート(fps) or 1フレーム描画にかかる時間(ms)表示
        function initStats() {
          var stats = new Stats();

          stats.setMode(0); // 0:fps, 1:ms

          // 位置設定
          stats.domElement.style.position = "absolute";
          stats.domElement.style.left = "0px";
          stats.domElement.style.top = "0px";
          // 出力をhtmlに追加する
          document.getElementById("Stats-output").appendChild(stats.domElement);

          return stats;
        }
      }
      // 表示領域をウィンドウサイズに合わせる
      function onResize() {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
      }
      // 表示が終わってからThree.js関連処理(関数init)を実行
      window.addEventListener("load", init());
      // リサイズイベント
      window.addEventListener("resize", onResize, false);
    </script>
  </body>
</html>

いろいろなものをインストールする環境構築の手間と、ストレージの圧迫を考慮する必要のないブラウザ上で動作するThree.jsは、手軽さから活用の幅が広そうなので今後も遊んでみたいと思います。

Three.jsを用いてBlenderなどを駆使した、以下のような作品もあります。


VRへの展開も可能なようなので、今後の動向を楽しみにしつつ学習していきます。

お疲れさまでした。



参考:
  • 初めてのThree.js 第2版 ―WebGLのためのJavaScript 3Dライブラリ

このエントリーをはてなブックマークに追加
コメントを閉じる

コメント

コメントフォーム
記事の評価
  • リセット
  • リセット