バグの発生
下の動画を見ていただくとわかると思うが、上面のテクスチャマップがうまくいかない問題が発生した。
この問題はテクスチャ番号として「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);
.
.
.
}
実装したものは以下にアップしてある。テクスチャ・インデックス(texNo)を3に指定すると現象が再現できる。
原因
原因は頂点シェーダーからフラグメント・シェーダーに値を渡すときに値が補完されるためであった。
私としては値を補完せずそのまま渡してほしいが、WebGL 1.0ではできないらしい。
(WebGL 2.0ではattribute
に相当するin
にflat
かsmooth
を指定できるので、これで回避できそうな気もしないでもないが。。)
とりあえずは補完を打ち消すコードをフラグメント・シェーダーに入れることで解消することができた。
具体的には四捨五入するだけである。
float t = floor(vTexIndex + .5);
修正したコードの結果は以下である。 以下のコードは00-03以外のテクスチャ・インデックスを指定した場合は普通にテクスチャ・マッピングするように改良している。
地図の描画コードに反映させる
これを機に、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点である。
- indexを統合するコードがない。
- 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;
};
ただこのメソッド、頂点数が多いととてつもなく遅くなる。なので統合するコードは別に書くことにした。
改良前の画像は以下。
改良後の画像は以下である。フロア階数を反映した壁面となっているのがお分かりいただけるかと思う。
一応完成したが、もういくつか別のやり方がありそうで、ちょっとそれを試してみようと思っている。
頂点数をカウントしたら5,682,312あった。すごいね。。
最終的な成果物は以下である。
動作サンプル
ソースコード・リソース
/dev/map/0006/clouds1_down.jpg
/dev/map/0006/clouds1_east.jpg
/dev/map/0006/clouds1_north.jpg
/dev/map/0006/clouds1_south.jpg
/dev/map/0006/clouds1_west.jpg
"use strict"
const http = require('http');
const fs = require('fs');
const lz = require('./lzbase62.min.js');
const d3 = require('d3');
const THREE = require('three');
const boundary = {
e: 135.5361,
n: 34.7076,
s: 34.6452,
w: 135.4601
};
// const boundary = {
// e: 135.5090,
// n: 34.7093,
// s: 34.6840,
// w: 135.4769
// };//tileToBoundary(targetTile.x, targetTile.y, targetTile.z);
// バウンダリの分割
const div = 4;
const boundaries = [];
const lonw = Math.abs(boundary.e - boundary.w) / div;
const latw = Math.abs(boundary.n - boundary.s) / div;
const project = createProjection(centroid(boundary));
for (let lon = 0; lon < div; ++lon) {
for (let lat = 0; lat < div; ++lat) {
boundaries.push(
{
e: Math.round(((lon + 1) * lonw + boundary.w) * 10000) / 10000,
w: Math.round((lon * lonw + boundary.w) * 10000) / 10000,
s: Math.round((lat * latw + boundary.s) * 10000) / 10000,
n: Math.round(((lat + 1) * latw + boundary.s) * 10000) / 10000
});
}
}
const apiServers = [
'http://overpass-api.de/api',
'http://api.openstreetmap.fr/oapi/interpreter',
'http://overpass-api.de/api',
'http://api.openstreetmap.fr/oapi/interpreter'
];
function midpoint(_arg, _arg1) {
let x1 = _arg[0], y1 = _arg[1],
x2 = _arg1[0], y2 = _arg1[1],
x = x1 - (x1 - x2) / 2,
y = y1 - (y1 - y2) / 2;
return [x, y];
};
function centroid(boundary) {
let p1 = [boundary.w, boundary.n],
p2 = [boundary.e, boundary.s];
return midpoint(p1, p2);
};
function createProjection(center) {
return d3.geoMercator().scale(6.5 * 1000 * 1000).center(center).translate([0, 0]);
};
function isArea(way) {
return way.nodes[0] == way.nodes[way.nodes.length - 1];
};
function loadOverpassData(boundary, serverIndex = 1) {
return new Promise((resolve, reject) => {
let url = `${apiServers[serverIndex]}/interpreter?data=[out:json];\n(\n node(${boundary.s},${boundary.w},${boundary.n},${boundary.e});\n way(bn);\n);\n(\n ._;\n node(w);\n);\nout;`;
http.get(url, (res) => {
let body = '';
res.setEncoding('utf8');
res.on('data', (chunk) => {
body += chunk;
});
res.on('end', (res) => {
resolve(JSON.parse(body));
});
}).on('error', (e) => {
console.log(e.message); //エラー時
reject(e);
});
}).then((rawData) => {
const acc = {
node: new Map(),
way: {
polygons: [],
lines: []
},
relation: []
};
rawData.elements.forEach(function (elem) {
switch (elem.type) {
case 'node':
acc.node.set(elem.id, elem);
break;
case 'way':
isArea(elem) ? acc.way.polygons.push(elem) : acc.way.lines.push(elem);
break;
case 'relation':
acc.relation.push(elem);
break;
}
//acc[elem.type][elem.id] = elem;
});
return acc;
});
};
let p1 = Promise.resolve();
let divi = 1;
let ps = [Promise.resolve(), Promise.resolve(), Promise.resolve(), Promise.resolve()];
for (let i = 0, e = boundaries.length / divi; i < e; ++i) {
for (let j = 0; j < divi; ++j) {
ps[j] = ps[j]
.then(loadOverpassData.bind(null, boundaries[i * divi + j], j))
.then((data) => {
data.way.polygons.forEach((d) => {
d.nodes.forEach((n, idx) => {
const t = data.node.get(n);
d.nodes[idx] = project([t.lon, t.lat]);
delete d.nodes[idx].id;
delete d.nodes[idx].type;
});
});
data.way.lines.forEach((d) => {
d.nodes.forEach((n, idx) => {
const t = data.node.get(n);
d.nodes[idx] = project([t.lon, t.lat]);
delete d.nodes[idx].id;
delete d.nodes[idx].type;
});
});
delete data.node;
let compressedData = { data: lz.compress(JSON.stringify(data)) };
fs.writeFileSync(`./map${('00000' + (i * divi + j)).slice(-5)}.json`, JSON.stringify(compressedData));
fs.writeFileSync(`./temp/map${('00000' + (i * divi + j)).slice(-5)}.json`, JSON.stringify(data, null, 2));
console.log(i * divi + j);
});
}
}
Promise.all(ps)
.then(() => {
console.log('end.');
});
/dev/map/0006/index-thumbnail.jpg
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Open Street Map のデータをthree.jsにインポートする(とりあえず完成版)</title>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/three.js/85/three.js"></script>
<script type="text/javascript" src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>
<script src="https://threejs.org/examples/js/libs/dat.gui.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.8.0/d3.min.js"></script>
<script type="text/javascript" src="./lzbase62.min.js"></script>
<script type="text/javascript" src="./main.js"></script>
<style>
body {margin:0;padding:0;overflow:hidden;}
#container {
position: relative;
}
#loading {
left:25vw;
top:50vh;
width:50vw;
position: absolute;
background: white;
color:black;
opacity: 0.5;
margin:auto;
text-align: center;
font-size:3vw;
}
</style>
</head>
<body>
<div id="container">
<div id="loading">Please wait while loading ...</div>
</div>
</body>
</html>
/*!
* lzbase62 v1.4.6 - LZ77(LZSS) based compression algorithm in base62 for JavaScript.
* Copyright (c) 2014-2015 polygon planet <polygon.planet.aqua@gmail.com>
* @license MIT
*/
!function(a,b,c){"undefined"!=typeof exports?"undefined"!=typeof module&&module.exports?module.exports=c():exports[a]=c():"function"==typeof define&&define.amd?define(c):b[a]=c()}("lzbase62",this,function(){"use strict";function a(a){this._init(a)}function b(a){this._init(a)}function c(){var a,b,c,d,e="abcdefghijklmnopqrstuvwxyz",f="",g=e.length;for(a=0;g>a;a++)for(c=e.charAt(a),b=g-1;b>15&&f.length<v;b--)d=e.charAt(b),f+=" "+c+" "+d;for(;f.length<v;)f=" "+f;return f=f.slice(0,v)}function d(a,b){return a.length===b?a:a.subarray?a.subarray(0,b):(a.length=b,a)}function e(a,b){if(null==b?b=a.length:a=d(a,b),l&&m&&o>b){if(p)return j.apply(null,a);if(null===p)try{var c=j.apply(null,a);return b>o&&(p=!0),c}catch(e){p=!1}}return f(a)}function f(a){for(var b,c="",d=a.length,e=0;d>e;){if(b=a.subarray?a.subarray(e,e+o):a.slice(e,e+o),e+=o,!p){if(null===p)try{c+=j.apply(null,b),b.length>o&&(p=!0);continue}catch(f){p=!1}return g(a)}c+=j.apply(null,b)}return c}function g(a){for(var b="",c=a.length,d=0;c>d;d++)b+=j(a[d]);return b}function h(a,b){if(!k)return new Array(b);switch(a){case 8:return new Uint8Array(b);case 16:return new Uint16Array(b)}}function i(a){for(var b=[],c=a&&a.length,d=0;c>d;d++)b[d]=a.charCodeAt(d);return b}var j=String.fromCharCode,k="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array,l=!1,m=!1;try{"a"===j.apply(null,[97])&&(l=!0)}catch(n){}if(k)try{"a"===j.apply(null,new Uint8Array([97]))&&(m=!0)}catch(n){}var o=65533,p=null,q=!1;-1!=="abc\u307b\u3052".lastIndexOf("\u307b\u3052",1)&&(q=!0);var r="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",s=r.length,t=Math.max(s,62)-Math.min(s,62),u=s-1,v=1024,w=304,x=o,y=x-s,z=o,A=z+2*v,B=11,C=B*(B+1),D=40,E=D*(D+1),F=s+1,G=t+20,H=s+5,I=s-t-19,J=D+7,K=J+1,L=K+1,M=L+5,N=M+5;a.prototype={_init:function(a){a=a||{},this._data=null,this._table=null,this._result=null,this._onDataCallback=a.onData,this._onEndCallback=a.onEnd},_createTable:function(){for(var a=h(8,s),b=0;s>b;b++)a[b]=r.charCodeAt(b);return a},_onData:function(a,b){var c=e(a,b);this._onDataCallback?this._onDataCallback(c):this._result+=c},_onEnd:function(){this._onEndCallback&&this._onEndCallback(),this._data=this._table=null},_search:function(){var a=2,b=this._data,c=this._offset,d=u;if(this._dataLen-c<d&&(d=this._dataLen-c),a>d)return!1;var e,f,g,h,i,j,k=c-w,l=b.substring(k,c+d),m=c+a-3-k;do{if(2===a){if(f=b.charAt(c)+b.charAt(c+1),g=l.indexOf(f),!~g||g>m)break}else 3===a?f+=b.charAt(c+2):f=b.substr(c,a);if(q?(j=b.substring(k,c+a-1),h=j.lastIndexOf(f)):h=l.lastIndexOf(f,m),!~h)break;i=h,e=k+h;do if(b.charCodeAt(c+a)!==b.charCodeAt(e+a))break;while(++a<d);if(g===h){a++;break}}while(++a<d);return 2===a?!1:(this._index=w-i,this._length=a-1,!0)},compress:function(a){if(null==a||0===a.length)return"";var b="",d=this._createTable(),e=c(),f=h(8,x),g=0;this._result="",this._offset=e.length,this._data=e+a,this._dataLen=this._data.length,e=a=null;for(var i,j,k,l,m,n=-1,o=-1;this._offset<this._dataLen;)this._search()?(this._index<u?(j=this._index,k=0):(j=this._index%u,k=(this._index-j)/u),2===this._length?(f[g++]=d[k+M],f[g++]=d[j]):(f[g++]=d[k+L],f[g++]=d[j],f[g++]=d[this._length]),this._offset+=this._length,~o&&(o=-1)):(i=this._data.charCodeAt(this._offset++),C>i?(D>i?(j=i,k=0,n=F):(j=i%D,k=(i-j)/D,n=k+F),o===n?f[g++]=d[j]:(f[g++]=d[n-G],f[g++]=d[j],o=n)):(E>i?(j=i,k=0,n=H):(j=i%E,k=(i-j)/E,n=k+H),D>j?(l=j,m=0):(l=j%D,m=(j-l)/D),o===n?(f[g++]=d[l],f[g++]=d[m]):(f[g++]=d[K],f[g++]=d[n-s],f[g++]=d[l],f[g++]=d[m],o=n))),g>=y&&(this._onData(f,g),g=0);return g>0&&this._onData(f,g),this._onEnd(),b=this._result,this._result=null,null===b?"":b}},b.prototype={_init:function(a){a=a||{},this._result=null,this._onDataCallback=a.onData,this._onEndCallback=a.onEnd},_createTable:function(){for(var a={},b=0;s>b;b++)a[r.charAt(b)]=b;return a},_onData:function(a){var b;if(this._onDataCallback){if(a)b=this._result,this._result=[];else{var c=z-v;b=this._result.slice(v,v+c),this._result=this._result.slice(0,v).concat(this._result.slice(v+c))}b.length>0&&this._onDataCallback(e(b))}},_onEnd:function(){this._onEndCallback&&this._onEndCallback()},decompress:function(a){if(null==a||0===a.length)return"";this._result=i(c());for(var b,d,f,g,h,j,k,l,m,n,o="",p=this._createTable(),q=!1,r=null,s=a.length,t=0;s>t;t++)if(d=p[a.charAt(t)],void 0!==d){if(I>d)q?(g=p[a.charAt(++t)],h=g*D+d+E*r):h=r*D+d,this._result[this._result.length]=h;else if(J>d)r=d-I,q=!1;else if(d===K)f=p[a.charAt(++t)],r=f-5,q=!0;else if(N>d){if(f=p[a.charAt(++t)],M>d?(j=(d-L)*u+f,k=p[a.charAt(++t)]):(j=(d-M)*u+f,k=2),l=this._result.slice(-j),l.length>k&&(l.length=k),m=l.length,l.length>0)for(n=0;k>n;)for(b=0;m>b&&(this._result[this._result.length]=l[b],!(++n>=k));b++);r=null}this._result.length>=A&&this._onData()}return this._result=this._result.slice(v),this._onData(!0),this._onEnd(),o=e(this._result),this._result=null,o}};var O={compress:function(b,c){return new a(c).compress(b)},decompress:function(a,c){return new b(c).decompress(a)}};return O});
"use strict"
var renderer, project, scene, camera, controls, buildingsTextures = [];
let vertcount = 0;
// var params = {
// e: 135.5423,
// n: 34.7102,
// s: 34.6421,
// w: 135.4594,
// redraw: redraw
// };
/***
* @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;//attributes['position'].count;
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));
// this.index.array = Uint32ArrayConcat(this.index.array, indices);
// this.index.count = this.index.array.length / this.index.itemSize;
}
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;
};
const MAX_TEX_NUM = 16; // 4 x 4 = 16 cell
const TEX_DIV = 4;// UV座標の分割数
const TEX_DIV_R = 1 / TEX_DIV;// UV座標の分割数の逆数
function toFloatString(number) {
const v = number.toString();
return v.match(/\./) ? v : v + '.0';
}
class BoundingUVGenerator {
constructor() {
}
// THREE.ExtrudeGeometryの前に呼び出す
setShape({
extrudedShape, // 押し出すShape
extrudedOptions// THREE.ExtrudeGeometryのオプション
}) {
// 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),
bby = (bb.max.y - bb.min.y);
return [
new THREE.Vector2((ax - bb.min.x) / bbx, (1.0 - (ay - bb.min.y) / bby)),
new THREE.Vector2((bx - bb.min.x) / bbx, (1.0 - (by - bb.min.y) / bby)),
new THREE.Vector2((cx - bb.min.x) / bbx, (1.0 - (cy - bb.min.y) / bby))
];
}
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,
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, 1.0 - az / amt),
new THREE.Vector2(bx / bbx, 1.0 - bz / amt),
new THREE.Vector2(cx / bbx, 1.0 - cz / amt),
new THREE.Vector2(dx / bbx, 1.0 - dz / amt)
];
} else {
return [
new THREE.Vector2((ay / bby), 1.0 - az / amt),
new THREE.Vector2((by / bby), 1.0 - bz / amt),
new THREE.Vector2((cy / bby), 1.0 - cz / amt),
new THREE.Vector2((dy / bby), 1.0 - dz / amt)
];
}
}
};
const vertexShader =
`#define PHONG
varying vec3 vViewPosition;
#ifndef FLAT_SHADED
varying vec3 vNormal;
#endif
#include <common>
#include <uv_pars_vertex>
#include <uv2_pars_vertex>
#include <displacementmap_pars_vertex>
#include <envmap_pars_vertex>
#include <color_pars_vertex>
#include <fog_pars_vertex>
#include <morphtarget_pars_vertex>
#include <skinning_pars_vertex>
#include <shadowmap_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>
attribute float texIndex;
attribute float amount;
varying float vTexIndex;
varying float vAmount;
varying vec3 vNormalView;
void main() {
#include <uv_vertex>
#include <uv2_vertex>
#include <color_vertex>
#include <beginnormal_vertex>
#include <morphnormal_vertex>
#include <skinbase_vertex>
#include <skinnormal_vertex>
#include <defaultnormal_vertex>
#ifndef FLAT_SHADED // Normal computed with derivatives when FLAT_SHADED
vNormal = normalize( transformedNormal );
#endif
#include <begin_vertex>
#include <displacementmap_vertex>
#include <morphtarget_vertex>
#include <skinning_vertex>
#include <project_vertex>
#include <logdepthbuf_vertex>
#include <clipping_planes_vertex>
vViewPosition = - mvPosition.xyz;
#include <worldpos_vertex>
#include <envmap_vertex>
#include <shadowmap_vertex>
#include <fog_vertex>
vTexIndex = texIndex;
vAmount = amount;
vNormalView = normal;
}
`;
const fragmentShader =
`#define PHONG
uniform vec3 diffuse;
uniform vec3 emissive;
uniform vec3 specular;
uniform float shininess;
uniform float opacity;
#include <common>
#include <packing>
#include <dithering_pars_fragment>
#include <color_pars_fragment>
#include <uv_pars_fragment>
#include <uv2_pars_fragment>
#include <map_pars_fragment>
#include <alphamap_pars_fragment>
#include <aomap_pars_fragment>
#include <lightmap_pars_fragment>
#include <emissivemap_pars_fragment>
#include <envmap_pars_fragment>
#include <gradientmap_pars_fragment>
#include <fog_pars_fragment>
#include <bsdfs>
#include <lights_pars>
#include <lights_phong_pars_fragment>
#include <shadowmap_pars_fragment>
#include <bumpmap_pars_fragment>
#include <normalmap_pars_fragment>
#include <specularmap_pars_fragment>
#include <logdepthbuf_pars_fragment>
#include <clipping_planes_pars_fragment>
varying float vTexIndex;
varying float vAmount;
varying vec3 vNormalView;
void main() {
#include <clipping_planes_fragment>
vec4 diffuseColor = vec4( diffuse, opacity );
ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );
vec3 totalEmissiveRadiance = emissive;
#include <logdepthbuf_fragment>
//#include <map_fragment>
//float ycomp = dot(vec3(0.,0.,1.),vNormalView);
float texIdx;
//線形補完をキャンセルする
float vti = floor(vTexIndex + .5);
float amount = floor(vAmount + .5);
if(vNormalView.z != 0.0 && vti < 4.){
texIdx = MAX_TEX_NUM - vti - 9.;
} else {
texIdx = MAX_TEX_NUM - vti - 1.;
}
vec2 uv;
uv.y = floor(texIdx / TEX_DIV) * TEX_DIV_R;
uv.x = floor(mod(texIdx,TEX_DIV)) * TEX_DIV_R;
vec2 vuv;
if(vNormalView.z == 0.0 && vti < 4.){
vuv = vec2(vUv.x * TEX_DIV_R,mod(vUv.y,1.0/ amount)* amount * TEX_DIV_R * 0.125);
uv = uv + vuv;
} else {
vuv = vUv * TEX_DIV_R;
uv = uv + vuv;
}
vec4 texelColor = texture2D(map, uv);
//vec4 texelColor = texture2D( map, vUv );
texelColor = mapTexelToLinear( texelColor );
diffuseColor *= texelColor;
#include <color_fragment>
#include <alphamap_fragment>
#include <alphatest_fragment>
#include <specularmap_fragment>
#include <normal_flip>
#include <normal_fragment>
#include <emissivemap_fragment>
// accumulation
#include <lights_phong_fragment>
#include <lights_template>
// modulation
#include <aomap_fragment>
vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;
#include <envmap_fragment>
gl_FragColor = vec4( outgoingLight, diffuseColor.a );
#include <tonemapping_fragment>
#include <encodings_fragment>
#include <fog_fragment>
#include <premultiplied_alpha_fragment>
#include <dithering_fragment>
}
`;
document.addEventListener('DOMContentLoaded', function () {
var atmosphere, boundary, center, loading, targetTile;
scene = new THREE.Scene();
renderer = new THREE.WebGLRenderer();
renderer.setClearColor(0xa0a0d0);
renderer.setSize(window.innerWidth, window.innerHeight);
//renderer.shadowMapEnabled = true;
document.getElementById('container').appendChild(renderer.domElement);
let light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(100, 100, -100);
//light.castShadow = true;
scene.add(light);
let light1 = new THREE.DirectionalLight(0xffffff, 0.7);
light1.position.set(-100, -1000, -100);
scene.add(light1);
scene.fog = new THREE.FogExp2(0xc0c0c0, 0.00015);
atmosphere = new THREE.Mesh(new THREE.PlaneGeometry(10000, 10000, 1, 1), new THREE.MeshPhongMaterial({
color: 0x232323, side: THREE.BackSide
}));
atmosphere.scale.z = -1;
atmosphere.position.y = -1;
atmosphere.rotation.x = Math.PI / 2;
scene.add(atmosphere);
let am = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(am);
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 40000);
camera.position.set(0, 300, 4000);
//camera.lookAt(0, 0, 0);
// camera.rotation._x = -0.057109919451218856;
// camera.rotation._y = -0.011502361238995906;
// camera.rotation._z = -0.0006575994103661149;
scene.add(camera);
//
//e=135.5189&w=135.4827&n=34.7085&s=34.6649
// var gui = new dat.GUI();
// gui.add(params,'w').step(0.00001);
// gui.add(params,'e').step(0.00001);
// gui.add(params,'n').step(0.00001);
// gui.add(params,'s').step(0.00001);
// gui.open();
// boundary = {
// e: 135.5090,
// n: 34.7093,
// s: 34.6840,
// w: 135.4769
// };
boundary = {
e: 135.5361,
n: 34.7076,
s: 34.6452,
w: 135.4601
};
center = centroid(boundary);
project = createProjection(center);
const cubeTexLoader = new THREE.CubeTextureLoader();
const urls = [
"./clouds1_east.jpg",
"./clouds1_west.jpg",
"./clouds1_up.jpg",
"./clouds1_down.jpg",
"./clouds1_north.jpg",
"./clouds1_south.jpg"
];
loading = new Promise((resolve, reject) => {
cubeTexLoader.load(urls, function (tex) {
const cubeShader = THREE.ShaderLib['cube'];
cubeShader.uniforms['tCube'].value = tex;
const skyBoxMaterial = new THREE.ShaderMaterial({
fragmentShader: cubeShader.fragmentShader,
vertexShader: cubeShader.vertexShader,
uniforms: cubeShader.uniforms,
depthWrite: false,
side: THREE.BackSide
});
const mesh = new THREE.Mesh(new THREE.BoxBufferGeometry(10000, 10000, 10000, 1, 1, 1),
skyBoxMaterial);
//mesh.receiveShadow = true;
scene.add(mesh);
render();
resolve();
});
});
loading.then(() => {
let pr = Promise.resolve(0);
let texloader = new THREE.TextureLoader();
let textures = [
'./texture.jpg'
];
textures.forEach((url) => {
pr = pr.then(() => new Promise((resolve, reject) => {
texloader.load(url, (tex) => {
tex.wrapS = THREE.RepeatWrapping;
// tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(1, 1);
buildingsTextures.push(tex);
resolve();
});
}));
});
return pr;
})
.then(() => {
let threads = 4;
let ps = [];
let ps1 = Promise.resolve();
for (let i = 0; i < threads; ++i) {
ps.push(Promise.resolve());
}
function createAndRender(i, overpassData) {
ps1 = ps1.then(() => {
console.log('data loaded' + i);
scene.add(createGeoObject(project, overpassData));
overpassData = null;
render();
return Promise.resolve();
});
};
for (let i = 0, e = 16 / threads; i < e; ++i) {
for (let j = 0; j < threads; ++j) {
ps[j] = ps[j]
.then(() => {
return new Promise((resolve, reject) => {
d3.json(`./map${('00000' + (i * threads + j)).slice(-5)}.json`,
(err, data) => {
if (err) reject(err);
resolve(JSON.parse(lzbase62.decompress(data.data)));
});
});
})
.then(createAndRender.bind(null, i * threads + j));
}
}
return Promise.all(ps).then(() => ps1);
}).then(() => {
console.log('rendered.');
d3.select('#loading').style('display', 'none');
// マウスでぐりぐりできるようにする
controls = new THREE.OrbitControls(camera);
controls.addEventListener('change', render);
render();
console.log(vertcount);
}).catch((e) => {
console.log('error' + e.stack);
});
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
render();
});
});
// レンダリング処理
function render() {
// controls.update();
renderer.render(scene, camera); // レンダリング
// requestAnimationFrame(render); // ループ処理
}
function lonlatToTile(lon, lat, zoom) {
let numOfTiles = Math.pow(2, zoom),
lonDegreesPerTile = 360 / numOfTiles,
sinLat = Math.sin(lat * Math.PI / 180),
tx = (lon + 180) / lonDegreesPerTile,
ty = (0.5 + -0.5 * Math.log((1 + sinLat) / (1 - sinLat)) / (2 * Math.PI)) * numOfTiles;
return [Math.floor(tx), Math.floor(ty)];
};
function tileToLonlat(tx, ty, zoom) {
let numOfTiles = Math.pow(2, zoom),
x = tx / numOfTiles,
y = ty / numOfTiles,
lon = (x - (1 / 2)) / (1 / 360),
latRadians = (y - (1 / 2)) / -(1 / (2 * Math.PI)),
lat = (2 * Math.atan(Math.exp(latRadians)) - Math.PI / 2) / Math.PI * 180;
return [lon, lat];
};
function tileToBoundary(x, y, zoom) {
let p1 = tileToLonlat(x, y, zoom);
let p2 = tileToLonlat(x + 1, y + 1, zoom);
return {
n: p1[1],
w: p1[0],
s: p2[1],
e: p2[0]
};
};
function midpoint(_arg, _arg1) {
let x1 = _arg[0], y1 = _arg[1],
x2 = _arg1[0], y2 = _arg1[1],
x = x1 - (x1 - x2) / 2,
y = y1 - (y1 - y2) / 2;
return [x, y];
};
function centroid(boundary) {
let p1 = [boundary.w, boundary.n],
p2 = [boundary.e, boundary.s];
return midpoint(p1, p2);
};
function createProjection(center) {
return d3.geoMercator().scale(6.5 * 1000 * 1000).center(center).translate([0, 0]);
};
const materialOptions = {
railway: {
platform: {
color: 0x555500,
amount: 1
},
rail: {
color: 0xffff00,
linewidth: 1
}
},
highway: {
pedestrian: {
color: 0x00cccc,
amount: 1
},
primary: {
color: 0xffaa555,
linewidth: 1000
},
secondary: {
color: 0xaa5500,
linewidth: 1
},
residential: {
color: 0xffffff,
linewidth: 1
},
"default": {
color: 0xcccccc,
linewidth: 1
}
},
waterway: {
"default": {
color: 0x0000ff,
texIndexTop: 4,
linewidth: 10
}
},
amenity: {
school: {
color: 0x00aa00,
amount: 10
},
theatre: {
color: 0xcc5500,
amount: 10
},
parking: {
color: 0xffffaa,
amount: 5
},
bus_station: {
color: 0xcc0000,
amount: 5
},
"default": {
color: 0xffffff,
amount: 10
}
},
building: {
commercial: {
amount: 60
},
house: {
amount: 5
},
yes: {
amount: 60
},
"default": {
amount: 60
}
},
natural: {
wood: {
texIndexTop: 5,
amount: 8
},
water: {
texIndexTop: 4,
amount: 1
},
"default": {
color: 0x00ff00,
texIndexTop: 6,
amount: 1
}
},
leisure: {
pitch: {
texIndexTop: 6,
amount: 1
},
golf_course: {
texIndexTop: 6,
amount: 1
},
"default": {
texIndexTop: 6,
amount: 1
}
},
landuse: {
forest: {
texIndexTop: 5,
amount: 5
},
old_forest: {
texIndexTop: 5,
amount: 5
},
"default": {
texIndexTop: 6,
amount: 1
}
}
};
const mkeys = new Set(Object.keys(materialOptions));
// function getNodes(overpassData, way) {
// return way.nodes.map(function (id) {
// return overpassData.node[id];
// });
// };
function isArea(way) {
return way.nodes[0] === way.nodes[way.nodes.length - 1];
};
function yxToVec3(_arg) {
var x, y;
x = _arg[0], y = _arg[1];
return new THREE.Vector3(x, y, 0);
};
function nodeToXy(node) {
return project([node.lon, node.lat]);
};
function nodeToVec3(node) {
let temp = project([node.lon, node.lat]);
return new THREE.Vector3(temp[0], temp[1]);
};
function createLineGeometry(overpassData, way) {
let nodes = way.nodes;//getNodes(overpassData, way);
let geometry = new THREE.Geometry();
geometry.vertices = nodes.map(function (node) {
return nodeToVec3(node);
});
return geometry;
}
function createLine(overpassData, way, opts) {
return createLineGeometry(overpassData, way);
//return new THREE.Line(createLineGeometry(overpassData, way), new THREE.LineBasicMaterial(opts));
};
function createShape(nodes) {
let shape = new THREE.Shape();
shape.moveTo.apply(shape, nodes[0]);
for (let i = 1, e = nodes.length; i < e; ++i) {
shape.lineTo.apply(shape, nodes[i]);
}
return shape;
};
function createPolygonGeometry(overpassData, area, opts) {
const nodes = area.nodes;//getNodes(overpassData, area);
const shape = createShape(nodes);
if (!('amount' in opts)) opts.amount = 1;
//console.log(opts.amount);
//if (!('bevelEnabled' in opts)) opts.bevelEnabled = false;
opts.bevelEnabled = false;
opts.UVGenerator && opts.UVGenerator.setShape({ extrudedShape: shape, extrudedOptions: opts, texIndexTop: opts.texIndexTop });
//opts.extrudeMaterial = 0;
//opts.material = 1;
const geometry = new THREE.ExtrudeBufferGeometry(shape, opts);
const texIndexs = new Float32Array(geometry.attributes.position.count);
const amounts = new Float32Array(geometry.attributes.position.count);
const texNo = opts.texIndexTop ? opts.texIndexTop : Math.floor(Math.random() * 4);
for (let i = 0, e = geometry.attributes.position.count; i < e; ++i) {
texIndexs[i] = texNo;
amounts[i] = opts.levels;
}
geometry.addAttribute('texIndex', new THREE.BufferAttribute(texIndexs, 1));
geometry.addAttribute('amount', new THREE.BufferAttribute(amounts, 1));
return geometry;
};
function createPolygon(overpassData, area, opts) {
if (opts == null) {
opts = {
color: 0xffffff,
opacity: 0.8,
transparent: true
};
}
return createPolygonGeometry(overpassData, area, opts);
};
function findMaterialOptions(tags, uvgen) {
if (tags == null) {
tags = {};
}
const tkeys = new Set(Object.keys(tags));
const is =
[...mkeys].filter(x => tkeys.has(x));
const key = is ? is[0] : null;
if (key) {
const category = materialOptions[key];
const tvalue = tags[key];
const _ref = category[tvalue] ? Object.assign({}, category[tvalue]) : Object.assign({}, category["default"]);
if (key == 'building') {
// _ref.map = buildingsTextures[parseInt(Math.random() * buildingTextures.length)];
_ref.map = buildingsTextures[0];
_ref.amount = 60;
_ref.levels = 5;
}
if (_ref) {
// if ('height' in tags) {
// _ref.amount = parseFloat(tags.height);
// // console.log('height',tvalue,_ref,tags.name,parseFloat(tags.height),tags.height,tags );
// } else
if ('building:levels' in tags) {
_ref.amount = parseFloat(tags['building:levels']) * 5;
_ref.levels = parseFloat(tags['building:levels']);
// console.log(key,tvalue,tags.name,parseFloat(tags['building:levels']),'階',tags);
} else {
_ref.amount = _ref.amount * Math.random() * 0.75 + _ref.amount * 0.25;
_ref.levels = Math.floor(_ref.amount / 5.0);
// console.log(key,tvalue,tags.name,_ref.amount,tags);
}
}
_ref.side = THREE.BackSide;
_ref.UVGenerator = uvgen;
return _ref;
} else {
return null;
}
};
function redraw() {
}
function createAndAddLines(overpassData, root) {
let geometry = new THREE.BufferGeometry();
let uvgen = new BoundingUVGenerator();
overpassData.way.lines.forEach((way) => {
geometry.merge(createLine(overpassData, way, findMaterialOptions(way.tags, uvgen)));
});
root.add(new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 0xffffff })));
};
function createAndAddPolygons(overpassData, root) {
// var areas = [];
// let geometry = null;
let uvgen = new BoundingUVGenerator();
const baseShader = THREE.ShaderLib['phong'];
const geometries = [];
let counts = {};
overpassData.way.polygons.forEach((way) => {
const g = createPolygon(overpassData, way, findMaterialOptions(way.tags, uvgen));
if (!counts.index) {
counts.index = g.index.array.length;
} else {
counts.index += g.index.array.length;
}
for (var key in g.attributes) {
if (counts[key]) {
counts[key] += g.attributes[key].array.length;
} else {
counts[key] = g.attributes[key].array.length;
}
}
geometries.push(g);
});
const geometry = new THREE.BufferGeometry();
const bufferArrays = {};
for (var key in counts) {
if (key == 'index') {
bufferArrays.index = { array: new Uint32Array(counts[key]), offset: 0 };
// geometry.setIndex(new Uint32Array(counts[key]));
} else {
bufferArrays[key] = { array: new Float32Array(counts[key]), offset: 0 };
// geometry.attributes[key] = new THREE.BufferAttribute(new Float32Array(counts[key]),geometries[0].attributes[key].itemSize,geometries[0].attributes[key].normalized);
}
}
const destIndex = bufferArrays.index;
var vcount = 0;
geometries.forEach((g) => {
const srcArray = g.index.array;
const destArray = destIndex.array;
vcount += g.attributes.position.count;
for (let i = 0, offset = destIndex.offset, j = offset, e = srcArray.length; i < e; ++i, ++j) {
destArray[j] = srcArray[i] + offset;
}
destIndex.offset += srcArray.length;
for (let key in g.attributes) {
const destAttr = bufferArrays[key];
const destArray = destAttr.array;
const srcArray = g.attributes[key].array;
for (let i = 0, offset = destAttr.offset, j = offset, e = srcArray.length; i < e; ++i, ++j) {
destArray[j] = srcArray[i] ? srcArray[i] : 0.0;
}
destAttr.offset += srcArray.length;
}
});
for (var key in counts) {
if (key == 'index') {
geometry.setIndex(new THREE.BufferAttribute(bufferArrays.index.array, 1));
} else {
geometry.addAttribute(key, new THREE.BufferAttribute(bufferArrays[key].array, geometries[0].attributes[key].itemSize, geometries[0].attributes[key].normalized));
}
}
const mat = new THREE.ShaderMaterial({
uniforms: THREE.UniformsUtils.clone(baseShader.uniforms),
defines: {
MAX_TEX_NUM: toFloatString(MAX_TEX_NUM),
TEX_DIV: toFloatString(TEX_DIV),
TEX_DIV_R: toFloatString(TEX_DIV_R),
USE_MAP: '', USE_FOG: ''
},
lights: true,
fog: true,
side: THREE.BackSide,
vertexShader: vertexShader,
fragmentShader: fragmentShader
});
mat.uniforms.emissive.value = new THREE.Color(0x000000);
mat.uniforms.ambientLightColor.value = new THREE.Color(0x303030);
mat.uniforms.map.value = buildingsTextures[0];
root.add(new THREE.Mesh(geometry,
mat
//new THREE.MeshPhongMaterial({ map: buildingsTextures[0], side: THREE.BackSide })
));
return vcount;
};
function createGeoObject(project, overpassData) {
let root = new THREE.Object3D();
root.rotation.x = 90 * Math.PI / 180;
root.scale.z = -1;
//createAndAddLines(overpassData,root);
vertcount += createAndAddPolygons(overpassData, root);
overpassData = null;
return root;
};
## Open Street Mapのデータの3D化
Open Street Mapのデータを使って、大阪の環状線の内側を3D化してみた。
建物については高さデータがあるものはそれを使用して、高さデータがないものはランダムに高さを設定して表示している。
コードは下記記事のサンプルをベースにしている。
http://qiita.com/i09158knct@github/items/f0f4c82fed8aab004737
以下の改良を行い、たくさんのメッシュを表示してもパフォーマンスができる限り落ちないようにしている。
* geometryをある程度の塊にまとめて高速化を図る
* 分割してデータを読み込む
* 事前にデータをoverpass apiからデータを読み込み、静的にキャッシュしておく
### 使用ライブラリ
* three.js (r85)
* d3.js (4.8.0)
* lzbase62.min.js
### 使用したテクスチャ
下記サイトから適当にいくつか使用
https://www.textures.com/download/buildingshighrise0625/104602
skybox用に下記からダウンロードして使用
https://opengameart.org/content/clouds-skybox-1