初めまして、キタムラです。
WebGL+Three.jsの入門編ということで、ブラウザ上に簡単な3Dオブジェクトを生成してアニメーションさせてみようと思います。
今回作ってみるのは全人類の夢である、RPGゲームでおなじみ回復ポイント的なアレです。

おおまかにやることは以下4点です。

  • 3D空間を生成(レンダラー、シーン、カメラ、光源)
  • デバッグに便利な環境を作る(XYZ軸のグリッドを表示、カメラ視点の移動をマウス操作で出来るようにする)
  • 複数の三角形を組み合わせて一つの3Dオブジェクトを生成
  • 3Dオブジェクトを回転させるアニメーションをくっつける

ではさっそく準備してみますー。

HTMLファイルの準備

HTMLはシンプルに、

  • Three.js本体の読み込み
  • canvas描画用のdiv

があればOKです。

index.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">

    <!-- Three.js本体 -->
    <script src="three.js"></script>
    <!-- デバッグ用のプラグイン(後述) -->
    <script src="OrbitControls.js"></script>
    <!-- メインの処理を記述するJavaScript -->
    <script src="index.js"></script>

    <!-- canvasのサイズを指定 -->
    <style>
      #canvas-frame {
        width: 500px;
        height: 700px;
      }
    </style>

  </head>

  <body>

    <!-- canvas要素を生成する領域 -->
    <div id="canvas-frame"></div>

  </body>

</html>

OrbitControls.jsは開発中にあると便利なプラグインです。
完成形には必要ないかもしれませんが、とりあえず読み込んでおきます。

Three.jsを使って3Dオブジェクトを表示させる

ここからは実際にThree.jsを使ってぐりぐりJavaScriptを書いていきます。

基本のオブジェクトを生成

レンダラー、シーン生成

var canvas;
var scene;
function initRenderer() {
  // canvasの挿入先要素を取得
  canvas = document.getElementById('canvas-frame');
  // レンダラー生成
  renderer = new THREE.WebGLRenderer({ antialias: true, alpha: 1.0 });
  // レンダラーサイズを指定(width, height)
  renderer.setSize(canvas.clientWidth, canvas.clientHeight);
  // レンダラークリアカラーを設定(hex, alpha)
  renderer.setClearColor(0x000000, 1.0);

  // シーンオブジェクト生成
  scene = new THREE.Scene();

  // DOMcanvasを追加
  canvas.appendChild(renderer.domElement);
}

カメラ生成

var camera;
function initCamera() {
  // カメラ生成(fov, aspect, near, far)
  camera = new THREE.PerspectiveCamera(45, canvas.clientWidth / canvas.clientHeight, 1, 2000);
  // カメラの位置座標(x, y, z)
  camera.position.set(0, 300, -800);
  // カメラのベクトル設定(x, y, z)
  camera.up.set(0, 1, 0);
  // カメラの中心座標設定(x, y, z)
  camera.lookAt({ x: 0, y: 0, z: 0 });
}

カメラにはいくつか種類があるようですが、今回は「透視投影」方式のカメラを使用しました。
透視投影方式は通常の人間の見え方と同じで、手前のものを大きく、遠くのものを小さく表示する方式です。
Three.jsのPerspectiveCameraクラスを使用します。

PerspectiveCameraの引数
  • fov: 視野角
  • aspect: アスペクト比
  • near: カメラから視界の手前までの距離
  • far: カメラから視界の奥までの距離

光源生成

function initLight() {
  var _directionalLight;
  var _ambientLight

  // 平行光源生成(hex, 光源強度)
  _directionalLight = new THREE.DirectionalLight(0xFFFFFF, 0.8);
  // 平行光源の位置座標(x, y, z)
  _directionalLight.position.set(400, 200, -800);
  // 平行光源をシーンへ追加
  scene.add(_directionalLight);

  // 環境光生成(hex)
  _ambientLight = new THREE.AmbientLight(0x999999);
  // 環境光をシーンへ追加
  scene.add(_ambientLight);
}

光源にもいくつが種類があり、今回は「平行光源」と「環境光」を使用しました。

平行光源

平行光源は現実世界の太陽のようなもので、無限に遠いところから降り注ぐ光源という意味で「無限遠光源」とも呼ばれているそうです。

平行光源参考画像

指定した光源位置座標から照射する対象オブジェクトの中心(対象オブジェクトが指定されていない場合は原点(0, 0, 0))への角度と平行に照らされます。

環境光

環境光は3D空間を均等に照らす光です。
これを設定することで平行光源で照らされていないオブジェクトの裏側などにも光が当たり、うっすらと表示されるようになります。

レンダリング用の関数を作成

function draw() {
  // レンダリング
  renderer.render(scene, camera);
}

このdraw()関数を呼び出せばレンダリング出来るようにしておきます。

デバッグ用のガイドとプラグインを使ってみる

