S.F. Page

Programming,Music,etc...

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

前回はジオメトリをまとめることによって表示の高速化を達成したが、そうするとマテリアルが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つのマテリアルで疑似的に複数のテクスチャを貼り付けることができるようになった。

デモ

Open Street Map のデータをthree.jsにインポートする

ソースコード

テクスチャマップを工夫することによる表示品質の改善 - bl.ocks.org

今後

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

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

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

表示領域の拡大と高速化に取り組む - Overpass APIとthree.jsで地図を3D表示(3)

表示領域の拡大と高速化に取り組んでみた。 なんとか大阪環状線の周囲まで表示してみたかったので、コードをいろいろ改良してみた。

Overpass APIから受け取ったデータをthree.jsのメッシュに変換するところは処理は重いが、変換が終わればサクサク動くと思う。うちの環境(Core i7 6700,Geforce 1060)では。

実際のサンプル:

https://bl.ocks.org/sfpgmr/raw/a2d4c0e9ecc5e81d91210c15b33b0a4b/

サンプル&ソースコード:

https://bl.ocks.org/sfpgmr/a2d4c0e9ecc5e81d91210c15b33b0a4b

高速化のアプローチとしては以下を行った。

  • Overpass APIを都度呼び出すのではなく、node.jsで事前に読み込んでファイル化した。
    データが巨大かつoverpass APIのサーバにかなり負荷がかかるし、呼び出し回数の制限があるので、このようにした。 一回に呼び出すデータが巨大だと、エラーになったり無応答になったりするので、大阪環状線内のエリアを16分割して取り込むようにした。 ついでにデータを処理しやすいように最適化し、さらに圧縮しておいた。

  • ジオメトリをまとめる
    大阪環状線の周囲ともなると、表示するオブジェクトは数十万個以上となり、そのままだと遅くて実用にならない。この原因は主にドローコールの増加にある。 ドローコールの回数を減らし、描画パフォーマンスを改善するため、ジオメトリをある程度の塊にまとめた。ただマテリアルがその塊単位でしか設定できないので、前回よりも表示のクォリティは落ちている。この問題についてはアイデアがあるので、それを今後実装してみようかと思っている。

メモリ不足への対処

大阪環状線の内側ともなるとデータが膨大となり、以前のコードだとJSヒープが枯渇してしまうようである。ガベージコレクタが追い付かないのだろう。

なので以下のことを行った。

  • 不要なクロージャの除去
  • 一時変数の見直しや除去
  • 関数呼び出しを減らす
  • オブジェクト生成を減らす
  • その他処理の見直し

以上のことを行ってみたところ、Chromeでなんとかギリギリ処理できるようになった。EdgeやFirefoxのほうがメモリ枯渇に強いのか、この改良をしなくとも普通に表示できたし、この改良によってさらにパフォーマンスが上がった。 Edgeで表示したほうがChromeより軽かったりする。

ただ前回コードよりもやることを少なくしているので、表現力は落ちている。GW中は改良にいそしむつもりだ。

これである程度のめどがつけば、シューティングゲームへの取り込みを行おうと思っている。

使用したテクスチャ

下記サイトからダウンロードして使用

https://www.textures.com/download/buildingshighrise0625/104602

skybox用に下記からダウンロードして使用

https://opengameart.org/content/clouds-skybox-1

Overpass APIとthree.jsで地図を3D表示(2)

スカイボックスを追加し、ビルにテクスチャを貼り付けてみた。リアルとは言えないが、雰囲気は出てきた。

大きい範囲を描画しようとすると相当に処理が重い。最適化しないといかんかな。。まあでも縦スクロールシューティングではかなり狭い範囲を描画するので、パフォーマンス的にはそんなに問題にはならなそうだが。

屋根の面には屋根用のテクスチャを貼らないといけないんだけど、どうやればいいかまだわかっていない。テクスチャの貼り方にはさらに工夫が必要だ。

デモ:(相当処理が重いので注意

Open Street Map のデータをthree.jsにインポートする

ソースコード&デモ:(相当処理が重いので注意

Open Street Mapのデータを使用してthree.jsで可視化する(2)テクスチャーマップ - bl.ocks.org

ちょっとはまったのは、ShapeExtrudeGeometryで立体化しているのだけれども、ExtrudeGeometryで作成されたジオメトリに普通にテクスチャを貼り付けようとしてもできないというところ。

このことはThree.jsのISSUEにも載っていた。

github.com

回避策としては、UVGeneratorにBoundingUVGeneratorをセットしてExtrudeGeometryする。だがこの記事で載っているBoundingUVGeneratorは最新バージョンでは動作しないので、改良版を作った。

class BoundingUVGenerator {
  constructor(extrudedShape, extrudedOptions) {
    this.extrudedShape = extrudedShape;
    this.bb = new THREE.Box2();

    this.bb.setFromPoints(this.extrudedShape.extractAllPoints().shape);
    this.extrudedOptions = extrudedOptions;
  }
  generateTopUV(geometry, indexA, indexB, indexC) {
    var ax = geometry.vertices[indexA].x,
      ay = geometry.vertices[indexA].y,

      bx = geometry.vertices[indexB].x,
      by = geometry.vertices[indexB].y,

      cx = geometry.vertices[indexC].x,
      cy = geometry.vertices[indexC].y,

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

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

  generateBottomUV(geometry, indexA, indexB, indexC) {
    return this.generateTopUV(geometry, indexA, indexB, indexC);
  }

  generateSideWallUV(geometry, indexA, indexB, indexC, indexD) {
    var ax = geometry.vertices[indexA].x,
      ay = geometry.vertices[indexA].y,
      az = geometry.vertices[indexA].z,

      bx = geometry.vertices[indexB].x,
      by = geometry.vertices[indexB].y,
      bz = geometry.vertices[indexB].z,

      cx = geometry.vertices[indexC].x,
      cy = geometry.vertices[indexC].y,
      cz = geometry.vertices[indexC].z,

      dx = geometry.vertices[indexD].x,
      dy = geometry.vertices[indexD].y,
      dz = geometry.vertices[indexD].z;

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

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