ArcGIS API在視圖中渲染Three.js場(chǎng)景

ArcGIS API中的SceneView使用WebGL在屏幕上渲染地圖和場(chǎng)景牡直,還提供了一個(gè)底層接口來(lái)訪問(wèn)SceneView的WebGL上下文,因此可以創(chuàng)建與場(chǎng)景交互的自定義??可視化效果,方式與內(nèi)置圖層相同证舟。那么我們可以直接編寫(xiě)WebGL代碼拴曲,也可以集成第三方WebGL庫(kù)(例如Three.js)。
現(xiàn)在我門(mén)就來(lái)嘗試在ArcGIS的三維場(chǎng)景中加入一個(gè)物體士修,比如說(shuō)UFO

UFO模型

該模型下載自CG模型網(wǎng)枷遂。然后通過(guò)3DS MAX軟件導(dǎo)出為obj格式模型。

1.引入需要用到的類(lèi)

引入以下所要用到的類(lèi)棋嘲,并創(chuàng)建一個(gè)地圖三維場(chǎng)景:

require([
    'esri/Map', // 生成地圖的類(lèi)
    'esri/views/SceneView', // 生成三維場(chǎng)景的類(lèi)
    'esri/views/3d/externalRenderers', // 外部渲染器對(duì)象
    'esri/geometry/SpatialReference', // 空間參考的類(lèi)
    ], 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.定義外部渲染器對(duì)象

我們需要使用回調(diào)方法和屬性來(lái)定義一個(gè)外部渲染器酒唉,在向SceneView注冊(cè)時(shí)需要用到。

const myRenderer = {
    renderer: null, // three.js 渲染器
    camera: null, // three.js 相機(jī)
    scene: null, // three.js 中的場(chǎng)景
    ambient: null, // three.js中的環(huán)境光
    sun: null, // three.js中的平行光源沸移,模擬太陽(yáng)光
    ufo: null, // ufo

    setup: function(context) {},
    render: function(context) {},
    dispose: function(context) {},
};

其中setup痪伦、renderdispose為渲染器回調(diào)。

setup 函數(shù)通常在將外部渲染器添加到視圖后調(diào)用一次雹锣,或者每當(dāng)SceneView準(zhǔn)備就緒時(shí)調(diào)用一次网沾。如果就緒狀態(tài)循環(huán)(例如,當(dāng)將不同的Map分配給視圖時(shí))蕊爵,則可以再次調(diào)用它辉哥。接收一個(gè)類(lèi)型為RenderContext的參數(shù)。
render 函數(shù)在每一幀中調(diào)用以執(zhí)行狀態(tài)更新和繪制在辆。接收一個(gè)類(lèi)型為RenderContext的參數(shù)证薇。
dispose 函數(shù)在從視圖中移除外部渲染器時(shí),或者視圖的就緒狀態(tài)變?yōu)閒alse時(shí)調(diào)用匆篓。接收一個(gè)類(lèi)型為RenderContext的參數(shù)浑度。

2.1完善setup回調(diào)函數(shù)

我們需要在該回調(diào)函數(shù)中定義Three.js的渲染器、場(chǎng)景鸦概、攝像機(jī)和光源箩张,還要導(dǎo)入需要加載到場(chǎng)景中的UFO模型甩骏。

①定義Three.js的渲染器

this.renderer = new THREE.WebGLRenderer({
    context: context.gl, // 可用于將渲染器附加到已有的渲染環(huán)境(RenderingContext)中
    premultipliedAlpha: false, // renderer是否假設(shè)顏色有 premultiplied alpha. 默認(rèn)為true
});

設(shè)置設(shè)備像素比,可以避免HiDPI設(shè)備上繪圖模糊:

this.renderer.setPixelRatio(window.devicePixelRatio);

設(shè)置視口大小和三維場(chǎng)景的大小一樣:

this.renderer.setViewport(0, 0, view.width, view.height);

為了防止Three.js清除ArcGIS JS API提供的緩沖區(qū)先慷,需要添加以下代碼:

