THREE.ShaderMaterialによる改良版が一応完成に至る。- Overpass APIとthree.jsで地図を3D表示(7)

公開:2017-05-21 08:53
更新:2017-07-29 13:18
カテゴリ:overpass apiとthree.jsで地図を3d表示

バグの発生

下の動画を見ていただくとわかると思うが、上面のテクスチャマップがうまくいかない問題が発生した。

この問題はテクスチャ番号として「3」を指定した時に発生する。テクスチャ番号0-2ではなぜか発生しない。

今一度、今回の取り組みを解説しておくと

テクスチャマッピングはShaderMaterialを使ってやや特殊な方法で行っている。 頂点情報のattributeとして、テクスチャ・インデックス(texIndex)、フロア階数(amount)を持たせ、それをフラグメントシェーダーに渡すことで、フロア階数に応じ、壁面は1フロア分のテクスチャを階数分繰り返すようにマッピングし、上面と底面は普通にテクスチャ・マッピングしているのである。

テクスチャ・ビットマップは下のように4×4セルのビットマップとなっている。大きさは2048x2048pixelである。ビルの壁面用としては00-03、上面・底面用のテクスチャとしては08-11を使用する。

コードの実装内容は以下の通りである。

1.ExtrudeBufferGeometryで作った建物geometryオブジェクトにtexIndex、amountのattributeを追加する。

      const texIndexs = new Uint16Array(geometry.attributes.position.count);
      const amounts = new Uint16Array(geometry.attributes.position.count);
      geometry.addAttribute('texIndex', new THREE.BufferAttribute(texIndexs, 1,false));
      geometry.addAttribute('amount', new THREE.BufferAttribute(amounts, 1,false));

2.追加したtexIndexに建物のテクスチャ・インデックス00-03をランダムにセット、amountには建物のフロア階数をセットする。

      for (let i = 0, e = geometry.attributes.position.count; i < e; ++i) {
        texIndexs[i] = data.texNo;
        amounts[i] = data.amount;
      }

3.頂点シェーダーでは建物のテクスチャ・インデックスと建物のフロア階数をフラグメント・シェーダーに引き渡す。

.
.
.
// テクスチャ・インデックス
attribute float texIndex;
// フロア階数情報
attribute float amount;
varying float vTexIndex;
varying float vAmount;
varying vec3 vNormalView;
.
.
.
void main(){
.
.
.
  // テクスチャ番号をフラグメントシェーダーに引き渡す
  vTexIndex = texIndex;
  // フロア階数情報をフラグメントシェーダーに引き渡す
  vAmount = amount;
  // 法線ベクトルをフラグメントシェーダーに引き渡す
  vNormalView = normal;
}

4.フラグメントシェーダーでは引き渡された建物のテクスチャ・インデックス(texIndex)とフロア階数(amount)をもとにテクスチャを読み出すためのuv座標を求め、テクスチャを読み出す。

.
.
.

varying float vTexIndex;
varying float vAmount;
varying vec3 vNormalView;

void main() {
.
.
.
 // 
  float texIdx;
  if(vNormalView.z != 0.0){// 法線ベクトルのz成分があれば上面とみなす
    // 上面用のテクスチャ・インデックスを計算で求める
    texIdx = MAX_TEX_NUM - vTexIndex - 8.0 - 1.0;
  } else {
    // 壁面用のインデックスを求める
    texIdx = MAX_TEX_NUM - vTexIndex - 1.0;
  }

  //テクスチャ・インデックスからマッピング開始位置のuv座標を求める
  vec2 uv;
  uv.y = floor(texIdx / (TEX_DIV)) * TEX_DIV_R;
  uv.x = mod(texIdx,TEX_DIV ) * TEX_DIV_R ;
  // 開始位置からuv量を求める。
  vec2 vuv;
  if(vNormalView.z == 0.0){
    // 壁面の場合
    vuv = vec2(vUv.x * TEX_DIV_R,mod(vUv.y,1.0 / vAmount)* vAmount * TEX_DIV_R * 0.125/* 1つのセルは8フロア分ありそのうちの1フロア分のみを使う*/);
  } else {
    // 上面の場合
    vuv = vUv * TEX_DIV_R;
 }
  vec4 texelColor = texture2D(map, vuv + uv);

.
.
.
}

実装したものは以下にアップしてある。テクスチャ・インデックスを3に指定すると現象が再現できる。 https://bl.ocks.org/sfpgmr/e1917c9f13455b4af39226c8835094ca

原因

