クモのようにコツコツと

フロントエンドエンジニア イイダリョウの技術ブログ。略称「クモコツ」

【Three.js】カメラ制御を実現している「三角関数」を理解する!(サイン、コサイン、タンジェント)

Three.jsの続きです。前回はジオメトリ設定で色々な形を作りました。今回はカメラの制御です。コードの理解のためには三角関数の理解が必要でした。サイン、コサイン、タンジェントとか聞き覚えはあったけど、それを使う目的はわかっていなかったです。それではいきましょう!

【目次】

※参考:【Three.js】ジオメトリ設定で形状を色々変えてみる - クモのようにコツコツと

Three.jsを習得するためにやったことまとめ
qiita.com

camera.positionでカメラを制御(完成品)

今回もICS mediaの「Three.js入門」を参考に進める。こちらの記事。

※参考:Three.jsのカメラの制御 - ICS MEDIA

作ってみたものこちら。

See the Pen three.js camera-1 by イイダリョウ (@i_ryo) on CodePen.

おお!惑星イイダが一周している。動きは前回と同じだが、惑星の周りに星屑があるのと、カメラポジションの設定の書き方を変えている。

JSコード

// ページの読み込みを待つ
window.addEventListener('load', init);

function init() {

  // サイズを指定
  const width = 960;
  const height = 540;
  let rot = 0;

  // レンダラーを作成
  const renderer = new THREE.WebGLRenderer({
    canvas: document.querySelector('#myCanvas')
  });
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(width, height);

  // シーンを作成
  const scene = new THREE.Scene();

  // カメラを作成
  const camera = new THREE.PerspectiveCamera(45, width / height);

  // ジオメトリを作成(球体)
  const geometry = new THREE.SphereGeometry(300, 30, 30);
  // 画像を読み込む
  const loader = new THREE.TextureLoader();
  const texture = loader.load('https://s.cdpn.io/profiles/user/305282/512.jpg');
  // マテリアルを作成
  const material = new THREE.MeshStandardMaterial({map: texture});
  //メッシュを作成
  const mesh = new THREE.Mesh(geometry, material);
  //シーンにメッシュを適用
  scene.add(mesh);

  // 平行光源
  const directionalLight = new THREE.DirectionalLight(0xFFFFFF);
  directionalLight.position.set(1, 1, 1);
  // シーンに追加
  scene.add(directionalLight);


  // 星屑を作成 (カメラの動きをわかりやすくするため)
  createStarField();

  function createStarField() {
    // 形状データを作成
    const geometry = new THREE.Geometry();
    for (let i = 0; i < 1000; i++) {
      geometry.vertices.push(
        new THREE.Vector3(
          3000 * (Math.random() - 0.5),
          3000 * (Math.random() - 0.5),
          3000 * (Math.random() - 0.5)
        )
      );
    }
    // マテリアルを作成
    const material = new THREE.PointsMaterial({
      size: 10,
      color: 0xffffff
    });

    // 物体を作成
    const mesh = new THREE.Points(geometry, material);
    scene.add(mesh);
  }

  tick();

  // 毎フレーム時に実行されるループイベントです
  function tick() {

    rot += 0.5; // 毎フレーム角度を0.5度ずつ足していく

    // ラジアンに変換する
    const radian = (rot * Math.PI) / 180;
    // 角度に応じてカメラの位置を設定
    camera.position.x = 1000 * Math.sin(radian);
    camera.position.z = 1000 * Math.cos(radian);
    // 原点方向を見つめる
    camera.lookAt(new THREE.Vector3(0, 0, 0));
    // レンダリング
    renderer.render(scene, camera);    
    requestAnimationFrame(tick);
  }
}

前回と変えた部分を見ていく。

星屑の追加(Vector3()メソッド)

まずは惑星イイダの周りに散らばっている星屑部分。コメントにもある通り、カメラの動きをわかりやすくするために追加している。

関数実行

まず下で定義しているcreateStarField()関数を実行。

 // 星屑を作成 (カメラの動きをわかりやすくするため)
  createStarField();

関数定義(大枠)