this.renderer.autoClearDepth = false; // 定義renderer是否清除深度緩存
this.renderer.autoClearStencil = false; // 定義renderer是否清除模板緩存
this.renderer.autoClearColor = false; // 定義renderer是否清除顏色緩存

ArcGIS JS API渲染自定義離屏緩沖區(qū)饮笛,而不是默認(rèn)的幀緩沖區(qū)。我們必須將這段代碼注入到Three.js運(yùn)行時(shí)中论熙,以便綁定這些緩沖區(qū)而不是默認(rèn)的緩沖區(qū)福青。

const originalSetRenderTarget = this.renderer.setRenderTarget.bind(
    this.renderer
);
this.renderer.setRenderTarget = function(target) {
    originalSetRenderTarget(target);
    if (target == null) {
        context.bindRenderTarget();
    }
};

②定義場(chǎng)景和相機(jī)

this.scene = new THREE.Scene(); // 場(chǎng)景
this.camera = new THREE.PerspectiveCamera(); // 相機(jī)

③定義光源并添加到場(chǎng)景中

this.ambient = new THREE.AmbientLight(0xffffff, 0.5); // 環(huán)境光
this.scene.add(this.ambient); // 把環(huán)境光添加到場(chǎng)景中
this.sun = new THREE.DirectionalLight(0xffffff, 0.5); // 平行光(模擬太陽(yáng)光)
this.scene.add(this.sun); // 把太陽(yáng)光添加到場(chǎng)景中

④添加輔助工具

為了更好的理解空間位置,可以添加坐標(biāo)軸輔助工具:

const axesHelper = new THREE.AxesHelper(10000000);
this.scene.add(axesHelper);

⑤加載OBJ模型

加載模型之前先要加載模型的材質(zhì)信息文件脓诡,也就是.mtl格式的文件无午,需要用到MTLLoader加載器。加載obj模型則需要用到OBJLoader加載器祝谚。它們都可以在全球最大同性交友網(wǎng)站(GitHub)的three.js代碼倉(cāng)庫(kù)下找到宪迟。

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', // 資源地址
        // 加載成功后的回調(diào)
        object => {
            // ...
        },
        // 加載過(guò)程中的回調(diào)
        function(xhr) {
            // console.log((xhr.loaded / xhr.total) * 100 + '% loaded');
        },
        // 加載模型出錯(cuò)的回調(diào)
        function(error) {
            console.error('An error happened: ', error);
        }
    );
});

加載成功的回調(diào)方法接收一個(gè)參數(shù),該參數(shù)就是Object3D對(duì)象交惯,也就是我們要加載的3D模型對(duì)象次泽。在該回調(diào)中,我們可以進(jìn)行模型的位置調(diào)整席爽,以及大小調(diào)整等設(shè)置意荤,然后添加到場(chǎng)景中。

this.ufo = object;
const entryPos = [70, 0, 550000]; // 輸入位置 [經(jīng)度, 緯度, 高程]
const renderPos = [0, 0, 0]; // 渲染位置
externalRenderers.toRenderCoordinates(
    view,
    entryPos,
    0,
    SpatialReference.WGS84,
    renderPos,
    0,
    1
);
this.ufo.scale.set(100000, 100000, 100000); // UFO放大一點(diǎn)
this.ufo.position.set( // 設(shè)置UFO位置
    renderPos[0],
    renderPos[1],
    renderPos[2]
);
this.scene.add(this.ufo); // 添加到場(chǎng)景中

