Skip welcome & menu and move to editor
Welcome to JS Bin
Load cached copy from
 
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
<title>Use three.js from an external renderer - 4.3</title>
<link rel="stylesheet" href="https://js.arcgis.com/4.3/esri/css/main.css">
<script src="https://js.arcgis.com/4.3/"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r79/three.js"></script>
<style>
  html, body, #viewDiv {
    padding: 0;
    margin: 0;
    height: 100%;
    width: 100%;
  }
  #instructions {
    background: rgba(0,0,0,.5);
    color: white;
    width: auto;
    position: fixed;
    top: 2%;
    right: 50%;
    transform: translateX(-50%);
    padding: 15px;
  }
</style>
<!-- Our application -->
<script>
require([
  "esri/Map",
  "esri/views/SceneView",
  "esri/views/3d/externalRenderers",
  "esri/geometry/SpatialReference", "esri/geometry/Polyline",
  "esri/geometry/Point", "esri/symbols/SimpleLineSymbol",
  "esri/geometry/Circle", "esri/geometry/geometryEngine",
  "esri/request",
  "dojo/domReady!"
],
function(
  Map,
  SceneView,
  externalRenderers,
  SpatialReference, Polyline,
  Point, SLS, Circle, geoEngine,
  esriRequest
) {
 // Create a map with elevation
  var map = new Map({
    basemap: "satellite",
    ground: 'world-elevation'
  });
  // Create a SceneView
  var view = new SceneView({
    container: "viewDiv",
    map: map,
    viewingMode: "global",
    zoom: 15,
    center: [-101.17, 21.78]
  });
  // create our custom external renderer
  var customRenderer = {
    renderer: null,     // three.js renderer
    camera: null,       // three.js camera
    scene: null,        // three.js scene
    ambient: null,      // three.js ambient light source
    sun: null,          // three.js sun light source
    /**
     * Setup function, called once by the ArcGIS JS API.
     */
    setup: function(context) {
      // initialize the three.js renderer
      this.renderer = new THREE.WebGLRenderer({
        context: context.gl,
        premultipliedAlpha: false
      });
      this.renderer.setPixelRatio(window.devicePixelRatio);
      this.renderer.setSize(context.camera.fullWidth, context.camera.fullHeight);
      // prevent three.js from clearing the buffers provided by the ArcGIS JS API.
      this.renderer.autoClearDepth = false;
      this.renderer.autoClearStencil = false;
      this.renderer.autoClearColor = false;
      // The ArcGIS JS API renders to custom offscreen buffers, and not to the default framebuffers.
      // We have to inject this bit of code into the three.js runtime in order for it to bind those
      // buffers instead of the default ones.
      var originalSetRenderTarget = this.renderer.setRenderTarget.bind(this.renderer);
      this.renderer.setRenderTarget = function(target) {
        originalSetRenderTarget(target);
        if (target == null) {
          context.bindRenderTarget();
        }
      }
      // setup the three.js scene
      this.scene = new THREE.Scene();
      // setup the camera
      var cam = context.camera;
      this.camera = new THREE.PerspectiveCamera(cam.fovY, cam.aspect, cam.near, cam.far);
      // setup scene lighting
      this.ambient = new THREE.AmbientLight( 0xffffff, 0.5);
      this.scene.add(this.ambient);
      this.sun = new THREE.DirectionalLight(0xffffff, 0.5);
      this.scene.add(this.sun);
      // cleanup after ourselfs
      context.resetWebGLState();
    },
    render: function(context) {
      // update camera parameters
      var cam = context.camera;
      this.camera.position.set(cam.eye[0], cam.eye[1], cam.eye[2]);
      this.camera.up.set(cam.up[0], cam.up[1], cam.up[2]);
      this.camera.lookAt(new THREE.Vector3(cam.center[0], cam.center[1], cam.center[2]));
      // Projection matrix can be copied directly
      this.camera.projectionMatrix.fromArray(cam.projectionMatrix);
      // update lighting
      view.environment.lighting.date = Date.now();
      var l = context.sunLight;
      this.sun.position.set(
        l.direction[0],
        l.direction[1],
        l.direction[2]
      );
      this.sun.intensity = l.diffuse.intensity;
      this.sun.color = new THREE.Color(l.diffuse.color[0], l.diffuse.color[1], l.diffuse.color[2]);
      this.ambient.intensity = l.ambient.intensity;
      this.ambient.color = new THREE.Color(l.ambient.color[0], l.ambient.color[1], l.ambient.color[2]);
      // draw the scene
      this.renderer.resetGLState();
      this.renderer.render(this.scene, this.camera);
      externalRenderers.requestRender(view);
      // cleanup
      context.resetWebGLState();
    },
    
    // function to add a square of material to the map that follows the terrain
    addTerrain: function(e){
      // width of the square in meters
      // since it's a square the height is the same
      const radius = 1000;
      
      // distance the geometry will be placed over the ground in meters
      const metersOverGround = 100;
      
      // create a circle and extent to get a geographic representation of the square we're 
      // going to add
      let circ = new Circle({
        center: e.mapPoint,
        geodesic: true,
        radius: radius
      });
      
      let extent = circ.extent;
      
      // position the user clicked on
      var pos = [e.mapPoint.longitude,e.mapPoint.latitude,0];
      
      // create the transformation.  this is used to describe how to transition wgs84 and ECEF
      // I'm not too clear on the specifics
      var transform = new THREE.Matrix4();
      transform.fromArray(externalRenderers.renderCoordinateTransformAt(view, pos, SpatialReference.WGS84, new Array(16)));
      
      
      // params: width, height, width resolution, height resolution
      // multiply radius * 2 to get the total width and height of the square we're adding
      // the resolution decides how many detailed the geometry will be/how many vertices it has
      // in this example, the width resolution and height resolution must be the same, but this can be tweaked:
      // high resolutions will take longer to build the elevation array but will be more detailed.
      var geometry = new THREE.PlaneBufferGeometry( radius * 2, radius *2, 200, 200);
      // color is set here.  wireframe can be true or false, if it's false, the plane will be rendered
      // solid
      var mesh = new THREE.Mesh( geometry, new THREE.MeshLambertMaterial({ wireframe:true, color: 0x00f4ff, opacity: .2, side: THREE.DoubleSide }) );
      // set the position of the mesh in terms of the transform calculated earlier
      mesh.position.set(transform.elements[12], transform.elements[13], transform.elements[14]);
      var rotation = new THREE.Matrix4();
      rotation.extractRotation(transform);
      mesh.setRotationFromMatrix(rotation);
      
      // this is the array of vertices in the geomerty
      let arr = geometry.attributes.position.array;
      // create array of elevations based on the extent we're looking at 
      let elevations = buildElevationRaster(extent, 201);
      
      // set the z value for each vertex based on the elevation data
      for (let i = 0, j = 2, l = arr.length / 3; i < l; i ++, j += 3) {
        arr[j] = elevations[i] + metersOverGround;
      }
      // compute shading
      geometry.computeFaceNormals();
      geometry.computeVertexNormals();
      // add mesh to scene
      this.scene.add(mesh)
    }
  }
  
  /** 
  *  build a raster of elevation values that matches up to the 
  *  planebuffer geometry that we're drawing.  we need to set the z
  *  value of each vertex in the plane buffer geometry based on the
  *  terrain's elevation at that location
  *
  *  the extent is a map space representation of the plane buffer geometry,
  *  without any z values
  *
  *  the width is the resolution of the planebuffer.  we're assuming it's a
  *  square here, so we just need one dimension
  **/
  function buildElevationRaster(extent, width){
    // get the width of each cell in meters
    let resolutionInMeters = extent.width / width;
    
    // the top of the extent densified based on the width
    // this is an array of points in web mercator
    let xAxis = geoEngine.densify(new Polyline({
      paths: [
        [extent.xmin, extent.ymax],
        [extent.xmax, extent.ymax]
      ],
      spatialReference: SpatialReference.WebMercator
    }), resolutionInMeters, 'meters').paths[0].slice(0,width);
    
    // the left of the extent densified by the width
    // this is an array of points in web mercator
    let yAxis = geoEngine.densify(new Polyline({
      paths: [
        [extent.xmin, extent.ymax],
        [extent.xmin, extent.ymin]
      ],
      spatialReference: SpatialReference.WebMercator
    }), resolutionInMeters, 'meters').paths[0].slice(0,width);
    // float 32 array to hold the elevation data.
    // this could be a normal array
    let elevations = new Float32Array(Math.pow(width,2));
    // fill the elevation array
    for (let i = 0; i < elevations.length; i++){
      let point = xy2Point(idx2XY(i, width), xAxis, yAxis);
      elevations[i] = view.basemapTerrain.getElevation(point);
    }
    return elevations;
  }
  // go from idx of array to [x, y] point (in raster space)
  // depends on known width of raster
  function idx2XY(idx, width){
    const x = idx % width;
    const y = (idx - x) / width;
    return([x,y]);
  }
  // go from [x,y] point in raster space to geographic point
  // in x,y web mercator coordinates.  we can use the x and y axis 
  // from the extent to make this easier.  note: this is a planar
  // calculation, not geodesic
  function xy2Point(xyPoint, xAxis, yAxis){
    return new Point({
      x: xAxis[xyPoint[0]][0],
      y: yAxis[xyPoint[1]][1],
      spatialReference: SpatialReference.WebMercator
    });
  }
  
  // set click event
  view.on("click", customRenderer.addTerrain.bind(customRenderer));
  // register the external renderer
  externalRenderers.add(view, customRenderer);
});
</script>
</head>
<body>
  <div id="viewDiv">
    <div id="instructions">Click to add square shaped to terrain</div>
  </div>
</body>
</html>
Output

You can jump to the latest bin by adding /latest to your URL

Dismiss x
public
Bin info
solowtpro
0viewers