日韩欧美国产精品免费一二-日韩欧美国产精品亚洲二区-日韩欧美国产精品专区-日韩欧美国产另-日韩欧美国产免费看-日韩欧美国产免费看清风阁

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發文檔 其他文檔  
 
網站管理員

制作數字農場3D可視化大屏

admin
2025年4月11日 15:57 本文熱度 48

1.介紹

數字農業可視化是一種將農業生產過程中的各類數據,通過先進的信息技術手段進行采集、整合、分析,并以直觀的可視化形式呈現出來的技術應用模式。它利用大數據、物聯網、人工智能、GIS等技術,為農業生產經營管理提供了全新的、高效的決策支持工具,使農業從業者能夠更加清晰、準確地了解農業生產的各個環節,從而實現精準決策、精細管理和高效運營。

最近對數字農業有點感興趣,于是就有了接下來的探索和嘗試,本文的內容比較有綜合性,基本上用到了之前在技術社區分享的大部分經驗,不僅包括高德開發平臺的技術,也集成了具體業務分析、GIS數據生成、3D模型制作等內容。附演示頁面地址,源代碼地址見文末。

2. 需求分析

本次做可視化大屏的開發,我希望最終的開發成果是可以在后續的產品或者項目中復用、至少能發揮一定的參考價值,因此需要做一些業務需求分析。由于我在這方面的業務涉獵比較淺顯,于是先看了幾個智慧農業解決方案方便的PPT,然后詢問AI助手,整理為下面幾個專題的內容:

2.1 基礎配套

  1. 地形:以三維地形圖的形式呈現,通過不同顏色和高度標識展示區域內的山地、沼澤、平原等地形分布??梢允褂玫雀呔€、陰影等效果增強立體感,讓用戶直觀了解地形的起伏。由于增加地形起伏會直接增加其他貼合地形圖層的實現復雜度,為降低閱讀難度本次示例選了塊地形相對平整的沖擊平原,因此規避地形問題。
  2. 影像:展示高分辨率的衛星影像圖,全面覆蓋智慧農業所涉及的區域范圍,讓用戶能夠以宏觀視角清晰了解整個區域的全貌,包括地形、河流、村居、植被等基礎配套元素的分布及相互關系。
  3. 水域:在地圖上清晰標注河流的走向、河道寬度以及與其他水體的連接關系
  4. 水質:如酸堿度、溶解氧、污染物含量等指標,并以不同顏色或圖表形式在大屏上直觀展示,以保障農業用水安全。
  5. 村居建筑:展示村莊的分布位置和范圍,以建筑模型或圖標形式呈現村居的布局。

2.2 農業生產

  1. 農田:以高精度地圖展示農田地塊的邊界和面積,對不同的農田進行編號和分類管理,例如按照種植作物類型、當前使用狀態等進行劃分
  2. 魚塘:標注魚塘的位置和范圍,顯示魚塘的面積和水深等基本信息。展示魚塘的養殖情況,包括養殖的魚類品種、生長階段、投喂記錄等,方便養殖戶進行科學管理和養殖計劃制定。
  3. 作物識別:利用圖像識別技術,通過攝像頭或衛星影像對農田中的作物進行實時識別和分類。在大屏上以不同顏色或圖標標注出不同作物的種植區域,方便用戶快速了解農田的作物布局
  4. 災害預測:通過監測田間的病蟲害發生情況、氣象條件、作物生長狀況等因素,運用病蟲害預測模型,預測病蟲害的發生趨勢和流行范圍。

2.3 安全監管

  1. 無人機巡查:在地圖上展示無人機的巡查路線和實時位置,用戶可以直觀地看到無人機的飛行軌跡。
  2. 入侵告警:在地圖上劃定重點安全區域,如農田保護區、魚塘養殖區、倉庫等,當有人員或車輛未經授權進入這些區域時,系統自動觸發入侵告警。
  3. 重點位置POI:在地圖上標注所有攝像頭的位置,形成 POI(Point of Interest)圖層。用戶可以點擊每個攝像頭圖標,查看該攝像頭的實時監控畫面和相關信息,如攝像頭編號、安裝位置、監控范圍等。

2.4 經濟效益

  1. 區塊產量預測:對比不同年份或不同種植季節的產量預測數據,分析產量變化趨勢和影響因素,為農業生產規劃和資源配置提供決策依據
  2. 投入產出比分析:詳細展示農業生產過程中的各項投入成本,包括土地租賃費用、農資采購成本、人工成本、水電費、運輸費用等,并以圖表形式呈現各項成本在總成本中的占比情況,幫助用戶清晰了解成本結構。

3. 技術分析

經過上面的業務需求分析,我們就可以開始將它們轉為技術上的需求模塊進行逐個實現,其中部分圖層可視化效果,使用高德平臺提供的可視化類Loca可以滿足了,其他部分圖層則需要自行開發,這里我將自己平時積累的可視化圖層整理為的gl-layers圖層庫,核心代碼是基于three JS和高德自定義圖層類CustomLayer、GLCustomLayer進行開發。

3.1 技術棧說明

