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>