次の方法で共有



2015 年 8 月

Volume 30 Number 8

ゲーム開発 - Web ゲーム向け 3D 入門

Michael Oneppo | 2015 年 8 月

ゲームを 3 次元 (3D) にすると、まさにゲームに命が吹き込まれます。ある視点から周囲を見渡したり、物体や風景をさまざまな角度で眺めることができます。では、どうすれば実際に 3D にすることができるのでしょう。そこで、この連載では、ゲームを 3D にする手順を説明し、Web 上で普及し始めているリッチな 3D 環境を実現するために、three.js などのライブラリを利用する方法を示します。連載初回の今回は、「Web ゲームを 1 時間で」(msdn.microsoft.com/magazine/dn913185) で初めて紹介したピンというゲームを、そのシンプルさを保ちつつ、3D にすることを目指します。

3D という錯覚

すべての 3D グラフィックのレンダリングには、驚くべきトリックが潜んでいます。人間は、特にコンピューターのモニター上では、3D を実際に視認できません。3D 描画の最終目標は、2D 画像の上にシーンの 3D 表現を生み出す (レンダリングする) ことです。3D を追加してシーンをよりリアルで没入できるものにするときは、特定の視点からのイメージを得るために一部のデータを切り捨てなければなりません。この考え方を投影と呼びます。図 1 の基本 3D シーンに示すように、投影は 3D グラフィックを機能させるのに不可欠な要素です。

シンプルな 3D シーン
図 1 シンプルな 3D シーン

図のシーンでは、Z 軸が後方かつ上向きに伸びています。これを実際に画面に表示する場合、すべてのオブジェクトから Z 軸方向の情報を取り除きます (図 2 参照)。これは 3D シーンを投影するための簡単で有効な方法です。

情報を落とした 3D シーン
図 2 情報を落とした 3D シーン

この図からわかるように、このシーンは Halo に近づきもしていません。写真のような精細さで描写する場合は、3D シーンに適切なカメラ投影、ジオメトリ、シェーディングという 3 つの要素が必要になります。ここからはピンを 3D 対決ゲームに作り直しながら、この 3 つの概念についてそれぞれ説明していきます。

作業の開始

まず、three.js ライブラリをセットアップします。three.js への作業はほぼすべて JavaScript で行うため、このセットアップの構成はすぐに終ります。ここで必要になる HTML コードへの変更は、以下のとおりです。

<html>
  <head>
    <title>Ping!</title>
    <script src=
      "//cdnjs.cloudflare.com/ajax/libs/three.js/r69/three.min.js"></script>
    <script src="ping3d.js"></script>
  </head>
  <body>
  </body>
</html>

JavaScript ファイル ping3d.js では、シンプルなシーンをレンダリングするように three.js をセットアップしていきます。始めに、以下のように three.js を初期化して、描画キャンバスをページに追加します。

var scene = new THREE.Scene();
var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

上記の scene はまさに文字どおり、今回のシーンとその内部のオブジェクトすべてを表すオブジェクトです。renderer もわかりやすい名前です。scene を提供すると、renderer がそれを画面上に描画します。これは以前執筆した「Web ゲームを 1 時間で」、「Web ゲーム用の 2D 描画技法とライブラリ」(msdn.microsoft.com/magazine/dn948109)、および「Web 向け 2D ゲーム エンジン」(msdn.microsoft.com/­magazine/dn973016) で取り上げた 2D 描画システムにある程度似ています。ここで、画面に要素をいくつかを追加する必要があります。

ジオメトリ

ほぼすべての 3D グラフィックは、多角形を使って組み立てます。ボールのような曲面でも、表面は三角形を集めて模倣します。こうして組み立てた三角形をメッシュと呼びます。以下にこのボールをシーンに追加する方法を示します。

var geometry = new THREE.SphereGeometry(10);
var material = new THREE.BasicMaterial({color: 0xFF0000});
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

このコードでは、球体を形作る大量の三角形 (変数 "geometry")、シンプルな明るい赤の素材 ("material")、およびメッシュ オブジェクト ("mesh") を作成し、その後、そのメッシュをシーンに追加しています。

三角形は、3D グラフィックでは基本的なビルディング ブロックです。それはなぜでしょうか。詳しくは次回のコラムで調査しますが、三角形を構成する直線は処理が簡単なことと、三角形はそれ以上単純な平面に分割できないことが主な 2 つの理由です。コンピューターまたはスマートフォンに搭載されるグラフィック処理ユニット (GPU) は、直線で構成される形状をピクセルにすばやく変換できる専用ハードウェアです。高品質の 3D グラフィックを実現できるかどうかは、GPU に大きく左右されます。

モデル化