externalRenderers對(duì)象的toRenderCoordinates方法是將位置從給定的空間參考轉(zhuǎn)換為內(nèi)部渲染坐標(biāo)系拳昌,共接收7個(gè)參數(shù)(view, srcCoordinates, srcStart, srcSpatialReference, destCoordinates, destStart, count )袭异。
view: 地圖場(chǎng)景。該參數(shù)類(lèi)型為SceneView
srcCoordinates: 一個(gè)或多個(gè)向量坐標(biāo)組成的一維數(shù)組炬藤,例如[x1, y1, z1, x2, y2, z2]御铃,數(shù)組中元素?cái)?shù)量必須是3的倍數(shù)。該參數(shù)類(lèi)型為Array
srcStart: srcCoordinates中的索引沈矿,從該索引開(kāi)始讀取坐標(biāo)上真。該參數(shù)類(lèi)型為Number
srcSpatialReference: 輸入坐標(biāo)的空間參考。如果為null羹膳,則用view.spatialReference替代睡互。該參數(shù)類(lèi)型為SpatialReference
destCoordinates: 對(duì)要寫(xiě)入結(jié)果的數(shù)組的引用。該參數(shù)類(lèi)型為Array
destStart: destCoordinates中的索引陵像,坐標(biāo)將從索引處開(kāi)始寫(xiě)入就珠。該參數(shù)類(lèi)型為Number
count: 要轉(zhuǎn)換的坐標(biāo)數(shù)量。該參數(shù)類(lèi)型為Number

2.2完善render回調(diào)函數(shù)

在每一幀中都會(huì)調(diào)用該回調(diào)函數(shù)醒颖,接收一個(gè)類(lèi)型為RenderContext的參數(shù)妻怎。在該回調(diào)中我們可以進(jìn)行相機(jī)參數(shù)更新,模型位置更新等操作泞歉。

// 更新相機(jī)參數(shù)
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])
);
// 投影矩陣可以直接復(fù)制
this.camera.projectionMatrix.fromArray(cam.projectionMatrix);

// 更新UFO
this.ufo.rotation.y += 0.1;

// 繪制場(chǎng)景
this.renderer.state.reset();
this.renderer.render(this.scene, this.camera);

externalRenderers.requestRender(view); // 請(qǐng)求重繪視圖逼侦。

// 清除WebGL狀態(tài)
context.resetWebGLState();

添加外部渲染器

最后還有一個(gè)關(guān)鍵步驟匿辩,向SceneView實(shí)例注冊(cè)外部渲染器:

externalRenderers.add(view, myRenderer);

這樣我們就成功地在地圖三維場(chǎng)景中渲染出用Three.js加載的外部模型UFO啦!

地圖中加載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場(chǎng)景</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 相機(jī)
          scene: null, // three.js 中的場(chǎng)景
          ambient: null, // three.js中的環(huán)境光
          sun: null, // three.js中的平行光源榛丢,模擬太陽(yáng)光
          ufo: null, // ufo

          setup: function(context) {
            this.renderer = new THREE.WebGLRenderer({
              context: context.gl, // 可用于將渲染器附加到已有的渲染環(huán)境(RenderingContext)中
              premultipliedAlpha: false, // renderer是否假設(shè)顏色有 premultiplied alpha. 默認(rèn)為true
            });
            this.renderer.setPixelRatio(window.devicePixelRatio); // 設(shè)置設(shè)備像素比铲球。通常用于避免HiDPI設(shè)備上繪圖模糊
            this.renderer.setViewport(0, 0, view.width, view.height); // 視口大小設(shè)置

            // 防止Three.js清除ArcGIS JS API提供的緩沖區(qū)。
            this.renderer.autoClearDepth = false; // 定義renderer是否清除深度緩存
            this.renderer.autoClearStencil = false; // 定義renderer是否清除模板緩存
            this.renderer.autoClearColor = false; // 定義renderer是否清除顏色緩存

            // ArcGIS JS API渲染自定義離屏緩沖區(qū)晰赞,而不是默認(rèn)的幀緩沖區(qū)稼病。
            // 我們必須將這段代碼注入到three.js運(yùn)行時(shí)中,以便綁定這些緩沖區(qū)而不是默認(rèn)的緩沖區(qū)宾肺。
            const originalSetRenderTarget = this.renderer.setRenderTarget.bind(
              this.renderer
            );
            this.renderer.setRenderTarget = function(target) {
              originalSetRenderTarget(target);
              if (target == null) {
                // 綁定外部渲染器應(yīng)該渲染到的顏色和深度緩沖區(qū)
                context.bindRenderTarget();
              }
            };

            this.scene = new THREE.Scene(); // 場(chǎng)景
            this.camera = new THREE.PerspectiveCamera(); // 相機(jī)

            this.ambient = new THREE.AmbientLight(0xffffff, 0.5); // 環(huán)境光
            this.scene.add(this.ambient); // 把環(huán)境光添加到場(chǎng)景中
            this.sun = new THREE.DirectionalLight(0xffffff, 0.5); // 平行光(模擬太陽(yáng)光)
            this.scene.add(this.sun); // 把太陽(yáng)光添加到場(chǎng)景中

            // 添加坐標(biāo)軸輔助工具
            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', // 資源地址
                // 加載成功后的回調(diào)
                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放大一點(diǎn)
                  this.ufo.position.set(
                    renderPos[0],
                    renderPos[1],
                    renderPos[2]
                  );
                  this.scene.add(this.ufo);
                  context.resetWebGLState();
                },
                // 加載過(guò)程中的回調(diào)
                function(xhr) {
                  // console.log((xhr.loaded / xhr.total) * 100 + '% loaded');
                },
                // 加載模型出錯(cuò)的回調(diào)
                function(error) {
                  console.error('An error happened: ', error);
                }
              );
            });
          },
          render: function(context) {
            // 更新相機(jī)參數(shù)
            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])
            );
            // 投影矩陣可以直接復(fù)制
            this.camera.projectionMatrix.fromArray(cam.projectionMatrix);
            // 更新UFO
            this.ufo.rotation.y += 0.1;
            // 繪制場(chǎng)景
            this.renderer.state.reset();
            this.renderer.render(this.scene, this.camera);
            // 請(qǐng)求重繪視圖溯饵。
            externalRenderers.requestRender(view); 
            // cleanup
            context.resetWebGLState();
          },
        };
        // 注冊(cè)renderer
        externalRenderers.add(view, myRenderer);
      });
    </script>
  </head>
  <body>
    <div id="viewDiv"></div>
  </body>
