Using OSM2World as a library on the Web

OSM2World is available as an ECMAScript module. This allows client-side conversion of OpenStreetMap data to 3D models, which offers an alternative to the server-side calculation of 3D scenes for web maps, such as 3D tiles. (However, this approach is best used for smaller data sets due to the performance implications.)

This is a recently introduced feature and some aspects may still be subject to change.

Quick example

Using OSM2World as a library on the Web involves the following steps:

  • Load the config with style information
  • Convert OSM data to 3D meshes
  • Use the results

We’ll explain the individual steps in more detail after the code example.

import { O2WConverter, loadO2WConfig } from "https://osm2world.org/build/web/0.5.0-SNAPSHOT_2025-02-18/osm2world-core-web.mjs";

const osmJson = `
{
  "version": 0.6,
  "elements": [
    {
      "type": "node",
      "id": 1,
      "lat": 42.0,
      "lon": -3.45,
      "tags": {
        "man_made": "flagpole",
        "flag:colour": "blue"
      }
    }
  ]
}`

loadO2WConfig("https://tiles.osm2world.org/styles/default/standard.properties",
  { lod: "4" },
  config => {
    const converter = new O2WConverter()
    converter.setConfig(config)
    converter.convertJson(osmJson, meshes => {
      // do something with the result ...
      meshes.forEach(mesh => console.log(mesh.baseColorTexture()))
    }, error => console.error(error))
  },
  error => console.error(error)
)

Load the config with style information

This only needs to be done once, no matter how many OSM datasets you want to convert. This step involves loading texture images and other files which control the visual appearance of the resulting models. For anything but initial experimentation, please host a copy of the style and its resources on your own server.

In our example, we also set the Level of Detail of the resulting scene to the maximum value of 4 – the most visually pleasing, but also most performance-heavy setting. You can choose any integer between 0 and 4.

Convert OSM data

OSM data is provided in the OSM JSON format.

Use the results

The result of the conversion will be a set of 3D triangle meshes with material information. This can be rendered with most 3D engines, including popular WebGL frameworks such as Babylon.js or Three.js.

Complete Babylon.js example

The following is an example of using Babylon.js to display the meshes produced by the conversion.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">

<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"/>
  <title>OSM2World + Babylon.js</title>

  <style>
    html, body {
      overflow: hidden;
      width: 100%;
      height: 100%;
      margin: 0;
      padding: 0;
    }
    #renderCanvas {
      width: 100%;
      height: 100%;
      display: block;
      touch-action: none;
    }
  </style>

  <script src="https://cdn.babylonjs.com/babylon.js"></script>

</head>