すべてのジオメトリは three.Mesh コンストラクターに渡すことができます。ジオメトリには、カスタム形状を作成するために生成したジオメトリや、ファイルからデータなども含まれます。ピン ゲームの場合、それぞれのプレイヤーの 3D モデルを用意します。そのため、今回はこの課題に対し、勝手ながら 3D モデル化プログラムでジオメトリを作成することにしました。three.js がまさに今回の目的に合った読み込みツールを提供してくれるため、驚くほど簡単に球体に代わるモデルを使用できます。

var jsonLoader = new THREE.JSONLoader();
jsonLoader.load('tank1.json', function (geometry) {
  var material = new THREE.BasicMaterial({color: 0xFF0000});
  var mesh = new THREE.Mesh(geometry, material);
  scene.add(mesh);
});

カメラ

カメラは、シーンの視点を表します。カメラには、ゲーム内の視点としての位置と角度の両方が格納されます。さらに重要なのが、本稿の冒頭で示したようにシーンが平面化される方法をカメラが表すことです。

今回の例のカメラは、画面の下部に配置され、右方向を向いています。最終的なイメージは、その方向からの眺めのように見せます。ただし、この投影手法を使用すると、オブジェクトからどれだけ離れても、最終的なイメージではオブジェクトのサイズが変わりません。これを正投影と呼びます。この手法は、都市シュミレーション ゲームなど、現実とは異なる視野角のゲームに有効です。今回実現したいのは、オブジェクトが遠ざかるに従って小さく見えるようにすることです。

そこで利用するのが、透視投影法です。透視投影では、カメラの視野がレンズからピラミッド型に広がっているようにイメージします。位置を画面にマップするときは、ピラミッドの側面との相対距離を基にして位置を計算します。このモデルを使用すると、オブジェクトが次第に遠ざかるにつれて、現実と同様、縮小していくように見えます。

ありがたいことに、three.js オブジェクトがこのマッピングを行い、シーン中のカメラを表すオブジェクトを提供してくれるため、自身でマッピングを行う必要がありません (他のオブジェクトの追加も簡単です)。

var camera = new THREE.PerspectiveCamera(
  75, window.innerWidth/window.innerHeight, 0.1, 1000 );

最初の引数は視野で、水平方向で捉えた角距離を示します。2 つ目の引数は画面の縦横比です。画面は正方形でないため、切り捨てられるものがないか確認する必要があります。最後の 2 つのパラメーターは、表示上、最も近い距離と最も遠い距離を定義します。これらの値よりも短い距離または長い距離にある物体は描画されません。これで、実際にシーンを描画できる状態になりました。ここで、以下のようにシーン全体を眺めるために少しカメラを後方へ移動し、描画を開始してみましょう。

camera.position.z = 50;
renderer.render(scene, camera);

素材と光源

次は、競技場にボールを置き、そこでバウンドさせます。

var room = new THREE.BoxGeometry( 50, 30, 100 );
var material = new THREE.MeshPhongMaterial({
    side:  THREE.BackSide,
    map: THREE.ImageUtils.loadTexture('arena.png')
});
var model = new THREE.Mesh(room, material);
model.position.y = 15;
scene.add(model);

ここで行っているのは、単なる四角形のジオメトリの作成とは異なります。また、素材も作成しています。素材とは、シーン内で光を反射する方法の定義です。これが、全体としての見た目を生み出します。今回の場合、Phong 素材を作成しています。これは輝く物体の既定に適した素材です。また、四角形にテクスチャも追加しています。これには three.js で関数 loadTexture を使うだけです。

このコードで注目すべきもう 1 つの部分は、side: THREE.BackSide を読み取っている行です。この行により、四角形の表面ではなく、内面だけを描画するよう three.js に指示しています。その結果、宙に浮いた立体の四角形ではなく、ボールが跳ねる部屋になります。

ここでシーンを描画すると、競技場は表示されず、単なる暗闇が描画されます。これは、素材によって物体からの光の反射方法は定義しましたが、このシーンには光源が存在しないためです。three.js で光源をシーンに追加するには以下のようにします。

this.lights = [];
this.lights[0] = new THREE.PointLight( 0x888888, 1, 300 );
this.lights[0].position.set( 0, 10, 40 );
scene.add( this.lights[0] );
this.lights[1] = new THREE.PointLight( 0x888888, 1, 300 );
this.lights[1].position.set( 0, 20, -40 );
scene.add( this.lights[1] );

ここでシーンを描画すると、競技場が正しくレンダリングされます。見た目を良くするため、以下のように競技場を横から見るカメラ位置を設定してからコードを実行します。

