Skip to main content

Integrating Cesium with Three.js

This is a guest post by Wilson Muktar about integrating Three.js with Cesium. Three.js is a lightweight cross-browser JavaScript library used to create and display animated 3D computer graphics in a browser. Combining Cesium’s planet-scale rendering and GIS features with Three.js’s extensive and accessible general 3D API opens many possibilities for new WebGL experiences. You can check out a live version of this demo here and the code itself here. - Gary

3D JavaScript libraries are now fully mature and widely known, allowing developers to avoid the headaches of getting going with 3D in the browser. Developers can easily create cameras, objects, lights, materials, and graphics and have their pick of renderers, allowing scenes to be drawn using HTML 5’s canvas, WebGL, or SVG.

Because both Cesium and Three.js are meant for 3D visualization and are built with JavaScript from scratch, they share similarities that make it possible to integrate these amazing libraries together. My approach to the integration of both frameworks was simpler than it seems: I separated both frameworks into different layers of view, referring to the HTML Canvas Element, and I combined their controllers within the same coordinate system. Since both are open-source, I can share this demo, which will cover some of the basics.

Left: Scene in Cesium. Center: Scene in Three.js. Right: Combined scene.

Cesium is a 3D library developed to create a digital earth, with rendering that is amazingly accurate to the real Earth. With 3D Tiles, a developer can completely re-render almost everything to a digital canvas within a browser.

The basic rendering principles guiding Cesium are not so different from Three.js. Three.js is a powerful 3D library for rendering 3D objects. By duplicating Cesium’s spherical coordinate system and matching digital globes within both scenes, it is easy to integrate both separate rendering engine layers into one main scene. I will give a simple illustration about its integration method, as follows:

  • Initialize Cesium renderer,
  • Initialize Three.js renderer,
  • Initialize 3D object of both libraries, and
  • Loop the renderer.

Main Function

The html needs containers for Three and for Cesium:

<body>
<div id="cesiumContainer"></div>
<div id="ThreeContainer"></div>
</body>
<script> main(); </script>

This is the main function:

function main(){
  // boundaries in WGS84 to help with syncing the renderers
  var minWGS84 = [115.23,39.55];
  var maxWGS84 = [116.23,41.55];
  var cesiumContainer = document.getElementById("cesiumContainer");
  var ThreeContainer = document.getElementById("ThreeContainer");

  var _3Dobjects = []; //Could be any Three.js object mesh
  var three = {
    renderer: null,
    camera: null,
    scene: null
  };

  var cesium = {
    viewer: null
  };

  initCesium(); // Initialize Cesium renderer
  initThree(); // Initialize Three.js renderer
  init3DObject(); // Initialize Three.js object mesh with Cesium Cartesian coordinate system
  loop(); // Looping renderer
}

Initialize Cesium Renderer

First, we can customize Cesium Viewer by adding custom imagery or other parts provided by default. By disabling the default render loop of Cesium, we can synchronize its animation frame with Three.js.

function initCesium(){
    cesium.viewer = new Cesium.Viewer(cesiumContainer,{
        useDefaultRenderLoop: false,
        selectionIndicator : false,
        homeButton:false,
        sceneModePicker:false,
        navigationHelpButton:false,
        infoBox : false,
        navigationHelpButton:false,
        navigationInstructionsInitiallyVisible:false,
        animation : false,
        timeline : false,
        fullscreenButton : false,
        allowTextureFilterAnisotropic:false,
        contextOptions:{
            webgl: {
                alpha: false,
                antialias: true,
                preserveDrawingBuffer : true,
                failIfMajorPerformanceCaveat: false,
                depth:true,
                stencil:false,
                anialias:false
            },
        },
        targetFrameRate:60,
        resolutionScale:0.1,
        orderIndependentTranslucency : true,
        creditContainer : "hidecredit",
        imageryProvider : new Cesium.TileMapServiceImageryProvider({
            url: 'Assets/imagery/NaturalEarthII/',
            maximumLevel : 5
        }),
        baseLayerPicker : false,
        geocoder : false,
        automaticallyTrackDataSourceClocks: false,
        dataSources: null,
        clock: null,
        terrainShadows: Cesium.ShadowMode.DISABLED
    });

    var center = Cesium.Cartesian3.fromDegrees(
        (minWGS84[0] + maxWGS84[0]) / 2,
        ((minWGS84[1] + maxWGS84[1]) / 2)-1,
        200000
    );
    cesium.viewer.camera.flyTo({
        destination : center,
        orientation : {
            heading : Cesium.Math.toRadians(0),
            pitch : Cesium.Math.toRadians(-60),
            roll : Cesium.Math.toRadians(0)
        },
        duration: 3
    });
}

