MISCELLANEOUS TECHNICAL ARTICLES BY Dr A R COLLINS

Cango3D Rotating Globe

Creating the 3D globe

The page shows an animated globe, demonstrating a more complex application created with the Cango3D graphics library. The input geographic data comes from a file containing the borders of each country of the world defined as a series of latitude and longitude pairs. These data are converted to Cgo3D (SVG style) x,y,z coordinates that are then used to make a Shape3D for each country.

The set of Shape3D representing the countries that will be visible for each frame are separated and further processed to hide the parts of the country that don't lie within the front facing hemisphere. It is task is computationally intensive and requires a dedicated 'paint' method replacing the standard Shape3D.paint. Some of the notable code snippets are shown below the canvas.

The animation has been made interactive, the rotation can be paused and resumes with the buttons and the globe axis can be tiled side to side or backward and forward using the sliders.

Animated 3D Globe

Features of the source code

The conversion from longitude, latitude pair to Cartesian [x,y,z]:

function sphericalToCart(p)   // p = [long, lat] in degrees
{
  const toRad = Math.PI/180.0;
  const lon = toRad*p[0];     // convert to radians
  const lat = toRad*p[1];
  const sinLon = Math.sin(lon);
  const cosLon = Math.cos(lon);
  const sinLat = Math.sin(lat);
  const cosLat = Math.cos(lat);

  return [radius*cosLat*sinLon, radius*sinLat, radius*cosLat*cosLon];  // [x, y, z]
}

To ensure land segments that are around the back of the globe in its current orientation aren't render, they must be mapped outside the the globe circle and then when the land is filled they are clipped by a global composition operation as follows:

function paintFrontFacing(gc) // 'paint' method for 'land3D' Shape3D
{
  // only draw this country if at least one point visible (transformed z coord >0)
  for (let i=0; i<this.drawCmds.length; i++)
  {
    const dc = this.drawCmds[i];
    if ((dc.drawFn === "moveTo" || dc.drawFn === "lineTo") && (dc.ep.tz > 0)) 
    {
      gc.ctx.save();

      gc.ctx.beginPath();
      // step through the drawCmds array and draw each one
      for (let j=0; j < this.drawCmds.length; j++)
      {
        const dc = this.drawCmds[j];
        // convert all parms to pixel coords
        if (dc.ep.tz < 0)  // line ends on back of globe, map it to outside the circle
        {
          const l = Math.sqrt(dc.parms[0]*dc.parms[0] + dc.parms[1]*dc.parms[1]);
          const scl = (2*radius - l)/l;
          dc.parms[0] = scl*dc.parms[0];
          dc.parms[1] = scl*dc.parms[1];
        }
        // map world coords to pixels
        dc.parmsPx[0] = gc.vpLLx+gc.xoffset+dc.parms[0]*gc.xscl;
        dc.parmsPx[1] = gc.vpLLy+gc.yoffset+dc.parms[1]*gc.yscl;
        // now actually draw the path onto the canvas
        gc.ctx[dc.drawFn].apply(gc.ctx, dc.parmsPx);
      }
      gc.ctx.closePath();
      // fill and stroke the path
      gc.ctx.fillStyle = this.fillColor.toRGBA();
      gc.ctx.fill();
      gc.ctx.restore();  // put things back the way they were

      break; // country has been drawn we are done
    }
  }
}