工具名稱版本用途
高德地圖 JSAPI2.0為GIS平臺提供基礎底圖和服務
three.js0.157主流webGL引擎之一,負責實現展示層面的功能
QGIS3.32.3GIS數據處理工具,用于處理本文的矢量化數據
cesiumlab3.1.11三維數據處理工具集,用于將模型轉換為互聯網可用的3DTiles
blender3.6模型處理工具,用于對BIM模型進行最簡單的預處理
CityEngine2023.0arcGIS團隊開發的程序化 3D 城市生成器 ,支持通過腳本將GIS轉換為3D模型
vue3.2.25實現可視化大屏UI的語言框架,特點是數據雙向綁定
vite2.9.15便捷的前端工程構建工具
AI Earth
達摩學院提供的AIE-SEM影像識別、分割、提取服務,可以幫忙我們從遙感影像圖片中提取GIS數據

3.2 圖層說明

專題內容GIS數據類型表現形式代碼層
基礎配套衛星影像底圖圖片瓦片地圖AMap.TileLayer
基礎配套村居建筑polygon三維建筑模型GlLayer.TilesLayer
基礎配套綠化區域point實例模型GlLayer.TilesLayer
基礎配套水域polygon水面多邊形GlLayer.WaterLayer
農業生產農田地塊polygon帶紋理多邊形,可區分當前使用狀態GlLayer.PolygonLayer
農業生產魚塘地塊polygon帶紋理多邊形,可區分當前水體狀態GlLayer.PolygonLayer
農業生產農作物識別結果point作物類型點圖標AMap.MassMarker
農業生產農田災害風險AI預測圖point熱力圖Loca.HeatMapLayer
安全監管區域邊界polyline三維發光墻面體,如果有監控目標進入區域內則會出現告警GlLayer.BorderLayer
安全監管無人機導航polyline無人機模型在空中飛行移動GlLayer.DrivingLayer
安全監管巡查路線polyline無人機移動軌跡GlLayer.FlowlineLayer
安全監管示范區服務點point帶名稱點標記,點擊可切換到專屬視角Loca.LabelsLayer
經濟效益產量AI預測圖層point網格蜂窩柱狀圖,產量越大柱狀越紅且越高Loca.HexagonLayer

4. 實現步驟

