study

暇だったのでWebGLで遊んでみたよ

こんにちは、omurinです。大学で学んだことって何だろうと思ったので授業で学んだWebGLで遊んでみたいと思います。

概要

 CADを使って戦闘機を作成し、ミサイルが発射できるアニメーションを実装してみる。これらを実現するためにWebGLを使用した。WebGLを使用することで3Dオブジェクトの作成、影の描画、アニメーション、マウスによる操作を可能にする。今回の作品では、ミサイルの発射、前進、後退、上昇、下降、マウスによる視点操作を可能にしたよ。

CADとはレンダリング、マテリアル、モデリング、アニメーションをするソフトのことで有名なものだとMayaやblenderなどがあるよ。

実行環境

アニメーションを実装するためにWebGLを使用したよ。ライブラリはMTLLoader、OBJLoader、dat.gui.js、stats.js、three.js、OrbitControls.jsを使用して、インターフェースの構築はHTML、JavaScriptを使用したよ。

完成した作品

ミサイル発射機能付き戦闘機を作成したよ
サンプルを動かしてみたい人はこちらをクリック!

 

Webブラウザによっては正しく開かない場合があります。chromeなどはパスの設定を行う必要があるので注意。おすすめはfirefox。

使用したソースコード

<!DOCTYPE html>

<html>

<head>
  <title>Example 01.04 - Materials, light and animation</title>
  <script type="text/javascript" src="../libs/three.js"></script>
  
  <!--視点操作-->
  <script type="text/javascript" src="../libs/OrbitControls.js"></script>
  <script type="text/javascript" src="../libs/loaders/OBJLoader.js"></script> <!-- OBJ用のローダー -->
  <script type="text/javascript" src="../libs/loaders/MTLLoader.js"></script> <!-- MTL用のローダー -->

  <script type="text/javascript" src="../libs/stats.js"></script>
  <script type="text/javascript" src="../libs/dat.gui.js"></script>
  <!--視点操作用のライブラリ読み込み-->
  <style>
  body {
    margin: 0;
    overflow: hidden;
  }
  </style>
