3Dコンテンツの制作において「三角関数は必須」とよく聞きます。Webサイト制作で三角関数を使う場面はほとんどなかったため筆者は意外に思っていましたが、WebGLの勉強をすすめるうちに3Dでは三角関数を使う場面がとても多いことに気づきました。そこで、本記事では3Dコンテンツ制作で使用頻度が多いであろう基本的な数式と概念をまとめました。

今回解説する内容は地味ですが、ゲームやデータビジュアライゼーションを作る上でこの数式が基本となってきます。高校数学(1年)で学んだことをベースに、3つのサンプルを通して学習できるようまとめましたので、ぜひ最後までお付き合いください。WebGLの人気ライブラリの一つ「Three.js」を使って解説しています。

まずは、本題に入る前に三角関数を使ったデモを作成したので紹介します。次のリンクをクリックしてご覧ください。

▲ 地球をモチーフにしたサンプル。地球の赤道上には衛星が回り、日本と世界の主要都市は線で結ばれています。本記事の内容を応用することでこのような表現ができるようになります。ソースコードはGitHubに公開しておりますので、あわせて参照ください。

※この記事ソースコードは 2016年1月15日時点で最新のThree.js(r73)とTypeScript(ver1.7.5)によって書かれています。
※デモで使用しているテクスチャ画像はNatural Earth IIIから引用しています。

1. 円周上を移動する座標の計算方法

円運動とは1点を中心に円周上を動くモーションのことです。地球の人工衛星や土星の輪っかを想像するとわかりやすいと思いますが、球体の円周上を動く計算式を紹介します。次のサンプルでは、球体のまわりを赤い点が移動します。

下図は円周上にある点のX座標、Y座標を求めるものです。三角関数を使えば角度と半径をもとに公式から座標を求めることができます。水平の位置を求めるにはコサインを、垂直の位置を求めるにはサインを使います。「サイン・コサイン・タンジェント」と高校のときに暗記しましたが、こんなところで役に立つのですね。

1601_trigonometric_function_sample1_diagram

この式を参考に半径と角度を渡して位置情報を返却するコードをJavaScriptで表現してみます。Three.jsでは角度ではなくラジアンを使うので、角度degreeに対してMath.PI / 180を掛け算することでラジアンに変換します。サインとコサインはともにMath.sin()Math.cos()という命令を使います。

// 角度をラジアンに変換します
var rad = degree * Math.PI / 180;

// X座標 = 半径 x Cosθ
var x = radius * Math.cos(rad);
// Y座標 = 半径 x Sinθ
var y = radius * Math.sin(rad);

フレーム毎に角度degreeを少しずつ増やすことで、円を描く位置情報を取得することができます。

2. 緯度/経度/高度から地球上に点を打つ

世界地図などで地点を示すには緯度と経度がよく利用されますが、緯度と経度を3次元の座標(X・Y・Z)に変換することで都市の場所を球体にプロットすることができます。次のサンプルでは、地球上に日本や北京、ロンドンの都市の場所に赤や緑の丸を配置しています。

指定した緯度、経度、高度で地球上に点を打てるようにしてみます。前章の円運動では二次元上での位置計算ですが、今回は三次元上での計算になります。下図の式は球上にある点のX座標、Y座標、Z座標を求めるものです。

1601_trigonometric_function_sample2_diagram

上の式を参考に緯度、経度、高度を渡して位置情報を返却するJavaScriptの関数を組んでみましょう。

/**
 * 緯度経度から位置を算出します
 * @param {number} latitude 緯度
 * @param {number} longitude 経度
 * @param {number} radius 半径
 * @returns {THREE.Vector3} 位置
 */
function translateGeoCoords(latitude, longitude, radius) {
  // 仰角
  var phi = (latitude) * Math.PI / 180;
  // 方位角
  var theta = (longitude - 180) * Math.PI / 180;

  var x = -(radius) * Math.cos(phi) * Math.cos(theta);
  var y = (radius) * Math.sin(phi);
  var z = (radius) * Math.cos(phi) * Math.sin(theta);

  return new THREE.Vector3(x, y, z);
}

translateGeoCoords()関数を使って緯度経度から地球上の位置を導き出す事ができるようになりました。

3. 球体状の2点を線でつなぐ