次はcreateStarField()関数の定義部分の大枠。

  function createStarField() {
    // 形状データを作成
    const geometry = new THREE.Geometry();
      // 中略
    }
    // マテリアルを作成
    const material = new THREE.PointsMaterial({
       // 中略
    });

    // 物体を作成
    const mesh = new THREE.Points(geometry, material);
      // 中略
  }
  • createStarField()関数を定義
  • 変数geometryでジオメトリ(形状データ)を作成
  • 変数materialでマテリアル作成
  • 変数meshでメッシュ(物体)を作成

ジオメトリ

関数の中身。最初にジオメトリ部分

    // 形状データを作成
    const geometry = new THREE.Geometry();
    for (let i = 0; i < 1000; i++) {
      geometry.vertices.push(
        new THREE.Vector3(
          3000 * (Math.random() - 0.5),
          3000 * (Math.random() - 0.5),
          3000 * (Math.random() - 0.5)
        )
      );
    }
  • 変数geometryGeometry()インスタンス作成
  • for文で1000まで繰り返す
  • geometryオブジェクトのverticesプロパティ にpush()メソッドで追加
  • Vector3()インスタンスを作成
    三つの引数に0〜0.5の間の乱数に3000をかけた数値を入れる

verticesとは頂点という意味のようだ。

push()メソッドは配列の末尾に追加するメソッド。verticesという配列に追加する。

※参考:Array.prototype.push() - JavaScript | MDN

Vector3()メソッドはthree.jsのメソッド

Vector3
Class representing a 3D vector. A 3D vector is an ordered triplet of numbers (labeled x, y, and z), which can be used to represent a number of things, such as:

Vector3
3Dベクトルを表すクラス。 3Dベクトルは、(x、y、およびzとラベル付けされた)数値の順序付けられたトリプレットであり、次のような多くのものを表すために使用できます。

※参考:three.js docs

この三つの引数が横x、縦y、奥行きzの位置になるわけだ。そこにMath.random()で乱数を入れると。

※参考:【JS】Math.random()でつくるサイコロとおみくじ - クモのようにコツコツと

マテリアル

次、マテリアル部分

    // マテリアルを作成
    const material = new THREE.PointsMaterial({
      size: 10,
      color: 0xffffff
    });
  • 変数materialPointsMateria()インスタンスを作成。引数は連想配列
  • 連想配列のsizeキーの値は10
    colorキーの値は0xffffff

PointsMaterial()はこちら。

The default material used by Points.

ポイントで使用されるデフォルトのマテリアル。

※参考:three.js docs

ポイントということは点を作るマテリアルかな。10pxの白い点が作られる。

メッシュ

最後にメッシュを作成。

    // 物体を作成
    const mesh = new THREE.Points(geometry, material);
    scene.add(mesh);
  }
  • 変数mesh()Points()インスタンスを作成。第一引数はgeometry、第二引数はmaterial
  • sceneadd()メソッドでメッシュを追加

これでシーンに星屑が追加される。上の設定によって1000個がランダムな位置に。

カメラ制御の設定(camera.position()メソッド)

次はカメラポジションの設定。

ローテーション(回転)の初期値

冒頭のサイズ設定のグローバル変数で変数rotを設定

  let rot = 0;

初期値は0。この変数名はrotation(回転)の略と思われる。

カメラポジション(前回)

前回はカメラポジションcamera.position()の設定は変数cameraのすぐ後に書いていた。

  // カメラを作成
  const camera = new THREE.PerspectiveCamera(45, width / height);
  camera.position.set(0, 0, +1000); // ←前回はここに書いていた

カメラの位置は固定だったため。今回はカメラを移動するため、最後のアニメーション設定(tick()関数)のところに書く。

アニメーション設定(前回)

前回のアニメーション設定(tick()関数)では、カメラではなくメッシュmesh自体を回転していた。

  function tick() {
    mesh.rotation.y += 0.01;
    renderer.render(scene, camera); // レンダリング

    requestAnimationFrame(tick);
  }

この部分

mesh.rotation.y += 0.01;