var camera_control;
function initDebugMode() {
  var _axis;
  // OrbitControls.jsを使ってマウスによる視点移動を可能にする
  //  これを使用中はカメラのlookAt関数が無効になります
  camera_control = new THREE.OrbitControls(camera, canvas);
  // XYZのガイドを生成(ガイドの長さ)
  _axis = new THREE.AxisHelper(500);
  // ガイドの位置座標
  _axis.position.set(0, 0, 0);
  // ガイドをシーンへ追加
  scene.add(_axis);

  // デバッグ用描画処理
  debugDraw();
}
// デバッグ用に別途でレンダリング関数を用意
function debugDraw() {
  // レンダリング
  renderer.render(scene, camera);
  // レンダリング処理をループで呼び出す
  requestAnimationFrame(debugDraw);
}

対応ブラウザが限られているためあまり使われる機会の少ない「requestAnimationFrame」関数ですが、
Three.jsでは未対応ブラウザの場合setTimeoutで同じような挙動をする処理を上書いているので気にせず使います。

ここまでの処理をすべて呼び出せば、3Dオブジェクトを表示させるための土台は完成です。

window.onload = function() {
  // initRendererは一番最初に呼び出す
  initRenderer();

  initCamera();
  initLight();

  // 開発中のみ使用
  initDebugMode();
}

このようにデバッグ用の記述で描画したガイドが表示されます。

土台完成図

赤がX軸、緑がY軸、青がZ軸です。
マウス操作による視点移動の処理を追加していますので、ドラッグしたりマウスホイールをぐりぐりしてみてください。
3D空間になっているのがわかるかと思います。

3Dオブジェクトを生成

ここまでは3Dオブジェクトを表示させるための土台を作りました。
ここから先は、実際に3Dオブジェクトを表示させてみます。

立方体を生成

まず試しに、簡単に作れる立方体を表示させてみます。

function createCube() {
  var _geometry;
  var _material;
  var _cube;

  // 形状オブジェクトを生成(width, height, depth)
  _geometry = new THREE.CubeGeometry(100, 100, 100);
  // 材質オブジェクト生成
  _material = new THREE.MeshLambertMaterial({ color: 0xFF0000,  });
  // 立方体オブジェクト生成
  _cube = new THREE.Mesh(_geometry, _material);

  // 立方体オブジェクトをシーンに追加
  scene.add(_cube);
}
形状オブジェクト

立方体を表示するにはThree.jsの「CubeGeometry」関数を使用します。
他にも球や円柱、トーラスなどThree.jsであらかじめ用意されている形状オブジェクトがあります。

材質オブジェクト

オブジェクトの表面を表現する材質も複数種類があり、今回は「ランバート反射モデル」を使用しました。
ランバート反射モデルはカメラ位置によって材質の明るさが変わらないという特徴があり、比較的低負荷のようです。
引数で渡している「color」は実際に表示される色とは少し異なり、光源色によって変化します。

  • R = 光源色のR値 × 材質色のR値
  • G = 光源色のG値 × 材質色のG値
  • B = 光源色のB値 × 材質色のB値

立方体表示

onload内にcreateCube()関数を呼び出してこのように立方体が表示されたら成功です。
この状態でカメラや光源などをいじってみると仕組みがわかりやすいかと思います。

回復ポイントの土台生成

先ほど生成した立方体はあくまで表示確認用なので、消してOKです。
ここからいよいよ回復ポイントを作っていきます。

function createBase() {
  // 円の形状オブジェクトを生成(radius, 円の分割数)
  var _geometry = new THREE.CircleGeometry(120, 100);
  // テクスチャを生成
  var _texture = new THREE.TextureLoader().load('base.png');
  // 材質オブジェクト生成
  var _material = new THREE.MeshBasicMaterial({
    map: _texture,
    transparent: true
  });
  // 円オブジェクトを生成
  var _circle = new THREE.Mesh(_geometry, _material);
  // 円オブジェクトの位置座標(x, y, z)
  _circle.position.set(0, -50, 0);
  // 円オブジェクトを回転させる(xの回転角, yの回転角, zの回転角)
  //  回転角はラジアンで指定
  _circle.rotation.set(toRadian(-90), 0, 0);

  // 円オブジェクトをシーンへ追加
  scene.add(_circle);
}

// degree -> radian変換用関数
function toRadian(_deg) {
  return _deg * Math.PI / 180;
}

円オブジェクトにテクスチャを貼って、水平方向に回転させました。
材質オブジェクトは基本的な「MeshBasicMaterial」を使用しています。
今回は透過画像を使用したので「transparent」をtrueにすることで半透明合成をオンにしています。

回復ポイント土台

テクスチャ画像は思春期のほろ苦い思い出を彷彿させながら選ぶといい感じになると思います。

クリスタル生成

ここまではThree.jsで用意された形状オブジェクトを使用してきましたが、
クリスタルは通常の三角形の形状オブジェクトを8枚組み合わせて作っていきます。
また、クリスタルの中心の色を濃く、外側を薄くすることでクリスタル感がグッと増したので頂点毎に色の設定をしています。