球体状の2点を線でつないでみましょう。次のサンプルでは、日本から世界の各都市へ線をつないでいます。航空会社のエアラインを示したかのような表現ができます。

前章でプロットした地球上の点と点をつなぐ線を描いてみます。下図のような球状の2点間の軌跡座標を取得するには、クォータニオンを使用します。クォータニオンとは、回転軸と回転角度の情報を持っており、3D上でのオブジェクトの姿勢を表すものです。物体の回転を実装する上で非常に役立ちます。

1601_trigonometric_function_sample3_diagram

クォータニオンの詳しい説明は以下のサイトで分かりやすく解説されているのでこちらを参照ください。

先ほどの図を参考に、OAベクトルをOBベクトルに向かって少しずつ回転させ、軌道座標を配列で返す関数をJavaScriptで表現してみます。

/**
 * 軌道の座標を配列で返します
 * @param {THREE.Vector3} startPos 開始点
 * @param {THREE.Vector3} endPos 終了点
 * @param {number} segmentNum 頂点の数 (線のなめらかさ)
 * @returns {THREE.Vector3[]} 軌跡座標の配列
 */
function getOrbitPoints(startPos, endPos, segmentNum) {

  // 頂点を格納する配列
  var vertices = [];
  var startVec = startPos.clone();
  var endVec = endPos.clone();

  // 2つのベクトルの回転軸
  var axis = startVec.clone().cross(endVec);
  // 軸ベクトルを単位ベクトルに
  axis.normalize();

  // 2つのベクトルのなす角度
  var angle = startVec.angleTo(endVec);

  // 2つの点を結ぶ弧を描くための頂点を打つ
  for (var i = 0; i < segmentNum; i++) {
    // axisを軸としたクォータニオンを生成
    var q = new THREE.Quaternion();
    q.setFromAxisAngle(axis, angle / segmentNum * i);
    // ベクトルを回転させる
    var vertex = startVec.clone().applyQuaternion(q);
    vertices.push(vertex);
  }
  // 終了点を追加
  vertices.push(endVec);

  return vertices;
}

クォータニオンを生成する上で必要な軸を作成します。今回の軸は、地球の中心から伸びた2つのベクトルのなす面に垂直なベクトルです。この場合、2つのベクトルの外積を求めることで垂直なベクトルを取得することができるので、cross()メソッドを使用して軸となるベクトルを生成しましょう

// 2つのベクトルの回転軸
var axis = startVec.clone().cross(endVec);
// 軸ベクトルを単位ベクトルに
axis.normalize();

先ほど作成した軸を基準にどこまで回転させるかを求めます。今回は、地球の中心から伸びた2つのベクトルのなす角度が回転角度の限界値になるのでangleTo()メソッドを用いて角度を求めます。

// 2つのベクトルのなす角度
var angle = startVec.angleTo(endVec);

回転軸と回転角度を求めることができたので、実際にクォータニオンを生成します。なめらかな線を引くため、頂点の数分だけ少しずつ角度をつけていきます

// axisを軸としたクォータニオンを生成
var q = new THREE.Quaternion();
// setFromAxisAngle(回転軸, 回転角度)
q.setFromAxisAngle(axis, angle / segmentNum * i);

クォータニオンで作った回転情報をOAベクトルに反映させます。

// ベクトルを回転させる
var vertex = startVec.clone().applyQuaternion(q);

二点を結ぶ軌跡の座標をgetOrbitPoints()関数から得ることができるようになりました。軌跡座標を線の頂点として設定することで、デモのように二点間を結ぶ軌跡を表現することができます

まとめ

円形・球体の座標制御に三角関数が役立つことがわかっていただけたのではないでしょうか。一般的に3Dのサンプルに地球や天体を使用したものが多いですが、これらの数式・概念がわかっているとサンプルコードが読み解きやすくなります。冒頭のデモは3つのサンプルから少しだけ装飾を付け加えたものなので、応用編として挑戦してみてください。

今回はThree.jsを使用して三角関数によるグラフィックを作成しましたが、この数式と概念は他の言語やライブラリでも同じように活用できます(このサンプルで利用したものは、もともとFlash Stage3Dの制作で使われていた公式です)。二次元での三角関数の活用方法も記事「CreateJS入門」で解説しています。是非みなさんも一度、三角関数を使った表現を試してみてください。