テクスチャ・マップを工夫して表示品質の向上と高速化を両立する - Overpass APIとthree.jsで地図を3D表示(4)

公開:2017-05-07 18:43
更新:2020-02-15 04:37
カテゴリ:open steet map,three.js,overpass api,overpass apiとthree.jsで地図を3d表示

前回はジオメトリをまとめることによって表示の高速化を達成したが、そうするとマテリアルが1種類しか選べず、表示品質が劣化してしまった。

これを何とかして、高速化した状態を維持したまま、前々回の表示品質並みの表現ができるように工夫してみた結果が以下のスクリーンショットである。

前回同様、そこそこぐりぐり動く。川は公園・森などもテクスチャを貼ってみた。

工夫した点

表示を高速化するために建物のジオメトリを統合したが、そのせいでマテリアルが1種類しか指定できない。この制約の中でできそうなことを考えた。結果以下のアイデアが浮かんだ。

・1つのテクスチャーマップにいろいろなテクスチャーをまとめ、それをUV座標で出しわける。

というものである。つまりは以下のようなテクスチャーを用意する。

例えば四角形のポリゴンに、左上のビルのテクスチャーを貼りたければ、UV座標を(0,0),(0.25,0.0),(0.25,0.25),(0.0,0.25)のように指定すればよい。

建物は平面図からTHREE.Shapeを作り、THREE.ExtrudeGeometryで押し出して作り出している。高さデータや階数のデータがあれば引数のamountに設定し、そうでなければランダムに高さをamountに設定して押し出す。よって表現される3D画像はかなりいい加減なものである。

でこのTHREE.ExtrudeGeometryに指定できるオプションでUVGeneratorというのがある。
これはジオメトリを押し出したときに同時に面のUVを生成するためのメソッドを持つオブジェクトである。デフォルトではWorldUVGeneratorがセットされる。 ただこのWorldUVGeneratorではきちんとテクスチャーマップを貼り付けることができない。
(なぜなのかはちゃんと理解できていない。。(^ ^!) ) なのでBoundingUVGeneratorというクラスをどこかで見つけて、それを修正して使っている。

このメソッド名を見ると、generateTopUV()が上面のUVを返し、generateSideWallUV()が側面のUVを返すメソッドである。

そこでこのBoundingUVGeneratorをカスタマイズして、任意のテクスチャーのUVを返すように工夫してみた。

const MAX_TEX_NUM = 16; // 4 x 4 = 16 cell
const TEX_DIV = 4;// UV座標の分割数
const TEX_DIV_R = 1 / TEX_DIV;// UV座標の分割数の逆数 

class BoundingUVGenerator {
  constructor() {
  }
  // THREE.ExtrudeGeometryの前に呼び出す
  setShape({
    extrudedShape, // 押し出すShape
    extrudedOptions,// THREE.ExtrudeGeometryのオプション
    // 上面のテクスチャーインデックス。既定値はランダム
    texIndexTop = (Math.random() * TEX_DIV) | 0 + 8
    // 側面のテクスチャーインデックス。既定値はランダム
    , texIndexSide = (Math.random() * TEX_DIV) | 0 }
    /*
    テクスチャは以下のように分割され、インデックスがつけられている。
    そのインデックスを指定することによってその範囲のUV座標をマップする。
    +----+----+----+----+
    | 03 | 02 | 01 | 00 |
    +----+----+----+----+
    | 07 | 06 | 05 | 04 |
    +----+----+----+----+
    | 11 | 10 | 09 | 08 |
    +----+----+----+----+
    | 15 | 14 | 13 | 12 |
    +----+----+----+----+
    */

  ) {
    texIndexTop = MAX_TEX_NUM - texIndexTop - 1;
    texIndexSide = MAX_TEX_NUM - texIndexSide - 1;
    this.extrudedShape = extrudedShape;
    this.bb = new THREE.Box2();
    this.texIndexTopV = Math.floor(texIndexTop / TEX_DIV) * TEX_DIV_R;
    this.texIndexTopU = (texIndexTop % TEX_DIV) * TEX_DIV_R;
    this.texIndexSideV = Math.floor(texIndexSide / TEX_DIV) * TEX_DIV_R;
    this.texIndexSideU = (texIndexSide % TEX_DIV) * TEX_DIV_R;
    this.bb.setFromPoints(this.extrudedShape.extractAllPoints().shape);
    this.extrudedOptions = extrudedOptions;
  }