var cristal;
function createCristal() {
  var _dark_color = 0x007a8c;
  var _light_color = 0x96f4ff;
  var _x_left = 70;
  var _x_right = -_x_left;
  var _x_middle = _x_right - _x_right;
  var _y_bottom = 20;
  var _y_middle = _y_bottom + 150;
  var _y_top = _y_middle + 90;
  var _z_back = 70;
  var _z_front = -_z_back;
  var _z_middle = _z_front - _z_front;

  // 表面用のマテリアル生成
  var _material_normal = new THREE.MeshPhongMaterial({
    vertexColors: THREE.VertexColors,
    specular: 0xffffff,
    shininess: 10,
    transparent: true,
    opacity: 0.85
  });
  // 裏面用のマテリアル生成
  var _material_back = new THREE.MeshPhongMaterial({
    side: THREE.BackSide,
    vertexColors: THREE.VertexColors,
    specular: 0xffffff,
    shininess: 10,
    transparent: true,
    opacity: 0.85
  });

  // クリスタルオブジェクト生成
  cristal = new THREE.Object3D();

  // 三角形の形状オブジェクトを生成
  var _front_left_top_g = new THREE.Geometry();
  // 三角形の形状オブジェクト各頂点の座標を指定
  _front_left_top_g.vertices = [
    new THREE.Vector3(_x_left, _y_middle, _z_middle),
    new THREE.Vector3(_x_middle, _y_top, _z_middle),
    new THREE.Vector3(_x_middle, _y_middle, _z_front)
  ];
  // 三角形の形状オブジェクト各頂点の色を指定
  var _front_left_top_c = [
    new THREE.Color(_dark_color),
    new THREE.Color(_light_color),
    new THREE.Color(_dark_color)
  ];
  // 三角形の形状オブジェクトの頂点インデックスを生成(座標番号, 座標番号, 座標番号, 法線ベクトル, 頂点色)
  _front_left_top_g.faces[0] = new THREE.Face3(0, 1, 2, null, _front_left_top_c);
  // 法線ベクトルを自動計算
  _front_left_top_g.computeFaceNormals();
  // 三角形オブジェクトを生成
  var _front_left_top = new THREE.Mesh(_front_left_top_g, _material_back);
  // クリスタルオブジェクトへ三角形オブジェクトを追加
  cristal.add(_front_left_top);


  // 頂点座標を変えた三角形オブジェクトを残り7枚生成する
  var _back_left_top_g = new THREE.Geometry();
  _back_left_top_g.vertices = [
    new THREE.Vector3(_x_left, _y_middle, _z_middle),
    new THREE.Vector3(_x_middle, _y_top, _z_middle),
    new THREE.Vector3(_x_middle, _y_middle, _z_back)
  ];
  var _back_left_top_c = [
    new THREE.Color(_dark_color),
    new THREE.Color(_light_color),
    new THREE.Color(_dark_color)
  ];
  _back_left_top_g.faces[0] = new THREE.Face3(0, 1, 2, null, _back_left_top_c);
  _back_left_top_g.computeFaceNormals();
  var _back_left_top = new THREE.Mesh(_back_left_top_g, _material_normal);
  cristal.add(_back_left_top);


  ... 省略 ...


  // クリスタルの位置座標
  cristal.position.set(0, -50, 0);
  // クリスタルをシーンへ追加
  scene.add(cristal);
}

三角形オブジェクトを8枚生成し、一つのオブジェクトにグループ化してシーンに追加しています。

クリスタルの材質オブジェクトには、「MeshPhongMaterial」を使用しました。
MeshPhongMaterialは光源を反射する性質を持っているので、クリスタルがキラキラと反射している感を出すことができました。
今回使用した引数は以下です。

  • side: 形状オブジェクトの表面に材質を描画するか、裏面にするか、両方にするかを指定
  • vertexColors: colorの設定方法を指定
  • specular: ハイライトの色を指定
  • shininess: ハイライトの大きさを指定
  • transparent: 半透明合成の有無を指定
  • opacity: 透明度を指定

クリスタル

こんな感じで回復ポイント的なアレが完成しました!

3Dオブジェクトをアニメーションさせる

ただ台座とクリスタルがあるだけではつまらないので、回転させてみようと思います。

var cristal_step = 0;
function rotateCristal() {
  cristal_step += 0.02;
  // Y軸を中心にクリスタルを回転させる
  cristal.rotation.set(0, cristal_step, 0);
  // レンダリング処理
  draw();
  // rotateCristal()関数をループで呼び出す
  requestAnimationFrame(rotateCristal);
}

クリスタルがくるくるしましたー。
この処理とデバッグ用の処理でレンダリングがダブルで走り続けているので、この時点でデバッグ用のdebugDraw()は消してOKです。

クリスタル回転

次回は回復時のエフェクトとしてパーティクルとシェーダをさわってみようと思います。