windowサイズいっぱいに広げたPlaneのテクスチャにbackground-size:coverのような挙動をさせる。

この記事は、[Three.js Advent Calendar 2016](http://qiita.com/advent-calendar/2016/threejs) 1日目の記事です。

## CSS background-size:cover とは

background-sizeは背景画像の大きさを決めるCSSのプロパティです。
https://developer.mozilla.org/ja/docs/Web/CSS/background-size

このプロパティに指定できる値のうち、coverの挙動については以下のような説明がされています。

> このキーワードは、背景画像が背景配置領域と同じか大きな幅と高さを持つことが保証される範囲で、なるべく小さくすべきであることを示します。

要はbackground-sizeが指定されたボックスのサイズがいずれの縦横比であっても、サイズいっぱいにフィットして背景画像が表示されるようになります。

ここでは、windowsサイズいっぱいに広げたcanvas上に、同様に広げたTHREE.PlaneGeometryのMeshを置いて、そのMeshに貼り付けたTextureにbackground-size:coverとおなじような挙動をさせる、というのをやります。

## windowサイズいっぱいに広がるPlaneのMeshをつくる

### JavaScript

    const uniforms = {
      resolution: {
        type: 'v2',
        value: new THREE.Vector2(window.innerWidth, window.innerHeight),
      },
      imageResolution: {
        type: 'v2',
        value: new THREE.Vector2(2048, 1356),
      },
      texture: {
        type: 't',
        value: texture, // textureのロード処理は割愛します。
      },
    };
    const plane = new THREE.Mesh(
      new THREE.PlaneBufferGeometry(2, 2),
      new THREE.RawShaderMaterial({
        uniforms: this.uniforms,
        vertexShader: glslify('cover.vs'), // glslifyの処理についても割愛します。
        fragmentShader: glslify('cover.fs'),
        transparent: true,
      })
    );

uniform resolution は画面の解像度、
uniform imageResolution は背景画像の解像度、
uniform texture は背景画像のファイルを読み込んでTHREE.texture化したものを指します。
また、PlaneBufferGeometryのサイズを2×2にするのがちょっとしたコツです。

[glslify](https://github.com/stackgl/glslify)は外部ファイル化したシェーダを読み込むための browserify transform モジュールです。本題と逸れますのでここでの解説は省きます。

### 頂点シェーダ

    attribute vec3 position;
    attribute vec2 uv;

    varying vec2 vUv;

    void main(void) {
      vUv = uv;
      gl_Position = vec4(position, 1.0);
    }

頂点シェーダの記述はシンプルです。
2x2のサイズで作成したPlaneBufferGeometryは両辺とも (-1.0 〜 1.0） の範囲で頂点を持っていますので、このgeometryに対し座標変換をまったく行わないことで、canvasのサイズにぴったり合わさるようにPlaneを表示させることができます。

### フラグメントシェーダ

ここがキモです。

さきほどまでに作成した頂点シェーダに対してそのままtextureを貼り付けてしまうと、以下のような見た目になってしまいます。

![FireShot Capture 87 - CodePen - (incomplete) background-imag_ - https___codepen.io_ykob_full_BQmLVN_.png](https://qiita-image-store.s3.amazonaws.com/0/44895/36664f48-f7e9-4078-e186-683f3e72c968.png)

<p>See the Pen <a href="https://codepen.io/ykob/pen/BQmLVN/">(incomplete) background-image fit to the window size made with three.js</a> by yoichi kobayashi (<a href="http://codepen.io/ykob">@ykob</a>) on <a href="http://codepen.io">CodePen</a>.</p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js"></script>

これに対して background-size:cover のような挙動をさせるために、フラグメントシェーダを以下のように記述します。

    uniform vec2 resolution;
    uniform vec2 imageResolution;
    uniform sampler2D texture;

    varying vec2 vUv;

    void main(void) {
      vec2 ratio = vec2(
          min((resolution.x / resolution.y) / (imageResolution.x / imageResolution.y), 1.0),
          min((resolution.y / resolution.x) / (imageResolution.y / imageResolution.x), 1.0)
        );

      vec2 uv = vec2(
          vUv.x * ratio.x + (1.0 - ratio.x) * 0.5,
          vUv.y * ratio.y + (1.0 - ratio.y) * 0.5
        );
      gl_FragColor = texture2D(texture, uv);
    }

要点は1点のみ、uvをそのまま使わず、windowと画像の解像度を比較して縦横の辺どちらを余らせればwindow全体にフィットするようになるのかを計算して、その結果をuvに反映させることです。
`vec2 ratio`で解像度を比較し、`vec2 uv`で中央に画像が配置されるようにuvを調整しています。

以下がその結果です。

![FireShot Capture 88 - CodePen - background-image fit to the _ - https___codepen.io_ykob_full_BQmLeK_.png](https://qiita-image-store.s3.amazonaws.com/0/44895/d99add9e-e9b3-326e-5407-81c7351a3d69.png)

<p>See the Pen <a href="https://codepen.io/ykob/pen/BQmLeK/">background-image fit to the window size made with three.js</a> by yoichi kobayashi (<a href="http://codepen.io/ykob">@ykob</a>) on <a href="http://codepen.io">CodePen</a>.</p>

画像がbackground-size:coverのような挙動になっていますね。
これで基本の処理は完了です。

## resizeイベントに対応させる。

windowのサイズはユーザーの操作によっていかようにも変更することができてしまうので、これまでに作成した処理もwindowのresizeイベントに対応させる必要があります。

これは単純に、JSでcanvas、camera、Meshのuniform、rendererをそれぞれ以下のように更新してあげればよいです。

    const resizeWindow = () => {
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      plane.mesh.material.uniforms.resolution.value.set(window.innerWidth, window.innerHeight);
      renderer.setSize(window.innerWidth, window.innerHeight);
    }

## 使いみち

描画したシーンをもとにPost Process Effectを施すなどして、ウェブサイトの背景を彩りましょう。
CSSだけではできない表現ができて楽しいですね。