<body>

  <canvas id="renderCanvas"></canvas>

  <script type="module">

    import { O2WConverter, loadO2WConfig } from "https://osm2world.org/build/web/0.5.0-SNAPSHOT_2025-02-18/osm2world-core-web.mjs";

    const canvas = document.getElementById("renderCanvas");
    const engine = new BABYLON.Engine(canvas, true);

    let converter = null;

    let currentMeshes = [];

    const createScene = function () {

      const scene = new BABYLON.Scene(engine);

      const camera = new BABYLON.ArcRotateCamera("camera", 1.571, 0.785, 50);
      camera.upperBetaLimit = Math.PI / 2.05; // almost horizontal
      camera.lowerRadiusLimit = 3;
      camera.setTarget(new BABYLON.Vector3(0, 10, 0));
      camera.attachControl(canvas, true);

      const light = new BABYLON.HemisphericLight("light",
              new BABYLON.Vector3(0, 1, 0), scene);
      light.intensity = 0.7;

      scene.environmentTexture = BABYLON.CubeTexture.CreateFromPrefilteredData("https://tiles.osm2world.org/styles/default/textures/sky/DaySkyHDRI041B.env", scene)

      return scene;

    };

    const updateMeshes = function (scene) {

      if (converter == null) return;

      /* remove old meshes */

      for (let mesh of currentMeshes) {
        mesh.dispose();
      }
      currentMeshes = [];

      /* define OSM data as JSON */

      const osmJson = `
        {
          "version": 0.6,
          "elements": [
            {
              "type": "node",
              "id": 1,
              "lat": 42.0,
              "lon": -3.45,
              "tags": {
                "man_made": "flagpole",
                "flag:colour": "blue"
              }
            }
          ]
        }`

      /* create new meshes */

      converter.convertJson(osmJson, meshes => {

        // Track vertical extent while creating meshes
        let minY = Infinity;
        let maxY = -Infinity;

        meshes.forEach((mesh, i) => {

          // Create a new mesh and a new vertex data object
          const customMesh = new BABYLON.Mesh("mesh_" + i, scene);
          const vertexData = new BABYLON.VertexData();

          const positions = mesh.positions();
          vertexData.positions = positions;
          vertexData.indices = mesh.indices();
          vertexData.normals = mesh.normals();
          vertexData.uvs = mesh.uvs();

          // Scan Y values (positions array is x,y,z repeating)
          for (let pi = 1; pi < positions.length; pi += 3) {
            const y = positions[pi];
            if (y < minY) minY = y;
            if (y > maxY) maxY = y;
          }

          // Apply the vertex data to the mesh
          vertexData.applyToMesh(customMesh);

          const material = new BABYLON.PBRMaterial("material_" + i, scene);

          const colorArr = mesh.color();
          material.albedoColor = new BABYLON.Color3(colorArr[0], colorArr[1], colorArr[2]);

          const baseTexBaseUrl = "https://tiles.osm2world.org/";

          const baseColorTex = mesh.baseColorTexture();
          if (baseColorTex) {
            material.albedoTexture = new BABYLON.Texture(baseTexBaseUrl + baseColorTex, scene, true, false, BABYLON.Texture.TRILINEAR_SAMPLINGMODE);
            if (mesh.clampTextures()) {
              material.albedoTexture.wrapU = 0;
              material.albedoTexture.wrapV = 0;
            }
            if (mesh.transparency()) {
              const opacityTex = mesh.opacityTexture();
              if (opacityTex) {
                material.opacityTexture = new BABYLON.Texture(baseTexBaseUrl + opacityTex, scene, true, false, BABYLON.Texture.TRILINEAR_SAMPLINGMODE);
              } else {
                material.albedoTexture.hasAlpha = true;
              }
            }
          }

          const normalTex = (typeof mesh.normalTexture === "function") ? mesh.normalTexture() : null;
          if (normalTex) {
            material.bumpTexture = new BABYLON.Texture(baseTexBaseUrl + normalTex, scene, true, false, BABYLON.Texture.TRILINEAR_SAMPLINGMODE);
          }

          const ormTex = (typeof mesh.ormTexture === "function") ? mesh.ormTexture() : null;
          if (ormTex) {
            material.metallicTexture = new BABYLON.Texture(baseTexBaseUrl + ormTex, scene, true, false, BABYLON.Texture.TRILINEAR_SAMPLINGMODE);
            material.useAmbientOcclusionFromMetallicTextureRed = true;
            material.useRoughnessFromMetallicTextureGreen = true;
            material.useMetallnessFromMetallicTextureBlue = true;
          } else {
            material.metallic = 0.0;
            material.roughness = 0.9;
          }

          customMesh.material = material;

          currentMeshes.push(customMesh);

        }, e => console.error("Error during conversion", e));

        // Update the camera target's Y to the middle of the vertical extent
        if (minY !== Infinity && maxY !== -Infinity) {
          scene.activeCamera.setTarget(new BABYLON.Vector3(0, (minY + maxY) / 2, 0));
        }

      });

    }

    const scene = createScene();

    loadO2WConfig("https://tiles.osm2world.org/styles/default/standard.properties",
            { lod: "4" },
            config => {
              converter = new O2WConverter();
              converter.setConfig(config);
              updateMeshes();
            },
            error => console.error(error)
    )

    // Register a render loop to repeatedly render the scene
    engine.runRenderLoop(function () {
      scene.render();
    });

    // Watch for browser/canvas resize events
    window.addEventListener("resize", function () {
      engine.resize();
    });
    // Observe canvas size changes (e.g., textarea resized) and notify engine
    if (window.ResizeObserver) {
      const ro = new ResizeObserver(() => {
        engine.resize();
      });
      ro.observe(canvas);
    }

  </script>

</body>

</html>