1フレームごとに縦軸が0.01度ずつ回転する。

今回はメッシュではなくカメラを移動させる。

カメラポジションのアニメーション設定(全体)

今回変更したtick()関数。

  // 毎フレーム時に実行されるループイベントです
  function tick() {

    rot += 0.5; // 毎フレーム角度を0.5度ずつ足していく

    // ラジアンに変換する
    const radian = (rot * Math.PI) / 180;
    // 角度に応じてカメラの位置を設定
    camera.position.x = 1000 * Math.sin(radian);
    camera.position.z = 1000 * Math.cos(radian);
    // 原点方向を見つめる
    camera.lookAt(new THREE.Vector3(0, 0, 0));
    // レンダリング
    renderer.render(scene, camera);    
    requestAnimationFrame(tick);
  }
  • tick()関数を設定。引数はなし
  • 変数rot()に0.5を加算(毎フレーム角度を0.5度ずつ)
  • 変数radianで回転角度と円周率3.14を掛けて、180で割る
  • camera.position.xでカメラの横位置、1000と数値の正弦Math.sin()を掛ける。正弦の引数はradian
  • camera.position.zでカメラの奥行き位置、1000と数値の正弦Math.sin()を掛ける。正弦の引数はradian
  • camera.lookAt()の引数でVector3()インスタンスを作成する。Vector3()の3つの引数は全て0

上記の設定によってカメラの回転を実現しているのだが、式を見ても自分には何をしているのかよくわからない。。

三角関数の理解(カメラ制御設定の詳細)

カメラ制御設定のコードを細かく見ていく。

ラジアン

まず冒頭にラジアンという言葉が。

// ラジアンに変換する
    const radian = (rot * Math.PI) / 180;

はて?そもそも「ラジアン」とは何ぞや?何をラジアンに変換?

ラジアンは弧度法と呼ばれる角度の単位です。

1 ラジアンは円の半径の長さに等しい弧に対する中心角の大きさ

f:id:idr_zz:20200412192150j:plain

※参考:ラジアン(弧度法)の意味と「度」への変換方法

半径と弧の長さが等しくなった時の角度が1ラジアンか。

Math.PIは円周率(π)

変数radianの式の中にMath.PIが出てくる。これは円周率(約3.14)のこと。

Math.PI プロパティは円周率、約3.14159を表します。

※参考:Math.PI - JavaScript | MDN

円周率とは

円周率の値が3.14であることは覚えている。だが、そもそも「円周率」って何だっけ?

円周率とは、円の直径に対する円周の長さの比のこと。

f:id:idr_zz:20200412192928p:plain

※参考:https://atarimae.biz/archives/2013

なるほど。円周の長さは2πr。これも聞き覚えがあるぞ!

ラジアンを度数に変換

で、その円周率とラジアンがどういう関係にあるのか?

まずは、1 ラジアンを「度」に変換してみます。

半径 r の円を考えたとき、円周の長さは 2πr で、円周に対する中心角は 360° です。ここで「弧の長さは中心角に比例する」という関係を用いると、 360° に、円周 2πr に対する弧の長さ r (← 1 rad の定義)の比をかけたものが 1 ラジアンとなります。

半径(R)と円周率(π)と弧度法(ラジアン)の関係は下記の図のようになる。

f:id:idr_zz:20200412193040j:plain

1ラジアンの計算方法は180度を1パイ(3.14)を割った数。大体57.3度くらいになる。

f:id:idr_zz:20200412193233j:plain

※参考:ラジアン(弧度法)の意味と「度」への変換方法

度数をラジアンに変換

逆に1度からラジアンに変換する計算は方法は…

f:id:idr_zz:20200412193654j:plain

先程とは逆で1π(3.14)を180度で割った数。計算すると0.017444...ラジアンか。

180度の場合は、掛ける180になるので相殺されて1パイと同じ3.14ラジアンになる。

f:id:idr_zz:20200412194431j:plain

※参考:ラジアン(弧度法)の意味と「度」への変換方法

そして、角度からラジアンを割り出す方法は下記の計算式になる。