Initialize Three.js Renderer

Next we simply initialize the Three.js compulsory stage, including the scene, camera, renderer, and its DOM element.

function initThree(){
    var fov = 45;
    var width = window.innerWidth;
    var height = window.innerHeight;
    var aspect = width / height;
    var near = 1;
    var far = 10*1000*1000; // needs to be far to support Cesium's world-scale rendering

    three.scene = new THREE.Scene();
    three.camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
    three.renderer = new THREE.WebGLRenderer({alpha: true});
    ThreeContainer.appendChild(three.renderer.domElement);  
}

Initialize 3D Objects in Both Libraries

A Cesium object could be simply added to its viewer using an entity object; for example, one could use a 3D Graphing class to render a 3D plotting object mesh created in Three.js, or any other 3D object that was created with Three.js. All of this is kept in a _3DObjects for further processing, which contains extra information for synchronizing cameras. Here we’ll render a [Lathe geometry] and a [dodecahedron]. Note that Three.js renders z-up and Cesium renders y-up.

function init3DObject(){
  //Cesium entity
  var entity = {
    name : 'Polygon',
    polygon : {
      hierarchy : Cesium.Cartesian3.fromDegreesArray([
        minWGS84[0], minWGS84[1],
        maxWGS84[0], minWGS84[1],
        maxWGS84[0], maxWGS84[1],
        minWGS84[0], maxWGS84[1],
      ]),
      material : Cesium.Color.RED.withAlpha(0.2)
    }
  };
  var Polygon = cesium.viewer.entities.add(entity);

  // Lathe geometry
  var doubleSideMaterial = new THREE.MeshNormalMaterial({
    side: THREE.DoubleSide
  });
  var segments = 10;
  var points = [];
  for ( var i = 0; i < segments; i ++ ) {
      points.push( new THREE.Vector2( Math.sin( i * 0.2 ) * segments + 5, ( i - 5 ) * 2 ) );
  }
  var geometry = new THREE.LatheGeometry( points );
  var latheMesh = new THREE.Mesh( geometry, doubleSideMaterial ) ;
  latheMesh.scale.set(1500,1500,1500); //scale object to be visible at planet scale
  latheMesh.position.z += 15000.0; // translate "up" in Three.js space so the "bottom" of the mesh is the handle
  latheMesh.rotation.x = Math.PI / 2; // rotate mesh for Cesium's Y-up system
  var latheMeshYup = new THREE.Group();
  latheMeshYup.add(latheMesh)
  three.scene.add(latheMeshYup); // don’t forget to add it to the Three.js scene manually

  //Assign Three.js object mesh to our object array
  var _3DOB = new _3DObject();
  _3DOB.threeMesh = latheMeshYup;
  _3DOB.minWGS84 = minWGS84;
  _3DOB.maxWGS84 = maxWGS84;
  _3Dobjects.push(_3DOB);

  // dodecahedron
  geometry = new THREE.DodecahedronGeometry();
  var dodecahedronMesh = new THREE.Mesh(geometry, new THREE.MeshNormalMaterial()) ;
  dodecahedronMesh.scale.set(5000,5000,5000); //scale object to be visible at planet scale
  dodecahedronMesh.position.z += 15000.0; // translate "up" in Three.js space so the "bottom" of the mesh is the handle
  dodecahedronMesh.rotation.x = Math.PI / 2; // rotate mesh for Cesium's Y-up system
  var dodecahedronMeshYup = new THREE.Group();
  dodecahedronMeshYup.add(dodecahedronMesh)
  three.scene.add(dodecahedronMeshYup); // don’t forget to add it to the Three.js scene manually

  //Assign Three.js object mesh to our object array
  _3DOB = new _3DObject();
  _3DOB.threeMesh = dodecahedronMeshYup;
  _3DOB.minWGS84 = minWGS84;
  _3DOB.maxWGS84 = maxWGS84;
  _3Dobjects.push(_3DOB);
}
function _3DObject(){
  this.graphMesh = null; //Three.js 3DObject.mesh
  this.minWGS84 = null; //location bounding box
  this.maxWGS84 = null;
}

