three.jsでcanvasを使ってテキストを表示させる方法

three.jsを使ってテキストを表示させる方法はいくつかあると思います。

1. `TextGeometry`を使う
1. Bitmap Fontsを使う
1. canvasにテキストを描画してテクスチャとして使う

大きくこの3つの方法があるのかと思いますが今回は**canvasにテキストを描画してテクスチャとして使う**方法を紹介していきます。

![qiita_cap.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241879/efd74f28-aa2c-0ddd-9b54-7a7d6ce0b61b.gif)
デモは[こちら](https://mtoutside.github.io/webgl-kvlt/index2.html)
ソースコードは[こちら](https://github.com/mtoutside/webgl-kvlt/tree/master/src/kvlt02)
（本編の記事と内容が若干異なります）

## テキストを表示させる大まかな流れ

- three.jsを描画するcanvasとは別にテキストを描画するcanvasを用意
- canvasにテキストを描画しテクスチャを生成
- テキストを描画したいメッシュを作成
- 作成したメッシュにテクスチャを送る

ざっとこんな流れ。あと、テクスチャに格納することをよく**テクスチャに焼く**なんて言います。

まずは下準備。今回は平面を描画するだけなのでパースのかからない`OrthographicCamera`を使います。

```js
export default class Canvas {
  constructor() {
    this.container = document.getElementById('CanvasContainer');
    this.setConfig();

    // レンダラを作成
    this.renderer = new THREE.WebGLRenderer({
      alpha: true,
      antialias: false,
    });
    this.renderer.setSize(this.Config.width, this.Config.height);
    this.renderer.setPixelRatio(this.Config.dpr);
    this.container.appendChild(this.renderer.domElement);

    // 関数をthisでバインドして持っておく
    this.resizeFunction = this.resize.bind(this);
    this.updateFunction = this.update.bind(this);

    // リサイズイベントを設定
    window.addEventListener('resize', this.resizeFunction);

    this.scene = new THREE.Scene();
    this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, -1);

    // レンダリング開始
    // 初期化
    this.init();
  }

  init() {
    this.createMesh();
    this.start();
  }

  createMesh() {
   // 後述
  }

  setConfig() {
    this.Config = {
      width: window.innerWidth, // Canvasの幅
      height: window.innerHeight, // Canvasの高さ
      cameraZ: 1000, // カメラのz座標
      dpr: 1, // device pixel ratio
      aspectRatio: 1.0, // 画面アスペクト比
    };
    // 親要素のサイズを取得
    const domRect = this.container.getBoundingClientRect();
    const width = domRect.width;
    const height = domRect.height;

    this.Config.dpr = Math.min(window.devicePixelRatio, 2);
    this.Config.width = width;
    this.Config.height = height;
    this.Config.halfWidth = this.Config.width / 2;
    this.Config.halfHeight = this.Config.height / 2;
    this.Config.aspectRatio = this.Config.width / this.Config.height;
  }

  resizeScene() {
    this.renderer.setSize(this.Config.width, this.Config.height);
  }

  start() {
    this.resize();
    this.update();
  }

  resize() {
    this.setConfig();
    this.resizeScene();
  }

  update() {
    // 最大60fpsでレンダリングをループ
    requestAnimationFrame(this.updateFunction);
    this.time = performance.now() * 0.001;

    this.material.uniforms.time.value = this.time;

    this.renderer.render(this.scene, this.camera);
  }
}

```

## canavasにテキストを描画してテクスチャを作る

canvasを使ってテクスチャを作るには`CanvasTexture`を使います。今回は`measureText`を使って文字数に応じてcanvasの長さを可変にします。

``` テクスチャを作る.js
  /**
   * 2D Canvasからテクスチャを作成する
   * @param {Object} options
   * @param {stirng} options.text 描画したい文字列
   * @param {number} options.fontSize フォントサイズ
   * @return {Object} テクスチャを返す。
   * @memberof Canvas
   */
  createTexture(options) {
    // Canvas要素を作成
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');

    // measureTextするためいったん設定
    const fontFamily = 'monospace';
    ctx.font = `bold ${options.fontSize * Config.dpr}px '${fontFamily}'`;
    const textWidth = ctx.measureText(options.text); // 文字の横幅を取得

    // dprに対応したサイズを計算
    const width = textWidth.width;
    const height = options.fontSize * Config.dpr * 0.8; // 文字に合わせて高さを調整。ここの高さは任意で
    // 幅を指定
    canvas.width = width;
    canvas.height = height;

    // 中央にテキストを描画
    ctx.font = `bold ${options.fontSize * Config.dpr}px '${fontFamily}'`;
    ctx.textAlign = 'left';
    ctx.textBaseline = 'hanging';
    ctx.fillStyle = 'rgba(255, 255, 255, 1.0)';
    ctx.fillText(options.text, -5, 0); // 文字が途切れないように調整。数値はよしなに

    // ↓canvasの文字を確認したいとき。テキストを描画したcanvasをbodyに追加しているだけです。
    // document.body.appendChild(canvas);
    // canvas.style.backgroundColor = '#933';
    // canvas.style.position = 'relative';

    // テクスチャを作成
    const texture = new THREE.CanvasTexture(canvas);
    texture.needsUpdate = false;
    // ↓ここら辺の設定をしておかないとthree.jsでエラーが出る時がある
    texture.minFilter = THREE.LinearFilter;
    texture.magFilter = THREE.LinearFilter;
    texture.format = THREE.RGBAFormat;

    return texture;
  }
```

### テキストとcanvasのサイズについて
テキストぴったりのcanvasを作るのがなかなかうまくいかず、微調整をしています。フォントやテキストよって`ctx.fillText(options.text, -5, 0);`ここら辺の調整が必要になってきます。

``` js
    // ↓canvasの文字を確認したいとき。テキストを描画したcanvasをbodyに追加しているだけです。
    // document.body.appendChild(canvas);
    // canvas.style.backgroundColor = '#933';
    // canvas.style.position = 'relative';
```

こうやってテキストを描画したcanvasを表示させてcanvasを調整しているのですがもっとうまい方法ないのかなー・・・

## メッシュを作る
メッシュを作ります。今回はただテキストを表示させるだけなので、ジオメトリには`PlaneBufferGeometry`を、マテリアルには`RawShaderMaterial`を使用します。


### three.jsのShaderMaterialについての補足
three.jsのShaderMaterialには

- ShaderMaterial
- RawShaderMaterial

のふたつがあります。

`ShaderMaterial`ではthree.jsビルトインの`attributes`と`uniforms`を自動で付加してくれます。自動で付加されたくない場合は`RawShaderMaterial`を使います。

ビルトイン`attributes`と`uniforms`はこちら。

``` builtInVertexShader.glsl
// = object.matrixWorld
uniform mat4 modelMatrix;

// = camera.matrixWorldInverse * object.matrixWorld
uniform mat4 modelViewMatrix;

// = camera.projectionMatrix
uniform mat4 projectionMatrix;

// = camera.matrixWorldInverse
uniform mat4 viewMatrix;

// = inverse transpose of modelViewMatrix
uniform mat3 normalMatrix;

// = camera position in world space
uniform vec3 cameraPosition;

///////////////////////////////////////

// default vertex attributes provided by Geometry and BufferGeometry
attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;
```
詳しくは↓に書いてあります。
[ShaderMaterial](https://threejs.org/docs/#api/en/materials/ShaderMaterial)

あらためてこちらがメッシュを作る部分。

``` メッシュを作る.js
  createMesh() {
    const segment = 1;

    this.geometry = new PlaneBufferGeometry(2, 2, segment, segment);

    // テクスチャの作成 前述の「テクスチャを作る.js」を参照
    this.texture = this.createTexture({
      text: 'HOGE', // 描画したいテキスト
      fontSize: 130, // フォントサイズ
    });

    // マテリアルの作成
    this.material = new RawShaderMaterial({
      uniforms: {
        texture: { value: this.texture },
        time: { value: 0.0 },
        speed: { value: 1.0 },
        resolution: { value: this.Config.aspectRatio },
      },
      vertexShader: vertexShader,
      fragmentShader: fragmentShader,
      transparent: false,
    });

    this.mesh = new Mesh(this.geometry, this.material);
    this.scene.add(this.mesh);
  }
```

## シェーダー
シェーダーを見ていきます。今回は`fract`関数を使いテキストをタイル状に表示させ左方向に流します。

``` frag.glsl
precision mediump float;

uniform sampler2D texture;
uniform float time;
uniform float resolution;
uniform float speed;

varying vec2 vUv;

void main() {
  float t = time * speed;

  vec2 repeat = vec2(8.0, 8.0); // (行, 列)回繰り返し
  vec2 uv = fract(vUv * repeat + vec2(t, 0.0));

  vec3 color = texture2D(texture, uv).rgb;
  gl_FragColor = vec4(color, 1.0);
}

```

vertexShaderは特に変わったことはしていません。

```vert.glsl
precision mediump float;

uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
attribute vec3 position;
attribute vec2 uv;
varying vec2 vUv;

void main() {
  vUv = uv;
  vec3 pos = position;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
```

あとは実行するだけです。

``` main.js
import Canvas from './Canvas/_index';

document.addEventListener('DOMContentLoaded', () => {
  new Canvas();
});

```

## まとめ
というわけでcanvasを使ってテキストを描画する方法を紹介しました。割と簡単な方法なのですが、スマホだったり高解像度ディスプレイで見るとテキストがかなりギザギザしてしまいます。もっと綺麗に描画したい！となったらBitmap Fontを使ったほうがいいかもしれません。

↓再掲
デモは[こちら](https://mtoutside.github.io/webgl-kvlt/index2.html)
ソースコードは[こちら](https://github.com/mtoutside/webgl-kvlt/tree/master/src/kvlt02)
（本編の記事と内容が若干異なります）

---

参考

[techniques-for-rendering-text-in-threejs](https://css-tricks.com/techniques-for-rendering-text-with-webgl/)
[Creating text](https://threejs.org/docs/index.html#manual/en/introduction/Creating-text)
[CanvasTexture](https://threejs.org/docs/#api/en/textures/CanvasTexture)