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

公開:2017-04-23 21:59
更新:2020-02-15 04:37
カテゴリ:three.js,overpass api,シューティングゲーム,overpass apiとthree.jsで地図を3d表示,ゲーム製作

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

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

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

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

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

https://github.com/mrdoob/three.js/issues/1824

回避策としては、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)
      ];
    }
  }
};

動作サンプル

新しいウィンドウで開く

ソースコード・リソース

/dev/map/0002/buildings.jpg

/dev/map/0002/buildings1.jpg

/dev/map/0002/buildings2.jpg

/dev/map/0002/buildings3.jpg

/dev/map/0002/clouds1_down.jpg

/dev/map/0002/clouds1_east.jpg

/dev/map/0002/clouds1_north.jpg

/dev/map/0002/clouds1_south.jpg

/dev/map/0002/clouds1_up.jpg

/dev/map/0002/clouds1_west.jpg

/dev/map/0002/index.html

<!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/84/three.js"></script>
    <script type="text/javascript" src="https://threejs.org/examples/js/controls/OrbitControls.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="./main.js"></script>
    <style>
        body {margin:0;padding:0;}
    </style>
</head>
<body>
</body>
</html>

/dev/map/0002/main.js

"use strict"

var renderer, scene, camera, controls, buildingsTextures = [];

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)
      ];
    }
  }
};

document.addEventListener('DOMContentLoaded', function () {

  var atmosphere, boundary, center, loading, project, targetTile;
  scene = new THREE.Scene();
  renderer = new THREE.WebGLRenderer();
  renderer.setClearColor(0xa0a0d0);
  renderer.setSize(window.innerWidth, window.innerHeight);

  document.body.appendChild(renderer.domElement);
  let light = new THREE.DirectionalLight(0xffffff, 1);
  light.position.set(1000, 1000, -1000);
  let light1 = new THREE.DirectionalLight(0xffffff, 0.5);
  light1.position.set(-1000, -1000, -0);
  scene.add(light);
  scene.add(light1);
  scene.fog = new THREE.Fog(0xc0c0c0, 3000, 7000);
  atmosphere = new THREE.Mesh(new THREE.PlaneGeometry(10000, 10000, 1, 1), new THREE.MeshBasicMaterial({
    color: 0x232323, side: THREE.DoubleSide
  }));
  //atmosphere.scale.z = -1;
  atmosphere.rotation.x = Math.PI / 2;
  scene.add(atmosphere);
  let am = new THREE.AmbientLight(0xffffff, 0.3);
  scene.add(am);

  camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 40000);
  camera.position.set(-35.06792003482036, 100.93456423116383, 3043);
  camera.rotation._x = -0.057109919451218856;
  camera.rotation._y = -0.011502361238995906;
  camera.rotation._z = -0.0006575994103661149;

  scene.add(camera);

  // マウスでぐりぐりできるようにする
  controls = new THREE.OrbitControls(camera);
  let sq = window.location.search;
  sq = sq.slice(1).split('&');
  let sargs = {};
  sq.forEach((q) => {
    let t = q.split('=');
    sargs[t[0]] = parseFloat(t[1]);
  });
//e=135.5189&w=135.4827&n=34.7085&s=34.6649
  boundary = {
    e: 135.5189,
    n: 34.7085,
    s: 34.6649,
    w: 135.4827
  };//tileToBoundary(targetTile.x, targetTile.y, targetTile.z);
  for (let p in sargs) {
    boundary[p] && (boundary[p] = sargs[p]);
  }
  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.BoxGeometry(40000, 40000, 40000, 1, 1, 1),
        skyBoxMaterial);
      scene.add(mesh);
      resolve();
    });

  });

  loading.then(() => {
    let pr = Promise.resolve(0);
    let texloader = new THREE.TextureLoader();


    let textures = [
      './buildings.jpg',
      './buildings1.jpg',
      './buildings2.jpg',
      './buildings3.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(4, 4);
          buildingsTextures.push(tex);
          resolve();
        });
      }));
    });
    return pr;
  })
    // .then(()=>{
    //   let cube = new THREE.CubeGeometry(1000,1000,1000,1,1,1);
    //   let mesh = new THREE.Mesh(cube,new THREE.MeshPhongMaterial({map:buildingsTexture}));
    //   scene.add(mesh);
    // })
    .then(loadOverpassData.bind(null, boundary))
    .then(function (overpassData) {
      var animate, controls, geoObject;
      geoObject = createGeoObject(project, overpassData);
      scene.add(geoObject);
      // controls = new THREE.TrackballControls(camera, renderer.domElement);
      //controls.target = geoObject.position.clone();
      //controls.target.add(new THREE.Vector3(0, 0, 0));
      render();
    });

  window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
  });

});