Looping Renderer

function loop(){
  requestAnimationFrame(loop);
  renderCesium();
  renderThreeObj();
}
function renderCesium(){
  cesium.viewer.render();
}

We will clone the Three.js camera to match the Cesium camera, so there is no need to assign a mouse controller for Three.js, but we still need to remove it because the Three.js DOM element is above Cesium. We remove it by adding the CSS attribute pointer-events:none to the Three.js renderer. Now everything will be rendered according to Cesium’s camera projection.

There is still a coordinate transformation to be done to make the object appear correctly on the globe. This includes converting the geodetic lat/long position to Cartesian XYZ and using the direction from bottom left to top left of the WGS84 region as the up vector so the object points away from the globe center. This also can be calculated by using transformation to local Cartesian East-North-Up or North-East-Down.

function renderThreeObj(){
  // register Three.js scene with Cesium
  three.camera.fov = Cesium.Math.toDegrees(cesium.viewer.camera.frustum.fovy) // ThreeJS FOV is vertical
  three.camera.updateProjectionMatrix();

  var cartToVec = function(cart){
    return new THREE.Vector3(cart.x, cart.y, cart.z);
  };

  // Configure Three.js meshes to stand against globe center position up direction
  for(id in _3Dobjects){
    minWGS84 = _3Dobjects[id].minWGS84;
    maxWGS84 = _3Dobjects[id].maxWGS84;
    // convert lat/long center position to Cartesian3
    var center = Cesium.Cartesian3.fromDegrees((minWGS84[0] + maxWGS84[0]) / 2, (minWGS84[1] + maxWGS84[1]) / 2);

    // get forward direction for orienting model
    var centerHigh = Cesium.Cartesian3.fromDegrees((minWGS84[0] + maxWGS84[0]) / 2, (minWGS84[1] + maxWGS84[1]) / 2,1);

    // use direction from bottom left to top left as up-vector
    var bottomLeft  = cartToVec(Cesium.Cartesian3.fromDegrees(minWGS84[0], minWGS84[1]));
    var topLeft = cartToVec(Cesium.Cartesian3.fromDegrees(minWGS84[0], maxWGS84[1]));
    var latDir  = new THREE.Vector3().subVectors(bottomLeft,topLeft ).normalize();

    // configure entity position and orientation
    _3Dobjects[id].graphMesh.position.copy(center);
    _3Dobjects[id].graphMesh.lookAt(centerHigh);
    _3Dobjects[id].graphMesh.up.copy(latDir);
  }

  // Clone Cesium Camera projection position so the
  // Three.js Object will appear to be at the same place as above the Cesium Globe
  three.camera.matrixAutoUpdate = false;
  var cvm = cesium.viewer.camera.viewMatrix;
  var civm = cesium.viewer.camera.inverseViewMatrix;
  three.camera.matrixWorld.set(
      civm[0], civm[4], civm[8 ], civm[12],
      civm[1], civm[5], civm[9 ], civm[13],
      civm[2], civm[6], civm[10], civm[14],
      civm[3], civm[7], civm[11], civm[15]
  );
  three.camera.matrixWorldInverse.set(
      cvm[0], cvm[4], cvm[8 ], cvm[12],
      cvm[1], cvm[5], cvm[9 ], cvm[13],
      cvm[2], cvm[6], cvm[10], cvm[14],
      cvm[3], cvm[7], cvm[11], cvm[15]
  );
  three.camera.lookAt(new THREE.Vector3(0,0,0));

  var width = ThreeContainer.clientWidth;
  var height = ThreeContainer.clientHeight;
  var aspect = width / height;
  three.camera.aspect = aspect;
  three.camera.updateProjectionMatrix();

  three.renderer.setSize(width, height);
  three.renderer.render(three.scene, three.camera);
}