ArcGIS API中的SceneView
使用WebGL
在屏幕上渲染地圖和場景,還提供了一個底層接口來訪問SceneView的WebGL上下文,因此可以創建與場景交互的自定義??可視化效果,方式與內置圖層相同。那么我們可以直接編寫WebGL代碼,也可以集成第三方WebGL庫(例如Three.js)。
現在我門就來嘗試在ArcGIS的三維場景中加入一個物體,比如說UFO
:
該模型下載自CG模型網。然后通過
3DS MAX
軟件導出為obj格式模型。
1.引入需要用到的類
引入以下所要用到的類,并創建一個地圖三維場景:
require([
'esri/Map', // 生成地圖的類
'esri/views/SceneView', // 生成三維場景的類
'esri/views/3d/externalRenderers', // 外部渲染器對象
'esri/geometry/SpatialReference', // 空間參考的類
], function(Map, SceneView, externalRenderers, SpatialReference) {
const map = new Map({
basemap: 'topo-vector',
});
const view = new SceneView({
container: 'viewDiv', // 包含視圖的容器
map: map,
center: [105, 29],
zoom: 3,
});
});
2.定義外部渲染器對象
我們需要使用回調方法和屬性來定義一個外部渲染器,在向SceneView注冊時需要用到。
const myRenderer = {
renderer: null, // three.js 渲染器
camera: null, // three.js 相機
scene: null, // three.js 中的場景
ambient: null, // three.js中的環境光
sun: null, // three.js中的平行光源,模擬太陽光
ufo: null, // ufo
setup: function(context) {},
render: function(context) {},
dispose: function(context) {},
};
其中setup
、render
和dispose
為渲染器回調。
setup
函數通常在將外部渲染器添加到視圖后調用一次,或者每當SceneView準備就緒時調用一次。如果就緒狀態循環(例如,當將不同的Map分配給視圖時),則可以再次調用它。接收一個類型為RenderContext的參數。
render
函數在每一幀中調用以執行狀態更新和繪制。接收一個類型為RenderContext的參數。
dispose
函數在從視圖中移除外部渲染器時,或者視圖的就緒狀態變為false時調用。接收一個類型為RenderContext的參數。
2.1完善setup回調函數
我們需要在該回調函數中定義Three.js的渲染器、場景、攝像機和光源,還要導入需要加載到場景中的UFO模型。
①定義Three.js的渲染器
this.renderer = new THREE.WebGLRenderer({
context: context.gl, // 可用于將渲染器附加到已有的渲染環境(RenderingContext)中
premultipliedAlpha: false, // renderer是否假設顏色有 premultiplied alpha. 默認為true
});
設置設備像素比,可以避免HiDPI設備上繪圖模糊:
this.renderer.setPixelRatio(window.devicePixelRatio);
設置視口大小和三維場景的大小一樣:
this.renderer.setViewport(0, 0, view.width, view.height);
為了防止Three.js清除ArcGIS JS API提供的緩沖區,需要添加以下代碼:
this.renderer.autoClearDepth = false; // 定義renderer是否清除深度緩存
this.renderer.autoClearStencil = false; // 定義renderer是否清除模板緩存
this.renderer.autoClearColor = false; // 定義renderer是否清除顏色緩存
ArcGIS JS API渲染自定義離屏緩沖區,而不是默認的幀緩沖區。我們必須將這段代碼注入到Three.js運行時中,以便綁定這些緩沖區而不是默認的緩沖區。
const originalSetRenderTarget = this.renderer.setRenderTarget.bind(
this.renderer
);
this.renderer.setRenderTarget = function(target) {
originalSetRenderTarget(target);
if (target == null) {
context.bindRenderTarget();
}
};
②定義場景和相機
this.scene = new THREE.Scene(); // 場景
this.camera = new THREE.PerspectiveCamera(); // 相機
③定義光源并添加到場景中
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); // 把太陽光添加到場景中
④添加輔助工具
為了更好的理解空間位置,可以添加坐標軸輔助工具:
const axesHelper = new THREE.AxesHelper(10000000);
this.scene.add(axesHelper);
⑤加載OBJ模型
加載模型之前先要加載模型的材質信息文件,也就是.mtl
格式的文件,需要用到MTLLoader
加載器。加載obj模型則需要用到OBJLoader
加載器。它們都可以在全球最大同性交友網站(GitHub)的three.js
代碼倉庫下找到。
let mtlLoader = new MTLLoader();
mtlLoader.setPath('../assets/model/');
mtlLoader.load('ufo.mtl', materials => {
materials.preload();
// OBJLoader
const loader = new OBJLoader();
loader.setMaterials(materials);
loader.setPath('../assets/model/');
loader.load(
'ufo.obj', // 資源地址
// 加載成功后的回調
object => {
// ...
},
// 加載過程中的回調
function(xhr) {
// console.log((xhr.loaded / xhr.total) * 100 + '% loaded');
},
// 加載模型出錯的回調
function(error) {
console.error('An error happened: ', error);
}
);
});
加載成功的回調方法接收一個參數,該參數就是Object3D對象,也就是我們要加載的3D模型對象。在該回調中,我們可以進行模型的位置調整,以及大小調整等設置,然后添加到場景中。
this.ufo = object;
const entryPos = [70, 0, 550000]; // 輸入位置 [經度, 緯度, 高程]
const renderPos = [0, 0, 0]; // 渲染位置
externalRenderers.toRenderCoordinates(
view,
entryPos,
0,
SpatialReference.WGS84,
renderPos,
0,
1
);
this.ufo.scale.set(100000, 100000, 100000); // UFO放大一點
this.ufo.position.set( // 設置UFO位置
renderPos[0],
renderPos[1],
renderPos[2]
);
this.scene.add(this.ufo); // 添加到場景中
externalRenderers
對象的toRenderCoordinates
方法是將位置從給定的空間參考轉換為內部渲染坐標系,共接收7個參數(view, srcCoordinates, srcStart, srcSpatialReference, destCoordinates, destStart, count )。
view: 地圖場景。該參數類型為SceneView
srcCoordinates: 一個或多個向量坐標組成的一維數組,例如[x1, y1, z1, x2, y2, z2],數組中元素數量必須是3的倍數。該參數類型為Array
srcStart: srcCoordinates中的索引,從該索引開始讀取坐標。該參數類型為Number
srcSpatialReference: 輸入坐標的空間參考。如果為null,則用view.spatialReference替代。該參數類型為SpatialReference
destCoordinates: 對要寫入結果的數組的引用。該參數類型為Array
destStart: destCoordinates中的索引,坐標將從索引處開始寫入。該參數類型為Number
count: 要轉換的坐標數量。該參數類型為Number
2.2完善render回調函數
在每一幀中都會調用該回調函數,接收一個類型為RenderContext
的參數。在該回調中我們可以進行相機參數更新,模型位置更新等操作。
// 更新相機參數
const 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])
);
// 投影矩陣可以直接復制
this.camera.projectionMatrix.fromArray(cam.projectionMatrix);
// 更新UFO
this.ufo.rotation.y += 0.1;
// 繪制場景
this.renderer.state.reset();
this.renderer.render(this.scene, this.camera);
externalRenderers.requestRender(view); // 請求重繪視圖。
// 清除WebGL狀態
context.resetWebGLState();
添加外部渲染器
最后還有一個關鍵步驟,向SceneView
實例注冊外部渲染器:
externalRenderers.add(view, myRenderer);
這樣我們就成功地在地圖三維場景中渲染出用Three.js加載的外部模型UFO啦!
以下是完整代碼
<html>
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="initial-scale=1, maximum-scale=1, user-scalable=no"
/>
<title>ArcGIS API在視圖中渲染Three.js場景</title>
<style>
html,
body,
#viewDiv {
padding: 0;
margin: 0;
height: 100%;
width: 100%;
}
</style>
<link
rel="stylesheet"
/>
<script src="https://js.arcgis.com/4.14/"></script>
<script type="module">
import * as THREE from 'https://threejs.org/build/three.module.js';
import { OBJLoader } from 'https://threejs.org/examples/jsm/loaders/OBJLoader.js';
import { MTLLoader } from 'https://threejs.org/examples/jsm/loaders/MTLLoader.js';
require([
'esri/Map',
'esri/views/SceneView',
'esri/views/3d/externalRenderers',
'esri/geometry/SpatialReference',
], function(Map, SceneView, externalRenderers, SpatialReference) {
const map = new Map({
basemap: 'topo-vector',
});
const view = new SceneView({
container: 'viewDiv',
map: map,
center: [105, 29],
zoom: 3,
});
const myRenderer = {
renderer: null, // three.js 渲染器
camera: null, // three.js 相機
scene: null, // three.js 中的場景
ambient: null, // three.js中的環境光
sun: null, // three.js中的平行光源,模擬太陽光
ufo: null, // ufo
setup: function(context) {
this.renderer = new THREE.WebGLRenderer({
context: context.gl, // 可用于將渲染器附加到已有的渲染環境(RenderingContext)中
premultipliedAlpha: false, // renderer是否假設顏色有 premultiplied alpha. 默認為true
});
this.renderer.setPixelRatio(window.devicePixelRatio); // 設置設備像素比。通常用于避免HiDPI設備上繪圖模糊
this.renderer.setViewport(0, 0, view.width, view.height); // 視口大小設置
// 防止Three.js清除ArcGIS JS API提供的緩沖區。
this.renderer.autoClearDepth = false; // 定義renderer是否清除深度緩存
this.renderer.autoClearStencil = false; // 定義renderer是否清除模板緩存
this.renderer.autoClearColor = false; // 定義renderer是否清除顏色緩存
// ArcGIS JS API渲染自定義離屏緩沖區,而不是默認的幀緩沖區。
// 我們必須將這段代碼注入到three.js運行時中,以便綁定這些緩沖區而不是默認的緩沖區。
const originalSetRenderTarget = this.renderer.setRenderTarget.bind(
this.renderer
);
this.renderer.setRenderTarget = function(target) {
originalSetRenderTarget(target);
if (target == null) {
// 綁定外部渲染器應該渲染到的顏色和深度緩沖區
context.bindRenderTarget();
}
};
this.scene = new THREE.Scene(); // 場景
this.camera = new THREE.PerspectiveCamera(); // 相機
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); // 把太陽光添加到場景中
// 添加坐標軸輔助工具
const axesHelper = new THREE.AxesHelper(10000000);
this.scene.add(axesHelper);
// 加載模型
let mtlLoader = new MTLLoader();
mtlLoader.setPath('../assets/model/');
mtlLoader.load('ufo.mtl', materials => {
materials.preload();
// OBJLoader
const loader = new OBJLoader();
loader.setMaterials(materials);
loader.setPath('../assets/model/');
loader.load(
'ufo.obj', // 資源地址
// 加載成功后的回調
object => {
this.ufo = object;
const entryPos = [70, 0, 550000]; // 輸入位置
const renderPos = [0, 0, 0]; // 渲染位置
externalRenderers.toRenderCoordinates(
view,
entryPos,
0,
SpatialReference.WGS84,
renderPos,
0,
1
);
this.ufo.scale.set(100000, 100000, 100000); // ufo放大一點
this.ufo.position.set(
renderPos[0],
renderPos[1],
renderPos[2]
);
this.scene.add(this.ufo);
context.resetWebGLState();
},
// 加載過程中的回調
function(xhr) {
// console.log((xhr.loaded / xhr.total) * 100 + '% loaded');
},
// 加載模型出錯的回調
function(error) {
console.error('An error happened: ', error);
}
);
});
},
render: function(context) {
// 更新相機參數
const 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])
);
// 投影矩陣可以直接復制
this.camera.projectionMatrix.fromArray(cam.projectionMatrix);
// 更新UFO
this.ufo.rotation.y += 0.1;
// 繪制場景
this.renderer.state.reset();
this.renderer.render(this.scene, this.camera);
// 請求重繪視圖。
externalRenderers.requestRender(view);
// cleanup
context.resetWebGLState();
},
};
// 注冊renderer
externalRenderers.add(view, myRenderer);
});
</script>
</head>
<body>
<div id="viewDiv"></div>
</body>
</html>