// レンダリング処理
function render() {
  controls.update();
  renderer.render(scene, camera); // レンダリング
  requestAnimationFrame(render); // ループ処理
}

function lonlatToTile(lon, lat, zoom) {
  let lonDegreesPerTile, numOfTiles, sinLat, tx, ty;
  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 lat, latRadians, lon, numOfTiles, x, y;
  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, p2;
  p1 = tileToLonlat(x, y, zoom);
  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 x, x1, x2, y, y1, y2;
  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, p2;
  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 loadOverpassData(boundary) {
  return new Promise((resolve, reject) => {
    let baseUrl = "//overpass-api.de/api/interpreter?data=[out:json];\n(\n  node({s},{w},{n},{e});\n  way(bn);\n);\n(\n  ._;\n  node(w);\n);\nout;";
    let url = baseUrl.replace(/\{([swne])\}/g, (match, key) => {
      return boundary[key];
    });

    d3.json(url, (error, root) => {
      if (error) reject(error);
      resolve(root);
    });
  })
    .then((rawData) => {
      var acc;
      acc = {
        node: {},
        way: {},
        relation: {}
      };
      rawData.elements.forEach(function (elem) {
        return acc[elem.type][elem.id] = elem;
      });
      return acc;
    });
};

let 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,
      linewidth: 10
    }
  },
  amenity: {
    school: {
      color: 0x00aa00,
      amount: 10
    },
    theatre: {
      color: 0xcc5500,
      amount: 10
    },
    parking: {
      color: 0xffffaa,
      amount: 1
    },
    bus_station: {
      color: 0xcc0000,
      amount: 1
    },
    "default": {
      color: 0xffffff,
      amount: 10
    }
  },
  building: {
    commercial: {
      color: 0xffffff,
      amount: 60
    },
    house: {
      color: 0xffffff,
      amount: 5
    },
    yes: {
      color: 0xffffff,
      amount: 60
    },
    "default": {
      color: 0xffffff,
      amount: 60
    }
  },
  natural: {
    wood: {
      color: 0x00ff00,
      amount: 5
    },
    water: {
      color: 0x0000cc,
      amount: 1
    },
    "default": {
      color: 0x00ff00,
      amount: 2
    }
  },
  leisure: {
    pitch: {
      color: 0xcc5500,
      amount: 1
    },
    golf_course: {
      color: 0x00cc55,
      amount: 1
    },
    "default": {
      color: 0x00cc55,
      amount: 1
    }
  },
  landuse: {
    forest: {
      color: 0x00ff00,
      amount: 5
    },
    old_forest: {
      color: 0x005500,
      amount: 10
    },
    "default": {
      color: 0x005500,
      amount: 1
    }
  }
};

