Skip to content
目录

简单的地形沙盘效果

Markdown 官方教程

WARNING

该方法只适用小范围的地形数据加载和处理,生活常见的情景如售楼处的那个沙盘,如果你要大规模的加载地形数据请使用maptalks自带的地形 功能

加载地形例子

准备数据

地形数据

这里我直接下载了mapbox的地形瓦片服务?之所以这么做:

  • maptalks的地形功能也是加载的瓦片服务
  • maptalks.three插件里也提供了简单的地形瓦片功能用来做简单的沙盘等效果使用
  • 我不想去制作地形数据瓦片
  • 没有在线直接使用mapbox的地形服务,因为怕密钥暴露,被嫖怕了

数据是杭州西湖这里的,数据层级是13层级,大概是30个瓦片这样,这里只是做测试用途,自己业务里需要:

  • 自行下载对应区域的瓦片数据
  • 或者自行生产地形数据瓦片
  • 或者直接使用在线服务

Markdown 官方教程

准备等高线数据

例子里的数据大概是我三年前从地理空间数据云下载的数据,具体时间我也不记得,然后用的QGIS从下载的 tif里提取的等高线数据

怎样从地理空间数据云里下载数据和用QGIS提取等高线请自行网上查阅相关资料

因为当时做示例时是挂在在github上的,这个数据有点大,故做了压缩,github的速度你懂的,所以在示例代码里你会看到数据decode的过程 这里只是想强调数据不是加密的,对数据压缩只是为了加快数据请求速度而已,你提取等高线的数据可以直接用geojson格式,然后直接加载的

js
fetch('./../assets/data/hangzhoudem').then(res => res.text()).then(evadata => {
             //数据解压
                evadata = LZString.decompressFromBase64(evadata);
                evadata = JSON.parse(evadata);
                lines = evadata.map(element => {
                    let coordinates = element.l;
                    if (coordinates) {
                        const elevation = element.h;
                        const height = elevation + 45;
                        coordinates.forEach(c => {
                            c[2] = height;
                        });
                        const line = threeLayer.toLine(new maptalks.LineString(coordinates), { altitude }, getLineMaterila(height));
                        return line;
                    }
                });
                threeLayer.addMesh(lines);
            });

加载地形瓦片数据

js
function addTerrainTile() {
           //瓦片行列号的范围
            const minx = 6826, maxx = 6830, miny = 3372, maxy = 3377;
            const tiles = [];
            for (let col = minx; col <= maxx; col++) {
                for (let row = miny; row <= maxy; row++) {
                    tiles.push([col, row, 13]);
                }
            }
            const TILESIZE = 256;
            tiles.forEach(tile => {
                const [x, y, z] = tile;
                //cal tile bbox
                const bbox = tilebelt.tileToBBOX(tile);
                const texture = `./../assets/data/tile-image/${z}/${x}/${y}.png`;
                // const texture = `https://a.basemaps.cartocdn.com/light_all/${z}/${x}/${y}.png`;
                // const texture = `https://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x=${x}&y=${y}&z=${z}`;
                const image = `./../assets/data/tile-rgb/${z}/${x}/${y}.png`;
                const terrain = threeLayer.toTerrain(bbox, {
                    flaserBoundary: false,
                    bufferPixel: 0.2,
                    image,
                    texture,
                    imageWidth: TILESIZE,
                    imageHeight: TILESIZE,
                    altitude
                }, new THREE.MeshPhongMaterial({ color: terrainColor }));
                terrain.on('textureload', () => {
                    //hide texture
                    terrain._map = terrain.getObject3d().material.map;
                    terrain.getObject3d().material.map = null;
                    terrain.getObject3d().material.needsUpdate = true;
                });
                terrain.on('dataload', () => {
                    //update normal
                    const geometry = terrain.getObject3d().geometry;
                    const index = geometry.index.array;
                    const position = geometry.attributes.position.array;
                    const normal = generateNormal(index, position);
                    geometry.setAttribute('normal', new THREE.BufferAttribute(normal, 3));
                });
                threeLayer.addMesh(terrain);
                terrains.push(terrain);
            });
        }

