【Three.js】環境マッピングその1

スゴクオモシロイヨ
環境マッピングとは
3D空間内の物体の表面に周囲の風景を"擬似的に"映り込ませる技術です。
擬似的じゃなくて本気のヤツはレイトレーシング(光線追跡法)というカッコイイ名前の手法を使うらしいです。チラっと調べたんですけどヤバそうなのでスルー。もうね、何言ってんのか全くわかりません。
そんなワケで擬似的な方でがんばってみます。
今回やること
内壁に風景画像を貼り付けた立方体を作って、その中にカメラを設置するところまでやってみます。
この立方体のことをskyboxって言うみたいですね。このskyboxの中にカメラをおくと、全方位に風景が広がっているので、まるで自分がその場所にいるかのような錯覚を起こさせることができるとか。
初めてのお店に行くときビクビクしてしまう僕としては、店内の様子がコレで見れたら不安も少し和らぐというもの。
できた

デモサイトはこちら。
先にコードを全部載せて、その後にポイントの解説を入れていきます。
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>横浜</title>
  <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  <script type="text/javascript" src="libs/three.min.js"></script>
  <script type="text/javascript" src="libs/renderers/CSS3DRenderer.js"></script>
  <script type="text/javascript" src="libs/controls/OrbitControls.js"></script>
  <style>
      html, body {
          margin: 0;
          overflow: hidden;
          height: 100%;
      }
      #screen {
        height: 100%;
      }
  </style>
</head>
<body>
  <!-- 3D空間描画領域 -->
  <div id="screen"></div>
  
  <script type="text/javascript">
    window.onload = function() {
      /***** シーン *****/
      var scene = new THREE.Scene();
      
      /***** カメラ *****/
      var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10000);
      camera.position.z = 1;
      camera.lookAt(scene.position);
      /***** レンダラー *****/
      var renderer = new THREE.WebGLRenderer();
      renderer.setSize(window.innerWidth, window.innerHeight);
      document.getElementById('screen').appendChild(renderer.domElement);
      /***** カメラのコントロール *****/
      var orbitControls = new THREE.OrbitControls(camera);
      /***** キューブマップ(全周囲の風景画像)の作成 *****/
      var path = 'assets/textures/cubemap/yokohama/';
      var urls = [
        path + 'posx.jpg', //右
        path + 'negx.jpg', //左
        path + 'posy.jpg', //上
        path + 'negy.jpg', //下
        path + 'posz.jpg', //前
        path + 'negz.jpg'  //後
      ]
      var cubeTextureLoader = new THREE.CubeTextureLoader();
      var textureCube = cubeTextureLoader.load(urls);
      /***** キューブマップを内側に貼り付けた立法体の作成 *****/
      var shader = THREE.ShaderLib['cube'];
      shader.uniforms['tCube'].value = textureCube;
      var material = new THREE.ShaderMaterial({
        fragmentShader: shader.fragmentShader,
        vertexShader: shader.vertexShader,
        uniforms: shader.uniforms,
        depthWrite: false,
        side: THREE.BackSide
      });
      var cubeMesh = new THREE.Mesh(new THREE.BoxGeometry(100, 100, 100), material);
      scene.add(cubeMesh);
      /***** 関数 *****/
      // 描画関数
      var clock = new THREE.Clock();
      function render() {
        var delta = clock.getDelta();
        orbitControls.update(delta);
        requestAnimationFrame(render);
        renderer.render(scene, camera);
      }
      /***** イベントリスナー *****/
      // ウィンドウサイズ変更
      window.addEventListener("resize", function() {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(window.innerWidth, window.innerHeight);
        render();
      });
      // 描画
      render();
    }
  </script>