原因は頂点シェーダーからフラグメント・シェーダーに値を渡すときに値が補完されるためであった。
私としては値を補完せずそのまま渡してほしいが、WebGL 1.0ではできないらしい。
(WebGL 2.0ではattributeに相当するinflatsmoothを指定できるので、これで回避できそうな気もしないでもないが。。)

とりあえずは補完を打ち消すコードをフラグメント・シェーダーに入れることで解消することができた。
具体的には四捨五入するだけである。


float t = floor(vTexIndex + .5);

修正したコードは以下である。以下のコードは00-03以外のテクスチャ・インデックスを指定した場合は普通にテクスチャ・マッピングするように改良している。

https://bl.ocks.org/sfpgmr/61fe805bb2a72bda86eff955838fda94

地図の描画コードに反映させる

これを機に、GeometryからBufferGeometryに変更した。
実はここでも問題が発生した。geometryをまとめるのにBufferGeomtry.mergeを使おうとしたが、r85バージョンではこのメソッドには不具合がある。 ソースコードは以下のとおりである。


merge: function ( geometry, offset ) {

        if ( ( geometry && geometry.isBufferGeometry ) === false ) {

            console.error( 'THREE.BufferGeometry.merge(): geometry not an instance of THREE.BufferGeometry.', geometry );
            return;

        }

        if ( offset === undefined ) offset = 0;

        var attributes = this.attributes;

        for ( var key in attributes ) {

            if ( geometry.attributes[ key ] === undefined ) continue;

            var attribute1 = attributes[ key ];
            var attributeArray1 = attribute1.array;

            var attribute2 = geometry.attributes[ key ];
            var attributeArray2 = attribute2.array;

            var attributeSize = attribute2.itemSize;

            for ( var i = 0, j = attributeSize * offset; i < attributeArray2.length; i ++, j ++ ) {

                attributeArray1[ j ] = attributeArray2[ i ];

            }

        }

        return this;

    },

問題点は以下の2点である。

  1. indexを統合するコードがない。
  2. attributeの統合コードが、TypedArrayを考慮したものになっていない。

この問題点はthree.jsのIssueに載っている。
https://github.com/mrdoob/three.js/issues/6188

具体的に修正するコード例も上のIssueに載っていたが、実はこの修正例にも不具合がある。それをさらに修正したバージョンが以下である。


/***
 * @param {Float32Array} first
 * @param {Float32Array} second
 * @returns {Float32Array}
 * @constructor
 */
function Float32ArrayConcat(first, second) {
  var firstLength = first.length,
    result = new Float32Array(firstLength + second.length);

  result.set(first);
  result.set(second, firstLength);

  return result;
}

/**
 * @param {Uint32Array} first
 * @param {Uint32Array} second
 * @returns {Uint32Array}
 * @constructor
 */
function Uint32ArrayConcat(first, second) {
  var firstLength = first.length,
    result = new Uint32Array(firstLength + second.length);

  result.set(first);
  result.set(second, firstLength);

  return result;
}

THREE.BufferGeometry.prototype.merge = function (geometry) {

  if (geometry instanceof THREE.BufferGeometry === false) {

    console.error('THREE.BufferGeometry.merge(): geometry not an instance of THREE.BufferGeometry.', geometry);
    return;

  }

  var attributes = this.attributes;

  if (this.index) {

    var indices = geometry.index.array;

    var offset = this.index.array.length;

    for (var i = 0, il = indices.length; i < il; i++) {

      indices[i] = offset + indices[i];

    }

    this.setIndex(new THREE.BufferAttribute(Uint32ArrayConcat(this.index.array, indices),1));

  }

  for (var key in attributes) {

    if (geometry.attributes[key] === undefined) continue;

    const dest = attributes[key].array;
    const src = geometry.attributes[key].array;

    attributes[key].array = Float32ArrayConcat(attributes[key].array, geometry.attributes[key].array);
    attributes[key].count = attributes[key].array.length / attributes[key].itemSize;


  }
  return this;
};

ただこのメソッド、頂点数が多いととてつもなく遅くなる。なので統合するコードは別に書くことにした。

最終的な結果が以下である。

https://bl.ocks.org/sfpgmr/2eba19ac24b355c12830b3acf26600d8

改良前の画像は以下。

改良後の画像は以下である。フロア階数を反映した壁面となっているのがお分かりいただけるかと思う。

一応完成したが、もういくつか別のやり方がありそうで、ちょっとそれを試してみようと思っている。

頂点数をカウントしたら5,682,312あった。すごいね。。