</head>
<body>
  <div id="Stats-output">
  </div>
  <div id="WebGL-output">
  </div>

  <script type="text/javascript">
  function init() {
    var stats = initStats();
    var scene = new THREE.Scene();
    var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
    var renderer = new THREE.WebGLRenderer();


    renderer.setClearColor(new THREE.Color(0x87cafa));
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.shadowMapEnabled = true;

    var orbitControls = new THREE.OrbitControls(camera, renderer.domElement);

    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);
    //spotLight追加
    var spotLight = new THREE.SpotLight(0xfafad2);
    spotLight.position.set(0, 1000, 1000);
    spotLight.castShadow = true;
    scene.add(spotLight);

    document.getElementById("WebGL-output").appendChild(renderer.domElement);

    //GUI追加
    var controls = new function () {
      this.positionSpeed = 0.01;
      this.misairuSpeed =0;
      this.rotationSpeed =0.02;
      this.backSpeed=0.01;
      this.highSpeed=0.01;
      this.lowSpeed=0.01;
      this.rightSpeed=0.01;
      this.leftSpeed=0.01;
    };
    var gui = new dat.GUI();
    gui.add(controls, 'positionSpeed', 0, 1);
    gui.add(controls, 'rotationSpeed', 0, 10000);
    gui.add(controls, 'misairuSpeed', 1,2);
    gui.add(controls, 'backSpeed', 0,1);
    gui.add(controls, 'highSpeed', 0,1);
    gui.add(controls, 'lowSpeed', 0,1.0);
    gui.add(controls, 'rightSpeed', 0,1);
    gui.add(controls, 'leftSpeed', 0,1);
    var mesh;//読み込んだオブジェクトを格納する変数//

    var mtlLoader = new THREE.MTLLoader();	// MTLローダの定義
    mtlLoader.setPath("sentouki/");	// MTLファイルへのパスを設定

    // MTLファイルの読み込み
    mtlLoader.load('sentouki.mtl', function (materials) {
      materials.preload();

      var objLoader = new THREE.OBJLoader();	// OBJローダの定義
      objLoader.setMaterials(materials);		// OBJローダにマテリアルを代入
      objLoader.setPath("sentouki/");		// OBJファイルへのパスを設定
      //OBJファイルの読み込み
      objLoader.load('sentouki.obj', function (object) {
        object.scale.set(0.2, 0.2, 0.2);	// スケール変更
        mesh = object;
        mesh.position.x = 0;
        mesh.position.z = 0;
        mesh.position.y = 0;
        mesh.rotation.x = -0.5 * Math.PI;
        scene.add(mesh);		// シーンに追加
      });
    });
    var mesh1;//読み込んだオブジェクトを格納する変数//

    var mtlLoader= new THREE.MTLLoader();	// MTLローダの定義
    mtlLoader.setPath("puropera/");	// MTLファイルへのパスを設定

    // MTLファイルの読み込み
    mtlLoader.load('puropera.mtl', function (materials) {
      materials.preload();

      var objLoader = new THREE.OBJLoader();	// OBJローダの定義
      objLoader.setMaterials(materials);		// OBJローダにマテリアルを代入
      objLoader.setPath("puropera/");		// OBJファイルへのパスを設定
      //OBJファイルの読み込み
      objLoader.load('puropera.obj', function (object) {
        object.scale.set(0.1, 0.1, 0.1);	// スケール変更
        mesh1 = object;
        mesh1.position.z = 17;
        mesh1.position.y = 7;
        mesh1.position.x = 0;
        mesh1.rotation.x = -2.05* Math.PI;
        scene.add(mesh1);		// シーンに追加
      });
    });
    //針山
    var mesh2;//読み込んだオブジェクトを格納する変数//

    var mtlLoader= new THREE.MTLLoader();	// MTLローダの定義
    mtlLoader.setPath("hariyama/");	// MTLファイルへのパスを設定

    // MTLファイルの読み込み
    mtlLoader.load('hariyama.mtl', function (materials) {
      materials.preload();

      var objLoader = new THREE.OBJLoader();	// OBJローダの定義
      objLoader.setMaterials(materials);		// OBJローダにマテリアルを代入
      objLoader.setPath("hariyama/");		// OBJファイルへのパスを設定
      //OBJファイルの読み込み
      objLoader.load('hariyama.obj', function (object) {
        object.scale.set(10,5,10);	// スケール変更
        mesh2 = object;
        mesh2.position.z = 0;
        mesh2.position.y = -200;
        mesh2.position.x = 0;
        scene.add(mesh2);		// シーンに追加
      });
    });

    //左ミサイル
    var mesh3;//読み込んだオブジェクトを格納する変数//

    var mtlLoader= new THREE.MTLLoader();	// MTLローダの定義
    mtlLoader.setPath("misairu/");	// MTLファイルへのパスを設定

    // MTLファイルの読み込み
    mtlLoader.load('misairu.mtl', function (materials) {
      materials.preload();

      var objLoader = new THREE.OBJLoader();	// OBJローダの定義
      objLoader.setMaterials(materials);		// OBJローダにマテリアルを代入
      objLoader.setPath("misairu/");		// OBJファイルへのパスを設定
      //OBJファイルの読み込み
      objLoader.load('misairu.obj', function (object) {
        object.scale.set(0.1,0.1,0.1);	// スケール変更
        mesh3 = object;
        mesh3.position.z = 3;
        mesh3.position.y = 2;
        mesh3.position.x = -8;
        scene.add(mesh3);		// シーンに追加
      });
    });
    //右ミサイル
    var mesh4;//読み込んだオブジェクトを格納する変数//

    var mtlLoader= new THREE.MTLLoader();	// MTLローダの定義
    mtlLoader.setPath("misairu/");	// MTLファイルへのパスを設定

    // MTLファイルの読み込み
    mtlLoader.load('misairu.mtl', function (materials) {
      materials.preload();

      var objLoader = new THREE.OBJLoader();	// OBJローダの定義
      objLoader.setMaterials(materials);		// OBJローダにマテリアルを代入
      objLoader.setPath("misairu/");		// OBJファイルへのパスを設定
      //OBJファイルの読み込み
      objLoader.load('misairu.obj', function (object) {
        object.scale.set(0.1,0.1,0.1);	// スケール変更
        mesh4 = object;
        mesh4.position.z = 3;
        mesh4.position.y = 2;
        mesh4.position.x = 8;
        scene.add(mesh4);		// シーンに追加
      });
    });

    render();
    function render() {
      stats.update();
      //アニメーション追加
      if (mesh) {
        mesh.position.z +=controls.positionSpeed;
        mesh.position.z -=controls.backSpeed;
        mesh.position.y +=controls.highSpeed;
        mesh.position.y -=controls.lowSpeed;
        mesh.position.x -=controls.rightSpeed;
        mesh.position.x +=controls.leftSpeed;
      }
      if (mesh1) {
        mesh1.rotation.z +=controls.rotationSpeed;
        mesh1.position.z +=controls.positionSpeed;
        mesh1.position.z -=controls.backSpeed;
        mesh1.position.y +=controls.highSpeed;
        mesh1.position.y -=controls.lowSpeed;
        mesh1.position.x -=controls.rightSpeed;
        mesh1.position.x +=controls.leftSpeed;
      }
      if(mesh3){
        mesh3.position.z +=controls.positionSpeed;
        mesh3.position.z +=controls.misairuSpeed;
        mesh3.position.z -=controls.backSpeed;
        mesh3.position.y +=controls.highSpeed;
        mesh3.position.y -=controls.lowSpeed;
        mesh3.position.x -=controls.rightSpeed;
        mesh3.position.x +=controls.leftSpeed;
      }
      if(mesh4){
        mesh4.position.z +=controls.positionSpeed;
        mesh4.position.z +=controls.misairuSpeed;
        mesh4.position.z -=controls.backSpeed;
        mesh4.position.y +=controls.highSpeed;
        mesh4.position.y -=controls.lowSpeed;
        mesh4.position.x -=controls.rightSpeed;
        mesh4.position.x +=controls.leftSpeed;
      }

      requestAnimationFrame(render);
      renderer.render(scene, camera);
    }
    function initStats() {
      var stats = new Stats();
      stats.setMode(0);

      stats.domElement.style.position = 'absolute';
      stats.domElement.style.left = '0px';
      stats.domElement.style.top = '0px';
      document.getElementById("Stats-output").appendChild(stats.domElement);
      return stats;
    }
  }
  window.onload = init;
