<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
Keyboard Shortcuts
Shortcut | Action |
---|---|
ctrl + [num] | Toggle nth panel |
ctrl + 0 | Close focused panel |
ctrl + enter | Re-render output. If console visible: run JS in console |
Ctrl + l | Clear the console |
ctrl + / | Toggle comment on selected lines |
ctrl + ] | Indents selected lines |
ctrl + [ | Unindents selected lines |
tab | Code complete & Emmet expand |
ctrl + shift + L | Beautify code in active panel |
ctrl + s | Save & lock current Bin from further changes |
ctrl + shift + s | Open the share options |
ctrl + y | Archive Bin |
Complete list of JS Bin shortcuts |
JS Bin URLs
URL | Action |
---|---|
/ | Show the full rendered output. This content will update in real time as it's updated from the /edit url. |
/edit | Edit the current bin |
/watch | Follow a Code Casting session |
/embed | Create an embeddable version of the bin |
/latest | Load the very latest bin (/latest goes in place of the revision) |
/[username]/last | View the last edited bin for this user |
/[username]/last/edit | Edit the last edited bin for this user |
/[username]/last/watch | Follow the Code Casting session for the latest bin for this user |
/quiet | Remove analytics and edit button from rendered output |
.js | Load only the JavaScript for a bin |
.css | Load only the CSS for a bin |
Except for username prefixed urls, the url may start with http://jsbin.com/abc and the url fragments can be added to the url to view it differently. |