</body>
</html>キューブマップを作る
キューブマップというのは全方位の風景画像のこと。上下左右前後6枚の画像が必要になります。
今回はHumusというサイトからダウンロードしました。ホントは自分のスマホで撮ったパノラマ写真からキューブマップを作りたかったんですけど、上下の画像が無いせいかうまくできませんでした。また今度チャレンジしてみましょう。
で、キューブマップを作ってるのがコレ↓。
/***** キューブマップ(全周囲の風景画像)の作成 *****/
var path = 'assets/textures/cubemap/yokohama/';
var urls = [
  path + 'posx.jpg', //右
  path + 'negx.jpg', //左
  path + 'posy.jpg', //上
  path + 'negy.jpg', //下
  path + 'posz.jpg', //前
  path + 'negz.jpg'  //後
]
var cubeTextureLoader = new THREE.CubeTextureLoader();
var textureCube = cubeTextureLoader.load(urls);まずは6枚の画像の配列を作ります。コード内のコメントにもあるように右左上下前後の順番にしないとダメです。そういう決まりです。「なんで?」とか思わないように。
その配列をTHREE.CubeTextureLoaderのload関数に渡してあげるとキューブマップのできあがりです。
マテリアルを作る
マテリアルというのは物体の表面のこと。赤なのか青なのか、ザラザラしてるのかツルっとしてるのか、みたいなのを決めるものです。
ちなみに物体の形状を決めるものはジオメトリと言い、ジオメトリとマテリアルを使って物体(メッシュ)を作ります。
マテリアルとメッシュを作るところ↓。
/***** キューブマップを内側に貼り付けた立法体の作成 *****/
var shader = THREE.ShaderLib['cube'];
shader.uniforms['tCube'].value = textureCube;
var material = new THREE.ShaderMaterial({
  fragmentShader: shader.fragmentShader,
  vertexShader: shader.vertexShader,
  uniforms: shader.uniforms,
  depthWrite: false,
  side: THREE.BackSide
});
var cubeMesh = new THREE.Mesh(new THREE.BoxGeometry(100, 100, 100), material);
scene.add(cubeMesh);“THREE.ShaderLib['cube’]"というシェーダーを使っていますね。
シェーダー?
シェーダーというのはシェーディング(陰影処理)を行うプログラムで、CPUではなくGPUで処理されるもの。GPUは大量の情報を一気に処理するのが得意で、CPUをスポーツカーとするならGPUはバスと言える。
……あやしくなってきましたね。もうちょっとだけ調べてみましょう。
今回使っている"THREE.ShaderLib['cube’]"というのはキューブマップに基づいて環境を作成できる特殊なシェーダー。この特殊なシェーダーを使う時は"THREE.ShaderMaterial"というマテリアルを使います。
……まだイケるか?
“THREE.ShaderMaterial"のコンストラクタで設定しているのは頂点シェーダーとフラグメント(ピクセル)シェーダー。シェーダーの処理は、頂点シェーダーで処理したデータをフラグメントシェーダーに送り、フラグメントシェーダーで処理したデータをディスプレイに描画する、といった流れになります。
……よ、よーし。もうやめよう。これ以上深追いするのは危険だ。当初の目的を見失ってはいけない。僕はただ、内側に画像を貼り付けた立方体を作りたかっただけなのだ。そうなのだ。逃げる口実もある。僕はただ、内側に画像を貼り付けた立方体を作りたかっただけなのだ。
とりあえずはコレで完成。
おまけコーナー
今回ハマった箇所とその解決方法です。
今回のHTMLの構造はこんな感じ↓。
<html>
  <body>
    <div id="screen">
      <canvas>
    </div>
  </body>
</html>“canvas"というのがJavaScript側で作った要素です。この"canvas"の高さと、親要素の"div"の高さが同じにならなくて困りました。iPhone6/7/8の例でいうと、"canvas"は667pxなのに"div"は674px。このせいで縦方向に7px分だけスクロールできてしまって画面がガクガクするのです。
CSSでこうしたら解決しました↓。
html, body {
  margin: 0;
  overflow: hidden;
  height: 100%;
}
#screen {
  height: 100%;
}“html"と"body"と"div"の高さを100%にしてあげたら解消しました。よく分かりませんが、忘れないように書いておきました。
あとがき
シェーダーって難しそうですね。「もっと知りたい」って思う理由がないと多分ムリ。だからその時がくるまでそっとしておきます。
次は今回作った3D空間の中に物体を作って、周りの風景を映り込ませたいと思います。ありがとうございました。
おしゃれ度
★★☆☆☆

ディスカッション
コメント一覧
まだ、コメントがありません