</script>
</body>
</html>

オブジェクトの作成

機体

機体は主にmayaを用いて作成。機体は座標(x,y,z)=(0.0.0)に配置。

プロペラ

プロペラは主にAUTODESK TINKERCADを用いて作成。プロペラは座標(x,y,z)=(0,7,17)に配置。

山はAUTODESK TINKERCADを用いて作成。山は座標(x,y,z)=(0,-200,0)に配置。

ミサイル

ミサイルはAUTODESK TINKERCADを用いて作成。右ミサイルは座標(x,y,z)=(8,-2,3)に配置。左ミサイルは座標(x,y,z)=(-8,-2,3)に配置。

AUTODESK TINKERCADはMayaに比べて、機能がシンプルで使いやすく、あらかじめ作られていたテンプレートを使用することができるので、時間短縮につながる。細かい作業はTINKERCADでは難しいので、Mayaを使用する。

アニメーションの実装

機体全体のアニメーション

機体の前後左右、高さ、低さの速度調整を可能にした。可能にするためにGUI(positionSpeed、backSpeed、highSpeed、lowSpeed、rightSpeed、leftSpeed)を追加した。z軸方向が前後、y軸方向が高さ、x軸方向が左右を表している。positionSpeedは+z方向、backSpeedは-z方向を表している。highSpeedは+y方向、lowSpeedは-y方向を表している。rightSpeedは-x方向、leftSpeedは+x方向を表している。これらすべての速度は0から1.00まで調整できる。

プロペラのアニメーション

