前回はジオメトリをまとめることによって表示の高速化を達成したが、そうするとマテリアルが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で街シーンを作るチュートリアルって結構あって、それを見ているとある程度のリアルさを求めるのであればある程度モデリングせんといかんのなかなぁ。。とか思い始めてもいる。
しかしながら、ブラウザでこんなに多数のポリゴンをぐりぐり動かせるとは、すごい時代になったものだ。。