这里有几个注意点:

  • 使用一个第三库 tilebelt 用来计算每个瓦片的经纬度包围盒

  • 当地形瓦片加载结束后,自动重构了地形图形的normal,默认的地形的geometry是plane,光照质感比较差

  • flaserBoundary 参数表示是否压扁地形裙边

Markdown 官方教程

因为要重构normal,开启后因为法线的原因会导致瓦片直接的缝隙显得特别大

Markdown 官方教程

但是如果不重构normal默认的plane的发现光照不强烈,质感比较差,所以为了normal的重构需要关闭这个功能

  • bufferPixel 参数表示地形的大小buffer几个像素,内部会自动将其转换为gl的长度和宽度的

法向量计算函数

js
function generateNormal(indices, position) {
            function v3Sub(out, v1, v2) {
                out[0] = v1[0] - v2[0];
                out[1] = v1[1] - v2[1];
                out[2] = v1[2] - v2[2];
                return out;
            }

            function v3Normalize(out, v) {
                const x = v[0];
                const y = v[1];
                const z = v[2];
                const d = Math.sqrt(x * x + y * y + z * z) || 1;
                out[0] = x / d;
                out[1] = y / d;
                out[2] = z / d;
                return out;
            }

            function v3Cross(out, v1, v2) {
                const ax = v1[0], ay = v1[1], az = v1[2],
                    bx = v2[0], by = v2[1], bz = v2[2];

                out[0] = ay * bz - az * by;
                out[1] = az * bx - ax * bz;
                out[2] = ax * by - ay * bx;
                return out;
            }

            function v3Set(p, a, b, c) {
                p[0] = a; p[1] = b; p[2] = c;
            }

            const p1 = [];
            const p2 = [];
            const p3 = [];

            const v21 = [];
            const v32 = [];

            const n = [];

            const len = indices.length;
            const normals = new Float32Array(position.length);
            let f = 0;
            while (f < len) {

                // const i1 = indices[f++] * 3;
                // const i2 = indices[f++] * 3;
                // const i3 = indices[f++] * 3;
                // const i1 = indices[f];
                // const i2 = indices[f + 1];
                // const i3 = indices[f + 2];
                const a = indices[f], b = indices[f + 1], c = indices[f + 2];
                const i1 = a * 3, i2 = b * 3, i3 = c * 3;

                v3Set(p1, position[i1], position[i1 + 1], position[i1 + 2]);
                v3Set(p2, position[i2], position[i2 + 1], position[i2 + 2]);
                v3Set(p3, position[i3], position[i3 + 1], position[i3 + 2]);

                v3Sub(v32, p3, p2);
                v3Sub(v21, p1, p2);
                v3Cross(n, v32, v21);
                // Already be weighted by the triangle area
                for (let i = 0; i < 3; i++) {
                    normals[i1 + i] += n[i];
                    normals[i2 + i] += n[i];
                    normals[i3 + i] += n[i];
                }
                f += 3;
            }

            let i = 0;
            const l = normals.length;
            while (i < l) {
                v3Set(n, normals[i], normals[i + 1], normals[i + 2]);
                v3Normalize(n, n);
                normals[i] = n[0] || 0;
                normals[i + 1] = n[1] || 0;
                normals[i + 2] = n[2] || 0;
                i += 3;
            }

            return normals;
        }

WARNING

这几个参数要求maptalks.three的版本 >=0.37.3 ,这个功能是我刚加的

加载等高线数据