プロペラの回転速度の調整を可能にした。可能にするためにGUI(rotationSpeed)を追加した。rotationSpeedは+z軸中心をに回転することを表している。速度は0から10000まで調整できる。

ミサイル発射のアニメーション

ミサイルの発射を可能にした。可能にするためにGUI(misairuSpeed)を追加した。misairuSpeedは+z軸方向を表している。

工夫した点

GUIとマウス操作の共存

ライブラリでTrackballControls.jsを用いてカメラ操作をするとGUIの操作ができないようになる。そこでライブラリをOrbitControls.jsを用いるとGUIも操作できるようになる。視点変更の方法は、左クリックを押しながらマウスを動かすと視点の角度を変えることができる。右クリックを押しながらマウスを動かすと視点を並行移動させることができる。

プロペラの回転軸

プロペラの回転軸はソースコードを変更するだけでは回転軸は調整するのは難しい。
たとえ、プロペラの位置を座標(0,0,0)にしたとしても回転軸が変わらない。そこでCADでオブジェクトを作成するときに注意が必要である。CADでオブジェクトを作成するときは位置を原点にする必要がある。特に回転するアニメーションを実装したいときはオブジェクトを作る際は必ず原点に配置する。そうすることで、ソースコードでオブジェクトの座標を移動させたとしても、オブジェクト中心で回転することができる。

背景・照明の実装

背景は空を表現したかったので、renderer.setClearColorを用いた。カラーコードは87cafaを使用した。
太陽の光を表現するために、spotLightを用いた。カラーコードはfafad2を使用した。太陽の光なので機体の正面上から当たるように座標(x,y,z)=(0,1000,1000)に設定した。

考察

ミサイル発射の改善点

改善点として、ミサイルを発射することに成功したが、連射することができないことである。これを改善するために考えられることはミサイルのオブジェクトをたくさん読み込むと少しは改善されると思う。ミサイルを同じ座標にたくさん読み込むことで無限に発射はできないが発射する回数は多くすることができる。ただし、オブジェクトをたくさん読み込むということは動作を重くする原因になるので今回はしなかった。一番良い方法はif文を用いてミサイルを発射したとき、ミサイルのオブジェクトを読み込むことであると思う。そうすることで連射も可能になり、動作も重たくならずに済むことが可能である。今回はエラーが発生したので実装することができなかった。

機体の動き方の改善点

実際に動画とかで戦闘機の動き方を見て、自分の作品を比べると大きな違いが生じた。上昇、下降、前進は大きな違いはなかったが、左右の移動で大きな違いが生じた。戦闘機の場合、機体を傾けて左右に曲がるが、自分の作品の場合、傾くことなく曲がっているので不自然な動きになってしまった。改善するためには、rotationを用いて、少し機体を回転することで少しは改善することができると思う。しかし、機体の傾けた角度によって、曲がり方を変えるのは非常に難しいと思う。機体を傾けたとしても、進む方向はx軸方向、y軸方向、z軸方向で表現する必要がある。つまり、曲がるときに高さは関係ないので、x軸方向とz軸方向に進むスピードをうまく合成させることで機体の傾きにあった曲がり方ができるということである。

まとめ

CADで戦闘機を作成して、アニメーションを実装するためにWebGLを用いた。今回、アニメーションを実装する上で大切なことが3つ分かった。
1つ目が動かしたいオブジェクトごとに作成するということである。例えば、戦闘機のプロペラやミサイルをまとめて作成すると、別々で動かすことができない。そのため、どのオブジェクトにどのような動きを与えたいのかをあらかじめ認識しておく必要がある。
2つ目がオブジェクト中心で回転させるためには、CAD内で座標を原点にする必要がある。そうすることで、ソースコードでオブジェクトの位置を変えたとしても、オブジェクト中心で回転することができる。
3つ目がGUIとマウス操作の共存である。GUIとマウスによる視点操作を実装したいときは、ライブラリのOrbitControls.jsを使用する必要がある。TrackballControls.jsではGUIの数値設定がうまくできないので、GUIとマウスによる視点操作を実装したいときはOrbitControls.jsを使用する。