角度(ここではθ°とします)をラジアンに変換するには、θにπ/180をかければ良い

f:id:idr_zz:20200412195227j:plain

※参考:ラジアンとは何か?角度をラジアンに変換する方法が理解できる練習問題付き|高校生向け受験応援メディア「受験のミカタ」

おお、この計算式が先ほどの変数radianの式と同じだ!やっと繋がった!

// ラジアンに変換する
    const radian = (rot * Math.PI) / 180;

変数rotはフレームごとに0.5度ずつ増える角度なので、その角度をラジアンに変換しているわけだ!

カメラの位置設定にラジアンを使う

ではなぜラジアンが必要?この後のカメラの位置設定に使われる。

// 角度に応じてカメラの位置を設定
    camera.position.x = 1000 * Math.sin(radian);
    camera.position.z = 1000 * Math.cos(radian);

カメラポジションのx(横)、z(奥行き)の位置に先ほどのラジアンの値が使われているようだ。

Math.sin()はサイン

カメラポジションのx(横)に出てくるMath.sin()メソッドはサインのことらしい。

Math.sin() 関数は数値の正弦 (sine)を返します。

※参考:Math.sin() - JavaScript | MDN

サインってなんだっけ。正弦、ともいうんだね。

三角関数(サイン、コサイン、タンジェント)

「サイン、コサイン、タンジェント」とか、紀元前くらいに聞いた覚えがある古の言葉…。

どうやら「三角関数」に関わる言葉のようだ。

三角関数(さんかくかんすう、英: trigonometric function)とは、平面三角法における、角の大きさと線分の長さの関係を記述する関数の族および、それらを拡張して得られる関数の総称である。鋭角を扱う場合、三角関数の値は対応する直角三角形の二辺の長さの比であり、三角関数は三角比とも呼ばれる。

「三角関数」は直角三角形における「鋭角」と「線の長さ」の関係を表す関数か。

三角関数には以下の6つがある。

  • sin(正弦、sine)
  • sec(正割、secant)
  • tan(正接、tangent)
  • cos(余弦、cosine)
  • csc(余割、cosecant)
  • cot(余接、cotangent)

え?そんなにあるの?サイン、コサイン、タンジェント以外は馴染みが薄い。。

※参考:三角関数 - Wikipedia

長さから角度を割り出す

直角三角形で下記のようにの長さから角度を割り出すことができる。

f:id:idr_zz:20200412174643p:plain

※参考:三角関数は何に使えるのか 〜 サイン・コサイン・タンジェントの活躍 〜 - Qiita

  • サインはBC/AC
  • コサインはAB/AC
  • タンジェントはBC/AB

角度から座標(位置)を割り出す

そして、サイン、コサインの角度から座標(位置)を割り出すこともできる。

x=r cosθ
y=r sinθ

  • 半径の長さrとコサインを掛けるとX(横)位置
  • 半径の長さrとサインを掛けるとY(縦)位置

f:id:idr_zz:20200413064010p:plain

※参考:三角関数で角度から座標を導くふたつの式の使い途 - Qiita

おお、これは先ほどサインMath.sin()が出てきたこの式が同じ形だ!

camera.position.x = 1000 * Math.sin(radian);

1000pxは半径Rなんだな。そしてサインの角度がラジアンだと。それを掛けるとcamera.position.xでカメラの横(X)位置になると。

Math.cos()はコサイン

その次に出てくるMath.cos()でコサインの式が出てくる。

Math.cos() 静的関数は、指定された(ラジアン単位の)角度の余弦 (cosine)を返します。

※参考:Math.cos() - JavaScript | MDN

こちらも先ほどの式と同じ形になっているわけだ。

camera.position.z = 1000 * Math.cos(radian);

半径は1000pxで、コサインの角度がラジアン。それを掛けるとcamera.position.zでカメラの奥行き(Z)位置になると。

これによってカメラは上下は無しで左右と奥行きの範囲で円状に移動するわけだ!

値を上下(x,y)ではなく横と奥行き(x,z)にするので、カメラポジションの円は真上から見るとわかるわけだ。