js
function addDemLines() {
            const colors = [
                [0, 'white'],
                [100, 'blue'],
                [300, 'yellow'],
                [400, 'red']
            ];
            const ci = new colorin.ColorIn(colors);
            const lineMaterialMap = {};
            function getLineMaterila(height) {
                const [r, g, b] = ci.getColor(height);
                const color = `rgb(${r},${g},${b})`;
                if (lineMaterialMap[color]) {
                    return lineMaterialMap[color];
                }
                lineMaterialMap[color] = new THREE.LineBasicMaterial({
                    linewidth: 1,
                    color,
                    opacity: 0.6,
                    transparent: true
                });
                return lineMaterialMap[color];
            }

            fetch('./../assets/data/hangzhoudem').then(res => res.text()).then(evadata => {
                evadata = LZString.decompressFromBase64(evadata);
                evadata = JSON.parse(evadata);
                lines = evadata.map(element => {
                    let coordinates = element.l;
                    if (coordinates) {
                        const elevation = element.h;
                        const height = elevation + 45;
                        coordinates.forEach(c => {
                            c[2] = height;
                        });
                        const line = threeLayer.toLine(new maptalks.LineString(coordinates), { altitude }, getLineMaterila(height));
                        return line;
                    }
                });
                threeLayer.addMesh(lines);
            });
        }

等高线的数据比较简单,这里我使用maptalks.three加载了

  • 数据里要携带海拔数据,这里我是手动赋值进去的
  • 根据不同的高度赋值了不同的颜色
  • 如果你使用LineStringLayer的话,你可以直接对LineColor进行颜色插值的
js
// LineStringLayer test
   function addDemLines1() {
            const colors = [
                [0, 'white'],
                [100, 'blue'],
                [300, 'yellow'],
                [400, 'red']
            ];

            fetch('./../assets/data/hangzhoudem').then(res => res.text()).then(evadata => {
                evadata = LZString.decompressFromBase64(evadata);
                evadata = JSON.parse(evadata);
                lines = evadata.map(element => {
                    let coordinates = element.l;
                    if (coordinates) {
                        const elevation = element.h;
                        const height = elevation + 45 + altitude;
                        coordinates.forEach(c => {
                            c[2] = height;
                        });
                        const line = new maptalks.LineString(coordinates, {
                            symbol: {
                                lineColor: {
                                    type: 'color-interpolate',
                                    property: 'height',
                                    stops: colors
                                }
                            },
                            properties: {
                                height
                            }
                        });
                        return line;
                    }
                });
                lineLayer.addGeometry(lines);
            });
        }

突显山顶的效果

为了突显山的效果,需要把开启ground

js
const sceneConfig = {
            postProcess: {
                enable: true,
                antialias: { enable: true }
            },
            ground: {
                enable: true,
                renderPlugin: {
                    type: "lit"
                },
                symbol: {
                    polygonOpacity: 1,
                    material: {
                        baseColorFactor: [0.48235, 0.48235, 0.48235, 1],
                        hsv: [0, 0, -0.532],
                        roughnessFactor: 0.22,
                        metallicFactor: 0.58
                    }
                }
            }
        };
        const groupLayer = new maptalks.GroupGLLayer('group', [threeLayer, lineLayer], { sceneConfig });
        groupLayer.addTo(map);
        layer.addTo(map);

为了不让低海拔的地形显示出来,需要把地形瓦片和等高线等数据向地下平移一定的距离,利用ground把低海拔的给遮住, 使只能看到一定高度的地形数据效果,这样可以是山体效果更加明显和突出

示例里把所有的图形都向下平移了90米

js
var lines = [];
        var terrains = [];
        const altitude = -90;
        const terrainColor = '#0a6142';

代码层面就是调整图层或者图形的海拔而已,

山顶上插几个旗子

这个比较简单,获取山顶上几个坐标和海拔值即可,然后用图层加载这个点的数据即可

js
function testPoints() {
            const coordinates = [
                [120.01456037029766, 30.165785577748437, 380],
                [120.02020512029901, 30.189324563181998, 380],
                [120.03532742521247, 30.20363465918578, 400],
                [120.00723025626826, 30.140722352969505, 330],
                [120.01522412598683, 30.107399355979993, 500]
            ];

            const points = coordinates.map(c => {
                return new maptalks.Marker(c, {
                    symbol: {
                        markerWidth: 35,
                        markerHeight: 35,
                        markerFile: './../assets/image/poi.png'
                    }
                })
            });
            layer.addGeometry(points);
        }

This document is generated by mdpress