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