</html>
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市锨用,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌隘谣,老刑警劉巖增拥,帶你破解...
    沈念sama閱讀 218,204評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異寻歧,居然都是意外死亡掌栅,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)码泛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)猾封,“玉大人,你說(shuō)我怎么就攤上這事噪珊∩卧担” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,548評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵痢站,是天一觀的道長(zhǎng)磷箕。 經(jīng)常有香客問(wèn)我,道長(zhǎng)阵难,這世上最難降的妖魔是什么岳枷? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,657評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮呜叫,結(jié)果婚禮上空繁,老公的妹妹穿的比我還像新娘。我一直安慰自己朱庆,他們只是感情好盛泡,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,689評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著椎工,像睡著了一般饭于。 火紅的嫁衣襯著肌膚如雪蜀踏。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,554評(píng)論 1 305
  • 那天掰吕,我揣著相機(jī)與錄音果覆,去河邊找鬼。 笑死殖熟,一個(gè)胖子當(dāng)著我的面吹牛局待,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播菱属,決...
    沈念sama閱讀 40,302評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼钳榨,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了纽门?” 一聲冷哼從身側(cè)響起薛耻,我...
    開(kāi)封第一講書(shū)人閱讀 39,216評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎赏陵,沒(méi)想到半個(gè)月后饼齿,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,661評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蝙搔,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,851評(píng)論 3 336
  • 正文 我和宋清朗相戀三年缕溉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吃型。...
    茶點(diǎn)故事閱讀 39,977評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡证鸥,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出勤晚,到底是詐尸還是另有隱情枉层,我是刑警寧澤,帶...
    沈念sama閱讀 35,697評(píng)論 5 347
  • 正文 年R本政府宣布运翼,位于F島的核電站返干,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏血淌。R本人自食惡果不足惜矩欠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望悠夯。 院中可真熱鬧癌淮,春花似錦、人聲如沸沦补。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,898評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)夕膀。三九已至虚倒,卻和暖如春美侦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背魂奥。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,019評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工菠剩, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人耻煤。 一個(gè)月前我還...
    沈念sama閱讀 48,138評(píng)論 3 370
  • 正文 我出身青樓具壮,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親哈蝇。 傳聞我的和親對(duì)象是個(gè)殘疾皇子棺妓,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,927評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容