  generateTopUV(geometry, vertices, indexA, indexB, indexC) {
    const ax = vertices[indexA * 3],
      ay = vertices[indexA * 3 + 1],

      bx = vertices[indexB * 3],
      by = vertices[indexB * 3 + 1],

      cx = vertices[indexC * 3],
      cy = vertices[indexC * 3 + 1],

      bb = this.bb,//extrudedShape.getBoundingBox(),
      bbx = (bb.max.x - bb.min.x) * TEX_DIV,
      bby = (bb.max.y - bb.min.y) * TEX_DIV;


    return [
      new THREE.Vector2((ax - bb.min.x) / bbx + this.texIndexTopU, (TEX_DIV_R - (ay - bb.min.y) / bby) + this.texIndexTopV),
      new THREE.Vector2((bx - bb.min.x) / bbx + this.texIndexTopU, (TEX_DIV_R - (by - bb.min.y) / bby) + this.texIndexTopV),
      new THREE.Vector2((cx - bb.min.x) / bbx + this.texIndexTopU, (TEX_DIV_R - (cy - bb.min.y) / bby) + this.texIndexTopV)
    ];
  }

  generateSideWallUV(geometry, vertices, indexA, indexB, indexC, indexD) {
    const ax = vertices[indexA * 3],
      ay = vertices[indexA * 3 + 1],
      az = vertices[indexA * 3 + 2],

      bx = vertices[indexB * 3],
      by = vertices[indexB * 3 + 1],
      bz = vertices[indexB * 3 + 2],

      cx = vertices[indexC * 3],
      cy = vertices[indexC * 3 + 1],
      cz = vertices[indexC * 3 + 2],

      dx = vertices[indexD * 3],
      dy = vertices[indexD * 3 + 1],
      dz = vertices[indexD * 3 + 2];

    const amt = this.extrudedOptions.amount * TEX_DIV,
      bb = this.bb,//extrudedShape.getBoundingBox(),
      bbx = (bb.max.x - bb.min.x) * TEX_DIV,
      bby = (bb.max.y - bb.min.y) * TEX_DIV;

    if (Math.abs(ay - by) < 0.01) {
      return [
        new THREE.Vector2(ax / bbx + this.texIndexSideU, az / amt + this.texIndexSideV),
        new THREE.Vector2(bx / bbx + this.texIndexSideU, bz / amt + this.texIndexSideV),
        new THREE.Vector2(cx / bbx + this.texIndexSideU, cz / amt + this.texIndexSideV),
        new THREE.Vector2(dx / bbx + this.texIndexSideU, dz / amt + this.texIndexSideV)
      ];
    } else {
      return [
        new THREE.Vector2((ay / bby) + this.texIndexSideU, az / amt + this.texIndexSideV),
        new THREE.Vector2((by / bby) + this.texIndexSideU, bz / amt + this.texIndexSideV),
        new THREE.Vector2((cy / bby) + this.texIndexSideU, cz / amt + this.texIndexSideV),
        new THREE.Vector2((dy / bby) + this.texIndexSideU, dz / amt + this.texIndexSideV)
      ];
    }
  }
};

これによって、1つのマテリアルで疑似的に複数のテクスチャを貼り付けることができるようになった。

デモ

https://bl.ocks.org/sfpgmr/raw/28fcf4d3372e8c96429452b1919e5b5b/

ソースコード

https://bl.ocks.org/sfpgmr/28fcf4d3372e8c96429452b1919e5b5b

今後

建物の平面はいびつであることが多いので、きちんとテクスチャーをマッピングするにはもっと工夫が必要だ。さらにはビルの高さに応じてテクスチャーマップのリピート回数を工夫したりとかしないといけないし、まだ影も投影できていない。ゲーム背景として現実のマップ情報から自動生成するのはちょっと敷居が高いような気もしてきた。。

同じようにOpenStreetMapの情報を使ってBlenderで街シーンを作るチュートリアルって結構あって、それを見ているとある程度のリアルさを求めるのであればある程度モデリングせんといかんのなかなぁ。。とか思い始めてもいる。

しかしながら、ブラウザでこんなに多数のポリゴンをぐりぐり動かせるとは、すごい時代になったものだ。。