4.1 主體框架開發

  1. 使用vite創建工程,安裝前文技術棧提及的各種依賴包
  2. 在入口模塊編寫主體邏輯,引入主要模塊、聲明變量

    html

    <script setup> import { getMap, initMap } from '@/utils/mainMap2.js' import GLlayer from '#/gl-layers/src/index' import * as THREE from 'three' import * as dat from 'dat.gui' //... // 高德可視化類 let loca // 容器 const container = ref(null) // 圖層管理 const layerManger = new LayerManager() // 信息提示浮層 let normalMarker //... onMounted(async () => {  // 初始化地圖  await init()  // 初始化各種圖層  await initLayers()  // 逐幀函數,用于更新模型動畫等內容  animateFn() }) </script> <template>  <div ref="container" class="container"></div>  <div class="tool">    <div class="btn" @click="gotoCenter()">回到中心</div>    <div class="btn" @click="toggleCross()">越界告警</div>    <div class="btn" @click="toggleDronView()">無人機巡航</div>  </div> </template>

  3. 初始化基礎地圖,并添加衛星影像圖

    jsx

    async function init() { // 將高德地圖Map實例化做了一次封裝  const map = await initMap({    viewMode: '3D',    dom: container.value,    showBuildingBlock: false,    center: SETTING.center,    zoom: 15.5,    pitch: 42.0,    rotation: 4.9,    mapStyle: 'amap://styles/light',    skyColor: '#c8edff'  })  // 添加衛星地圖  const satelliteLayer = new AMap.TileLayer.Satellite();  map.add([satelliteLayer]);    // 監聽地圖縮放和點擊,用于開發調試  map.on('zoomend', (e) => {    console.log(map.getZoom())  })  map.on('click', (e) => {    const { lng, lat } = e.lnglat    console.log([lng, lat])  })  // 高德可視化類  loca = new Loca.Container({    map,  });  // 鼠標懸浮于圖層元素上時,出現信息浮層  normalMarker = new AMap.Marker({    offset: [70, -15],    zooms: [1, 22]  }); }

4.2 村居/綠化圖層

村居是指農業示范區內的建筑面生成模型,綠化圖層則是綠樹等植物的覆蓋區域,原本應該是兩個圖層,因為在本場景中僅僅作為地圖三維底座,均無交互性,我就直接把它們合并為一個3Dtiles以提升性能了。

4.2.1 制作村居數據

  1. 村居數據的建筑面獲取方法有兩種,我們可以通過一些GIS數據工具下載指定區域內建筑面數據,也可以通過AI Earth進行衛星影像圖建筑物提取,最終生成geoJSON文件,導入QGIS進行數據清洗和加工。
  2. 如果建筑面沒有高度數據,我們根據目標場景的實際情況,可以在QGIS中生成一定范圍內的隨機值

4.2.2 制作綠化區域數據

  1. 使用QGIS新建多邊形面圖層,在目標場景區域內將綠化區域圈選出來。在過程中可能會涉及到帶孔多邊形的制作,我們可以利用矢量多邊形的布爾運算獲得。
  2. 在QGIS工具箱找到“矢量創建-多邊形內部的隨機點”即可生成隨機點功能,即可在綠化區域生成均勻分布的隨機點,后續每個點我們都可以種上一棵樹。

4.2.3 轉換為3D瓦片

  1. 新建cityEngine工程,并將制作好的村居和綠化數據另存為SHP格式,置入到工程中
  2. 將目標場景的矩形范圍也導出一張TIF格式的圖片,置入到工程中,作為本工程場景的底圖
  3. 將村居數據Polygons拖入場景編輯面板中,選中元素對象并配置規則文件,我們就可以快速生成建筑模型,并通過配置將建筑高度與建筑面高度數據關聯上,選擇合適的房屋造型和風格。

  1. 同理將綠化區域數據Points拖拽入場景編輯面板,并配置植物生成規則文件,我們就可以快速得到效果非常不錯的植物綠化區域

  1. 選中兩個圖層的模型并導出為FBX,注意配置面板中的設置,中心一項關系到所有模型在地圖上的位置是否正確,需要格外關注

  2. 開啟cesiumlab,進入通用模型切片,直接轉換為3Dtiles,可以在ceisumlab的預覽頁面中看到建筑和植物都落在地球的地面上,可能原點的地理位置是錯誤的。這個不用擔心,我們在將其接入高德地圖時做再做調整。更細節的步驟可以看我之前寫的低成本創建數字孿生場景

4.2.4 在高德地圖呈現

  1. 部署3dtiles靜態服務,在高德地圖中需要重新定義3dtiles的原點坐標,因此需要創建一個tileset.json入口文件副本,并將其初始轉置矩陣歸零

  1. 編寫代碼,這里使用之前開發的TilesLayer圖層做加載,關于如何在高德地圖中實現3dtiles,想了解具體實現可以看看這里

    csharp

    async function initBuildingLayer() {  const map = getMap()  const layer = new TilesLayer({    id: 'buildingLayer',    title: '村居建筑圖層',    alone: SETTING.alone,    map,    center: [113.531905, 22.737473], // 圖層中心點    zooms: [4, 30],    interact: false,    tilesURL: 'http://localhost:9003/model/twQ1mVSwQ/tileset.0.json', // 村居模型    needShadow: true  })  layerManger.add(layer) }

  1. 為保證視覺效果,加載完成后還對模型打光調亮、添加陰影,關于如何在地圖的平面上添加陰影,需要開個單獨的小節在后文詳敘。

    jsx

    layer.on('complete', ({ scene, renderer }) => {    // 調整模型的亮度    const aLight = new THREE.AmbientLight(0xffffff, 0.5)    scene.add(aLight)    //...    // 平行光,增加投影    var dLight = new THREE.DirectionalLight(0xffffff, intetity);    dLight.position.set(lightPositionX, lightPositionY, lightPositionZ);    dLight.castShadow = true; // 開啟陰影投射    dLight.shadow.mapSize.width = mapSize; // 增加陰影分辨率    dLight.shadow.mapSize.height = mapSize;    dLight.shadow.camera.near = cameraNear;    dLight.shadow.camera.far = caremaFar;    dLight.shadow.camera.left = cameraLeft;    dLight.shadow.camera.right = cameraRight;    dLight.shadow.camera.top = cameraTop;    dLight.shadow.camera.bottom = cameraBottom;    dLight.shadow.bias = -0.0001; // 負值將陰影稍微向外偏移    scene.add(dLight);    directionalLight = dLight    // 平面陰影    const geometry1 = new THREE.PlaneGeometry(5000, 5000);    const material1 = new THREE.ShadowMaterial({ opacity: 1.0 })    const plane = new THREE.Mesh(geometry1, material1);    plane.position.z = 0;    plane.receiveShadow = true;    scene.add(plane);  })?
  2. 最終的效果如下

4.3 水域圖層

  1. 我們同樣可以使用QGIS自行繪制、或者使用GIS工具獲取水域范圍數據

  1. 水面的實現方式是在指定的多邊形平面上添加水紋材質,這里使用到了ShaderMaterial編寫自定義著色器材質,我們封裝為WaterLayer圖層,詳細步驟可以看這里

    jsx

    async function initWaterLayer() {  const map = getMap()  const data = await fetchMockData('water.geojson')  const layer = new GLlayers.WaterLayer({    id: 'waterLayer',    map,    data, // 水域GIS數據    alone: SETTING.alone,    zooms: [16, 22],    animate: true,    waterColor: '#CFEACD', // 水體顏色    altitude: -5 // 水面Mesh高度  })  layerManger.add(layer) }

  1. 最終效果如下,動靜結合這樣一來村居看起來更靈動了

4.4 農田地塊

  1. 農田和魚塘地塊具有共同的特性,實現方法類似可以合起來講,在QGIS上我們就可以通過屬性表對polygone按屬性做分類

  1. 獲取數據,實例化Polylone,其實這種常規的Polygon,高德地圖Loca也有提供,之所以用自己開發的polygon是想給Polygon添加圖片紋理,比如正在使用的地塊使用水稻田紋理 ,而養護中的地塊則使用土地紋理,簡單一點就是用顏色做區分。

    jsx

    async function initFarmLayer() {  const map = getMap()  const data = await fetchMockData('farm.geojson')  console.log(data)  data.features.forEach(item => {    const { used } = item.properties    // 根據地塊不同的使用狀態,賦予不同的顏色    item.properties.color = used == 1 ? "#33a02c" : (used == 0 ? "#b2df8a" : "#ceb89e")  })  const layer = new GlLayer.PolygonLayer({    id: 'farmLayer',    alone: SETTING.alone,    map,    data,    lineWidth: 0,    opacity: 0.4,    interact: true, //可鼠標互動    zIndex: 100,    altitude: 2  })  // 放入圖層管理器  layerManger.add(layer) }

  1. 單個PolygonLayer生成Mesh的核心代碼如下,將空間坐標數組轉為Mesh的頂點三角面,并賦予材質,更詳細的的實現步驟可以看看之前分享的在高德地圖上實現Polylone圖層

    jsx

    /** * 繪制多邊形 * @private * @param {Array} path 路徑 * @param {Object} properties 屬性 */ drawPolygon ({ path, properties }) {  const { altitude, opacity } = this._conf  // 將路徑數據扁平化  const flatArr = path.map(v => {    return [v[0], v[1], altitude]  }).flat()  // 三角剖分  const triangles = Earcut.triangulate(flatArr, null, 3)  // 創建一個THREE.Geometry對象  const geometry = new THREE.BufferGeometry()  // 將三角形的頂點添加到geometry對象  let faceList = []  for (let i = 0; i < triangles.length; i++) {    const [x, y, z] = path[triangles[i]]    faceList = [...faceList, x, y, altitude]  }  // 頂點三角面  geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(faceList), 3))  // 計算法線和頂點的面連接關系  geometry.computeVertexNormals()  // 創建材質  const material = new THREE.MeshBasicMaterial({    color: properties.color || '#0674F1',    transparent: true,    opacity: properties.opacity || opacity  })  // 創建多邊形的網格對象  const polygon = new THREE.Mesh(geometry, material)  // 將多邊形網格對象添加到場景中  const _scene = this.group || this.scene  _scene.add(polygon) }

  1. 最終效果如下

4.5 作物識別圖層

  1. 作物識別圖層的作用是展示AI遙感識別技術對農田作物的識別結果,以及展示AI技術對魚塘產量做出的預測數據,用AMap.MassMarker就可以滿足了
  2. 需要注意的是點標記的坐標位置是如何生成的,總不可能手動創建效率太低了,我們可以使用QGIS自帶的矢量數據處理功能自動創建質心,直接為每個polygon生成中心坐標點。右鍵圖層打開屬性表添加識別結果,導出geojson格式備用。

  1. 在高德地圖中添加圖層實現,為保證與其他圖層的接口統一,我對MassMark和MassMakers進行了封裝,統一基礎屬性、初始化配置參數和顯示隱藏方法。

    jsx

    import BaseUtils from './BaseUtils'; class CropLayer extends BaseUtils {    data = [];    markers = [];    id = null    layer = null    iconMap = {        '香蕉': { icon: 'xiangjiao.png', style: 0},        '火龍果': { icon: 'huolongguo.png', style: 1},        // ...    }    constructor(config) {        super(config);        this.getData(config.data);        this.map = config.map;        this.zooms = config.zooms ?? [10, 22];        this._zIndex = config.zIndex        this.id = config.id        this.init();    }    /**     * 處理具體的圖層顯示邏輯     * @param val     */    _handleVisible(val) {        const {layer} = this;        const fn = val ? 'show' : 'hide';        if(layer){                    layer[fn]()        }    }    // 整理數據    getData(geoJSON) {        const arr  = []        const {iconMap} = this        geoJSON.features.forEach(item=>{            const {geometry, properties} = item            const {crop} = properties            const match = iconMap[crop]            const [lng, lat] =  geometry.coordinates            if(match){                            arr.push({                    lnglat:  [lng, lat, 50],                    crop,                    style: match.style                })            }        })        this.data = arr    }    async init() {        const {data, map, iconMap, zooms, _zIndex} = this;        const style = Object.keys(this.iconMap).map(key=>{            const {icon, style} = iconMap[key]            return {                url: `./static/icons/${icon}`,                size: new AMap.Size(30,30),                name: key            }        })        const layer = new AMap.MassMarks(data, {            opacity: 1,            zIndex: _zIndex,            cursor: 'pointer',            style,            zooms        });        layer.setMap(map)        layer.on('mouseover',  (e) => {            this.dispatchEvent('mouseover', e)        });        this.layer = layer        this.visible = true;    }    //... } export default CropLayer;

  1. 這樣一來就可以輕松調用了,直接將農田和魚塘數據合并使用一個圖層展示

    jsx

    async function initCropLayer() {  const map = getMap()  const data1 = await fetchMockData('crop.geojson')  const data2 = await fetchMockData('poolCenter.geojson')  data1.features = data1.features.concat(data2.features)  const layer = new CropLayer({    id: 'cropLayer',    data: data1,    zooms: [16, 22],    zIndex: 200,    map  })  // 鼠標懸浮時彈出信息浮層  layer.on('mouseover', (e) => {    const { crop, style } = e.data    normalMarker.setPosition(e.data.lnglat);    normalMarker.setOffset(new AMap.Pixel(90, -10))    let content = ''    if (style <= 4) {      //農作物      content = `<div class="amap-info-window">        <p>作物: ${crop}</p>        <p>識別匹配度: ${parseInt(Math.random() * 20) + 80}%</p>        <p>產量預計: ${parseInt(Math.random() * 30) + 20}噸</p>      </div>`    } else {      //水產品      content = `<div class="amap-info-window">        <p>作物: ${crop}</p>        <p>產量預計: ${parseInt(Math.random() * 20) + 10}噸</p>      </div>`    }    normalMarker.setContent(content)    normalMarker.setMap(map)  })  layer.on('mouseout', (e) => {    map.remove(normalMarker);  })  // 放入圖層管理器  layerManger.add(layer) }

  2. 最終效果如下

4.6 區域邊界

  1. 區域邊界的數據繪制很簡單,就是一個常規的封閉線圖形polyline。

  1. 我使用之前開發的GlLayer.BorderLayer進行實例化渲染,方便定制各種動畫。

    jsx

    async function initBorderLayer() {  const map = getMap()  const data = await fetchMockData('border.geojson')  const layer = new GlLayer.BorderLayer({    id: 'borderLayer',    alone: SETTING.alone,    map,    wallColor: '#3dfcfc', // 墻體顏色    wallHeight: 100, // 墻體高度    data,    speed: 0.3,    animate: true,    zooms: [11, 22],    altitude: 0  })  layerManger.add(layer) }

  1. 區域入侵監控這部分操作正常來說是由物聯網設備檢測到,推送消息給服務端,再由服務端推送給前端一條消息。為方便演示我直接在前端模擬了,定時檢測指定目標位置,如果在polygon內部,則區域邊界圖層出現告警狀態,整體變為紅色;目標離開,則解除告警狀態。為此新增了setColor方法用于切換顏色狀態。

    jsx

    /** * 設置區域邊界顏色 * @param {String} newColor 顏色值,比如'#ffffff' */ setColor(newColor){  // 創建新紋理  const newTexture = this.generateTexture (128, newColor)  newTexture.wrapS = THREE.RepeatWrapping // 水平重復平鋪  newTexture.wrapT = THREE.RepeatWrapping // 垂直重復平鋪  this._color = newColor  this._texture_offset = 0  this.mainMesh.material.color = newColor  this.animateMesh.material.map = newTexture  this._texture = newTexture } // 創建材質 generateTexture (size = 64, color = '#ff0000') {  const canvas = document.createElement('canvas')  canvas.width = size  canvas.height = size  const ctx = canvas.getContext('2d')  const linearGradient = ctx.createLinearGradient(0, 0, 0, size)  linearGradient.addColorStop(0.2, hexToRgba(color, 0.0))  linearGradient.addColorStop(0.8, hexToRgba(color, 0.5))  linearGradient.addColorStop(1.0, hexToRgba(color, 1.0))  ctx.fillStyle = linearGradient  ctx.fillRect(0, 0, size, size)  const texture = new THREE.Texture(canvas)  texture.needsUpdate = true // 必須  return texture }
  2. 模擬邊界入侵檢測,我們可以使用AMap.GeometryUtils提供的幾何計算方法,判斷點是否在多邊形內,是的話則改變邊界狀態為告警,否則移除告警。

    jsx

    // 是否進入入侵檢測模式 let isInvadeMode = false // 定時器 let invadeClock = null // 入侵者標記 let invadeMarker /** * 切換入侵檢測模式 */ async function toggleInvade() {  const map = getMap()  const borderLayer = layerManger.findLayerById('borderLayer')    isInvadeMode = !isInvadeMode  // 入侵檢測范圍  let ring = []  // 入侵者路徑  let invadePath  // 當前步數  let invadeStep = 0    if (isInvadeMode) {    const borderPath =  await fetchMockData('border.geojson')    ring = borderPath.features[0].geometry.coordinates[0]    initInvade()    invadeClock = setInterval(() => {      // 更新目標位置      const pos = invadePath[invadeStep]      invadeStep = (invadeStep + 1) % invadePath.length      invadeMarker.setPosition(pos)            // 判斷為入侵,邊界墻修改顏色      const color = isInRing(pos, ring) ? '#ff0000' : '#3dfcfc'      if(borderLayer._color !== color){        borderLayer.setColor(color)      }    }, 1000)  } else {    clearInvade()    borderLayer.setColor('#3dfcfc')  }  // 創建  async function initInvade(){    // 路徑    const {features} = await fetchMockData('invade-path.geojson')    invadePath = features[0].geometry.coordinates[0]    // 目標    invadeMarker = new AMap.Marker({      content: `<img style="width:30px;" src="./static/icons/ico-invade.png">`,      anchor: 'bottom-center',      offset: new AMap.Pixel(-15, -20)    })    map.add(invadeMarker)    }  // 銷毀  function clearInvade(){    clearInterval(invadeClock)    invadeClock = null        map.remove(invadeMarker)    invadeMarker = null    }  // 檢測是否在范圍內  function isInRing (pos, ring){    const res = AMap.GeometryUtil.isPointInRing(pos, ring)    console.log('is in ring ', res)    return res  } }
  3. 最終效果如下

4.7 無人機巡查功能

最近“低空經濟”這個概念很火,說的是是以各種有人駕駛和無人駕駛航空器的各類低空飛行活動為牽引,輻射帶動相關領域融合發展的綜合性經濟形態,既然如此怎么能少得了無人機的出場。在本文中我們實現的是單架無人機模型沿著指定的閉合軌跡飛行移動,并且可以用無人機的第三人稱視角俯瞰地圖。

  1. 關于自動巡航的功能在之前做無人車巡航的時候已經實現過了,這里再講解一下核心代碼,其實就是在Tween更新函數中,按照既定的路徑軌跡不斷調整NPC的位置和朝向,如果需要第三人稱視角,則同步更新相機的朝向即可,更詳細的步驟可以看在高德地圖實現自動巡航
  2. jsx
    代碼解讀
    復制代碼
    // 創建移動目標NPC 和 移動控制器 // NPC 是外部加載的gltf模型 onReady () {  if (this._conf.NPC) {    this.initNPC()  }  this.initController() } /** * 初始化主體NPC的狀態 * @private */ initNPC () {  const { _PATH_COORDS, scene } = this  const { NPC } = this._conf  // z軸朝上  NPC.up.set(0, 0, 1)  // 初始位置和朝向  if (_PATH_COORDS.length > 1) {    NPC.position.copy(_PATH_COORDS[0])    NPC.lookAt(_PATH_COORDS[1])  }  // 添加到場景中  scene.add(NPC) } /** * 創建移動控制器 * @private */ initController () {  // 狀態記錄器  const target = { t: 0 }  // 獲取第一段線段的移動時長  const duration = this.getMoveDuration()  // 路線數據  const { _PATH_COORDS, _PATH_LNG_LAT, map } = this  this._rayController = new TWEEN.Tween(target)    .to({ t: 1 }, duration)    .easing(TWEEN.Easing.Linear.None)    .onUpdate(() => {      const { NPC, cameraFollow } = this._conf      // 終點坐標索引      const nextIndex = this.getNextStepIndex()      // 獲取當前位置在路徑上的位置      const point = new THREE.Vector3().copy(_PATH_COORDS[this.npc_step])      // 計算下一個路徑點的位置      const nextPoint = new THREE.Vector3().copy(_PATH_COORDS[nextIndex])      // 計算物體應該移動到的位置,并移動物體      const position = new THREE.Vector3().copy(point).lerp(nextPoint, target.t)      if (NPC) {        // 更新NPC的位置        NPC.position.copy(position)      }      // 需要鏡頭跟隨      if (cameraFollow) {        // 計算兩個lngLat端點的中間值        const pointLngLat = new THREE.Vector3().copy(_PATH_LNG_LAT[this.npc_step])        const nextPointLngLat = new THREE.Vector3().copy(_PATH_LNG_LAT[nextIndex])        const positionLngLat = new THREE.Vector3().copy(pointLngLat).lerp(nextPointLngLat, target.t)        // 更新地圖鏡頭位置        this.updateMapCenter(positionLngLat)      }      // 更新地圖朝向      if (cameraFollow) {        const angle = this.getAngle(position, _PATH_COORDS[(this.npc_step + 3) % _PATH_COORDS.length])        this.updateMapRotation(angle)      }    })    .onStart(() => {      const { NPC } = this._conf      // 計算線段重點的位置和角度      const nextPoint = _PATH_COORDS[(this.npc_step + 3) % _PATH_COORDS.length]      // 更新主體的正面朝向      if (NPC) {        NPC.lookAt(nextPoint)        NPC.up.set(0, 0, 1)      }    })    .onComplete(() => {      // 更新到下一段路線      this.npc_step = this.getNextStepIndex()      // 調整時長      const duration = this.getMoveDuration()      // 重新出發      target.t = 0      this._rayController        .stop()        .to({ t: 1 }, duration)        .start()    })    .start() }
  3. 實例化GlLayer.DrivinLayer圖層,我們將無人機巡航和飛行軌跡拆分為兩個圖層實現

    jsx

    async function initDroneLayer() {  const map = getMap()  const data = await fetchMockData('dronWander2.geojson')  const NPC = await getDroneModel()  // 巡航圖層  const layer = new DrivingLayer({    id: 'dronLayer',    map,    zooms: [4, 30],    path: data,    altitude: 50,    speed: 50.0,    NPC,    interact: true  })  layer.on('complete', ({ scene }) => {    // 調整模型的亮度    const aLight = new THREE.AmbientLight(0xffffff, 3.5)    scene.add(aLight)    layer.resume()  })  layerManger.add(layer)  // 路徑軌跡動畫圖層  const dronPathLayer = new FlowlineLayer({    id: 'dronPathLayer',    map,    zooms: [16, 22],    data,    speed: 0.5,    lineWidth: 10,    altitude: 50  })  layerManger.add(dronPathLayer) }
  4. 本實例最大的難度在于如何讓無人機在飛行的時候4個螺旋槳旋轉擺動,這里最后選擇了在逐幀函數更新gltf自帶動畫的方法;關于gltf動畫如何制作,在后面有單獨章節。

    jsx

    // 加載無人機 function getDroneModel() {  return new Promise((resolve) => {    const loader = new GLTFLoader()    loader.load('./static/model/drone/drone1.glb', (gltf) => {      // 調整模型尺寸      const model = gltf.scene.children[0]      const size = 10.0      model.scale.set(size, size, size)      // 播放動畫      mixer = new THREE.AnimationMixer(gltf.scene);      const action = mixer.clipAction(gltf.animations[0])      // 動畫播放速度      action.setEffectiveTimeScale(guiCtrl.mixerPlaySpeed);      action.play();      resolve(model)    })  }) } // 播放無人機動畫 function animateFn() { requestAnimationFrame(animateFn); if (mixer) {  // 更新無人機旋轉動畫  mixer.update(0.01); //必須加上參數才有動畫     } }
  5. 最終實現效果如下,第三人稱游戲的代入感出來了有沒有。

4.8 災害預測圖層

  1. 該圖層本質上是個3D熱力圖,源數據是帶有權重屬性的坐標點集合,我們可以在QGIS上編輯它們甚至可以查看二維效果
  2. 導出數據,使用高德自帶的可視化圖層Loca.Heatmap實現

    jsx

    /** * 災害風險檢測圖層 */ async function initRiskLayer() {  const map = getMap()  const data = await fetchMockData('fertility.geojson')  const geo = new Loca.GeoJSONSource({ data })  const heatmap = new Loca.HeatMapLayer({    zIndex: 10,    opacity: 1,    visible: false,    zooms: [2, 22],  });  heatmap.setSource(geo, {    id: 'riskLayer',    radius: 150,    unit: 'meter',    height: 300,    gradient: {      1: '#FF4C2F',      0.8: '#FAA53F',      0.6: '#FFF100',      0.5: '#7DF675',      0.4: '#5CE182',      0.2: '#29CF6F',    },    value: function (index, feature) {      return feature.properties.weight ?? 0;    },    min: 0,    max: 100,    visible: true  });  loca.add(heatmap);  map.on('click', function (e) {    const feat = heatmap.queryFeature(e.pixel.toArray());    // 展示更多信息...  });  heatmap.id = 'riskLayer'  layerManger.add(heatmap) }
  3. 在切換圖層為顯示狀態時,可以加上動畫以達到更好的視覺效果

    jsx

    // 給圖層的顯示增加動畫效果 function animateLayer(layer){  switch(layer.id){    case 'riskLayer':      layer.addAnimate({        key: 'height',        value: [0, 1],        duration: 2000,        easing: 'BackOut',      });      layer.addAnimate({        key: 'radius',        value: [0, 1],        duration: 2000,        easing: 'BackOut',        transform: 1000,        random: true,        delay: 5000,      });        break;          //... }
  4. 最終效果如下,產量AI預測圖層的實現方法類似就不贅述

4.9 使用圖層管理器操作圖層

本示例涉及到圖層數量已經有十幾個,為方便進行圖層的統一操作(比如在專題A哪些圖層需要顯示,其他圖層隱藏;或者調用圖層的某個功能),我們需要圖層管理器layerManager,且給圖層賦予唯一的id值便于在管理器中獲取。

如下面代碼所示,提供最基礎的添加、查找、清除功能

jsx

/** * 圖層管理器 * @extends null * @author Zhanglinhai <gyrate.sky@qq.com> */ class Manager {  /**   * @description 創建一個實例   * @param {Object} conf   * @param {Array} conf.data 圖層數組 [layer,...] 默認為[]   */  constructor (config = {}) {    this._list = config.data || []  }  /**   * @description 添加1個圖層到管理器   * @param {String} id 圖層id   * @param {String} title 圖層名稱   * @param {*} layer 圖層實例   */  add (layer) {    if (layer === undefined) {      console.error('缺少圖層實例')      return    }    if (layer.id === undefined) {      console.error('缺少圖層id')      return    }    const { id } = layer    const match = this.findLayerById(id)    if (match) {      console.error(`圖層的id ${id} 不是唯一標識,請更換`)      return    }    this._list.push(layer)  }  /**   * @description 通過id查找圖層信息   * @param {String} id 圖層id   * @returns {*} 返回匹配的第一個圖層   */  findLayerById (id) {    const match = this._list.find(item => item.id === id)    return match  }  /**   * @description 清空當前的圖層管理器   */  clear () {    this._list.forEach((layer) => {      if (layer.destroy) {        layer.destroy()      }      console.log(`銷毀layer ${layer.id}`)    })    this._list = []  } }

這樣一來就方便我們快捷操作圖層,將整個地圖作為可視化大屏的主體,放置到帶有導航和圖表的低代碼大屏框架中,就完成了初步的搭建工作。

5. 其他問題解決方案

5.1 如何在場景中產生投影

如何在高德地圖的底圖上添加模型的投影,我被困擾了一段時間,后來請教了高德的技術大佬WT才得到啟發解開了這個問題,感謝wt大佬的支持。three.js提供了一種陰影材(ShadowMaterial)此材質可以接收陰影,但在其他方面完全透明。

要想在場景中獲得投影,需要下面幾個步驟都齊全

  1. 渲染器打開投影
jsx
代碼解讀
復制代碼
// 禁用自動清理,以保持地圖底圖可見 renderer.autoClear = false; renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 重要:會影響到畫布尺寸 renderer.setSize(window.innerWidth, window.innerHeight);
  1. 創建合適的平行光源,有各種參數需要設置

jsx

// 創建平行光 var dLight = new THREE.DirectionalLight(0xffffff, 3); dLight.position.set(lightPositionX, lightPositionY, lightPositionZ); dLight.castShadow = true; // 開啟陰影投射 dLight.shadow.mapSize.width = mapSize; // 增加陰影分辨率 dLight.shadow.mapSize.height = mapSize; dLight.shadow.camera.near = cameraNear; dLight.shadow.camera.far = caremaFar; dLight.shadow.camera.left = cameraLeft; dLight.shadow.camera.right = cameraRight; dLight.shadow.camera.top = cameraTop; dLight.shadow.camera.bottom = cameraBottom; scene.add(dLight);
  1. 各種關聯物體也必須將屬性castShadow 、receiveShadow設置為true

jsx

// 創建幾何體 var geo = new THREE.BoxGeometry(1000, 1000, 1000); for (let i = 0; i < data.length; i++) {  const d = data[i];  var mesh = new THREE.Mesh(geo, mat);  mesh.position.set(d[0], d[1], 500);  mesh.castShadow = true; // 啟用陰影投射!  mesh.receiveShadow = true; // 接收陰影!  //... }
  1. 給底部平面賦予shadowMaterial材質

    jsx
    ?
    // 創建接收陰影的平面 var planeGeo = new THREE.PlaneGeometry(50000, 50000); var shadowMat = new THREE.ShadowMaterial({  opacity: planeMaterialOpacity, }); plane = new THREE.Mesh(planeGeo, shadowMat); plane.receiveShadow = true; // 接收陰影! scene.add(plane);
  2. 最終效果如下,演示代碼鏈接放到這里了

5.2 給模型制作常規動畫

  1. 下載一個無人機模型FBX格式,推薦在sketchfab上找,素材齊全。打開blender,導入FBX模型,把所有部件歸屬到一個根節點,后續控制根節點其他部件也跟著移動
  2. 在動畫時間軸給每個部件加上動畫關鍵幀,調試好動畫

  3. 補間動畫默認是緩入緩出的,可以同個左上角切換面板到曲線編輯器修改補間動畫線

  4. 最關鍵的一步。導出gltf時動畫一項必須勾選,且動畫模式設置為“合并的活動動作”,這樣的話,導出的gltf就能把所有部件動作合并為一個動作了。

  5. 最終預覽效果,螺旋槳的旋轉動畫不需要做太快,因為在web端實際播放時,速度倍率是可以通過action.setEffectiveTimeScal()調節的,要多快有多塊。

5.3  圖層的深度關系

如何處理高德自有圖層和自定義圖層的深度關系,這里必須了解高德提供的CustomLayer和GLCustomLayer的區別。

前者是在地圖實例畫布Canvas1之外另外覆蓋了一個Canvas標簽,因此所有內容都會置于Canvas1內容之上,無論空間上是否合理;而后者則是與地圖實例共享畫布的,在GLCustomLayer上創建的內容能夠與地圖上的元素、高德可視化類創建的元素共享深度關系,因此使用GLCustomLayer會讓多圖層的場景視覺上更加和諧,但代價就是Map需要逐幀重繪,性能損耗更高。所以如何取舍還是要看具體的業務場景進行選擇。

總結

至此,使用高德地圖制作數字農業可視化大屏的分享就告一段落了。事實上這并不是一個最終成本,因為我還有很多想法沒有落實, 比如精細化農業大棚的搭建,無人機實時視頻投影、火災預測等等功能展示;還有一些技術問題沒有解決,比如cesiumlab使用FBX生成的3dtiles沒有支持LOD,即不同地圖縮放層級下的精細度,這在性能和視覺效果上肯定是存在優化空間的,據我所見在cityEngine階段LOD信息還是存在的,至于具體在哪個過程中丟失了,還需要排查一下。

但戰線拉太長的話項目可能就會永遠沒有階段成果,時間關系就先發布這么多了了。說不定分享出來之后,可以起到拋磚引玉的作用,最好能撈到更多志同道合的伙伴來一起共建虛擬農場元宇宙。

本示例使用到的高德JSAPI

3D自定義圖層AMap.GLCustomLayer

自定義圖層AMap.CustomLayer

AMap.Map地圖對象類

海量點類AMap.MassMarkers

LOCA 數據可視化 API 2.0

空間數據計算的函數庫 GeometryUtil

相關鏈接

數字孿生×低空經濟 | 天空地一體化 城市數字孿生電子沙盤指揮系統

在cityEngine編寫模型生成規則

THREEJS 陰影材質的使用文檔

源代碼Github地址

演示頁面地址


作者:Gyrate
鏈接:https://juejin.cn/post/7432127587919298600
來源:稀土掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

該文章在 2025/4/12 18:27:20 編輯過
關鍵字查詢
相關文章
正在查詢...
點晴ERP是一款針對中小制造業的專業生產管理軟件系統,系統成熟度和易用性得到了國內大量中小企業的青睞。
點晴PMS碼頭管理系統主要針對港口碼頭集裝箱與散貨日常運作、調度、堆場、車隊、財務費用、相關報表等業務管理,結合碼頭的業務特點,圍繞調度、堆場作業而開發的。集技術的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業的高效ERP管理信息系統。
點晴WMS倉儲管理系統提供了貨物產品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質期管理,貨位管理,庫位管理,生產管理,WMS管理系統,標簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務都免費,不限功能、不限時間、不限用戶的免費OA協同辦公管理系統。
Copyright 2010-2025 ClickSun All Rights Reserved

主站蜘蛛池模板: 暖暖日韩欧美视频免费 | 国女精品爽爽一区二区 | 青青手机国产在线视频 | 国产午夜福利精品一区 | 精品日产一区二区三区 | 甜性涩爱 | 欧美精品一区二区三区四 | 国产精品第一页第一页 | 午夜国产福利在线直播 | 亚洲v日韩天堂片 | 看一级特黄a大一片电影 | 日韩午夜伦 | 乱码一二三入区口 | 成+人+黄+色+免费观看 | 国产日韩高 | 福利一区二区三区视频在线观看 | 蜜芽va亚洲 | 水蜜桃www | 国产乱码精品一区二区三区百度 | 国产午夜福利精品一 | 最新版本安卓下载 | 国产福利91精品一区二区三区 | 亚洲欧美日韩综合在线播放 | 含羞草影院在线 | 欧亚天堂在线播放 | 国内永久福利在线视频 | 最新全网影视大全电影电视剧 | 中文字幕一冢本 | 色久悠悠色久在线观看 | 亚洲精品宾馆在线精品酒店 | 国产亚洲精aa在线观看不卡 | 欧美日韩中文字幕在线一区二区 | 国产综合一区二区在线观看 | 999国内精品永久免费视频 | 国产精品天干天干在线综合 | 97精品国产自在现线免费 | 成人福利在线视频免费观看 | 色偷偷人人澡人人添老妇人 | 大香伊蕉在人线国产最新75 | 日本在线观看中文字幕无线观看 | 日本高清一区免费中文视频 |