カメラの方向(camera.lookAt())

次はカメラの方向の設定。

    // 原点方向を見つめる
    camera.lookAt(new THREE.Vector3(0, 0, 0));

地球を中心としてカメラが円周上を自動的に移動します。カメラの位置の設定はcameraオブジェクトのpositionプロパティーに数値を代入します。

カメラは常に中央を見るようにしておきたいので、cameraオブジェクトのlookAt()メソッドを使って原点座標(0, 0, 0)を指定しています。lookAt()メソッドはどの位置からでも指定した座標に強制的に向かせることができる命令です。

※参考:Three.jsのカメラの制御 - ICS MEDIA

lookAt()メソッドでカメラの向きは常に原点を向くようになる。

ちなみに、lookAt()メソッドはthree.jsの公式サイトでは見当たらなかった。(しかしちゃんと動くので存在するメソッドと思われる)

レンダリングは変更無し

その下のレンダリング部分は変更なしだった。

    // レンダリング
    renderer.render(scene, camera);    
    requestAnimationFrame(tick);

最後に(三角関数はとても重要!)

f:id:idr_zz:20200413070929j:plain

ということで、今回はカメラを回転させるために、三角関数(サイン、コサイン、タンジェント)を理解する必要がありました。

ICSメディアの記事にもこうあります。

Three.jsで説明していますが、類似の3Dフレームワーク(たとえば、AwayJSやBabylon.jsなど)でも同じように利用できます。今回のチュートリアルではThree.jsの使い方の理解というよりは、3Dの基本的な制御の理解という認識をもっていただければ幸いです。

※参考:Three.jsのカメラの制御 - ICS MEDIA

三角関数はThree.jsに限らず、3D全般で使う場面がありそうです。

三角関数(三角比)は紀元前2世紀に測量のために生まれた!すごすぎる歴史!

三角比は紀元前2世紀に、アレクサンドリアの学者ヒッパルコスが考えだしたものです。

そして三角比はなんのために考え出されたかというと、測量のためです。

※参考:三角関数のsin・cos・tanとは?図解ですぐわかる!超重要な公式と練習問題も|高校生向け受験応援メディア「受験のミカタ」

その技術がコンピュータ時代になってもいろいろな場面で使われている!

その考え方が色んなところに応用できることです。今回の肝は「長さと長さの関係から角度が計算できる」というところにあります。

使われ方 具体的な場面
測量 GPS、地図、土木工学、精密機械工学、ケーキカット
回転 ゲームプログラミング、CG、航法、機械工学、宇宙機や航空機の姿勢制御
音声処理、画像処理、ノイズフィルター、振動工学、量子力学などさまざまな物理学分野
その他1 内積、コサイン類似度、ボックス=ミュラー法(正規乱数の生成)、カーネル法における非線形関数としてなど

※参考:三角関数は何に使えるのか 〜 サイン・コサイン・タンジェントの活躍 〜 - Qiita

3DCGだけでなくゲームの動きとかにも必要か。

回転だけでなく曲線的な動きにも利用できそう!

f:id:idr_zz:20200413082515g:plain

※参考:【スクラッチ】で【三角関数】の使い方をわかりやすくまとめてみた | もんプロ~問題発見と解決のためのプログラミング〜

音声処理で言うとシンセのサイン波の波形も三角関数が関係しているようだ。

f:id:idr_zz:20200413122111p:plain

※参考:「あぁ~!サインの音~~!」【隙間リサーチ】 | ちりつもFILE (β

そして「おっぱい関数」でおっぱいを揺らしているのも三角関数!

f:id:idr_zz:20200413082812j:plain

この動きを実現するための数式がなんだか凄い!感動した!

※参考:おっぱい関数 by CHARTMAN

と言うことで今回は三角関数の理解がメインになりました。これは今後プログラミングをしていく上でも知っておいた方がいい内容だったのでよかったです。

次回はカメラを自動回転ではなくマウス位置に反応させるインタラクティブな動きに入っていきたい。それではまた!


Three.jsを習得するためにやったことまとめ
qiita.com