camera.up.copy(new THREE.Vector3(0,1,0));
camera.position.copy(new THREE.Vector3(0,17, -80));
camera.lookAt(new THREE.Vector3(0,0,40));

1 行目ではカメラを上向きにするよう指示する変数 up を設定しています。関数 lookAt はまさに文字どおり、カメラを特定の位置に向けます。

3D ゲームへの変換

これで、ゲームは 3D になります。残りは非常に簡単です。ただし、2D オブジェクトの代わりに 3D で構成しているため、結果としてゲームは以前の実装よりもやや冗長になってしまいます。そこで、追加コードを処理しやすくするために、コードを別々のファイルに分割します。

また、JavaScript 式のオブジェクト定義を、従来型に近いコンストラクター モデルに移行します。これをデモするため、競技場と光源を 1 つのオブジェクトにラップし、そのオブジェクトを 1 つのファイルに配置します (図 3 参照)。

図 3 Arena オブジェクト

function Arena(scene) {
  var room = new THREE.BoxGeometry( 50, 30, 100 );
  var material = new THREE.MeshPhongMaterial({
    side:  THREE.BackSide,
    map: THREE.ImageUtils.loadTexture('arena.png')
  });
  var model = new THREE.Mesh(room, material);
  model.position.y = 15;
  scene.add(model);
  this.lights = [];
  this.lights[0]= new THREE.PointLight( 0x888888, 1, 300 );
  this.lights[0].position.set( 0, 10, 40 );
  scene.add( this.lights[0] );
  this.lights[1]= new THREE.PointLight( 0x888888, 1, 300 );
  this.lights[1].position.set( 0, 20, -40 );
  scene.add( this.lights[1] );
}

競技場を作成する場合、以下のコンストラクター関数を使用する新しいオブジェクトを作成できます。

var arena = new Arena(scene);

次に、競技場で跳ね回るボール オブジェクトを作成します。three.js で赤いボールを作成する方法を理解したので、このコードを以下のように 1 つのオブジェクトにラップします。

function Ball(scene) {
  var mesh = new THREE.SphereGeometry(1.5, 10, 10);
  var material = new THREE.MeshPhongMaterial({
    color: 0xff0000,
    specular: 0x333333
  });
  var _model = new THREE.Mesh(mesh, material);
  _model.position.y = 10;
  scene.add(_model);
}

さらに、ボールのオブジェクトに関数を追加することで、ボールが跳ねる際の基本的な物理法則を定義します (図 4 参照)。

図 4 ボール オブジェクトの update 関数

// Create a private class variable and set it to some initial value.
var _velocity = new THREE.Vector3(40,0,40);
this.update = function(t) {
  // Apply a little gravity to the ball.
  _velocity.y -= 25 * t;
  // Move the ball according to its velocity
  var offset = _velocity.clone()
    .multiplyScalar(t);
  _model.position.add(offset);
  // Now bounce it off the walls and the floor.
  // Ignore the ends of the arena.
  if (_model.position.y - 1.5 <= 0) {
    _model.position.y = 1.5;
    _velocity.y *= -1;
  }
  if (_model.position.x - 1.5 <= -25) {
    _model.position.x = -23.5;
    _velocity.x *= -1;
  }
  if (_model.position.x + 1.5 >= 25) {
    _model.position.x = 23.5;
    _velocity.x *= -1;
  }
}

three.js では requestAnimationFrame を使用するたびにシーンをレンダリングする必要があります。以下はおなじみのパターンです。

var ball = new Ball(scene);
var Arena = new Arena(scene);
var render = function (time) {
  var t = (time - lastTime) / 1000.0;
  lastTime = time;
  ball.update(t);
  renderer.render(scene, camera);
  requestAnimationFrame( render );
}
requestAnimationFrame(render);

第 2 部の前に

これで光源のある競技場、適切な位置に置いたカメラ、シーン全体で跳ね回るボールをそれぞれ用意できました。今回の説明はここまでとします。次回は、マウスで狙いを定めると、3D 投影がどのように機能するかを説明します。さらにテクスチャに関する説明を加え、tween.js という強力なライブラリを使って滑らかなアニメーションを作成します。この 3 部構成のコラムの最終回は、three.js の内容を詳しく調べ、これが実際どのように忠実度の高いグラフィックを描画しているかを確かめます。


Michael Oneppo はクリエイティブな技術者で、マイクロソフトの Direct3D チームで前プログラム マネージャーを務めていました。近年では、非営利技術団体 Library For All の CTO としての業務、さらにニューヨーク大学の Interactive Telecommunications Program で修士号の取得などに努めました。

この記事のレビューに協力してくれた技術スタッフの Mohamed Ameen Ibrahim に心より感謝いたします。