function createGeoObject(project, overpassData) {

  function getNodes(overpassData, way) {
    return way.nodes.map(function (id) {
      return overpassData.node[id];
    });
  };

  function isArea(way) {

    var first, last;
    first = way.nodes[0];
    last = way.nodes[way.nodes.length - 1];
    return first === last;
  };
  function lonlatToArray(_arg) {
    var lat, lon;
    lon = _arg.lon, lat = _arg.lat;
    return [lon, lat];
  };
  function yxToVec3(_arg) {
    var x, y;
    x = _arg[0], y = _arg[1];
    return new THREE.Vector3(x, y, 0);
  };
  function nodeToXy(node) {
    return project(lonlatToArray(node));
  };
  function nodeToVec3(node) {
    return yxToVec3(nodeToXy(node));
  };
  function createLine(way, opts) {
    var create, line;
    create = (function (_this) {
      return function (way) {
        var geometry, nodes;
        nodes = getNodes(overpassData, way);
        geometry = new THREE.Geometry();
        geometry.vertices = nodes.map(function (node) {
          return nodeToVec3(node);
        });
        return geometry;
      };
    })(this);
    return line = new THREE.Line(create(way), new THREE.LineBasicMaterial(opts));
  };
  function createPolygon(area, opts) {

    if (opts == null) {
      opts = {
        color: 0xffffff,
        opacity: 0.8,
        transparent: true
      };
    }

    function createShape(nodes) {
      var shape;
      shape = new THREE.Shape();
      shape.moveTo.apply(shape, nodeToXy(nodes[0]));
      nodes.slice(1).forEach((function (_this) {
        return function (node) {

          return shape.lineTo.apply(shape, nodeToXy(node));
        };
      })(this));
      return shape;
    };

    var create = (function (_this) {
      return function (area, opts) {
        var geometry, nodes, shape;
        nodes = getNodes(overpassData, area);
        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 = new BoundingUVGenerator(shape, opts);
        opts.extrudeMaterial = 0;
        opts.material = 1;
        geometry = new THREE.ExtrudeGeometry(shape, opts);
        geometry.computeFaceNormals();
        geometry.uvsNeedUpdate = true;
        return geometry;
      };
    })(this);

    if (!('side' in opts)) opts.side = THREE.BackSide;

    return new THREE.Mesh(create(area, opts), new THREE.MeshPhongMaterial(opts));
  };

  function findMaterialOptions(tags) {
    let category, key, mkeys, tkeys, tvalue, _ref;
    if (tags == null) {
      tags = {};
    }
    mkeys = new Set(Object.keys(materialOptions));
    tkeys = new Set(Object.keys(tags));
    let is =
      [...mkeys].filter(x => tkeys.has(x));

    key = is ? is[0] : null;

    if (key) {
      category = materialOptions[key];
      tvalue = tags[key];
      if (category[tvalue]) {
        _ref = Object.assign({}, category[tvalue]);
      } else {
        _ref = Object.assign({}, category["default"]);
      }

      if (key == 'building') {
        _ref.map = buildingsTextures[parseInt(Math.random() * 4)];
      }

      if (_ref && _ref.amount) {
        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;
            //          console.log(key,tvalue,tags.name,parseFloat(tags['building:levels']),'階',tags);
          } else {
            _ref.amount = _ref.amount * Math.random() * 0.75 + _ref.amount * 0.25;
            //        console.log(key,tvalue,tags.name,_ref.amount,tags);          
          }
      }
      return _ref;
    } else {
      return null;
    }
  };

  function createAndAddLines(root) {
    let ways = [];


    for (let i in overpassData.way) {
      if (!isArea(overpassData.way[i])) ways.push(overpassData.way[i]);
    }

    // overpassData.way.filter((way)=>{
    //   return !isArea(way);
    // });

    ways.forEach(function (way) {
      var opts;
      opts = findMaterialOptions(way.tags);
      root.add(createLine(way, opts));
    });
  };

  function createAndAddPolygons(root) {
    var areas = [];
    for (let i in overpassData.way) {
      if (isArea(overpassData.way[i])) areas.push(overpassData.way[i]);
    }
    //areas = overpassData.way.filter(isArea);
    areas.forEach(function (area) {
      var opts;
      opts = findMaterialOptions(area.tags);
      root.add(createPolygon(area, opts));
    });
  };

  let root = new THREE.Object3D();
  root.rotation.x = 90 * Math.PI / 180;
  root.scale.z = -1;
  createAndAddLines(root);
  createAndAddPolygons(root);
  return root;
};

/dev/map/0002/readme.md

Open Street Mapのデータの3D化 ... テクスチャマッピング
詳細はこちら
http://blog.sfpgmr.net/entry/2017/04/23/215902

/dev/map/0002/thumbnail.png

/dev/map/0002/tsconfig.json

/dev/map/0002/typings.json